in

如何使用 Playwright 和 Python 进行网页爬取

如何使用 Playwright 和 Python 进行网页爬取

Playwright 是一种流行的浏览器自动化工具包,可用于网络爬取以爬取动态网络内容或网络应用程序。 使用 Playwright,我们不需要逆向工程和理解复杂的网络技术,因为浏览器为我们做了一切。这使得 Playwright 成为无需高级 Web 开发知识即可轻松爬取数据的绝佳工具。 在这个深入的实用教程中,我们将了解如何使用 Playwright 和 Python 进行爬取。为此,我们将通过爬取twitch.tv使用示例爬取项目。 我们将讨论常见问题,例如 Playwright 的运作方式以及与竞争对手的比较。如何执行浏览器导航、按钮点击、文本输入和数据解析等常见任务;以及一些高级任务,如 javascript 评估和资源拦截和阻止。

什么是Playwright?

Playwright 是一个跨平台和跨语言的 Web 浏览器自动化工具包。它主要用作网站测试套件,但它完全有能力进行一般的浏览器自动化和网络爬取。 使用 playwright,我们可以使 Firefox 或 Chrome 等网络无头浏览器自动化,就像人类一样浏览网络:转到 URL、单击按钮、编写文本和执行 javascript。 它是一个很好的网络爬取工具,因为它允许爬取动态的 javascript 驱动的网站,而无需对其行为进行逆向工程。它还可以帮助阻止,因为爬取器正在运行一个完整的浏览器,它看起来比独立的 HTTP 请求更人性化。

Playwright vs Selenium vs Puppeteer

与其他流行的浏览器自动化工具包(如 Selenium 或 Puppeteer)相比,Playwright 具有一些优势:

  • Playwright支持多种编程语言,而 Puppeteer 仅在 Javasrcipt 中可用。
  • Playwright 使用 Chrome Devtools Protocol (CDP) 和更现代的 API,而 Selenium 使用webdriver协议和不太现代的 API。
  • Playwright 同时支持异步和同步客户端,而 Selenium 仅支持同步客户端,而 Puppeteer 则支持异步客户端。在 Playwright 中,我们可以使用同步客户端编写小型爬虫,并通过切换到更复杂的异步架构来简单地进行扩展。

换句话说,Playwright 是对 Selenium 和 Puppeteer 的横向改进。但是,每个工具包都有自己的优势。如果您想了解更多信息,请参阅我们的其他介绍文章:

设置

Playwright for Python 可以通过以下方式安装pip

# install playwright package:
$ pip install playwright
# install playwright chrome and firefox browsers
$ playwright install chrome firefox

上面的命令将安装 playwright 包和 playwright 浏览器二进制文件。对于 Playwright 爬取,最好使用 Chrome 或 Firefox 浏览器,因为这些是最稳定的实现,而且通常最不可能被阻止。

提示:REPL 中的编剧

理解 Playwright 最简单的方法是像ipython一样通过 Python REPL(Read、Evaluate、Print、Loop)对其进行实时实验 首先,ipython我们可以启动 playwright 浏览器并实时执行浏览器自动化命令来试验和原型化我们的网络爬取工具:

$ pip install ipython nest_asyncio
$ ipython
import nest_asyncio; nest_asyncio.apply()  # This is needed to use sync API in repl
from playwright.sync_api import sync_playwright
pw = sync_playwright.start()
chrome = pw.chromium.launch(headless=False)
page = chrome.new_page()
page.goto("https://twitch.tv")

以下是我们将通过 REPL 的视角在本文中进行的操作的概览:

通过 iPython REPL Playwright

现在,让我们更详细地看一下这一点。

基础

首先,我们需要启动浏览器并启动一个新的浏览器选项卡:

