安装

安装:pip install playwright

安装完playwright后还需要安装playwright自带的浏览器内核才能使用

安装浏览器:playwright install 此命令安装多个浏览器,如需自定义安装浏览器可使用以下代码:

#安装Chrome浏览器
playwright install chromium
#安装Firefox浏览器
playwright install firefox
#安装Webkit浏览器
playwright install webkit

playwright还可以录制脚本,使用命令开启后,当在浏览器进相关操作时,playwright会自动给出相应操作的代码:

playwright codegen [options] [url]

    -o, --output <file name> :保存生成脚本
    --target <language> :生成的脚本语言,可以设置javascript, test, python, python-async和csharp,默认为python。
    -b, --browser <browserType> :要使用的浏览器,可以选择cr, chromium, ff, firefox, wk和webkit,默认chromium。
    --channel <channel>:chromium版本,比如chrome, chrome-beta, msedge-dev等,
    --color-scheme <scheme>:模拟器的颜色主题,可选择light 或者 dark样式。
    --device <deviceName> :模拟的设备,比如iPhone 11。
    --save-storage <filename> :保存上下文状态,用于保存cookies 和localStorage,可用它来实现重用。例如playwright codegen --save-storage=auth.json
    --load-storage <filename> :加载--save-storage 保存的数据,重用认证数据。
    --proxy-server <proxy> :指定代理服务器
    --timezone <time zone> : 指定时区
    --geolocation <coordinates> :指定地理位置坐标
    --lang <language> :指定语言/地区,比如中国大陆:zh-CN
    --timeout <timeout> :超时时间,定位毫秒,默认10000ms
    --user-agent <ua string> :用户代理
    --viewport-size <size> :浏览器窗口大小
    -h, --help :查看帮助信息

开始使用

from playwright.sync_api import sync_playwright

async with sync_playwright() as p:
    # 使用Chrome内核 创建一个新的浏览器页面
    browser = await p.chromium.launch()
    # 创建一个新的页面,并且返回页面句柄
    page = await browser.new_page()
    # 进入指定页面,在登录完毕之后输入任意内容继续
    await page.goto("http://www.baidu.com")
    print(page.title())
    browser.close()

p.*.launch()常见参数说明:

  • executable_path : Chromium的执行路径(如果需要使用本机浏览器的话) args : 递给浏览器实例的附加参数。具体可参考浏览器启动配置项

      eg:`args=["--disable-infobars", "--enable-automation=False""--disable-blink-features=AutomationControlled","--start-maximized"]`
    
  • timeout : 等待浏览器实例启动的最长时间(以毫秒为单位)。 默认为30000(30 秒)。 传递0以禁用超时。

  • env : 指定浏览器可见的环境变量。 默认为 process.env

  • headless : 是否以无头模式运行浏览器。

  • devtools : 仅限 Chromium 是否为每个选项卡自动打开开发者工具面板。 如果此选项为true,则 headless 选项将设置为 false

  • proxy : 网络代理设置。

  • downloads_path : 如果指定,接受的下载将下载到此目录中。 否则,创建临时目录并当浏览器关闭时被删除。 在任何一种情况下,下载都会在浏览器上下文中被删除创建于已关闭。

  • slow_mo : 将 Playwright 操作减慢指定的毫秒数

  • chromium_sandbox : 启用 Chromium 沙盒。 默认为 false.

Browser常用方法

# 打开一个新页面
browser.new_page()
#创建无痕浏览器。
browser.contexts()
# 关闭窗口
browser.close()
# 浏览器类型 (chromium, firefox or webkit)
browser.browser_type()
# 表示浏览器是否连接。
browser.is_connected() 
# 返回新创建的浏览器会话。
browser.new_browser_cdp_session()
# 终止Playwright实例
playwright.stop()

new_page()和new_context()区别

