playwright的使用

playwright是微软在2020年开源的新一代支持异步的自动化测试工具,对市面上的主流浏览器(Chromium、Firefox、Webkit)都提供了支持,API功能简洁又强大。

官网文档:https://playwright.dev/python/docs/api/class-playwright

一、特点

  • 安装和配置过程非常简单,安装过程中自动安装对应的浏览器和驱动
  • 支持无头、有头模式
  • 提供和自动等待相关的API,页面加载时会自动等待对应的节点加载,大大减小编写复杂度,比如:page.wait_for_load_state(state='networkidle'),会等待页面加载完成

二、安装

  • 安装依赖

    pip3 install playwright

    python版本需要 > 3.7

  • 安装驱动

    playwright install

    会下载全部驱动,速度较慢,如果只安装某一个驱动,就在命令后带上驱动名,可以用:playwright install --help查看命令

三、基本使用

  • 同步模式 -- 类似于Selenium

    from playwright.sync_api import sync_playwright
    
    # 方式一:使用with上下文管理器
    from playwright.sync_api import sync_playwright
    with sync_playwright() as p:#创建Playwright管理器
        bro = p.chromium.launch(headless=False)
        page = bro.new_page()
        page.goto('https://www.baidu.com')
        #自行设置等待时长,注意:不可使用time.sleep
        page.wait_for_timeout(1000)
        title = page.title()
        content = page.content()
        print(title,content)
        page.close()
        bro.close()
    
    # 方式二:不使用with上下文管理器
    p = sync_playwright().start() # 开启
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto('https://www.baidu.com/')
    page.screenshot(path='baidu.png')
    browser.close()
    p.stop() # 关闭
  • 异步模式 -- 类似于Pyppetter

    import asyncio
    from playwright.async_api import async_playwright
    async def main():
        async with async_playwright() as p:
            bro = await p.chromium.launch(headless=False,slow_mo=2000)
            page = await bro.new_page()
            await page.goto('https://www.baidu.com')
            title = await page.title()
            content = await page.content()
            print(title,content)
            await page.close()
            await bro.close()
    
    asyncio.run(main())
  • 移动端浏览器页面

    from playwright.sync_api import sync_playwright
    
    with sync_playwright() as playwright:
        iphone = playwright.devices["iPhone 12"]
        browser = playwright.webkit.launch(headless=False)
        context = browser.new_context(**iphone)
        page = context.new_page()
        page.goto("https://weibo.com/")
        page.wait_for_load_state(state='networkidle') # 当前页面初始化和加载完成的状态
        page.screenshot(path='weibo.png')
        browser.close()

四、规避webdriver检测

  • page.add_init_script(js)

    • 针对简单的检测
      from playwright.sync_api import Playwright, sync_playwright
      
      p = sync_playwright().start()
      browser = p.chromium.launch(headless=False) # chromium有头浏览器
      page = browser.new_page() #创建page对象
      js="""
      Object.defineProperties(navigator, {webdriver:{get:()=>undefined}});
      """
      page.add_init_script(js); # 执行js,规避webdriver检测
      page.goto('http://xxx.com') # 访问目标网站
      browser.close()
    • 针对严格检测
      from playwright.sync_api import sync_playwright
      
      with sync_playwright() as p:
          bro = p.chromium.launch(headless=False)
          context = bro.new_context()
          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(3000)
          page.close()
          bro.close()

五、自动录制脚本

该功能可以录制我们在浏览器中的操作并自动生成代码,有了该功能,甚至一行代码都不用写

  • 命令行

    playwright codegen -o main.py

    执行命令后会自动打开浏览器,后续在浏览器上的操作都会自动翻译成代码,并将代码保存到main.py

    • 设置同步代码还是异步代码
      • 在打开的浏览器右上角,Target默认是Library【同步】,可以下拉选择Library Async【异步】
    • 指定网址,并设置窗口大小
      playwright codegen --viewport-size=800,600  www.baidu.com -o main.py
  • 查看codegen命令具体参数

    playwright codegen --help