with sync_playwright() as pw:
    # create browser instance
    browser = pw.chromium.launch(
        # we can choose either a Headful (With GUI) or Headless mode:
        headless=False,
    )
    # create context
    # using context we can define page properties like viewport dimensions
    context = browser.new_context(
        # most common desktop viewport is 1920x1080
        viewport={"width": 1920, "height": 1080}
    )
    # create page aka browser tab which we'll be using to do everything
    page = context.new_page()

一旦我们准备好浏览器页面,我们就可以开始 Playwright 网络爬取,为此我们只需要少数 Playwright 功能:

  • 导航(即转到 URL)
  • 按钮点击
  • 文字输入
  • Javascript 执行
  • 等待内容加载

让我们通过一个真实的例子来了解这些特性。 为此,我们将从twitch.tv艺术部分爬取视频数据,用户可以在其中流式传输他们的艺术创作过程。我们将收集动态数据,例如流名称、观看人数和作者详细信息。 我们在 Playwright 中为此练习的任务是:

  1. 启动浏览器实例、上下文和浏览器选项卡(页面)
  2. 转到twitch.tv/directory/game/Art
  3. 等待页面完全加载
  4. 解析所有活动流的加载页面数据

要导航,我们可以使用page.goto()将浏览器定向到任何 URL 的函数:

with sync_playwright() as pw:
    browser = pw.chromium.launch(headless=False)
    context = browser.new_context(viewport={"width": 1920, "height": 1080})
    page = context.new_page()

    # go to url
    page.goto("https://twitch.tv/directory/game/Art")
    # get HTML
    print(page.content())

然而,对于像 twitch.tv 这样大量使用 javascript 的网站,我们的page.content()代码可能会在加载所有内容之前过早地返回数据。 为确保不会发生这种情况,我们可以等待特定元素出现在页面上。换句话说,如果视频列表出现在页面上,那么我们可以安全地假设页面已经加载:

page.goto("https://twitch.tv/directory/game/Art")
# wait for first result to appear
page.wait_for_selector("div[data-target=directory-first-item]")
# retrieve final HTML content
print(page.content())

上面,我们使用page.wait_for_selector()函数等待我们的 CSS 选择器定义的元素出现在页面上。

解析数据

由于 Playwright 使用带有 javascript 环境的真实网络浏览器,我们可以使用浏览器的 HTML 解析功能。在 Playwright 中,这是通过locators功能实现的:

from playwright.sync_api import sync_playwright

with sync_playwright() as pw:
    browser = pw.chromium.launch(headless=False)
    context = browser.new_context(viewport={"width": 1920, "height": 1080})
    page = context.new_page()

    page.goto("https://twitch.tv/directory/game/Art")  # go to url
    page.wait_for_selector("div[data-target=directory-first-item]")  # wait for content to load

    parsed = []
    stream_boxes = page.locator("//div[contains(@class,'tw-tower')]/div[@data-target]")
    for box in stream_boxes.element_handles():
        parsed.append({
            "title": box.query_selector("h3").inner_text(),
            "url": box.query_selector(".tw-link").get_attribute("href"),
            "username": box.query_selector(".tw-link").inner_text(),
            "viewers": box.query_selector(".tw-media-card-stat").inner_text(),
            # tags are not always present:
            "tags": box.query_selector(".tw-tag").inner_text() if box.query_selector(".tw-tag") else None,
        })
    for video in parsed:
        print(video)

示例输出

{"title": "art", "url": "/lunyatic/videos", "username": "Lunyatic", "viewers": "25 viewers", "tags": "en"}
{"title": "생존신고", "url": "/lilllly1/videos", "username": "생존신고\n\n릴리작가 (lilllly1)", "viewers": "51 viewers", "tags": "한국어"}
{"title": "The day 0914.", "url": "/niai_serie/videos", "username": "The day 0914", "viewers": "187 viewers", "tags": None}
...

在上面的代码中,我们使用 XPath 选择器选择每个结果框,并使用 CSS 选择器从中提取详细信息。 不幸的是,Playwright的解析能力有点笨拙,并且在解析tags我们示例中的字段等可选元素时很容易崩溃。相反,我们可以通过parselbeautifulsoup包使用传统的 Python 解析,它们执行得更快并提供更强大的 API:

...
# using Parsel:
from parsel import Selector

page_html = page.content()

sel = Selector(text=page_html)
parsed = []
for item in sel.xpath("//div[contains(@class,'tw-tower')]/div[@data-target]"):
    parsed.append({
        'title': item.css('h3::text').get(),
        'url': item.css('.tw-link::attr(href)').get(),
        'username': item.css('.tw-link::text').get(),
        'tags': item.css('.tw-tag ::text').getall(),
        'viewers': ''.join(item.css('.tw-media-card-stat::text').re(r'(\d+)')),
    })

# using Beautifulsoup:
from bs4 import BeautifulSoup

soup = BeautifulSoup(page.content())
parsed = []
for item in soup.select(".tw-tower div[data-target]"):
    parsed.append({
        'title': item.select_one('h3').text,
        'url': item.select_one('.tw-link::attr(href)').attrs.get("href"),
        'username': item.select_one('.tw-link').text,
        'tags': [tag.text for tag in item.select('.tw-tag')],
        'viewers': item.select_one('.tw-media-card-stat').text,
    })

虽然Playwright定位器不适合解析,但它们非常适合与网站交互。接下来,让我们看看如何使用定位器单击按钮和输入文本。

单击按钮和文本输入

为了探索点击和文本输入,让我们用搜索功能扩展我们的 twitch.tv 爬取工具:

  1. 我们去twitch.tv
  2. 选择搜索框并输入搜索查询
  3. 单击搜索按钮或按 Enter
  4. 等待内容加载
  5. 解析结果

在 playwright 中与 Web 组件交互,我们可以使用locator我们在解析中使用的相同功能:

with sync_playwright() as pw:
    browser = pw.chromium.launch(headless=False)
    context = browser.new_context(viewport={"width": 1920, "height": 1080})
    page = context.new_page()
    
    page.goto("https://www.twitch.tv/directory/game/Art")
    # find search box and enter our query:
    search_box = page.locator('input[autocomplete="twitch-nav-search"]')
    search_box.type("Painting", delay=100)
    # then, we can either send Enter key:
    search_box.press("Enter")
    # or we can press the search button explicitly:
    search_button = page.locator('button[aria-label="Search Button"]')
    search_button.click()
    # click on tagged channels link:
    page.locator('.search-results .tw-link[href*="all/tags"]').click()

    # Finally, we can parse results like we did before:
    parsed = []
    stream_boxes = page.locator("//div[contains(@class,'tw-tower')]/div[@data-target]")
    for box in stream_boxes.element_handles():
        ...
注意:playwright 的定位器不允许选择器产生多个值。它不知道点击哪一个。意思是,我们的选择器对于我们想要与之交互的元素必须是唯一的。

我们让搜索功能正常工作并提取了结果的第一页,但我们如何获得其余页面呢?为此,我们需要滚动功能 – 让我们来看看它。

滚动和无限分页

twitch.tv 的流结果部分使用无限滚动分页。要在我们的 Playwright 爬取器中检索其余结果,我们需要不断滚动到页面上可见的最后一个结果以触发新页面加载。 我们可以通过滚动到整个页面的底部来做到这一点,但这在无头浏览器中并不总是有效。更好的方法是找到所有元素并将最后一个元素显式滚动到视图中。 scroll_into_view_if_needed()在Playwright中,这可以通过使用定位符和函数来完成。我们将继续将最后一个结果滚动到视图中以触发下一个页面加载,直到不再出现新结果为止:

with sync_playwright() as pw:
    browser = pw.chromium.launch(headless=False)
    context = browser.new_context(viewport={"width": 1920, "height": 1080})
    page = context.new_page()
    page.goto("https://www.twitch.tv/directory/game/Art")
    # wait for content to fully load:
    page.wait_for_selector("div[data-target=directory-first-item]")

    # loop scrolling last element into view until no more new elements are created
    stream_boxes = None
    while True:
        stream_boxes = page.locator("//div[contains(@class,'tw-tower')]/div[@data-target]")
        stream_boxes.element_handles()[-1].scroll_into_view_if_needed()
        items_on_page = len(stream_boxes.element_handles())
        page.wait_for_timeout(2_000) # give some time for new items to load
        items_on_page_after_scroll = len(stream_boxes.element_handles())
        if items_on_page_after_scroll > items_on_page:
            continue  # more items loaded - keep scrolling
        else:
            break  # no more items - break scrolling loop
    # parse data:
    parsed = []
    for box in stream_boxes.element_handles():
        ...

在上面的示例代码中,我们会不断触发新的结果加载,直到分页结束。在这种情况下,我们的代码应该生成数百个解析结果。

进阶功能

我们已经介绍了网络爬取中最常见的Playwright功能:导航、等待、点击、打字和滚动。但是,有一些高级功能可以方便地爬取更复杂的网络爬取目标。

评估 Javascript

Playwright 可以评估当前页面上下文中的任何 javacript 代码。使用 javascript 我们可以做我们以前做过的一切,比如导航、点击和滚动等等!其实这些编剧功能很多都是通过javascript求值来实现的。 例如,如果内置滚动失败,我们可以定义我们自己的滚动 javascript 函数并将其提交给 Playwright:

page.evaluate("""
let items=document.querySelectorAll('.tw-tower>div');
items[items.length-1].scrollIntoView({behavior: "smooth", block: "end", inline: "end"});
""")

上面的代码将像以前一样将最后一个结果滚动到视图中,但它会平滑地滚动到对象的最边缘。与 Playwright 的功能相比,这种做法更容易触发下一页加载scroll_into_view_if_needed。 Javascript 评估是一项强大的功能,可用于爬取复杂的网络应用程序,因为它使我们能够通过 javascript 完全控制浏览器的功能。

请求和响应拦截

Playwright 跟踪浏览器发送和接收的所有后台请求和响应。在网络爬取中,我们可以使用它来修改后台请求或从后台响应中收集秘密数据:

from playwright.sync_api import sync_playwright

def intercept_request(request):
    # we can update requests with custom headers
    if "secret" in request.url :
        request.headers['x-secret-token'] = "123"
        print("patched headers of a secret request")
    # or adjust sent data
    if request.method == "POST":
        request.post_data = "patched"
        print("patched POST request")
    return request

def intercept_response(response):
    # we can extract details from background requests
    if response.request.resource_type == "xhr":
        print(response.headers.get('cookie'))
    return response

with sync_playwright() as pw:
    browser = pw.chromium.launch(headless=False)
    context = browser.new_context(viewport={"width": 1920, "height": 1080})
    page = context.new_page()
    # enable intercepting for this page
    page.on("request", intercept_request)
    page.on("response", intercept_response)

    page.goto("https://www.twitch.tv/directory/game/Art")
    page.wait_for_selector("div[data-target=directory-first-item]")

在上面的示例中,我们定义了我们的拦截器函数并将它们附加到我们的编剧页面。这将允许我们检查和修改浏览器发出的每个后台和前台请求。

阻塞资源

使用无头浏览器进行网络爬取确实是带宽密集型的。浏览器正在下载我们的网络爬取工具不关心的所有图像、字体和其他昂贵的资源。为了优化这一点,我们可以配置我们的 Playwright 实例来阻止这些不必要的资源:

from collections import Counter
from playwright.sync_api import sync_playwright

# block pages by resource type. e.g. image, stylesheet
BLOCK_RESOURCE_TYPES = [
  'beacon',
  'csp_report',
  'font',
  'image',
  'imageset',
  'media',
  'object',
  'texttrack',
#  we can even block stylsheets and scripts though it's not recommended:
# 'stylesheet',
# 'script',  
# 'xhr',
]