在playwright中,new_page()和new_context()都是创建新的浏览器页面的方法,但它们有以下区别:

  1. new_page()方法会在当前的浏览器上下文中创建一个新的页面,而new_context()方法会创建一个全新的浏览器上下文,其中包含一个或多个页面。

  2. new_page()方法返回一个Promise,该Promise在成功时解析为一个Page对象,该对象表示新创建的页面。而new_context()方法返回一个Promise,该Promise在成功时解析为一个BrowserContext对象,该对象表示新创建的浏览器上下文。

  3. 在同一浏览器实例中,可以使用new_page()方法创建多个页面。但是,在同一浏览器实例中,只能使用new_context()方法创建一个浏览器上下文。

  4. new_page()方法创建的页面与当前页面共享相同的浏览器上下文和cookies等信息。而new_context()方法创建的浏览器上下文是完全独立的,不与其他上下文共享任何信息。

因此,如果需要在同一浏览器实例中创建多个页面,则应使用new_page()方法。如果需要在不同的浏览器上下文中创建多个页面,则应使用new_context()方法。

new_page 方法并不是直接通过 browser 调用的,而是通过 context 变量调用的,这个 context 又是由 browser 通过调用 new_context 方法生成的。有读者可能就会问了,这个 context 究竟是做什么的呢?

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

API

ConsoleMessage

ConsoleMessage对象通过page.on("console")事件按页面调度。对于页面中记录的每个控制台消息,Playwright 上下文中都会有相应的事件。

# 监听所有控制台日志
page.on("console", lambda msg: print(msg.text))

# 监听所有控制台事件并处理错误
page.on("console", lambda msg: print(f"error: {msg.text}") if msg.type == "error" else None)

# 获取下一个控制台日志
async with page.expect_console_message() as msg_info:
    # Issue console.log inside the page
    await page.evaluate("console.log('hello', 42, { foo: 'bar' })")
msg = await msg_info.value

# 解构打印参数
await msg.args[0].json_value() # hello
await msg.args[1].json_value() # 42

ElementHandle

ElementHandle 表示页内 DOM 元素。ElementHandles 可以使用page.query_selector()方法创建。

href_element = await page.query_selector("a")
await href_element.click()

参数链接:https://playwright.dev/python/docs/api/class-elementhandle

ElementHandle常用方法

box = await element_handle.bounding_box()
await page.mouse.click(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2)
  • element_handle.bounding_box():返回元素 xy 坐标和宽高。
    • 如果要点击元素的中心位置请使用:box["x"] + box["width"] / 2, box["y"] + box["height"] / 2
  • element_handle.click(**kwargs):点击元素,参数很多。
  • element_handle.dblclick(**kwargs):双击元素。
  • element_handle.content_frame():返回 iframe 的内容框架
  • await element_handle.dispatch_event("click"):触发元素 click 事件。无论元素的可见性状态如何。
  • element_handle.fill(value, **kwargs):为指定元素设置值
  • element_handle.focus():让元素获得焦点。
  • element_handle.get_attribute(name) :获取元素的指定属性值。
  • element_handle.hover(**kwargs):将鼠标移动到指定元素上
  • element_handle.scroll_into_view_if_needed(**kwargs):等待元素可以被操作后将元素移动到视图中。
  • element_handle.select_option(**kwargs):选择下拉框的选项,可以通过 index,value 进行选择。不是下拉框则报错。
  • element_handle.select_text(**kwargs):选择一个元素上所有文本,相当于鼠标拖动选择。
  • element_handle.set_checked(checked, **kwargs):勾选,或取消勾选复选框元素。
  • element_handle.set_input_files(files, **kwargs):设置 input 上传文件。
  • element_handle.text_content():返回 node.textContent

Keyboard

参数链接:https://playwright.dev/python/docs/api/class-keyboard

  • element_handle.press(key, **kwargs):模拟键盘按键,支持组合键
  • element_handle.press("a"):直接输入字母a
  • element_handle.press("Control+v"):Ctrl+v
  • page.keyboard.press('Tab'):Tab
  • element_handle.press("Control+Shift+T"):Ctrl+Shift+T

Mouse

