說起Python,我們或許自然而然的想到其在爬蟲方面的重大貢獻。Python的流行在于其語言的優美以及良好的氛圍。相對于Java,js等語言來說,Python API在封裝上面要好很多。今天我們要說的是Python的一個通用的開源爬蟲框架 scrapy。
scrapy在爬蟲界可謂是鼎鼎大名。其內部寫好的各種組件使用起來可謂是順風順水。各組件的功能我不在此一一列舉了,下面教程中會簡略提到。整體架構如下圖所示:
框架簡介:
引用自 http://blog.csdn.net/zbyufei/article/details/7554322 已經了解scrapy可以直接略過,也可以當復習使用
一、綠線是數據流向,首先從初始 URL 開始,Scheduler 會將其交給 Downloader 進行下載,下載之后會交給 Spider 進行分析,Spider 分析出來的結果有兩種:一種是需要進一步抓取的鏈接,例如之前分析的“下一頁”的鏈接,這些東西會被傳回 Scheduler ;另一種是需要保存的數據,它們則被送到 Item Pipeline 那里,那是對數據進行后期處理(詳細分析、過濾、存儲等)的地方。另外,在數據流動的通道里還可以安裝各種中間件,進行必要的處理。
二、組件
1、Scrapy Engine(Scrapy引擎)
Scrapy引擎是用來控制整個系統的數據處理流程,并進行事務處理的觸發。更多的詳細內容可以看下面的數據處理流程。
2、Scheduler(調度)
調度程序從Scrapy引擎接受請求并排序列入隊列,并在Scrapy引擎發出請求后返還給他們。
3、Downloader(下載器)
下載器的主要職責是抓取網頁并將網頁內容返還給蜘蛛( Spiders)。
4、Spiders(蜘蛛)
蜘蛛是有Scrapy用戶自己定義用來解析網頁并抓取制定URL返回的內容的類,每個蜘蛛都能處理一個域名或一組域名。換句話說就是用來定義特定網站的抓取和解析規則。
蜘蛛的整個抓取流程(周期)是這樣的:
首先獲取第一個URL的初始請求,當請求返回后調取一個回調函數。第一個請求是通過調用start_requests()方法。該方法默認從start_urls中的Url中生成請求,并執行解析來調用回調函數。
在回調函數中,你可以解析網頁響應并返回項目對象和請求對象或兩者的迭代。這些請求也將包含一個回調,然后被Scrapy下載,然后有指定的回調處理。
在回調函數中,你解析網站的內容,同程使用的是Xpath選擇器(但是你也可以使用BeautifuSoup, lxml或其他任何你喜歡的程序),并生成解析的數據項。
最后,從蜘蛛返回的項目通常會進駐到項目管道。
5、Item Pipeline(項目管道)
項目管道的主要責任是負責處理有蜘蛛從網頁中抽取的項目,他的主要任務是清晰、驗證和存儲數據。當頁面被蜘蛛解析后,將被發送到項目管道,并經過幾個特定的次序處理數據。每個項目管道的組件都是有一個簡單的方法組成的Python類。他們獲取了項目并執行他們的方法,同時他們還需要確定的是是否需要在項目管道中繼續執行下一步或是直接丟棄掉不處理。
項目管道通常執行的過程有:
清洗HTML數據
驗證解析到的數據(檢查項目是否包含必要的字段)
檢查是否是重復數據(如果重復就刪除)
將解析到的數據存儲到數據庫中
6、Downloader middlewares(下載器中間件)
下載中間件是位于Scrapy引擎和下載器之間的鉤子框架,主要是處理Scrapy引擎與下載器之間的請求及響應。它提供了一個自定義的代碼的方式來拓展Scrapy的功能。下載中間器是一個處理請求和響應的鉤子框架。他是輕量級的,對Scrapy盡享全局控制的底層的系統。
7、Spider middlewares(蜘蛛中間件)
蜘蛛中間件是介于Scrapy引擎和蜘蛛之間的鉤子框架,主要工作是處理蜘蛛的響應輸入和請求輸出。它提供一個自定義代碼的方式來拓展Scrapy的功能。蛛中間件是一個掛接到Scrapy的蜘蛛處理機制的框架,你可以插入自定義的代碼來處理發送給蜘蛛的請求和返回蜘蛛獲取的響應內容和項目。
8、Scheduler middlewares(調度中間件)
調度中間件是介于Scrapy引擎和調度之間的中間件,主要工作是處從Scrapy引擎發送到調度的請求和響應。他提供了一個自定義的代碼來拓展Scrapy的功能。
三、數據處理流程
Scrapy的整個數據處理流程有Scrapy引擎進行控制,其主要的運行方式為:
引擎打開一個域名,時蜘蛛處理這個域名,并讓蜘蛛獲取第一個爬取的URL。
引擎從蜘蛛那獲取第一個需要爬取的URL,然后作為請求在調度中進行調度。
引擎從調度那獲取接下來進行爬取的頁面。
調度將下一個爬取的URL返回給引擎,引擎將他們通過下載中間件發送到下載器。
當網頁被下載器下載完成以后,響應內容通過下載中間件被發送到引擎。
引擎收到下載器的響應并將它通過蜘蛛中間件發送到蜘蛛進行處理。
蜘蛛處理響應并返回爬取到的項目,然后給引擎發送新的請求。
引擎將抓取到的項目項目管道,并向調度發送請求。
系統重復第二部后面的操作,直到調度中沒有請求,然后斷開引擎與域之間的聯系。
模擬登陸
無恥的引用完別人的介紹之后,我們來說說怎樣去爬知乎。
我的思路:通過當前用戶的主頁拿到其用戶數據,如下圖:
查找其關注的用戶和粉絲列表,以其關注者為例:
然后依次取其關注者或者粉絲的數據,最后將爬到的數據進行處理,存入數據庫(我這里使用mongodb,mongodb相關知識就不細說了)
如果你嘗試過去爬知乎的話,當你爬到起粉絲列表你會發現無論你怎么努力,最后爬到的也只不過是知乎的登錄頁面:
因為知乎是在線用戶才能訪問其他用戶的關注列表和粉絲列表,所以基于此,我們必須偽裝在線用戶才能拿到數據。
首先我們得知道在線用戶在訪問知乎的時候向其發送了什么數據。我們在chrome里打開調試器,然后點擊network選項卡:
現在幾乎是沒有什么數據的,此時刷新一下當前頁面
就這樣我們看到下載的源碼就是已經登錄用戶的源碼了。
代碼測試
下面我們要找到發送的參數點擊上邊的headers選項卡,往下拉倒RequestHeaders,這就是我們要訪問知乎所需要的數據:
這里主要說一下幾個參數:
- User-Agent: 用戶代理,主要給服務器提供目前瀏覽器的參數,不同的用戶代理用戶獲取的數據可能會不一樣。服務器也可以根據是否存在此字段來防爬,也可以通過robots.txt來屏蔽掉某些爬蟲。
- cookie: 服務器返回的標志用戶信息的一些鍵值對。大多數網站也是基于此驗證用戶身份的
- Referer:HTTP參照位址,用以表示從哪連接到目前的網頁。通過這個字段可以做些圖片防盜鏈處理,這也常被用來對付偽造的跨網站請求。
我們將數據粘貼到接口測試工具里(這里我使用的paw)結果如下:
看,我們已經拿到了結果
既然已經拿到了我們想要的數據,那么其粉絲列表自然也就不在話下:
現在我們可以測試下用代碼獲取數據啦
先利用paw給出的demo來測試下:
代碼如上圖所示(這里的代碼我就不給出了,沒什么東西)
可以證明,我們拿到的數據是對的
當然,這個列表最長只有20個,我會在以后的教程里給出如何獲取所有的關注者和粉絲(這里有點小坑)
scrapy上場
好了,萬事俱備,就差去爬東西了。
scrapy框架的具體使用方式官方有中文文檔,我這里就不詳說了
這里直接開始寫代碼:
設置settings文件
settings文件是爬蟲項目的配置文件,我們可以把cookie和header放在這里,用于調用
在這里我做成了兩個字典
在這里提供一個把一行的cookie分成字典的小工具
print u'請輸入你要分割的 cookie,回車開始分割'
str = raw_input()
print '\n\n\n\n\n\n'
arr = str.split(';')
for i in arr:
i = '"' + i
if not i.startswith('"'):
i = '"' + i
if not i.endswith('"'):
i += '"'
i += ','
arrs = i.split('=', 1)
if not arrs[0].endswith('"'):
arrs[0] += '"'
if not arrs[1].startswith('"'):
arrs[1] = '"' + arrs[1]
print ':'.join(arrs)
接下來配置 AutoThrottle,用來設置爬蟲的延遲,防止內服務器ban掉ip
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1
AUTOTHROTTLE_MAX_DELAY = 10
創建spider文件
在spiders文件夾里創建一個名為ZhihuSpider.py 文件
代碼如下 :
# -*- coding: utf-8 -*-
from zhihu_spider.settings import *
import scrapy
class ZhihuSpider(scrapy.Spider):
# 爬蟲的名字,啟動爬蟲時需要用
name = 'zhihu'
# 從哪開始爬
start_urls = ['https://www.zhihu.com/people/chi-chu-63']
# 只能爬數組內的域名
allowed_domains = ['www.zhihu.com']
上面寫好了之后,我們需要用上我們設置的COOKIE
這里我們重寫下make_requests_from_url方法,把COOKIE傳入:
def make_requests_from_url(self, url):
return Request(url, method='GET',headers=ZHIHU_HEADER, cookies=ZHIHU_COOKIE)
接下來,在parse方法里通過response參數我們就能通過xpath或者css selector里拿到需要的參數了
# 拿到用戶名
response.css('.title-section .name::text').extract_first()
配置 item
拿到參數我們需要通過item包裝起來,yield出去才能保證數據通過scrapy框架進入下一個流程(處理item)
我們需要寫一個類繼承 scrapy.Item,代碼如下:
class ZhihuSpiderItem(scrapy.Item):
user_name = scrapy.Field() # 用戶名
followees = scrapy.Field() # 用戶粉絲
followers = scrapy.Field() # 用戶關注的人
introduce = scrapy.Field() # 簡介
ellipsis = scrapy.Field() # 用戶詳細介紹
location = scrapy.Field() # 住址
major = scrapy.Field() # 主修
head_image = scrapy.Field() # 頭像url
views = scrapy.Field() # 瀏覽次數
ask = scrapy.Field() # 提問
answer = scrapy.Field() # 回答
articles = scrapy.Field() # 文章
collected = scrapy.Field() # 收藏
public_editor = scrapy.Field() # 公共編輯
main_page = scrapy.Field()
_id = scrapy.Field()
image_urls = scrapy.Field()
images = scrapy.Field()
接下來,我們在 spider.py文件里將取到的參數放到item里:
def parse(self, response):
item = ZhihuSpiderItem()
user_name = response.css('.title-section .name::text').extract_first()
print user_name
if user_name:
item['user_name'] = user_name
follow = response.css(
'body > div.zg-wrap.zu-main.clearfix > div.zu-main-sidebar > div.zm-profile-side-following.zg-clear > a> strong::text').extract()
if follow:
if follow[0]:
item['followees'] = int(follow[0])
if follow[1]:
item['followers'] = int(follow[1])
item['introduce'] = ''.join(response.css(
'div.zg-wrap.zu-main.clearfix > div.zu-main-content > div > div.zm-profile-header.ProfileCard > div.zm-profile-header-main > div > div.zm-profile-header-info > div > div.zm-profile-header-description.editable-group > span.info-wrap.fold-wrap.fold.disable-fold > span.fold-item > span::text').extract())
item['ellipsis'] = ''.join(response.css(
'body > div.zg-wrap.zu-main.clearfix > div.zu-main-content > div > div.zm-profile-header.ProfileCard > div.zm-profile-header-main > div > div.top > div.title-section > div::text').extract())
item['location'] = ''.join(response.css('.location .topic-link::text').extract())
item['major'] = ''.join(response.css('.business .topic-link::text').extract())
head_url = re.sub(r'_l\.', '.', ''.join(response.css('.body .Avatar--l::attr(src)').extract()))
arr = []
arr.append(head_url)
item['head_image'] = head_url
item['image_urls'] = arr
item['ask'] = int(''.join(response.css('.active+ .item .num::text').extract()))
item['answer'] = int(''.join(response.css('.item:nth-child(3) .num::text').extract()))
item['articles'] = int(''.join(response.css('.item:nth-child(4) .num::text').extract()))
item['collected'] = int(''.join(response.css('.item:nth-child(5) .num::text').extract()))
item['public_editor'] = int(''.join(response.css('.item:nth-child(6) .num::text').extract()))
item['views'] = int(''.join(response.css('.zg-gray-normal strong::text').extract()))
if response.url:
item['main_page'] = response.url
print response.url
item['_id'] = hashlib.sha1(response.url).hexdigest()
yield item
這里我使用了 用戶url的sha1作為主鍵_id,用來存入數據庫排重
目前我們已經拿到了一個用戶的數據,并且yield出去,接下來我們要拿到用戶關注的列表:
首先我們先拿到用戶關注人數所對應的URL,同樣這里用CSS Selector取到URL之后通過一個回調方法進入下一個頁面取到關注的用戶列表,這里就不贅述了
urls = response.css(
'body > div.zg-wrap.zu-main.clearfix > div.zu-main-sidebar > div.zm-profile-side-following.zg-clear > a:nth-child(1)::attr(href)').extract()
if urls:
for url in urls:
url = 'https://www.zhihu.com' + url
yield scrapy.Request(url=url, callback=self.parse_followers,headers=ZHIHU_HEADER, cookies=ZHIHU_COOKIE)
def parse_followers(self, response):
urls = response.xpath('//*[@id="zh-profile-follows-list"]/div/div/a/@href').extract()
if urls:
for url in urls:
url = self.base_url + url
yield scrapy.Request(url=url, callback=self.parse,headers=ZHIHU_HEADER, cookies=ZHIHU_COOKIE)
由此,知乎就可以一直運行下去了。
將item數據存至數據庫
處理item主要是由pipeline完成的,在這里我們需要自定義一個pipeline
# -*- coding: utf-8 -*-
# Define your item pipelines here
#
# Don't forget to add your pipeline to the ITEM_PIPELINES setting
# See: http://doc.scrapy.org/en/latest/topics/item-pipeline.html
import pymongo
from zhihu_spider.items import ZhihuSpiderItem
# from scrapy.contrib.pipeline.images import ImagesPipeline
from scrapy.exceptions import DropItem
class ZhihuSpiderPipeLine(object):
def __init__(self):
import pymongo
connection = pymongo.MongoClient('127.0.0.1', 27017)
self.db = connection["zhihu"]
self.zh_user = self.db['zh_user']
def process_item(self, item, spider):
if isinstance(item, ZhihuSpiderItem):
self.saveOrUpdate(self.zh_user, item)
def saveOrUpdate(self, collection, item):
try:
collection.insert(dict(item))
return item
except:
raise DropItem('重復嘍')
我們在init方法中初始化了數據庫,然后設置相關的 db_name和collection_name
然后將pipeline路徑放置到settings.py文件中,scrapy會自動調用
ITEM_PIPELINES = {
'zhihu_spider.pipelines.ZhihuSpiderPipeLine': 300,
}
下載用戶頭像
首先將settings.py中的ROBOTSTXT_OBEY
設置為False,讓爬蟲不遵循robots.txt中的規定。不然圖片是拿不到的,因為:
如上圖,按照規則,知乎的robots.txt是不允許我們抓取/people/文件夾下的東西的,但是用戶頭像就是在這。所以不得不卑鄙一回啦~
scrapy已經提供給我們幾個寫好的的下載器了,我這里使用 默認的ImagePipeline
使用方式很簡單,只需要在item里加入
image_urls = scrapy.Field()
images = scrapy.Field()
兩個字段,然后將頭像以數組形式傳入image_urls里,然后在settings.py里面添加
'scrapy.contrib.pipeline.images.ImagesPipeline': 100
到ITEM_PIPELINES
字典中便可以了,pipeline路徑后邊的數字越小,越先被處理,越大越后處理,如果配置有覆蓋,數字大的會覆蓋小的。
最后一步:設置文件下載路徑:將IMAGES_STORE = '/path/to/image'
添加至settings.py文件中
在命令行中通過 scrapy crawl zhihu 啟動爬蟲就可以爬取數據了
接下來查詢數據庫:
上圖就是我的數據了
頭像也拿到了