Lzh on GitHub

Selecting dynamically-loaded content (选择动态加载内容)

有些网页在您用网络浏览器加载时会显示所需数据。但是,当您使用 Scrapy 下载它们时,您无法使用 选择器 访问所需数据。

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

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

Finding the data source (查找数据源)

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

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

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

在这种情况下,您可以使用 wgrep 等工具查找该资源的 URL。

如果数据最终来自原始 URL 本身,您必须检查网页源代码以确定数据所在的位置。

如果数据来自不同的 URL,您将需要重现相应的请求

Inspecting the source code of a webpage (检查网页源代码)

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

使用 Scrapy 的 fetch 命令下载 Scrapy 看到的网页内容:

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

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

如果您找不到所需数据,请首先确保这不是 Scrapy 的问题:使用 curlwget 等 HTTP 客户端下载网页,并查看是否可以在它们获得的响应中找到信息。

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

如果它们也获得不包含所需数据的响应,您将需要采取措施使您的请求更类似于网络浏览器的请求。请参阅重现请求

Reproducing requests (重现请求)

有时我们需要像网络浏览器执行请求那样重现请求。

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

生成具有相同 HTTP 方法和 URL 的 Request 可能就足够了。但是,您可能还需要重现该请求的正文标头表单参数(参见 FormRequest)。

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

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

您可以使用 Scrapy 重现任何请求。但是,有时重现所有必要的请求在开发时间上可能看起来效率不高。如果这是您的情况,并且抓取速度不是您主要关注的问题,您可以选择使用无头浏览器

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

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

Handling different response formats (处理不同的响应格式)

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

  • 如果响应是 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 代码转换为光栅图像,并处理该光栅图像

Parsing JavaScript code (解析 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 提供了将 JavaScript 对象解析为 dict 的 API。
    例如,如果 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>'
    

Using a headless browser (使用无头浏览器)

在从附加请求获取数据的网页上,重现包含所需数据的请求是首选方法。这种努力通常值得结果:结构化、完整的数据,解析时间和网络传输最小。

但是,有时重现某些请求可能真的很难。或者您可能需要请求无法提供的内容,例如网页在网络浏览器中显示的屏幕截图。在这种情况下,使用无头浏览器将有所帮助。

无头浏览器是一种特殊的网络浏览器,提供用于自动化的 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:,"]  # 避免使用默认的 Scrapy 下载器

    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 以获得更好的集成。