python模块详解 | pyppeteer

Selenium & Pyppeteer

selenium的功能十分强大,但很多时候我们发现他也有其自身的缺点,比如:

  1. 需要安装好相关的浏览器,Chrome或Firefox等
  2. 需要下载并配置对应版本的驱动

因此如果要做大规模部署的话,环境配置方面其实是一个头疼的事情。其实有另一个类似的替代品,叫作 Pyppeteer。

Pyppeteer 介绍

Puppeteer 是 Google 基于 Node.js 开发的一个工具,有了它我们可以通过 JavaScript 来控制 Chrome 浏览器的一些操作,当然也可以用作网络爬虫上,其 API 极其完善,功能非常强大,Selenium 当然同样可以做到。

而 Pyppeteer 又是什么呢?它实际上是 Puppeteer 的 Python 版本的实现,但它不是 Google 开发的,是一位来自于日本的工程师依据 Puppeteer 的一些功能开发出来的非官方版本。

在 Pyppetter 中,实际上它背后也是有一个类似 Chrome 浏览器的 Chromium 浏览器在执行一些动作进行网页渲染,首先说下 Chrome 浏览器和 Chromium 浏览器的渊源。

Chromium 是谷歌为了研发 Chrome 而启动的项目,是完全开源的。二者基于相同的源代码构建,Chrome 所有的新功能都会先在 Chromium 上实现,待验证稳定后才会移植,因此 Chromium 的版本更新频率更高,也会包含很多新的功能,但作为一款独立的浏览器,Chromium 的用户群体要小众得多。两款浏览器“同根同源”,它们有着同样的 Logo,但配色不同,Chrome 由蓝红绿黄四种颜色组成,而 Chromium 由不同深度的蓝色构成。

WechatIMG526

总的来说,两款浏览器的内核是一样的,实现方式也是一样的,可以认为是开发版和正式版的区别,功能上基本是没有太大区别的。

Pyppeteer 就是依赖于 Chromium 这个浏览器来运行的。那么有了 Pyppeteer 之后,我们就可以免去那些烦琐的环境配置等问题。如果第一次运行的时候,Chromium 浏览器没有安装,那么程序会帮我们自动安装和配置,就免去了烦琐的环境配置等工作。另外 Pyppeteer 是基于 Python 的新特性 async 实现的,所以它的一些执行也支持异步操作,效率相对于 Selenium 来说也提高了。

由于 Pyppeteer 采用了 Python 的 async 机制,所以其运行要求的 Python 版本为 3.5 及以上。

pip install pyppeteer

快速上手

import asyncio
from pyppeteer import launch
from pyquery import PyQuery as pq
async def main():
   browser = await launch()
   page = await browser.newPage()
   await page.goto('https://dynamic2.scrape.center/')
   await page.waitForSelector('.item .name')
   doc = pq(await page.content())
   names = [item.text() for item in doc('.item .name').items()]
   print('Names:', names)
   await browser.close()
  
asyncio.get_event_loop().run_until_complete(main())

官方文档 - https://miyakogi.github.io/pyppeteer/reference.html

launch

使用 Pyppeteer 的第一步便是启动浏览器,在puppeteer中需要调用「launch」方法去实现,launch方法的API:

pyppeteer.launcher.launch(options: dict = None, **kwargs) → pyppeteer.browser.Browser

  • ignoreHTTPSErrors (bool):是否要忽略 HTTPS 的错误,默认是 False。
  • headless (bool):是否启用 Headless 模式,即无界面模式,如果 devtools 这个参数是 True 的话,那么该参数就会被设置为 False,否则为 True,即默认是开启无界面模式的。
  • executablePath (str):可执行文件的路径,如果指定之后就不需要使用默认的 Chromium 了,可以指定为已有的 Chrome 或 Chromium。
  • slowMo (int|float):通过传入指定的时间,可以减缓 Pyppeteer 的一些模拟操作。
  • args (List[str]):在执行过程中可以传入的额外参数。
  • ignoreDefaultArgs (bool):不使用 Pyppeteer 的默认参数,如果使用了这个参数,那么最好通过 args 参数来设定一些参数,否则可能会出现一些意想不到的问题。这个参数相对比较危险,慎用。
  • handleSIGINT (bool):是否响应 SIGINT 信号,也就是可以使用 Ctrl + C 来终止浏览器程序,默认是 True。
  • handleSIGTERM (bool):是否响应 SIGTERM 信号,一般是 kill 命令,默认是 True。
  • handleSIGHUP (bool):是否响应 SIGHUP 信号,即挂起信号,比如终端退出操作,默认是 True。
  • dumpio (bool):是否将 Pyppeteer 的输出内容传给 process.stdout 和 process.stderr 对象,默认是 False。
  • userDataDir (str):即用户数据文件夹,即可以保留一些个性化配置和操作记录。
  • env (dict):环境变量,可以通过字典形式传入。
  • devtools (bool):是否为每一个页面自动开启调试工具,默认是 False。如果这个参数设置为 True,那么 headless 参数就会无效,会被强制设置为 False。
  • logLevel (int|str):日志级别,默认和 root logger 对象的级别相同。
  • autoClose (bool):当一些命令执行完之后,是否自动关闭浏览器,默认是 True。
  • loop (asyncio.AbstractEventLoop):事件循环对象。

