Python爬虫之JavaScript动态渲染页面爬取(Pyppeteer的使用)

JavaScript动态渲染页面爬取之Pyppeteer的使用

JavaScript动态渲染的页面不止Ajax一种。例如有些页面的分页部分由JavaScript生成,非原始HTML代码。

为了解决这些问题,我们可以直接模拟浏览器运行,然后爬取数据,这样就可以实现所见即所爬。

Python提供了许多模拟浏览器运行的库,例如Selenium、Splash、Pyppeteer、Playwright等。

一、Pyppeteer的使用

在很多情况下,Ajax请求的接口含有加密参数,例如token、sign等。由于请求Ajax接口时必须加上token参数,因此得深入分析并找到token参数的构造逻辑,难以模拟请求。

因此,模拟浏览器的运行,爬取数据即可解决。

1、Pyppeteer介绍

Pyppeteer依赖Chromium浏览器运行的。如果第一次运行Pyppeteer的时候,没有安装Chromium浏览器,程序会自动帮我们自动安装和配置好,另外,Pyppeteer是基于Python的新特性asnc实现的,所以它的一些操作执行也支持异步方式。

2、安装

pip3 install Pyppeteer

3、快速上手

import asyncio
from pyppeteer import launch
from pyquery import PyQuery as pq

async def main():
    browser = await launch()	# 新建一个browser对象。相当于启动浏览器
    page = await browser.newPage()	#新建一个page对象并赋值给page变量,这相当于在浏览器中新建了一个选项卡,但还未访问任何页面。
    
    await page.goto('https://spa2.scrape.center/') # 调用page的goto方法,相当于在浏览器中输入page方法参数中的URL,浏览器加载对应的页面
    
    await page.waitForSelector('.item .name')	#调用page的waitForSelector方法,传入选择器,页面就会等待选择器对应的节点信息加载出来后立即返回,否则等待直到超时。
    
    doc = pq(await page.content())	# 页面加载出来后,调用content方法,获取当前浏览器的源代码,这就是JavaScript渲染后的结果
    
    names = [item.text() for item in doc('.item .name').items()]	# 使用pyquery解析页面,提取信息
    print('Names:',names)
    await browser.close()