六、保留记录cookie消息

  • playwright codegen --save-storage=taobao.json https://www.taobao.com

    • 在屏幕录制时,进行登录操作,并将cookie信息保存到taobao.json文件中
  • playwright codegen --load-storage=taobao.json https://www.taobao.com -o main.py

    • 基于taobao.json进行屏幕录制,会自动进入登录成功后的页面中
  • 携带cookie信息进行操作--方式1

    context = browser.new_context(storage_state="taobao.json")
  • 携带cookie信息进行操作--方式2

    from playwright.sync_api import sync_playwright
    import requests
    import json
    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.xiaohongshu.com")
        #设置cookie:cookies是一个数组对象,必须包含最主要的四个值:domain ,name,path,value
        context.add_cookies(
            [
                {
                    "name": "web_session",
                    "value": "030037a2b8f42aec75ab13e6f3224a2a8683e1",
                    "domain": ".xiaohongshu.com",
                    "path": "/"
                },
                {
                    "name": "a1",
                    "value": '18ca96a6f38939kidvzjak0exvrf00zbflw95v3uc30000903431',
                    "domain": ".xiaohongshu.com",
                    "path": "/"
                }
            ]
        )
        page.reload() #添加cookie后,务必要重载page页面
    
        #json_data就是参数i
        json_data = {
            "cursor_score": "1.7036686839800014E9",
            "num": 20,
            "refresh_type": 3,
            "note_index": 55,
            "unread_begin_note_id": "",
            "unread_end_note_id": "",
            "unread_note_count": 0,
            "category": "homefeed_recommend",
            "search_key": "",
            "need_num": 10,
            "image_formats": [
                "jpg",
                "webp",
                "avif"
            ]  
    }
        #进行js注入,执行window._webmsxyw函数获取x-s的值
        encrypt_params = page.evaluate("([s,i]) => window._webmsxyw(s,i)",
                                       ["/api/sns/web/v1/homefeed",json_data])
        #将返回值转换成python字典
        x_s = dict(encrypt_params)['X-s']
    
        headers = {
            'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36',
            'X-S':x_s,
            'Content-Type':'application/json;charset=UTF-8'
        }
        cookies = {
            'a1':'18ca96a6f38939kidvzjak0exvrf00zbflw95v3uc30000903431',
            'web_session':'030037a2b8f42aec75ab13e6f3224a2a8683e1'
        }
        url = 'https://edith.xiaohongshu.com/api/sns/web/v1/homefeed'
    
        #处理json串的请求参数
        #separators将逗号(,)作为键值对之间的分隔符,将冒号(:)作为键和值之间的分隔符。
        json_str = json.dumps(json_data,separators=(",",":"),ensure_ascii=False)
        ret = requests.post(url=url,headers=headers,data=json_str,cookies=cookies).json()
        print(ret)

