main.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  1. #!/usr/bin/env python3
  2. """
  3. main.py - 阿里云百炼模型完整信息抓取入口
  4. 整合以下模块,对每个 URL 只打开一次浏览器,依次运行所有抓取逻辑:
  5. - scrape_aliyun_models.py → 模型价格(含阶梯计费)
  6. - scrape_model_info.py → 模型基本信息 + 能力
  7. - scrape_rate_limits.py → 限流与上下文
  8. - scrape_tool_prices.py → 工具调用价格
  9. 用法:
  10. python main.py --url "https://bailian.console.aliyun.com/...#/model-market/detail/qwen3-max"
  11. python main.py --file urls.txt
  12. python main.py --url "..." --browser-path "D:\\playwright-browsers\\...\\chrome.exe"
  13. python main.py --url "..." --modules info,price,rate,tool # 只运行指定模块
  14. python main.py --url "..." --headful # 有头模式调试
  15. 输出: JSON 到 stdout,同时保存到 output/<model_id>.json
  16. """
  17. import argparse
  18. import json
  19. import os
  20. import re
  21. import time
  22. from typing import Dict, List, Optional
  23. from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError
  24. # 导入各模块的核心解析函数(不启动独立浏览器)
  25. from scrape_model_info import (
  26. _extract_model_id_from_url,
  27. _find_model_in_json,
  28. parse_model_info,
  29. API_URL_RE as INFO_API_RE,
  30. )
  31. from scrape_rate_limits import (
  32. parse_rate_limits_from_text,
  33. _get_rate_limit_section_text,
  34. )
  35. from scrape_tool_prices import (
  36. parse_tool_prices_from_text,
  37. _get_tool_price_section_text,
  38. )
  39. from scrape_aliyun_models import (
  40. scrape_model_price,
  41. )
  42. def _navigate(page, url: str, timeout: int) -> bool:
  43. """导航到 URL,返回是否成功。"""
  44. try:
  45. page.goto(url, wait_until="networkidle", timeout=timeout)
  46. return True
  47. except PlaywrightTimeoutError:
  48. try:
  49. page.goto(url, wait_until="load", timeout=timeout)
  50. return True
  51. except Exception as e:
  52. print(f"[ERROR] 导航失败: {e}")
  53. return False
  54. def _wait_for_content(page) -> None:
  55. """等待页面核心内容渲染完成。"""
  56. for sel in ["text=模型价格", "text=模型介绍", "text=模型能力"]:
  57. try:
  58. page.wait_for_selector(sel, timeout=6000)
  59. break
  60. except PlaywrightTimeoutError:
  61. pass
  62. time.sleep(1.5)
  63. # 滚动触发懒加载
  64. try:
  65. page.evaluate("window.scrollTo(0, document.body.scrollHeight)")
  66. time.sleep(0.8)
  67. page.evaluate("window.scrollTo(0, 0)")
  68. time.sleep(0.3)
  69. except Exception:
  70. pass
  71. def scrape_all(
  72. url: str,
  73. headless: bool = True,
  74. timeout: int = 20000,
  75. executable_path: Optional[str] = None,
  76. modules: Optional[List[str]] = None,
  77. ) -> Dict:
  78. """
  79. 对单个 URL 运行所有(或指定)模块,共享一个浏览器实例。
  80. modules 可选值: ["info", "rate", "tool", "price"]
  81. 默认全部运行。
  82. """
  83. if modules is None:
  84. modules = ["info", "rate", "tool", "price"]
  85. target = _extract_model_id_from_url(url)
  86. result: Dict = {"url": url, "model_id": target, "error": None}
  87. # price 模块复用原始脚本,独立启动浏览器(原脚本结构限制)
  88. # 其余模块共享一个浏览器实例
  89. shared_modules = [m for m in modules if m != "price"]
  90. # ── 共享浏览器:info / rate / tool ──────────────────────────────────────────
  91. if shared_modules:
  92. api_data: List[Dict] = []
  93. with sync_playwright() as p:
  94. launch_kwargs: Dict = {"headless": headless}
  95. if executable_path:
  96. launch_kwargs["executable_path"] = executable_path
  97. browser = p.chromium.launch(**launch_kwargs)
  98. page = browser.new_context().new_page()
  99. # 拦截 API 响应
  100. def on_response(resp):
  101. try:
  102. if "application/json" not in resp.headers.get("content-type", ""):
  103. return
  104. if not INFO_API_RE.search(resp.url):
  105. return
  106. try:
  107. api_data.append(resp.json())
  108. except Exception:
  109. pass
  110. except Exception:
  111. pass
  112. page.on("response", on_response)
  113. if not _navigate(page, url, timeout):
  114. result["error"] = "导航失败"
  115. browser.close()
  116. else:
  117. _wait_for_content(page)
  118. # 从 API 找模型对象
  119. model_obj = None
  120. for body in api_data:
  121. found = _find_model_in_json(body, target)
  122. if found:
  123. model_obj = found
  124. print(f"[INFO] API 找到模型: {found.get('model', found.get('name', target))}")
  125. break
  126. if not model_obj:
  127. print(f"[WARN] 未从 API 找到模型 '{target}',部分字段将为空")
  128. # ── info 模块 ──
  129. if "info" in shared_modules:
  130. if model_obj:
  131. result["info"] = parse_model_info(model_obj)
  132. else:
  133. result["info"] = {"error": f"未找到模型 '{target}'"}
  134. # ── rate 模块 ──
  135. if "rate" in shared_modules:
  136. rate_text = _get_rate_limit_section_text(page)
  137. result["rate_limits"] = parse_rate_limits_from_text(rate_text) if rate_text else {}
  138. # ── tool 模块 ──
  139. if "tool" in shared_modules:
  140. html = page.content()
  141. tool_text = _get_tool_price_section_text(html)
  142. result["tool_call_prices"] = parse_tool_prices_from_text(tool_text) if tool_text else []
  143. browser.close()
  144. # ── price 模块(原始脚本,独立浏览器) ──────────────────────────────────────
  145. if "price" in modules:
  146. print(f"[INFO] 运行价格模块...")
  147. price_result = scrape_model_price(
  148. url,
  149. headless=headless,
  150. timeout=timeout,
  151. executable_path=executable_path,
  152. )
  153. result["prices"] = price_result.get("prices", {})
  154. if price_result.get("error"):
  155. result["price_error"] = price_result["error"]
  156. return result
  157. def main():
  158. ap = argparse.ArgumentParser(
  159. description="阿里云百炼模型完整信息抓取(整合所有模块)",
  160. formatter_class=argparse.RawDescriptionHelpFormatter,
  161. epilog="""
  162. 模块说明:
  163. info - 模型基本信息、能力、模态
  164. rate - 限流与上下文(RPM、context window 等)
  165. tool - 工具调用价格
  166. price - 模型 token 价格(含阶梯计费)
  167. 示例:
  168. python main.py --url "https://..." --browser-path "D:\\chrome.exe"
  169. python main.py --file urls.txt --headful
  170. python main.py --url "https://..." --modules info,rate
  171. """,
  172. )
  173. group = ap.add_mutually_exclusive_group(required=True)
  174. group.add_argument("--url", help="单个模型页面 URL")
  175. group.add_argument("--file", help="URL 列表文件(每行一个)")
  176. ap.add_argument("--headful", action="store_true", help="有头模式(方便调试)")
  177. ap.add_argument("--timeout", type=int, default=20000, help="导航超时毫秒,默认 20000")
  178. ap.add_argument("--browser-path", help="浏览器可执行文件路径")
  179. ap.add_argument(
  180. "--modules",
  181. default="info,rate,tool,price",
  182. help="要运行的模块,逗号分隔,可选: info,rate,tool,price(默认全部)",
  183. )
  184. ap.add_argument("--output-dir", default="output", help="结果保存目录,默认 output/")
  185. args = ap.parse_args()
  186. urls: List[str] = []
  187. if args.url:
  188. urls = [args.url]
  189. else:
  190. with open(args.file, "r", encoding="utf-8") as f:
  191. urls = [ln.strip() for ln in f if ln.strip()]
  192. exec_path = args.browser_path or os.environ.get("PLAYWRIGHT_EXECUTABLE")
  193. headless = not args.headful
  194. if os.environ.get("PLAYWRIGHT_HEADLESS", "").lower() == "false":
  195. headless = False
  196. modules = [m.strip() for m in args.modules.split(",") if m.strip()]
  197. print(f"[INFO] 运行模块: {modules}")
  198. os.makedirs(args.output_dir, exist_ok=True)
  199. all_results = []
  200. for u in urls:
  201. print(f"\n{'='*60}\n[INFO] 抓取: {u}", flush=True)
  202. res = scrape_all(u, headless=headless, timeout=args.timeout,
  203. executable_path=exec_path, modules=modules)
  204. all_results.append(res)
  205. # 保存单个结果
  206. model_id = res.get("model_id", "unknown")
  207. safe_id = re.sub(r"[^\w\-.]", "_", model_id)
  208. out_path = os.path.join(args.output_dir, f"{safe_id}.json")
  209. with open(out_path, "w", encoding="utf-8") as f:
  210. json.dump(res, f, ensure_ascii=False, indent=2)
  211. print(f"[INFO] 已保存: {out_path}")
  212. # 输出到 stdout
  213. print(json.dumps(all_results, ensure_ascii=False, indent=2))
  214. if __name__ == "__main__":
  215. main()