深入探索Pyppeteer:从振坤行到阳光高考的网页爬取与数据处理实战

279 阅读11分钟

Pyppeteer

反屏蔽

selenium的消除指纹来源于pyppeteer的消除指纹.

所以有的网站仍会检测到消除指纹的selenium并屏蔽你,而此时用pyppeteer即可解决反屏蔽

安装

 pip install pyppeteer

详细用法

官方文档:miyakogi.github.io/pyppeteer/r…

lanuch

使用 Pyppeteer 的第一步便是启动浏览器,首先我们看下怎样启动一个浏览器,其实就相当于我们点击桌面上的浏览器图标一样,把它运行起来。用 Pyppeteer 完成同样的操作,只需要调用 launch 方法即可。

 # 设置启动时是否开启浏览器可视,消除控制条信息
     browser = await launch(headless=False, args=['--disable-infobars'])  # 设置浏览器和添加属性

无头模式

首先可以试用下最常用的参数 headless,如果我们将它设置为 True 或者默认不设置它,在启动的时候我们是看不到任何界面的,如果把它设置为 False,那么在启动的时候就可以看到界面了,一般我们在调试的时候会把它设置为 False,在生产环境上就可以设置为 True,我们先尝试一下关闭 headless 模式:

 await launch(headless=False)

调试模式

另外我们还可以开启调试模式,比如在写爬虫的时候会经常需要分析网页结构还有网络请求,所以开启调试工具还是很有必要的,我们可以将 devtools 参数设置为 True,这样每开启一个界面就会弹出一个调试窗口,非常方便,示例如下:

 browser = await launch(devtools=True)

禁用提示条

这时候我们可以看到上面的一条提示:“Chrome 正受到自动测试软件的控制”,这个提示条有点烦,那该怎样关闭呢?这时候就需要用到 args 参数了,禁用操作如下:

 browser = await launch(headless=False, args=['--disable-infobars'])
防止检测

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

 page.evaluateOnNewDocument('Object.defineProperty(navigator, "webdriver", {get: () => undefined})')

页面大小设置

在上面的例子中,我们还发现了页面的显示 bug,整个浏览器窗口比显示的内容窗口要大,这个是某些页面会出现的情况。

对于这种情况,我们通过设置窗口大小就可以解决,可以通过 Page 的 setViewport 方法设置,代码如下:

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

用户数据持久化

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

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

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

Browser

开启无痕模式

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

 context = await browser.createIncognitoBrowserContext()

关闭

怎样关闭自不用多说了,就是 close 方法,但很多时候我们可能忘记了关闭而造成额外开销,所以要记得在使用完毕之后调用一下 close 方法,示例如下:

 await browser.close()

Page

选择器

Page 对象内置了一些用于选取节点的选择器方法,如 J 方法传入一个选择器 Selector,则能返回对应匹配的第一个节点,等价于 querySelector。如 JJ 方法则是返回符合 Selector 的列表,类似于 querySelectorAll。

下面我们来看下其用法和运行结果,示例如下:

 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.cuiqingcai.com/')
    await page.waitForSelector('.item .name')
    j_result1 = await page.J('.item .name')
    j_result2 = await page.querySelector('.item .name')
    jj_result1 = await page.JJ('.item .name')
    jj_result2 = await page.querySelectorAll('.item .name')
    print('J Result1:', j_result1)
    print('J Result2:', j_result2)
    print('JJ Result1:', jj_result1)
    print('JJ Result2:', jj_result2)
    await browser.close()
  
 asyncio.get_event_loop().run_until_complete(main())
 ​

