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

小分隊第一期的最后一次作業(yè),本次作業(yè)的內(nèi)容是爬取簡書百萬以上的用戶,不過我只爬了60多萬用戶,因為之前沒有設(shè)置代理IP,同時請求過快的原因而被封了,然后就沒再繼續(xù)爬了。本次選用的工具是scrapy+mongodb,爬取速度為每分鐘1500頁,獲取item的速度為500個每分鐘,峰值在1000個每分鐘,在運行了一段時間后,發(fā)現(xiàn)如果再修改一下, 比如說再增加線程,同時通過設(shè)置爬取合適的頁數(shù)來減少請求的阻塞來增加異步的效率還可以更快,不過最終的速度還是取決于網(wǎng)速和電腦配置,當然后期還可以改成分布式爬蟲。

看圖
image.png
image.png

作業(yè)的思路

一開始,想了兩種方案

方案一

通過專題數(shù)的關(guān)注人數(shù)這個入口來獲取用戶的ID,然后再通過這個ID跳轉(zhuǎn)到用戶的信息頁,但是在經(jīng)過嘗試后發(fā)現(xiàn),一方面是重復性不是很高,另一方面還是請求阻塞的問題,這一點在另一種方案里也會體現(xiàn)。

方案二

通過用戶的粉絲來定位用戶,也就是解析粉絲的粉絲的ID,但是這樣也會出現(xiàn)一個問題,就是會遇到一連串的沒有關(guān)注用戶的ID,所以可以選擇兩個口,一個是用戶的粉絲,另一個是用戶的關(guān)注,解析都是一樣的,再說說請求阻塞的問題,因為Scrapy是一個異步框架,如果是在一個回調(diào)函數(shù)里請求的時間太長的話,便會極大的影響異步的效率。比較好的方法是對那些有很多粉絲數(shù)的用戶的粉絲進行分割,比如說以50頁為一次回調(diào)到start_requests函數(shù),但是在這里我進行了簡化處理,就是只選取前100頁的粉絲進行回調(diào)

代碼的分析

items.py

在本次作業(yè)中,我想爬取的信息定義了五個ITEM,一方面是想要獲取個人的基本信息,另一方面還想要獲取一個用戶的興趣偏好,個人的基本信息包括如粉絲數(shù)呀,性別之類的,興趣則是可以從其所關(guān)注的專欄,喜歡的文章,關(guān)注的人來看

import scrapy


class InformationItem(scrapy.Item):
    #個人信息
    _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()

因為這次所選用數(shù)據(jù)庫是mongodb,所以以_id作為索引

image.png

URL的去重

URL去重是實現(xiàn)大規(guī)模爬取的一個關(guān)鍵點,當時所想到的方案主要有兩個方向,一個是利用內(nèi)存,另一個是利用硬盤空間,內(nèi)存又分為兩種,一種是利用redis這個內(nèi)存數(shù)據(jù)庫,另一個利用List,當然前一種更合適,但是也更復雜,所以就選用了后一種,另一種利用硬盤空間的辦法就是在每次爬取的ID存入數(shù)據(jù)庫,并作判斷是否重復,然后再從數(shù)據(jù)庫中選取ID進行下一步的爬取,但是考慮到當數(shù)據(jù)量大的時候,進行IO操作會越來越慢,所以就沒有選這種,但是這種也有優(yōu)勢,就是可以實現(xiàn)簡單的斷點續(xù)爬。
在代碼中,定義了一個列表,并將這個列表變成一個集合,保證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()
#當已經(jīng)爬取過了的ID就將它放入另一個集合里
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的時候只需要判斷一下其是否在已經(jīng)爬取過的集合里

回調(diào)