# we can also block popular 3rd party resources like tracking:
BLOCK_RESOURCE_NAMES = [
  'adzerk',
  'analytics',
  'cdn.api.twitter',
  'doubleclick',
  'exelator',
  'facebook',
  'fontawesome',
  'google',
  'google-analytics',
  'googletagmanager',
]

def intercept_route(route):
    """intercept all requests and abort blocked ones"""
    if route.request.resource_type in BLOCK_RESOURCE_TYPES:
        print(f'blocking background resource {route.request} blocked type "{route.request.resource_type}"')
        return route.abort()
    if any(key in route.request.url for key in BLOCK_RESOURCE_NAMES):
        print(f"blocking background resource {route.request} blocked name {route.request.url}")
        return route.abort()
    return route.continue_()

with sync_playwright() as pw:
    browser = pw.chromium.launch(
        headless=False, 
        # enable devtools so we can see total resource usage:
        devtools=True, 
    )
    context = browser.new_context(viewport={"width": 1920, "height": 1080})
    page = context.new_page()
    # enable intercepting for this page, **/* stands for all requests
    page.route("**/*", intercept_route)
    page.goto("https://www.twitch.tv/directory/game/Art")
    page.wait_for_selector("div[data-target=directory-first-item]")

在上面的例子中,我们定义了一个拦截规则,告诉 Playwright 丢弃任何不需要的后台资源请求,这些请求要么是被忽略的类型,要么在 URL 中包含被忽略的短语(比如谷歌分析)。 我们可以在 Devtools 的网络选项卡中看到保存的数据量:

devtools 网络选项卡的屏幕截图比较了阻塞和未阻塞的带宽使用情况
通过阻塞,我们使用的流量几乎减少了 4 倍!

避免阻塞

虽然 Playwright 使用的是真实浏览器,但仍然可以确定它是由真实用户控制还是由自动化工具包自动控制。有关这方面的更多信息,请参阅我们涵盖 javascript 指纹识别和变量泄漏的广泛文章:

常问问题

为了总结这个介绍,让我们看一下有关使用 Playwright 进行网页爬取的一些常见问题:

如何使用 Playwright 代理?

我们可以为每个编剧浏览器分配代理 IP 地址:
from playwright.sync_api import sync_playwright

with sync_playwright() as pw:
    browser = pw.chromium.launch(
        headless=False,
        # direct proxy server
        proxy={"server": "11.11.11.1:9000"},
        # or with username/password:
        proxy={"server": "11.11.11.1:9000", "username": "A", "password": "B"},
    )
    page = browser.new_page()

如何加快 Playwright Scraper 的速度?

通过确保无头浏览器阻止图像和媒体的渲染,我们可以使用 Playwright 大大加快爬取速度。这样可以大大降低带宽,加快爬取速度2-5倍!

哪种无头浏览器最适合用于 Playwright Scraping?

在使用 Playwright 进行爬取时,Headless chrome 表现最好。不过,Firefox 通常可以帮助避免阻塞和验证码,因为它是一种不太受欢迎的浏览器。

概括

在这篇深入介绍中,我们了解了如何使用 Playwright 网络浏览器自动化工具包进行网络爬取。我们通过现实生活中的twitch.tv scraper 示例探索了核心功能,例如导航、按钮单击、输入键入和数据解析。 我们还研究了更高级的功能,例如资源阻塞,它可以显着减少我们的浏览器驱动的网络爬取工具的带宽使用。同样的功能也可用于拦截浏览器后台请求以提取 cookie 等详细信息或修改连接。

Written by 河小马

河小马是一位杰出的数字营销行业领袖,广告中国论坛的重要成员,其专业技能涵盖了PPC广告、域名停放、网站开发、联盟营销以及跨境电商咨询等多个领域。作为一位资深程序开发者,他不仅具备强大的技术能力,而且在出海网络营销方面拥有超过13年的经验。