在这里我们分别调用了 J、querySelector、JJ、querySelectorAll 四个方法,观察下其运行效果和返回结果的类型,运行结果:

 J Result1: <pyppeteer.element_handle.ElementHandle object at 0x1166f7dd0>
 J Result2: <pyppeteer.element_handle.ElementHandle object at 0x1166f07d0>
 JJ Result1: [<pyppeteer.element_handle.ElementHandle object at 0x11677df50>, <pyppeteer.element_handle.ElementHandle object at 0x1167857d0>, <pyppeteer.element_handle.ElementHandle object at 0x116785110>,
 ...
 <pyppeteer.element_handle.ElementHandle object at 0x11679db10>, <pyppeteer.element_handle.ElementHandle object at 0x11679dbd0>]
 JJ Result2: [<pyppeteer.element_handle.ElementHandle object at 0x116794f10>, <pyppeteer.element_handle.ElementHandle object at 0x116794d10>, <pyppeteer.element_handle.ElementHandle object at 0x116794f50>,
 ...
 <pyppeteer.element_handle.ElementHandle object at 0x11679f690>, <pyppeteer.element_handle.ElementHandle object at 0x11679f750>]
 ​

在这里我们可以看到,J、querySelector 一样,返回了单个匹配到的节点,返回类型为 ElementHandle 对象。JJ、querySelectorAll 则返回了节点列表,是 ElementHandle 的列表。

项卡操作

前面我们已经演示了多次新建选项卡的操作了,也就是 newPage 方法,那新建了之后怎样获取和切换呢,下面我们来看一个例子:

 import asyncio
 from pyppeteer import launch
  
 async def main():
    browser = await launch(headless=False)
    page = await browser.newPage()
    await page.goto('https://www.baidu.com')
    page = await browser.newPage()
    await page.goto('https://www.bing.com')
    pages = await browser.pages()
    print('Pages:', pages)
    page1 = pages[1]
    await page1.bringToFront()
    await asyncio.sleep(100)
  
 asyncio.get_event_loop().run_until_complete(main())
 ​

在这里我们启动了 Pyppeteer,然后调用了 newPage 方法新建了两个选项卡并访问了两个网站。那么如果我们要切换选项卡的话,只需要调用 pages 方法即可获取所有的页面,然后选一个页面调用其 bringToFront 方法即可切换到该页面对应的选项卡。

常见操作

作为一个页面,我们一定要有对应的方法来控制,如加载、前进、后退、关闭、保存等,示例如下:

 import asyncio
 from pyppeteer import launch
 from pyquery import PyQuery as pq
  
 async def main():
    browser = await launch(headless=False)
    page = await browser.newPage()
    await page.goto('https://dynamic1.scrape.cuiqingcai.com/')
    await page.goto('https://dynamic2.scrape.cuiqingcai.com/')
    # 后退
    await page.goBack()
    # 前进
    await page.goForward()
    # 刷新
    await page.reload()
    # 保存 PDF
    await page.pdf()
    # 截图
    await page.screenshot()
    # 设置页面 HTML
    await page.setContent('<h2>Hello World</h2>')
    # 设置 User-Agent
    await page.setUserAgent('Python')
    # 设置 Headers
    await page.setExtraHTTPHeaders(headers={})
    # 关闭
    await page.close()
    await browser.close()
  
 asyncio.get_event_loop().run_until_complete(main())
 ​

这里我们介绍了一些常用方法,除了一些常用的操作,这里还介绍了设置 User-Agent、Headers 等功能。

点击

Pyppeteer 同样可以模拟点击,调用其 click 方法即可。示例如下:

 import asyncio
 from pyppeteer import launch
 from pyquery import PyQuery as pq
  
 async def main():
    browser = await launch(headless=False)
    page = await browser.newPage()
    await page.goto('https://dynamic2.scrape.cuiqingcai.com/')
    await page.waitForSelector('.item .name')
    await page.click('.item .name', options={
        'button': 'right',
        'clickCount': 1,  # 1 or 2
        'delay': 3000,  # 毫秒
    })
    await browser.close()
  
 asyncio.get_event_loop().run_until_complete(main())
 ​

这里 click 方法第一个参数就是选择器,即在哪里操作。第二个参数是几项配置:

  • button:鼠标按钮,分为 left、middle、right。
  • clickCount:点击次数,如双击、单击等。
  • delay:延迟点击。
  • 输入文本

对于文本的输入,Pyppeteer 也不在话下,使用 type 方法即可,示例如下:

 import asyncio
 from pyppeteer import launch
 from pyquery import PyQuery as pq
  
 async def main():
    browser = await launch(headless=False)
    page = await browser.newPage()
    await page.goto('https://www.taobao.com')
    # 输入
    await page.type('#q', 'iPad')
    # 关闭
    await asyncio.sleep(10)
    await browser.close()
  
 asyncio.get_event_loop().run_until_complete(main())
 ​

