跳转到主要内容

Scrapy的Playwright集成

项目描述

scrapy-playwright: Scrapy的Playwright集成

version pyversions Tests codecov

一个Scrapy下载处理器,它使用Python的Playwright执行请求。它可以用来处理需要JavaScript(以及其他一些功能)的页面,同时遵守常规的Scrapy工作流程(即不干扰请求调度、项目处理等)。

要求

版本2.0发布后,该版本包括协程语法支持asyncio支持,Scrapy允许集成基于asyncio的项目,如Playwright

最低要求版本

  • Python >= 3.8
  • Scrapy >= 2.0 (!= 2.4.0)
  • Playwright >= 1.15

安装

scrapy-playwright可在PyPI上找到,并可以使用pip进行安装

pip install scrapy-playwright

playwright被定义为依赖项,因此它会自动安装,但是可能需要安装将使用的特定浏览器

playwright install

还可以只安装可用浏览器的一个子集

playwright install firefox chromium

更新日志

请参阅更新日志文档。

激活

下载处理器

通过DOWNLOAD_HANDLERS替换默认的http和/或https下载处理器。

# settings.py
DOWNLOAD_HANDLERS = {
    "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
    "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
}

请注意,ScrapyPlaywrightDownloadHandler类继承自默认的http/https处理器。除非明确标记(见基本用法),否则请求将由常规Scrapy下载处理器处理。

Twisted反应器

安装基于asyncio的Twisted反应器:

# settings.py
TWISTED_REACTOR = "twisted.internet.asyncioreactor.AsyncioSelectorReactor"

Scrapy 2.7以来,这是新项目的默认设置。

基本用法

playwright Request.meta键设置为使用Playwright下载请求

import scrapy

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

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

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

关于User-Agent头部的说明

默认情况下,发出的请求包含由Scrapy设置的User-Agent(通过USER_AGENTDEFAULT_REQUEST_HEADERS设置或通过Request.headers属性)。这可能导致一些网站以意外的方式反应,例如如果用户代理与运行的浏览器不匹配。如果您希望使用您使用的特定浏览器默认发送的User-Agent,请将Scrapy用户代理设置为None

Windows支持

通过在单独的线程中运行Playwright以使用ProactorEventLoop,可以实现Windows支持。这是必要的,因为无法在Scrapy爬虫相同的asyncio事件循环中运行Playwright。

  • Playwright在子进程中运行驱动程序。来源:Playwright仓库
  • "在Windows上,默认的事件循环ProactorEventLoop支持子进程,而SelectorEventLoop不支持"。来源:Python文档
  • Twisted的asyncio反应器需要SelectorEventLoop。来源:Twisted仓库

支持的设置

PLAYWRIGHT_BROWSER_TYPE

类型 str,默认 "chromium"

要启动的浏览器类型,例如chromiumfirefoxwebkit

PLAYWRIGHT_BROWSER_TYPE = "firefox"

PLAYWRIGHT_LAUNCH_OPTIONS

类型 dict,默认 {}

一个字典,包含在启动浏览器时要传递的选项作为关键字参数。有关支持的参数列表,请参阅BrowserType.launch文档。

PLAYWRIGHT_LAUNCH_OPTIONS = {
    "headless": False,
    "timeout": 20 * 1000,  # 20 seconds
}

PLAYWRIGHT_CDP_URL

类型 Optional[str],默认 None

通过BrowserType.connect_over_cdp连接到远程Chromium浏览器的端点,使用Chrome DevTools Protocol

PLAYWRIGHT_CDP_URL = "http://localhost:9222"

如果使用此设置

  • 所有非持久上下文都将创建在连接的远程浏览器上
  • PLAYWRIGHT_LAUNCH_OPTIONS设置被忽略
  • PLAYWRIGHT_BROWSER_TYPE设置必须设置为不同于"chromium"的值

此设置不能与PLAYWRIGHT_CONNECT_URL同时使用

PLAYWRIGHT_CDP_KWARGS

类型 dict[str, Any],默认 {}

当使用PLAYWRIGHT_CDP_URL时,传递给BrowserType.connect_over_cdp的附加关键字参数。始终忽略endpoint_url键,使用PLAYWRIGHT_CDP_URL代替。

PLAYWRIGHT_CDP_KWARGS = {
    "slow_mo": 1000,
    "timeout": 10 * 1000
}

PLAYWRIGHT_CONNECT_URL

