Playwright的使用

Playwright 是微软在 2020 年年初开源的新一代自动化测试工具,其功能和 Selenium、Pyppeteer 等类似,都可以驱动浏览器进行各种自动化操作。Playwright 对市面上的主流浏览器都提供了支持,API 功能简洁又强大,虽然诞生比较晚,但是现在发展得非常火热。

Playwright的特点

  • Playwright 支持当前所有的主流浏览器,包括 Chrome 和 Edge(基于 Chromium)、Firefox、Safari(基于 WebKit),提供完善的自动化控制的 API。

  • Playwright 支持移动端页面测试,使用设备模拟技术,可以让我们在移动 Web 浏览器中测试响应式的 Web 应用程序。

  • Playwright 支持所有浏览器的无头模式和非无头模式的测试。

  • Playwright 的安装和配置过程非常简单,安装过程中会自动安装对应的浏览器和驱动,不需要额外配置 WebDriver 等。

  • Playwright 提供和自动等待相关的 API,在页面加载时会自动等待对应的节点加载,大大减小了 API 编写的复杂度。

本节我们就来了解下 Playwright 的使用方法。

安装

首先请确保 Python 的版本大于等于 3.7。

要安装 Playwright,可以直接使用 pip3 工具,命令如下:

pip3 install playwright

安装完成后需要进行一些初始化操作:

playwright install

这时 Playwright 会安装 Chromium、Firefox 和 WebKit 浏览器并配置一些驱动,我们不必关心具体的配置过程,Playwright 会自动为我们配置好。

具体的安装说明可以参考 https://setup.scrape.center/playwright。

安装完成后,便可以使用 Playwright 启动 Chromium、Firefox 或 WebKit 浏览器来进行自动化操作了。

基本使用

Playwright 支持两种编写模式,一种是和 Pyppeteer 一样的异步模式,一种是和 Selenium 一样的同步模式,可以根据实际需要选择使用不同的模式。

先来看一个同步模式的例子:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    for browser_type in [p.chromium, p.firefox, p.webkit]:
        browser = browser_type.launch(headless=False)
        page = browser.new_page()
        page.goto('https://www.baidu.com')
        page.screenshot(path=f'screenshot-{browser_type.name}.png')
        print(page.title())
        browser.close()

这里我们首先导入并直接调用了 sync_playwright 方法,该方法的返回值是一个 PlaywrightContextManager 对象,可以理解为一个浏览器上下文管理器,我们将其赋值为 p 变量。然后依次调用 pchromiumfirefoxwebkit 属性创建了 Chromium、Firefox 以及 Webkit 浏览器实例。接着用一个 for 循环依次执行了这 3 个浏览器实例的 launch 方法,同时设置 headless 参数为 False。

如果不把 headless 参数设置为 False,就会默认以无头模式启动浏览器,我们将看不到任 何窗口。

for 循环中,launch 方法返回的是一个 Browser 对象,我们将其赋值为 browser 变量。然后调用 browsernew_page 方法新建了一个选项卡,返回值是一个 Page 对象,将其赋值为 page,这个整个过程其实和 Pyppeteer 非常类似。之后调用 page 的一系列 API 完成了各种自动化操作,调用 goto 方法加载某个页面,这里访问的是百度首页;调用 screenshot 方法获取页面截图,其参数传入的文件名称是截图自动保存后的图片名称,这里的名称中我们加入了 browser_typename 属性,代表浏览器的类型,于是 3 次循环中 screenshot 方法的结果分别是 chromiumfirefoxwebkit。另外,还调用了 title 方法,该方法会返回页面的标题,即 HTML 源码中 title 节点中的文字,也就是选项卡上的文字,并将返回页面标题打印到控制台。最后,调用 browserclose 方法关闭整个浏览器,代码结束。

运行这一段代码,可以看到有 3 个浏览器依次启动,分别是 Chromium、Firefox 和 Webkit 浏览器,启动后都是加载百度首页,页面加载完成后,然后把页面标题打印到控制台,就退出了。