asyncio.get_event_loop().run_until_complete(main())
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://spa2.scrape.center/')
    await page.waitForSelector('.item .name')
    await asyncio.sleep(2)
    await page.screenshot(path='example.png')	# 保存页面截图
    dimensions = await page.evaluate('''() => {	# 执行JavaScript语句并返回对应的数据
        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())

在screenshot方法里,通过path参数用于传入页面截图的保存路径,另外还可以指定截图的格式type、清晰度quality、是否全屏fullPage和裁切clip等参数。

总之、利用pyppeteer可以控制浏览器执行几乎所有想实现的操作和功能。

4、launch方法

使用pyppeteer的第一步就是启动浏览器。调用launch方法即可。

launch方法的API:

pyppeteer,launcher.launch(options:dic = None,**kwargs) -> pyppeteer.browser.Browser
# 观察源码可以发现,这是一个async修饰的方法,所以在调用的时候要加await

launch方法的参数

  • ignoreHTTPSErrors(bool):是否忽略HTTPS的错误,默认是False。
  • headless(bool):是否启用无头模式,即无界面模式。如果devtools参数是True,该参数会被设置为False,否则为True,即默认开启无界面模式
  • executablePath(str):可执行文件的路径。指定该参数之后就不需要使用默认的Chromium浏览器了,可以指定已有的Chrome或Chromium。
  • slowMo(int|float):通过传入指定的时间,可以减缓Pyppeteer的一些模拟操作。
  • args(List|float):在执行过程中可以传入额外参数。
  • 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):事件循环对象

5、无头模式

import asyncio
from pyppeteer import launch

async def main():
    await launch(headless=False)	# 设为False,启动时就能看见界面了
    await asyncio.sleep(100)
asyncio.get_event_loop().run_until_complete(main())

6、调试模式

在写爬虫的时候会经常需要分析网页结构和网络请求,所以开启调试模式是非常有必要的。

import asyncio
from pyppeteer import launch

async def main():
    browser = await launch(devtools=True)
    page = await browser.newPage()
    await page.goto('https://www.baidu.com')
    await asyncio.sleep(100)
asyncio.get_event_loop().run_until_complete(main())

刚才说过,如果devtools参数设置为True,无头模式就会关闭,界面始终会显示出来。

7、禁用提示条

可以看到在第5点上有一个提示“Chrome正受到自动测试软件的控制”,用args参数去除。

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

8、防止检测

刚刚只是提示关闭了,有些网站还是能检测到Webdriver属性。

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())

可以看到整个页面成功加载出来了,绕过了对Webdriver属性的检测

9、页面大小的设置

在上述,可以发现页面的显示BUG,整个浏览器的窗口要比显示内容的窗口大,这个情况并非每个页面都会出现。调用Page对象的setViewport方法可以设置窗口大小:

import asyncio
from pyppeteer import launch

width,height = 1366,768

async def main():
    browser = await launch(headless=False,args=['--disable-infobars',f'--window-size={width},{height}'])
    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())

10、用户数据持久化

我们看到,每次打开pyppeteer的时候,都是一个新的空白浏览器。如果网页需要登录,那得反复登录!!

设置用户目录即可解决:

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://taobao.com')
    await asyncio.sleep(100)
asyncio.get_event_loop().run_until_complete(main())

这里将userData属性的值设置为了./userdata,即当前目录的userdata文件夹。关于这个文件夹,具体看https://chromium.googlesource.com/chromium/sec/+/master/docs/user_data_dir.md

以上是launch方法及其对应参数的配置。

11、Browser

我们了解launch方法,它的返回值是一个Browser对象,即浏览器对象,我们通常会赋值给browser变量(其实就是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)

12、开启无痕模式

可以通过createIncognitoBrowserConText方法开启无痕模式:

import asyncio
from pyppeteer import launch

width,height = 1200,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(100)

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

13、关闭

close方法关闭浏览器:

async def main():
    browser = await launch()
    page = await browser.newPage()
    await page.goto('https://spa2.scrape.center/')
    await browser.close()
    
asyncio.get_event_loop().run_until_complete(main())

14、Page

Page即页面,对应一个网页、一个选项卡。

14.1、选择器

Page对象内置了很多用于选取节点的选择器方法,例如J方法,给它传入一个选择器,就能返回匹配到的第一个节点,等价于querySelector方法;又如JJ方法,给它传入选择器,会返回符合选择器的所有节点组成的列表,等价于queySelectorAll方法。

import asyncio
from pyppeteer import launch

async def main():
    browser = await launch()
    page = await browser.newPage()
    await page.goto('https://spa2.scrape.center/')
    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())

14.2、选项卡操作

新建选项卡后,先调用pages方法获取所有打开的页面,然后选择一个页面调用其bringToFront方法即可切换页面。

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方法新建了两个选项卡,并访问了两个网站。

14.3、页面操作

一定要有对应的方法来控制一个页面的加载、前进、后退、关闭和保存等行为:

import asyncio
from pyppeteer import launch

async def main():
    browser = await launch(headless=False)
    page = await browser.newPage()
    await page.goto('https://dynamic1.scrape.cuiqingcai.com/')
    await page.goto('https://spa2.scrape.cemter/')
    # 后退
    await page.goBack()
    # 前进
    await page.goForward()
    # 刷新
    await page.reload()
    # 保存PDF
    await page.pdf()
    # 截图
    await page.screenshot()
    # 设置页面HTML
    await page.setContent()
    # 设置User-Agent
    await page.setUserAgent()
    # 设置Headers
    await page.setExtraHTTPHeaders(headers={})
    # 关闭
    await page.close()
    await browser.close()

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

14.4、点击

Pyppeteer同样可以模拟点击,调用click方法即可。

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://spa2.scrape.center/')
    await page.waitForSelector('.item .name')
    await page.click('.item .name',options={
        'button':'right',
        'clickCount':1,
        'delay':3000,
    })
    await browser.close()
asyncio.get_event_loop().run_until_complete(main())
  • button:鼠标按钮,取值有left、middle、right。
  • clickCount:点击次数,取值有left,right,middle。
  • delay:延迟点击。

14.5、输入文本

使用type方法可以输入文本

import asyncio
from pyppeteer import launch

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())

14.6、获取信息

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://spa2.scrape.center/')
    print('HTML:',await page.content())
    print('Cookies:',await page.cookies())
    await browser.close()
asyncio.get_event_loop().run_until_complete(main())

14.7、执行

可以用evaluate执行JavaScript语句

import asyncio
from pyppeteer import launch
from pyquery import PyQuery as pq

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://spa2.scrape.center/')
    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())

14.8、延迟等待

在本节开始的时候,我们演示了waitForSelector的用法,它可以让页面等待某些符合条件的节点加载出来再返回结果。还有其他等待方法:

  • waitForFunction:等待某个JavaScript方法执行完毕或返回结果
  • waitForNavigation:等待页面跳转,如果没加载出来就报错
  • waitForRequest:等待某个特定的请求发出
  • waitForResponse:等待某个特定请求对应的响应
  • waitFor:通用的等待方法
  • waitForXPath:等待符合XPath的节点加载出来。

二、pyppeteer爬取实战

1、爬取目标

电影网站:https://spa2.scrape.center/

2、工作

  • 遍历每一页列表页,获取每部电影详情页的URL
  • 爬取每部电影的详情页,提取电影的名称、评分、类别、封面、简介等信息。
  • 将爬取的数据存储至数据库

3、准备工作

  • Python与pyppeteer库

4、爬取列表页

准备工作:

# 准备工作
import logging	

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s : %(message)s')	# 定义日志配置

INDEX_URL = 'https://spa2.scrape.center/page/{page}'
TIEMEOUT = 10
TOTAL_PAGE = 10
WINDOW_WIDTH, WINDOW_HEIGHT = 1366, 768		# 浏览器的宽和高
HEADLESS = False	# 指定是否启用无头模式,False代表会弹窗

定义初始化pyppeteer的方法:

# 初始化pyppeteer方法
from pyppeteer import launch

browser, tab = None, None	# 声明变量,前者代表浏览器对象,后者代表新建的页面选项卡。

async def init():
    global browser, tab		# 设置为全局变量,能够在其它方法里调用 
    browser = await launch(headless=HEADLESS,
                           args=['--disabled-infobars', f'--window-size={WINDOW_WIDTH},{WINDOW_HEIGHT}'])	# args参数指定隐藏提示条和设置浏览器窗口的宽高
    tab = await browser.newPage()
    await tab.setViewport({'width': WINDOW_WIDTH, 'height': WINDOW_HEIGHT})

定义一个通用的爬取方法

# 定义一个通用的爬取方法
from pyppeteer.errors import TimeoutError

async def scrape_page(url,selector):	# 定义两个参数,url代表要爬取的页面的URL,使用goto方法调用此参数即可访问对应页面;另一个是selector,即等待渲染出的节点对应的CSS选择器。
    logging.info('scraping %s',url)
    try:
        await tab.goto(url)
        await tab.waitForSelector(selector,options={
        'timeout':TIEMEOUT * 1000
        })	# waitForSelector方法等待selector选择器匹配的节点加载出来,通过option指定最长等待时间
    except TimeoutError:	# 超时则报出异常
        logging.error('error occurred while scraping %s',url,exc_info=True)

实现爬取列表页的方法:

# 列表页的爬取
async def scrape_index(page):	# 接受参数page,代表要爬取的页面的页码
    url = INDEX_URL.format(page=page)	# 通过format方法构造出列表页的URL
    await scrape_page(url,'.item .name')	# 同时传入选择器,.item .name是列表页中电影的名称

在定义一个解析列表页的方法,用来提取每部电影的详情页URL

# 解析列表页
async def parse_index():
    return await tab.querySelectorAllEval('.item .name','nodes => nodes.map(node => node.href)')

# 这里调用了querySelectorAllEval方法,接受两个参数:一是selector,代表选择器;另一个是pageFunction,代表要执行的JavaScript方法。这个方法的作用是找出和选择器匹配的节点,然后根据pageFunction定义的逻辑从这些节点中抽取对应的结果并返回。

# 我们给参数selector传入了电影名称。由于和选择器相匹配的节点有多个,所以给pageFunction参数输入的JavaScript方法就是nodes,其返回值是调用map方法得到node,然后调用node的href属性得到的超链接。这样querSelectorAllEval的返回结果就是当前列表页中的所有电影的详情页的URL组成的列表。

串联调用刚刚实现的方法:

import asyncio

async def main():
    await init()	# 首先调用init方法
    try:
        for page in range(1,TOTAL_PAGE + 1):	# 遍历所有页码
            await scrape_index(page)	# 爬取每一个列表页
            detail_urls = await parse_index()	# 从列表页提取每个URL
            logging.info('detail_urls %s',detail_data)	# 输出
    finally:
        await browser.close()

if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(main())

5、爬取详情页

定义爬取详情页的方法:

async def scrape_detail(url):
    await scrape_page(url,'h2')		# 直接调用scrape_page方法,传入详情页url和选择器即可,这里h2代表电影名称。

提取详情页里的信息的方法:

# 提取详情页里面的信息
async def parse_detail():
    url = tab.url
    name = await tab.querySelectorEval('h2','node => node.innerText')
    categories = await tab.querySelectorAllEval('.categories button span','nodes => nodes.map(node => node.innerText)')
    cover = await tab.querySelectorEval('.cover','node => node.src')
    score = await tab.querySelectorEval('.score','node => node.innerText')
    drama = await tab.querySelectorEval('.drama p','node => node.innerText')
    return {
        'url':url,
        'name':name,
        'categories':categories,
        'cover':cover,
        'score':score,
        'drama':drama
    }	# 将提取结果作为一个字典返回

# URL:直接调用tab对象的url属性即可获取当前页面的URL
# 名称:由于名称只涉及一个节点,因此我们调用querySelectorEval方法,第一个参数h2代表根据电影名称提取对应的节点;第二个参数pageFunction,这里调用node的innerText属性,提取了文本值,即电影名称。
# 类别:类别有多个,因此调用querySelectorAllEval方法。其CSS选择器.categories button span,可以选中多个类别节点;第二个参数与上相似。
# 封面:同上
# 分数:同上
# 简介:同上

在main方法里面添加对其的调用即可:

import asyncio

async def main():
    await init()
    try:
        for page in range(1,TOTAL_PAGE + 1):
            await scrape_index(page)
            detail_urls = await parse_index()
            for detail_url in detail_urls:
                await scrape_detail(detail_url)
                detail_data = await parse_detail()
                await save_data(detail_data)
                logging.info('detail_urls %s',detail_data)
    finally:
        await browser.close()
if __name__ == '__main__':
    asyncio.get_event_loop().run_until_complete(main())

6、数据存储

定义一个数据存储的方法,将爬取下来的数据保存为JSON格式:

# 存储数据
import json
from os import makedirs
from os.path import exists

RESULTS_DIR = 'results'

exists(RESULTS_DIR) or makedirs(RESULTS_DIR)

async def save_data(data):
    name = data.get('name')
    data_path = f'{RESULTS_DIR}/{name}.json'
    json.dump(data,open(data_path,'a',encoding='utf-8'),ensure_ascii=False,indent=2)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小李学不完

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值
>