类型 Optional[str],默认 None

要使用BrowserType.connect连接的远程Playwright浏览器实例的URL。

来自上游Playwright文档

当通过Node.js中的BrowserType.launchServer连接到另一个浏览器时,主版本和副版本需要与客户端版本匹配(1.2.3 → 兼容1.2.x)。

PLAYWRIGHT_CONNECT_URL = "ws://localhost:35477/ae1fa0bc325adcfd9600d9f712e9c733"

如果使用此设置

  • 所有非持久上下文都将创建在连接的远程浏览器上
  • PLAYWRIGHT_LAUNCH_OPTIONS设置被忽略

此设置不能与PLAYWRIGHT_CDP_URL同时使用

PLAYWRIGHT_CONNECT_KWARGS

类型 dict[str, Any],默认 {}

当使用PLAYWRIGHT_CONNECT_URL时,传递给BrowserType.connect的额外关键字参数。键ws_endpoint始终被忽略,而是使用PLAYWRIGHT_CONNECT_URL

PLAYWRIGHT_CONNECT_KWARGS = {
    "slow_mo": 1000,
    "timeout": 10 * 1000
}

PLAYWRIGHT_CONTEXTS

类型dict[str, dict],默认{}

一个字典,用于定义启动时要创建的浏览器上下文。它应该是(名称,关键字参数)的映射。

PLAYWRIGHT_CONTEXTS = {
    "foobar": {
        "context_arg1": "value",
        "context_arg2": "value",
    },
    "default": {
        "context_arg1": "value",
        "context_arg2": "value",
    },
    "persistent": {
        "user_data_dir": "/path/to/dir",  # will be a persistent context
        "context_arg1": "value",
    },
}

有关更多信息,请参阅浏览器上下文部分。有关Browser.new_context的文档。

PLAYWRIGHT_MAX_CONTEXTS

类型Optional[int],默认None

允许的最大并发Playwright上下文数量。如果没有设置或为None,则不强制限制。有关更多信息,请参阅最大并发上下文计数部分。

PLAYWRIGHT_MAX_CONTEXTS = 8

PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT

类型Optional[float],默认None

Playwright请求页面时使用的超时时间,以毫秒为单位。如果为None或未设置,则使用默认值(写作时的30000毫秒)。请参阅BrowserContext.set_default_navigation_timeout的文档。

PLAYWRIGHT_DEFAULT_NAVIGATION_TIMEOUT = 10 * 1000  # 10 seconds

PLAYWRIGHT_PROCESS_REQUEST_HEADERS

类型Optional[Union[Callable, str]],默认scrapy_playwright.headers.use_scrapy_headers

一个函数(或函数的路径),用于处理Playwright请求,并返回一个包含要覆盖的头的字典。支持协程函数(async def)。

此函数至少会被每个Scrapy请求调用一次,但如果Playwright生成更多请求(例如检索资产如图像或脚本),则可能还会被调用更多次。

函数必须返回一个Dict[str, str]对象,并接收以下三个关键字参数

- browser_type_name: str
- playwright_request: playwright.async_api.Request
- scrapy_request_data: dict
    * method: str
    * url: str
    * headers: scrapy.http.headers.Headers
    * body: Optional[bytes]
    * encoding: str

默认函数(scrapy_playwright.headers.use_scrapy_headers)试图模仿Scrapy的导航请求行为,即用Scrapy请求的值覆盖头。对于非导航请求(例如图像、样式表、脚本等),只有User-Agent头被覆盖,以保持一致性。

设置PLAYWRIGHT_PROCESS_REQUEST_HEADERS=None将使Playwright完全控制,即忽略Scrapy请求的头,只发送Playwright设置的头。请注意,在这种情况下,通过Request.headers属性或由Scrapy组件设置的头部(包括通过Request.cookies属性设置的cookie)将被忽略。

示例

async def custom_headers(
    *,
    browser_type_name: str,
    playwright_request: playwright.async_api.Request,
    scrapy_request_data: dict,
) -> Dict[str, str]:
    headers = await playwright_request.all_headers()
    scrapy_headers = scrapy_request_data["headers"].to_unicode_dict()
    headers["Cookie"] = scrapy_headers.get("Cookie")
    return headers

PLAYWRIGHT_PROCESS_REQUEST_HEADERS = custom_headers

已弃用的参数处理