此时,当前目录下会生成 3 个截图文件,图片都是百度首页,文件名中都带有对应浏览器的名称,如图 7-30 所示。

图 7-30 同步模式示例的运行结果

控制台的运行结果如下:

百度一下,你就知道
百度一下,你就知道
百度一下,你就知道

可以发现,我们非常方便地启动了三种浏览器,完成了自动化操作,并通过几个 API 就获取了页面的截图和数据,整个过程速度非常快,这就是 Playwright 最为基本的用法。

当然,除了同步模式,Playwright 还提供了支持异步模式的 API,如果我们的项目里面使用了 asyncio 关键字,就应该使用异步模式,写法如下:

import asyncio
from playwright.async_api import async_playwright

async def main():
    async with async_playwright() as p:
        for browser_type in [p.chromium, p.firefox, p.webkit]:
            browser = await browser_type.launch()
            page = await browser.new_page()
            await page.goto('https://www.baidu.com')
            await page.screenshot(path=f'screenshot-{browser_type.name}.png')
            print(await page.title())
            await browser.close()

asyncio.run(main())

可以看到,写法和同步模式基本一样,只不过这里导人的是 async_playwright 方法,不再是 sync_playwright 方法,以及写法上添加了 async/await 关键字,最后的运行效果和同步模式是一样的。

另外可以注意到,这个例子中使用了 with as 语句,with 用于管理上下文对象,可以返回一个上下文管理器,即一个 PlaywrightContextManager 对象,无论代码运行期间是否抛出异常,该对象都能帮助我们自动分配并且释放 Playwright 的资源。

代码生成

Playwright 还有一个强大的功能,是可以录制我们在浏览器中的操作并自动生成代码,有了这个功能,我们甚至一行代码都不用写。这个功能可以通过 playwright 命令行调用 codegen 实现,先来看看 codegen 命令都有什么参数,输入如下命令:

playwright codegen --help

结果类似如下:

Usage: npx playwright codegen [options] [url]

open page and generate code for user actions

Options:

-o, --output <file name> saves the generated script to a file
--target <language> language to use, one of javascript, python, python-async, csharp (default: "python")
-b, --browser <browserType> browser to use, one of cr, chromium, ff, firefox, wk, webkit (default: "chromium")
--channel <channel> Chromium viewport channel, "chrome", "chrome-beta", "msedge-dev", etc
--color-scheme <scheme> emulate preferred color scheme, "light" or "dark"
--device <deviceName> emulate device, for example "iPhone 11"
--geolocation <coordinates> specify geolocation coordinates, for example "37.819722,-122.478611"
--load-storage <filename> load context storage state from the file, previously saved with --save-storage
--lang <language> specify language / locale, for example "en-GB"
--proxy-server <proxy> specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"
--save-storage <filename> save context storage state at the end, for later use with --load-storage
--timezone <time zone> time zone to emulate, for example "Europe/Rome"
--timeout <timeout> timeout for Playwright actions in milliseconds (default: "10000")
--user-agent <ua string> specify user agent string
--viewport-size <size> specify browser viewport size in pixels, for example "1280, 720"
-h, --help display help for command

Examples:

$ codegen
$ codegen --target=python
$ codegen -b webkit https://example.com

可以看到结果中有几个选项,-o 代表输出的代码文件的名称;-target 代表使用的语言,默认是 python,代表会生成同步模式的操作代码,如果传入 python-async 则会生成异步模式的代码;-b 代表使用的浏览器,默认是 chromium。还有很多其他设置,例如 -device 可以模拟使用手机浏览器(如 iPhone 11),-lang 代表设置浏览器的语言,-timeout 可以设置页面加载的超时时间。

了解了这些用法后,我们来尝试启动一个 Firefox 浏览器,然后将操作结果输出到 script.py 文件,命令如下:

playwright codegen -o script.py-b firefox

