日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

第46讲:遇到动态页面怎么办?详解渲染页面爬取

發布時間:2024/4/11 编程问答 46 豆豆
生活随笔 收集整理的這篇文章主要介紹了 第46讲:遇到动态页面怎么办?详解渲染页面爬取 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

前面我們已經介紹了 Scrapy 的一些常見用法,包括服務端渲染頁面的抓取和 API 的抓取,Scrapy 發起 Request 之后,返回的 Response 里面就包含了想要的結果。

但是現在越來越多的網頁都已經演變為 SPA 頁面,其頁面在瀏覽器中呈現的結果是經過 JavaScript 渲染得到的,如果我們使用 Scrapy 直接對其進行抓取的話,其結果和使用 requests 沒有什么區別。

那我們真的要使用 Scrapy 完成對 JavaScript 渲染頁面的抓取應該怎么辦呢?

之前我們介紹了 Selenium 和 Pyppeteer 都可以實現 JavaScript 渲染頁面的抓取,那用了 Scrapy 之后應該這么辦呢?Scrapy 能和 Selenium 或 Pyppeteer 一起使用嗎?答案是肯定的,我們可以將 Selenium 或 Pyppeteer 通過 Downloader Middleware 和 Scrapy 融合起來,實現 JavaScript 渲染頁面的抓取,本節我們就來了解下它的實現吧。

回顧

在前面我們介紹了 Downloader Middleware 的用法,在 Downloader Middleware 中有三個我們可以實現的方法 process_requestprocess_response 以及 process_exception 方法。

  • 我們再看下 process_request 方法和其不同的返回值的效果:
    當返回為 None 時,Scrapy 將繼續處理該 Request,接著執行其他 Downloader Middleware 的 process_request 方法,一直到 Downloader 把 Request 執行完后得到 Response 才結束。這個過程其實就是修改 Request 的過程,不同的 Downloader Middleware 按照設置的優先級順序依次對 Request 進行修改,最后送至 Downloader 執行。

  • 當返回為 Response 對象時,更低優先級的 Downloader Middleware 的 process_request 和 process_exception 方法就不會被繼續調用,每個 Downloader Middleware 的 process_response 方法轉而被依次調用。調用完畢之后,直接將 Response 對象發送給 Spider 來處理。

  • 當返回為 Request 對象時,更低優先級的 Downloader Middleware 的 process_request 方法會停止執行。這個 Request 會重新放到調度隊列里,其實它就是一個全新的 Request,等待被調度。如果被 Scheduler 調度了,那么所有的 Downloader Middleware 的 process_request 方法都會被重新按照順序執行。

  • 如果 IgnoreRequest 異常拋出,則所有的 Downloader Middleware 的 process_exception 方法會依次執行。如果沒有一個方法處理這個異常,那么 Request 的 errorback 方法就會回調。如果該異常還沒有被處理,那么它便會被忽略。

這里我們注意到第二個選項,當返回結果為 Response 對象時,低優先級的 process_request 方法就不會被繼續調用了,這個 Response 對象會直接經由 process_response 方法處理后轉交給 Spider 來解析。

然后再接著想一想,process_request 接收的參數是 request,即 Request 對象,怎么會返回 Response 對象呢?原因可想而知了,這個 Request 對象不再經由 Scrapy 的 Downloader 來處理了,而是在 process_request 方法里面直接就完成了 Request 的發送操作,然后在得到了對應的 Response 結果后再將其返回就好了。

那么對于 JavaScript 渲染的頁面來說,照這個方法來做,我們就可以把 Selenium 或 Pyppeteer 加載頁面的過程在 process_request 方法里面實現,得到網頁渲染完后的源代碼后直接構造 Response 返回即可,這樣我們就完成了借助 Downloader Middleware 實現 Scrapy 爬取動態渲染頁面的過程。

案例

本節我們就用實例來講解一下 Scrapy 和 Pyppeteer 實現 JavaScript 渲染頁面抓取的流程。

本節使用的實例網站為 https://dynamic5.scrape.center/,這是一個 JavaScript 渲染頁面,其內容是一本本的圖書信息。

同時這個網站的頁面帶有分頁功能,只需要在 URL 加上 /page/ 和頁碼就可以跳轉到下一頁,如 https://dynamic5.scrape.center/page/2 就是第二頁內容,https://dynamic5.scrape.center/page/3 就是第三頁內容。

那我們這個案例就來試著爬取前十頁的圖書信息吧。

實現

首先我們來新建一個項目,叫作 scrapypyppeteer,命令如下:

scrapy startproject scrapypyppeteer