鼠标对于左上角开始的坐标进行移动

  • await page.mouse.move(0, 0):鼠标移动到指定位置
  • await page.mouse.down():鼠标按下
  • await page.mouse.up():鼠标弹起
  • mouse.click(x, y, **kwargs):鼠标单击
  • mouse.dblclick(x, y, **kwargs):鼠标双击
  • mouse.wheel(delta_x, delta_y):鼠标滚轮,x,y 像素。

自定义操作(执行 JS)

在各种情况下,将元素传入 JS 进行操作。

  1. evaluate:如果传入的是函数,那么将解析为函数,否则以表达式运算。
  2. 如果结果是一个 Promise 或者函数是异步的,evaluate 将自动等待直到它被解决:
    • *await* page.evaluate("([x, y]) => Promise.resolve(x * y)", [7, 8])
    • *print*(*await* page.evaluate("1 + 2"))
  3. eval_on_selector:将定位到的元素传入 JS 中操作。 await page.eval_on_selector('div', 'el => window.getComputedStyle(el).fontSize')
  4. eval_on_selector_all:将定位到的多个元素传入 JS 中操作
    • await page.eval_on_selector_all('li.selected', '(items) => items.length')
  5. 具体内容可以见官网:https://playwright.dev/python/docs/core-concepts

重用认证状态

浏览器在登录后,保存状态,下次启动之后读取,无须重新登录。

  • 保存状态到本地:await context.storage_state(path="state.json")
  • 读取本地状态:context = await browser.new_context(storage_state="state.json"):读取本地状态,并创建一个新的上下文。

这里的两步操作都需要创建一个无痕浏览器进行操作

browser = await p.chromium.launch(headless=False)
context = await browser.new_context()
# 将本地数据用于创建无痕浏览器
# context = await browser.new_context(storage_state="state.json")
# 创建新页面进行访问
page = await context.new_page()
await page.goto("https://www.baidu.com")
# 保存状态到本地
await context.storage_state(path="state.json")

会话存储

import os
# Get session storage and store as env variable
session_storage = await page.evaluate("() => JSON.stringify(sessionStorage)")
os.environ["SESSION_STORAGE"] = session_storage

# Set session storage in a new context
session_storage = os.environ["SESSION_STORAGE"]
await context.add_init_script("""storage => {
  if (window.location.hostname == 'example.com') {
    entries = JSON.parse(storage)
    Object.keys(entries).forEach(key => {
      window.sessionStorage.setItem(key, entries[key])
    })
  }
}""", session_storage)

模拟移动设备

Playwright 另外一个特色功能就是可以支持移动端浏览器的模拟,比如模拟打开 iPhone 12 Pro Max 上的 Safari 浏览器,然后手动设置定位,并打开百度地图并截图。首先我们可以选定一个经纬度,比如故宫的经纬度是 39.913904, 116.39014,我们可以通过 geolocation 参数传递给 Webkit 浏览器并初始化。

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',
        geolocation={'longitude': 116.39014, 'latitude': 39.913904},
        permissions=['geolocation']
    )
    page = context.new_page()
    page.goto('https://amap.com')
    page.wait_for_load_state(state='networkidle')
    page.screenshot(path='location-iphone.png')
    browser.close()

这里我们先用 PlaywrightContextManager 对象的 devices 属性指定了一台移动设备,这里传入的是手机的型号,比如 iPhone 12 Pro Max,当然也可以传其他名称,比如 iPhone 8,Pixel 2 等。 前面我们已经了解了 BrowserContext 对象,BrowserContext 对象也可以用来模拟移动端浏览器,初始化一些移动设备信息、语言、权限、位置等信息,这里我们就用它来创建了一个移动端 BrowserContext 对象,通过 geolocation 参数传入了经纬度信息,通过 permissions 参数传入了赋予的权限信息,最后将得到的 BrowserContext 对象赋值为 context 变量。

接着我们就可以用 BrowserContext 对象来新建一个页面,还是调用 new_page 方法创建一个新的选项卡,然后跳转到高德地图,并调用了 wait_for_load_state 方法等待页面某个状态完成,这里我们传入的 state 是 networkidle,也就是网络空闲状态。因为在页面初始化和加载过程中,肯定是伴随有网络请求的,所以加载过程中肯定不算 networkidle 状态,所以这里我们传入 networkidle 就可以标识当前页面和数据加载完成的状态。加载完成之后,我们再调用 screenshot 方法获取当前页面截图,最后关闭浏览器。

