ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

网络爬虫与 CDP 实战(一):网页数据到底在哪?从 HTTP 到分页采集

网络爬虫与 CDP 实战(一):网页数据到底在哪?从 HTTP 到分页采集 网络爬虫与 CDP 实战拆解网页数据的 20 个实验配套实验网站网页数据实验场。网站只提供实验页面教程与分析均在本系列博文中。浏览器能看到商品Python 为什么只拿到空壳接口已经找对为什么复制请求还是失败价格一直变化为什么 Network 里没有新的普通请求这个系列从这些具体问题出发带你沿着 HTTP、身份状态、浏览器内部、参数计算和实时推送一步步拆解网页数据的来路。CDP 是 Chrome DevTools Protocol即 Chrome 开发者工具协议你不需要预先熟悉它第一篇会先建立背景。五篇文章围绕同一批 24 件商品展开。每篇保留实验主线并加入原理、失败对照、教学业务案例和工程取舍。文章默认使用公开配套实验站也可以通过CDP_BASE_URL切换到本地实例分析方法可以迁移凭证与字段契约需要按实际网站重新确认。适合怎样的读者如果你会写基础 Python但遇到真实网页时还不清楚该解析 HTML、请求接口还是启动浏览器可以从第一篇开始。如果已有采集脚本却常被过期状态、等待超时和数据缺失困住可以按表中的问题进入对应篇章再回看前置知识。读完后应能够解释一条采集链路目标字段在哪里身份从哪里来参数怎样生成完成条件是什么结果怎样对账。这些判断比背下一段固定爬虫代码更容易用于下一次任务。网络爬虫与 CDP 实战一网页数据到底在哪从 HTTP 到分页采集配套实验网站实验 01静态商品目录 · 实验 02GET 商品接口 · 实验 03商品筛选与分页 · 实验 04POST 目录搜索浏览器里有一张完整的商品表Python 请求页面却得到空列表。很多爬虫项目就是从这个困惑开始的。接着有人加等待有人换浏览器有人开始复制一大串请求头代码越来越多却没回答最初的问题网页上的这些数据是从哪里来的如果把这个问题查清后面的工作通常会简单很多。商品可能已经写在 HTML 里也可能由接口送来还可能是浏览器运行 JavaScript 后计算出来的。不同位置的数据需要不同工具。《网络爬虫与 CDP 实战拆解网页数据的 20 个实验》沿着这条线展开。第一篇先讲四个基础实验静态 HTML、GET API、查询参数与分页、POST JSON。后面依次加入登录状态、CDP、动态参数与签名最后分析 WebSocket 和综合门户。你不需要一开始就记住协议命令但需要逐渐学会看到一个页面时先找证据再选实现方式。跟着文章动手在线实验站为了让你能边读边验证我为这个系列搭建了一个真实可访问的配套网站——网页数据实验场。打开首页就能看到按五篇文章分组的 20 个实验入口。本篇使用实验 01—04分别对应静态商品目录、GET 接口、筛选分页和 POST 搜索你可以直接打开这些页面用 F12 和下面的 Python 代码采集同一批商品无需先搭建示例服务器。网站使用固定的 24 件虚拟商品页面负责提供业务操作操作步骤和分析方法在本文中。下面的请求代码默认使用这个公开站点如有自己的本地实例可以设置CDP_BASE_URL切换地址。1. CDP 是什么为什么网络爬虫会用到它CDP 是Chrome DevTools Protocol即 Chrome 开发者工具协议。它允许外部工具与 Chromium 系浏览器通信查看页面、调试脚本、观察网络活动等。Chrome DevTools 自身也使用它。协议按 Network、DOM、Debugger 等域组织每个域提供相关命令和事件。[1]你可以把它放进下面这张关系图里理解网站服务器 ──HTTP 响应── 浏览器 ──解析 HTML / 运行 JS── 屏幕上的页面 ↑ DevTools 或 CDP 客户端 查看浏览器内部发生了什么注意图中两条不同的链路。HTTP 负责网站与客户端之间的请求响应CDP 负责工具与浏览器之间的调试通信。用 Python 直接请求 API不必经过 CDP如果需要浏览器的执行上下文、动态 DOM 或协议事件CDP 才提供额外能力。这解释了为什么一个以 CDP 为主题的系列第一篇要从 HTTP 开始。先把简单路径用明白才能判断浏览器能力解决了哪个具体问题。否则“用了浏览器”很容易变成一个习惯而不是有依据的选择。2. 先把网页拆成三样东西想象一个有 24 件商品的目录站。屏幕上显示名称、价格和库存底部有翻页按钮。我们把它拆成三样东西服务器返回的文本、浏览器里的结构、提供业务数据的请求。HTML是服务器返回的页面文本描述标题、表格、按钮等结构。浏览器的 View Source 显示一次页面源码请求得到的文本若要核对刚才那次导航的响应Network 中 document 请求的 Response 更直接。两次请求之间网站状态可能变化因此不要把它们无条件视为同一份快照。DOM是浏览器解析 HTML 后维护的节点树。JavaScript 可以继续往树里插入商品卡片所以 Elements 面板里的内容可能比原始 HTML 多。BeautifulSoup 只解析你交给它的文本不会替你执行页面脚本。API 响应是另一条可能提供商品的路径。页面可以先收到一个空表格再通过 JavaScript 请求 JSON把数据填进去。API 是应用程序接口JSON 是它常用的数据表达格式。三者有联系却不能混用证据。看到 Elements 里有价格只能证明浏览器当前有这个节点不能证明价格就在最初的 HTML 响应里。后面第三篇还会增加 JavaScript 内存这一层。3. 实验一HTML 已经包含数据时解析文本就够了动手入口打开静态商品目录实验页。打开页面后先看商品表再用“查看网页源代码”或 Network 的 document 响应查找商品行。下面的解析代码就可以请求这个在线页面。先在原始 HTML 中搜索一个完整商品名。如果找得到再核对名称、价格和库存是否都出现。如果三个字段都在同一行普通 HTTP 客户端通常就能完成这一步采集。例如响应中包含tableidproduct-listtrclassproductdata-product-id1tdclassproduct-nameKeyboard A/tdtdclassproduct-price¥899.00/tdtdclassproduct-stock12/td/tr/table选择器#product-list tr.product表示“在 id 为 product-list 的区域中找 class 含 product 的 tr”。这里需要的是能解释选择器与字段对应关系不必先掌握整套 CSS。下面的解析函数可以直接用这段 HTML 试验不依赖浏览器fromdecimalimportDecimalfrombs4importBeautifulSoupdefparse_products(html:str)-list[dict]:soupBeautifulSoup(html,html.parser)products[]forrowinsoup.select(#product-list tr.product):namerow.select_one(.product-name)pricerow.select_one(.product-price)stockrow.select_one(.product-stock)ifany(nodeisNonefornodein(name,price,stock)):raiseValueError(商品行缺少必要字段需要检查页面结构)products.append({id:int(row[data-product-id]),name:name.get_text(stripTrue),price:Decimal(price.get_text(stripTrue).removeprefix(¥)),stock:int(stock.get_text(stripTrue)),})returnproducts把实际响应交给它时先检查请求是否成功importosimporthttpxwithhttpx.Client(timeout10.0,trust_envFalse)asclient:responseclient.get(os.environ.get(CDP_BASE_URL,https://web-data-lab.pages.dev).rstrip(/)/labs/static-html/)response.raise_for_status()productsparse_products(response.text)iflen(products)!24:raiseValueError(f示例应有 24 条实际只有{len(products)}条)Decimal是这里的一处工程补充。金额若要参与对账保留十进制精度比立刻转为二进制浮点数更合适。HTML 中的货币符号、千位分隔符和空白还可能因地区而变化正式采集应把原始价格文本一并保存方便回看解析规则。[2]另一个容易忽略的细节是空列表。页面改版后BeautifulSoup 仍可能正常运行只是一个节点也找不到。这样的“成功”会把空数据悄悄送进下游。把预期字段、数量范围、唯一 ID 写成检查才能及时识别结构变化。4. 实验二HTML 是空壳时去找真正的商品响应动手入口打开GET 商品接口实验页。在打开页面前启用 Network随后刷新页面比较 document 响应与商品 JSON 请求找到表格数据实际来自哪条响应。现在换一个页面原始 HTML 只有div idproducts/div屏幕上却有完整商品。这时继续修改 HTML 选择器没有意义需要观察页面加载之后发了哪些请求。在 DevTools 打开 Network再刷新页面。先用 Fetch/XHR 过滤页面脚本的取数请求选一条请求看 Response 是否含有某件商品的完整字段。如果没有继续看其他请求不要仅凭接口名称判断。Chrome 的 Network 面板可分别查看请求头、Payload、响应内容与发起者。[3]假设真正的数据响应是{count:24,results:[{id:1,name:Keyboard A,price:899.00,stock:12}]}这时应记录一份最小请求说明而不是立刻把浏览器的全部内容复制进程序项目要记录什么为什么重要Method 与 URLGET以及完整路径同一路径可能支持不同方法查询参数关键词、页码、排序决定查询的数据范围响应结构商品数组在哪个字段防止把公告、配置误当商品状态与 Content-Type200、JSON判断实际收到的内容必要凭证当前是否需要登录决定后面能否直接复现有了这份说明Python 的请求才有明确目标importoswithhttpx.Client(base_urlos.environ.get(CDP_BASE_URL,https://web-data-lab.pages.dev),timeout10.0,trust_envFalse)asclient:responseclient.get(/labs/api-basic/products/)response.raise_for_status()dataresponse.json()foritemindata[results]:print(item[name],item[price],item[stock])Copy as cURL可以作为请求样本但它保存的是某一次请求。里面可能混有过期凭证、与业务无关的浏览器头或一次性参数。后面两篇会解释这些字段的寿命此刻至少要知道“复制成功”和“长期复现”是不同验收标准。[3]5. 延伸JavaScript 页面不一定都要浏览器爬“前端用了 JavaScript”很容易被理解成“必须用浏览器”。这两个判断之间还缺一步JavaScript 用来计算数据还是只负责展示接口已经返回的数据如果 JSON 已包含你要的名称、价格和库存浏览器只是把它画成卡片Python 直接调用这个接口通常更简单。它不用等待样式、图片与页面交互也不会因为按钮文本变了而失效。还有混合情况。服务端先渲染商品再由前端接管交互或者 HTML 里嵌了一段初始化 JSON。此时页面既不是纯静态也不是纯客户端取数。分析时搜索目标字段而不是先给技术栈贴标签。需要的字段在哪一份响应里完整出现往往比它用了哪个前端框架更有决定性。类似地GraphQL 请求也没有改变核心方法关注发送的查询、变量与返回字段仅凭 URL 可能分不出不同业务请求。你掌握的是请求与数据的关系不是某一种 URL 长相。6. 实验三分页是数据完整性问题动手入口打开商品筛选与分页实验页。试着筛选商品并翻页观察 URL 查询参数如何变化再按下面的方法逐页采集并核对是否取得全部 24 件商品。第一条 API 请求成功后你可能发现只有 6 条商品。响应写着count24页面也有下一页。这时真正的问题从“能拿到吗”变成“拿全了吗”。示例使用页码分页page是页码page_size是每页条数count是匹配总数total_pages是总页数。查询字符串中的中文关键词交给params编码即可避免自己拼百分号。HTTPX 文档介绍了参数、JSON 请求体和响应解析的对应写法。[4]defcollect_pages(client:httpx.Client,keyword:str)-list[dict]:items[]seenset()expected_countNonepage1whileTrue:responseclient.get(/labs/pagination/products/,params{q:keyword,page:page,page_size:6,})response.raise_for_status()dataresponse.json()ifexpected_countisNone:expected_countdata[count]elifexpected_count!data[count]:raiseValueError(翻页期间总数变化这次结果不能当作稳定快照)foritemindata[results]:ifitem[id]inseen:raiseValueError(f商品{item[id]}在多页重复出现)seen.add(item[id])items.append(item)ifdata[total_pages]0orpagedata[total_pages]:breakpage1iflen(items)!expected_count:raiseValueError(f应有{expected_count}条实际{len(items)}条)returnitems这个函数针对静态演示数据故意采用严格检查。实时网站的总数可能正常变化届时不应简单把它定性为程序错误而要重新定义采集口径抓某个固定时间的快照还是收集一段时间内出现过的所有记录如果列表按最新更新时间排序采集第二页前又插入一条记录原来的边界会移动第一页末尾的记录可能再次出现在第二页。去重能处理重复却不能证明没有遗漏。若接口支持固定排序、截止时间或服务端快照应优先使用如果只能滚动翻页就把这种边界写进结果说明。从页码延伸到游标分页有些接口不返回total_pages而是返回next_cursor或has_more。游标通常代表服务端认可的下一段边界不应把它猜成页码。每次把响应给出的游标原样传回并记录已经使用过的游标防止重复游标造成死循环。页码分页的完成证据可以是总数对账游标分页则需要沿链走到明确的终止标志并检查唯一 ID。无限滚动只是页面交互形式底层仍可能是这两种分页协议之一。先找滚动触发的请求通常比自动滚到页面底部更容易解释完整性。7. 实验四POST JSON变的是请求结构动手入口打开POST 目录搜索实验页。在页面上输入关键词并提交搜索然后查看请求方法和 Payload。将这次操作与上一节的 GET 查询对照再用 Python 发送同样的 JSON 请求体。搜索条件多起来后接口可能使用 POST把关键词、分类和排序放进请求体。Network 的 Payload 展示这些内容Content-Type: application/json告诉服务端正文应按 JSON 解释。[3][4]importoswithhttpx.Client(base_urlos.environ.get(CDP_BASE_URL,https://web-data-lab.pages.dev),timeout10.0,trust_envFalse)asclient:responseclient.post(/labs/post-json/products/search/,json{keyword:keyboard,page:1,})response.raise_for_status()print(response.json())这里有三个容易混淆的参数params编码到 URLjson编码为 JSON 正文data常用于表单编码。服务端按它的接口契约解释请求不会因为三者都能放“键值对”就把它们当成一样。[4]GET 与 POST 的意义也不能只概括成“参数放在哪里”。GET 定义为读取资源POST 的业务语义由服务端决定可能是搜索也可能是创建订单。对搜索 POST 是否可以重试需要确认接口是否有副作用不能照搬“GET 失败就重试”的策略。[5]8. 从演示脚本走向一次有质量的采集假设一个经营分析团队要比较商品目录中的价格和库存。这是教学业务案例不是某个客户的真实结果。交付一份 24 行 CSV 很容易证明这份 CSV 没漏商品、没把旧价格当新价格才是后面的工作。我会在数据旁边保留采集时间、商品 ID、原始价格文本与来源路径。价格解析失败就保留错误记录不把异常吞掉后填零。若目标字段缺失停止这一批结果进入正式统计先核对响应与页面结构。这些检查的价值在于让问题发生时仍能找回证据。HTTP 客户端层也应有边界。httpx.Client提供连接池与跨请求配置复用超时可以按连接、读取、写入和连接池等待区分。出现超时先确认卡在哪个阶段再决定是重试、减小批量还是降低并发。[6][7] 示例中的trust_envFalse用于本地实验避免环境代理改变环回请求路径正式环境是否使用代理应由实际网络配置决定。最后不要只保留“爬虫成功”的一条日志。至少知道请求了多少页、每页多少条、原始记录数与唯一 ID 数是否一致以及哪些页失败。这样第二天目录变成 23 条时才能判断是业务变动还是采集退化。9. 这一篇之后应该能解释哪些现象你应能解释为什么屏幕有商品而原始 HTML 没有为什么 API 返回 200 却只得到一页为什么 POST 也能做搜索为什么相同解析器喂入不同 HTML 会得到不同结果。四个实验的价值在于形成一条可迁移的分析顺序而不是记住四个接口地址。下一篇的商品接口会加入身份校验浏览器成功Python 却收到 401 或 403。我们会从 Cookie 的自动携带讲到 Session、CSRF 和 Token继续追查请求到底差在哪里。References[1] Chrome DevTools Protocol. Chrome DevTools. Published/updated: not listed. Accessed: 2026-10-01. Used for: CDP 的定义、域与命令事件。[2] decimal — Decimal fixed-point and floating-point arithmetic. Python documentation. Published/updated: not listed. Accessed: 2026-10-01. Used for: 十进制金额处理。[3] Network features reference. Chrome for Developers. Published/updated: not listed. Accessed: 2026-10-01. Used for: Network 观察入口与请求复制。[4] QuickStart. HTTPX. Published/updated: not listed. Accessed: 2026-10-01. Used for: 参数、请求正文与响应解析。[5] HTTP request methods. MDN Web Docs. Published/updated: not listed. Accessed: 2026-10-01. Used for: HTTP 方法语义。[6] Clients. HTTPX. Published/updated: not listed. Accessed: 2026-10-01. Used for: 客户端连接池和配置复用。[7] Timeouts. HTTPX. Published/updated: not listed. Accessed: 2026-10-01. Used for: 超时阶段的区分。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进