news 2026/9/7 9:54:34

Scrapy 动态内容抓取实战:定位数据源、复现请求与 Headless Browser 集成

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Scrapy 动态内容抓取实战:定位数据源、复现请求与 Headless Browser 集成

Scrapy 动态内容抓取实战:定位数据源、复现请求与 Headless Browser 集成

【免费下载链接】scrapyScrapy, a fast high-level web crawling & scraping framework for Python.项目地址: https://gitcode.com/GitHub_Trending/sc/scrapy

在浏览器中能看到的网页数据,用 Scrapy 下载后往往在 HTML 中"消失"——这正是动态加载页面的典型症状。本篇指南以 Scrapy 官方文档docs/topics/dynamic-content.rst为主体,系统讲解"选取动态加载内容"的完整方法论:优先寻找真实数据源(API/JSON 接口)、用scrapy fetch检查网页源码、用Request.from_curl复现浏览器请求、按响应类型选择解析策略,以及必要时接入 headless browser(Playwright)。读完后,你将能够独立诊断"为什么 Scrapy 抓不到数据",并掌握从源码复现请求到底层解析的完整实战链路。

一、问题的本质:浏览器看到的 DOM 不等于 Scrapy 拿到的 HTML

有些网页在浏览器中打开时能看到目标数据,但用 Scrapy 下载后,用选择器(selectors)却提取不到。原因是这类页面的数据并非由初始 HTML 提供,而是由 JavaScript 在页面加载后通过额外请求拉取、再动态插入 DOM。

Scrapy 文档给出的处理顺序是一个清晰的决策链:

  1. 首选:找到数据源(finding the data source)——直接定位并提供数据的请求,从源头取数;
  2. 若找不到数据源,但浏览器 DOM 中确实能看到数据,则考虑使用 headless browser渲染页面。

这个"先找数据源、渲染兜底"的顺序在docs/topics/dynamic-content.rst中是明确的主张,后文各节均围绕这条主线展开。

二、Finding the data source:如何定位数据的真实来源

要提取目标数据,必须先找到它的位置。文档按"数据形态"分两条路径:

2.1 非文本格式(图片、PDF 等)

如果数据本身是图片、PDF 等非文本格式,浏览器开发者工具的 Network 面板可以直接定位到对应的下载请求,然后按"复现请求"一节用 Scrapy 重放它即可。

2.2 文本格式(可在浏览器中选中为文本)

如果数据能以文本形式被选中,那么它要么内嵌在 JavaScript 代码里,要么以文本格式从外部资源加载。此时文档建议用wgrep这类全文搜索工具在所有资源中检索特征字符串,从而找到该资源对应的 URL。

找到 URL 后分两种情况:

  • 数据来自原始 URL 本身(即初始 HTML 里就有,只是不在<body>可见区域,而在<script/>内或注释里):需要检查网页源码,见下节;
  • 数据来自另一个 URL:需要复现对应请求,见第四节。

浏览器 Network 工具的使用细节可参考仓库文档 developer-tools 中的 Network-tool 一节,其中用quotes.toscrape.com/scroll页面演示了如何从"只有 Loading... 的初始响应"中找到真正的 JSON API 请求。

三、Inspecting the source code:用 scrapy fetch 检查网页源码

有时你需要检查网页的原始源码(而不是浏览器渲染后的 DOM)来定位数据。文档给出的标准操作是使用 Scrapy 的fetch命令,把"Scrapy 视角下的页面内容"直接落盘:

scrapy fetch --nolog https://example.com > response.html

--nolog用于关闭日志输出,保证重定向的文件只包含响应正文。从源码看,FetchCommand 在 help 文案中即明确提示了这一用法("output to stdout. You may want to use --nolog to disable logging"),这与文档命令一一对应。

