Scrapy抓取新浪微博用户保存到MongoDB

前一段时间我发现这个版本https://m.weibo.cn的新浪微博,然后就花了些时间找到一些Ajax请求和链接规律。

代码我已经放在我的GitHub上了。

废话不多说,进入正题吧。

URL分析

首先:

1
https://m.weibo.cn/u/1713926427

这就是用户个人主页了,后面的数字就是用户ID。

然后,下面这个链接就有用户的一些信息:

1
https://m.weibo.cn/api/container/getIndex?containerid=1076031713926427

可以看到后面的数字是107603加上用户ID,107603我还不明白是什么意思,但是它可以直接用于其他用户。

下面是用户的关注人:

1
https://m.weibo.cn/api/container/getIndex?containerid=231051_-_followers_-_1713926427

前面的231051我也不知道是什么意思,不过也可以用于其他用户,后面的数字是用户ID。然后翻页的话是这样的:

1
https://m.weibo.cn/api/container/getIndex?containerid=231051_-_followers_-_1713926427&page=2

下面是用户的粉丝:

1
https://m.weibo.cn/api/container/getIndex?containerid=231051_-_fans_-_1713926427

粉丝的翻页则和关注人不同,是这样的:

1
https://m.weibo.cn/api/container/getIndex?containerid=231051_-_fans_-_1713926427&since_id=2

知道了这些,就可以开始编写代码了。

爬虫实现

实现方法是先选取一个用户获取他的关注人与粉丝,然后再分别获取他关注人和粉丝的关注人与粉丝,以此类推。

首先,先编写items.py,定义好要存取的内容我选择了以下信息:

1
2
3
4
5
6
7
8
id = Field()  # 用户ID
screen_name = Field() # 用户名
profile_image_url = Field() # 头像地址
profile_url = Field() # 个人主页,地址是包括fid等东西的,其实有ID就能构造个人主页
follow_count = Field() # 关注数
followers_count = Field() # 粉丝数
gender = Field() # 性别
description = Field() # 简介

然后,编写爬虫吧,首先,需要一个初始用户,爬取从他开始:

1
start_user = 'XXXXXXX'

然后,构造一下各种URL:

1
2
3
4
5
6
# 用户信息
user_info_url = 'https://m.weibo.cn/api/container/getIndex?containerid=107603{user_id}'
# 关注人列表
follows_url = 'https://m.weibo.cn/api/container/getIndex?containerid=231051_-_followers_-_{user_id}&page={page}'
# 粉丝列表
fans_url = 'https://m.weibo.cn/api/container/getIndex?containerid=231051_-_fans_-_{user_id}&since_id={since_id}'

之后重写一下start_requests()方法,这是初始的调用方法:

1
2
3
4
5
6
7
def start_requests(self):
# 获取初始用户信息
yield Request(self.user_info_url.format(user_id=self.start_user), callback=self.parse_user_info)
# 获取初始用户关注列表
yield Request(self.follows_url.format(user_id=self.start_user, page=1), callback=self.parse_follows)
# 获取初始用户粉丝列表
yield Request(self.fans_url.format(user_id=self.start_user, since_id=1), callback=self.parse_fans)

接下来就把上面要用到的parse_user_info()parse_follows()parse_fans()方法写一下。

获取用户信息的parse_user_info()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def parse_user_info(self, response):
cards = json.loads(response.text).get('data').get('cards')
if cards:
result = cards[0].get('mblog').get('user')
if result:
item = WeiboUsersItem()
for field in item.fields:
if field in result.keys():
item[field] = result.get(field)
yield item

# 对每个用户再获取他们的关注列表
yield Request(self.follows_url.format(user_id=item['id'], page=1), callback=self.parse_follows)
# 对每个用户再获取他们的粉丝列表
yield Request(self.fans_url.format(user_id=item['id'], since_id=1), callback=self.parse_follows)

通过解析response返回的json数据,可以得出我们需要获取的信息的位置,那串json实在是太长了,我就不贴出来了…

然后对每个用户,都要通过他们的ID在调用获取关注人和粉丝列表的方法。

接下来是获取关注列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def parse_follows(self, response):
cards = json.loads(response.text).get('data').get('cards')
if cards: # 判断是否获取完全部关注人
for card in cards:
if card.get('title') and '全部关注' in card.get('title'):
result = card.get('card_group')
for item in result:
# 调用parse_user_info()获取用户信息
if item.get('user').get('id'):
yield Request(self.user_info_url.format(user_id=item.get('user').get('id')), callback=self.parse_user_info)
# 获取下一页关注的人
page = re.search('since_id=(\d+)', response.url)
next_page = int(page.group(1)) + 1 if page else 2
yield Request(self.follows_url.format(user_id=self.start_user, page=str(next_page)), callback=self.parse_follows)