七、元素定位

  • css选择器

    • 语法
      • page.locator(参数) 
      • 参数:标签/id/层级/class 选择器
    • 交互操作
      • 基于page.locator(参数)
        • 点击元素:click()
        • 元素内输入文本:fill()
        • 获取满足要求的所有元素:all()
        • 获取满足要求的元素数量:count()
        • 根据下标定位具体元素:nth(index)【下标从0开始】
        • 聚焦当前元素:focus()
      • 基于page
        • 基于文本定位:page.get_by_text()
          • 可以是包含的文本,也可以是正则表达式re.compile(r'xxx'),指定参数exact=True,可以指定文本内容必须相等
      • 基于元素
        • 获取属性:get_attribute('属性名')
        • 获取文本:inner_text
    • 示例1
      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.baidu.com')
      
          #定位到输入框,进行文本录入
          page.locator('#kw').fill('Python教程') #id定位
          # 定位搜索按钮,进行点击操作
          page.locator('#su').click()
          # 等待页面加载完成
          page.wait_for_load_state(state='networkidle')
          #后退操作
          page.go_back()
      
          # 聚焦于当前标签
          page.locator('#kw').focus()
          input_text = 'Hello, World!'
          for char in input_text:
              # 设置内容的输入的时间间隔
              page.keyboard.type(char, delay=200) # 每隔200ms输入1个字符
          page.locator('#su').click()
          page.wait_for_load_state(state='networkidle')
          page.go_back()
      
          page.locator('.s_ipt').fill('爬虫')  # class定位
          page.locator('#su').click()
          page.wait_for_load_state(state='networkidle')
          page.go_back()
      
          page.locator('input#kw').fill('人工智能')  # 标签+属性定位
          page.locator('#su').click()
          page.wait_for_load_state(state='networkidle')
          page.go_back()
      
          page.locator('#form > span > input#kw').fill('数据分析') #层级定位
          page.locator('#su').click()
          page.wait_for_load_state(state='networkidle')
      
          page.close()
          bro.close()
    • 示例2
      from playwright.sync_api import sync_playwright
      
      with sync_playwright() as p:
          browser = p.chromium.launch(headless=False,slow_mo=2000)
          context = browser.new_context(storage_state="taobao.json")
          page = context.new_page()
          page.goto("https://www.taobao.com/")
      
          page.locator('#q').fill('mac pro')
          # class属性值为btn-search tb-bg,在定位的时候选择空格左右两侧任意一个属性值即可
          page.locator('.btn-search').click()
          page.wait_for_timeout(1000)
      
          # 根据文本定位
          page.get_by_text('发货地').click()
          page.wait_for_timeout(1000)
      
          # 定位到满足要求所有的标签(商品列表最外层的a标签)
          locator = page.locator('.Content--contentInner--QVTcU0M > div > a')
          all_eles = locator.all()
          # for a_ele in all_eles:
          #     a_ele.click()  # 点击每个链接,打开不同的page页面
      
          # 查看定位到满足要求标签的数量
          count = locator.count()
          print(count)
          # 定位到第10个a标签,nth下标从0开始
          a_10 = locator.nth(9)
          print(a_10.get_attribute('href'), a_10.inner_text())
          print('---------------------------------------------------------------------------')
          # 获得每一个a标签中的文本内容和href属性值
          for index in range(count):
              ele = locator.nth(index)
              text = ele.inner_text()
              href = ele.get_attribute('href')
              print(text, href)
      
          page.close()
          context.close()
          browser.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()
  • 其它

    • 除了正常的css选择器以外,还扩展了文本选择、xpath、以及css选择器与文本和节点关系来配合筛选
      # css选择器
      page.click('#button')
      
      # 文本选择器
      page.click('text=登录') # 开头需指明'text='
      
      # xpath选择器
      page.click('xpath=//button') # 开头需指明'xpath='
      
      # css选择器+文本
      page.click('p:has-text("Playwright")') # :has-text() -> 包含指定字符串
      page.click('p:has-text("contact us")') # :text() -> 完全匹配字符串
      
      # css选择器+节点关系
      page.click('p:has(span)') # :has() -> 包含某个子节点  选择包含span子节点的p标签
      page.click('input:right-of(:text("用户名"))') # :right-of() -> 位置在某个节点的右侧  选择在文本内容为'用户名'节点右侧的input节点

八、Context上下文

  • 浏览器的上下文管理对象Context可以用于管理Context打开/创建的多个page页面。并且可以创建多个Context对象,那么不同的Context对象打开/创建的page之间是相互隔离的(每个Context上下文都有自己的Cookie、浏览器存储和浏览历史记录)
  • 一个Context可以理解为打开了一次浏览器,通过该Context创建或者打开的page页面,相当于打开的多个的标签页,都在同一浏览器界面中
  • 示例
    • B站
      from playwright.sync_api import sync_playwright
      from lxml import etree
      
      
      # 封装页面切换的函数
      def switch_to_page(context, title):
          # 使用上下文管理对象获取浏览器打开的所有page页面
          for page in context.pages:
              if title == page.title():
                  # 浏览器停留在此page页面
                  page.bring_to_front()
                  return page
      
      
      with sync_playwright() as p:
          bro = p.chromium.launch(headless=False, slow_mo=1000) # slow_mo参数:指定操作减慢的时间,避免太快报错
          # 创建上下文管理对象
          context = bro.new_context()
          # 基于上下文管理对象打开一个page页面
          page = context.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中
          select_page = switch_to_page(context, 'Python教程-哔哩哔哩_bilibili')
          page_text = select_page.content()
          tree = etree.HTML(page_text)
          div_list = tree.xpath('//*[@id="i_cecream"]/div/div[2]/div[2]/div/div/div/div[3]/div/div')
          for div in div_list:
              title = div.xpath('.//h3[@class="bili-video-card__info--tit"]/@title')[0]
              author = div.xpath('.//span[@class="bili-video-card__info--author"]/text()')[0]
              print(title, author)
          page.close()
          bro.close()