运行代码后会弹出一个 Firefox 浏览器,同时右侧输出一个脚本窗口,实时显示当前操作对应的代码。我们可以在浏览器中随意操作,例如打开百度,点击搜索框并输入 nba,再点击搜索按钮,这时的览器窗口如图 7-31 所示。

图7-31 运行结果

可以看到,浏览器中会高亮显示我们正在操作的页面节点,同时显示对应的选择器字符串 input[name="wd"],右侧的代码窗口如图 7-32 所示。

图7-32 代码窗口

在操作浏览器的过程中,该窗口中的代码会跟着实时变化,现在这里已经生成了刚刚一系列操作对应的代码,例如:

page.fill("input[name=\"wd\"]", "nba")

这行代码就对应在搜索框中输入 nba 的操作。所有操作完毕之后,关闭浏览器,Playwright 会生成一个 script.py 文件,内容如下:

from playwright.sync_api import sync_playwright

def run(playwright):
    browser = playwright.firefox.launch(headless=False)
    context = browser.new_context()

    # 打开新页面
    page = context.new_page()

    # 访问 https://www.baidu.com/
    page.goto("https://www.baidu.com/")

    # 点击搜索框
    page.click("input[name=\"wd\"]")

    # 往搜索框中输入文字
    page.fill("input[name=\"wd\"]", "nba")

    # 点击搜索按钮
    with page.expect_navigation():
        page.click("text=百度一下")

    context.close()
    browser.close()

with sync_playwright() as playwright:
    run(playwright)

可以看到这里生成的代码和我们之前写的示例代码几乎差不多,而且也是可以运行的,运行之后会看到它在复现我们刚才所做的操作。

所以,有了代码生成功能,只通过简单的可视化点击操作就能生成代码,可谓非常方便!

另外这里有一个值得注意的点,仔细观察一下生成的代码,和前面例子不同的是,这里的 new_page 方法并不是直接通过 browser 调用的,而是通过 context,这个 context 又是由 browser 调用 new_context 方法生成的。有朋友可能会问,这个 context 究竟是做什么的呢?

其实,context 变量是一个 BrowserContext 对象,这是一个类似隐身模式的独立上下文环境,其运行资源是单独隔离的。在一些自动化测试过程中,我们可以为每个测试用例单独创建一个 BrowserContext 对象,这样能够保证各个测试用例互不干扰,具体的 API 可以参考 https://playwright.dev/python/docs/api/class-browsercontext

支持移动端浏览器

Playwright 的另一个特色就是支持模拟移动端浏览器,例如模拟打开 iPhone 12 Pro Max 上的 Safari 浏览器。

示代码如下:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    iphone_12_pro_max = p.devices['iPhone 12 Pro Max']
    browser = p.webkit.launch(headless=False)
    context = browser.new_context(
        **iphone_12_pro_max,
        locale='zh-CN'
    )
    page = context.new_page()
    page.goto('https://www.whatismybrowser.com/')
    page.wait_for_load_state(state='networkidle')
    page.screenshot(path='browser-iphone.png')
    browser.close()

这里我们先用 PlaywrightContextManager 对象的 devices 属性指定了一台移动设备,传入的参数是移动设备的型号,例如 iPhone 12 Pro Max,当然也可以传入其他内容,例如 iPhone 8、Pixel 2 等。

前面我们已经了解了 BrowserContext 对象,它也可以用来模拟移动端浏览器,初始化一些移动设备信息、语言、权限、位置等内容。这里我们就创建了一个移动端 BrowserContext 对象,最后把返回的 BrowserContext 对象赋值给 context 变量。

接着,我们调用 contextnew_page 方法创建了一个新的选项卡,然后跳转到一个用于获取浏览器信息的网站,调用 wait_for_load_state 方法等待页面的某个状态完成,这里我们传入的 statenetworkidle,也就是网络空闲状态。因为在页面初始化和数据加载过程中,肯定有网络请求伴随产生,所以加载肯定不算 networkidle 状态,意味着这里传入 networkidle 可以标识当前页面初始化和数据加载完成的状态。加载完成后,我们调用 screenshot 方法获取了当前的页面截图,最后关闭了浏览器。