获取信息

Page 获取源代码用 content 方法即可,Cookies 则可以用 cookies 方法获取,示例如下

 import asyncio
 from pyppeteer import launch
 from pyquery import PyQuery as pq
  
 async def main():
    browser = await launch(headless=False)
    page = await browser.newPage()
    await page.goto('https://dynamic2.scrape.cuiqingcai.com/')
    print('HTML:', await page.content())
    print('Cookies:', await page.cookies())
    await browser.close()
  
 asyncio.get_event_loop().run_until_complete(main())
 ​

执行JS

Pyppeteer 可以支持 JavaScript 执行,使用 evaluate 方法即可,看之前的例子:

 import asyncio
 from pyppeteer import launch
  
 width, height = 1366, 768
  
 async def main():
    browser = await launch()
    page = await browser.newPage()
    await page.setViewport({'width': width, 'height': height})
    await page.goto('https://dynamic2.scrape.cuiqingcai.com/')
    await page.waitForSelector('.item .name')
    await asyncio.sleep(2)
    await page.screenshot(path='example.png')
    dimensions = await page.evaluate('''() => {
        return {
            width: document.documentElement.clientWidth,
            height: document.documentElement.clientHeight,
            deviceScaleFactor: window.devicePixelRatio,
        }
    }''')
 ​
    print(dimensions)
    await browser.close()
  
 asyncio.get_event_loop().run_until_complete(main())
 ​

延时等待

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

  • waitForSelector:等待符合选择器的节点加载出来。
  • waitForXPath:等待符合 XPath 的节点加载出来。

消除指纹

 pip install pyppeteer-stealth
 from pyppeteer_stealth import stealth
 ​
 # 消除指纹
 await stealth(page)  # <-- Here

入库

基于异步的pyppeteer的入库可以不用基于异步的mysql(aiomysql)

振坤行案例