接著進入項目,然后新建一個 Spider,名稱為 book,命令如下:

cd scrapypyppeteer scrapy genspider book dynamic5.scrape.center

這時候可以發現在項目的 spiders 文件夾下就出現了一個名為 spider.py 的文件,內容如下:

# -*- coding: utf-8 -*- import scrapy ? class BookSpider(scrapy.Spider):name = 'book'allowed_domains = ['dynamic5.scrape.center']start_urls = ['http://dynamic5.scrape.center/'] ?def parse(self, response):pass

首先我們構造列表頁的初始請求,實現一個 start_requests 方法,如下所示:

# -*- coding: utf-8 -*- from scrapy import Request, Spiderclass BookSpider(Spider):name = 'book'allowed_domains = ['dynamic5.scrape.center']base_url = 'https://dynamic5.scrape.center/page/{page}'max_page = 10def start_requests(self):for page in range(1, self.max_page + 1):url = self.base_url.format(page=page)yield Request(url, callback=self.parse_index)def parse_index(self, response):print(response.text)

這時如果我們直接運行這個 Spider,在 parse_index 方法里面打印輸出 Response 的內容,結果如下:

我們可以發現所得到的內容并不是頁面渲染后的真正 HTML 代碼。此時如果我們想要獲取 HTML 渲染結果的話就得使用 Downloader Middleware 實現了。

這里我們直接以一個我已經寫好的組件來演示了,組件的名稱叫作 GerapyPyppeteer,組件里已經寫好了 Scrapy 和 Pyppeteer 結合的中間件,下面我們來詳細介紹下。

我們可以借助于 pip3 來安裝組件,命令如下:

pip3 install gerapy-pyppeteer

GerapyPyppeteer 提供了兩部分內容,一部分是 Downloader Middleware,一部分是 Request。
首先我們需要開啟中間件,在 settings 里面開啟 PyppeteerMiddleware,配置如下:

DOWNLOADER_MIDDLEWARES = {'gerapy_pyppeteer.downloadermiddlewares.PyppeteerMiddleware': 543, }

然后我們把上文定義的 Request 修改為 PyppeteerRequest 即可:

# -*- coding: utf-8 -*- from gerapy_pyppeteer import PyppeteerRequest from scrapy import Request, Spider ? class BookSpider(Spider):name = 'book'allowed_domains = ['dynamic5.scrape.center']base_url = 'https://dynamic5.scrape.center/page/{page}'max_page = 10def start_requests(self):for page in range(1, self.max_page + 1):url = self.base_url.format(page=page)yield PyppeteerRequest(url, callback=self.parse_index, wait_for='.item .name')def parse_index(self, response):print(response.text)

這樣其實就完成了 Pyppeteer 的對接了,非常簡單。
這里 PyppeteerRequest 和原本的 Request 多提供了一個參數,就是 wait_for,通過這個參數我們可以指定 Pyppeteer 需要等待特定的內容加載出來才算結束,然后才返回對應的結果。

為了方便觀察效果,我們把并發限制修改得小一點,然后把 Pyppeteer 的 Headless 模式設置為 False:

CONCURRENT_REQUESTS = 3 GERAPY_PYPPETEER_HEADLESS = False

這時我們重新運行 Spider,就可以看到在爬取的過程中,Pyppeteer 對應的 Chromium 瀏覽器就彈出來了,并逐個加載對應的頁面內容,加載完成之后瀏覽器關閉。
另外觀察下控制臺,我們發現對應的結果也就被提取出來了,如圖所示:

這時候我們再重新修改下 parse_index 方法,提取對應的每本書的名稱和作者即可:

def parse_index(self, response):for item in response.css('.item'):name = item.css('.name::text').extract_first()authors = item.css('.authors::text').extract_first()name = name.strip() if name else Noneauthors = authors.strip() if authors else Noneyield {'name': name,'authors': authors}

重新運行,即可發現對應的名稱和作者就被提取出來了,運行結果如下:

這樣我們就借助 GerapyPyppeteer 完成了 JavaScript 渲染頁面的爬取。

原理分析

但上面僅僅是我們借助 GerapyPyppeteer 實現了 Scrapy 和 Pyppeteer 的對接,但其背后的原理是怎樣的呢?

我們可以詳細分析它的源碼,其 GitHub 地址為 https://github.com/Gerapy/GerapyPyppeteer。

首先通過分析可以發現其最核心的內容就是實現了一個 PyppeteerMiddleware,這是一個 Downloader Middleware,這里最主要的就是 process_request 的實現,核心代碼如下所示:

def process_request(self, request, spider):logger.debug('processing request %s', request) return as_deferred(self._process_request(request, spider))

這里其實就是調用了一個 _process_request 方法,這個方法的返回結果被 as_deferred 方法調用了。

這個 as_deferred 是怎么定義的呢?代碼如下:

import asyncio from twisted.internet.defer import Deferred ? def as_deferred(f):return Deferred.fromFuture(asyncio.ensure_future(f))

這個方法接收的就是一個 asyncio 庫的 Future 對象,然后通過 fromFuture 方法轉化成了 twisted 里面的 Deferred 對象。這是因為 Scrapy 本身的異步是借助 twisted 實現的,一個個的異步任務對應的就是一個個 Deferred 對象,而 Pyppeteer 又是基于 asyncio 的,它的異步任務是 Future 對象,所以這里我們需要借助 Deferred 的 fromFuture 方法將 Future 轉為 Deferred 對象。

另外為了支持這個功能,我們還需要在 Scrapy 中修改 reactor 對象,修改為 AsyncioSelectorReactor,實現如下:

import sys from twisted.internet.asyncioreactor import AsyncioSelectorReactor import twisted.internet ? reactor = AsyncioSelectorReactor(asyncio.get_event_loop()) ? # install AsyncioSelectorReactor twisted.internet.reactor = reactor sys.modules['twisted.internet.reactor'] = reactor

這段代碼已經在 PyppeteerMiddleware 里面定義好了,在 Scrapy 正式開始爬取之前這段代碼就會被執行,將 Scrapy 中的 reactor 修改為 AsyncioSelectorReactor,從而實現 Future 的調度。
接下來我們再來看下 _process_request 方法,實現如下:

async def _process_request(self, request: PyppeteerRequest, spider):"""use pyppeteer to process spider:param request::param spider::return:"""options = {'headless': self.headless,'dumpio': self.dumpio,'devtools': self.devtools,'args': [f'--window-size={self.window_width},{self.window_height}',]}if self.executable_path: options['executable_path'] = self.executable_pathif self.disable_extensions: options['args'].append('--disable-extensions')if self.hide_scrollbars: options['args'].append('--hide-scrollbars')if self.mute_audio: options['args'].append('--mute-audio')if self.no_sandbox: options['args'].append('--no-sandbox')if self.disable_setuid_sandbox: options['args'].append('--disable-setuid-sandbox')if self.disable_gpu: options['args'].append('--disable-gpu')# set proxyproxy = request.proxyif not proxy:proxy = request.meta.get('proxy')if proxy: options['args'].append(f'--proxy-server={proxy}')logger.debug('set options %s', options)browser = await launch(options)page = await browser.newPage()await page.setViewport({'width': self.window_width, 'height': self.window_height})# set cookiesif isinstance(request.cookies, dict):await page.setCookie(*[{'name': k, 'value': v}for k, v in request.cookies.items()])else:await page.setCookie(request.cookies)# the headers must be set using request interceptionawait page.setRequestInterception(True)@page.on('request')async def _handle_interception(pu_request):# handle headersoverrides = {'headers': {k.decode(): ','.join(map(lambda v: v.decode(), v))for k, v in request.headers.items()}}# handle resource types_ignore_resource_types = self.ignore_resource_typesif request.ignore_resource_types is not None:_ignore_resource_types = request.ignore_resource_typesif pu_request.resourceType in _ignore_resource_types:await pu_request.abort()else:await pu_request.continue_(overrides)timeout = self.download_timeoutif request.timeout is not None:timeout = request.timeoutlogger.debug('crawling %s', request.url)response = Nonetry:options = {'timeout': 1000 * timeout,'waitUntil': request.wait_until}logger.debug('request %s with options %s', request.url, options)response = await page.goto(request.url,options=options)except (PageError, TimeoutError):logger.error('error rendering url %s using pyppeteer', request.url)await page.close()await browser.close()return self._retry(request, 504, spider)if request.wait_for:try:logger.debug('waiting for %s finished', request.wait_for)await page.waitFor(request.wait_for)except TimeoutError:logger.error('error waiting for %s of %s', request.wait_for, request.url)await page.close()await browser.close()return self._retry(request, 504, spider)# evaluate scriptif request.script:logger.debug('evaluating %s', request.script)await page.evaluate(request.script)# sleepif request.sleep is not None:logger.debug('sleep for %ss', request.sleep)await asyncio.sleep(request.sleep)content = await page.content()body = str.encode(content)# close page and browserlogger.debug('close pyppeteer')await page.close()await browser.close()if not response:logger.error('get null response by pyppeteer of url %s', request.url)# Necessary to bypass the compression middleware (?)response.headers.pop('content-encoding', None)response.headers.pop('Content-Encoding', None)return HtmlResponse(page.url,status=response.status,headers=response.headers,body=body,encoding='utf-8',request=request)