运行一下代码,可以发现弹出了一个移动版浏览器,然后加载出了对应的页面。如图7-33所示。

图7-33 当前的页面截图

输出的截图也是浏览器中显示的结果,可以看到,这里显示的浏览器信息是 iPhone 上的 Safari 浏览器,也就是说我们成功模拟了一个移动端浏览器。

这样我们就成功模拟了移动端浏览器并做了一些设置,其操作 API 和 PC 版浏览器是完全一样的。

选择器

不知道大家有没有注意,前面的 clickfill 等方法都有一个字符串类型的参数,这些字符串有的符合 CSS 选择器的语法,有的以 text= 开头,似乎不大有规律,那么它们到底支持怎样的匹配规则呢?下面就一起来了解一下。

我们可以把传入的字符串称为 Element Selector,除了它已经支持的 CSS 选择器、XPath,Playwright 还为它扩展了一些方便好用的规则,例如直接根据文本内容筛选、根据节点层级结构筛选等。

文本选择

文本选择支持直接使用 text= 这样的语法进行筛选,示例如下:

page.click("text=Login")

这代表选择并点击文本内容是 Login 的节点。

CSS 选择器

CSS 选择器在 3.3 节就介绍过,例如根据 id 或者 class 筛选:

page.click("button")
page.click("#nav-bar.contact-us-item")

根据特定的节点属性筛选:

page.click("[data-test=login-button]")
page.click("[aria-label='Sign in']")

CSS选择器+文本值

可以使用 CSS 选择器结合文本值的方式进行筛选,比较常用的方法是 has-texttext,前者代表节点中包含指定的字符串,后者代表节点中的文本值和指定的字符串完全匹配,示例如下:

page.click("article:has-text('Playwright')")
page.click("#nav-bar :text('Contact us')")

第一行代码就是选择文本值中包含 Playwright 字符串的 article 节点,第二行代码是选择 idnav-bar 的节点中文本值为 Contact us 的节点。

CSS选择器+节点关系

CSS 选择器还可以结合节点关系来筛选节点,例如使用 has 指定另外一个选择器,示例如下:

page.click(".item-description:has(.item-promo-banner)")

这里选择的就是 classitem-description 的节点,且该节点还要包含 classitem-promo-banner 的子节点。

另外还可以结合一些相对位置关系,例如使用 right-of 指定位于某个节点右侧的节点,示例如下:

page.click("input:right-of(:text('Username'))")

这里选择的就是一个 input 节点,并且该节点要位于文本值为 Username 的节点的右侧。

XPath

当然,XPath 也是支持的,不过 xpath 这个关键字需要我们自行指定,示例如下:

page.click("xpath=//button")

这里在开头指定 “xpath=字符串”,代表这个字符串是一个 XPath 表达式。

更多关于选择器的用法和最佳实践,可以参考官方文档 https://playwright.dev/python/docs/selectors

常用操作方法

上面我们了解了浏览器的初始化设置和基本的操作实例,下面再介绍一些常用的操作方法。例如 click(点击),fill(输入)等,这些方法都属于 Page 对象,所以所有的方法都可以从 Page 对象的 API 文档查找,文档地址是 https://playwright.dev/python/docs/api/class-page

下面介绍儿个常见操作方法的用法。

事件监听

Page 对象提供一个 on 方法,用来监听页面中发生的各个事件,例如 closeconsoleloadrequestresponse 等。

这里我们监听 response 事件,在每次网络请求得到响应的时候会触发这个事件,我们可以设置回调方法来获取响应中的全部信息,示例如下:

from playwright.sync_api import sync_playwright

def on_response(response):
    print(f'Status {response.status}: {response.url}')

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.on('response', on_response)
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    browser.close()