九、鼠标操作

  • 获取元素位置
    slide = page.locator('xxx')
    box = slide.bounding_box() # 找到滑块在当前页面的坐标(元素左上角的坐标,以及宽、高) {'x': 858, 'y': 339.9921875, 'width': 55, 'height': 55}
    slide_x = box['x'] + box['width'] / 2  # 滑块中心横坐标
    slide_y = box['y'] + box['height'] / 2 # 滑块中心纵坐标
  • 鼠标移动到指定位置
    • page.mouse.move(x, y)
      page.mouse.move(slide_x, slide_y)
  • 按下鼠标
    • page.mouse.down()
  • 释放鼠标
    • page.mouse.up()
  • 鼠标点击指定位置
    • page.mouse.click(x, y)
      # 等同于如下操作
      page.mouse.move(x, y)
      page.mouse.down()
      page.mouse.up()

十、浏览器接管

  • 将chrome.exe安装路径添加到环境变量
  • 新建一个文件夹,用于保存接管浏览器的运行数据
  • 命令行启动浏览器
    chrome.exe --remote-debugging-port=8899 --user-data-dir="E:\playwright_chrome_data"

    --remote-debugging-port:指定浏览器运行端口,只要没有被占用即可
    --user-data-dir:保存接管浏览器运行的数据,避免影响系统原来浏览器的数据

  • 在打开的浏览器中,可以执行登录或者验证码等操作,然后让playwright接管继续操作
  • playwright接管
    from playwright.sync_api import sync_playwright
    
    with sync_playwright() as p:
        bro = p.chromium.connect_over_cdp('http://localhost:8899/')
        # 获取page对象
        page = bro.contexts[0].pages[0]
        #该操作会直接作用在接管的浏览器中
        page.locator('//*[@id="kw"]').fill('haha')
        print(page.url)
        print(page.title())

十一、代理设置

  • 无身份验证

    browser = p.chromium.launch(proxy={'server': 'http://ip:port'})
  • 有身份验证

    browser = p.chromium.launch(proxy={'server': 'http://ip:port', 'username':'用户名', 'password':'密码'})

十二、常用方法

  • 方法

    # 监听事件, 比如close、console、load、request、response等
    page.on(event, callback)
    
    # 请求拦截
    page.route(url, handler) # url可以是普通字符串(必须含url末位字符),也可以是正则pattern字符串,即re.compile(r'xxx')
    
    # 设置页面大小
    page.set_viewport_size({'width':1366, 'height':768})
    
    # 执行js代码
    data1 = page.evaluate('() => window.encrypt("xx", "yyy")')
    data2 = page.evaluate('([a, b]) => a + b', [3, 4]) # 7
    data3 = page.evaluate('10+5') # 15
    
    js_code1 = '''
    var test = function(a,b){window.hello = 20;return a + b}; // 只能使用函数表达式,使用函数声明则会报错
    test(30,60) // 不能使用return
    '''
    js_code2 = '''window.hello'''
    js_code3 = '''
    var a = 20;
    var b = 30;
    a + b
    '''
    print(page.evaluate(js_code1)) # 90
    print(page.evaluate(js_code2)) # 20
    print(page.evaluate(js_code3)) # 50
    
    # 访问具体网站
    page.goto(url)
    
    # 等待页面加载完成
    page.wait_for_load_state(state='networkidle')
    
    # 截图
    page.screenshot(type=None, path=None)
    
    # 点击页面元素
    page.click(selector)
    
    # 获取页面源码
    page.content()
    
    # 获取单个节点
    element = page.query_selector(selector)
    element.get_attribute('属性名') # 获取节点属性
    element.text_content() # 获取节点文本
    
    # 获取多个节点
    elements = page.query_selector_all(selector)
    for ele in elements:
        ele.get_attribute('属性名') # 获取属性
        ele.text_content() # 获取文本
    
    # 文本输入
    # 方式一
    page.fill(selector, value, timeout=None) # 根据选择器,输入文本内容,timeout可设置对应节点的最长等待时间
    # 方式二: 可以先获取节点,然后调用节点的fill()方法

