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 文档给出的处理顺序是一个清晰的决策链:
- 首选:找到数据源(finding the data source)——直接定位并提供数据的请求,从源头取数;
- 若找不到数据源,但浏览器 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),进入下一节"复现请求"。
- 其他客户端能拿到→ 问题出在 Scrapy 的请求特征上。修改你的
四、Reproducing requests:把浏览器的请求原样复现到 Scrapy
当数据来自额外的 API 请求时,标准流程是:打开浏览器 Network 工具,观察浏览器是如何发起该请求的,再用 Scrapy 复现它。
只需发起相同 method 与 URL 的scrapy.Request有时就够了;有时你还必须复现请求体(body)、请求头(headers)和表单参数。
下图展示了在 Network 面板中定位到真正的数据请求(type 为json的quotes?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)或DefaultHeadersMiddleware、UserAgentMiddleware、HttpCompressionMiddleware等下载器中间件处于启用状态,最终发出的请求可能被中间件再次修改,复现失败时可从这个方向排查。
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 | 按常规使用 选择器 |
| JSON | response.json()直接加载;若目标数据是内嵌在 JSON 值中的 HTML/XML 片段,可把该片段载入Selector后照常使用选择器 |
JavaScript / 含目标数据的<script/>元素 | 见第五节"解析 JavaScript 代码" |
| CSS | 对response.text使用正则表达式提取 |
| 图片 / PDF 等基于图像的格式 | 从response.body读取字节,用 OCR 方案提取为文本(如pytesseract);从 PDF 读表格时tabula-py可能是更好的选择 |
| SVG / 内嵌 SVG 的 HTML | SVG 基于 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")。
第二步:三种提取手段,按数据形态选择
正则 +
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'}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'}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 手动安装AsyncioSelectorReactor(TWISTED_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 shell的view(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),仅供参考