运行下代码,可以发现这里就弹出了一个移动版浏览器,然后加载了高德地图,并定位到了故宫的位置,如图所示: 输出的截图也是浏览器中显示的结果。

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

操作 frame

具体查看:https://playwright.dev/python/docs/api/class-frame#frame-add-script-tag

当你定位到 frame 之后,可以像正常页面一样操作。

  • frame.add_script_tag(**kwargs):添加 js 代码到 frame
  • frame.child_frames:返回当前页 frame 列表
# 定位到网页的iframe
frame_element_handle = await page.query_selector('.frame-class')
# 返回内容进行操作
frame = await frame_element_handle.content_frame()
# 之后这个frame就可以操作内部元素。
await frame.fill('#username-input', 'John')

操作浏览器弹出的对话框

# 如果没有对话框监听器,那么对话框都会被自动关闭。
# 但如果有对话框监听器,浏览器却没有弹出对话框,那么会一直等待
# 等待弹出窗口,输出窗口的信息
page.on("dialog", lambda dialog: print(dialog.message))
# 等待弹出框,并点击确定。
page.on("dialog", lambda dialog: dialog.accept())
# 等待窗口弹出,并且在窗口中输入指定内容后点击确定。
page.on("dialog", lambda dialog: dialog.accept('66666'))
# 等待弹出框,并点击取消。
page.on("dialog", lambda dialog: dialog.dismiss())
# 输入指定内容后点击取消(我想没有人会这么做)
page.on("dialog", lambda dialog: dialog.dismiss())
# 返回浏览器默认提示内容,没有返回空
page.on("dialog", lambda dialog: dialog.default_value)
# 返回浏览器对话框类型 可以是一个alert,beforeunload,confirm或prompt
page.on("dialog", lambda dialog: dialog.type)

操作浏览器弹出的对话框

# 如果没有对话框监听器,那么对话框都会被自动关闭。
# 但如果有对话框监听器,浏览器却没有弹出对话框,那么会一直等待
# 等待弹出窗口,输出窗口的信息
page.on("dialog", lambda dialog: print(dialog.message))
# 等待弹出框,并点击确定。
page.on("dialog", lambda dialog: dialog.accept())
# 等待窗口弹出,并且在窗口中输入指定内容后点击确定。
page.on("dialog", lambda dialog: dialog.accept('66666'))
# 等待弹出框,并点击取消。
page.on("dialog", lambda dialog: dialog.dismiss())
# 输入指定内容后点击取消(我想没有人会这么做)
page.on("dialog", lambda dialog: dialog.dismiss())
# 返回浏览器默认提示内容,没有返回空
page.on("dialog", lambda dialog: dialog.default_value)
# 返回浏览器对话框类型 可以是一个alert,beforeunload,confirm或prompt
page.on("dialog", lambda dialog: dialog.type)

文件选择器

这里文件选择器记得修改弹出的页面,如果是新窗口要修改监听的 page。

这里注意,如果网页限制了,不让上传的文件是没有办法上传的。

比如限制 3M,但文件超过了 3M 可能就会报错。

# 进入上传文件界面
await page.goto('https://www.wenshushu.cn/')
# 定位按钮
but = await page.query_selector('//*[@id="page_content"]')
# 开始监听文件选择窗口
async with page.expect_file_chooser() as fc_info:
    # 点击按钮,弹出上传窗口
    await but.click()
    # 获取上传窗口信息
    file_chooser = await fc_info.value
    # 选择需要上传的路径
    await file_chooser.set_files(["back.jpeg", 'driver.py'])
    # --------------------------------------------------------
    # 返回与此文件选择器相关联的元素
    but2 = file_chooser.element
    # 返回次文件选择器是否接受选择多个文件
    file_chooser.is_multiple()
    # 返回此文件选择器所属页面
    url = file_chooser.page
    # 上传文件
    await file_chooser.set_files(files, **kwargs)

