說起Python癣亚,我們或許自然而然的想到其在爬蟲方面的重大貢獻(xiàn)。Python的流行在于其語言的優(yōu)美以及良好的氛圍获印。相對(duì)于Java述雾,js等語言來說,Python API在封裝上面要好很多兼丰。今天我們要說的是Python的一個(gè)通用的開源爬蟲框架 scrapy玻孟。
scrapy在爬蟲界可謂是鼎鼎大名。其內(nèi)部寫好的各種組件使用起來可謂是順風(fēng)順?biāo)⒄鳌8鹘M件的功能我不在此一一列舉了黍翎,下面教程中會(huì)簡(jiǎn)略提到。整體架構(gòu)如下圖所示:
框架簡(jiǎn)介:
引用自 http://blog.csdn.net/zbyufei/article/details/7554322 已經(jīng)了解scrapy可以直接略過艳丛,也可以當(dāng)復(fù)習(xí)使用
一匣掸、綠線是數(shù)據(jù)流向,首先從初始 URL 開始氮双,Scheduler 會(huì)將其交給 Downloader 進(jìn)行下載旺聚,下載之后會(huì)交給 Spider 進(jìn)行分析,Spider 分析出來的結(jié)果有兩種:一種是需要進(jìn)一步抓取的鏈接眶蕉,例如之前分析的“下一頁”的鏈接砰粹,這些東西會(huì)被傳回 Scheduler ;另一種是需要保存的數(shù)據(jù),它們則被送到 Item Pipeline 那里碱璃,那是對(duì)數(shù)據(jù)進(jìn)行后期處理(詳細(xì)分析弄痹、過濾、存儲(chǔ)等)的地方嵌器。另外肛真,在數(shù)據(jù)流動(dòng)的通道里還可以安裝各種中間件,進(jìn)行必要的處理爽航。
二蚓让、組件
1、Scrapy Engine(Scrapy引擎)
Scrapy引擎是用來控制整個(gè)系統(tǒng)的數(shù)據(jù)處理流程讥珍,并進(jìn)行事務(wù)處理的觸發(fā)历极。更多的詳細(xì)內(nèi)容可以看下面的數(shù)據(jù)處理流程。
2衷佃、Scheduler(調(diào)度)
調(diào)度程序從Scrapy引擎接受請(qǐng)求并排序列入隊(duì)列趟卸,并在Scrapy引擎發(fā)出請(qǐng)求后返還給他們。
3氏义、Downloader(下載器)
下載器的主要職責(zé)是抓取網(wǎng)頁并將網(wǎng)頁內(nèi)容返還給蜘蛛( Spiders)锄列。
4、Spiders(蜘蛛)
蜘蛛是有Scrapy用戶自己定義用來解析網(wǎng)頁并抓取制定URL返回的內(nèi)容的類惯悠,每個(gè)蜘蛛都能處理一個(gè)域名或一組域名邻邮。換句話說就是用來定義特定網(wǎng)站的抓取和解析規(guī)則。
蜘蛛的整個(gè)抓取流程(周期)是這樣的:
首先獲取第一個(gè)URL的初始請(qǐng)求克婶,當(dāng)請(qǐng)求返回后調(diào)取一個(gè)回調(diào)函數(shù)筒严。第一個(gè)請(qǐng)求是通過調(diào)用start_requests()方法。該方法默認(rèn)從start_urls中的Url中生成請(qǐng)求鸠补,并執(zhí)行解析來調(diào)用回調(diào)函數(shù)萝风。
在回調(diào)函數(shù)中嘀掸,你可以解析網(wǎng)頁響應(yīng)并返回項(xiàng)目對(duì)象和請(qǐng)求對(duì)象或兩者的迭代紫岩。這些請(qǐng)求也將包含一個(gè)回調(diào),然后被Scrapy下載睬塌,然后有指定的回調(diào)處理泉蝌。
在回調(diào)函數(shù)中,你解析網(wǎng)站的內(nèi)容揩晴,同程使用的是Xpath選擇器(但是你也可以使用BeautifuSoup, lxml或其他任何你喜歡的程序)勋陪,并生成解析的數(shù)據(jù)項(xiàng)。
最后硫兰,從蜘蛛返回的項(xiàng)目通常會(huì)進(jìn)駐到項(xiàng)目管道诅愚。
5、Item Pipeline(項(xiàng)目管道)
項(xiàng)目管道的主要責(zé)任是負(fù)責(zé)處理有蜘蛛從網(wǎng)頁中抽取的項(xiàng)目劫映,他的主要任務(wù)是清晰违孝、驗(yàn)證和存儲(chǔ)數(shù)據(jù)刹前。當(dāng)頁面被蜘蛛解析后,將被發(fā)送到項(xiàng)目管道雌桑,并經(jīng)過幾個(gè)特定的次序處理數(shù)據(jù)喇喉。每個(gè)項(xiàng)目管道的組件都是有一個(gè)簡(jiǎn)單的方法組成的Python類。他們獲取了項(xiàng)目并執(zhí)行他們的方法校坑,同時(shí)他們還需要確定的是是否需要在項(xiàng)目管道中繼續(xù)執(zhí)行下一步或是直接丟棄掉不處理拣技。
項(xiàng)目管道通常執(zhí)行的過程有:
清洗HTML數(shù)據(jù)
驗(yàn)證解析到的數(shù)據(jù)(檢查項(xiàng)目是否包含必要的字段)
檢查是否是重復(fù)數(shù)據(jù)(如果重復(fù)就刪除)
將解析到的數(shù)據(jù)存儲(chǔ)到數(shù)據(jù)庫中
6、Downloader middlewares(下載器中間件)
下載中間件是位于Scrapy引擎和下載器之間的鉤子框架耍目,主要是處理Scrapy引擎與下載器之間的請(qǐng)求及響應(yīng)膏斤。它提供了一個(gè)自定義的代碼的方式來拓展Scrapy的功能。下載中間器是一個(gè)處理請(qǐng)求和響應(yīng)的鉤子框架制妄。他是輕量級(jí)的掸绞,對(duì)Scrapy盡享全局控制的底層的系統(tǒng)。
7耕捞、Spider middlewares(蜘蛛中間件)
蜘蛛中間件是介于Scrapy引擎和蜘蛛之間的鉤子框架衔掸,主要工作是處理蜘蛛的響應(yīng)輸入和請(qǐng)求輸出。它提供一個(gè)自定義代碼的方式來拓展Scrapy的功能俺抽。蛛中間件是一個(gè)掛接到Scrapy的蜘蛛處理機(jī)制的框架敞映,你可以插入自定義的代碼來處理發(fā)送給蜘蛛的請(qǐng)求和返回蜘蛛獲取的響應(yīng)內(nèi)容和項(xiàng)目昵宇。
8华蜒、Scheduler middlewares(調(diào)度中間件)
調(diào)度中間件是介于Scrapy引擎和調(diào)度之間的中間件实撒,主要工作是處從Scrapy引擎發(fā)送到調(diào)度的請(qǐng)求和響應(yīng)资盅。他提供了一個(gè)自定義的代碼來拓展Scrapy的功能恳谎。
三隙疚、數(shù)據(jù)處理流程
Scrapy的整個(gè)數(shù)據(jù)處理流程有Scrapy引擎進(jìn)行控制楔脯,其主要的運(yùn)行方式為:
引擎打開一個(gè)域名喇肋,時(shí)蜘蛛處理這個(gè)域名侣颂,并讓蜘蛛獲取第一個(gè)爬取的URL档桃。
引擎從蜘蛛那獲取第一個(gè)需要爬取的URL,然后作為請(qǐng)求在調(diào)度中進(jìn)行調(diào)度憔晒。
引擎從調(diào)度那獲取接下來進(jìn)行爬取的頁面藻肄。
調(diào)度將下一個(gè)爬取的URL返回給引擎,引擎將他們通過下載中間件發(fā)送到下載器拒担。
當(dāng)網(wǎng)頁被下載器下載完成以后嘹屯,響應(yīng)內(nèi)容通過下載中間件被發(fā)送到引擎。
引擎收到下載器的響應(yīng)并將它通過蜘蛛中間件發(fā)送到蜘蛛進(jìn)行處理从撼。
蜘蛛處理響應(yīng)并返回爬取到的項(xiàng)目州弟,然后給引擎發(fā)送新的請(qǐng)求。
引擎將抓取到的項(xiàng)目項(xiàng)目管道,并向調(diào)度發(fā)送請(qǐng)求婆翔。
系統(tǒng)重復(fù)第二部后面的操作桐经,直到調(diào)度中沒有請(qǐng)求,然后斷開引擎與域之間的聯(lián)系浙滤。
模擬登陸
無恥的引用完別人的介紹之后阴挣,我們來說說怎樣去爬知乎。
我的思路:通過當(dāng)前用戶的主頁拿到其用戶數(shù)據(jù)纺腊,如下圖:
查找其關(guān)注的用戶和粉絲列表畔咧,以其關(guān)注者為例:
然后依次取其關(guān)注者或者粉絲的數(shù)據(jù),最后將爬到的數(shù)據(jù)進(jìn)行處理揖膜,存入數(shù)據(jù)庫(我這里使用mongodb誓沸,mongodb相關(guān)知識(shí)就不細(xì)說了)
如果你嘗試過去爬知乎的話,當(dāng)你爬到起粉絲列表你會(huì)發(fā)現(xiàn)無論你怎么努力壹粟,最后爬到的也只不過是知乎的登錄頁面:
因?yàn)橹跏窃诰€用戶才能訪問其他用戶的關(guān)注列表和粉絲列表拜隧,所以基于此,我們必須偽裝在線用戶才能拿到數(shù)據(jù)趁仙。
首先我們得知道在線用戶在訪問知乎的時(shí)候向其發(fā)送了什么數(shù)據(jù)洪添。我們?cè)赾hrome里打開調(diào)試器,然后點(diǎn)擊network選項(xiàng)卡:
現(xiàn)在幾乎是沒有什么數(shù)據(jù)的雀费,此時(shí)刷新一下當(dāng)前頁面
就這樣我們看到下載的源碼就是已經(jīng)登錄用戶的源碼了干奢。
代碼測(cè)試
下面我們要找到發(fā)送的參數(shù)點(diǎn)擊上邊的headers選項(xiàng)卡,往下拉倒RequestHeaders盏袄,這就是我們要訪問知乎所需要的數(shù)據(jù):
這里主要說一下幾個(gè)參數(shù):
- User-Agent: 用戶代理忿峻,主要給服務(wù)器提供目前瀏覽器的參數(shù),不同的用戶代理用戶獲取的數(shù)據(jù)可能會(huì)不一樣辕羽。服務(wù)器也可以根據(jù)是否存在此字段來防爬逛尚,也可以通過robots.txt來屏蔽掉某些爬蟲。
- cookie: 服務(wù)器返回的標(biāo)志用戶信息的一些鍵值對(duì)刁愿。大多數(shù)網(wǎng)站也是基于此驗(yàn)證用戶身份的
- Referer:HTTP參照位址绰寞,用以表示從哪連接到目前的網(wǎng)頁。通過這個(gè)字段可以做些圖片防盜鏈處理酌毡,這也常被用來對(duì)付偽造的跨網(wǎng)站請(qǐng)求克握。
我們將數(shù)據(jù)粘貼到接口測(cè)試工具里(這里我使用的paw)結(jié)果如下:
看蕾管,我們已經(jīng)拿到了結(jié)果
既然已經(jīng)拿到了我們想要的數(shù)據(jù)枷踏,那么其粉絲列表自然也就不在話下:
現(xiàn)在我們可以測(cè)試下用代碼獲取數(shù)據(jù)啦
先利用paw給出的demo來測(cè)試下:
代碼如上圖所示(這里的代碼我就不給出了,沒什么東西)
可以證明掰曾,我們拿到的數(shù)據(jù)是對(duì)的
當(dāng)然旭蠕,這個(gè)列表最長(zhǎng)只有20個(gè),我會(huì)在以后的教程里給出如何獲取所有的關(guān)注者和粉絲(這里有點(diǎn)小坑)
scrapy上場(chǎng)
好了,萬事俱備掏熬,就差去爬東西了佑稠。
scrapy框架的具體使用方式官方有中文文檔,我這里就不詳說了
這里直接開始寫代碼:
設(shè)置settings文件
settings文件是爬蟲項(xiàng)目的配置文件旗芬,我們可以把cookie和header放在這里舌胶,用于調(diào)用
在這里我做成了兩個(gè)字典
在這里提供一個(gè)把一行的cookie分成字典的小工具
print u'請(qǐng)輸入你要分割的 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,用來設(shè)置爬蟲的延遲疮丛,防止內(nèi)服務(wù)器ban掉ip
AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 1
AUTOTHROTTLE_MAX_DELAY = 10
創(chuàng)建spider文件
在spiders文件夾里創(chuàng)建一個(gè)名為ZhihuSpider.py 文件
代碼如下 :
# -*- coding: utf-8 -*-
from zhihu_spider.settings import *
import scrapy
class ZhihuSpider(scrapy.Spider):
# 爬蟲的名字幔嫂,啟動(dòng)爬蟲時(shí)需要用
name = 'zhihu'
# 從哪開始爬
start_urls = ['https://www.zhihu.com/people/chi-chu-63']
# 只能爬數(shù)組內(nèi)的域名
allowed_domains = ['www.zhihu.com']
上面寫好了之后,我們需要用上我們?cè)O(shè)置的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參數(shù)我們就能通過xpath或者css selector里拿到需要的參數(shù)了
# 拿到用戶名
response.css('.title-section .name::text').extract_first()
配置 item
拿到參數(shù)我們需要通過item包裝起來,yield出去才能保證數(shù)據(jù)通過scrapy框架進(jìn)入下一個(gè)流程(處理item)
我們需要寫一個(gè)類繼承 scrapy.Item,代碼如下:
class ZhihuSpiderItem(scrapy.Item):
user_name = scrapy.Field() # 用戶名
followees = scrapy.Field() # 用戶粉絲
followers = scrapy.Field() # 用戶關(guān)注的人
introduce = scrapy.Field() # 簡(jiǎn)介
ellipsis = scrapy.Field() # 用戶詳細(xì)介紹
location = scrapy.Field() # 住址
major = scrapy.Field() # 主修
head_image = scrapy.Field() # 頭像url
views = scrapy.Field() # 瀏覽次數(shù)
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()
接下來呢蔫,我們?cè)?spider.py文件里將取到的參數(shù)放到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切心,用來存入數(shù)據(jù)庫排重
目前我們已經(jīng)拿到了一個(gè)用戶的數(shù)據(jù),并且yield出去片吊,接下來我們要拿到用戶關(guān)注的列表:
首先我們先拿到用戶關(guān)注人數(shù)所對(duì)應(yīng)的URL绽昏,同樣這里用CSS Selector取到URL之后通過一個(gè)回調(diào)方法進(jìn)入下一個(gè)頁面取到關(guān)注的用戶列表,這里就不贅述了
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)
由此俏脊,知乎就可以一直運(yùn)行下去了而涉。
將item數(shù)據(jù)存至數(shù)據(jù)庫
處理item主要是由pipeline完成的,在這里我們需要自定義一個(gè)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('重復(fù)嘍')
我們?cè)趇nit方法中初始化了數(shù)據(jù)庫联予,然后設(shè)置相關(guān)的 db_name和collection_name
然后將pipeline路徑放置到settings.py文件中啼县,scrapy會(huì)自動(dòng)調(diào)用
ITEM_PIPELINES = {
'zhihu_spider.pipelines.ZhihuSpiderPipeLine': 300,
}
下載用戶頭像
首先將settings.py中的ROBOTSTXT_OBEY
設(shè)置為False,讓爬蟲不遵循robots.txt中的規(guī)定。不然圖片是拿不到的沸久,因?yàn)椋?/p>
如上圖季眷,按照規(guī)則,知乎的robots.txt是不允許我們抓取/people/文件夾下的東西的卷胯,但是用戶頭像就是在這子刮。所以不得不卑鄙一回啦~
scrapy已經(jīng)提供給我們幾個(gè)寫好的的下載器了,我這里使用 默認(rèn)的ImagePipeline
使用方式很簡(jiǎn)單窑睁,只需要在item里加入
image_urls = scrapy.Field()
images = scrapy.Field()
兩個(gè)字段挺峡,然后將頭像以數(shù)組形式傳入image_urls里,然后在settings.py里面添加
'scrapy.contrib.pipeline.images.ImagesPipeline': 100
到ITEM_PIPELINES
字典中便可以了担钮,pipeline路徑后邊的數(shù)字越小橱赠,越先被處理,越大越后處理箫津,如果配置有覆蓋狭姨,數(shù)字大的會(huì)覆蓋小的宰啦。
最后一步:設(shè)置文件下載路徑:將IMAGES_STORE = '/path/to/image'
添加至settings.py文件中
在命令行中通過 scrapy crawl zhihu 啟動(dòng)爬蟲就可以爬取數(shù)據(jù)了
接下來查詢數(shù)據(jù)庫:
上圖就是我的數(shù)據(jù)了
頭像也拿到了