详解简书用户信息采集——crawlspider&itemloader
爬取简书全站用户信息其实并不是特别容易,其中有个小坎就是A、B两用户互相关注,爬取时很容易造成死循环,这样的死循环会越爬越多越爬越多,像雪球一样越滚越大,造成爬取效率聚减,群内交流有说先入库再去重其实是一样的,当你入库去重完毕后会发现入库30W条,去重后只有5W条了,这都是由互相关注所造成的,下面给出策略图、爬虫代码、代码注释,仅供参考。(翻到最后有惊喜)
策略图
策略图
一、策略讲解
1、策略入口
1、策略入口
红圈位置是策略图入口,个人认为这些用户都是大咖所以从这入手是再好不过的,里面有好几页AJX的用户推荐名单,需要拼接和循环去获取用户KEY_ID,将用户关注页面分为2层,上层是用户基本信息,下层是用户关注信息,新建一个空集合,将爬取过基本信息用户的KEY_ID存入集合,然后将下层关注信息遍历取出其他用户KEY_ID,然后让去集合去判断是否爬去过该KEY_ID,最后构筑这些用户信息的关注页面的链接做循环操作。
2、左击关注
点击查看全部之后推荐用户的界面,点击红圈
2、左击关注
3、取得用户URL
红圈中的字符串是 图2 用户的KEY_ID,爬取该用户基本信息,将该KEY_ID放入集合(敲黑板!!!注意这里最好放集合,尽量不要用列表,用列表放入列表中的KEY_ID也都是重复的,不会去重,用集合可以节约开销!),然后看url后面following是关注页面的后缀,所以之后只需要将KEY_ID + following就可以构筑出关注页面
3、取得用户URL
4、取得关注页面
这里需要注意红圈的地方,如果关注用户超过10个,有ajx动态加载的,需要去重新构筑URL遍历用户获取关注的用户KEY_ID
URL是这样的 http://www.jianshu.com/users/7e54016a5a06/following?page=1 自己去重构,不会看下面代码,获取完用户KEY_ID然后用这些KEY_ID去集合中去重,如果不在集合里则拿出来构筑following关注页面,就这样再回到第一步。
4、取得关注页面
二、代码讲解
Scrapy / python3.5
spider
# -*- coding: utf-8 -*- import scrapy import re from urllib.parse import urljoin from scrapy.http import Request from article.items import JianShuItem, JianShuItemLoader class JianshuSpider(scrapy.Spider): name = "jianshu" allowed_domains = ["www.jianshu.com"] url_top_half = "http://www.jianshu.com" start_urls = [] used_id = set() # 设置一个空集合存爬过的用户 for pg in range(1, 11): # 第一步将推荐作者的链接放入start_url start_users_url_join = "http://www.jianshu.com/recommendations/users?page=" + str(pg) start_urls.append(start_users_url_join) def parse(self, response): start_user_list = response.css("div.wrap a.avatar::attr(href)").extract() # 获取推荐作者的href if start_user_list: # 起始页面判断,循环完start_url就没用了 # 判断推荐作者href是否为空,空则爬完推荐作者,不进入if判断 for k_1 in start_user_list: # 取得href格式为"/users/"+key url_second_half1 = k_1 + "/following" # k_1是列表循环出来未经清洗的字符串格式为"/users/"+key 连接上"/following" user_following_url1 = urljoin(self.url_top_half, url_second_half1) yield Request(url=user_following_url1, callback=self.parse_kernel) # 核心解析函数 def parse_kernel(self, response): # 既然解析item 又解析关注用户 reg_key = re.compile(r"http://www.jianshu.com/users/(.*)/following") key_2 = reg_key.findall(str(response.url))[0] # 获取key if key_2 not in self.used_id: # 判断key是否使用过,阻断互相关注循环 item_loader = JianShuItemLoader(item=JianShuItem(), response=response) item_loader.add_css("key_id", "div.main-top a.name::attr(href)") item_loader.add_css("user_name", "div.main-top a.name::text") item_loader.add_css("contracted", ".main-top span.author-tag::text") item_loader.add_xpath("follow", ".//div[@class ='info']/ul/li[1]//p/text()") item_loader.add_xpath("fans", ".//div[@class ='info']/ul/li[2]//p/text()") item_loader.add_xpath("article", ".//div[@class ='info']/ul/li[3]//p/text()") item_loader.add_xpath("words_count", ".//div[@class ='info']/ul/li[4]//p/text()") item_loader.add_xpath("get_likes", ".//div[@class ='info']/ul/li[5]//p/text()") jianshu_item_loader = item_loader.load_item() self.used_id.add(key_2) # 将用过的key,放入集合 yield jianshu_item_loader follow_num = response.xpath(".//div[@class ='main-top']//li[1]//p/text()").extract()[0] # 获取该用户关注人数 follow_pg = round(int(follow_num)/9) # AJX每个页面有9个关注 if follow_pg != 0: # 关注数不为0的进入if判断 for f_pg in range(1, follow_pg+1): # 获取该用户关注人数的页 user_following_url_pg = response.url + "?page=" + str(f_pg) yield Request(url=user_following_url_pg, callback=self.parse_following) # 将每一页的用户关注人的url回调给parse_following def parse_following(self, response): following_list = response.css("ul.user-list a.avatar::attr(href)").extract() # 解析出被关注用户的key for k_3 in following_list: key_3 = k_3.replace("/u/", "") # 清洗出key url_second_half3 = "/users/{}/following".format(key_3) user_following_url3 = urljoin(self.url_top_half, url_second_half3) # 直接拼接following url,返回给parse_kernel,进入循环 yield Request(url=user_following_url3, callback=self.parse_kernel)
item
# -*- coding: utf-8 -*- import re import scrapy from scrapy.loader import ItemLoader from scrapy.loader.processors import MapCompose, TakeFirst, Join from w3lib.html import remove_tags from article.settings import SQL_DATETIME_FORMAT, SQL_DATE_FORMAT import datetime # 以下为简书用户数据清洗函数 def key_id_filter(value): return value.replace("/u/", "") def contracted_filter(value): if value == ' 签约作者': return value.strip() elif value == "": return "未签约" class JianShuItemLoader(ItemLoader): default_output_processor = TakeFirst() # itemloader提取默认为list,所以这里需要重筑这个类的默认值 class JianShuItem(scrapy.Item): key_id = scrapy.Field( input_processor=MapCompose(key_id_filter) ) user_name = scrapy.Field() contracted = scrapy.Field( input_processor=MapCompose(contracted_filter) ) follow = scrapy.Field() fans = scrapy.Field() article = scrapy.Field() words_count = scrapy.Field() get_likes = scrapy.Field()
pipeline
# 常规写法拿去改下参数就可以用了 class JianShuMongodbPipeline(object): def __init__(self): client = pymongo.MongoClient( host="localhost", port=27017, ) db = client["jianshu"] self.coll = db["user_info"] def process_item(self, item, spider): self.coll.insert(dict(item)) return item
以上是个人见解,如有错误或更优解欢迎交流~
原文地址:https://www.jianshu.com/p/0c32e0c62aaa