小分隊(duì)第一期的最后一次作業(yè)婉商,本次作業(yè)的內(nèi)容是爬取簡書百萬以上的用戶坟桅,不過我只爬了60多萬用戶,因?yàn)橹皼]有設(shè)置代理IP哟玷,同時(shí)請求過快的原因而被封了狮辽,然后就沒再繼續(xù)爬了。本次選用的工具是
scrapy
+mongodb
,爬取速度為每分鐘1500頁巢寡,獲取item的速度為500個(gè)每分鐘喉脖,峰值在1000個(gè)每分鐘,在運(yùn)行了一段時(shí)間后抑月,發(fā)現(xiàn)如果再修改一下, 比如說再增加線程树叽,同時(shí)通過設(shè)置爬取合適的頁數(shù)來減少請求的阻塞來增加異步的效率還可以更快,不過最終的速度還是取決于網(wǎng)速和電腦配置爪幻,當(dāng)然后期還可以改成分布式爬蟲菱皆。
作業(yè)的思路
一開始,想了兩種方案
方案一
通過專題數(shù)的關(guān)注人數(shù)這個(gè)入口來獲取用戶的ID挨稿,然后再通過這個(gè)ID跳轉(zhuǎn)到用戶的信息頁仇轻,但是在經(jīng)過嘗試后發(fā)現(xiàn),一方面是重復(fù)性不是很高奶甘,另一方面還是請求阻塞的問題篷店,這一點(diǎn)在另一種方案里也會(huì)體現(xiàn)。
方案二
通過用戶的粉絲來定位用戶臭家,也就是解析粉絲的粉絲的ID疲陕,但是這樣也會(huì)出現(xiàn)一個(gè)問題,就是會(huì)遇到一連串的沒有關(guān)注用戶的ID钉赁,所以可以選擇兩個(gè)口蹄殃,一個(gè)是用戶的粉絲,另一個(gè)是用戶的關(guān)注你踩,解析都是一樣的诅岩,再說說請求阻塞的問題讳苦,因?yàn)镾crapy是一個(gè)異步框架,如果是在一個(gè)回調(diào)函數(shù)里請求的時(shí)間太長的話吩谦,便會(huì)極大的影響異步的效率鸳谜。比較好的方法是對那些有很多粉絲數(shù)的用戶的粉絲進(jìn)行分割,比如說以50頁為一次回調(diào)到start_requests
函數(shù)式廷,但是在這里我進(jìn)行了簡化處理咐扭,就是只選取前100頁的粉絲進(jìn)行回調(diào)
代碼的分析
items.py
在本次作業(yè)中,我想爬取的信息定義了五個(gè)ITEM滑废,一方面是想要獲取個(gè)人的基本信息蝗肪,另一方面還想要獲取一個(gè)用戶的興趣偏好,個(gè)人的基本信息包括如粉絲數(shù)呀蠕趁,性別之類的穗慕,興趣則是可以從其所關(guān)注的專欄,喜歡的文章妻导,關(guān)注的人來看
import scrapy
class InformationItem(scrapy.Item):
#個(gè)人信息
_id = scrapy.Field()
nickname = scrapy.Field()
sex = scrapy.Field()
num_follows = scrapy.Field()
num_fans = scrapy.Field()
num_articles = scrapy.Field()
num_words = scrapy.Field()
num_likes = scrapy.Field()
introduction = scrapy.Field()
class FollowColletionItem(scrapy.Item):
#關(guān)注的專題
_id = scrapy.Field()
collection = scrapy.Field()
class LikeArticleItem(scrapy.Item):
_id = scrapy.Field()
title = scrapy.Field()
class FanListItem(scrapy.Item):
_id = scrapy.Field()
fans = scrapy.Field()
class FollowListItem(scrapy.Item):
_id = scrapy.Field()
follows = scrapy.Field()
因?yàn)檫@次所選用數(shù)據(jù)庫是mongodb逛绵,所以以_id作為索引
URL的去重
URL去重是實(shí)現(xiàn)大規(guī)模爬取的一個(gè)關(guān)鍵點(diǎn),當(dāng)時(shí)所想到的方案主要有兩個(gè)方向倔韭,一個(gè)是利用內(nèi)存术浪,另一個(gè)是利用硬盤空間,內(nèi)存又分為兩種寿酌,一種是利用redis這個(gè)內(nèi)存數(shù)據(jù)庫胰苏,另一個(gè)利用List,當(dāng)然前一種更合適醇疼,但是也更復(fù)雜硕并,所以就選用了后一種,另一種利用硬盤空間的辦法就是在每次爬取的ID存入數(shù)據(jù)庫秧荆,并作判斷是否重復(fù)倔毙,然后再從數(shù)據(jù)庫中選取ID進(jìn)行下一步的爬取,但是考慮到當(dāng)數(shù)據(jù)量大的時(shí)候乙濒,進(jìn)行IO操作會(huì)越來越慢陕赃,所以就沒有選這種,但是這種也有優(yōu)勢颁股,就是可以實(shí)現(xiàn)簡單的斷點(diǎn)續(xù)爬么库。
在代碼中,定義了一個(gè)列表甘有,并將這個(gè)列表變成一個(gè)集合诉儒,保證ID的惟一性
start = ["a987b338c373", "9104ebf5e177", "d83382d92519", "71a1df9e98f6","6c9580370539","65b9e2d90f5b","9d275c04c96c","a0d5c3ff90ff",
"a3f1fcaaf638", "4bbc9ef1dcf1", "7aa3354b3911", "99ec19173874","9d275c04c96c","36dcae36116b","33caf0c83b37","06d1de030894",
"6e161a868e6e", "f97610f6687b", "f1a3c1e12bc7", "b563a1b54dce","1a01d066c080","6e3331023a99","0a4cff63df55","405f676a0576",
"009f670fe134", "bbd3cf536308", "b76f1a7d4b8a", "009eac2d558e","daa7f275c77b","84c482c251b5","ca2e2b33f7d5","69b44c44f3d1",
"da35e3a5abba","00a810cfecf0", "ffb6541382aa", "5bb1e17887cf","662fac27db8c","fcd14f4d5b23","8317fcb5b167","a2a2066694de"]
scrawl_ID = set(start)
finish_ID = set()
去重
#從待爬取的最后一選取要爬取的ID
ID = self.scrawl_ID.pop()
#當(dāng)已經(jīng)爬取過了的ID就將它放入另一個(gè)集合里
self.finish_ID.add(ID)
#--------------------------分割線------------------------
ID = re.findall('/u/(.*?)">',fan,re.S)
if ID[0] not in self.finish_ID:
self.scrawl_ID.add(ID[0])
#在獲取到新的ID的時(shí)候只需要判斷一下其是否在已經(jīng)爬取過的集合里
回調(diào)
由于想要獲取的是一個(gè)用戶的很多信息,并存在不同的表亏掀,那么問題來了忱反,一個(gè)用戶如果有很多粉絲的話运准,那么其粉絲頁就要分頁了,如何將這個(gè)ITEM給傳遞下去缭受,并保證ID是一一對應(yīng)的呢?所以為了解決上面的問題该互,就需要在start_requests
這個(gè)函數(shù)里先定義好item了
follows = []
followsItem = FollowListItem()
followsItem["_id"] = ID
followsItem["follows"] = follows
fans = []
fansItems = FanListItem()
fansItems["_id"] = ID
fansItems["fans"] = fans
collection = []
collectionitem = FollowColletionItem()
collectionitem["_id"] = ID
collectionitem["collection"] = collection
likearticle = []
likearticleitem = LikeArticleItem()
likearticleitem["_id"] = ID
likearticleitem["title"] = likearticle
在代碼中先確定好ID米者,來保證ID與數(shù)據(jù)是一致的,因?yàn)槊總€(gè)用戶的粉絲數(shù)是不一樣的宇智,所以為了統(tǒng)一處理蔓搞,先定義一個(gè)列表來接受用戶的粉絲信息之類的,有多少個(gè)粉絲随橘,就需要添加多少個(gè)到列表中喂分,將它變成一個(gè)對象就好處理多了。還有一個(gè)是information的ITEM机蔗,就直接在其對應(yīng)的函數(shù)里定義了蒲祈。
#構(gòu)造URL
# /http://www.reibang.com/users/7b5031117851/timeline
url_information = "http://www.reibang.com/users/%s/timeline" % ID
url_fans = "http://www.reibang.com/users/%s/followers" % ID
url_follow = "http://www.reibang.com/users/%s/following" % ID
url_collection = "http://www.reibang.com/users/%s/subscriptions" % ID
url_like = "http://www.reibang.com/users/%s/liked_notes" % ID
yield Request(url=url_information, meta={"ID":ID}, callback=self.parse0) #爬用戶的個(gè)人信息
yield Request(url=url_collection, meta={"item": collectionitem, "result": collection}, callback=self.parse1) #用戶的關(guān)注專題
yield Request(url=url_fans, meta={"item": fansItems, "result": fans},callback=self.parse2) #用戶的粉絲,目的在于獲取ID
yield Request(url=url_follow, meta={"item":followsItem, "result": follows}, callback=self.parse3) #用戶的關(guān)注數(shù)萝嘁,目的一是為了獲取ID梆掸,二是為了獲取個(gè)人愛好
yield Request(url=url_like, meta={"item":likearticleitem, "result": likearticle}, callback=self.parse4) #用戶的愛好信息,目的是為了獲取偏好
定義好了這些ITEM牙言,只需要利用meta傳遞下去即可
解析
雖然一共需要爬取5個(gè)頁面的信息酸钦,但是其實(shí)就是兩類信息,一類是簡單的個(gè)人信息咱枉,一類是如具體的信息
information個(gè)人信息
def parse0(self, response):
#爬取個(gè)人的基本信息
informationItems = InformationItem()
selector = Selector(response)
informationItems["_id"] = response.meta["ID"]
try:
sexes = selector.xpath(u'//div[@class="title"]/i').extract()
sex = re.findall('ic-(.*?)">', sexes[0])
informationItems["sex"] = sex[0]
except:
informationItems["sex"] = "未注明"
try:
soup = BeautifulSoup(response.text, 'lxml')
intro = soup.find("div", {"class": "description"}).get_text()
informationItems["introduction"] = intro
except:
informationItems["introduction"] = "暫無簡介"
informationItems["nickname"] = selector.xpath(u'//div[@class="title"]/a/text()').extract()[0]
informationItems["num_follows"] = selector.xpath('//div[@class="info"]/ul/li[1]/div/a/p/text()').extract()[0]
informationItems["num_fans"] = selector.xpath('//div[@class="info"]/ul/li[2]/div/a/p/text()').extract()[0]
informationItems["num_articles"] = selector.xpath('//div[@class="info"]/ul/li[3]/div/a/p/text()').extract()[0]
informationItems["num_words"] = selector.xpath('//div[@class="info"]/ul/li[4]/div/p/text()').extract()[0]
informationItems["num_likes"] = selector.xpath('//div[@class="info"]/ul/li[5]/div/p/text()').extract()[0]
yield informationItems
這一類信息直接處理就可以了
具體的信息
def parse2(self, response):
items = response.meta["item"]
#這樣做的目的只是為了到時(shí)能夠返回item,但是實(shí)際上我們所操作的是result這個(gè)列表
#爬取粉絲數(shù)
selector = Selector(response)
total = selector.xpath('//a[@class="name"]').extract()
#去重卑硫,添加ID
if len(total) != 0:
for fan in total:
fan = fan.encode("utf-8")
ID = re.findall('/u/(.*?)">',fan,re.S)
a = ID[0]
if a not in self.finish_ID:
self.scrawl_ID.add(a)
nickname = re.findall('>(.*?)<',fan,re.S)
response.meta["result"].append(nickname[0])
#獲取更多的ID,翻頁
num = selector.xpath('//li[@class="active"]/a/text()').extract()
pagenum = re.findall('\d+', num[0], re.S)
n = pagenum[0]
if int(n) > 9:
page = int(n)//9
pages = page + 2
if pages < 101:
for one in range(1, pages):
baseurl = "http://www.reibang.com/users/%s/followers"%items["_id"]
#http://www.reibang.com/users/deeea9e09cbc/followers?page=4
next_url = baseurl + "?page=%s"%one
yield Request(url=next_url, meta={"item": items,
"result": response.meta["result"]}, callback=self.parse2)
else:
for one in range(1,101):
baseurl = "http://www.reibang.com/users/%s/followers" % items["_id"]
# http://www.reibang.com/users/deeea9e09cbc/followers?page=4
next_url = baseurl + "?page=%s" % one
yield Request(url=next_url, meta={"item": items,
"result": response.meta["result"]}, callback=self.parse2)
else:
yield items
#還有一種情況就是已經(jīng)爬完了第一頁,但是沒有下一頁了response.meta["ID"]
else:
e = "沒有粉絲"
response.meta["result"].append(e)
yield items
個(gè)人的具體信息處理方式有點(diǎn)不同蚕断,因?yàn)槿绻P(guān)注了很多信息欢伏,就會(huì)產(chǎn)生分頁,我們還需要通過一個(gè)回調(diào)來分頁亿乳,還有一點(diǎn)需要注意的是這個(gè)信息量的多少颜懊,當(dāng)一個(gè)用戶有幾萬用戶的時(shí)候,在這個(gè)函數(shù)里就請求很長的時(shí)候风皿,而造成異步阻塞河爹,同時(shí)會(huì)導(dǎo)致粉絲ID的不連續(xù)性,會(huì)漏了很多ID桐款,這也是為什么在上面所給出的統(tǒng)計(jì)圖里每個(gè)ITEM的數(shù)據(jù)量有挺大差別的原因咸这,為了解決這個(gè)問題,可以在分頁的時(shí)候加一個(gè)判斷的條件魔眨,比如說當(dāng)遇到有很多粉絲的大V媳维,那就只爬取其前100頁粉絲酿雪,解決這個(gè)問題的還有一個(gè)辦法,就是增大線程數(shù)侄刽,也就是在start 列表中多添加一些ID指黎,還有一點(diǎn)要注意的是,在start列表中最少都要添加16個(gè)ID州丹,也就是開16個(gè)線程醋安,因?yàn)閟crapy默認(rèn)的線程數(shù)就是16個(gè)。如何使這些ITEM的數(shù)據(jù)數(shù)量相同墓毒,更多的原因還是取決于網(wǎng)速和電腦配置吓揪,因?yàn)檫@次是大規(guī)模抓取,所以就沒太在意這些小細(xì)節(jié)了所计。
數(shù)據(jù)存儲(chǔ)
本來是打算存到mysql里的柠辞,但是感覺要操作的步驟太多了,就用了mongodb主胧,存數(shù)據(jù)簡直是不能再方便叭首,連儲(chǔ)存字段都不用定義,在代碼中踪栋,只需要對item進(jìn)行一個(gè)判斷就可以了放棒。
class MongoDBPipleline(object):
def __init__(self):
clinet = pymongo.MongoClient("localhost", 27017)
db = clinet["jianshu"]
self.Information = db["Information"]
self.FollowColletion = db["FollowColletion"]
self.LikeArticle = db["LikeArticle"]
self.Fans = db["Fans"]
self.FollowList = db["FollowList"]
def process_item(self, item, spider):
""" 判斷item的類型,并作相應(yīng)的處理己英,再入數(shù)據(jù)庫 """
if isinstance(item, InformationItem):
try:
self.Information.insert(dict(item))
except Exception:
pass
elif isinstance(item, FanListItem):
fansItems = dict(item)
try:
self.Fans.insert(fansItems)
except Exception:
pass
elif isinstance(item, FollowColletionItem):
try:
self.FollowColletion.insert(dict(item))
except Exception:
pass
elif isinstance(item, LikeArticleItem):
try:
self.LikeArticle.insert(dict(item))
except Exception:
pass
elif isinstance(item, FollowListItem):
try:
self.FollowList.insert(dict(item))
except Exception:
pass
return item
Setting配置
ROBOTSTXT_OBEY = False
COOKIES_ENABLED = True
DOWNLOADER_MIDDLEWARES = {
'jianshu.middlewares.UserAgentMiddleware': 401,
}
ITEM_PIPELINES = {
'jianshu.pipelines.MongoDBPipleline': 300,
}
DOWNLOAD_TIMEOUT = 10
RETRY_ENABLED = False
LOG_LEVEL = 'INFO'
#這個(gè)的作用是顯示簡略的爬取過程信息
后續(xù)
在這里源碼就不放出了间螟,需要的話可以給我留言
在這個(gè)小項(xiàng)目中,還有很多細(xì)節(jié)方面的沒有去考慮损肛,以及一些后續(xù)內(nèi)容厢破,比如說scrapy部署(過段時(shí)間再來寫篇爬蟲的部署與過程可視化),還有API也是簡單的寫了一下(也在后面再寫文章吧),在編寫API的時(shí)候越來越感覺這個(gè)程序里有很多的不足治拿。不知不覺摩泪,這已經(jīng)是爬蟲小分隊(duì)第一期里的最后一次作業(yè)了,時(shí)間過得真得好快劫谅,再一次對爬蟲小分隊(duì)的老師們表示感謝见坑,再插入一個(gè)硬廣,如果你有足夠的興趣想要入門python捏检,入門爬蟲荞驴,但是又苦于沒有可交流的小伙伴與可以請教的老師,小分隊(duì)也許適合你贯城,如果你已經(jīng)入門了python熊楼,但是沒有一個(gè)合適的清晰的學(xué)習(xí)路線,小分隊(duì)也許適合你能犯。
報(bào)名鏈接