關於Scrapy如何安裝部署的文章已經相當多了,但是網上實戰的例子還不是很多,近來正好在學習該爬蟲框架,就簡單寫了個Spider Demo來實踐。
作為硬件數碼控,我選擇了經常光顧的中關村在線的手機頁面進行爬取,大體思路如下圖所示。

1 # coding:utf-8
2 import scrapy
3 import re
4 import os
5 import sqlite3
6 from myspider.items import SpiderItem
7
8
9 class ZolSpider(scrapy.Spider):
10 name = "zol"
11 # allowed_domains = ["http://detail.zol.com.cn/"] # 用於限定爬取的服務器域名
12 start_urls = [
13 # 主要爬去中關村在線的手機信息頁面,考慮到是演示目的就僅僅爬了首頁,其實爬分頁跟二級爬蟲原理相同,出於節省時間目的這里就不爬了
14 # 這里可以寫多個入口URL
15 "http://detail.zol.com.cn/cell_phone_index/subcate57_list_1.html"
16 ]
17 item = SpiderItem() # 沒法動態創建,索性沒用上,用的meta在spider函數間傳值
18 # 只是test一下就用sqlite吧,比較輕量化
19 #database = sqlite3.connect(":memory:")
20 database_file = os.path.dirname(os.path.abspath(__file__)) + "\\phonedata.db"
21 if os.path.exists(database_file):
22 os.remove(database_file)
23 database = sqlite3.connect(database_file)
24 # 先建個字段,方便理解字段含義就用中文了
25 database.execute(
26 '''
27 CREATE TABLE CELL_PHONES
28 (
29 手機型號 TEXT
30 );
31 '''
32 )
33 # 用於檢查數據增改是否全面,與total_changes對比
34 counter = 0
35
36 # 手機報價首頁爬取函數
37 def parse(self, response):
38 # 獲取手機詳情頁鏈接並以其創建二級爬蟲
39 hrefs = response.xpath("//h3/a")
40 for href in hrefs:
41 url = response.urljoin(href.xpath("@href")[0].extract())
42 yield scrapy.Request(url, self.parse_detail_page)
43
44 # 手機詳情頁爬取函數
45 def parse_detail_page(self, response):
46 # 通過xpath獲取手機型號
47 model = response.xpath("//h1").xpath("text()")[0].extract()
48 # 創建該型號手機的數據庫記錄
49 sql = 'INSERT INTO CELL_PHONES (手機型號) VALUES ("' + model + '")'
50 self.counter += 1
51 self.database.execute(sql)
52 self.database.commit()
53 # 獲取參數詳情頁的鏈接
54 url = response.urljoin(response.xpath("//div[@id='tagNav']//a[text()='參數']").xpath("@href")[0].extract())
55 # 由於Scrapy是異步驅動的(逐級啟動爬蟲函數),所以當需綁定父子級爬蟲函數間的某些變量時,可以采用meta字典傳遞,全局的item字段無法動態創建,在較靈活的爬取場景中不是很適用
56 yield scrapy.Request(url, callback=self.parse_param_page, meta={'model': model})
57
58 # 手機參數詳情頁爬取函數
59 def parse_param_page(self, response):
60 # 獲取手機參數字段並一一遍歷
61 params = response.xpath("//span[contains(@class,'param-name')]")
62 for param in params:
63 legal_param_name_field = param_name = param.xpath("text()")[0].extract()
64 # 將手機參數字段轉變為合法的數據庫字段(非數字開頭,且防止SQL邏輯污染剔除了'/'符號)
65 if re.match(r'^\d', param_name):
66 legal_param_name_field = re.sub(r'^\d', "f" + param_name[0], param_name)
67 if '/' in param_name:
68 legal_param_name_field = legal_param_name_field.replace('/', '')
69 # 通過查詢master表檢查動態添加的字段是否已經存在,若不存在則增加該字段
70 sql = "SELECT * FROM sqlite_master WHERE name='CELL_PHONES' AND SQL LIKE '%" + legal_param_name_field + "%'"
71 if self.database.execute(sql).fetchone() is None:
72 sql = "ALTER TABLE CELL_PHONES ADD " + legal_param_name_field + " TEXT"
73 self.database.execute(sql)
74 self.database.commit()
75 # 根據參數字段名的xpath定位參數值元素
76 xpath = "//span[contains(@class,'param-name') and text()='" + param_name +\
77 "']/following-sibling::span[contains(@id,'newPmVal')]//text()"
78 vals = response.xpath(xpath)
79 # 由於有些字段的參數值是多個值,所以需將其附加到一起,合成一個字段,以方便存儲。
80 # 如需數據細分選用like子句或支持全文索引的數據庫也不錯,當然nosql更好
81 pm_val = ""
82 for val in vals:
83 pm_val += val.extract()
84 re.sub(r'\r|\n',"",pm_val)
85 sql = "UPDATE CELL_PHONES SET %s = '%s' WHERE 手機型號 = '%s'" \
86 % (legal_param_name_field, pm_val, response.meta['model'])
87 self.database.execute(sql)
88 self.counter += 1
89 # 檢查下爬取的數據對不對
90 results = self.database.execute("SELECT * FROM CELL_PHONES").fetchall()
91 # 千萬別忘了commit否則持久化數據庫可能結果不全
92 self.database.commit()
93 print(self.database.total_changes, self.counter) # 對比下數據庫的增改情況是否有丟失
94 for row in results:
95 print(row, end='\n') # 其實這里有個小小的編碼問題需要解決
96 # 最后愉快的用scrapy crawl zol 啟動爬蟲吧!
部分爬到數據庫的數據

最后建議在settings腳本中修改USER_AGENT,以模擬瀏覽器請求,避免反爬,例如:
USER_AGENT = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/55.0.2883.87 Safari/537.36'
當然高級點的反爬手段也有別的辦法應付:
1.基於用戶行為的爬取,可以增設爬取邏輯路由,以動態的爬行方式獲取資源,並且頻繁切換IP及UA,基於session及cookie的反爬亦可以基於此手段;
2.AJAX等異步js交互的頁面,可自定義js請求,如果請求被加密了,結合selenium + webdriver來驅動瀏覽器,模擬用戶交互異曲同工;
3.關於匹配方式,正則,XPath、CSS等等selector因人而異,前端經常調整的話不建議用CSS selector;
正則表達式從執行效率上較XPath會高一些,但是XPath可以基於元素邏輯層次、屬性值條件,甚至結合XPath函數十分靈活的定位一個多個(組)元素;
總的來說做爬蟲的同學,正則和XPath應該是基本功啦,特別是在定向爬取數據時尤為重要。
4.關於路由及任務調度問題,雖然Scrapy提供了非常簡單的異步IO方案,能夠輕松爬取多級頁面,並根據base URL及靈活的自定義回調函數實現深層(有選擇的)爬蟲,
但對於爬取海量數據的場景,靈活性較差,因此隊列管理(排重、防中斷、防重跑)及分布式爬蟲可能更為適用。
當然,學習Python爬蟲,掌握urllib(2、3)、requests、BeautifulSoup、lxml等模塊也會讓你如虎添翼,還需因地制宜才是。
p.s.進來用Golang做爬蟲的童鞋也多了起來,性能較之Python會好不少,可以嘗試一下。會JAVA的童鞋,也可以關注下Nutch引擎。(路漫漫其修遠兮啊,一起學習吧。)