分享

爬虫神器 Pyppeteer 介绍及爬取某商城实战

 liqualife 2020-11-03

重磅干货,第一时间送达

作者:叶庭云,来自读者投稿

编辑:Lemon

出品:Python数据之道

提起 selenium 想必大家都不陌生,作为一款知名的 Web 自动化测试框架,selenium 支持多款主流浏览器,提供了功能丰富的API 接口,经常被我们用作爬虫工具来使用。

但是 selenium 的缺点也很明显,比如速度太慢、对版本配置要求严苛,最麻烦是经常要更新对应的驱动。还有些网页是可以检测到是否是使用了selenium 。并且selenium 所谓的保护机制不允许跨域 cookies 保存以及登录的时候必须先打开网页然后后加载 cookies 再刷新的方式很不友好。

今天给大家介绍另一款 web 自动化测试工具 Pyppeteer,虽然支持的浏览器比较单一,但在安装配置的便利性和运行效率上相比selenium要好。

介绍 Pyppeteer 之前先说一下 Puppeteer,Puppeteer 是 Google 基于 Node.js 开发的一个工具,主要是用来操纵 Chrome 浏览器的 API,通过 Javascript 代码来操纵 Chrome 浏览器的一些操作,用作网络爬虫进行数据爬取、Web 程序自动测试等任务。

其 API 极其完善,功能非常强大。而 Pyppeteer 实际上是 Puppeteer 的 Python 版本的实现,但他不是 Google 开发的,是一位来自于日本的工程师依据 Puppeteer 的一些功能开发出来的非官方版本。

1. 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 由不同深度的蓝色构成。

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

Pyppeteer 就是依赖于 Chromium 这个浏览器来运行的。在有了 Pyppeteer 之后,就可以免去那些烦琐的环境配置等问题。

第一次运行的时候,如果Chromium 浏览器没有安装,那么程序会帮我们自动安装和配置,就免去了烦琐的环境配置等工作。

另外 Pyppeteer 是基于Python 的新特性 async 实现的,所以它的一些执行也支持异步操作,效率相对于 Selenium 来说也提高了。

2. Pyppeteer 的安装与使用

Pyppeteer 的安装

由于 Pyppeteer 采用了 Python 的 async 机制,所以其运行要求的 Python 版本为 3.5 及以上。安装方式很简单,命令行 pip 安装即可。

  1. pip3 install pyppeteer

安装完成之后在命令行测试:

  1. import pyppeteer

如果没有报错,那就证明安装成功了。

Pyppeteer 的基本使用

Pyppeteer 是一款非常高效的 web 自动化测试工具,由于 Pyppeteer 是基于 asyncio 构建的,它的所有属性和方法几乎都是 coroutine (协程) 对象,因此在构建异步程序的时候非常方便,天生就支持异步运行。

测试代码如下:

  1. import asyncio

  2. from pyppeteer import launch

  3. import random

  4. def screen_size():

  5. # 使用tkinter获取屏幕大小

  6. import tkinter

  7. tk = tkinter.Tk()

  8. width = tk.winfo_screenwidth()

  9. height = tk.winfo_screenheight()

  10. tk.quit()

  11. return width, height

  12. async def main():

  13. # 建立一个浏览器对象

  14. browser = await launch(headless=False)

  15. # 打开新的标签页

  16. page = await browser.newPage()

  17. # 设置网页视图大小

  18. width, height = screen_size()

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

  20. # 访问目标url网页

  21. await page.goto('https://www.baidu.com/', options={'timeout': 5 * 1000})

  22. # 休眠

  23. await asyncio.sleep(10)

  24. # 对当前页面截图并保存为example1.png

  25. await page.screenshot({'path': 'example1.png'})

  26. # 搜索框输入 python Pyppeteer爬虫

  27. await page.type('#kw', 'python Pyppeteer爬虫')

  28. # 点击百度一下

  29. await page.click('#su')

  30. # 休眠

  31. await asyncio.sleep(random.randint(1, 3))

  32. # 对当前页面截图并保存为example2.png

  33. await page.screenshot({'path': 'example2.png'})

  34. # 关闭浏览器

  35. await browser.close()

  36. asyncio.get_event_loop().run_until_complete(main())

第一次使用 pyppeteer 的时候会自动下载并安装 chromium 浏览器

运行效果如下:

页面截图:

 

程序成功运行,在main函数中进行的操作有,初始化一个浏览器对象,然后打开新的标签页,设置页面视图大小,访问百度主页,对当前页面截图并保存为example1.png,然后模拟在搜索框中输入'python Pyppeteer爬虫',模拟点击百度一下,跳转到搜索结果网页,再对当前页面截图并保存为example2.png,最后关闭浏览器。

pyppeteer是基于asyncio 构建的,所以在使用的时候要用到 async/await 结构。

用Pyppeteer启动浏览器,调用 launch 方法即可实现。

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

