2017/5/19 爬取简书百万数据

小分队第一期的最后一次作业,本次作业的内容是爬取简书百万以上的用户,不过我只爬了60多万用户,因为之前没有设置代理IP,同时请求过快的原因而被封了,然后就没再继续爬了。本次选用的工具是scrapy+mongodb,爬取速度为每分钟1500页,获取item的速度为500个每分钟,峰值在1000个每分钟,在运行了一段时间后,发现如果再修改一下, 比如说再增加线程,同时通过设置爬取合适的页数来减少请求的阻塞来增加异步的效率还可以更快,不过最终的速度还是取决于网速和电脑配置,当然后期还可以改成分布式爬虫。

看图
image.png
image.png

作业的思路

一开始,想了两种方案

方案一

通过专题数的关注人数这个入口来获取用户的ID,然后再通过这个ID跳转到用户的信息页,但是在经过尝试后发现,一方面是重复性不是很高,另一方面还是请求阻塞的问题,这一点在另一种方案里也会体现。

方案二

通过用户的粉丝来定位用户,也就是解析粉丝的粉丝的ID,但是这样也会出现一个问题,就是会遇到一连串的没有关注用户的ID,所以可以选择两个口,一个是用户的粉丝,另一个是用户的关注,解析都是一样的,再说说请求阻塞的问题,因为Scrapy是一个异步框架,如果是在一个回调函数里请求的时间太长的话,便会极大的影响异步的效率。比较好的方法是对那些有很多粉丝数的用户的粉丝进行分割,比如说以50页为一次回调到start_requests函数,但是在这里我进行了简化处理,就是只选取前100页的粉丝进行回调

代码的分析

items.py

在本次作业中,我想爬取的信息定义了五个ITEM,一方面是想要获取个人的基本信息,另一方面还想要获取一个用户的兴趣偏好,个人的基本信息包括如粉丝数呀,性别之类的,兴趣则是可以从其所关注的专栏,喜欢的文章,关注的人来看

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):
    #关注的专题
    _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()

因为这次所选用数据库是mongodb,所以以_id作为索引

image.png

URL的去重

URL去重是实现大规模爬取的一个关键点,当时所想到的方案主要有两个方向,一个是利用内存,另一个是利用硬盘空间,内存又分为两种,一种是利用redis这个内存数据库,另一个利用List,当然前一种更合适,但是也更复杂,所以就选用了后一种,另一种利用硬盘空间的办法就是在每次爬取的ID存入数据库,并作判断是否重复,然后再从数据库中选取ID进行下一步的爬取,但是考虑到当数据量大的时候,进行IO操作会越来越慢,所以就没有选这种,但是这种也有优势,就是可以实现简单的断点续爬。
在代码中,定义了一个列表,并将这个列表变成一个集合,保证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()
#当已经爬取过了的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的时候只需要判断一下其是否在已经爬取过的集合里

回调

由于想要获取的是一个用户的很多信息,并存在不同的表,那么问题来了,一个用户如果有很多粉丝的话,那么其粉丝页就要分页了,如何将这个ITEM给传递下去,并保证ID是一一对应的呢?所以为了解决上面的问题,就需要在start_requests这个函数里先定义好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与数据是一致的,因为每个用户的粉丝数是不一样的,所以为了统一处理,先定义一个列表来接受用户的粉丝信息之类的,有多少个粉丝,就需要添加多少个到列表中,将它变成一个对象就好处理多了。还有一个是information的ITEM,就直接在其对应的函数里定义了。

            #构造URL
            # ///www.greatytc.com/users/7b5031117851/timeline
            url_information = "//www.greatytc.com/users/%s/timeline" % ID
            url_fans = "//www.greatytc.com/users/%s/followers" % ID
            url_follow = "//www.greatytc.com/users/%s/following" % ID
            url_collection = "//www.greatytc.com/users/%s/subscriptions" % ID
            url_like = "//www.greatytc.com/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) #用户的关注专题
            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) #用户的关注数,目的一是为了获取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这个列表
        #爬取粉丝数
        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 = "//www.greatytc.com/users/%s/followers"%items["_id"]
                        #//www.greatytc.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 = "//www.greatytc.com/users/%s/followers" % items["_id"]
                        # //www.greatytc.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
        #还有一种情况就是已经爬完了第一页,但是没有下一页了response.meta["ID"]
        else:
            e = "没有粉丝"
            response.meta["result"].append(e)
            yield items

个人的具体信息处理方式有点不同,因为如果他关注了很多信息,就会产生分页,我们还需要通过一个回调来分页,还有一点需要注意的是这个信息量的多少,当一个用户有几万用户的时候,在这个函数里就请求很长的时候,而造成异步阻塞,同时会导致粉丝ID的不连续性,会漏了很多ID,这也是为什么在上面所给出的统计图里每个ITEM的数据量有挺大差别的原因,为了解决这个问题,可以在分页的时候加一个判断的条件,比如说当遇到有很多粉丝的大V,那就只爬取其前100页粉丝,解决这个问题的还有一个办法,就是增大线程数,也就是在start 列表中多添加一些ID,还有一点要注意的是,在start列表中最少都要添加16个ID,也就是开16个线程,因为scrapy默认的线程数就是16个。如何使这些ITEM的数据数量相同,更多的原因还是取决于网速和电脑配置,因为这次是大规模抓取,所以就没太在意这些小细节了。

数据存储

本来是打算存到mysql里的,但是感觉要操作的步骤太多了,就用了mongodb,存数据简直是不能再方便,连储存字段都不用定义,在代码中,只需要对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的类型,并作相应的处理,再入数据库 """
        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'
#这个的作用是显示简略的爬取过程信息

后续

在这里源码就不放出了,需要的话可以给我留言
在这个小项目中,还有很多细节方面的没有去考虑,以及一些后续内容,比如说scrapy部署(过段时间再来写篇爬虫的部署与过程可视化),还有API也是简单的写了一下(也在后面再写文章吧),在编写API的时候越来越感觉这个程序里有很多的不足。不知不觉,这已经是爬虫小分队第一期里的最后一次作业了,时间过得真得好快,再一次对爬虫小分队的老师们表示感谢,再插入一个硬广,如果你有足够的兴趣想要入门python,入门爬虫,但是又苦于没有可交流的小伙伴与可以请教的老师,小分队也许适合你,如果你已经入门了python,但是没有一个合适的清晰的学习路线,小分队也许适合你。
报名链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容