前面幾個(gè)章節(jié)利用 python 的基礎(chǔ)庫實(shí)現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)的獲取、解構(gòu)以及存儲(chǔ),同時(shí)也完成了簡單的數(shù)據(jù)讀取操作。在這個(gè)過程中使用了其他人完成的功能庫來加快我們的爬蟲實(shí)現(xiàn)過程,對(duì)于爬蟲也有相應(yīng)的 python 框架供我們使用「不重復(fù)造輪子是程序員的一大特點(diǎn)」,當(dāng)我們了解爬蟲的實(shí)現(xiàn)過程以后就可以嘗試使用框架來完成自己的爬蟲,加快開發(fā)速度。
在 python 中比較常用的爬蟲框架有 Scrapy 和 PySpider,今天針對(duì) Scrapy 爬蟲框架來實(shí)現(xiàn)前面幾篇所實(shí)現(xiàn)的功能。
準(zhǔn)備工作
首先需要在系統(tǒng)中安裝 Scrapy 「也可以使用 virtualenv 創(chuàng)建一個(gè)虛擬環(huán)境」,可以通過以下方式來安裝 Scrapy。
#使用 pip 來安裝 Scrapy
pip install Scrapy
Scrapy 安裝完成以后,通過以下方式來創(chuàng)建一個(gè)基本的 Scrapy 項(xiàng)目。
scrapy startproject project
編寫你的爬蟲
在 Scrapy 中所有的爬蟲類必須是 scrapy.Spider 的子類,你可以自定義要發(fā)出的初始請(qǐng)求,選擇如何跟蹤頁面中的鏈接,以及如何解析下載的頁面內(nèi)容以提取數(shù)據(jù)。
一個(gè)基礎(chǔ)爬蟲
第一個(gè)爬蟲我們選擇使用 scrapy.Spider 作為父類,建立一個(gè)簡單的單頁面爬蟲。建立一個(gè) Scrapy 爬蟲文件可以直接在 spider 目錄下新建文件然后手動(dòng)編寫相關(guān)內(nèi)容,也可以使用 scrapy genspider [options] <name> <domain> 命令來建立一個(gè)空白模板的爬蟲文件,文件內(nèi)容如下:
# -*- coding: utf-8 -*-
import scrapy
class TestSpider(scrapy.Spider):
name = 'test'
allowed_domains = ['domain.com']
start_urls = ['http://domain.com/']
def parse(self, response):
pass
如上所示 TestSpider 繼承自 scrapy.Spider,并定義了一些屬性和方法:
- name:當(dāng)前爬蟲的名稱,用來標(biāo)識(shí)該爬蟲。
- allowed_domains:當(dāng)前爬蟲所爬取的域名。
- start_urls:爬蟲將順序爬取其中的 url。
- parse:爬蟲的回調(diào)函數(shù),用來處理請(qǐng)求的響應(yīng)內(nèi)容,數(shù)據(jù)解析通常在該函數(shù)內(nèi)完成。
我們使用 scrapy.Spider 來建立一個(gè)爬取「立創(chuàng)商城」上所有元件分類的爬蟲,爬蟲名稱命名為 catalog,將 start_urls 更換為 https://www.szlcsc.com/catalog.html,下面貼出解析函數(shù)的代碼
def parse(self, response):
catalogs = response.xpath('//div[@class="catalog_a"]')
for catalog in catalogs:
catalog_dl = catalog.xpath('dl')
for tag in catalog_dl:
parent = tag.xpath('dt/a/text()').extract()
childs = tag.xpath('dd/a/text()').extract()
parent = self.catalog_filter_left(self.catalog_filter_right(parent[0]))
yield CatalogItem(
parent = parent,
child = None,
)
for child in childs:
yield CatalogItem(
parent = parent,
child = self.catalog_filter_right(child),
)
通過以下命令來啟動(dòng)爬蟲,觀察爬蟲的爬取過程及結(jié)果。
scrapy crawl catalog
遞歸爬蟲
上一小節(jié)中實(shí)現(xiàn)了一個(gè)簡單的單頁面爬蟲,它僅能訪問在 start_urls 中列明的頁面,無法從獲取的頁面中提取出鏈接并跟進(jìn)。scrapy 通過 CrawlSpider 來實(shí)現(xiàn)按照一定的規(guī)則從當(dāng)前頁面中提取出 url,并跟進(jìn)爬取??梢酝ㄟ^命令 scrapy genspider -t crawl test domain.com 來指定使用 CrawlSpider 建立爬蟲。生產(chǎn)文件內(nèi)容如下:
mport scrapy
from scrapy.linkextractors import LinkExtractor
from scrapy.spiders import CrawlSpider, Rule
class TestSpider(CrawlSpider):
name = 'test'
allowed_domains = ['domain.com']
start_urls = ['http://domain.com/']
rules = (
Rule(LinkExtractor(allow=r'Items/'), callback='parse_item', follow=True),
)
def parse_item(self, response):
item = {}
#item['domain_id'] = response.xpath('//input[@id="sid"]/@value').get()
#item['name'] = response.xpath('//div[@id="name"]').get()
#item['description'] = response.xpath('//div[@id="description"]').get()
return item
基于 CrawlerSpider 的爬蟲不同之處在于多了一個(gè) rules 的屬性,該屬性定義了如何從網(wǎng)頁中提取 url,并使用指定的回調(diào)函數(shù)來處理爬取結(jié)果。
使用遞歸爬蟲來實(shí)現(xiàn)「立創(chuàng)商城」中生產(chǎn)商的爬取在合適不過了,以下貼出相應(yīng)的鏈接提取規(guī)則和處理函數(shù)。
rules = (
Rule(LinkExtractor(allow=(r'https://list.szlcsc.com/brand/[0-9]+.html', )), callback='parse_item', follow=False),
)
def parse_item(self, response):
brand_info_logo = response.xpath('//div[@class="brand-info-logo"]')
brand_info_text = response.xpath('//div[@class="brand-info-text"]')
name = brand_info_logo.xpath('h1[@class="brand-info-name"]/text()').extract()
url = brand_info_logo.xpath('descendant::a[@class="blue"]/@href').extract()
desc = brand_info_text.xpath('div[@class="introduce_txt"]//text()').extract()
...
return BrandItem(
name = name,
url = url,
desc = desc_text,
)
動(dòng)態(tài)數(shù)據(jù)處理
爬蟲在處理的過程中不可避免的會(huì)遇到動(dòng)態(tài)數(shù)據(jù)的處理,「立創(chuàng)商城」中元件的列表頁面的翻頁即是通過 ajax 來實(shí)現(xiàn)的,如果僅僅使用上一節(jié)中的遞歸爬取的方法,有很多的元件將會(huì)被漏掉,在這里可以使用 scrapy 模擬 post 方法來實(shí)現(xiàn)翻頁的效果。
在 scrapy 中向網(wǎng)站中提交數(shù)據(jù)使用 scrapy.FormRequest 來實(shí)現(xiàn)。FormRequest 類擴(kuò)展了基 Request 具有處理HTML表單的功能。通過 FormReques 向翻頁 API 上提交新的頁面信息,從而獲取新頁面中的 Json 數(shù)據(jù),通過解析 Json 數(shù)據(jù)來獲取整個(gè)網(wǎng)站中的元件信息。
動(dòng)態(tài)翻頁所需要的 API 及提交數(shù)據(jù)的格式在 外行學(xué) Python 爬蟲 第六篇 動(dòng)態(tài)翻頁 中做過分析,可以在那里找到相關(guān)的信息。
通過 FormRequest 來指定 url、提交數(shù)據(jù)、返回?cái)?shù)據(jù)的回調(diào)函數(shù)等,具體實(shí)現(xiàn)如下:
yield scrapy.FormRequest(url=product_post_url,
formdata=post_data,
callback=self.json_callback,
dont_filter = True)
由于 Scrapy 中自帶了 url 去重功能,因此需在 FormRequest 中設(shè)置
dont_filter = True,否則 FormRequest 只會(huì)執(zhí)行一次。
數(shù)據(jù)的存儲(chǔ)
Scrapy 使用 Item 來定義通用的輸出數(shù)據(jù)格式,數(shù)據(jù)通過 Item 在 Scrapy 的各個(gè)模塊中進(jìn)行傳遞,以下是一個(gè)簡單的 Item 定義:
class BrandItem(scrapy.Item):
name = scrapy.Field()
url = scrapy.Field()
desc = scrapy.Field()
數(shù)據(jù)的處理通常在 Pipeline 中進(jìn)行,在爬蟲中獲取的數(shù)據(jù)將通過 Item 傳遞到 Pipeline 的 process_item 方法中進(jìn)行處理,以下代碼實(shí)現(xiàn)了將數(shù)據(jù)存在 sqlite 數(shù)據(jù)庫中。
class BrandPipeline(object):
def __init__(self, database_uri):
db.init_url(url=database_uri)
self.save = SaveData()
@classmethod
def from_crawler(cls, crawler):
return cls(
database_uri=crawler.settings.get('SQLALCHEMY_DATABASE_URI'),
)
def process_item(self, item, spider):
if spider.name == 'brand':
self.save.save_brand(brand=item)
raise DropItem('Drop Item: %s' % item)
return item
其中 from_crawler 方法用來沖 setting 文件中獲取數(shù)據(jù)庫鏈接。
Item 按照在 setting 中定義的優(yōu)先級(jí)在各個(gè) Pipeline 中進(jìn)行傳遞,如果在某個(gè) Pipeline 中對(duì)該 Item 處理完成后續(xù)無需處理,可以使用 DropItem 來終止 Item 向其他的 Pipeline 傳遞。
反爬處理
爬蟲不可避免的會(huì)遇到網(wǎng)站的反爬策略,一般的反爬策略是限制 IP 的訪問間隔,判斷當(dāng)前的訪問代理是否總是爬蟲等。
針對(duì)以上策略,可以通過設(shè)置兩個(gè)請(qǐng)求之間間隔隨機(jī)的時(shí)間,并設(shè)置 User-Agent 來規(guī)避一部分的反爬策略。
設(shè)置請(qǐng)求間隔隨機(jī)時(shí)間的中間件實(shí)現(xiàn)如下:
class ScrapyTestRandomDelayMiddleware(object):
def __init__(self, crawler):
self.delay = crawler.spider.settings.get("RANDOM_DELAY")
@classmethod
def from_crawler(cls, crawler):
return cls(crawler)
def process_request(self, request, spider):
delay = random.randint(0, self.delay)
spider.logger.debug("### random delay: %s s ###" % delay)
time.sleep(delay)
然后在 setting 文件中啟用該中間件。
RANDOM_DELAY = 3
DOWNLOADER_MIDDLEWARES = {
'scrapy_test.middlewares.ScrapyTestDownloaderMiddleware': None,
'scrapy_test.middlewares.ScrapyTestRandomDelayMiddleware': 999,
}
User-Agent 可以直接在 setting 文件中修改,在我們的瀏覽器中查看當(dāng)前瀏覽器的 User-Agent,將 Scrapy 的 User-Agent 設(shè)置為瀏覽器的 User-Agent。以下是 Chrome 流量中 User-Agent 的查找方法。
前面都沒有提到過網(wǎng)站的反爬蟲,這次提到的原因是真的被「立創(chuàng)商城」給限制訪問了。
運(yùn)行爬蟲
今天將前面所完成的爬蟲功能使用 Scrapy 進(jìn)行了一個(gè)重構(gòu),catalog 使用的是單頁爬蟲用來獲取原件的分類信息,brand 是一個(gè)遞歸爬蟲用來獲取原件生產(chǎn)商信息,product 是一個(gè)通過 post 動(dòng)態(tài)獲取 json 并解析的爬蟲,主要用來獲取所有元件的信息。
有多個(gè)爬蟲需要運(yùn)行,可以使用以下方法逐個(gè)運(yùn)行爬蟲
# -*- coding:utf-8 -*-
import os
os.system("scrapy crawl brand")
os.system("scrapy crawl catalog")
os.system("scrapy crawl product")
如果想同時(shí)運(yùn)行多個(gè)爬蟲,以下方法是個(gè)不錯(cuò)的選擇
# -*- coding:utf-8 -*-
from scrapy.utils.project import get_project_settings
from scrapy.crawler import CrawlerProcess
def main():
setting = get_project_settings()
process = CrawlerProcess(setting)
process.crawl(brand)
process.crawl(catalog)
process.crawl(product)
process.start()