爬取简书全站用户信息其实并不是特别容易,其中有个小坎就是A、B两用户互相关注,爬取时很容易造成死循环,这样的死循环会越爬越多越爬越多,像雪球一样越滚越大,造成爬取效率聚减,群内交流有说先入库再去重其实是一样的,当你入库去重完毕后会发现入库30W条,去重后只有5W条了,这都是由互相关注所造成的,下面给出策略图、爬虫代码、代码注释,仅供参考。(翻到最后有惊喜)
策略图
一、策略讲解
1、策略入口
红圈位置是策略图入口,个人认为这些用户都是大咖所以从这入手是再好不过的,里面有好几页AJX的用户推荐名单,需要拼接和循环去获取用户KEY_ID,将用户关注页面分为2层,上层是用户基本信息,下层是用户关注信息,新建一个空集合,将爬取过基本信息用户的KEY_ID存入集合,然后将下层关注信息遍历取出其他用户KEY_ID,然后让去集合去判断是否爬去过该KEY_ID,最后构筑这些用户信息的关注页面的链接做循环操作。
2、左击关注
点击查看全部之后推荐用户的界面,点击红圈
3、取得用户URL
红圈中的字符串是 图2 用户的KEY_ID,爬取该用户基本信息,将该KEY_ID放入集合(敲黑板!!!注意这里最好放集合,尽量不要用列表,用列表放入列表中的KEY_ID也都是重复的,不会去重,用集合可以节约开销!),然后看url后面following是关注页面的后缀,所以之后只需要将KEY_ID + following就可以构筑出关注页面
4、取得关注页面
这里需要注意红圈的地方,如果关注用户超过10个,有ajx动态加载的,需要去重新构筑URL遍历用户获取关注的用户KEY_ID
URL是这样的 http://www.jianshu.com/users/7e54016a5a06/following?page=1 自己去重构,不会看下面代码,获取完用户KEY_ID然后用这些KEY_ID去集合中去重,如果不在集合里则拿出来构筑following关注页面,就这样再回到第一步。
二、代码讲解
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
以上是个人见解,如有错误或更优解欢迎交流~