Playwright的基本使用

发布于:2024-04-29 ⋅ 阅读:(21) ⋅ 点赞:(0)

介绍

Playwright 是一个用于自动化浏览器操作的开源工具,由 Microsoft 开发和维护。它支持多种浏览器(包括 Chromium、Firefox 和 WebKit)和多种编程语言(如 Python、JavaScript 和 C#),可以用于测试、爬虫、自动化任务等场景。

安装

环境安装

python版本的Playwright官网文档:
https://playwright.dev/python/docs/intro

  • 系统要求:

    • Python 3.8 或更高版本。
    • Windows 10+、Windows Server 2016+ 或适用于 Linux 的 Windows 子系统 (WSL)。
    • MacOS 12 Monterey 或 MacOS 13 Ventura。
    • Debian 11、Debian 12、Ubuntu 20.04 或 Ubuntu 22.04。
  • 安装playwright的python版本

    • pip install playwright
  • 安装Playwright所需的所有工具插件和所支持的浏览器

    • playwright install
    • 该步骤耗时较长

屏幕录制

  • 创建一个py文件,比如:main.py

  • 在终端中,执行如下指令:

 playwright codegen -o main.py  

在这里插入图片描述

playwright codegen --viewport-size=800,600  www.baidu.com -o main.py 
  • 模拟手机设备进行网络请求(只支持手机模拟器,无需单独安装)
  • 访问指定网址,并且设置浏览器窗口大小
playwright codegen --device="iPhone 13" -o main.py

保留记录cookie

  • 在屏幕录制时,进行登录操作,登录后,cookie信息会被保存到auth.json文件中
playwright codegen --save-storage=auth.json http://download.java1234.com/ 
  • 基于auth.json进行屏幕录制,会自动进入到登录成功后的页面中
playwright codegen --load-storage=auth.json http://download.java1234.com/ -o main.py 

基本使用

from playwright.sync_api import sync_playwright
with sync_playwright() as p:
    # headless 是否是无头浏览器
    bro=p.chromium.launch(headless=False)
    page=bro.new_page()
    # 访问的网站
    page.goto("https://www.baidu.com")
    # 等待时长
    page.wait_for_timeout(1000)
    # 获取网页标头
    title=page.title()
    # 获取网站源码
    content=page.content()
    print(title,content)
    page.close()
    bro.close()

在这里插入图片描述

元素定位

CSS选择器定位

  • 语法结构:page.locator()
    • 参数:标签/id/层级/class 选择器
  • 交互操作:
    • 点击元素, click() 方法
    • 元素内输入文本, fill() 方法
import random

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    # headless 是否是无头浏览器
    bro = p.chromium.launch(headless=False)
    page = bro.new_page()
    # 访问的网站
    page.goto("https://www.baidu.com")
    # 等待时长
    page.wait_for_timeout(1000)

    # 定位并输入python
    page.locator("#kw").fill("python")

    # 定位搜索按钮,进行搜索 #id定位  .class定位
    page.locator("#su").click()
    page.wait_for_timeout(1000)
    # 网页回退
    page.go_back()
    page.wait_for_timeout(1000)
    # 标签+属性定位
    page.locator("input#kw").fill("人工智能")
    page.locator("#su").click()
    page.go_back()
    page.wait_for_timeout(1000)
    # 层级定位
    page.locator('#form > span > input#kw').fill('数据分析')
    page.locator('#su').click()
    page.wait_for_timeout(1000)
    page.go_back()

    page.wait_for_timeout(1000)

    # 聚焦于当前标签
    page.locator('#form > span > input#kw').focus()

    input_text = 'Hello, World!'
    for char in input_text:
        page.keyboard.type(char, delay=random.randint(300, 600))

    # 定位搜索按钮,进行点击操作
    page.locator('#su').click()

    page.close()
    bro.close()

xpath定位

page.locator(xpath表达式)

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    bro = p.chromium.launch(headless=False,slow_mo=2000)
    page = bro.new_page()
    page.goto('https://www.bilibili.com/')

    #xpath定位
    page.locator('//*[@id="nav-searchform"]/div[1]/input').fill('Python教程')
    page.locator('//*[@id="nav-searchform"]/div[2]').click()

    page.close()
    bro.close()

count

const count = await page.locator('div.my-class').count();  
console.log(count); // 输出匹配 'div.my-class' 的元素数量

nth(index)

const element =page.locator('button').nth(1); // 获取第二个按钮元素  
element.click();

inner_text()

const element = await page.locator('h1');  
const text = await element.inner_text();  
console.log(text); // 输出 h1 元素的内部文本

get_by_text(xxx)

const element =page.locator('button:text("Click me")'); // 定位包含文本 "Click me" 的按钮  
element.click();

get_attribute(attrName)

const element = page.locator('img');  
const src = element.get_attribute('src');  
console.log(src); // 输出图像的 src 属性值

Context管理多个页面

浏览器的上下文管理对象Context可以用于管理Context打开/创建的多个page页面。并且可以创建多个Context对象,那么不同的Context对象打开/创建的page之间是相互隔离的(每个Context上下文都有自己的Cookie、浏览器存储和浏览历史记录)
在这里插入图片描述

使用示例一

from playwright.sync_api import sync_playwright


def swith_page(context, title):
    for i in context.pages:

        if i.title() == title:
            print(title)
            i.bring_to_front()
            return i


with sync_playwright() as p:
    bro = p.chromium.launch(headless=False)
    # 多个窗口管理器
    context = bro.new_context()
    
    page = context.new_page()
    page.goto("https://baidu.com")

    a_list = page.locator('//div[@id="s-top-left"]/a').all()
    for a in a_list:
        a.click()

    win = swith_page(context, "hao123_上网从这里开始")
    page.wait_for_timeout(10000)
    win.locator('/html/body/div[2]/div/div[3]/div[1]/div[1]/form/div[2]/input').fill("hello")
    win.locator('/html/body/div[2]/div/div[3]/div[1]/div[1]/form/div[3]/input').click()

    page.close()
    context.close()

使用示例2

from playwright.sync_api import sync_playwright
from lxml import etree


with sync_playwright()as p:
    bro=p.chromium.launch(headless=False,timeout=1000)
    context=bro.new_context()
    page=context.new_page()
    page.goto("https://www.bilibili.com/")
    page.locator('//*[@id="nav-searchform"]/div[1]/input').fill("python")
    page.locator('//*[@id="nav-searchform"]/div[2]').click()
    page.wait_for_timeout(2000)
    page=context.pages[1]

    tree=etree.HTML(page.content())
    name=tree.xpath('//*[@id="i_cecream"]/div/div[2]/div[2]/div/div/div/div[3]/div/div[1]/div/div[2]/div/div/a/h3/text()')
    title=tree.xpath('//*[@id="i_cecream"]/div/div[2]/div[2]/div/div/div/div[3]/div/div[1]/div/div[2]/div/div/p/a/span[1]/text()')
    print("".join(name),title[0])

滑动验证

基于opencv实现的免费操作:

  • pip install opencv-python
  • 可能会存在滑动误差,需要手动调整
from playwright.sync_api import sync_playwright
from lxml import etree

from playwright.sync_api import sync_playwright
import cv2
from urllib import request


# 获取要滑动的距离
def get_distance(background, gap):
    # 滑动验证码的整体背景图片
    background = cv2.imread(background, 0)
    # 缺口图片
    gap = cv2.imread(gap, 0)
    res = cv2.matchTemplate(background, gap, cv2.TM_CCOEFF_NORMED)
    # value就是cv2计算出来的滑动距离的像素值
    value = cv2.minMaxLoc(res)[2][0]

    # 单位换算
    return value * 278 / 360


# 有的检测移动速度的 如果匀速移动会被识别出来,来个简单点的渐进
def get_track(distance):  # distance为传入的总距离
    # 移动轨迹
    track = []
    # 当前位移
    current = 0
    # 减速阈值
    mid = distance * 4 / 5
    # 计算间隔
    t = 0.2
    # 初速度
    v = 1
    while current < distance:
        if current < mid:
            # 加速度为2
            a = 4
        else:
            # 加速度为-2
            a = -3
        v0 = v
        # 当前速度
        v = v0 + a * t
        # 移动距离
        move = v0 * t + 1 / 2 * a * t * t
        # 当前位移
        current += move
        # 加入轨迹
        track.append(round(move))
    return track


with sync_playwright() as p:
    bro = p.chromium.launch(headless=False, timeout=1000)
    context = bro.new_context()
    page = context.new_page()
    page.goto("https://passport.jd.com/new/login.aspx?ReturnUrl=https%3A%2F%2Fwww.jd.com%2F")
    page.locator('//*[@id="loginname"]').fill("13795680749")
    page.locator('//*[@id="nloginpwd"]').fill("13795680749jcr")
    page.locator('//*[@id="loginsubmit"]').click()

    page.wait_for_timeout(2000)

    bg_img_src = page.locator('.JDJRV-bigimg > img').get_attribute('src')
    small_img_src = page.locator('.JDJRV-smallimg > img').get_attribute('src')
    request.urlretrieve(bg_img_src, "bg_img_src.png")
    request.urlretrieve(small_img_src, "small_img_src.png")

    value = int(get_distance("./bg_img_src.png", "./small_img_src.png"))

    tracks=get_track(value)
    # 找到滑块在当前页面的坐标(这个会返回一个字典里边四个数字)
    # {'x': 858, 'y': 339.9921875, 'width': 55, 'height': 55}
    box=page.locator('//*[@id="JDJRV-wrap-loginsubmit"]/div/div/div/div[2]/div[3]').bounding_box()
    # 让鼠标移动到滑块标签的中间上
    page.mouse.move(box["x"] + box["width"] / 2, box["y"] + box["height"] / 2)
    # 按下鼠标
    page.mouse.down()

    x = box["x"] + 14  # 加上一个数值调整滑动误差
    # 滑动的长度放到轨迹加工一下得到一个轨迹
    for track in tracks:
        # 循环鼠标按照轨迹移动
        page.mouse.move(x + track, 0)
        x += track
    # 移动结束鼠标释放
    page.mouse.up()

    page.wait_for_timeout(5000)
    page.close()
    bro.close()

文字点击验证码

图鉴验证码封装

import base64
import json

import requests
# 一、图片文字类型(默认 3 数英混合):
# 1 : 纯数字
# 1001:纯数字2
# 2 : 纯英文
# 1002:纯英文2
# 3 : 数英混合
# 1003:数英混合2
#  4 : 闪动GIF
# 7 : 无感学习(独家)
# 11 : 计算题
# 1005:  快速计算题
# 16 : 汉字
# 32 : 通用文字识别(证件、单据)
# 66:  问答题
# 49 :recaptcha图片识别
# 二、图片旋转角度类型:
# 29 :  旋转类型
#
# 三、图片坐标点选类型:
# 19 :  1个坐标
# 20 :  3个坐标
# 21 :  3 ~ 5个坐标
# 22 :  5 ~ 8个坐标
# 27 :  1 ~ 4个坐标
# 48 : 轨迹类型
#
# 四、缺口识别
# 18 : 缺口识别(需要2张图 一张目标图一张缺口图)
# 33 : 单缺口识别(返回X轴坐标 只需要1张图)
# 五、拼图识别
# 53:拼图识别

def base64_api(uname, pwd, img, typeid):
    with open(img, 'rb') as f:
        base64_data = base64.b64encode(f.read())
        b64 = base64_data.decode()
    data = {"username": uname, "password": pwd, "typeid": typeid, "image": b64}
    result = json.loads(requests.post("http://api.ttshitu.com/predict", json=data).text)
    if result['success']:
        return result["data"]["result"]
    else:
        # !!!!!!!注意:返回 人工不足等 错误情况 请加逻辑处理防止脚本卡死 继续重新 识别
        return result["message"]
    return ""

案例

from playwright.sync_api import sync_playwright
from lxml import etree

from playwright.sync_api import sync_playwright
import cv2
from urllib import request

from demo import base64_api



with sync_playwright() as p:
    bro = p.chromium.launch(headless=False, timeout=1000)
    context = bro.new_context()
    page = context.new_page()
    page.goto("https://passport.bilibili.com/login")
    # 登录输入
    page.locator('//*[@id="app"]/div[2]/div[2]/div[3]/div[2]/div[1]/div[1]/input').fill("13795680749")
    page.locator('//*[@id="app"]/div[2]/div[2]/div[3]/div[2]/div[1]/div[3]/input').fill("13795680749")
    page.locator('//*[@id="app"]/div[2]/div[2]/div[3]/div[2]/div[2]/div[2]').click()

    ele=page.locator('//div[@class="geetest_widget"]')
    ele.screenshot(path="./code.png")

    # 获取ele的当前坐标
    box=ele.bounding_box()

    result = base64_api(uname='amxr', pwd='Admin123', img="./code.png", typeid=27)
    # result = '154,251|145,167'
    result_list = result.split('|')
    # 6.根据识别出验证码的结果进行处理
    for pos in result_list:
        x = int(pos.split(',')[0])
        y = int(pos.split(',')[1])
        # page.mouse.move(box['x']+x,box['y']+y)
        # page.mouse.down()
        # page.mouse.up()  #或者直接使用click方式
        page.mouse.click(box['x'] + x, box['y'] + y)
        page.wait_for_timeout(500)

    # 找到确认按钮
    submit = page.locator('//div[@class="geetest_commit_tip"]')
    submit.click()

    page.wait_for_timeout(5000)
    page.close()
    bro.close()


规避检测

如果网站有对Playwright采取监测机制的话,比如正常情况下我们用浏览器访问淘宝等网站的 window.navigator.webdriver的值为 undefined或者为false。而使用Playwright访问则该值为true。

  • 查看taobao是否检测Playwright
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    bro = p.chromium.launch(headless=False)
    page = bro.new_page()
    page.goto('https://www.taobao.com/')

    ret = page.evaluate('window.navigator.webdriver')
    print(ret)

    page.wait_for_timeout(30000)
    page.close()
    bro.close()

在这里插入图片描述

from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    bro = p.chromium.launch(headless=False)
    context = bro.new_context()
    #加载该js文件目的是给Playwright的浏览器模拟真实的浏览器环境
    context.add_init_script(path='./stealth.min.js')
    page = context.new_page()
    page.goto('https://www.taobao.com/')

    ret = page.evaluate('window.navigator.webdriver')
    print(ret)

    page.wait_for_timeout(30000)
    page.close()
    bro.close()

在这里插入图片描述

浏览器接管

window版本
  • 首先右键 Chrome 浏览器桌面图标,找到 chrome.exe 的安装路径,然后将其添加到环境变量Path中

在这里插入图片描述

  • 添加环境变量
    在这里插入图片描述

  • 在任意目录下新建一个空白文件夹(playwright_chrome_data):用于保存接管的浏览器的运行数据。

  • 打开cmd输入命令启动chrome浏览器:

chrome.exe --remote-debugging-port=8899 --user-data-dir="E:\playwright_chrome_data"

–remote-debugging-port 是指定浏览器运行端口,只要没被占用就行
–user-data-dir 指定运行浏览器的运行数据,新建一个干净目录,不影响系统原来的数据

  • 执行后会启动chrome浏览器
    • 可以在浏览器中手动访问百度。
  • 在你已经打开的浏览器页面,手工操作登录,登录成功后,让playwright 继续操作。
  • Playwright接管浏览器
from playwright.sync_api import sync_playwright

with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp('http://localhost:8899/')
    # 获取page对象
    page = browser.contexts[0].pages[0]
    #该操作会直接作用在接管的浏览器中
    page.locator('//*[@id="kw"]').fill('haha')
    print(page.url)
    print(page.title())

JS注入

  • url:https://www.ciweimao.com/chapter/109936632

  • 分析:

    • 点击下一章,简单分析可知小说内容是动态加载的数据。查看数据包的fetch/xhr选项卡,会不会到ajax_get_session_code和get_book_chapter_detail_info两个数据包。根据抓包工具分析,get_book_chapter_detail_info数据包中存在chapter_content为小说章节内容,但是数据为加密后的形式。

    • 全局搜索chapter_content关键字,定位到了数据解密环节的ajax代码。然后在ajax代码的首位各自打上两个断点进行调试。最终发现如下代码就是解密操作:其中messageInfo就是密文数据

    • $.myDecrypt({
                          content: messageInfo,
                          keys: keys,
                          accessKey: HB.config.chapterAccessKey
                      })
      
    
    
  • 因此可以在Playwright中基于补充浏览器环境和js注入的机制进行解密js代码测试:

  •   from playwright.sync_api import sync_playwright
    with sync_playwright() as p:
          browser = p.chromium.launch(headless=True)
        context = browser.new_context()
          #规避检测,伪造真实浏览器环境
          context.add_init_script(path='stealth.min.js')
          page = context.new_page()
          page.goto("https://www.ciweimao.com/chapter/109936632")
          page.wait_for_timeout(3000)
        # #进行js注入,执行解密js代码
          encrypt_data = page.evaluate('$.myDecrypt({content: messageInfo,keys: keys,accessKey: HB.config.chapterAccessKey})')
        print(encrypt_data)