如果返回的json里还有用户,那么就继续获取这些用户的信息。然后再构造下一页的URL继续提取用户信息。

获取粉丝的方法也差不多:

1
2
3
4
5
6
7
8
9
10
11
def parse_fans(self, response):
cards = json.loads(response.text).get('data').get('cards')
if cards: # 判断是否获取完全部粉丝
result = cards[0].get('card_group')
for item in result:
# 调用parse_user_info()获取用户信息
yield Request(self.user_info_url.format(user_id=item.get('user').get('id')), callback=self.parse_user_info)
# 获取下一页粉丝
since_id = re.search('since_id=(\d+)', response.url)
next_since_id = int(since_id.group(1)) + 1 if since_id else 2
yield Request(self.fans_url.format(user_id=self.start_user, since_id=next_since_id), callback=self.parse_fans)

存储

要把结果存储到MongoDB,首先在pipelines.py写一个MongoPipeline(),这个方法Scrapy的官方文档里也有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MongoPipeline(object):
collection_name = 'scrapy_items'

def __init__(self, mongo_uri, mongo_db):
self.mongo_uri = mongo_uri
self.mongo_db = mongo_db

@classmethod
def from_crawler(cls, crawler):
return cls(
mongo_uri=crawler.settings.get('MONGO_URI'),
mongo_db=crawler.settings.get('MONGO_DATABASE')
)

def open_spider(self, spider):
self.client = pymongo.MongoClient(self.mongo_uri)
self.db = self.client[self.mongo_db]

def close_spider(self, spider):
self.client.close()

def process_item(self, item, spider):
self.db['user'].update({'id': item['id']}, {'$set': item}, True)
return item

这里的update()起到去重的效果,根据用户ID判断是否已经有这个用户的数据。

写好之后要在settings.py加上MongoDB的配置和ITEM_PIPELINES

1
2
3
4
5
6
MONGO_URI = 'localhost'
MONGO_DATABASE = 'weibo'

ITEM_PIPELINES = {
'weibo_users.pipelines.MongoPipeline': 300,
}

之后scrapy crawl weibo运行,Scrapy就会帮我们把数据存到MongoDB了。

问题

虽然爬虫写好了,也能运行,但是一段时间之后就无法爬取数据了,后面的URL全部都报403,类似:

1
DEBUG: Crawled (403)HTTP status code is not handled or not allowed

可能是由于访问量太大被ban了,这里我的解决是禁用了cookies,然后设置了延迟:

1
2
COOKIES_ENABLED=False
DOWNLOAD_DELAY=1

另外,还写了一个随机更换User-Agent的中间件,middlewares.py添加:

1
2
3
4
5
6
7
8
9
10
11
class RandomUserAgent(object):

def __init__(self, agents):
self.agents = agents

@classmethod
def from_crawler(cls, crawler):
return cls(crawler.settings.getlist('USER_AGENTS'))

def process_request(self, request, spider):
request.headers.setdefault('User-Agent', random.choice(self.agents))

settings.py的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
DOWNLOADER_MIDDLEWARES = {
'weibo_users.middlewares.RandomUserAgent': 1,
}

USER_AGENTS = [
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.1 "
"(KHTML, like Gecko) Chrome/22.0.1207.1 Safari/537.1",
"Mozilla/5.0 (X11; CrOS i686 2268.111.0) AppleWebKit/536.11 "
"(KHTML, like Gecko) Chrome/20.0.1132.57 Safari/536.11",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.6 "
"(KHTML, like Gecko) Chrome/20.0.1092.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.6 "
"(KHTML, like Gecko) Chrome/20.0.1090.0 Safari/536.6",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/537.1 "
"(KHTML, like Gecko) Chrome/19.77.34.5 Safari/537.1",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.9 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.0) AppleWebKit/536.5 "
"(KHTML, like Gecko) Chrome/19.0.1084.36 Safari/536.5",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 5.1) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_0) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1063.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1062.0 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.1) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.1 Safari/536.3",
"Mozilla/5.0 (Windows NT 6.2) AppleWebKit/536.3 "
"(KHTML, like Gecko) Chrome/19.0.1061.0 Safari/536.3",
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.24 "
"(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24",
"Mozilla/5.0 (Windows NT 6.2; WOW64) AppleWebKit/535.24 "
"(KHTML, like Gecko) Chrome/19.0.1055.1 Safari/535.24"
]

然后403的错误就解决了。

结语

我这里跑了一段时间得到2500+个用户信息是没有报错或其他问题的,然后我就没有继续跑了……

具体的代码在我的GitHub上了,感兴趣可以看看。