12

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

 3 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzI1MTE2ODg4MA%3D%3D&%3Bmid=2650074901&%3Bidx=1&%3Bsn=5ffe2c8f735bbd9fe203ec4a8d2bb917
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

编辑: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 由不同深度的蓝色构成。

imuARra.png!mobile

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

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

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

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

2. Pyppeteer 的安装与使用

Pyppeteer 的安装

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

pip3 install pyppeteer

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

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 浏览器

vaIVzi6.png!mobile

运行效果如下:

2mamUjr.png!mobile

页面截图:

faMjQf2.png!mobileiyQ7fmY.png!mobile

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

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

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

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 参数将其禁用,禁用操作如下:

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://intoli.com/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())

运行效果如下:

JRnMjuY.png!mobile

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

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

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

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

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

await page.evaluateOnNewDocument('() =>{ Object.defineProperties(navigator,'
'{ 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://intoli.com/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())

运行效果如下:

mIfuA3V.png!mobile

可以看到,绕过了 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.zhangkunzhi.com/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. exceptExceptionas 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+1for 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))

运行效果如下:

JBNfm2B.png!mobile

aEVfAf6.png!mobile

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

近期文章


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK