2017/5/19 爬取簡書百萬數(shù)據(jù)

小分隊(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)然后期還可以改成分布式爬蟲菱皆。

看圖
image.png
image.png

作業(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作為索引

image.png

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)名鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鲫骗,一起剝皮案震驚了整個(gè)濱河市犬耻,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌执泰,老刑警劉巖枕磁,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異术吝,居然都是意外死亡计济,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門顿苇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人税弃,你說我怎么就攤上這事纪岁。” “怎么了则果?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵幔翰,是天一觀的道長。 經(jīng)常有香客問我西壮,道長遗增,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任款青,我火速辦了婚禮做修,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘抡草。我一直安慰自己饰及,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布康震。 她就那樣靜靜地躺著燎含,像睡著了一般。 火紅的嫁衣襯著肌膚如雪腿短。 梳的紋絲不亂的頭發(fā)上屏箍,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天,我揣著相機(jī)與錄音橘忱,去河邊找鬼赴魁。 笑死,一個(gè)胖子當(dāng)著我的面吹牛钝诚,可吹牛的內(nèi)容都是我干的尚粘。 我是一名探鬼主播,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼敲长,長吁一口氣:“原來是場噩夢啊……” “哼郎嫁!你這毒婦竟也來了秉继?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤泽铛,失蹤者是張志新(化名)和其女友劉穎尚辑,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體盔腔,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡杠茬,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了弛随。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瓢喉。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖舀透,靈堂內(nèi)的尸體忽然破棺而出栓票,到底是詐尸還是另有隱情,我是刑警寧澤愕够,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布走贪,位于F島的核電站,受9級特大地震影響惑芭,放射性物質(zhì)發(fā)生泄漏坠狡。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一遂跟、第九天 我趴在偏房一處隱蔽的房頂上張望逃沿。 院中可真熱鬧,春花似錦幻锁、人聲如沸感挥。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽触幼。三九已至,卻和暖如春究飞,著一層夾襖步出監(jiān)牢的瞬間置谦,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工亿傅, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留媒峡,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓葵擎,卻偏偏與公主長得像谅阿,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354

推薦閱讀更多精彩內(nèi)容