我们在创建 Page 对象后,就开始监听 response 事件,同时将回调方法设置为 on_responseon_response 接收一个参数,然后输出响应中的状态码和链接。

运行上述代码后,可以看到控制台输出如下结果:

Statue 200: https://spa6.scrape.center/
Statue 200: https://spa6.scrape.center/css/app.ea9d802a.css
Statue 200: https://spa6.scrape.center/js/app.5ef0d454.js
Statue 200: https://spa6.scrape.center/js/chunk-vendors.77daf991.js
Statue 200: https://spa6.scrape.center/css/chunk-19c920f8.2a6496e0.css
...
Statue 200: https://spa6.scrape.center/css/chunk-19c920f8.2a6496e0.css
Statue 200: https://spa6.scrape.center/js/chunk-19c920f8.c3a1129d.js
Statue 200: https://spa6.scrape.center/img/logo.a508a8f0.png
Statue 200: https://spa6.scrape.center/fonts/element-icons.535877f5.woff
Statue 301: https://spa6.scrape.center/api/movie?limit=10&offset=0&token=NCMWMzFhNGEzMTFjMzJKOGE0ZTQ1YjUz
MTC20WNIYTIyKZDM3MSwxNjIyOTE4NTE5
Statue 200: https://spa6.scrape.center/api/movie?limit=10&offset=0&token=NCMWMzFhNGEzMTFjMzJKOGE0ZTQ1YjUz
MTC20WNIYTIyKZDM3MSwxNjIyOTE4NTE5
Statue 200: https://p0.meituan.net/movie/da64660f82b98cdc1b8a3804e69609e041108.jpg@464w_644h_1e_1c
Statue 200: https://p0.meituan.net/movie/283292171619cdfd5b240c8fd093f1eb255670.jpg@464w_644h_1e_1c
....
Statue 200: https://p1.meituan.net/movie/b607fba7513e7f15eab170aac1e14000d878112.jpg@464w_644h_1e_1c

注意这里省略了部分重复的内容。

可以发现,这个输出结果其实正好对应浏览器 Network 面板中的所有请求和响应,和图 7-34 里的内容是一对应的。

图7-34 浏览器的 Network 面板

我们之前分析过这个网站,其真实的数据都是 Ajax 加载的,同时 Ajax 请求中还带有加密参数,不好轻易获取。但有了 on_response 方法,如果我们想截获 Ajax 请求,岂不是就非常容易了?改写一下这里的判定条件,输出对应的 JSON 结果,代码如下:

from playwright.sync_api import sync_playwright

def on_response(response):
    if '/api/movie/' in response.url and response.status == 200:
        print(response.json())

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.on('response', on_response)
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    browser.close()

控制台的输出结果如下:

{'count': 100, 'results': [{'id': 1, 'name': '霸王别姬', 'alias': 'Farewell My Concubine', 'cover':
'https://p0.meituan.net/movie/ce4da3e03e65b0588ed3b19cd7896cf62472.jpg@464w_644h_1e_1c', 'categories':
['剧情', '爱情'], 'published_at': '1993-07-26', 'minute': 171, 'score': 9.5, 'regions': ['中国大陆',
'中国香港']},

'published_at': None, 'minute': 103, 'score': 9.0, 'regions': ['美国']}, {'id': 10, 'name': '狮子王', 'alias':
'The Lion King', 'cover':
'https://p0.meituan.net/movie/27b76fe6cf3903f3d74963f70786001e1438406.jpg@464w_644h_1e_1c', 'categories':
['动画', '歌舞', '冒险'], 'published_at': '1995-07-15', 'minute': 89, 'score': 9.0, 'regions': ['美国']}]}

简直是得来全不费工夫,我们通过 on_response 方法拦截了 Ajax 请求,直接拿到了响应结果,即使这个 Ajax 请求中有加密参数,也不用担心,因为我们截获的是最后的响应结果,让数据爬取方便太多了。

其他的事件监听,这里就不再一一介绍了,可以查阅官方文档。

