小分队第一期的最后一次作业,本次作业的内容是爬取简书百万以上的用户,不过我只爬了60多万用户,因为之前没有设置代理IP,同时请求过快的原因而被封了,然后就没再继续爬了。本次选用的工具是
scrapy
+mongodb
,爬取速度为每分钟1500页,获取item的速度为500个每分钟,峰值在1000个每分钟,在运行了一段时间后,发现如果再修改一下, 比如说再增加线程,同时通过设置爬取合适的页数来减少请求的阻塞来增加异步的效率还可以更快,不过最终的速度还是取决于网速和电脑配置,当然后期还可以改成分布式爬虫。
作业的思路
一开始,想了两种方案
方案一
通过专题数的关注人数这个入口来获取用户的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作为索引
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,但是没有一个合适的清晰的学习路线,小分队也许适合你。
报名链接