13.8 Scrapy 對接 Selenium
Scrapy 抓取頁面的方式和 requests 庫類似也祠,都是直接模擬 HTTP 請求,而 Scrapy 也不能抓取 JavaScript 動態(tài)渲染的頁面愿棋。在前文中抓取 JavaScript 渲染的頁面有兩種方式科展。一種是分析 Ajax 請求,找到其對應的接口抓取初斑,Scrapy 同樣可以用此種方式抓取辛润。另一種是直接用 Selenium 或 Splash 模擬瀏覽器進行抓取膨处,我們不需要關心頁面后臺發(fā)生的請求见秤,也不需要分析渲染過程,只需要關心頁面最終結果即可真椿,可見即可爬鹃答。那么,如果 Scrapy 可以對接 Selenium突硝,那 Scrapy 就可以處理任何網(wǎng)站的抓取了测摔。
1. 本節(jié)目標
本節(jié)我們來看看 Scrapy 框架如何對接 Selenium,以 PhantomJS 進行演示。我們依然抓取淘寶商品信息,抓取邏輯和前文中用 Selenium 抓取淘寶商品完全相同犹菱。
2. 準備工作
請確保 PhantomJS 和 MongoDB 已經(jīng)安裝好并可以正常運行也糊,安裝好 Scrapy、Selenium雕凹、PyMongo 庫,安裝方式可以參考第 1 章的安裝說明。
3. 新建項目
首先新建項目紊服,名為 scrapyseleniumtest,命令如下所示:
scrapy startproject scrapyseleniumtest
新建一個 Spider胸竞,命令如下所示:
scrapy genspider taobao www.taobao.com
修改 ROBOTSTXT_OBEY 為 False欺嗤,如下所示:
ROBOTSTXT_OBEY = False
4. 定義 Item
首先定義 Item 對象,名為 ProductItem卫枝,代碼如下所示:
from scrapy import Item, Field
class ProductItem(Item):
collection = 'products'
image = Field()
price = Field()
deal = Field()
title = Field()
shop = Field()
location = Field()
這里我們定義了 6 個 Field煎饼,也就是 6 個字段,跟之前的案例完全相同校赤。然后定義了一個 collection 屬性吆玖,即此 Item 保存到 MongoDB 的 Collection 名稱。
初步實現(xiàn) Spider 的 start_requests() 方法痒谴,如下所示:
from scrapy import Request, Spider
from urllib.parse import quote
from scrapyseleniumtest.items import ProductItem
class TaobaoSpider(Spider):
name = 'taobao'
allowed_domains = ['www.taobao.com']
base_url = 'https://s.taobao.com/search?q='
def start_requests(self):
for keyword in self.settings.get('KEYWORDS'):
for page in range(1, self.settings.get('MAX_PAGE') + 1):
url = self.base_url + quote(keyword)
yield Request(url=url, callback=self.parse, meta={'page': page}, dont_filter=True)
首先定義了一個 base_url衰伯,即商品列表的 URL,其后拼接一個搜索關鍵字就是該關鍵字在淘寶的搜索結果商品列表頁面积蔚。
關鍵字用 KEYWORDS 標識意鲸,定義為一個列表。最大翻頁頁碼用 MAX_PAGE 表示。它們統(tǒng)一定義在 setttings.py 里面怎顾,如下所示:
KEYWORDS = ['iPad']
MAX_PAGE = 100
在 start_requests() 方法里读慎,我們首先遍歷了關鍵字,遍歷了分頁頁碼槐雾,構造并生成 Request夭委。由于每次搜索的 URL 是相同的,所以分頁頁碼用 meta 參數(shù)來傳遞募强,同時設置 dont_filter 不去重株灸。這樣爬蟲啟動的時候,就會生成每個關鍵字對應的商品列表的每一頁的請求了擎值。
5. 對接 Selenium
接下來我們需要處理這些請求的抓取慌烧。這次我們對接 Selenium 進行抓取,采用 Downloader Middleware 來實現(xiàn)鸠儿。在 Middleware 里面的 process_request() 方法里對每個抓取請求進行處理屹蚊,啟動瀏覽器并進行頁面渲染,再將渲染后的結果構造一個 HtmlResponse 對象返回进每。代碼實現(xiàn)如下所示:
from selenium import webdriver
from selenium.common.exceptions import TimeoutException
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from scrapy.http import HtmlResponse
from logging import getLogger
class SeleniumMiddleware():
def __init__(self, timeout=None, service_args=[]):
self.logger = getLogger(__name__)
self.timeout = timeout
self.browser = webdriver.PhantomJS(service_args=service_args)
self.browser.set_window_size(1400, 700)
self.browser.set_page_load_timeout(self.timeout)
self.wait = WebDriverWait(self.browser, self.timeout)
def __del__(self):
self.browser.close()
def process_request(self, request, spider):
"""
用 PhantomJS 抓取頁面
:param request: Request 對象
:param spider: Spider 對象
:return: HtmlResponse
"""
self.logger.debug('PhantomJS is Starting')
page = request.meta.get('page', 1)
try:
self.browser.get(request.url)
if page > 1:
input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '#mainsrp-pager div.form> input')))
submit = self.wait.until(EC.element_to_be_clickable((By.CSS_SELECTOR, '#mainsrp-pager div.form> span.btn.J_Submit')))
input.clear()
input.send_keys(page)
submit.click()
self.wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, '#mainsrp-pager li.item.active> span'), str(page)))
self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, '.m-itemlist .items .item')))
return HtmlResponse(url=request.url, body=self.browser.page_source, request=request, encoding='utf-8', status=200)
except TimeoutException:
return HtmlResponse(url=request.url, status=500, request=request)
@classmethod
def from_crawler(cls, crawler):
return cls(timeout=crawler.settings.get('SELENIUM_TIMEOUT'),
service_args=crawler.settings.get('PHANTOMJS_SERVICE_ARGS'))
|
首先我們在 init() 里對一些對象進行初始化汹粤,包括 PhantomJS、WebDriverWait 等對象田晚,同時設置頁面大小和頁面加載超時時間嘱兼。在 process_request() 方法中,我們通過 Request 的 meta 屬性獲取當前需要爬取的頁碼肉瓦,調用 PhantomJS 對象的 get() 方法訪問 Request 的對應的 URL遭京。這就相當于從 Request 對象里獲取請求鏈接,然后再用 PhantomJS 加載泞莉,而不再使用 Scrapy 里的 Downloader哪雕。
隨后的處理等待和翻頁的方法在此不再贅述,和前文的原理完全相同鲫趁。最后斯嚎,頁面加載完成之后,我們調用 PhantomJS 的 page_source 屬性即可獲取當前頁面的源代碼挨厚,然后用它來直接構造并返回一個 HtmlResponse 對象堡僻。構造這個對象的時候需要傳入多個參數(shù),如 url疫剃、body 等钉疫,這些參數(shù)實際上就是它的基礎屬性〕布郏可以在官方文檔查看 HtmlResponse 對象的結構:https://doc.scrapy.org/en/latest/topics/request-response.html牲阁,這樣我們就成功利用 PhantomJS 來代替 Scrapy 完成了頁面的加載固阁,最后將 Response 返回即可。
有人可能會納悶:為什么實現(xiàn)這么一個 Downloader Middleware 就可以了城菊?之前的 Request 對象怎么辦备燃?Scrapy 不再處理了嗎?Response 返回后又傳遞給了誰凌唬?
是的并齐,Request 對象到這里就不會再處理了,也不會再像以前一樣交給 Downloader 下載客税。Response 會直接傳給 Spider 進行解析况褪。
我們需要回顧一下 Downloader Middleware 的 process_request() 方法的處理邏輯,內容如下所示:
當 process_request() 方法返回 Response 對象的時候霎挟,更低優(yōu)先級的 Downloader Middleware 的 process_request() 和 process_exception() 方法就不會被繼續(xù)調用了窝剖,轉而開始執(zhí)行每個 Downloader Middleware 的 process_response() 方法,調用完畢之后直接將 Response 對象發(fā)送給 Spider 來處理酥夭。
這里直接返回了一個 HtmlResponse 對象,它是 Response 的子類脊奋,返回之后便順次調用每個 Downloader Middleware 的 process_response() 方法熬北。而在 process_response() 中我們沒有對其做特殊處理,它會被發(fā)送給 Spider诚隙,傳給 Request 的回調函數(shù)進行解析讶隐。
到現(xiàn)在,我們應該能了解 Downloader Middleware 實現(xiàn) Selenium 對接的原理了久又。
在 settings.py 里巫延,我們設置調用剛才定義的 SeleniumMiddleware、設置等待超時變量 SELENIUM_TIMEOUT地消、設置 PhantomJS 配置參數(shù) PHANTOMJS_SERVICE_ARGS炉峰,如下所示:
DOWNLOADER_MIDDLEWARES = {'scrapyseleniumtest.middlewares.SeleniumMiddleware': 543,}
6. 解析頁面
Response 對象就會回傳給 Spider 內的回調函數(shù)進行解析。所以下一步我們就實現(xiàn)其回調函數(shù)脉执,對網(wǎng)頁來進行解析疼阔,代碼如下所示:
def parse(self, response):
products = response.xpath('//div[@id="mainsrp-itemlist"]//div[@class="items"][1]//div[contains(@class, "item")]')
for product in products:
item = ProductItem()
item['price'] = ''.join(product.xpath('.//div[contains(@class, "price")]//text()').extract()).strip()
item['title'] = ''.join(product.xpath('.//div[contains(@class, "title")]//text()').extract()).strip()
item['shop'] = ''.join(product.xpath('.//div[contains(@class, "shop")]//text()').extract()).strip()
item['image'] = ''.join(product.xpath('.//div[@class="pic"]//img[contains(@class, "img")]/@data-src').extract()).strip()
item['deal'] = product.xpath('.//div[contains(@class, "deal-cnt")]//text()').extract_first()
item['location'] = product.xpath('.//div[contains(@class, "location")]//text()').extract_first()
yield item
在這里我們使用 XPath 進行解析,調用 response 變量的 xpath() 方法即可半夷。首先我們傳遞選取所有商品對應的 XPath婆廊,可以匹配所有商品,隨后對結果進行遍歷巫橄,依次選取每個商品的名稱淘邻、價格、圖片等內容湘换,構造并返回一個 ProductItem 對象宾舅。
7. 存儲結果
最后我們實現(xiàn)一個 Item Pipeline敬尺,將結果保存到 MongoDB,如下所示:
import pymongo
class MongoPipeline(object):
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db
@classmethod
def from_crawler(cls, crawler):
return cls(mongo_uri=crawler.settings.get('MONGO_URI'), mongo_db=crawler.settings.get('MONGO_DB'))
def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]
def process_item(self, item, spider):
self.db[item.collection].insert(dict(item))
return item
def close_spider(self, spider):
self.client.close()
此實現(xiàn)和前文中存儲到 MongoDB 的方法完全一致贴浙,原理不再贅述砂吞。記得在 settings.py 中開啟它的調用,如下所示:
ITEM_PIPELINES = {'scrapyseleniumtest.pipelines.MongoPipeline': 300,}
其中崎溃,MONGO_URI 和 MONGO_DB 的定義如下所示:
MONGO_URI = 'localhost'
MONGO_DB = 'taobao'
8. 運行
整個項目就完成了蜻直,執(zhí)行如下命令啟動抓取即可:
scrapy crawl taobao
運行結果如圖 13-13 所示:
圖 13-13 運行結果
再查看一下 MongoDB,結果如圖 13-14 所示:
圖 13-14 MongoDB 結果
這樣我們便成功在 Scrapy 中對接 Selenium 并實現(xiàn)了淘寶商品的抓取袁串。
9. 本節(jié)代碼
本節(jié)代碼地址為:https://github.com/Python3WebSpider/ScrapySeleniumTest概而。
10. 結語
我們通過改寫 Downloader Middleware 的方式實現(xiàn)了 Selenium 的對接。但這種方法其實是阻塞式的囱修,也就是說這樣就破壞了 Scrapy 異步處理的邏輯赎瑰,速度會受到影響。為了不破壞其異步加載邏輯破镰,我們可以使用 Splash 實現(xiàn)餐曼。下一節(jié)我們再來看看 Scrapy 對接 Splash 的方式。