获取页面源代码

获取页面源代码的过程其实很简单,直接调用 Page 对象的 content 方法就行,用法如下:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    html = page.content()
    print(html)
    browser.close()

运行结果就是页面源代码。获取了页面源代码之后,借助一些解析工具就可以提取想要的信息了。

页面点击

实现页面点击的方法,我们已经不生了,就是 click 方法,这里详细介绍一下这个方法如何使用。click 方法的 API 定义如下:

page.click(selector, **kwargs)

可以看到,必须传入的参数是 selector,其他参数都是可选的。selector 代表选择器,用来匹配想要点击的节点,如果有多个节点和传入的选择器相匹配,那么只使用第一个节点。

其他一些比较重要的参数如下。

  • click_count:点击次数,默认为 1。

  • timeout:等待找到要点击的节点的超时时间(单位为秒),默认是 30。

  • position:需要传入一个字典,带有 x 属性和 y 属性,代表点击位置相对节点左上角的偏移量。

  • force:即使按钮设置了不可点击,也要强制点击,默认是 False。

click 方法的内部执行逻辑如下。

  • 找到与 selector 匹配的节点,如果没有找到,就一直等待直到超时,超时时间由 timeout 参数设置。

  • 检查匹配到的节点是否存在可操作性,等待检查结果,如果某个按钮设置了不可点击,就等该按钮变成可点击的时候再去点击,除非通过 force 参数设置了跳过可操作性检查的步骤,才会强制点击。

  • 如果有需要,就滚动一下页面,使需要点击的节点呈现出来。

  • 调用 Page 对象的 mouse 方法,点击节点的中心位置,如果指定了 position 参数,就点击参数指定的位置。

具体的参数设置可以参考官方文档 https://playwright.dev/python/docs/api/class-page/#pageclickselector-kwargs

文本输入

文本输入对应的方法是 fill,其 API 定义如下:

page.fill(selector, value, **kwargs)

这个方法有两个必传参数,第一个也是 selector,依然代表选择器;第二个是 value,代表输入的文本内容;还可以通过 timeout 参数指定查找对应节点的最长等待时间。

获取节点属性

除了操作节点本身,我们还可以获取节点的属性,方法是 get_attribute,其 API 定义如下:

page.get_attribute(selector, name, **kwargs)

这个方法有两个必传参数,第一个还是 selector;第二个是 name,代表要获取的属性的名称;还可以通过 timeout 参数指定查找对应节点的最长等待时间。示例如下:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    href = page.get_attribute('a.name', 'href')
    print(href)
    browser.close()

这里我们调用了 get_attribute 方法,传入的 selector 参数值是 a.name,代表查找 classnamea 节点,name 参数值传入了 href,代表获取超链接的内容,输出结果如下:

/detail/ZWYZNzcNOZXvXMGJodWFjJKc01N3kxcCTVVnSoTakaA50HnS2Z1tbiHmelMqLSFpLTAtbWix

可以看到获取了对应节点的 href 属性,但只有一条结果,这是因为如果传入的选择器匹配了多个节点,就只会用第一个。那么怎样获取所有匹配到的节点呢?

获取多个节点

使用 query_selector_all 方法可以获取所有节点,它会返回节点列表,通过遍历得到其中的单个节点后,可以接着调用上面介绍的针对单个节点的方法完成一些操作和获取属性,示例如下:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    elements = page.query_selector_all('a.name')
    for element in elements:
        print(element.get_attribute('href'))
        print(element.text_content())
    browser.close()

这里通过 query_selector_all 方法获取了所有匹配到的节点,每个节点各对应一个 ElementHandle 对象,可以调用 ElementHandle 对象的 get_attribute 方法获取节点属性,也可以通过 text_content 方法获取节点文本。

运行结果如下:

/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWix
霸王别姬 - Farewell My Concubine
/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWiy
这个杀手不太冷 - Léon
/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWiz
肖申克的救赎 - The Shawshank Redemption
/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWio
泰坦尼克号 - Titanic
/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWii
罗马假日 - Roman Holiday
/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWi2
唐伯虎点秋香 - Flirting Scholar
/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWi3
乱世佳人 - Gone with the Wind
/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWi4
喜剧之王 - The King of Comedy
/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWi5
楚门的世界 - The Truman Show
/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWiXMA==
狮子王 - The Lion King

获取单个节点

获取单个节点也有特定的方法,就是 query_selector,如果传入的选择器匹配到多个节点,那它只会返回第一个,示例如下:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()
    page.goto('https://spa6.scrape.center/')
    page.wait_for_load_state('networkidle')
    element = page.query_selector('a.name')
    print(element.get_attribute('href'))
    print(element.text_content())
    browser.close()

运行结果如下:

/detail/ZWYZNzcNOZXvXMGJodWEjJKc01N3kxcCTVVnSoTakaA50HhS2Z21tbHmelMqLSFpLTAtbWix 霸王别姬 - Farewell My Concubine

可以看到这里只输出了第一个节点的信息。

网络劫持

再介绍一个实用的方法—— route,利用这个方法可以实现网络劫持和修改操作,例如修改 request 的属性,修改响应结果等。来看一个实例:

from playwright.sync_api import sync_playwright
import re

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    def cancel_request(route, request):
        route.abort()

    page.route(re.compile(r"(\.png)|(\.jpg)"), cancel_request)
    page.goto("https://spa6.scrape.center/")
    page.wait_for_load_state('networkidle')
    page.screenshot(path='no_picture.png')
    browser.close()

这里我们调用了 route 方法,第一个参数通过正则表达式传入了 URL 路径,这里的 (\.png)|(\.jpg) 代表所有包含 .png.jpg 的链接,遇到这样的请求,会回调 cancel_request 方法做处理。

cancel_request 方法接收两个参数,一个是 route,代表一个 CallableRoute 对象;另一个是 request,代表 Request 对象。这里我们直接调用 CallableRoute 对象的 abort 方法,取消了这次请求,导致最终的结果是取消全部图片的加载。

观察下运行结果,如图 7-35 所示,可以看到图片全都加载失败了。

图7-35 图片全部加载失败

也许有人会说这个设置看起来没什么用啊?其实是有用的,图片资源都是二进制文件,我们在爬取过程中可能并不想关心具体的二进制文件内容,而只关心图片的 URL 是什么,所以浏览器中是否把图片加载出来就不重要了,如此设置可以提高整个页面的加载速度,提高爬取效率。

另外,利用这个功能,还可以对一些响应内容进行修改,例如直接将响应结果修改为自定义的文本内容。

这里首先定义一个 HTML 文本文件,命名为 custom_response.html,内容如下:

<!DOCTYPE html>
<html>
    <head>
        <title>Hack Response</title>
    </head>
    <body>
        <h1>Hack Response</h1>
    </body>
</html>

代码编写如下:

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.launch(headless=False)
    page = browser.new_page()

    def modify_response(route, request):
        route.fulfill(path="./custom_response.html")

    page.route('/', modify_response)
    page.goto("https://spa6.scrape.center/")
    page.wait_for_load_state('networkidle')
    browser.close()

这里我们使用 CallableRoute 对象的 fulfill 方法指定了一个本地文件,就是刚才我们定义的 HTML 文件,运行结果如图 7-36 所示。

图7-36 修改响应结果后的代码运行结果

可以看到,响应结果已经被我们修改了,URL 依然不变,但结果已经变成我们修改后的 HTML 代码。所以通过 route 方法,我们可以灵活地控制请求和响应的内容,从而在某些场景下达成某些目的。

总结

本节介绍了 Playwright 的基本用法,其 API 强大又易于使用,同时具备很多Selenium、Pyppeteer 不具备的更好用的 API,是新一代爬取 JavaScript 渲染页面的利器。