由于想要獲取的是一個用戶的很多信息,并存在不同的表,那么問題來了,一個用戶如果有很多粉絲的話,那么其粉絲頁就要分頁了,如何將這個ITEM給傳遞下去,并保證ID是一一對應的呢?所以為了解決上面的問題,就需要在start_requests這個函數(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ù)是一致的,因為每個用戶的粉絲數(shù)是不一樣的,所以為了統(tǒng)一處理,先定義一個列表來接受用戶的粉絲信息之類的,有多少個粉絲,就需要添加多少個到列表中,將它變成一個對象就好處理多了。還有一個是information的ITEM,就直接在其對應的函數(shù)里定義了。

            #構(gòu)造URL
            # /http://www.itdecent.cn/users/7b5031117851/timeline
            url_information = "http://www.itdecent.cn/users/%s/timeline" % ID
            url_fans = "http://www.itdecent.cn/users/%s/followers" % ID
            url_follow = "http://www.itdecent.cn/users/%s/following" % ID
            url_collection = "http://www.itdecent.cn/users/%s/subscriptions" % ID
            url_like = "http://www.itdecent.cn/users/%s/liked_notes" % ID

            yield Request(url=url_information, meta={"ID":ID}, callback=self.parse0) #爬用戶的個人信息
            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,二是為了獲取個人愛好
            yield Request(url=url_like, meta={"item":likearticleitem, "result": likearticle}, callback=self.parse4) #用戶的愛好信息,目的是為了獲取偏好

定義好了這些ITEM,只需要利用meta傳遞下去即可

解析

雖然一共需要爬取5個頁面的信息,但是其實就是兩類信息,一類是簡單的個人信息,一類是如具體的信息

information個人信息

def parse0(self, response):
        #爬取個人的基本信息
        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"]
        #這樣做的目的只是為了到時能夠返回item,但是實際上我們所操作的是result這個列表
        #爬取粉絲數(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.itdecent.cn/users/%s/followers"%items["_id"]
                        #http://www.itdecent.cn/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.itdecent.cn/users/%s/followers" % items["_id"]
                        # http://www.itdecent.cn/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

個人的具體信息處理方式有點不同,因為如果他關(guān)注了很多信息,就會產(chǎn)生分頁,我們還需要通過一個回調(diào)來分頁,還有一點需要注意的是這個信息量的多少,當一個用戶有幾萬用戶的時候,在這個函數(shù)里就請求很長的時候,而造成異步阻塞,同時會導致粉絲ID的不連續(xù)性,會漏了很多ID,這也是為什么在上面所給出的統(tǒng)計圖里每個ITEM的數(shù)據(jù)量有挺大差別的原因,為了解決這個問題,可以在分頁的時候加一個判斷的條件,比如說當遇到有很多粉絲的大V,那就只爬取其前100頁粉絲,解決這個問題的還有一個辦法,就是增大線程數(shù),也就是在start 列表中多添加一些ID,還有一點要注意的是,在start列表中最少都要添加16個ID,也就是開16個線程,因為scrapy默認的線程數(shù)就是16個。如何使這些ITEM的數(shù)據(jù)數(shù)量相同,更多的原因還是取決于網(wǎng)速和電腦配置,因為這次是大規(guī)模抓取,所以就沒太在意這些小細節(jié)了。

數(shù)據(jù)存儲

本來是打算存到mysql里的,但是感覺要操作的步驟太多了,就用了mongodb,存數(shù)據(jù)簡直是不能再方便,連儲存字段都不用定義,在代碼中,只需要對item進行一個判斷就可以了。

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的類型,并作相應的處理,再入數(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'
#這個的作用是顯示簡略的爬取過程信息

后續(xù)

在這里源碼就不放出了,需要的話可以給我留言
在這個小項目中,還有很多細節(jié)方面的沒有去考慮,以及一些后續(xù)內(nèi)容,比如說scrapy部署(過段時間再來寫篇爬蟲的部署與過程可視化),還有API也是簡單的寫了一下(也在后面再寫文章吧),在編寫API的時候越來越感覺這個程序里有很多的不足。不知不覺,這已經(jīng)是爬蟲小分隊第一期里的最后一次作業(yè)了,時間過得真得好快,再一次對爬蟲小分隊的老師們表示感謝,再插入一個硬廣,如果你有足夠的興趣想要入門python,入門爬蟲,但是又苦于沒有可交流的小伙伴與可以請教的老師,小分隊也許適合你,如果你已經(jīng)入門了python,但是沒有一個合適的清晰的學習路線,小分隊也許適合你。
報名鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容