拿到源码后:

  • 若目标数据在内嵌 JavaScript 代码(<script/>元素)里,跳转至本文第五节"解析 JavaScript 代码";
  • 若源码里根本没有目标数据,先排除"只是 Scrapy 拿不到"的可能:换用 curl 或 wget 等 HTTP 客户端下载同一页面,看它们的响应里有没有数据:
    • 其他客户端能拿到→ 问题出在 Scrapy 的请求特征上。修改你的scrapy.Request使其与对方客户端一致,例如使用相同的 user-agent 字符串(对应设置项USER_AGENT,其默认值在 default_settings.py 中定义为Scrapy/<版本> (+https://scrapy.org),这正是很多站点识别并降级响应 Scrapy 请求的特征)或相同的Request.headers
    • 其他客户端也拿不到→ 需要让请求更接近真实浏览器(如携带 cookies、特定 headers),进入下一节"复现请求"。

四、Reproducing requests:把浏览器的请求原样复现到 Scrapy

当数据来自额外的 API 请求时,标准流程是:打开浏览器 Network 工具,观察浏览器是如何发起该请求的,再用 Scrapy 复现它。

只需发起相同 method 与 URL 的scrapy.Request有时就够了;有时你还必须复现请求体(body)、请求头(headers)和表单参数。

下图展示了在 Network 面板中定位到真正的数据请求(type 为jsonquotes?page=1)及其返回的 JSON 对象——这就是"找到数据源"环节的典型成果:

4.1 Request.from_curl:从 cURL 命令直接生成 Scrapy 请求

由于所有主流浏览器都支持把 Network 面板中的请求导出为 cURL 格式,Scrapy 内置了Request.from_curl类方法,直接从一条 cURL 命令生成等价的scrapy.Request。该方法定义在 Request 类中,签名为:

Request.from_curl( curl_command: str, ignore_unknown_options: bool = True, **kwargs: Any, )

行为要点(由 scrapy/utils/curl.py 中的curl_to_request_kwargs实现可逐一印证):

  • 识别的参数:URL 位置参数,以及-H/--header-X/--request-b/--cookie-d/--data/--data-raw-u/--user
  • 可安全忽略的参数--compressed(因为HttpCompressionMiddleware默认开启)、-s/--silent-v/--verbose-#,这些出现时不会报错;
  • 未知参数:默认仅发出 warning(ignore_unknown_options=True);传入ignore_unknown_options=False时则抛出ValueError
  • 细节还原-d指定数据但未指定-X时,method 自动取POST(curl.py L136-L141);URL 缺少 scheme 时自动补http://;Cookie header 会被解析进cookies字典,-u基础认证会被转换为Authorization头;重复的-d参数会按 curl 的语义用&拼接成一个 body;
  • 注意from_curl的 docstring 中特别提醒——若 Request 子类(如JsonRequest)或DefaultHeadersMiddlewareUserAgentMiddlewareHttpCompressionMiddleware等下载器中间件处于启用状态,最终发出的请求可能被中间件再次修改,复现失败时可从这个方向排查。

developer-tools 文档 中给出了一个完整的from_curl实战示例(针对quotes.toscrape.com/api/quotes接口,还原了 User-Agent、X-Requested-With: XMLHttpRequest、Referer 等浏览器特征头),可复制参考。

若只想拿到等价参数字典而不构建 Request 对象,可直接使用 curl_to_request_kwargs 函数。文档还提到第三方工具curl2scrapy可作为 cURL 到 Scrapy 请求的转换辅助。

4.2 什么时候不该硬复现

Scrapy 可以复现任何请求,但当复现全部必要请求的开发成本过高、且爬取速度不是主要矛盾时,文档明确建议转向 headless browser 方案(见第六节)。

4.3 偶发失败的诊断

文档给出了一个重要判据:如果你有时能拿到期望响应、有时不能,问题大概率不在你的请求上,而在目标服务器——它可能有 bug、过载,或者正在封禁(banning)部分请求。封禁与应对策略参见 practices 文档的 bans 一节。

五、Handling different response formats:按响应类型选择解析策略

拿到包含目标数据的响应后,解析方式取决于响应类型。文档给出了完整分派表,这里完整继承并补充说明:

响应类型推荐解析方式
HTML / XML按常规使用 选择器
JSONresponse.json()直接加载;若目标数据是内嵌在 JSON 值中的 HTML/XML 片段,可把该片段载入Selector后照常使用选择器
JavaScript / 含目标数据的<script/>元素见第五节"解析 JavaScript 代码"
CSSresponse.text使用正则表达式提取
图片 / PDF 等基于图像的格式response.body读取字节,用 OCR 方案提取为文本(如pytesseract);从 PDF 读表格时tabula-py可能是更好的选择
SVG / 内嵌 SVG 的 HTMLSVG 基于 XML,可直接用选择器提取;否则可先把 SVG 转为栅格图片,再按图片方式处理

JSON 场景的两种用法:

data = response.json() # 数据内嵌于 JSON 中的 HTML 片段时: selector = Selector(text=data["html"])

六、Parsing JavaScript code:从 JS 中提取硬编码数据

若目标数据硬编码在 JavaScript 里,分两步:先拿到 JS 代码,再从中提取数据。

第一步:取得 JS 代码

  • 数据在独立 JS 文件中:直接读response.text
  • 数据在 HTML 页面的<script/>元素里:用选择器提取该元素的文本,例如response.css("script::text")

第二步:三种提取手段,按数据形态选择

  1. 正则 +json.loads:数据恰为 JSON 文本时最简单。例如 JS 中有一行var data = {"field": "value"};

    >>> pattern = r"\bvar\s+data\s*=\s*(\{.*?\})\s*;\s*\n" >>> json_data = response.css("script::text").re_first(pattern) >>> json.loads(json_data) {'field': 'value'}
  2. chompjs:把非严格的 JS 对象解析成dict。适合var data = {field: "value", secondField: "second value"};(键不带引号)这类浏览器常见的 JS 对象字面量:

    >>> import chompjs >>> javascript = response.css("script::text").get() >>> data = chompjs.parse_js_object(javascript) >>> data {'field': 'value', 'secondField': 'second value'}
  3. js2xml + 选择器:把整段 JS 转成 XML 文档,再用 XPath/CSS 选择器查询。例如:

    >>> import js2xml >>> import lxml.etree >>> from parsel import Selector >>> javascript = response.css("script::text").get() >>> xml = lxml.etree.tostring(js2xml.parse(javascript), encoding="unicode") >>> selector = Selector(text=xml) >>> selector.css('var[name="data"]').get() '<var name="data"><object><property name="field"><string>value</string></property></object></var>'

三种方案的适用边界可以归纳为:数据是标准 JSON 用正则;数据是 JS 对象字面量用 chompjs;需要按变量名/结构做复杂查询时用 js2xml。

七、Using a headless browser:Playwright 集成与 asyncio 前提

文档再次强调:能从额外请求中直接取数时,复现请求仍是首选——结构化、完整的数据量,以最小的解析时间和网络传输开销换来。但在两种情况下 headless browser 是必要工具:某些请求确实难以复现(签名、加密参数、复杂的 JS 逻辑链),或者你需要的是请求本身给不了的东西,例如"浏览器视角下的网页截图"。

headless browser 是提供自动化 API 的特殊浏览器。使用它的前提是启用 asyncio 支持:Scrapy 原生支持asyncio,用startproject创建的新项目默认已启用;若使用CrawlerRunner/AsyncCrawlerRunner编程接口,则需通过 install_reactor 手动安装AsyncioSelectorReactorTWISTED_REACTOR的默认值即twisted.internet.asyncioreactor.AsyncioSelectorReactor)。

asyncio 文档中还给出了一个容易被忽视的坑:twisted.internet.reactor等 import 会作为副作用安装默认 reactor,reactor 一旦安装无法在运行时切换——若在 Spider 模块顶层导入了这类 Twisted 符号,需把它们移入函数体内再导入。

7.1 官方示例:PlaywrightSpider

docs/topics/dynamic-content.rst给出了playwright-python在 Spider 内的最小可用示例:

import scrapy from playwright.async_api import async_playwright class PlaywrightSpider(scrapy.Spider): name = "playwright" start_urls = ["data:,"] # avoid using the default Scrapy downloader async def parse(self, response): async with async_playwright() as pw: browser = await pw.chromium.launch() page = await browser.new_page() await page.goto("https://example.org") title = await page.title() return {"title": title}

示例中一个值得注意的细节是start_urls = ["data:,"]:它用data:URI 触发 Spider,从而绕过默认下载器对真实 URL 的 HTTP 抓取(data:scheme 由 DataURIDownloadHandler 直接解析为响应,不产生网络请求)。这样页面的全部抓取都交给 Playwright 独立完成。

文档同时给出明确警告:像上面这样直接调用playwright-python,会绕过 Scrapy 的大部分组件(下载器中间件、去重过滤器等)。对于需要与 Scrapy 生态完整集成的场景,官方推荐使用独立的scrapy-playwright扩展(见 Scrapy 插件生态,仓库文档中作为推荐方案给出)。

7.2 小结:三条路线的取舍

场景推荐路线依据
数据来自可发现的 API 请求Network 面板定位 +Request复现 /from_curl结构化数据、最小网络开销
数据内嵌在 JS 变量中正则 / chompjs / js2xml 解析无需执行 JS
请求难复现或需要截图/渲染结果headless browser(playwright + asyncio reactor,集成用 scrapy-playwright)文档明确列出的两个适用情形

八、延伸阅读(仓库内相关文档)

  • dynamic-content 原文档:本篇所有方法论的出处;
  • developer-tools:scrapy shellview(response)fetch()、Network 工具与 livedom 的完整演示;
  • asyncio:asyncio reactor 配置、install_reactor与预装 reactor 冲突处理;
  • practices:被请求封禁(bans)时的应对实践;
  • 核心实现:Request.from_curl、scrapy/utils/curl.py、fetch 命令、data: URI 下载处理器。

【免费下载链接】scrapyScrapy, a fast high-level web crawling & scraping framework for Python.项目地址: https://gitcode.com/GitHub_Trending/sc/scrapy

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 9:52:35

AI编程准确率提升指南:从需求拆解到验证闭环

我先讲一个最近常被问到的问题&#xff1a;很多人觉得 AI 编程不靠谱&#xff0c;让模型写个脚本、补个接口、修个 Bug&#xff0c;结果经常是“看起来很有道理&#xff0c;跑起来全是意外”。更让人无语的是&#xff0c;你追问它是怎么得出这个结论的&#xff0c;它会非常诚恳…

作者头像 李华
网站建设 2026/9/7 9:51:35

SpringBoot+Vue前后端分离在线考试系统毕业设计全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 9:48:35

2026数模国赛模块求解Skill:问题分析、数据处理与图表绘制全攻略

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华