观察launch方法源码发现他是被async修饰的,是一个协程对象,因此使用时需要加上await关键字。

无头模式

headless参数默认是设为True,也就是不显示浏览器界面,改为False便可以看到界面了,一般我们在开发阶段需要调试的时候会将其设置为False,在生产环境下就可以设置为True。

browser = await launch(headless=False)

禁用"Chrome 正受到自动测试软件的控制"提示条

browser = await launch(args=['--disable-infobars'])

浏览器大小设置

在初始化时加入 defaultViewport字典值即可:

browser = await pyppeteer.launch(
        {'headless': False,
         'userDataDir': UserDataDir,
         'defaultViewport': {'width': 1800, 'height': 1000}
         }
    )

or

browser = await pyppeteer.launch(
  headless=False,
  args=['--disable-infobars', f'--window-size={width},{height}']
)

附上获取屏幕大小的代码:

def screen_size():
    """使用tkinter获取屏幕大小"""
    import tkinter
    tk = tkinter.Tk()
    width = tk.winfo_screenwidth()
    height = tk.winfo_screenheight()
    tk.quit()
    return width, height

用户数据持久化 🌹

每次我们打开 Pyppeteer 的时候都是一个新的空白的浏览器。而且如果遇到了需要登录的网页之后,如果我们这次登录上了,下一次再启动又是空白了,又得登录一次,这的确是一个问题。

比如以淘宝举例,平时我们逛淘宝的时候,在很多情况下关闭了浏览器再打开,淘宝依然还是登录状态。这是因为淘宝的一些关键 Cookies 已经保存到本地了,下次登录的时候可以直接读取并保持登录状态。

那么这些信息保存在哪里了呢?其实就是保存在用户目录下了,里面不仅包含了浏览器的基本配置信息,还有一些 Cache、Cookies 等各种信息都在里面,如果我们能在浏览器启动的时候读取这些信息,那么启动的时候就可以恢复一些历史记录甚至一些登录状态信息了。

这也就解决了一个问题:很多时候你在每次启动 Selenium 或 Pyppeteer 的时候总是一个全新的浏览器,那这究其原因就是没有设置用户目录,如果设置了它,每次打开就不再是一个全新的浏览器了,它可以恢复之前的历史记录,也可以恢复很多网站的登录信息。

那么这个怎么来做呢?很简单,在启动的时候设置 userDataDir 就好了,示例如下:

import asyncio
from pyppeteer import launch
 
async def main():
   browser = await launch(headless=False, userDataDir='./userdata', args=['--disable-infobars'])
   page = await browser.newPage()
   await page.goto('https://www.taobao.com')
   await asyncio.sleep(100)
 
asyncio.get_event_loop().run_until_complete(main())

具体的介绍可以看官方的一些说明,如: https://chromium.googlesource.com/chromium/src/+/master/docs/user_data_dir.md,这里面介绍了 userdatadir 的相关内容。

再次运行上面的代码,这时候可以发现现在就已经是登录状态了,不需要再次登录了,这样就成功跳过了登录的流程。当然可能时间太久了,Cookies 都过期了,那还是需要登录的。

browser

launch 方法,其返回的就是一个 Browser 对象,即浏览器对象,Browser 类的定义:

class pyppeteer.browser.Browser(connection: pyppeteer.connection.Connection, contextIds: List[str], ignoreHTTPSErrors: bool, setDefaultViewport: bool, process: Optional[subprocess.Popen] = None, closeCallback: Callable[[], Awaitable[None]] = None, **kwargs)

开启无痕模式

browser = await launch(headless=False)
context = await browser.createIncognitoBrowserContext()
page = await context.newPage()

关闭

await browser.close()

page

页面大小设置

窗口大小可以通过 Page 的 setViewport 方法设置:

await page.setViewport({'width':width,'height':height})

防止检测

Pyppeteer 的 Page 对象有一个方法叫作 evaluateOnNewDocument,意思就是在每次加载网页的时候执行某个语句,所以这里我们可以执行一下将 WebDriver 隐藏的命令,改写如下:

import asyncio
from pyppeteer import launch
 
