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

2024-05-09 1810阅读

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

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

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

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

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

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

一、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)
            

    免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们,邮箱:ciyunidc@ciyunshuju.com。本站只作为美观性配图使用,无任何非法侵犯第三方意图,一切解释权归图片著作权方,本站不承担任何责任。如有恶意碰瓷者,必当奉陪到底严惩不贷!

    目录[+]