在版本0.0.40及之前,参数以位置方式传递给函数,并且只传递Scrapy头,而不是包含Scrapy请求数据的字典。自版本0.0.41起已弃用,并且将根据弃用策略最终删除对此种处理参数的支持。

传递的参数

- browser_type: str
- playwright_request: playwright.async_api.Request
- scrapy_headers: scrapy.http.headers.Headers

示例

def custom_headers(
    browser_type: str,
    playwright_request: playwright.async_api.Request,
    scrapy_headers: scrapy.http.headers.Headers,
) -> dict:
    if browser_type == "firefox":
        return {"User-Agent": "foo"}
    return {"User-Agent": "bar"}

PLAYWRIGHT_PROCESS_REQUEST_HEADERS = custom_headers

PLAYWRIGHT_RESTART_DISCONNECTED_BROWSER

类型bool,默认True

如果浏览器断开连接,浏览器是否会重新启动,例如,如果本地浏览器崩溃或远程连接超时。通过监听disconnected Browser事件来实现,因此它不适用于持久上下文,因为BrowserType.launch_persistent_context直接返回上下文。

PLAYWRIGHT_MAX_PAGES_PER_CONTEXT

类型int,默认值是Scrapy的CONCURRENT_REQUESTS设置值

每个上下文中允许的并发 Playwright 页面的最大数量。有关未关闭页面的说明,请参阅注意事项

PLAYWRIGHT_MAX_PAGES_PER_CONTEXT = 4

PLAYWRIGHT_ABORT_REQUEST

类型 Optional[Union[Callable, str]],默认 None

一个接收 playwright.async_api.Request 对象的谓词函数(或函数的路径),如果请求应该被中止,则必须返回 True,否则返回 False。支持协程函数(async def)。

注意,所有请求都将出现在 DEBUG 级别日志中,但是中止请求将没有相应的响应日志行。中止请求会在 playwright/request_count/aborted 工作统计项中计数。

def should_abort_request(request):
    return (
        request.resource_type == "image"
        or ".jpg" in request.url
    )

PLAYWRIGHT_ABORT_REQUEST = should_abort_request

关于设置的通用说明

对于接受对象路径作为字符串的设置,只有在使用 Scrapy≥2.4 时才支持传递可调用对象。在较早版本中,仅支持字符串。

支持的 Request.meta

playwright

类型 bool,默认 False

如果设置为评估为 True 的值,则请求将由 Playwright 处理。

return scrapy.Request("https://example.org", meta={"playwright": True})

playwright_context

类型 str,默认 "default"

要用于下载请求的上下文名称。有关更多信息,请参阅浏览器上下文部分。

return scrapy.Request(
    url="https://example.org",
    meta={
        "playwright": True,
        "playwright_context": "awesome_context",
    },
)

playwright_context_kwargs

类型 dict,默认 {}

一个字典,包含在创建新上下文时使用的关键字参数。如果不存在具有 playwright_context 元键中指定的名称的上下文,则会使用这些关键字参数。有关更多信息,请参阅浏览器上下文部分。

return scrapy.Request(
    url="https://example.org",
    meta={
        "playwright": True,
        "playwright_context": "awesome_context",
        "playwright_context_kwargs": {
            "ignore_https_errors": True,
        },
    },
)

playwright_include_page

类型 bool,默认 False

如果设置为 True,则用于下载请求的 Playwright 页面 将在回调中可用,位置为 response.meta['playwright_page']。如果设置为 False(或未设置),则页面将在处理请求后立即关闭。

重要!

此元键完全可选,不是页面加载或执行任何异步操作(特别是不是应用 PageMethod 对象)所必需的。仅在需要访问回调中处理响应的页面对象时使用它。

有关更多信息和建议,请参阅在回调中接收页面对象

return scrapy.Request(
    url="https://example.org",
    meta={"playwright": True, "playwright_include_page": True},
)

playwright_page_event_handlers

类型 Dict[Str, Callable],默认 {}

要附加到页面事件的处理器字典。请参阅处理页面事件

playwright_page_init_callback

类型 Optional[Union[Callable, str]],默认 None

一个要为新创建的页面调用的协程函数(async def)。在附加页面事件处理器和设置内部路由处理之后、发出任何请求之前调用。它接收 Playwright 页面和 Scrapy 请求作为位置参数。对于初始化代码很有用。如果请求的页面已经存在(例如,通过传递 playwright_page),则忽略。