可以看到它处于 launcher 模块中,参数没有在声明中特别指定,返回类型是 browser 模块中的 Browser 对象,另外查看其源码发现这是一个 async 修饰的方法,所以调用它的时候需要使用 await。

常用参数:

  • headless (bool):是否启用 Headless 模式,即无界面模式,如果 devtools 这个参数是 True 的话,那么该参数就会被设置为 False,否则为 True,即默认是开启无界面模式的

  • devtools (bool):是否为每一个页面自动开启调试工具,默认是 False。如果这个参数设置为 True,那么 headless 参数就会无效,会被强制设置为 False。

  • args (List[str]):在执行过程中可以传入的额外参数。

  • userDataDir (str):即用户数据文件夹,即可以保留一些个性化配置和操作记录。

  • loop (asyncio.AbstractEventLoop):事件循环对象。

  • executablePath (str):可执行文件的路径,如果指定之后就不需要使用默认的 Chromium 了,可以指定为已有的 Chrome 或 Chromium。

  • env (dict):环境变量,可以通过字典形式传入。

禁用提示条

在之前运行效果图中,我们可以看到上面的一条提示:'Chrome 正受到自动测试软件的控制',不喜欢这个提示出现的话,我们可以利用 args 参数将其禁用,禁用操作如下:

  1. browser = await launch(headless=False, args=['--disable-infobars'])

修改网站检测浏览器特征值

只是把提示关闭了,有些网站还是会检测到是 WebDriver,测试如下:

  1. import asyncio

  2. from pyppeteer import launch

  3. async def main():

  4. browser = await launch(headless=False, args=['--disable-infobars'])

  5. page = await browser.newPage()

  6. await page.setUserAgent('Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5')

  7. await page.setViewport(viewport={'width': 1536, 'height': 768})

  8. await page.goto('https:///blog/not-possible-to-block-chrome-headless/chrome-headless-test.html')

  9. await asyncio.sleep(25)

  10. await browser.close()

  11. asyncio.get_event_loop().run_until_complete(main())

运行效果如下:

这说明 Pyppeteer 开启 Chromium 照样还是能被检测到 WebDriver 的存在。

无论是 selenium 的 execute_script() 方法,还是 pyppeteer 的 evaluate() 方法执行下面代码都能临时修改浏览器属性中的 webdriver 属性,当页面刷新或者跳转之后该值就会原形毕露。

  1. () =>{ Object.defineProperties(navigator,{ webdriver:{ get: () => false } }) }

但是 pyppeteer 的最底层是封装的puppeteer,是 js 库,是和网站源码交互最深的方式。

在 pyppeteer 中提供了一个方法:evaluateOnNewDocument(),该方法是将一段 js 代码加载到页面文档中,当发生页面导航、页面内嵌框架导航的时候加载的 js 代码会自动执行,那么当页面刷新的时候该 js 也会执行,这样就保证了修改网站的属性持久化的目的。

  1. await page.evaluateOnNewDocument('() =>{ Object.defineProperties(navigator,'

  2. '{ webdriver:{ get: () => false } }) }')

代码改写如下:

  1. import asyncio

  2. from pyppeteer import launch

  3. async def main():

  4. browser = await launch(headless=False, args=['--disable-infobars'])

  5. page = await browser.newPage()

  6. await page.setUserAgent('Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5')

  7. await page.evaluateOnNewDocument('() =>{ Object.defineProperties(navigator,'

  8. '{ webdriver:{ get: () => false } }) }')

  9. await page.setViewport(viewport={'width': 1536, 'height': 768})

  10. await page.goto('https:///blog/not-possible-to-block-chrome-headless/chrome-headless-test.html')

  11. await asyncio.sleep(25)

  12. await browser.close()

  13. asyncio.get_event_loop().run_until_complete(main())

运行效果如下:

可以看到,绕过了 WebDriver 的检测。

开启无痕模式

Chrome 浏览器是可以开无痕模式的,它的好处就是环境比较干净,不与其他的浏览器示例共享 Cache、Cookies 等内容,其开启方式可以通过 createIncognitoBrowserContext 方法,代码如下:

  1. import asyncio

  2. from pyppeteer import launch

  3. width, height = 1536, 768

  4. async def main():

  5. browser = await launch(headless=False,

  6. args=['--disable-infobars', f'--window-size={width},{height}'])

  7. context = await browser.createIncognitoBrowserContext()

  8. page = await context.newPage()

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

  10. await page.goto('https://www.baidu.com')

  11. await asyncio.sleep(5)

  12. await browser.close()

  13. asyncio.get_event_loop().run_until_complete(main())

更多详细使用可以参考如下文档

  • pyppeteer github 地址:https://github.com/miyakogi/pyppeteer

  • pyppeteer 常用方法手册:https://blog./2019/05/13/pyppeteer%E5%B8%B8%E7%94%A8%E6%96%B9%E6%B3%95%E6%89%8B%E5%86%8C/index.html

