跳转到主要内容

Scrapy的Pyppeteer集成

项目描述

Scrapy的Pyppeteer集成

version pyversions actions codecov

此项目提供了一个Scrapy下载处理程序,该处理程序使用Pyppeteer执行请求。它可以用于处理需要JavaScript的页面。此包不会干扰Scrapy的正常工作流程,如请求调度或项目处理。

动机

在发布版本2.0之后,该版本包含部分协程语法支持和实验性的asyncio支持,Scrapy允许集成基于asyncio的项目,如Pyppeteer。

要求

  • Python 3.6+
  • Scrapy 2.0+
  • Pyppeteer 0.0.23+

安装

$ pip install scrapy-pyppeteer

配置

通过DOWNLOAD_HANDLERS替换默认的httphttps下载处理程序

DOWNLOAD_HANDLERS = {
    "http": "scrapy_pyppeteer.handler.ScrapyPyppeteerDownloadHandler",
    "https": "scrapy_pyppeteer.handler.ScrapyPyppeteerDownloadHandler",
}

注意,ScrapyPyppeteerDownloadHandler类继承自默认的http/https处理程序,并且它将仅对明确标记的请求使用Pyppeteer(有关详细信息,请参阅“基本用法”部分)。

另外,请确保安装基于asyncio的Twisted反应器

TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

scrapy-pyppeteer接受以下设置

  • PYPPETEER_LAUNCH_OPTIONS(类型 dict,默认 {}

    一个字典,包含在启动浏览器时要传递的选项。请参阅pyppeteer.launcher.launch的文档

  • PYPPETEER_NAVIGATION_TIMEOUT(类型 Optional[int],默认 None

    Pyppeteer请求页面时使用的默认超时时间(以毫秒为单位)。如果设置为None或未设置,则使用默认值(写作本文时为30000毫秒)。有关详细信息,请参阅pyppeteer.page.Page.setDefaultNavigationTimeout文档。

  • PYPPETEER_PAGE_COROUTINE_TIMEOUT(类型 Optional[Union[int, float]],默认 None

    使用页面协程(例如 waitForSelectorwaitForXPath)时传递的默认超时时间(以毫秒为单位)。如果设置为None或未设置,则使用默认值(写作本文时为30000毫秒)。

基本用法

pyppeteerRequest.meta键设置,以便使用Pyppeteer下载请求

import scrapy

class AwesomeSpider(scrapy.Spider):
    name = "awesome"

    def start_requests(self):
        # GET request
        yield scrapy.Request("https://httpbin.org/get", meta={"pyppeteer": True})
        # POST request
        yield scrapy.FormRequest(
            url="https://httpbin.org/post",
            formdata={"foo": "bar"},
            meta={"pyppeteer": True},
        )

    def parse(self, response):
        # 'response' contains the page as seen by the browser
        yield {"url": response.url}

页面协程

可以在pyppeteer_page_coroutinesRequest.meta键中传递一个排序的可迭代对象(例如 listtupledict),以便在返回最终 Response 到回调函数之前在 Page 上等待请求协程。

当您需要在页面上执行某些操作(如滚动或点击链接)并将所有操作视为单个Scrapy Response(包含最终结果)时,这很有用。

支持的操作

  • scrapy_pyppeteer.page.PageCoroutine(method: str, *args, **kwargs):

    表示在pyppeteer.page.Page对象上等待的协程,例如 "click"、"screenshot"、"evaluate" 等。 method 应为协程的名称,*args**kwargs 是传递给函数调用的参数。

    协程结果将存储在 PageCoroutine.result 属性中

    例如,

    PageCoroutine("screenshot", options={"path": "quotes.png", "fullPage": True})
    

    产生与以下相同的效果

    # 'page' is a pyppeteer.page.Page object
    await page.screenshot(options={"path": "quotes.png", "fullPage": True})
    
  • scrapy_pyppeteer.page.NavigationPageCoroutine(method: str, *args, **kwargs):

    PageCoroutine的子类。它等待导航事件:当您知道协程将触发导航事件时使用此功能,例如在点击链接时。这将强制使用 Page.waitForNavigation() 调用,并使用 asyncio.gather 包装,正如在Pyppeteer文档中建议的那样。

    例如,

    NavigationPageCoroutine("click", selector="a")
    

    产生与以下相同的效果

    # 'page' is a pyppeteer.page.Page object
    await asyncio.gather(
        page.waitForNavigation(),
        page.click(selector="a"),
    )
    

在回调函数中接收Page对象

pyppeteer.page.Page指定为回调参数的类型会导致相应的 Page 对象被注入到回调函数中。为了能够在提供的 Page 对象上等待协程,回调函数需要定义为协程函数(async def)。

import scrapy
import pyppeteer

class AwesomeSpiderWithPage(scrapy.Spider):
    name = "page"

    def start_requests(self):
        yield scrapy.Request("https://example.org", meta={"pyppeteer": True})

    async def parse(self, response, page: pyppeteer.page.Page):
        title = await page.title()  # "Example Domain"
        yield {"title": title}
        await page.close()

注意

  • 为了避免内存问题,建议手动通过等待 Page.close 协程来关闭页面。
  • Page 对象上等待协程产生的任何网络操作(如 gotogoBack 等)将直接由Pyppeteer执行,绕过Scrapy请求工作流程(调度器、中间件等)。

示例

点击链接,将结果页面保存为PDF

import scrapy
from scrapy_pyppeteer.page import PageCoroutine, NavigationPageCoroutine

class ClickAndSavePdfSpider(scrapy.Spider):
    name = "pdf"

    def start_requests(self):
        yield scrapy.Request(
            url="https://example.org",
            meta=dict(
                pyppeteer=True,
                pyppeteer_page_coroutines={
                    "click": NavigationPageCoroutine("click", selector="a"),
                    "pdf": PageCoroutine("pdf", options={"path": "/tmp/file.pdf"}),
                },
            ),
        )

    def parse(self, response):
        pdf_bytes = response.meta["pyppeteer_page_coroutines"]["pdf"].result
        with open("iana.pdf", "wb") as fp:
            fp.write(pdf_bytes)
        yield {"url": response.url}  # response.url is "https://www.iana.org/domains/reserved"

在无限滚动页面上向下滚动,截取整个页面的屏幕截图

import scrapy
import pyppeteer
from scrapy_pyppeteer.page import PageCoroutine

class ScrollSpider(scrapy.Spider):
    name = "scroll"

    def start_requests(self):
        yield scrapy.Request(
            url="http://quotes.toscrape.com/scroll",
            meta=dict(
                pyppeteer=True,
                pyppeteer_page_coroutines=[
                    PageCoroutine("waitForSelector", "div.quote"),
                    PageCoroutine("evaluate", "window.scrollBy(0, document.body.scrollHeight)"),
                    PageCoroutine("waitForSelector", "div.quote:nth-child(11)"),  # 10 per page
                    PageCoroutine("screenshot", options={"path": "quotes.png", "fullPage": True}),
                ],
            ),
        )

    def parse(self, response):
        return {"quote_count": len(response.css("div.quote"))}

致谢

本项目灵感来源于

由支持