文本选择

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

page.click("text=Log in")

这就代表选择文本是 Log in 的节点,并点击。

CSS 选择器

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')")

第一个就是选择文本中包含 Playwright 的 article 节点,第二个就是选择 id 为 nav-bar 节点中文本值等于 Contact us 的节点。

CSS 选择器 + 节点关系

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

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

比如这里选择的就是选择 class 为 item-description 的节点,且该节点还要包含 class 为 item-promo-banner 的子节点。

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

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

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

XPath

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

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

这里需要在开头指定 xpath= 字符串,代表后面是一个 XPath 表达式。

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

处理弹出新窗口

# 等待新窗口的弹出
async with page.expect_popup() as popup_info:
    # 获取页面操作句柄
    new_page = await popup_info.value
    # 等待加载,但只是等待页面的加载,动态加载内容无法处理。
    await new_page.wait_for_load_state()
    await new_page.click('...')

鼠标滑动轨迹

async def get_tracks(distance):
    """
    匀变速公式:
    v = v0 + at
    x = v0t + 1/2 * a*t**2
    """

    # 定义初速度
    v = 0
    # 定义单位时间
    t = 0.6
    # 定义加速运动和减速运动的分界线
    mid = distance * 4 / 5
    # 定义当前位移
    current = 0
    # 定义运动轨迹列表
    tracks = []
    # 为了一直移动
    while current < distance:
        if mid > current:
            # 定义加速运动的加速度
            a = 2
        else:
            # 定义减速运动的加速度
            a = -3
        v0 = v
        # 计算位移
        x = v0 * t + 1 / 2 * a * t ** 2
        # 计算当前位移
        current += x
        # 计算每一次移动的末速度
        v = v0 + a * t
        tracks.append(round(x))
    return tracks

为浏览器增加代理访问

proxy = {
    'server': "127.0.0.1:7777",
    'username': "",
    'password': "",
}

playwright = await async_playwright().start()
browser = await playwright.chromium.launch(headless=False, proxy=proxy)
context = await browser.new_context()
page = await context.new_page()
await page.goto("https://ip138.com/")

playwright基本操作

基本创建代码

import asyncio
from playwright.async_api import async_playwright


async def main():
    async with async_playwright() as p:
        # p.chromium.launch 创建一个新的浏览器页面 headless 是否设置无头浏览器
        browser = await p.chromium.launch(headless=False)
        # 创建一个新的页面,并且返回页面句柄
        page = await browser.new_page()
        # 通过句柄操作浏览器
        await page.goto("http://www.baidu.com")
        # 输出页面标题
        print(await page.title())
        # 关闭浏览器
        await browser.close()

asyncio.run(main())

创建无痕浏览器

import asyncio
from playwright.async_api import async_playwright

if __name__ == '__main__':
    async def main():
        async with async_playwright() as p:
            # p.chromium.launch 创建一个新的浏览器页面 headless 是否设置无头浏览器
            browser = await p.chromium.launch(headless=False,args=["--disable-infobars", "--enable-automation=False",
                                                                "--disable-blink-features=AutomationControlled", "--start-maximized"])
            # 创建无痕浏览器上下文
            context = await browser.new_context(no_viewport=True)
            # 创建新页面进行访问
            page = await context.new_page()
            await page.goto("https://example.com")

    asyncio.run(main())

事件监听

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

比如这里我们可以监听 response 事件,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()

这里我们在创建 Page 对象之后,就开始监听 response 事件,同时将回调方法设置为 on_responseon_response 对象接收一个参数,然后把 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=NGMwMzFhNGEzMTFiMzJkOGE0ZTQ1YjUzMTc2OWNiYTI1Yzk0ZDM3MSwxNjIyOTE4NTE5
Statue 200: https://spa6.scrape.center/api/movie/?limit=10&offset=0&token=NGMwMzFhNGEzMTFiMzJkOGE0ZTQ1YjUzMTc2OWNiYTI1Yzk0ZDM3MSwxNjIyOTE4NTE5
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/b607fba7513e7f15eab170aac1e1400d878112.jpg@464w_644h_1e_1c