3. Pyppeteer爬虫实战异步爬取京东商城书籍信息

有些网站的页面是 JavaScript 渲染而成的,我们所看到的内容都是网页加载后又执行了 JavaScript 代码之后才呈现出来的,因此这些数据并不存在于原始 HTML 代码中,而 requests 仅仅抓取的是原始 HTML 代码。

抓取这种类型网站的页面数据,解决方案如下:

  • 分析网页源代码数据,如果数据是隐藏在 HTML 中的其他地方,以 JavaScript 变量的形式存在,直接提取就好了。

  • 分析Ajax,很多数据可能是经过 Ajax 请求时候获取的,所以可以分析其接口。

  • 模拟 JavaScript 渲染过程,直接抓取渲染后的结果。

Pyppeteer 爬虫就是用的第三种方法

  1. import asyncio

  2. from pyppeteer import launch

  3. import random

  4. import logging

  5. import openpyxl

  6. import datetime

  7. wb = openpyxl.Workbook() # 获取工作簿对象

  8. sheet = wb.active # 活动的工作表

  9. # 添加列名

  10. sheet.append(['book_info', 'price', 'comment', 'shop_name', 'link'])

  11. # 日志的基本配置

  12. logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')

  13. start = datetime.datetime.now() # 当前时间

  14. semaphore = asyncio.Semaphore(3) # 设置信号量 防止一下开启过多浏览器卡死

  15. async def scrape_info(page):

  16. # 滑动到页面底部 不然刚进入页面只渲染出了30条信息 一半

  17. await page.evaluate('window.scrollBy(0, document.body.scrollHeight)')

  18. # 休眠 防止爬取过快被返回首页

  19. await asyncio.sleep(random.randint(2, 4))

  20. # Xpath定位提取数据

  21. items = await page.xpath('//*[@id='J_goodsList']/ul/li')

  22. for item in items:

  23. # 捕捉异常 有些没提取到数据 报list index out of range错误

  24. try:

  25. book_info = await item.xpath('.//div[@class='p-name p-name-type-2']/a/em')

  26. book_info = await (await book_info[0].getProperty('textContent')).jsonValue()

  27. price = await item.xpath('.//div[@class='p-price']/strong/i')

  28. price = await (await price[0].getProperty('textContent')).jsonValue()

  29. comment = await item.xpath('.//div[@class='p-commit']/strong/a')

  30. comment = await (await comment[0].getProperty('textContent')).jsonValue()

  31. shop_name = await item.xpath('.//span[@class='J_im_icon']/a')

  32. shop_name = await (await shop_name[0].getProperty('textContent')).jsonValue()

  33. link = await item.xpath('.//div[@class='p-img']/a')

  34. link = await (await link[0].getProperty('href')).jsonValue()

  35. logging.info({'book_info': book_info, 'price': price, 'comment': comment, 'shop_name': shop_name, 'link': link})

  36. sheet.append([book_info, price, comment, shop_name, link])

  37. except Exception as e:

  38. logging.info(e)

  39. async def main(i):

  40. # 调试好后 headless设置为True 运行不弹出浏览器界面

  41. async with semaphore:

  42. browser = await launch(

  43. {'headless': True,

  44. 'dumpio': True

  45. }

  46. )

  47. url = f'https://search.jd.com/Search?keyword=python%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90&wq=python%E6%95%B0%E6%8D%AE%E5%88%86%E6%9E%90&page={pages[i]}&s={size[i]}&click=0'

  48. page = await browser.newPage()

  49. # 设置页面视图大小

  50. await page.setViewport(viewport={'width': 1366, 'height': 768})

  51. # 设置请求头

  52. await page.setUserAgent(

  53. 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1')

  54. # 超时时间 设置为6s

  55. await page.goto(url, options={'timeout': 30 * 1000})

  56. await asyncio.sleep(random.randint(1, 3))

  57. # 调用函数抓取数据

  58. await scrape_info(page)

  59. # 关闭浏览器

  60. await browser.close()

  61. if __name__ == '__main__':

  62. # 构造参数

  63. pages = [i for i in range(1, 200) if i % 2 != 0]

  64. size = [i * 60 + 1 for i in range(100)]

  65. scrape_index_tasks = [asyncio.ensure_future(main(index)) for index in range(0, 100)]

  66. # 创建事件循环

  67. loop = asyncio.get_event_loop()

  68. tasks = asyncio.gather(*scrape_index_tasks)

  69. # 将协程注册到事件循环中

  70. loop.run_until_complete(tasks)

  71. wb.save('book_info.xlsx')

  72. delta = (datetime.datetime.now() - start).total_seconds()

  73. print('用时:{:.3f}s'.format(delta))

运行效果如下:

成功实现利用 Pyppeteer 爬虫异步爬取 100 页的书籍信息保存到Excel,用时249.160s。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多