async def init_page(page, request):
    await page.add_init_script(path="./custom_script.js")

class AwesomeSpider(scrapy.Spider):
    def start_requests(self):
        yield scrapy.Request(
            url="https://httpbin.org/headers",
            meta={
                "playwright": True,
                "playwright_page_init_callback": init_page,
            },
        )

重要!

scrapy-playwright 使用 Page.routePage.unroute,避免使用这些方法,除非您确切知道自己在做什么。

playwright_page_methods

类型 Iterable[PageMethod],默认 ()

一个指示在返回最终响应之前在页面上执行的操作的 scrapy_playwright.page.PageMethod 对象的可迭代对象。请参阅在页面上执行操作

playwright_page

类型 Optional[playwright.async_api.Page],默认 None

用于下载请求的 Playwright 页面。如果没有指定,则每个请求都会创建一个新的页面。此键可以与 playwright_include_page 一起使用,以使用同一页面进行请求链。例如

from playwright.async_api import Page

def start_requests(self):
    yield scrapy.Request(
        url="https://httpbin.org/get",
        meta={"playwright": True, "playwright_include_page": True},
    )

def parse(self, response, **kwargs):
    page: Page = response.meta["playwright_page"]
    yield scrapy.Request(
        url="https://httpbin.org/headers",
        callback=self.parse_headers,
        meta={"playwright": True, "playwright_page": page},
    )

playwright_page_goto_kwargs

类型 dict,默认 {}

一个字典,包含要传递给页面goto方法的键值参数,用于导航到URL。如果存在url键,则忽略它,而使用请求URL代替。

return scrapy.Request(
    url="https://example.org",
    meta={
        "playwright": True,
        "playwright_page_goto_kwargs": {
            "wait_until": "networkidle",
        },
    },
)

playwright_security_details

类型Optional[dict],只读

包含有关给定响应的安全信息的字典。仅适用于HTTPS请求。可以通过response.meta['playwright_security_details']在回调中访问。

def parse(self, response, **kwargs):
    print(response.meta["playwright_security_details"])
    # {'issuer': 'DigiCert TLS RSA SHA256 2020 CA1', 'protocol': 'TLS 1.3', 'subjectName': 'www.example.org', 'validFrom': 1647216000, 'validTo': 1678838399}

playwright_suggested_filename

类型Optional[str],只读

当响应是下载的二进制内容(例如PDF文件)时,Download.suggested_filename属性的值。仅适用于仅导致下载的响应。可以通过response.meta['playwright_suggested_filename']在回调中访问。

def parse(self, response, **kwargs):
    print(response.meta["playwright_suggested_filename"])
    # 'sample_file.pdf'

在回调中接收Page对象

对于请求,在playwright_include_page元键中指定一个评估为True的值,将导致相应的playwright.async_api.Page对象在请求回调中的playwright_page元键中可用。为了能够在提供的Page对象上等待协程,回调需要定义为协程函数(async def)。

注意

请谨慎使用,并且只有当您确实需要在回调中使用Page对象时才使用它。如果页面对象在不再需要后没有正确关闭,蜘蛛作业可能会因为PLAYWRIGHT_MAX_PAGES_PER_CONTEXT设置的限制而卡住。

from playwright.async_api import Page
import scrapy

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

    def start_requests(self):
        yield scrapy.Request(
            url="https://example.org",
            callback=self.parse_first,
            meta={"playwright": True, "playwright_include_page": True},
            errback=self.errback_close_page,
        )

    def parse_first(self, response):
        page: Page = response.meta["playwright_page"]
        return scrapy.Request(
            url="https://example.com",
            callback=self.parse_second,
            meta={"playwright": True, "playwright_include_page": True, "playwright_page": page},
            errback=self.errback_close_page,
        )

    async def parse_second(self, response):
        page: Page = response.meta["playwright_page"]
        title = await page.title()  # "Example Domain"
        await page.close()
        return {"title": title}

    async def errback_close_page(self, failure):
        page: Page = failure.request.meta["playwright_page"]
        await page.close()