async def main():
   browser = await launch(headless=False, args=['--disable-infobars'])
   page = await browser.newPage()
   await page.evaluateOnNewDocument('Object.defineProperty(navigator, "webdriver", {get: () => undefined})')
   await page.goto('https://antispider1.scrape.center/')
   await asyncio.sleep(100)
 
asyncio.get_event_loop().run_until_complete(main())

选项卡操作

  • page = browser.newPage() - 新建
  • page.pages() - 获取所有的页面
  • page.bringToFront() - 切换到该页面的对应的选项卡
  • page.goBack() - 后退
  • page.goForward() - 前进
  • page.reload() - 刷新
  • page.pdf() - 保存PDF
  • page.screenshot() - 截图
    • path(str):保存图像的文件路径。屏幕截图类型将从文件扩展名中推断出来。
    • type(str):指定屏幕截图类型,可以是 jpegpng。默认为 png
    • quality(int):图像的质量,在 0-100 之间。不适用于 png 图像。
    • fullPage(bool):如果为 true,请截取完整的可滚动页面。默认为 False
    • clip(字典):指定页面剪切区域的对象。此选项应包含以下字段:
      • x (int):剪辑区域左上角的 x 坐标。
      • y (int):剪辑区域左上角的 y 坐标。
      • width (int):剪切区域的宽度。
      • height (int):剪切区域的高度。
    • omitBackground (bool):隐藏默认的白色背景并允许捕获具有透明度的屏幕截图。
    • encoding(str):图像的编码可以是 'base64''binary'。默认为 'binary'
  • page.setViewport() - 设置页面大小
    • width (int):以像素为单位的页面宽度。
    • height (int):以像素为单位的页面高度。
    • deviceScaleFactor (float):默认为 1.0。
    • isMobile(bool):默认为 False
    • hasTouch(bool):默认为 False
    • isLandscape(bool):默认为 False
  • page.setContent() - 设置该页面HTML
  • page.setCookies() - 设置cookies
  • page.setUserAgent() - 设置User Agent
  • page.setExtraHTTPHeaders(headers={}) - 设置headers
  • page.evaluate() - 执行js语句
  • page.close() - 关闭

选项卡信息

  • page.content() - 获取源代码
  • page.cookies() - 获取cookies
  • page.title() - 获取页面标签

选择器

  • page.waitForSelector('.item .name') - 等待元素出现
  • page.J()/page.querySelector() - css选择器,选取第一个节点
  • page.JJ()/page.querySelectorAll() - css选择器,选取所有节点
  • page.Jeval()/page.querySelectorEval() - 功能多一点,可以选出网页文本或者属性指
  • page.Jx()/page.xpath() - xpath选择器
  • page.addScriptTag() - 将脚本标记添加到此页面, 返回 ElementHandle 其中一个 url,path 或 content 选择是必要的
    • url (字符串):要添加的脚本的 URL。
    • path (字符串):要添加的本地 JavaScript 文件的路径。
    • content (字符串):要添加的 JavaScript 字符串。
    • type(字符串):脚本类型。使用 module 以加载一个 JavaScript ES6 模块。
  • page.addStyleTag() - 将样式或链接标记添加到此页面, 返回 ElementHandle 其中一个 urlpathcontent 选择是必要的。
    • url (字符串):要添加的链接标记的 URL。
    • path (字符串):要添加的本地 CSS 文件的路径。
    • content (字符串):要添加的 CSS 字符串。

点击

await asyncio.gather(
    page.waitForNavigation(waitOptions),
    page.click(
      selector='', 
      clickOptions={
        'button':'left', # left or right
        'clickCount':1, # 1 or 2
        'delay':3000 # 毫秒
      }
    )
)
  • selector - css选择器语句
  • clickOptions - 选项参数
    • button - 鼠标按钮,left、middle、right
    • clickCount - 点击次数,1 or 2
    • delay - 延迟点击,单位毫秒

输入文本

  • page.type() - 第一个参数为 css 选择器,第二个为文本内容
page.type('.item .username','chenxuefan')

延迟等待

等待某些符合条件的节点加载出来再返回

  • page.waitFor() - 通用的等待方法

  • page.waitForFunction() - 等待某个 JavaScript 方法执行完毕或返回结果

  • page.waitForNavigation() - 等待页面跳转,如果没加载出来就会报错

    navigationPromise = async.ensure_future(page.waitForNavigation())
    await page.click('a.my-link')  # indirectly cause a navigation
    await navigationPromise  # wait until navigation finishes
    # or
    await asyncio.wait([
        page.click('a.my-link'),
        page.waitForNavigation(),
    ])
    
  • page.waitForRequest() - 等待某个特定的请求被发出

  • page.waitForResponse() - 等待某个特定的请求收到了回应

  • page.waitForSelector() - 等待符合选择器的节点加载出来

  • page.waitForPath() - 等待符合 XPath 的节点加载出来

更多参考


3921 字