可以看到,这里的输出结果其实正好对应浏览器 Network 面板中所有的请求和响应内容,和下图是一一对应的:

这个网站我们之前分析过,其真实的数据都是 Ajax 加载的,同时 Ajax 请求中还带有加密参数,不好轻易获取。

但有了这个方法,这里如果我们想要截获 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/ce4da3e03e655b5b88ed31b5cd7896cf62472.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': ['美国']}]}

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

另外还有很多其他的事件监听,这里不再一一介绍了,可以查阅官方文档,参考类似的写法实现。

获取页面源码

要获取页面的 HTML 代码其实很简单,我们直接通过 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()

运行结果就是页面的 HTML 代码。获取了 HTML 代码之后,我们通过一些解析工具就可以提取想要的信息了。

页面点击

刚才我们通过示例也了解了页面点击的方法,那就是 click,这里详细说一下其使用方法。

页面点击的 API 定义如下:

page.click(selector, **kwargs)

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

这个方法的内部执行逻辑如下:

  • 根据 selector 找到匹配的节点,如果没有找到,那就一直等待直到超时,超时时间可以由额外的 timeout 参数设置,默认是 30 秒。
  • 等待对该节点的可操作性检查的结果,比如说如果某个按钮设置了不可点击,那它会等待该按钮变成了可点击的时候才去点击,除非通过 force 参数设置跳过可操作性检查步骤强制点击。
  • 如果需要的话,就滚动下页面,将需要被点击的节点呈现出来。
  • 调用 page 对象的 mouse 方法,点击节点中心的位置,如果指定了 position 参数,那就点击指定的位置。

click 方法的一些比较重要的参数如下:

  • click_count:点击次数,默认为 1。
  • timeout:等待要点击的节点的超时时间,默认是 30 秒。
  • position:需要传入一个字典,带有 x 和 y 属性,代表点击位置相对节点左上角的偏移位置。
  • force:即使不可点击,那也强制点击。默认是 False。 具体的 API 设置参数可以参考官方文档: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 方法,传入的 selectora.name,选定了 classnamea 节点,然后第二个参数传入了 href,获取超链接的内容,输出结果如下:

/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()

这里我们通过 query_selector_all 方法获取了所有匹配到的节点,每个节点对应的是一个 ElementHandle 对象,然后 ElementHandle 对象也有 get_attribute 方法来获取节点属性,另外还可以通过 text_content 方法获取节点文本。

运行结果如下:

/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
霸王别姬 - Farewell My Concubine
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIy
这个杀手不太冷 - Léon
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIz
肖申克的救赎 - The Shawshank Redemption
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI0
泰坦尼克号 - Titanic
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI1
罗马假日 - Roman Holiday
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI2
唐伯虎点秋香 - Flirting Scholar
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI3
乱世佳人 - Gone with the Wind
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI4
喜剧之王 - The King of Comedy
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWI5
楚门的世界 - The Truman Show
/detail/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIxMA==
狮子王 - 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/ZWYzNCN0ZXVxMGJ0dWEjKC01N3cxcTVvNS0takA5OHh5Z2ltbHlmeHMqLSFpLTAtbWIx
霸王别姬 - Farewell My Concubine

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

网络劫持

最后再介绍一个实用的方法 route ,利用 route 方法,我们可以实现一些网络劫持和修改操作,比如修改 request 的属性,修改 response 响应结果等。

看一个实例:

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 的链接,遇到这样的请求,会回调 cancel_request 方法处理, cancel_request 方法可以接收两个参数,一个是 route ,代表一个 CallableRoute 对象,另外一个是 request ,代表 Request 对象。这里我们直接调用了 routeabort 方法,取消了这次请求,所以最终导致的结果就是图片的加载全部取消了。 观察下运行结果,如图所示:

可以看到图片全都加载失败了。

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

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

首先这里定义一个 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/")
    browser.close()

这里我们使用 routefulfill 方法指定了一个本地文件,就是刚才我们定义的 HTML 文件,运行结果如下: