Repository navigation
接入浏览器抓取 - #77
接入浏览器抓取#77
Conversation
There was a problem hiding this comment.
Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.
An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.
JunieXD
left a comment
There was a problem hiding this comment.
感谢!浏览器抓取难度不小,你做得很扎实。我们用 PR 里的 PageSession 原样跑了一遍电科计算机学院:列表页和你举例的那条通知都拿到了 200,附件是 581,642 字节的 PDF,和你写的一致。./check.sh 的 77 项、pnpm check 的 42 项也都通过了。你手动试了 45 个网站,验证码和错误页的情况都考虑到了,很用心。
主要问题有两个:
按下面改完,这个 PR 能小一大半。
方向:浏览器的一次页面访问算一个请求。打开页面前取得这个网站的许可,一直拿到页面关闭;页面里的脚本、样式这些子资源,照浏览器正常的方式加载。这和 #41 范围第 2 条一致。
必须改
-
把附件下载失败的上报拆出去。你想解决的是个真问题:现在一个附件下载不了,整条通知就交不上快照,正文也抽取不了,每一轮都会重抓。但怎么处理,要改接口、表和抓取频率,不在 #41 的范围里,按 AGENTS.md「先讨论再动手」要先商量。PR 里的做法也还有问题:重抓没有终止条件,一个要验证码的附件(比如你试过的杭州高研院),会让这条通知在项目截止以后也每天被抓一次。
请把下面这些全部删掉:
attachments.py整个文件:根据 HTML 判断错误页、把源站的提示存成原因。Attachment放回rounds.py,data还是bytes;- 快照接口的
error、上传接口的url参数、attachments.error列,以及「最新快照有附件失败,就每天重抓」; - 连带的
ingest改动、迁移、openapi/crawler.json、contract.py、db/types.ts,和 api.md、data-model.md、design.md §7.4 里的相关文字。
带 Cookie 下载附件的
PageSession.download留着,这是 #41 要的。这个 PR 里附件下载不成,照开发约定 §2 抛FetchError或StepError,记进抓取记录。附件失败怎么处理,欢迎另开 Issue,把你的方案和试过的例子写进去,我们在那里商量。 -
浏览器页面用 Playwright 自己的接口,不走 CDP。
browser.py里这些都可以不要:Fetch.enable拦截;_Paused、_Frames、_Version这些 CDP 模型;_generation一类的导航状态;_source_error按错误文案的分类。
照上面的方向,
get大致是:- 取得页面名额和网站许可;
new_page(),再goto(url, wait_until="networkidle");- 读
page.content(); - 关掉页面,放回许可。
瑞数这类挑战会自己刷新页面,等到网络空闲时,就已经是最终页面了。我们审 #66 时用这个办法打开过 15 个挑战页,网址见 #41 的评论。状态码用
page.on("response")记下主框架最后一次导航的响应,还是 202 或 412 就当失败。另外:
- 跨网站跳转沿用起始网站的许可,不另外排队。
- UA 开一个空白页读
navigator.userAgent就行。 - Playwright 的错误统一转成
FetchError:超时是timeout,其余是unreachable,不按文案细分。
-
其他防御性的代码一起删:
- 新建 context 和页面时套了
create_task、asyncio.wait,防的是取消(browser.py:161、:238)。一轮结束时整个 context 都会关掉,里面的页面跟着关,不用逐个回收。 PageSession._closed和Permit._released的检查也删掉:关闭以后再用、重复释放,都不会发生。- 弹窗:在
context.on("page")里,有 opener 的直接关掉,不用另存任务再gather。 - 浏览器来源在进入
PageSession时就开好会话,不用_lock懒加载:每一轮的第一步就要用它。
- 新建 context 和页面时套了
-
测试跟着精简。
browser_test.py的 1183 行,大多是用假的 CDP 事件,测上面要删掉的状态机和取消回收。改完以后,用很薄的假 Page 和 Context,测下面这几件事就够了:- 同一网站的页面访问和普通请求互相等,间隔 5 秒;
- 同时最多 2 个页面;
- 一轮一个会话,结束后关掉;
- 附件下载用的是这一轮的 context。
-
文档写规矩,不写实现细节。
- development.md §2 新加的几段写的是实现,删掉:30 秒和 300 秒的期限、
DOMContentLoaded、许可的释放顺序。 - 浏览器的安装命令保留。
- 原来「浏览器抓取打开页面前,也要先过这些规矩」那句,改成「一次页面访问取一次许可,拿到页面关闭」。
- design.md §7.3 改回原来的话,在后面补半句「浏览器的一次页面访问算一个请求」就够了。
- development.md §2 新加的几段写的是实现,删掉:30 秒和 300 秒的期限、
以后提 PR 前请先自查
这次要改的几处都不是小问题,而且 AGENTS.md 里都写明了:
- 一个 PR 只做 Issue 里商定的事,影响接口和抓取频率的先讨论(「只做要求的事」「先讨论再动手」);
- 用库自带的接口,不为不会发生的情况写防御和回收(「用现成的」「只在边界校验」);
- 文档写规矩,不写实现细节(「写文档」)。
如果代码是用 Codex 这类 agent 写的,转为待审前,请让它对照 AGENTS.md 和 Issue 的范围逐条自查几遍;你自己也把 diff 从头读一遍,看每一处是不是 Issue 要的、能不能写得更短更直接。这样能省下好几轮来回,审查也快得多。
说明
JunieXD
left a comment
There was a problem hiding this comment.
感谢!这一版照上次的意见改得很彻底:附件失败的上报整个拆掉了,浏览器部分换成了 Playwright 自己的接口,这次的提交删掉了 2400 多行,读起来清楚多了。
我们是这样核对的:
- 本地跑了
./check.sh,37 项都通过; - 用 PR 里的代码原样打开了北邮计算机学院和电科计算机学院的列表页,两个都过了瑞数的挑战、拿到 200,分别用了 5.1 秒和 2.3 秒;拦掉图片和字体不影响挑战。
必须改
fetch.py和browser.py互相引用:browser.py从fetch.py导入FetchError、Response等,fetch.py又在session()里导入Browser,类型靠TYPE_CHECKING导入(fetch.py:13-14、:79)。这是在绕开循环导入,而 AGENTS.md「按领域组织」要求依赖单向、不循环。请改成只有browser.py依赖fetch.py:fetch.py回到只管 HTTP 请求,保留新加的permit,不再知道浏览器;PageSession和按来源开会话挪进browser.py。比如Browser(fetcher)持有共享的浏览器,browser.session(source)给出这一轮的PageSession:浏览器来源用这一轮的 context,其他来源直接用fetcher.get;crawl_source改收Browser,不再收Fetcher,一轮在async with browser.session(source)里跑完。
- 测试精简到上次说的那几件:
- 删掉这 4 个:
browser_test.py里测错误转换和初始化失败的两个(:320、:354),以及fetch_test.py新加的取消、失败后放回许可的两个(:91、:122)。它们测的是try/finally本身,或者是上次说过不用管的取消。 - 留下这 2 个:测主框架最后一次导航的(
:273)和挑战没过就失败的(:299)。前一个保证跨网站跳转沿用起始网站的许可,后一个保证瑞数没过时不会被当成抓取成功。
- 删掉这 4 个:
建议一起改
configure()合进Browser.session(),去掉包着它的try(browser.py:79-83):注册路由和事件不会单独出错;浏览器出了问题,外层已经转成FetchError。- development.md 只改上次说的那一句:「所有对来源网站的请求都经过……」和「一轮里同一个网站共用一个无痕会话……」这两条原来的写法没问题,请改回原文。只把「浏览器抓取打开页面前,也要先过这些规矩」换成「浏览器一次页面访问取一次许可,持有到页面关闭;页面的子资源正常加载,跨网站跳转沿用起始网站的许可」。
说明
cc7f531 to
8858d1a
Compare
JunieXD
left a comment
There was a problem hiding this comment.
感谢!这一版把上次的意见都改到位了:fetch.py 和 browser.py 只剩 browser → fetch 一个方向;Browser(fetcher)、session(source) 的形状,crawl_source 和 list_items 的参数都和我们说的一样;测试精简到了约定的 6 个,开发约定那句话也一字不差。
我们是这样核对的:
- 本地跑了
./check.sh,198 项测试都通过; - 用 PR 里的代码原样抓了电科计算机学院:列表页和通知页都是 200,分别用了 3.9 秒和 6.5 秒;同一会话下载的附件 581,642 字节,是有效的 PDF。
还剩一处,改完就能合并。
必须改
-
Browser进入时就启动 Chromium,去掉懒加载:现在第一次用到浏览器来源时才启动(_start加_lock,browser.py:74-91)。可浏览器起来以后一直开到进程退出,来源清单的 45 个里又有 15 个要用浏览器,每次领活都会用到,懒加载只是把启动推迟了一会儿。第一轮说 context 不用_lock懒加载,也是这个道理。另外,浏览器起不来(比如没装 Chromium)是抓取节点自己的问题,现在会被session()转成FetchError("unreachable", source.url),记成这个学院的网站打不开。请改成:
__aenter__里启动 Chromium、读 UA,失败就直接抛出,抓取节点一启动就能发现;删掉_start和_lock。session()去掉包着整段的try/except:页面和附件的错误已经在BrowserSession里转成FetchError了;新建 context 出错是 bug,照常抛出(开发约定 §2「别的异常是 bug,照常抛出」)。crawl_source直接用async with browser.session(source) as pages:,去掉AsyncExitStack,try还是只包读列表的那几行。- 删掉
rounds_test.py的test_a_session_that_cant_start_is_reported。rounds_test.py里剩下的都是普通来源,用不到 Chromium,可以不进入Browser,免得 CI 要装浏览器;browser_test.py的假浏览器改成替换async_playwright。
建议一起改
- 日志照
rounds.py的写法:browser.py:44的_logger改叫logger,网址、状态码和耗时放进extra,不拼在文字里(browser.py:149-154)。
说明
- 改完在这里说一声,或者转回待审,我们看完这一处就合并。推了新提交以后我们再批准 CI。
JunieXD
left a comment
There was a problem hiding this comment.
这一处改到位了:Browser 进入时就启动 Chromium,起不来直接报错;session() 去掉了包着整段的 try,crawl_source 也不再用 AsyncExitStack;日志的字段放进了 extra。
我们本地跑了 ./check.sh,197 项都通过;用真实的 Chromium 进入 Browser,UA 也是对的。感谢这几轮耐心的修改,合并了!
关联
Closes #41
改了什么
需要执行脚本才能读到内容的来源,使用 Playwright Chromium 无头精简版抓取。每轮使用独立的无痕会话,列表、正文和附件下载共用 Cookie。
Browser进入时启动 Chromium、读取 UA,启动和新建会话失败直接抛出;页面和附件访问失败记入抓取记录。Fetcher管理 HTTP 请求和网站许可。一次页面访问持有许可到页面关闭,同时最多打开 2 个页面;跨网站跳转沿用起始网站的许可。列表解析已接入PageSession,正文和附件文字解析仍由 #40 实现。怎么验证的
crawler/./check.sh通过:格式、lint、严格类型检查、生成合约一致性和 197 项测试。将浏览器路径指向未安装目录,确认测试不依赖 Chromium。web/pnpm check通过:类型检查、lint、格式检查和 60 项测试。影响
crawl_source接收共享的Browser,列表和正文步骤接收本轮的PageSession。