本篇將談一些scrapy的進階內容,幫助大家能更熟悉這個框架。
1. 站點選取
現在的大網站基本除了pc端都會有移動端,所以需要先確定爬哪個。
比如爬新浪微博,有以下幾個選擇:
- www.weibo.com,主站
- www.weibo.cn,簡化版
- m.weibo.cn,移動版
上面三個中,主站的微博數據是動態加載的,意味着光看源碼是看不到數據的,想爬的話要么搞清楚其api訪問情況,要么模擬js,那樣的話花的力氣可能就有點多了。weibo.cn是一個簡化版,數據能直接從網頁源代碼中解析出來,但使用正則或xpath取網頁中的元素仍然是無聊且費時的,更不用說偶爾的頁面結構錯誤更讓人抓狂。 相比之下,移動版的爬蟲最好寫,因為移動版能直接拿到json格式的數據。
一般來說,有移動版的網站優先爬移動版,會節省很多力氣。
2. 模擬登錄
現在需要登錄才能正常瀏覽的網站的越來越多了,對爬蟲也越來越不友好...所以模擬登錄在不少場合都是必不可少的。
首先,最簡單的模擬登錄是只有用戶名密碼的登錄。這時候只需要在發送第一個請求時加上登錄表單數據即可:
def start_requests(self):
return scrapy.FormRequest(
formdata={'username': '***', 'password': '***'},
callback=self.after_login
)
如果不知道登錄頁面是哪一個的話,也可以在返回第一個請求后登錄:
def parse(self, response):
return scrapy.FormRequest.from_response(
response,
formdata={'username': '***', 'password': '***'},
callback=self.after_login
)
為了保持登錄,注意cookie是不能關閉的(默認情況是開着的,可以在settings.py中設置)。
如果需要驗證碼的話,網上有一些提取分析驗證碼圖片的包,可以提取出來然后手動輸入驗證碼。
上面只是一些簡單的登錄情況,如果驗證碼很變態(比如需要鼠標滑動)或者登錄過程很復雜,需要各種加密(比如新浪微博pc端的登陸)的話,模擬登錄確實是個很讓人頭大的問題。這時候有另一個通用辦法,那就是cookie模擬登錄。網站想知道我們的登錄狀態,都是通過cookie來確認的,所以我們只需要在每次request的時候都附帶上cookie即可實現已登錄的效果。
那么,如何獲得cookie呢?有chrome的可以F12打開Network界面,這時候人工網頁登錄,即可在headers中看到cookie。得到cookie后,只需要在request中加入自己的cookie即可。
self.cookie = {"_T_WM": self.cookie_T_WM,
...
"SSOLoginState": self.cookie_SSOLoginState}
return Request(url, cookies=self.cookie, callback=self.parse_page)
3. 網頁解析
一般來說,使用xpath和css已經可以應付所有的html源碼了,剩下的只是耐心加細心...要是有心的話也可以Item Loaders,方便后期的維護,下面摘自官方文檔:
def parse(self, response):
l = ItemLoader(item=Product(), response=response)
l.add_xpath('name', '//div[@class="product_name"]')
l.add_xpath('name', '//div[@class="product_title"]')
l.add_xpath('price', '//p[@id="price"]')
l.add_css('stock', 'p#stock]')
l.add_value('last_updated', 'today')
return l.load_item()
值得一提的是如果獲取的是json格式的數據,可以使用python自帶的json庫來解析成一個字典或列表:
data = json.loads(response.body)
4. 數據存儲
可以使用twisted提供的數據庫庫來維護一個連接池:
class CnblogPipeline(object):
def __init__(self):
self.dbpool = adbapi.ConnectionPool('MySQLdb',
host='localhost',
db='cnblog',
user='root',
passwd='root',
cursorclass=MySQLdb.cursors.DictCursor,
charset='utf8',
use_unicode=True)
def process_item(self, item, spider):
self.dbpool.runInteraction(self.cnblog_insert, item)
return item
def cnblog_insert(self, cur, item):
try:
cur.execute('insert into ***')
exception MySQLdb.Error, e:
logging.info("cnblog_insert:%s" % str(e))
如果爬的是社交網站這種有着樹型結構關系的網站的話,mongodb其符合人的思維的存儲方式讓其成為首選。
如果使用mysql的話記得將innodb_flush_log_at_trx_commit這個參數置為0(每一秒讀寫一次數據和log),可以大大提高讀寫速度。
5. scrapy小知識點
- Request傳遞消息。在Request中加入meta,即可將meta傳遞給response。
Request(url, meta={'how': 'ok'}, callback=self.parse_page)
def parse_page(self, response):
print response.meta['how']
- CrawlSpider。都知道在寫自己的spider的時候需要繼承scrapy的spider,除了scrapy.Spider外,scrapy還提供了好幾種spider,其中CrawlSpider算是比較常用的。CrawlSpider的優勢在於可以用rules方便地規定新的url的樣子,即通過正則匹配來約束url。並且不需要自己生成新的url,CrawlSpider會自己尋找源碼中所有符合要求的新url的。另外,rules的回調方法名字最好不要叫parse。
class CnblogSpider(CrawlSpider):
name = "cnblog_spider"
allowed_domain = ["cnblog.com"]
start_urls = ["http://www.cnblogs.com/rubinorth"]
rules = [
Rule(LinkExtractor(allow=r"http://www.cnblogs.com/rubinorth/p/\d+\.html"),
callback="parse_page", follow=True)
]
- parse中既返回item又生成新的request。平時在parse中return item即可返回item,return request則生成新的request請求。如果我們將return換為yield的話即可既返回item又生成新的request。注意一旦使用了yield,那么parse方法中就不能有return了。
def parse_page(self, response):
item = CnblogItem()
****
yield item
yield Request(new_url, callback=self.parse_page)
- 每個spider不同設置。在spider中加入custom_settings即可覆蓋settings.py中相應的設置,這樣的話在settings.py中只需要放一些公用的設置就行了。最常用的就是設置每個spider的pipeline。
custom_settings={
'ITEM_PIPELINES' : {
'cnblog_project.pipelines.CnblogPipeline': 300,
}
}
- 讀取自己的設置。不管在spider中還是pipeline中,都可以寫from_crawler這個方法(注意spider中參數數目不同)。此方法在初始化階段由scrapy自己調用,其最大的作用就是從settings.py讀取自己的設置了。下面的代碼從settings.py中讀取了MONGO_URI。
class MongoPipeline(object):
def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI')
)
- 順序。settings.py中middleware和pipeline設置的時候需要在后面跟上一個數字,而這個數字的含義就是調用的順序,數字越小越早調用。
DOWNLOADER_MIDDLEWARES = {
'cnblog_project.my_mv.middleware.UserAgentMiddleware': 543,
'cnblog_project.my_mv.middleware.ProxyMiddleware':544,
}
- pipeline中spider.name的應用。pipeline中的process_item中可以根據spider.name來對不同的item進行不同的處理。
def process_item(self, item, spider):
if spider.name == 'a':
****
if spider.name == 'b':
****
- 多個pipeline處理一個item。pipeline中的process_item方法必須返回一個item或者raise一個DropItem的異常,如果返回item的話這個item將會被之后的pipeline接收到。
def process_item(self, item, spider):
return item
- 之后可能會有添加。
其實這些在scrapy的官方文檔都有提及,在此只是總結一些常用的知識點。若想更深入地了解scrapy,定然是閱讀其官方文檔最好了。:)
6. scrapy_redis簡介
scrapy_redis是一個分布式爬蟲的解決方案。其思想為多個爬蟲共用一個爬取隊列,此隊列使用redis存儲,因為redis是一個內存數據庫,所以速度上與單機的隊列相差不大。
那么,scrapy_redis到底對scrapy做了什么修改,達到分布式的目的呢?
查看github上面的源碼,可以發現其功能性代碼集中在scheduler.py,dupefilter.py和queue.py中,分別是調度器(部分功能),去重,以及redis隊列的實現。scrapy_redis就是將這些功能代替了scrapy原本的功能(並沒有修改scrapy源碼,只需要在settings.py中進行設置即可),從而達到了分布式的效果。