十三、事件监听

Page对象提供了on方法,用来监听页面中的各个事件,比如close、console、load、request、response等

  • 示例

    from playwright.sync_api import sync_playwright
    
    
    def on_response(response):
        # 直接截获ajax请求数据
        if '/api/movie' in response.url and response.status == 200:
            print(response.json())
    
    
    with sync_playwright() as playwright:
        browser = playwright.chromium.launch(headless=False)
        page = browser.new_page()
        # 事件监听,例如close、console、load、request、response等
        page.on('response', on_response)
        page.goto("https://spa6.scrape.center/") # 访问网址
        page.wait_for_load_state(state='networkidle') # 等待当前页面初始化和加载完成
        browser.close()

十四、请求拦截器

  • page.route(url,  handler)

    • 参数1:表示匹配的url
      • 1、可以是完整的url,或者url的后半部分(必须到末位字符)
        • 比如'https://www.baidu.com/'、'www.baidu.com/'、'baidu.com'等均可拦截'https://www.baidu.com/'
      • 2、可以是正则pattern
        • 比如re.compile(r'\.(png|jpg|jpeg)')
    • 参数2:handler(route)
      • 参数route:
        • route.request:返回Request对象
        • route.abort():停止请求
        • route.continue_()
          • 不指定参数,表示继续请求
          • 指定关键字参数url,则表示跳转
            • 其它参数还包括method、headers、postData
        • route.fulfill():修改响应内容
          • 指定body参数,可以直接响应自定义文本内容
          • 指定path参数,可响应指定的本地文件
          • 其它参数还包括status、headers、json、contentType、response
  • 示例

    import re
    from playwright.sync_api import sync_playwright
    
    
    with sync_playwright() as playwright:
        browser = playwright.chromium.launch(headless=False)
        page = browser.new_page()
        # 停止请求
        page.route(re.compile(r'\.(png|jpg|jpeg)'), lambda route: route.abort()) # 过滤图片
    
        # 跳转请求
        page.route('https://spa6.scrape.center/', lambda route: route.continue_(url='https://www.baidu.com/'))
    
        # 用给定的内容响应
        # page.route('https://www.zhihu.com/', lambda route: route.fulfill(body='指定返回的内容'))
        page.route('https://www.zhihu.com/', lambda route: route.fulfill(path='./test.html'))
    
        page.goto("https://spa6.scrape.center/") # 访问网址
        page.wait_for_load_state(state='networkidle') # 等待当前页面初始化和加载完成
        page.goto('https://www.zhihu.com/')
        page.wait_for_load_state(state='networkidle')
        print(page.content())
        browser.close()

十五、浏览器环境JS代码模拟执行(辅助逆向) 

  • 思路

    • 找到js中加密函数所在位置,在代码中将其赋值给window对象的某个属性,只要不和现有属性冲突即可(比如encrypt)
    • 将修改后的js保存到本地,通过page.route拦截该js,然后利用保存后的js文件进行本地替换
    • 通过page.goto()访问一次目标网站,使得window对象的赋值被加载执行
    • 模拟调用,比如:
      result = page.evaluate('([x,y]) => {return window.encrypt(x,y)}',['aaa','bbb'])
      # 注意传递参数时,需要使用[x,y]这种形式
posted @ 2023-02-08 01:35  eliwang  阅读(6169)  评论(0)    收藏  举报