前面幾個章節利用 python 的基礎庫實現網絡數據的獲取、解構以及存儲,同時也完成了簡單的數據讀取操作。在這個過程中使用了其他人完成的功能庫來加快我們的爬蟲實現過程,對於爬蟲也有相應的 python 框架供我們使用「不重複造輪子是程序員的一大特點」,當我們瞭解爬蟲的實現過程以後就可以嘗試使用框架來完成自己的爬蟲,加快開發速度。
在 python 中比較常用的爬蟲框架有 Scrapy 和 PySpider,今天針對 Scrapy 爬蟲框架來實現前面幾篇所實現的功能。
準備工作
首先需要在系統中安裝 Scrapy 「也可以使用 virtualenv 創建一個虛擬環境」,可以通過以下方式來安裝 Scrapy。
#使用 pip 來安裝 Scrapy
pip install Scrapy
Scrapy 安裝完成以後,通過以下方式來創建一個基本的 Scrapy 項目。
scrapy startproject project
編寫你的爬蟲
在 Scrapy 中所有的爬蟲類必須是 scrapy.Spider 的子類,你可以自定義要發出的初始請求,選擇如何跟蹤頁面中的鏈接,以及如何解析下載的頁面內容以提取數據。
一個基礎爬蟲
第一個爬蟲我們選擇使用 scrapy.Spider 作爲父類,建立一個簡單的單頁面爬蟲。建立一個 Scrapy 爬蟲文件可以直接在 spider 目錄下新建文件然後手動編寫相關內容,也可以使用 scrapy genspider [options] <name> <domain>
命令來建立一個空白模板的爬蟲文件,文件內容如下:
# -*- 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:當前爬蟲的名稱,用來標識該爬蟲。
- allowed_domains:當前爬蟲所爬取的域名。
- start_urls:爬蟲將順序爬取其中的 url。
- parse:爬蟲的回調函數,用來處理請求的響應內容,數據解析通常在該函數內完成。
我們使用 scrapy.Spider 來建立一個爬取「立創商城」上所有元件分類的爬蟲,爬蟲名稱命名爲 catalog,將 start_urls 更換爲 https://www.szlcsc.com/catalog.html
,下面貼出解析函數的代碼
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),
)
通過以下命令來啓動爬蟲,觀察爬蟲的爬取過程及結果。
scrapy crawl catalog
遞歸爬蟲
上一小節中實現了一個簡單的單頁面爬蟲,它僅能訪問在 start_urls 中列明的頁面,無法從獲取的頁面中提取出鏈接並跟進。scrapy 通過 CrawlSpider 來實現按照一定的規則從當前頁面中提取出 url,並跟進爬取。可以通過命令 scrapy genspider -t crawl test domain.com
來指定使用 CrawlSpider 建立爬蟲。生產文件內容如下:
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 的爬蟲不同之處在於多了一個 rules 的屬性,該屬性定義瞭如何從網頁中提取 url,並使用指定的回調函數來處理爬取結果。
使用遞歸爬蟲來實現「立創商城」中生產商的爬取在合適不過了,以下貼出相應的鏈接提取規則和處理函數。
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,
)
動態數據處理
爬蟲在處理的過程中不可避免的會遇到動態數據的處理,「立創商城」中元件的列表頁面的翻頁即是通過 ajax 來實現的,如果僅僅使用上一節中的遞歸爬取的方法,有很多的元件將會被漏掉,在這裏可以使用 scrapy 模擬 post 方法來實現翻頁的效果。
在 scrapy 中向網站中提交數據使用 scrapy.FormRequest
來實現。FormRequest 類擴展了基 Request 具有處理HTML表單的功能。通過 FormReques 向翻頁 API 上提交新的頁面信息,從而獲取新頁面中的 Json 數據,通過解析 Json 數據來獲取整個網站中的元件信息。
動態翻頁所需要的 API 及提交數據的格式在 外行學 Python 爬蟲 第六篇 動態翻頁 中做過分析,可以在那裏找到相關的信息。
通過 FormRequest 來指定 url、提交數據、返回數據的回調函數等,具體實現如下:
yield scrapy.FormRequest(url=product_post_url,
formdata=post_data,
callback=self.json_callback,
dont_filter = True)
由於 Scrapy 中自帶了 url 去重功能,因此需在 FormRequest 中設置
dont_filter = True
,否則 FormRequest 只會執行一次。
數據的存儲
Scrapy 使用 Item 來定義通用的輸出數據格式,數據通過 Item 在 Scrapy 的各個模塊中進行傳遞,以下是一個簡單的 Item 定義:
class BrandItem(scrapy.Item):
name = scrapy.Field()
url = scrapy.Field()
desc = scrapy.Field()
數據的處理通常在 Pipeline 中進行,在爬蟲中獲取的數據將通過 Item 傳遞到 Pipeline 的 process_item 方法中進行處理,以下代碼實現了將數據存在 sqlite 數據庫中。
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 文件中獲取數據庫鏈接。
Item 按照在 setting 中定義的優先級在各個 Pipeline 中進行傳遞,如果在某個 Pipeline 中對該 Item 處理完成後續無需處理,可以使用 DropItem 來終止 Item 向其他的 Pipeline 傳遞。
反爬處理
爬蟲不可避免的會遇到網站的反爬策略,一般的反爬策略是限制 IP 的訪問間隔,判斷當前的訪問代理是否總是爬蟲等。
針對以上策略,可以通過設置兩個請求之間間隔隨機的時間,並設置 User-Agent 來規避一部分的反爬策略。
設置請求間隔隨機時間的中間件實現如下:
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 文件中修改,在我們的瀏覽器中查看當前瀏覽器的 User-Agent,將 Scrapy 的 User-Agent 設置爲瀏覽器的 User-Agent。以下是 Chrome 流量中 User-Agent 的查找方法。
前面都沒有提到過網站的反爬蟲,這次提到的原因是真的被「立創商城」給限制訪問了。
運行爬蟲
今天將前面所完成的爬蟲功能使用 Scrapy 進行了一個重構,catalog 使用的是單頁爬蟲用來獲取原件的分類信息,brand 是一個遞歸爬蟲用來獲取原件生產商信息,product 是一個通過 post 動態獲取 json 並解析的爬蟲,主要用來獲取所有元件的信息。
有多個爬蟲需要運行,可以使用以下方法逐個運行爬蟲
# -*- coding:utf-8 -*-
import os
os.system("scrapy crawl brand")
os.system("scrapy crawl catalog")
os.system("scrapy crawl product")
如果想同時運行多個爬蟲,以下方法是個不錯的選擇
# -*- 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()