scrapy-playwright

repository·main·Indexed 23 days ago

https://github.com/scrapy-plugins/scrapy-playwright

A Scrapy Download Handler that integrates Playwright for Python, enabling Scrapy spiders to render and interact with JavaScript-heavy websites. It supports browser engine selection (chromium, firefox, webkit), remote browser connection via CDP or Playwright Connect, and advanced page interaction through PageMethod objects and custom initialization callbacks.

Tokens
14.1K
Snippets
34
Records
52
Agent score
81%

What's inside scrapy-playwright

  1. Manage Browser Contexts

    main

    You can define multiple browser contexts at startup using the PLAYWRIGHT_CONTEXTS setting.

    • Default Context: If no context is specified, requests use the default context.
    • Choosing a Context: Use the playwright_context meta key in your scrapy.Request to select a pre-defined context.
    • Creating New Contexts: If a named context does not exist, it will be created on the fly. You can pass configuration for this new context using the playwright_context_kwargs meta key.
    • Limiting Contexts: Use PLAYWRIGHT_MAX_CONTEXTS to limit concurrent contexts and prevent resource exhaustion.
    # Select a pre-defined context
    yield scrapy.Request(
        url="https://example.org",
        meta={"playwright": True, "playwright_context": "first"},
    )
    
    # Create a new context with specific arguments
    yield scrapy.Request(
        url="https://example.org",
        meta={
            "playwright": True,
            "playwright_context": "new",
            "playwright_context_kwargs": {
                "java_script_enabled": False,
                "ignore_https_errors": True,
            },
        },
    )
  2. Extend browser lifecycle with PLAYWRIGHT_BROWSER_PROVIDER

    main

    The PLAYWRIGHT_BROWSER_PROVIDER setting is an extension point that allows you to replace the default browser lifecycle management. By providing a custom class (or an import path string), you can integrate third-party drivers that are Playwright-compatible (e.g., patchright or camoufox) without modifying the Scrapy handler.

    PLAYWRIGHT_BROWSER_PROVIDER = "myproject.providers.CustomBrowserProvider"
  3. Use pluggable browser providers

    main

    You can replace the default Playwright browser startup with third-party projects like patchright, camoufox, or invisible_playwright. These projects provide drop-in replacements that maintain standard Browser, BrowserContext, and Page objects, meaning your existing routing and page code remains unchanged.

    To use a custom provider, set the PLAYWRIGHT_BROWSER_PROVIDER setting to the import path of your provider class or the class itself.

    # settings.py
    PLAYWRIGHT_BROWSER_PROVIDER = "myproject.providers.CustomBrowserProvider"
  4. Configure User-Agent for Playwright requests

    main
    By default, Scrapy sends its own User-Agent. If a website detects a mismatch between the Scrapy User-Agent and the actual browser being used by Playwright, it may block the request. To use the default User-Agent provided by the specific browser, set the Scrapy user agent to None in your settings or request.
  5. Access Playwright Page objects in callbacks

    main

    To access the Playwright Page object in your callback, set playwright_include_page: True.

    Important Requirements:

    1. The callback must be an async def function if you intend to await methods on the Page object.
    2. You must manually close the page using await page.close() when finished, especially in errback handlers. If pages are not closed, you may hit the PLAYWRIGHT_MAX_PAGES_PER_CONTEXT limit, causing the spider to freeze.

    Warning: Any network operations performed directly on the Page object (like page.goto()) bypass Scrapy's middleware and scheduler.

    from playwright.async_api import Page
    import scrapy
    
    class AwesomeSpiderWithPage(scrapy.Spider):
        name = "page_spider"
    
        async def start(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()
  6. Use Persistent Browser Contexts

    main

    To launch a context as persistent, provide a user_data_dir in the PLAYWRIGHT_CONTEXTS setting.

    Warning on Multiple Handlers: If ScrapyPlaywrightDownloadHandler is registered for both http and https, Scrapy creates two independent handler instances. Since both will attempt to open the same user_data_dir simultaneously, it will cause an error.

    To fix this, either:

    1. Make every user_data_dir unique (e.g., using a UUID suffix).
    2. Register the handler for only one scheme (typically https).
    import uuid
    
    # Solution 1: Unique directories to avoid collisions between http/https handlers
    PLAYWRIGHT_CONTEXTS = {
        "persistent": {
            "user_data_dir": f"/path/to/dir-{uuid.uuid4()}",
            "context_arg1": "value",
        },
    }
    
    # Solution 2: Register only for https
    DOWNLOAD_HANDLERS = {
        "https": "scrapy_playwright.handler.ScrapyPlaywrightDownloadHandler",
    }
  7. Install scrapy-playwright

    main

    Install the package via pip. Note that playwright is a dependency and will be installed automatically. You must also manually install the browser binaries (e.g., chromium, firefox) using the Playwright CLI.

    pip install scrapy-playwright
    
    # Install specific browsers
    playwright install firefox chromium
  8. Create a minimal reproducible example for reporting issues

    main

    When reporting issues, provide a self-contained spider that can be run with scrapy runspider. Include the necessary custom_settings to ensure the Twisted reactor and Download Handlers are correctly configured for Playwright.

    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",
            },
        }
    
        async def start(self):
            yield scrapy.Request(
                url="https://example.org",
                meta={"playwright": True},
            )
  9. Close Browser Contexts to Prevent Memory Leaks

    main

    When using dynamic contexts, you must manually close them to avoid memory leaks and hitting the PLAYWRIGHT_MAX_CONTEXTS limit.

    Crucial Order: Always close the Page object before closing its corresponding context.

    Access the context via response.meta['playwright_page'].context and use await context.close().

    async def parse_in_new_context(self, response):
        page = response.meta["playwright_page"]
        title = await page.title()
        await page.close()  # Close page first
        await page.context.close()  # Then close context
        return {"title": title}
    
    async def close_context_on_error(self, failure):
        page = failure.request.meta["playwright_page"]
        await page.close()
        await page.context.close()
  10. Use scrapy-playwright with CrawlSpider

    main

    To use Playwright with a CrawlSpider, you must ensure that requests generated by your crawling rules have the playwright key set to True in their meta attribute. This is achieved by providing a process_request method to your Rule objects that modifies the request in-place.

    def set_playwright_true(request, response):
        request.meta["playwright"] = True
        return request
    
    class MyCrawlSpider(CrawlSpider):
        ...
        rules = (
            Rule(
                link_extractor=LinkExtractor(...),
                callback="parse_item",
                follow=False,
                process_request=set_playwright_true,
            ),
        )
  11. Replace Scrapy memory usage extension with Playwright-aware version

    main

    The default Scrapy MemoryUsage extension does not account for memory used by Playwright because browsers run as separate processes. To track total memory usage including Playwright, replace the built-in extension with scrapy_playwright.memusage.ScrapyPlaywrightMemoryUsageExtension.

    Requirements:

    • You must install the psutil package.
    • Note: This extension does not work on Windows because it relies on the Python resource module.

    Update your settings.py as follows:

    # settings.py
    EXTENSIONS = {
        "scrapy.extensions.memusage.MemoryUsage": None,
        "scrapy_playwright.memusage.ScrapyPlaywrightMemoryUsageExtension": 0,
    }
  12. Download all requests using scrapy-playwright via middleware

    main

    If you want every request in your spider to be processed by Playwright without manually setting meta['playwright'] = True for each one, you can use a Scrapy middleware to inject the required metadata.

    Depending on your needs, you can use either a Spider Middleware or a Downloader Middleware.

    # Spider middleware example
    class PlaywrightSpiderMiddleware:
        def process_spider_output(self, response, result, spider):
            for obj in result:
                if isinstance(obj, scrapy.Request):
                    obj.meta.setdefault("playwright", True)
                yield obj
    
    # Downloader middleware example
    class PlaywrightDownloaderMiddleware:
        def process_request(self, request, spider):
            request.meta.setdefault("playwright", True)
            return None