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

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 编程资源 > 编程问答 >内容正文

编程问答

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

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

前面我們已經(jīng)介紹了 Scrapy 的一些常見用法,包括服務(wù)端渲染頁(yè)面的抓取和 API 的抓取,Scrapy 發(fā)起 Request 之后,返回的 Response 里面就包含了想要的結(jié)果。

但是現(xiàn)在越來越多的網(wǎng)頁(yè)都已經(jīng)演變?yōu)?SPA 頁(yè)面,其頁(yè)面在瀏覽器中呈現(xiàn)的結(jié)果是經(jīng)過 JavaScript 渲染得到的,如果我們使用 Scrapy 直接對(duì)其進(jìn)行抓取的話,其結(jié)果和使用 requests 沒有什么區(qū)別。

那我們真的要使用 Scrapy 完成對(duì) JavaScript 渲染頁(yè)面的抓取應(yīng)該怎么辦呢?

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

回顧

在前面我們介紹了 Downloader Middleware 的用法,在 Downloader Middleware 中有三個(gè)我們可以實(shí)現(xiàn)的方法 process_requestprocess_response 以及 process_exception 方法。

  • 我們?cè)倏聪?process_request 方法和其不同的返回值的效果:
    當(dāng)返回為 None 時(shí),Scrapy 將繼續(xù)處理該 Request,接著執(zhí)行其他 Downloader Middleware 的 process_request 方法,一直到 Downloader 把 Request 執(zhí)行完后得到 Response 才結(jié)束。這個(gè)過程其實(shí)就是修改 Request 的過程,不同的 Downloader Middleware 按照設(shè)置的優(yōu)先級(jí)順序依次對(duì) Request 進(jìn)行修改,最后送至 Downloader 執(zhí)行。

  • 當(dāng)返回為 Response 對(duì)象時(shí),更低優(yōu)先級(jí)的 Downloader Middleware 的 process_request 和 process_exception 方法就不會(huì)被繼續(xù)調(diào)用,每個(gè) Downloader Middleware 的 process_response 方法轉(zhuǎn)而被依次調(diào)用。調(diào)用完畢之后,直接將 Response 對(duì)象發(fā)送給 Spider 來處理。

  • 當(dāng)返回為 Request 對(duì)象時(shí),更低優(yōu)先級(jí)的 Downloader Middleware 的 process_request 方法會(huì)停止執(zhí)行。這個(gè) Request 會(huì)重新放到調(diào)度隊(duì)列里,其實(shí)它就是一個(gè)全新的 Request,等待被調(diào)度。如果被 Scheduler 調(diào)度了,那么所有的 Downloader Middleware 的 process_request 方法都會(huì)被重新按照順序執(zhí)行。

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

這里我們注意到第二個(gè)選項(xiàng),當(dāng)返回結(jié)果為 Response 對(duì)象時(shí),低優(yōu)先級(jí)的 process_request 方法就不會(huì)被繼續(xù)調(diào)用了,這個(gè) Response 對(duì)象會(huì)直接經(jīng)由 process_response 方法處理后轉(zhuǎn)交給 Spider 來解析。

然后再接著想一想,process_request 接收的參數(shù)是 request,即 Request 對(duì)象,怎么會(huì)返回 Response 對(duì)象呢?原因可想而知了,這個(gè) Request 對(duì)象不再經(jīng)由 Scrapy 的 Downloader 來處理了,而是在 process_request 方法里面直接就完成了 Request 的發(fā)送操作,然后在得到了對(duì)應(yīng)的 Response 結(jié)果后再將其返回就好了。

那么對(duì)于 JavaScript 渲染的頁(yè)面來說,照這個(gè)方法來做,我們就可以把 Selenium 或 Pyppeteer 加載頁(yè)面的過程在 process_request 方法里面實(shí)現(xiàn),得到網(wǎng)頁(yè)渲染完后的源代碼后直接構(gòu)造 Response 返回即可,這樣我們就完成了借助 Downloader Middleware 實(shí)現(xiàn) Scrapy 爬取動(dòng)態(tài)渲染頁(yè)面的過程。

案例

本節(jié)我們就用實(shí)例來講解一下 Scrapy 和 Pyppeteer 實(shí)現(xiàn) JavaScript 渲染頁(yè)面抓取的流程。