备注

  • 当传递playwright_include_page=True时,请确保在不再使用页面时始终关闭页面。建议设置请求回退以确保即使在请求失败的情况下也关闭页面(如果playwright_include_page=False,页面在遇到异常时自动关闭)。这很重要,因为打开的页面计入由PLAYWRIGHT_MAX_PAGES_PER_CONTEXT设置的限制,如果达到限制且页面无限期打开,则爬虫可能会冻结。
  • 定义回调为async def仅当您需要等待事物时才是必要的,如果您只是需要将Page对象从一个回调传递到另一个回调(请参阅上面的示例),则不是必要的。
  • 在Page对象上等待协程(如gotogo_back等)产生的任何网络操作都将由Playwright直接执行,绕过Scrapy请求工作流程(调度器、中间件等)。

浏览器上下文

可以在启动时通过PLAYWRIGHT_CONTEXTS设置定义要启动的多个浏览器上下文

为请求选择特定的上下文

playwright_context元键中传递所需上下文名称。

yield scrapy.Request(
    url="https://example.org",
    meta={"playwright": True, "playwright_context": "first"},
)

默认上下文

如果请求没有通过playwright_context元键显式指定上下文,则默认使用名为default的通用上下文。此default上下文也可以通过启动时的PLAYWRIGHT_CONTEXTS设置进行自定义。

持久上下文

通过传递user_data_dir关键字参数来启动上下文作为持久上下文。另请参阅BrowserType.launch_persistent_context

请注意,持久上下文独立于主浏览器实例启动,因此PLAYWRIGHT_LAUNCH_OPTIONS设置中传递的关键字参数不适用。

在爬取过程中创建上下文

如果 playwright_context 元键中指定的上下文不存在,它将被创建。您可以在 playwright_context_kwargs 元键中指定要传递给 Browser.new_context 的关键字参数

yield scrapy.Request(
    url="https://example.org",
    meta={
        "playwright": True,
        "playwright_context": "new",
        "playwright_context_kwargs": {
            "java_script_enabled": False,
            "ignore_https_errors": True,
            "proxy": {
                "server": "http://myproxy.com:3128",
                "username": "user",
                "password": "pass",
            },
        },
    },
)

请注意,如果已存在具有指定名称的上下文,则将使用该上下文,并且忽略 playwright_context_kwargs

在爬取过程中关闭上下文

您的回调中接收到页面对象 后,您可以通过对应的 Page.context 属性访问上下文,并等待其 close

def parse(self, response, **kwargs):
    yield scrapy.Request(
        url="https://example.org",
        callback=self.parse_in_new_context,
        errback=self.close_context_on_error,
        meta={
            "playwright": True,
            "playwright_context": "awesome_context",
            "playwright_include_page": True,
        },
    )

async def parse_in_new_context(self, response):
    page = response.meta["playwright_page"]
    title = await page.title()
    await page.close()
    await page.context.close()
    return {"title": title}

async def close_context_on_error(self, failure):
    page = failure.request.meta["playwright_page"]
    await page.close()
    await page.context.close()

避免关闭上下文时的竞态条件和内存泄漏

确保在关闭上下文之前关闭页面。有关更多信息,请参阅 此评论,位于 #191

最大并发上下文计数

PLAYWRIGHT_MAX_CONTEXTS 设置指定一个值以限制并发上下文数量。谨慎使用:如果不再使用上下文后没有关闭它们,则可能会阻塞整个爬取(请参阅 本节 以动态关闭上下文)。请确保定义一个错误回调,以便即使在出现错误的情况下也能关闭上下文。

代理支持

通过在 PLAYWRIGHT_LAUNCH_OPTIONS 设置中指定 proxy 键,在浏览器级别支持代理

from scrapy import Spider, Request

class ProxySpider(Spider):
    name = "proxy"
    custom_settings = {
        "PLAYWRIGHT_LAUNCH_OPTIONS": {
            "proxy": {
                "server": "http://myproxy.com:3128",
                "username": "user",
                "password": "pass",
            },
        }
    }

    def start_requests(self):
        yield Request("http://httpbin.org/get", meta={"playwright": True})

    def parse(self, response, **kwargs):
        print(response.text)

代理也可以通过 PLAYWRIGHT_CONTEXTS 设置在上下文级别设置

PLAYWRIGHT_CONTEXTS = {
    "default": {
        "proxy": {
            "server": "http://default-proxy.com:3128",
            "username": "user1",
            "password": "pass1",
        },
    },
    "alternative": {
        "proxy": {
            "server": "http://alternative-proxy.com:3128",
            "username": "user2",
            "password": "pass2",
        },
    },
}

或者在 爬取时创建上下文 时传递 proxy 键。

另请参阅

在页面上执行操作

