选择动态加载的内容

有些网页在浏览器中加载时会显示所需数据。然而,当你使用 Scrapy 下载它们时,无法通过选择器获取到所需数据。

发生这种情况时,建议的方法是查找数据源并从中提取数据。

如果你无法做到这一点,但仍然可以通过网络浏览器的DOM访问所需数据,请参阅使用无头浏览器

查找数据源

要提取所需数据,你必须首先找到其源位置。

如果数据是非文本格式,例如图像或 PDF 文档,请使用网络浏览器的网络工具查找相应的请求,并复现它

如果你的网络浏览器允许你将所需数据选择为文本,则该数据可能定义在嵌入式 JavaScript 代码中,或者从外部资源以文本格式加载。

在这种情况下,你可以使用像 wgrep 这样的工具来查找该资源的 URL。

如果数据显示来自原始 URL 本身,你必须检查网页源代码以确定数据位于何处。

如果数据来自不同的 URL,你需要复现相应的请求

检查网页源代码

有时你需要检查网页的源代码(而不是DOM)以确定所需数据位于何处。

使用 Scrapy 的 fetch 命令下载 Scrapy 所见的网页内容

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

如果所需数据位于 <script/> 元素内的嵌入式 JavaScript 代码中,请参阅解析 JavaScript 代码

如果你找不到所需数据,首先要确保这不只是 Scrapy 的问题:使用 curlwget 等 HTTP 客户端下载网页,看看它们获取的响应中是否包含所需信息。

如果它们获得了包含所需数据的响应,请修改你的 Scrapy Request 以匹配其他 HTTP 客户端的请求。例如,尝试使用相同的 user-agent 字符串 (USER_AGENT) 或相同的 headers

如果它们也获得了不包含所需数据的响应,你需要采取措施使你的请求更接近网络浏览器的请求。请参阅复现请求

复现请求

有时我们需要以网络浏览器执行请求的方式来复现请求。

使用网络浏览器的网络工具查看你的网络浏览器如何执行所需请求,并尝试使用 Scrapy 复现该请求。

只用相同的 HTTP 方法和 URL 发出Request可能就足够了。但是,你可能还需要复现该请求的主体、头部和表单参数(请参阅创建提交 HTML 表单的请求)。

由于所有主流浏览器都允许以 curl 格式导出请求,Scrapy 集成了 from_curl() 方法,可以从 cURL 命令生成等效的 Request。要获取更多信息,请访问网络工具部分中的从 curl 获取请求

一旦你获得了预期响应,就可以从中提取所需数据

你可以使用 Scrapy 复现任何请求。然而,有时复现所有必要的请求在开发时间上可能效率不高。如果这是你的情况,并且抓取速度不是你的主要关注点,你可以选择使用无头浏览器

如果你有时能获得预期响应,但并非总是如此,那么问题可能不在于你的请求,而在于目标服务器。目标服务器可能存在 bug、过载,或者禁止了你的某些请求。

请注意,要将 cURL 命令转换为 Scrapy 请求,你可以使用 curl2scrapy

处理不同的响应格式

一旦你获得了包含所需数据的响应,如何从中提取所需数据取决于响应的类型

  • 如果响应是 HTML、XML 或 JSON,照常使用选择器

  • 如果响应是 JSON,请使用 response.json() 加载所需数据

    data = response.json()
    

    如果所需数据位于嵌入在 JSON 数据中的 HTML 或 XML 代码内部,你可以将该 HTML 或 XML 代码加载到 Selector 中,然后照常使用它

    selector = Selector(data["html"])
    
  • 如果响应是 JavaScript,或者包含所需数据的 <script/> 元素的 HTML,请参阅解析 JavaScript 代码

  • 如果响应是 CSS,请使用正则表达式response.text 中提取所需数据。

  • 如果响应是图像或基于图像的其他格式(例如 PDF),请从 response.body 中以字节形式读取响应,并使用 OCR 解决方案将所需数据提取为文本。

    例如,你可以使用 pytesseract。要从 PDF 中读取表格,tabula-py 可能是更好的选择。

  • 如果响应是 SVG,或者包含所需数据的嵌入式 SVG 的 HTML,由于 SVG 基于 XML,你可能能够使用选择器提取所需数据。

    否则,你可能需要将 SVG 代码转换为栅格图像,并处理该栅格图像

解析 JavaScript 代码

如果所需数据硬编码在 JavaScript 中,你首先需要获取 JavaScript 代码

  • 如果 JavaScript 代码位于 JavaScript 文件中,只需读取 response.text

  • 如果 JavaScript 代码位于 HTML 页面的 <script/> 元素内,请使用选择器提取该 <script/> 元素内的文本。

一旦你获得了包含 JavaScript 代码的字符串,就可以从中提取所需数据

  • 你可能能够使用正则表达式以 JSON 格式提取所需数据,然后使用 json.loads() 进行解析。

    例如,如果 JavaScript 代码包含一行类似 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 提供了一个 API,用于将 JavaScript 对象解析为 dict

    例如,如果 JavaScript 代码包含 var data = {field: "value", secondField: "second value"};,你可以按如下方式提取该数据

    >>> import chompjs
    >>> javascript = response.css("script::text").get()
    >>> data = chompjs.parse_js_object(javascript)
    >>> data
    {'field': 'value', 'secondField': 'second value'}
    
  • 否则,使用 js2xml 将 JavaScript 代码转换为 XML 文档,然后可以使用选择器进行解析。

    例如,如果 JavaScript 代码包含 var data = {field: "value"};,你可以按如下方式提取该数据

    >>> 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>'
    

使用无头浏览器

对于从额外请求获取数据的网页,复现包含所需数据的那些请求是首选方法。这种努力通常是值得的:可以获得结构化、完整的数据,同时最大程度地减少解析时间和网络传输。

然而,有时复现某些请求可能非常困难。或者你可能需要某些请求无法提供的东西,例如在网页浏览器中看到的网页截图。在这种情况下,使用无头浏览器会有所帮助。

无头浏览器是一种特殊的网络浏览器,提供用于自动化的 API。通过安装asyncio reactor,可以集成处理无头浏览器的基于 asyncio 的库。

其中一个库是 playwright-pythonplaywright 的官方 Python 移植)。下面是一个简单的代码片段,用于说明它在 Scrapy 爬虫中的用法

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}

然而,像上述示例那样直接使用 playwright-python 会绕过 Scrapy 的大部分组件(中间件、重复过滤器等)。我们建议使用 scrapy-playwright 以获得更好的集成。