本節(jié)使用的實(shí)例網(wǎng)站為 https://dynamic5.scrape.center/,這是一個(gè) JavaScript 渲染頁(yè)面,其內(nèi)容是一本本的圖書信息。

同時(shí)這個(gè)網(wǎng)站的頁(yè)面帶有分頁(yè)功能,只需要在 URL 加上 /page/ 和頁(yè)碼就可以跳轉(zhuǎn)到下一頁(yè),如 https://dynamic5.scrape.center/page/2 就是第二頁(yè)內(nèi)容,https://dynamic5.scrape.center/page/3 就是第三頁(yè)內(nèi)容。

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

實(shí)現(xiàn)

首先我們來新建一個(gè)項(xiàng)目,叫作 scrapypyppeteer,命令如下:

scrapy startproject scrapypyppeteer

接著進(jìn)入項(xiàng)目,然后新建一個(gè) Spider,名稱為 book,命令如下:

cd scrapypyppeteer scrapy genspider book dynamic5.scrape.center

這時(shí)候可以發(fā)現(xiàn)在項(xiàng)目的 spiders 文件夾下就出現(xiàn)了一個(gè)名為 spider.py 的文件,內(nèi)容如下:

# -*- 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

首先我們構(gòu)造列表頁(yè)的初始請(qǐng)求,實(shí)現(xiàn)一個(gè) 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)

這時(shí)如果我們直接運(yùn)行這個(gè) Spider,在 parse_index 方法里面打印輸出 Response 的內(nèi)容,結(jié)果如下:

我們可以發(fā)現(xiàn)所得到的內(nèi)容并不是頁(yè)面渲染后的真正 HTML 代碼。此時(shí)如果我們想要獲取 HTML 渲染結(jié)果的話就得使用 Downloader Middleware 實(shí)現(xiàn)了。

這里我們直接以一個(gè)我已經(jīng)寫好的組件來演示了,組件的名稱叫作 GerapyPyppeteer,組件里已經(jīng)寫好了 Scrapy 和 Pyppeteer 結(jié)合的中間件,下面我們來詳細(xì)介紹下。

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

pip3 install gerapy-pyppeteer

GerapyPyppeteer 提供了兩部分內(nèi)容,一部分是 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)

這樣其實(shí)就完成了 Pyppeteer 的對(duì)接了,非常簡(jiǎn)單。
這里 PyppeteerRequest 和原本的 Request 多提供了一個(gè)參數(shù),就是 wait_for,通過這個(gè)參數(shù)我們可以指定 Pyppeteer 需要等待特定的內(nèi)容加載出來才算結(jié)束,然后才返回對(duì)應(yīng)的結(jié)果。

為了方便觀察效果,我們把并發(fā)限制修改得小一點(diǎn),然后把 Pyppeteer 的 Headless 模式設(shè)置為 False:

CONCURRENT_REQUESTS = 3 GERAPY_PYPPETEER_HEADLESS = False

這時(shí)我們重新運(yùn)行 Spider,就可以看到在爬取的過程中,Pyppeteer 對(duì)應(yīng)的 Chromium 瀏覽器就彈出來了,并逐個(gè)加載對(duì)應(yīng)的頁(yè)面內(nèi)容,加載完成之后瀏覽器關(guān)閉。
另外觀察下控制臺(tái),我們發(fā)現(xiàn)對(duì)應(yīng)的結(jié)果也就被提取出來了,如圖所示:

這時(shí)候我們?cè)僦匦滦薷南?parse_index 方法,提取對(duì)應(yīng)的每本書的名稱和作者即可:

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}

重新運(yùn)行,即可發(fā)現(xiàn)對(duì)應(yīng)的名稱和作者就被提取出來了,運(yùn)行結(jié)果如下:

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

原理分析

但上面僅僅是我們借助 GerapyPyppeteer 實(shí)現(xiàn)了 Scrapy 和 Pyppeteer 的對(duì)接,但其背后的原理是怎樣的呢?

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

首先通過分析可以發(fā)現(xiàn)其最核心的內(nèi)容就是實(shí)現(xiàn)了一個(gè) PyppeteerMiddleware,這是一個(gè) Downloader Middleware,這里最主要的就是 process_request 的實(shí)現(xiàn),核心代碼如下所示:

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

