爬虫利器Pyppeteer的介绍和使用 爬取京东商城书籍信息

提起 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的介绍和使用 爬取京东商城书籍信息_第1张图片
总而言之,两款浏览器的内核是一样的,实现方式也是一样的,可以认为是开发版和正式版的区别,功能上基本是没有太大区别的。

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

2. Pyppeteer的安装与使用

安装:

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

pip3 install pyppeteer

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

import pyppeteer

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

使用:

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

测试代码如下:

import asyncio
from pyppeteer import launch
import random


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



async def main():
    # 建立一个浏览器对象
    browser = await launch(headless=False)
    # 打开新的标签页
    page = await browser.newPage()
    # 设置网页视图大小
    width, height = screen_size()
    await page.setViewport(viewport={'width': width, 'height': height})
    # 访问目标url网页
    await page.goto('https://www.baidu.com/', options={'timeout': 5 * 1000})
    # 休眠
    await asyncio.sleep(10)
    # 对当前页面截图并保存为example1.png
    await page.screenshot({'path': 'example1.png'})
    # 搜索框输入  python Pyppeteer爬虫
    await page.type('#kw', 'python Pyppeteer爬虫')
    # 点击百度一下
    await page.click('#su')
    # 休眠
    await asyncio.sleep(random.randint(1, 3))
    # 对当前页面截图并保存为example2.png
    await page.screenshot({'path': 'example2.png'})
    # 关闭浏览器
    await browser.close()


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

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

在这里插入图片描述

运行效果如下:
爬虫利器Pyppeteer的介绍和使用 爬取京东商城书籍信息_第2张图片

页面截图:
爬虫利器Pyppeteer的介绍和使用 爬取京东商城书籍信息_第3张图片
爬虫利器Pyppeteer的介绍和使用 爬取京东商城书籍信息_第4张图片

程序成功运行,在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,测试如下:

import asyncio
from pyppeteer import launch


async def main():
    browser = await launch(headless=False, args=['--disable-infobars'])
    page = await browser.newPage()
    await page.setUserAgent("Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5")
    await page.setViewport(viewport={'width': 1536, 'height': 768})
    await page.goto('https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html')
    await asyncio.sleep(25)
    await browser.close()


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

运行效果如下:

爬虫利器Pyppeteer的介绍和使用 爬取京东商城书籍信息_第5张图片

这说明 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 } }) }')  

代码改写如下:

import asyncio
from pyppeteer import launch


async def main():
    browser = await launch(headless=False, args=['--disable-infobars'])
    page = await browser.newPage()
    await page.setUserAgent("Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 (KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5")
    await page.evaluateOnNewDocument('() =>{ Object.defineProperties(navigator,'
                                       '{ webdriver:{ get: () => false } }) }')  
    await page.setViewport(viewport={'width': 1536, 'height': 768})
    await page.goto('https://intoli.com/blog/not-possible-to-block-chrome-headless/chrome-headless-test.html')
    await asyncio.sleep(25)
    await browser.close()


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

运行效果如下:

爬虫利器Pyppeteer的介绍和使用 爬取京东商城书籍信息_第6张图片
可以看到,绕过了 WebDriver 的检测。

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

import asyncio
from pyppeteer import launch

width, height = 1536, 768


async def main():
    browser = await launch(headless=False,
                           args=['--disable-infobars', f'--window-size={width},{height}'])
    context = await browser.createIncognitoBrowserContext()
    page = await context.newPage()
    await page.setViewport({'width': width, 'height': height})
    await page.goto('https://www.baidu.com')
    await asyncio.sleep(5)
    await browser.close()


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爬虫就是用的第三种方法

import asyncio
from pyppeteer import launch
import random
import logging
import openpyxl
import datetime

wb = openpyxl.Workbook()
sheet = wb.active
sheet.append(['book_info', 'price', 'comment', 'shop_name', 'link'])
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s: %(message)s')
start = datetime.datetime.now()
semaphore = asyncio.Semaphore(5)   # 设置信号量   防止一下开启过多浏览器


async def scrape_info(page):
    # 滑动到页面底部   不然刚进入页面只渲染出了30条信息  一半
    await page.evaluate('window.scrollBy(0, document.body.scrollHeight)')
    # 休眠   防止爬取过快被返回首页
    await asyncio.sleep(random.randint(2, 4))
    items = await page.xpath('//*[@id="J_goodsList"]/ul/li')
    for item in items:
        # 捕捉异常   有些没提取到数据   报list index out of range
        try:
            book_info = await item.xpath('.//div[@class="p-name p-name-type-2"]/a/em')
            book_info = await (await book_info[0].getProperty('textContent')).jsonValue()
            price = await item.xpath('.//div[@class="p-price"]/strong/i')
            price = await (await price[0].getProperty('textContent')).jsonValue()
            comment = await item.xpath('.//div[@class="p-commit"]/strong/a')
            comment = await (await comment[0].getProperty('textContent')).jsonValue()
            shop_name = await item.xpath('.//span[@class="J_im_icon"]/a')
            shop_name = await (await shop_name[0].getProperty('textContent')).jsonValue()
            link = await item.xpath('.//div[@class="p-img"]/a')
            link = await (await link[0].getProperty('href')).jsonValue()
            logging.info({'book_info': book_info, 'price': price, 'comment': comment, 'shop_name': shop_name, 'link': link})
            sheet.append([book_info, price, comment, shop_name, link])
        except Exception as e:
            logging.info(e)


async def main(i):
    # 调试好后   headless设置为True  运行不弹出浏览器界面
    async with semaphore:
        browser = await launch(
            {'headless': True}
        )
        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'
        page = await browser.newPage()
        # 设置页面视图大小
        await page.setViewport(viewport={'width': 1366, 'height': 768})
        # 设置请求头
        await page.setUserAgent(
            "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 (KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1")
        # 超时时间  设置为6s
        await page.goto(url, options={'timeout': 6 * 1000})
        await asyncio.sleep(random.randint(1, 3))
        # 调用函数抓取数据
        await scrape_info(page)
        # 关闭浏览器
        await browser.close()


if __name__ == '__main__':
    # 构造参数
    pages = [i for i in range(1, 200) if i % 2 != 0]
    size = [i * 60 + 1 for i in range(100)]
    scrape_index_tasks = [asyncio.ensure_future(main(index)) for index in range(0, 100)]
    loop = asyncio.get_event_loop()
    tasks = asyncio.gather(*scrape_index_tasks)
    loop.run_until_complete(tasks)
    wb.save('book_info.xlsx')
    delta = (datetime.datetime.now() - start).total_seconds()
    print("用时:{:.3f}s".format(delta))

运行效果如下:


爬虫利器Pyppeteer的介绍和使用 爬取京东商城书籍信息_第7张图片

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

你可能感兴趣的:(python,爬虫,python,Pyppeteer,异步爬虫,京东商品数据)