PageMethod 对象的排序可迭代对象(例如 listtupledict)传递到 playwright_page_methods Request.meta 键中,以请求要在返回最终 Response 到回调之前在 Page 对象上调用的方法。

当您需要在页面上执行某些操作(如向下滚动或点击链接)并只想在回调中处理最终结果时,这很有用。

PageMethod

scrapy_playwright.page.PageMethod(method: str, *args, **kwargs):

表示要在 playwright.page.Page 对象上调用(如果需要则等待)的方法(例如 "click"、"screenshot"、"evaluate" 等)。method 是方法名称,*args**kwargs 是调用此方法时传递的。返回值将存储在 PageMethod.result 属性中。

例如

def start_requests(self):
    yield Request(
        url="https://example.org",
        meta={
            "playwright": True,
            "playwright_page_methods": [
                PageMethod("screenshot", path="example.png", full_page=True),
            ],
        },
    )

def parse(self, response, **kwargs):
    screenshot = response.meta["playwright_page_methods"][0]
    # screenshot.result contains the image's bytes

产生的效果与

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

async def parse(self, response, **kwargs):
    page = response.meta["playwright_page"]
    screenshot = await page.screenshot(path="example.png", full_page=True)
    # screenshot contains the image's bytes
    await page.close()

支持的方法

有关可用方法的详细信息,请参阅 Page 类的上游文档

对响应对象的影响

某些 Response 属性(例如 urlip_address)反映了在页面上执行的最后一个动作后的状态。如果您发出一个导致导航(例如在链接上单击)的 PageMethod,则 Response.url 属性将指向新的 URL,这可能与请求的 URL 不同。

处理页面事件

可以在 playwright_page_event_handlers Request.meta 键中指定页面事件处理器字典。键是要处理的事件名称(例如 dialogdownload 等)。值可以是可调用对象或字符串(在这种情况下,将查找具有该名称的爬虫方法)。

示例

from playwright.async_api import Dialog

async def handle_dialog(dialog: Dialog) -> None:
    logging.info(f"Handled dialog with message: {dialog.message}")
    await dialog.dismiss()

class EventSpider(scrapy.Spider):
    name = "event"

    def start_requests(self):
        yield scrapy.Request(
            url="https://example.org",
            meta={
                "playwright": True,
                "playwright_page_event_handlers": {
                    "dialog": handle_dialog,
                    "response": "handle_response",
                },
            },
        )

    async def handle_response(self, response: PlaywrightResponse) -> None:
        logging.info(f"Received response with URL {response.url}")

查看 上游 Page 文档 了解接受的事件及其处理器的参数列表。

关于页面事件处理器的说明

  • 事件处理器将保留在页面中,并将对使用相同页面的后续下载进行调用,除非之后被删除。这通常不是问题,因为默认情况下请求是在单次使用的页面中执行的。
  • 事件处理器将处理 Playwright 对象,而不是 Scrapy 对象。例如,对于每个 Scrapy 请求/响应,都存在对应的 Playwright 请求/响应,但反之则不然:获取图像、脚本、样式表等的后台请求/响应不会被 Scrapy 所见。

内存使用扩展

默认的 Scrapy 内存使用扩展(scrapy.extensions.memusage.MemoryUsage)不包括 Playwright 所使用的内存,因为浏览器是以单独进程启动的。scrapy-playwright 包提供了一种替换扩展,该扩展也考虑了 Playwright 所使用的内存。此扩展需要 psutil 包才能工作。

更新 EXTENSIONS 设置以禁用内置的 Scrapy 扩展,并用 scrapy-playwright 包中的扩展替换

# settings.py
EXTENSIONS = {
    "scrapy.extensions.memusage.MemoryUsage": None,
    "scrapy_playwright.memusage.ScrapyPlaywrightMemoryUsageExtension": 0,
}

有关受支持的设置的更多信息,请参阅上游文档

Windows支持

就像上游 Scrapy 扩展一样,此自定义内存扩展在 Windows 上不可用。这是因为 stdlib resource 模块不可用。