這里其實(shí)就是調(diào)用了一個(gè) _process_request 方法,這個(gè)方法的返回結(jié)果被 as_deferred 方法調(diào)用了。

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

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

這個(gè)方法接收的就是一個(gè) asyncio 庫(kù)的 Future 對(duì)象,然后通過 fromFuture 方法轉(zhuǎn)化成了 twisted 里面的 Deferred 對(duì)象。這是因?yàn)?Scrapy 本身的異步是借助 twisted 實(shí)現(xiàn)的,一個(gè)個(gè)的異步任務(wù)對(duì)應(yīng)的就是一個(gè)個(gè) Deferred 對(duì)象,而 Pyppeteer 又是基于 asyncio 的,它的異步任務(wù)是 Future 對(duì)象,所以這里我們需要借助 Deferred 的 fromFuture 方法將 Future 轉(zhuǎn)為 Deferred 對(duì)象。

另外為了支持這個(gè)功能,我們還需要在 Scrapy 中修改 reactor 對(duì)象,修改為 AsyncioSelectorReactor,實(shí)現(xiàn)如下:

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

這段代碼已經(jīng)在 PyppeteerMiddleware 里面定義好了,在 Scrapy 正式開始爬取之前這段代碼就會(huì)被執(zhí)行,將 Scrapy 中的 reactor 修改為 AsyncioSelectorReactor,從而實(shí)現(xiàn) Future 的調(diào)度。
接下來我們?cè)賮砜聪?_process_request 方法,實(shí)現(xiàn)如下:

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)

代碼內(nèi)容比較多,我們慢慢來說。

首先最開始的部分是定義 Pyppeteer 的一些啟動(dòng)參數(shù):

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

這些參數(shù)來自 from_crawler 里面讀取項(xiàng)目 settings 的內(nèi)容,如配置 Pyppeteer 對(duì)應(yīng)瀏覽器的無頭模式、窗口大小、是否隱藏滾動(dòng)條、是否棄用沙箱,等等。

緊接著就是利用 options 來啟動(dòng) Pyppeteer:

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

這里啟動(dòng)了 Pyppeteer 對(duì)應(yīng)的瀏覽器,將其賦值為 browser,然后新建了一個(gè)選項(xiàng)卡,賦值為 page,然后通過 setViewport 方法設(shè)定了窗口的寬高。

接下來就是對(duì)一些 Cookies 進(jìn)行處理,如果 Request 帶有 Cookies 的話會(huì)被賦值到 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)

再然后關(guān)鍵的步驟就是進(jìn)行頁(yè)面的加載了:

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)

這里我們首先制定了加載超時(shí)時(shí)間 timeout 還有要等待完成的事件 waitUntil,接著調(diào)用 page 的 goto 方法訪問對(duì)應(yīng)的頁(yè)面,同時(shí)進(jìn)行了異常檢測(cè),如果發(fā)生錯(cuò)誤就關(guān)閉瀏覽器并重新發(fā)起一次重試請(qǐng)求。

在頁(yè)面加載出來之后,我們還需要判定我們期望的結(jié)果是不是加載出來了,所以這里又增加了 waitFor 的調(diào)用:

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 有個(gè) wait_for 屬性,就可以定義想要加載的節(jié)點(diǎn)的選擇器,如 .item .name 等,這樣如果頁(yè)面在規(guī)定時(shí)間內(nèi)加載出來就會(huì)繼續(xù)向下執(zhí)行,否則就會(huì)觸發(fā) TimeoutError 并被捕獲,關(guān)閉瀏覽器并重新發(fā)起一次重試請(qǐng)求。

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

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

最后關(guān)鍵的一步就是將當(dāng)前頁(yè)面的源代碼打印出來,然后構(gòu)造一個(gè) 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 )

所以,如果代碼可以執(zhí)行到最后,返回到就是一個(gè) Response 對(duì)象,這個(gè) Resposne 對(duì)象的 body 就是 Pyppeteer 渲染頁(yè)面后的結(jié)果,因此這個(gè) Response 對(duì)象再傳給 Spider 解析,就是 JavaScript 渲染后的頁(yè)面結(jié)果了。

這樣我們就通過 Downloader Middleware 通過對(duì)接 Pyppeteer 完成 JavaScript 動(dòng)態(tài)渲染頁(yè)面的抓取了。

總結(jié)

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

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。