代碼內容比較多,我們慢慢來說。

首先最開始的部分是定義 Pyppeteer 的一些啟動參數:

options = {'headless': self.headless,'dumpio': self.dumpio,'devtools': self.devtools,'args': [f'--window-size={self.window_width},{self.window_height}',] } if self.executable_path: options['executable_path'] = self.executable_path if self.disable_extensions: options['args'].append('--disable-extensions') if self.hide_scrollbars: options['args'].append('--hide-scrollbars') if self.mute_audio: options['args'].append('--mute-audio') if self.no_sandbox: options['args'].append('--no-sandbox') if self.disable_setuid_sandbox: options['args'].append('--disable-setuid-sandbox') if self.disable_gpu: options['args'].append('--disable-gpu')

這些參數來自 from_crawler 里面讀取項目 settings 的內容,如配置 Pyppeteer 對應瀏覽器的無頭模式、窗口大小、是否隱藏滾動條、是否棄用沙箱,等等。

緊接著就是利用 options 來啟動 Pyppeteer:

browser = await launch(options) page = await browser.newPage() await page.setViewport({'width': self.window_width, 'height': self.window_height})

這里啟動了 Pyppeteer 對應的瀏覽器,將其賦值為 browser,然后新建了一個選項卡,賦值為 page,然后通過 setViewport 方法設定了窗口的寬高。

接下來就是對一些 Cookies 進行處理,如果 Request 帶有 Cookies 的話會被賦值到 Pyppeteer 中:

# set cookies if isinstance(request.cookies, dict):await page.setCookie(*[{'name': k, 'value': v}for k, v in request.cookies.items()]) else:await page.setCookie(request.cookies)

再然后關鍵的步驟就是進行頁面的加載了:

try:options = {'timeout': 1000 * timeout,'waitUntil': request.wait_until}logger.debug('request %s with options %s', request.url, options)response = await page.goto(request.url,options=options) except (PageError, TimeoutError):logger.error('error rendering url %s using pyppeteer', request.url)await page.close()await browser.close()return self._retry(request, 504, spider)

這里我們首先制定了加載超時時間 timeout 還有要等待完成的事件 waitUntil,接著調用 page 的 goto 方法訪問對應的頁面,同時進行了異常檢測,如果發生錯誤就關閉瀏覽器并重新發起一次重試請求。

在頁面加載出來之后,我們還需要判定我們期望的結果是不是加載出來了,所以這里又增加了 waitFor 的調用:

if request.wait_for:try:logger.debug('waiting for %s finished', request.wait_for)await page.waitFor(request.wait_for)except TimeoutError:logger.error('error waiting for %s of %s', request.wait_for, request.url)await page.close()await browser.close()return self._retry(request, 504, spider)

這里 request 有個 wait_for 屬性,就可以定義想要加載的節點的選擇器,如 .item .name 等,這樣如果頁面在規定時間內加載出來就會繼續向下執行,否則就會觸發 TimeoutError 并被捕獲,關閉瀏覽器并重新發起一次重試請求。

等想要的結果加載出來之后,我們還可以執行一些自定義的 JavaScript 代碼完成我們想要自定義的功能:

# evaluate script if request.script:logger.debug('evaluating %s', request.script)await page.evaluate(request.script)

最后關鍵的一步就是將當前頁面的源代碼打印出來,然后構造一個 HtmlResponse 返回即可:

content = await page.content() body = str.encode(content) ? # close page and browser logger.debug('close pyppeteer') await page.close() await browser.close() ? if not response:logger.error('get null response by pyppeteer of url %s', request.url) ? # Necessary to bypass the compression middleware (?) response.headers.pop('content-encoding', None) response.headers.pop('Content-Encoding', None) ? return HtmlResponse(page.url,status=response.status,headers=response.headers,body=body,encoding='utf-8',request=request )

所以,如果代碼可以執行到最后,返回到就是一個 Response 對象,這個 Resposne 對象的 body 就是 Pyppeteer 渲染頁面后的結果,因此這個 Response 對象再傳給 Spider 解析,就是 JavaScript 渲染后的頁面結果了。

這樣我們就通過 Downloader Middleware 通過對接 Pyppeteer 完成 JavaScript 動態渲染頁面的抓取了。

總結

以上是生活随笔為你收集整理的第46讲:遇到动态页面怎么办?详解渲染页面爬取的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。