Playwright 的特点
支持当前所有主流浏览器,包括 Chrome 和 Edge (基于 Chromiuns), Firefox , Safari
支持移动端页面测试,使用设备模拟技术,可以让我们在移动Web 浏览器中测试响应式的 Web 应用程序
支持所有浏览器的无头模式和非无头模式的测试
安装和配置过程简单,安装过程中会自动安装对应的浏览器和驱动,不需要额外配置 WebDriber
提供和自动等待相关的API , 在页面加载时会自动等待对应的节点加载,大大减小了API 编写的复杂的
安装
1. 首先确保 python 版本大于 3.7
pip install playwright
这时,playwright 会安装 Chromium , Firefox 和 WebKit 浏览器并配置一些驱动
基本使用
Playwright 支持两种模式,同步 和 异步 根据需要选择不同的模式
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 方法, 该方法的返回值是一个 PlaywrightContextManger 对象,可以理解为一个浏览器上下文管理器,我们将其配置为 p 变量。然后依次调用 p 的 chromium, firefox 和 webkit 属性创建了 Chromium, Firefox 以及 webkit 浏览器实例。接着用一个 for 循环依次执行了这 3 个浏览器实例的 launch 方法, 同时设置了 headless 参数为 False
如果不把 headless 设置为 False ,就会默认以无头模式打开浏览器,我们将看不到任何窗口
在 for 循环中, launch 方法返回一个 Browser 对象, 我们将其赋值为 browser 变量。 然后调用 browseer 的 new_page 方法新建了一个选项卡, 返回值是一个 Page 对象, 将其赋值为 page , ,之后调用 page 的一系列API 完成了各种自动化操作,调用 goto 方法加载某个页面,这里我们访问的是百度首页, 调用 screenshot 方法获取页面截图, 往其里面传入的文件名称是截图自动保存后的图片名称, 这里的名称中我们加入了 browser_type 的 name 属性, 代表浏览器的类型,于是 3 次循环中 screenshot 方法的结果分别是 chromium , firefox , 和 webkit 另外,还调用了 title 方法,该方法会返回页面的标题, 即 HTML 源代码中 title 节点的文字, 也就是选项卡上的文字,并将返回的页面标题打印到控制台, 最后,调用 browser 的 close 方法关闭整个浏览器,代码结束
上面演示的是同步模式,Playwright 还支持异步模式
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 用于管理上下文对象,可以返回一个上下文管理器,寄一个 PlaywrightContextManger 对象,无论代码运行期间是否抛出异常,该对象都能帮助我们自动分配并且释放 Playwright 的资源
这里并没有传入 headless 为 True 所以没有显示浏览器页面,也就是用的是无头模式
代码生成
Playwright 还有一个强大的功能,是可以录制我们在浏览器中的操作并自动生成代码,有了这个功能,我们甚至一行代码都不用写。这个功能可以通过 playwright 命令行调用 codegen 实现,先来看看 codegen 有什么参数
在终端输入命令如下;
playwright codegen --help
结果类似如果下:
Usage: 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 generate, one of javascript, playwright-test, python, python-async,
python-pytest, csharp, csharp-mstest, csharp-nunit, java, java-junit (default:
"python")
--save-trace <filename> record a trace for the session and save it to a file
--test-id-attribute <attributeName> use the specified attribute to generate data test ID selectors
-b, --browser <browserType> browser to use, one of cr, chromium, ff, firefox, wk, webkit (default:
"chromium")
--block-service-workers block service workers
--channel <channel> Chromium distribution 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"
--ignore-https-errors ignore https errors
--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"
--proxy-bypass <bypass> comma-separated domains to bypass proxy, for example
".com,chromium.org,.domain.com"
--save-har <filename> save HAR file with all network activity at the end
--save-har-glob <glob pattern> filter entries in the HAR by matching url against this glob pattern
--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, no timeout by default
--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 commandExamples:
$ codegen
$ codegen --target=python
$ codegen -b webkit https://example.com
说明:
1. 这里是命令行,所以要在终端 (cmd) 输入
2. 这里输入命令是配置了环境变量的, 如果没有配置环境变量则需要加上全路径
例如我的在:
C:\Users\86151\AppData\Roaming\Python\Python311\Scripts
里面有
主要是找到自己的 python 环境, 然后找到 Scripts 文件夹
可以看到结果中有几个选项, -o 代表输出的代码文件名称, -target 代表使用的语言, 默认是 python , 代表会生成同步模式的代码, 如果传入 pythhon-async 则会生成异步模式的代码, -b 代表使用的浏览器, 默认是 chromium 还有很多其他设置, 例如 -device 可以模拟使用手机浏览器(如 iPthon 11), -lang 代表设置浏览器的语言, -timeout 可以设置页面加载超时时间
playwright codegen -o script.py -b firefox
运行代码后会弹出一个 Firefox 浏览器, 同时右侧输出一个脚本框,实时显示当前操作对应的代码,我们可以在浏览器随意操作,例如打开百度,点击搜索框输入 nba, 在点击搜索按钮
这里看到随着我们的操作,右侧也生成了对应的代码,操作完成之后,关闭浏览器,playwright 会生成一 script.py 的文件, 前面我们自定义的,这个文件的位置,取决于你执行命令时的位置
这个文件我们也是可以执行的,执行之后,它会重复我们刚刚的操作。这里哦们可以发现,自动生成的代码和我们写的是有些区别的,例如:这里的 new_page 方法并不是直接通过 browser 调用的,而是通过 context ,这个 context 又是由 browser 调用 new_context 方法生成的,这个context 变量其实是一个 BrowserContext 对象, 这是一个类似隐身模式的独立上下文环境, 其运行资源是单独隔离的, 在一些自动化测试过程中, 我们可以为每个测试用例单独创建一个 BrowserContext 对象, 这样能够保证各个测试用例互不干扰,就提API参考:
https://playwright.dev/python/docs/api/class-browsercontext
支持移动端浏览器
模拟移动端浏览器
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()
这里我们先用 PlaywrightContextManger 对象的 devices 属性指定了一台移动设备,传入的参数是移动设备的型号, 例如 iPhone 12 Pro Max ,当然也可以传入其他内容,例如 iPhone 8 ....
前面我们已经了解了 BrowserContext 对象, 它可以用来模拟移动端浏览器, 初始化一些移动设备的信息,语言,权限,位置等内容, 这里我们就创建了一个移动端 BrowserContext 对象, 最后把返回的 BrowserContext 对象赋值给了 context 变量
接着我们调用 context 的 new_page 方法创建了一个新的选项卡, 然后跳转到一个用于获取浏览器信息的网站, 调用 wait_for_load_state 方法等待页面的某个状态完成, 这里我们传入的 state 是 networkidle , 也就是网络空闲状态, 因为在页面初始化和数据加载过程中,肯定有网络请求伴随产生,所以加载过程肯定不算 networkle 状态,意味着这里传入 networkidle 可以标识当前页面初始化和数据加载完成的状态, 加载完成之后, 我们调用 screenshot 方法获取了当前页面的截图,最后关闭了浏览器
选择器
文本选择器
文本选择器支持用 text= 这样的语法进行筛选
page.click('text-log in ')
这里代表选择并点击文本的内容是 Log in 的节点
CSS 选择器
例如根据 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-text 和 text ,前者代表节点中包含指定的字符串, 后者代表节点中的文本值和指定的字符串完全匹配
page.click("article:has-text('Playwright')")
page.click("#nav-bar : text(' Contact us')")
第一行代码就是选择选择文本值中包含 Playwrght 字符串的 article 节点, 第二行代码是选择 id 为 nav-bar 的节点中文本值为 Contact us 的节点
CSS 选择器 +节点关系
CSS 选择器还可以结合节点关系来筛选节点,例如使用 has 指定另外一个选择器,
page.click(".item-description:has(.item-promo-banner)")
这里选择的就是class 为 item-description 的节点, 且该节点还要包含 class 为 item-promo-banner 的子节点
另外还可以结合一些相对位置关系,例如使用 right-of 指定位于某个节点右侧的节点
page.click("input:right:right-of(:text('Username'))")
这里选择的就是一个 imput 节点, 并且该节点要求位于文本值为 Username 的节点的右侧
XPath
当然 XPath 也是支持的, 不过 xpath 这个关键字需要我们自行指定
page.click("xpath=//button")
这里开头指定 xpath= 字符串 , 代表这个字符串是一个 XPath 表达式
更多关于选择器的用法和实践可以关注官网:
https://playwright.dev/python/docs/api/class-page
常用操作方法
例如: click(点击), fill(输入)等
文档地址:
https://playwright.dev/python/docs/api/class-page
事件监听
page 对象提供一个 on 方法, 用来监听页面中发生的各个事件, 例如 close, console, load, request, response 等
这里我们监听 response 事件 , 在每次网络请求得到响应的时候会出发这个事件,我们可以设置回调方法来获取响应中的全部信息
from playwright.sync_api import sync_playwright def on_response(response): print(f"Statue {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()
Statue 200: https://spa6.scrape.center/
Statue 200: https://spa6.scrape.center/js/chunk-19c920f8.c3a1129d.js
Statue 200: https://spa6.scrape.center/css/chunk-19c920f8.2a6496e0.css
Statue 200: https://spa6.scrape.center/css/chunk-2f73b8f3.5b462e16.css
Statue 200: https://spa6.scrape.center/js/chunk-vendors.77daf991.js
Statue 200: https://spa6.scrape.center/css/app.ea9d802a.css
Statue 200: https://spa6.scrape.center/js/chunk-4dec7ef0.e4c2b130.js
Statue 200: https://spa6.scrape.center/js/app.5ef0d454.js
Statue 200: https://spa6.scrape.center/js/chunk-2f73b8f3.8f2fc3cd.js
这里省略了一部分输出内容
我们在创建 page 对象后, 就开始监听 response 事件, 同时将回调函数方法设置为 on_response
on_response 接收一个参数,然后输出响应中的状态码和链接
可以发现输出结果其实正好对应浏览器 Network 面板中的所有请求和响应
这个网站真实的数据都是 Ajax 加载的, 同时 Ajax 请求中还带有加密参数, 不好轻易获取, 但有了 on_response 方法, 如果我们想截获 Ajax 请求,岂不是很容易
改写一下这里的判定条件, 输出对应的 JSON 结果
from playwright.sync_api import sync_playwright # 通过事件监听来获取 Ajax 加密的JSON 内容 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')
{'count': 103, 'results': [{'id': 1, 'name': '霸王别姬', 'alias': 'Farewell My Concubine', 'cover': 'https://p0.meituan.net/movie/ce4da3e03e655b5b88ed31b5cd7896cf62472.jpg@464w_644h_1e_1c', 'categories': ['剧情', '爱情'], 'published_at': '1993-07-26', 'minute': 171, 'score': 9.5, 'regions': ['中国内地', '中国香港']}, {'id': 2, 'name': '这个杀手不太冷', 'alias': 'Léon', 'cover': 'https://p0.meituan.net/movie/27b76fe6cf3903f3d74963f70786001e1438406.jpg@464w_644h_1e_1c', 'categories': ['动画', '歌舞', '冒险'], 'published_at': '1995-07-15', 'minute': 89, 'score': 9.0, 'regions': ['美国']}]}
这里省略了部分内容
可以看出来,相对来说简单了很多
获取页面源代码
获取页面源代码其实很简单,直接调用 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 方法
page.click(selector, **kwargs)
可以看到必须传入的参数是 selector , 其他参数都是可选的。 selector 代表选择器,用来匹配想要点击的节点,如果有多个节点和传入的选择器匹配, 那么只使用第一个节点
其他一些比较重要的参数如下
click_count :点击次数,默认为1
timeout: 等待找到要点击得劲节点的超时时间(单位为秒),默认是 30
psition: 需要传入一个字典,带有 x 属性和 y 属性, 代表点击位置相对节点左上角的偏移量
force : 即使按钮设置了不可点击, 也要强制点击, 默认为 False
click 内部执行逻辑如下
找到与 selector 匹配到的节点, 如果没有找到,就一直等待直到超时,超时时间由 timeout 参数设置
检查匹配到的节点是否存在可操作性,等待检查结果,如果某个按钮设置了不可点击,就等该按钮变成可点击的时候再去点击,除非通过 force 参数设置跳过了可操作性检查步骤,才会强制点击
如果需要,就滚动一下页面,使需要点击的节点呈现出来
调用 Page 对选哪个的 mouse 方法, 点击节点的中心位置,如果指定了 positon 参数, 就点击参数指定的位置
具体参数设置可以参考官方文档:
Page | Playwright
文本输入
文本输入对应的方法是 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()
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
可以看到获取的对应节点的 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()
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
霸王别姬 - Farewell My Concubine
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIy
这个杀手不太冷 - Léon
省略部分输出内容
这里我们通过 query_selector_all 方法获取了所有匹配到的节点, 每个节点各对应一个 ElementHandle 对象, 可以调用 ElementHandle 对象的 get_attribute 方法获取节点属性, 也可以通过 text_content 方法获取节点文本
获取单个节点
获取单个节点的特定方法 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/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
霸王别姬 - Farewell My Concubine
可以看到只输出了第一个节点的信息
网络劫持
在介绍一个方法 route 利用这个方法可以实现网络劫持和修改操作。如修改 request 的属性,修改响应结果等
from playwright.sync_api import sync_playwright import re import time # 设置图片不加载,劫持请求数据 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') time.sleep(3) browser.close()
这里我们调用了 route 方法, 第一个参数通过正则表达式传入了 URL 路径,这里的 (\.png) |(\.jpg)代表所有包含 .png 或 .jpg 的链接,遇到这样的请求,会回调 cancel_request 方法做处理。 cancel_request 方法接收两个参数,一个是 route 代表一个 CallableRoute 对象, 另一个是 request , 代表 Request 对象, 这里我们直接调用 CallableRoute 对象的 abort 方法,取消了这次请求,最终导致所有的图片都取消加载
这里解释这么做的一个作用: 图片资源都是二进制文件, 我们在爬取的过程中可能并不想关心具体的二进制内容,而只关心图片的 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 import time # 改变响应数据 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/') time.sleep(3) browser.close()
这里按照教程所说应该是要展示出 上面自己编写的 HTML 内容,但是实际效果并没有。