main.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  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. # 额外 Chrome 启动参数(生产环境 Linux 可通过 PLAYWRIGHT_EXTRA_ARGS 注入)
  98. extra_args_env = os.environ.get("PLAYWRIGHT_EXTRA_ARGS", "")
  99. extra_args = [a.strip() for a in extra_args_env.split(",") if a.strip()]
  100. if extra_args:
  101. launch_kwargs["args"] = extra_args
  102. browser = p.chromium.launch(**launch_kwargs)
  103. page = browser.new_context().new_page()
  104. # 拦截 API 响应
  105. def on_response(resp):
  106. try:
  107. if "application/json" not in resp.headers.get("content-type", ""):
  108. return
  109. if not INFO_API_RE.search(resp.url):
  110. return
  111. try:
  112. api_data.append(resp.json())
  113. except Exception:
  114. pass
  115. except Exception:
  116. pass
  117. page.on("response", on_response)
  118. if not _navigate(page, url, timeout):
  119. result["error"] = "导航失败"
  120. browser.close()
  121. else:
  122. _wait_for_content(page)
  123. # 从 API 找模型对象
  124. model_obj = None
  125. for body in api_data:
  126. found = _find_model_in_json(body, target)
  127. if found:
  128. model_obj = found
  129. print(f"[INFO] API 找到模型: {found.get('model', found.get('name', target))}")
  130. break
  131. if not model_obj:
  132. print(f"[WARN] 未从 API 找到模型 '{target}',部分字段将为空")
  133. # ── info 模块 ──
  134. if "info" in shared_modules:
  135. if model_obj:
  136. result["info"] = parse_model_info(model_obj)
  137. else:
  138. result["info"] = {"error": f"未找到模型 '{target}'"}
  139. # ── rate 模块 ──
  140. if "rate" in shared_modules:
  141. rate_text = _get_rate_limit_section_text(page)
  142. result["rate_limits"] = parse_rate_limits_from_text(rate_text) if rate_text else {}
  143. # ── tool 模块 ──
  144. if "tool" in shared_modules:
  145. html = page.content()
  146. tool_text = _get_tool_price_section_text(html)
  147. result["tool_call_prices"] = parse_tool_prices_from_text(tool_text) if tool_text else []
  148. browser.close()
  149. # ── price 模块(原始脚本,独立浏览器) ──────────────────────────────────────
  150. if "price" in modules:
  151. print(f"[INFO] 运行价格模块...")
  152. price_result = scrape_model_price(
  153. url,
  154. headless=headless,
  155. timeout=timeout,
  156. executable_path=executable_path,
  157. )
  158. result["prices"] = price_result.get("prices", {})
  159. if price_result.get("error"):
  160. result["price_error"] = price_result["error"]
  161. return result
  162. def main():
  163. ap = argparse.ArgumentParser(
  164. description="阿里云百炼模型完整信息抓取(整合所有模块)",
  165. formatter_class=argparse.RawDescriptionHelpFormatter,
  166. epilog="""
  167. 模块说明:
  168. info - 模型基本信息、能力、模态
  169. rate - 限流与上下文(RPM、context window 等)
  170. tool - 工具调用价格
  171. price - 模型 token 价格(含阶梯计费)
  172. 示例:
  173. python main.py --url "https://..." --browser-path "D:\\chrome.exe"
  174. python main.py --file urls.txt --headful
  175. python main.py --url "https://..." --modules info,rate
  176. """,
  177. )
  178. group = ap.add_mutually_exclusive_group(required=True)
  179. group.add_argument("--url", help="单个模型页面 URL")
  180. group.add_argument("--file", help="URL 列表文件(每行一个)")
  181. ap.add_argument("--headful", action="store_true", help="有头模式(方便调试)")
  182. ap.add_argument("--timeout", type=int, default=20000, help="导航超时毫秒,默认 20000")
  183. ap.add_argument("--browser-path", help="浏览器可执行文件路径")
  184. ap.add_argument(
  185. "--modules",
  186. default="info,rate,tool,price",
  187. help="要运行的模块,逗号分隔,可选: info,rate,tool,price(默认全部)",
  188. )
  189. ap.add_argument("--output-dir", default="output", help="结果保存目录,默认 output/")
  190. args = ap.parse_args()
  191. urls: List[str] = []
  192. if args.url:
  193. urls = [args.url]
  194. else:
  195. with open(args.file, "r", encoding="utf-8") as f:
  196. urls = [ln.strip() for ln in f if ln.strip()]
  197. exec_path = args.browser_path or os.environ.get("PLAYWRIGHT_EXECUTABLE")
  198. headless = not args.headful
  199. if os.environ.get("PLAYWRIGHT_HEADLESS", "").lower() == "false":
  200. headless = False
  201. modules = [m.strip() for m in args.modules.split(",") if m.strip()]
  202. print(f"[INFO] 运行模块: {modules}")
  203. os.makedirs(args.output_dir, exist_ok=True)
  204. all_results = []
  205. for u in urls:
  206. print(f"\n{'='*60}\n[INFO] 抓取: {u}", flush=True)
  207. res = scrape_all(u, headless=headless, timeout=args.timeout,
  208. executable_path=exec_path, modules=modules)
  209. all_results.append(res)
  210. # 保存单个结果
  211. model_id = res.get("model_id", "unknown")
  212. safe_id = re.sub(r"[^\w\-.]", "_", model_id)
  213. out_path = os.path.join(args.output_dir, f"{safe_id}.json")
  214. with open(out_path, "w", encoding="utf-8") as f:
  215. json.dump(res, f, ensure_ascii=False, indent=2)
  216. print(f"[INFO] 已保存: {out_path}")
  217. # 输出到 stdout
  218. print(json.dumps(all_results, ensure_ascii=False, indent=2))
  219. if __name__ == "__main__":
  220. main()