技术:基于异步的pyppeteer开多个协程进行爬取数据

 import asyncio  # 协程
 from pyppeteer import launch
 from pyppeteer_stealth import stealth  # 消除指纹
 from lxml import etree  # xpath解析数据
 import pymysql
 import re
 import aiomysql
 ​
 width, height = 1366, 768  # 设置浏览器宽度和高度
 ​
 ​
 # 异步
 async def async_basic(num, detail_url):
     pool = await aiomysql.create_pool(
         host="127.0.0.1",
         port=3306,
         user='root',
         password='123456',
         db='zhenkunxing',
     )
     async with pool.acquire() as conn:
         async with conn.cursor() as cursor:
             for x in range(10000):
                 sql = 'insert into task_urls(type_id,url) values ("{}","{}")'.format(num, detail_url)
                 # 执行sql语句
                 await cursor.execute(sql)
             await conn.commit()
 ​
     # 关闭连接池
     pool.close()
     await pool.wait_closed()
 ​
 ​
 # 爬取此分类url的所有商品的详细信息url网址
 async def run(url):
     # 获取分类id
     global browser
     d = re.compile('https://www.zkh.com/list/c-(\d+).html')
     num = d.findall(url)[0]
     try:
         # 设置启动时是否开启浏览器可视,消除控制条信息
         browser = await launch(headless=False, args=['--disable-infobars'])  # 设置浏览器和添加属性
         # 开启一个页面对象
         page = await browser.newPage()
         # 设置浏览器宽高
         await page.setViewport({'width': width, 'height': height})
         # 消除指纹
         await stealth(page)  # <-- Here
         # 设置浏览器宽高
         await page.setViewport({'width': width, 'height': height})
         # await page.evaluateOnNewDocument('Object.defineProperty(navigator, "webdriver", {get: () => undefined})')
         # 访问某个页面
         current_page = 1
         await page.goto(url)
         while True:
             print(f"scraping {current_page} page start")
             await asyncio.sleep(2)
             # 获取最大页
             max_page_elem = await page.xpath("//b[@class='pagination-page-total']")
             if len(max_page_elem) == 0:
                 break
             # 等待class=key的这个元素出现,等9秒,超过不出现报超时错误
             await page.waitForXPath('//div[@class="goods-item-wrap-new clearfix common-item-wrap"]',
                                     {'timeout': 9000})  # 根据xpath来等待某个节点出现
             # 拉滚动条
             await page.evaluate('window.scrollBy(200, document.body.scrollHeight)')
             await asyncio.sleep(1)
             # 拉滚动条
             await page.evaluate('window.scrollBy(200, document.body.scrollHeight)')
             await asyncio.sleep(1)
             # 拉滚动条
             await page.evaluate('window.scrollBy(200, document.body.scrollHeight)')
             await asyncio.sleep(1)
 ​
             # 等待class=key的这个元素出现,等9秒,超过不出现报超时错误
             await page.waitForXPath('//*[@id="app"]/div/div/div[5]/div[6]/div/div[2]/div/div/div[40]',
                                     {'timeout': 9000})  # 根据xpath来等待某个节点出现
             # 爬取信息
             divs = await page.xpath("//div[@class='goods-item-wrap-new clearfix common-item-wrap']")
             for div in divs:
                 # 应该使用 . 来表示相对路径,这样会从当前的 div 元素开始查找。正确的 XPath 应该是 './/a'
                 a = await div.xpath('./a')
                 # print(a)
                 # 商品详细信息url
                 detail_url = await (await a[0].getProperty("href")).jsonValue()
                 print(f"{num}, {detail_url}")
                 await async_basic(num, detail_url)
             print(f"scraping {current_page} page end")
 ​
             max_page = await (await max_page_elem[0].getProperty("textContent")).jsonValue()
             if current_page == max_page:
                 break
             else:
                 current_page += 1
                 # 等待class=key的这个元素出现,等9秒,超过不出现报超时错误
                 await page.waitForXPath('//button[@class="nextbtn"]',
                                         {'timeout': 9000})  # 根据xpath来等待某个节点出现
                 # 点击下一页
                 await page.click('button.nextbtn', options={
                     'button': 'left',  # 左键点击
                     'clickCount': 1,  # 1 or 2
                     'delay': 3000,  # 毫秒  元素出现后延迟多少毫秒后点击
                 })
     finally:
         await browser.close()
 ​
 ​
 async def main():
     task_urls = ['https://www.zkh.com/list/c-10293630.html', 'https://www.zkh.com/list/c-11004927.html']
 ​
     await asyncio.gather(*[run(_) for _ in task_urls])
 ​
 ​
 asyncio.get_event_loop().run_until_complete(main())
 ​

阳光高考案例

