爬取簡書全站用戶信息其實并不是特別容易,其中有個小坎就是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.lxweimin.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.lxweimin.com"]
url_top_half = "http://www.lxweimin.com"
start_urls = []
used_id = set()
# 設置一個空集合存爬過的用戶
for pg in range(1, 11):
# 第一步將推薦作者的鏈接放入start_url
start_users_url_join = "http://www.lxweimin.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.lxweimin.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
以上是個人見解,如有錯誤或更優解歡迎交流~