示例

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

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

    def start_requests(self):
        yield scrapy.Request(
            url="https://example.org",
            meta=dict(
                playwright=True,
                playwright_page_methods={
                    "click": PageMethod("click", selector="a"),
                    "pdf": PageMethod("pdf", path="/tmp/file.pdf"),
                },
            ),
        )

    def parse(self, response, **kwargs):
        pdf_bytes = response.meta["playwright_page_methods"]["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"

在无限滚动页面上向下滚动,捕获全页截图

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

    def start_requests(self):
        yield scrapy.Request(
            url="http://quotes.toscrape.com/scroll",
            meta=dict(
                playwright=True,
                playwright_include_page=True,
                playwright_page_methods=[
                    PageMethod("wait_for_selector", "div.quote"),
                    PageMethod("evaluate", "window.scrollBy(0, document.body.scrollHeight)"),
                    PageMethod("wait_for_selector", "div.quote:nth-child(11)"),  # 10 per page
                ],
            ),
        )

    async def parse(self, response, **kwargs):
        page = response.meta["playwright_page"]
        await page.screenshot(path="quotes.png", full_page=True)
        await page.close()
        return {"quote_count": len(response.css("div.quote"))}  # quotes from several pages

有关更多信息,请参阅示例目录

已知问题

不支持按请求代理

不支持通过 proxy 请求元键指定代理。有关更多信息,请参阅代理支持部分。

不支持信号

headers_receivedbytes_received 信号不会被 scrapy-playwright 下载处理器触发。

报告问题

在打开问题之前,请确保意外行为只能通过使用此包来观察,而不能通过独立的 Playwright 来观察。为此,将您的爬虫代码翻译为接近合理的 Playwright 脚本:如果问题以这种方式发生,您应该将其报告给上游。例如

import scrapy

class ExampleSpider(scrapy.Spider):
    name = "example"

    def start_requests(self):
        yield scrapy.Request(
            url="https://example.org",
            meta=dict(
                playwright=True,
                playwright_page_methods=[
                    PageMethod("screenshot", path="example.png", full_page=True),
                ],
            ),
        )

大致翻译为

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as pw:
        browser = await pw.chromium.launch()
        page = await browser.new_page()
        await page.goto("https://example.org")
        await page.screenshot(path="example.png", full_page=True)
        await browser.close()

asyncio.run(main())

软件版本

请确保包括您使用的 Scrapy、Playwright 和 scrapy-playwright 的版本

$ playwright --version
Version 1.44.0
$ python -c "import scrapy_playwright; print(scrapy_playwright.__version__)"
0.0.34
$ scrapy version -v
Scrapy       : 2.11.1
lxml         : 5.1.0.0
libxml2      : 2.12.3
cssselect    : 1.2.0
parsel       : 1.8.1
w3lib        : 2.1.2
Twisted      : 23.10.0
Python       : 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]
pyOpenSSL    : 24.0.0 (OpenSSL 3.2.1 30 Jan 2024)
cryptography : 42.0.5
Platform     : Linux-6.5.0-35-generic-x86_64-with-glibc2.35

可复现代码示例

在打开问题之前,请包括一个最小化、可复现示例,该示例显示了报告的行为。此外,请尽可能使代码自包含,这样就不需要活动的 Scrapy 项目,并且可以执行爬虫,只需通过 scrapy runspider 从文件中直接执行。这通常意味着在爬虫的custom_settings 属性中包括相关的设置

import scrapy

class ExampleSpider(scrapy.Spider):
    name = "example"
    custom_settings = {
        "TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor",
        "DOWNLOAD_HANDLERS": {
            "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
            "http": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
        },
    }

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

最小化代码

请尽量将代码缩减到最少,同时仍能展示问题。完整项目(包括中间件、管道、项目处理等)实际上很少是重现问题的必要条件。不包含实际调试尝试的报告将不予考虑。

日志和统计数据

显示问题的详细信息的爬虫作业日志非常有用,有助于理解可能的错误。请包括问题和问题之前之后的行,而不仅仅是孤立的跟踪信息。作业结束时显示的作业统计数据也非常重要。

常见问题解答

查看常见问题解答文档

弃用策略

弃用的功能将在它们被弃用后的发布之日起至少支持六个月。之后,它们可能随时被删除。有关弃用和删除的更多信息,请参阅变更日志

项目详情


下载文件

下载适用于您平台的自定义文件。如果您不确定选择哪一个,请了解更多关于安装包的信息。

源代码发行版

scrapy_playwright-0.0.41.tar.gz (47.1 kB 查看散列)

上传日期 源代码

构建发行版

scrapy_playwright-0.0.41-py3-none-any.whl (26.7 kB 查看散列)

上传日期 Python 3

支持者:

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误记录 StatusPage StatusPage 状态页面