技术:基于异步的pyppeteer进行翻页爬取数据

 import requests
 ​
 from selenium import webdriver
 from selenium.webdriver.chrome.options import Options
 from selenium.webdriver.chrome.service import Service
 from selenium.webdriver.common.by import By
 from selenium.webdriver.common.keys import Keys
 from selenium.webdriver.support import expected_conditions as EC
 from selenium.webdriver.support.wait import WebDriverWait
 import time
 ​
 import asyncio  # 协程
 from pyppeteer import launch
 from pyppeteer_stealth import stealth  # 消除指纹
 from lxml import etree  # xpath解析数据
 ​
 ​
 # 使用selenium,他的stealth.min.js文件是不完全的,他是仿造pyppeteer的stealth的,所以有的网站他是无法访问的
 # options = webdriver.ChromeOptions()
 # # 设置无头模式
 # # options.add_argument('--headless')
 #
 # # selenium新版本: 将谷歌驱动网址用service包裹一下
 # service = Service('D:\extention\spider\day4\chormedriver\chromedriver-win64\chromedriver.exe')
 # # 驱动
 # browser = webdriver.Chrome(service=service, options=options)
 #
 # browser.maximize_window()  # 最大化浏览器窗口
 # with open('stealth.min.js') as f:
 #     js = f.read()
 # browser.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
 #     "source": js
 # })
 # browser.get('https://gaokao.chsi.com.cn/sch/search--ss-on,option-qg,searchType-1,start-0.dhtml')
 ​
 width, height = 1366, 768  # 设置浏览器宽度和高度
 ​
 ​
 #
 async def main():
     # 设置启动时是否开启浏览器可视,消除控制条信息
     browser = await launch(headless=False, args=['--disable-infobars'])  # 设置浏览器和添加属性
     # 开启一个页面对象
     page = await browser.newPage()
     # 设置浏览器宽高
     await page.setViewport({'width': width, 'height': height})
     # 消除指纹
     await stealth(page)  # <-- Here
     # 设置浏览器宽高
     await page.setViewport({'width': width, 'height': height})
     # await page.evaluateOnNewDocument('Object.defineProperty(navigator, "webdriver", {get: () => undefined})')
     # 访问某个页面
 ​
     await page.goto('https://gaokao.chsi.com.cn/')
     await page.waitForXPath('//ul[@class="clearfix margin-b4"]/li[2]/a')  # 根据xpaath来等待某个节点出现
     await page.click('ul.nav-index-list li:nth-child(2) ul:nth-child(1) li:nth-child(2) a', options={
         'button': 'left',  # 左键点击
         'clickCount': 1,  # 1 or 2
         'delay': 3000,  # 毫秒  元素出现后延迟多少毫秒后点击
     })
     for _ in range(3):
         print(f"scraping {_+1} page start")
         await asyncio.sleep(2)
         # 等待元素出现 根据CSS选择器的语法等待某个节点出现,跟pyquery语法差不多
         await page.waitForSelector('div.sch-list-container')
         # 拉滚动条
         # arg1: 文档向右滚动的像素数 arg2: 文档向下滚动的像素数
         await page.evaluate('window.scrollBy(200, document.body.scrollHeight)')
         # 等待最后一个商品出现
         await asyncio.sleep(2)
         await page.waitForXPath('//*[@id="app-yxk-sch-list"]/div[1]/div[20]')
         # 获取页面源码就可以采取 xpath进行数据解析,没必要用pyppeteer的解析方式去解析
         # divs = etree.HTML(await page.content()).xpath('//div[@class="sch-item"]')
         # for div in divs:
         #     title = div.xpath('div[1]/div[1]/a/text()')[0].strip()
         #     print(title)
 ​
         # 这个是不获取页面源码但使用xpath的解析方式去解析
         li_list = await page.xpath('//div[@class="sch-item"]')
         for i in li_list:
             a = await i.xpath('div[1]/div[1]/a')
             b = await i.xpath('div[1]/a[1]')
             # 取商品标题
             title = await (await a[0].getProperty("textContent")).jsonValue()
             desc = await (await b[0].getProperty("textContent")).jsonValue()
             print(title, desc)
         print(f"scraping {_ + 1} page end")
         # 点击下一页 //li[@class='lip'][3]/a[@class='changepage-icon']
         await page.click('ul.ch-page li:nth-child(9) a.changepage-icon', options={
             'button': 'left',  # 左键点击
             'clickCount': 1,  # 1 or 2
             'delay': 3000,  # 毫秒  元素出现后延迟多少毫秒后点击
         })
     await asyncio.sleep(100)
 ​
 asyncio.get_event_loop().run_until_complete(main())
 ​

OCR识别

这里先简单介绍一下免费的OCR识别库ddddocr

后续会介绍百度OCR识别以及如何训练OCR

安装

 pip install ddddocr
 pip install ddddocr -i https://pypi.tuna.tsinghua.edu.cn/simple/

使用

 import ddddocr
 from PIL import Image
 im = Image.open("D:\1\资料\项目列\87\code.png")
 try:
     #放大图片
     image=im.resize((3000,300))
     #将新图像保存至桌面
     image.save("D:\1\资料\项目列\87\big.png")
     print("查看新图像的尺寸",image.size)
 except IOError:
     print("放大图像失败")
 ​
 ocr = ddddocr.DdddOcr()
 with open('big.png', 'rb') as f:
     img_bytes = f.read()
 res = ocr.classification(img_bytes)
 print('识别出的验证码为:' + res)

更多精致内容:[CodeRealm]

公众号.png