目標
用爬蟲程序抓取目標用戶人民日報的微博文本,通過分析詞頻,生成直觀的詞雲圖。
編寫Python微博爬蟲
注意:微博的接口可能會發生變化,所以請不要盲目照抄,建議按照下述流程獨立分析。
數據來源
微博移動版網頁(點此跳轉)
內容簡潔,便於分析,因此選用移動版網頁作爲爬取對象。
微博列表請求分析
打開目標用戶的移動版微博主頁:人民日報
注意:此處需要退出微博登錄來保證請求內容的普適性。
F12
打開開發者工具,這裏使用的是谷歌瀏覽器。選中最上方的Network
標籤頁,刷新頁面來監測網絡連接請求。
通過分析preview和response兩個標籤頁的內容,可以確定獲取微博列表的鏈接請求爲:
https://m.weibo.cn/api/container/getIndex?uid=2803301701&t=0&luicode=10000011&lfid=100103type%3D1%26q%3D%E4%BA%BA%E6%B0%91%E6%97%A5%E6%8A%A5&type=uid&value=2803301701&containerid=1076032803301701
在開發者工具中查看該請求的頭部信息,下拉到最後查看請求參數:
分析到請求參數一共有七個:
- uid: 2803301701
- t: 0
- luicode: 10000011
- lfid: 100103type=1&q=人民日報
- type: uid
- value: 2803301701
- containerid: 1076032803301701
其中uid
和value
都是用來唯一標識用戶的,內容相同,lifd
是用來標識微博用戶名,containerid
用來標識不同範圍的微博,表示公開的所有微博,其他參數則都是默認無需變化。
去掉參數後的請求地址爲
https://m.weibo.cn/api/container/getIndex?
請求地址+特定參數即可訪問特定用戶的微博列表。
應答報文分析
通過開發者工具可以發現微博服務器迴應請求的是一個較爲複雜的json格式文件。
不要慌,一步步分析。
首先,通過request
庫的get方法,向上述分析出的url地址發送請求,獲得迴應的字符串文件,代碼如下:
import requests
import json
url = 'https://m.weibo.cn/api/container/getIndex?'
#headers信息防止觸發反爬蟲機制
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36'}
parameter = {
'uid': '2803301701',
't': '0',
'luicode': '10000011',
'lfid': '100103type=1&q=人民日報',
'type': 'uid',
'value': '2803301701',
'containerid': '1076032803301701',
'page_type': '60',
'page': '1'
}
respones = requests.get(url + urlencode(parameter), headers=headers)
#通過json庫的loads方法將返回的字符串轉換爲字典數據格式
data = json.loads(respones.text)
通過使用代碼循環遍歷字典中的鍵值對,對每個值輸出其類型,是字典則迭代遍歷,可以很快搞清楚該json文件的結構,並定位到我們所需用的信息。
該json文件最外層有兩個鍵,一個是ok,值爲0或1,代表查詢成功或失敗。
一個是data,值是一個字典,我們需要的信息在該字典的鍵cards
對應的值中,而cards對應的值則是一個列表。在這裏可以推測,因爲移動版微博的每條微博都是卡片樣式,所以cards中的每一個元素,對應主頁的一條微博。
獲取微博正文
通過訪問微博正文,可以發現url
格式爲:每條微博對應一個獨一無二的標識數字,而我們需要獲取這個標識來訪問每條微博。
cards
對應的列表中,每個元素都是一個字典,對應了一條微博的主要內容和各種信息,其中我們需要的標識數字,在mblog
鍵對應的值當中。mblog
對應的值又是一個字典,而我們需要的數字,則是該字典中的idstr
或mid
鍵值對
將獲取到的微博標識數字與url
請求部分結合,可以獲取到微博正文。
循環遍歷cards列表,即可獲取所有微博的正文
對應的代碼如下:
#接上述得到的data字典
if data['ok'] == 0:
print("爬取完成~")
break
if data['ok'] == 1:
for item in data['data']['cards']:
if 'mblog' not in item:
continue
blog = requests.get('https://m.weibo.cn/detail/' + item['mblog']['idstr'], headers=headers)
微博正文文本提取
獲取到微博正文頁面的html代碼後,需要從中提取出正文內容。
檢查html代碼,可以很容易定位到微博正文部分,特徵爲"text":
後面的內容,正文內容被雙引號包裹,且不存在換行字符,據此可以正則匹配出正文內容,正則表達式如下:
其中.代表任一不是換行符的字符,*表示匹配任意多次,即採用貪婪匹配模式,儘可能多的匹配字符,因爲正文對應的代碼中可能含有"。而正文結束後會有換行符,所以不擔心過度匹配。
匹配完成後,會發現正文中依然含有html代碼和首部的"text":多餘字符,需要再次匹配除去。採用re
庫的sub
函數,利用正則匹配表達式去除匹配到的字符串。
- 去除html代碼:html代碼都在<>中,非貪婪模式匹配尖括號,來防止正文被除去,這裏需要注意,並不是所有微博正文都會含有代碼,沒有時會拋出異常,需要進行異常處理
- 去除首部的
"text":
,\s用來匹配空格 : - 去除尾部的
“
:因爲引號也會出現在正文中,故這裏採用字符串轉列表,直接操作列表後再改字符串的方法去多多餘的引號
正文處理部分的代碼如下:
#接上部分的for循環
for item in data['data']['cards']:
if 'mblog' not in item:
continue
blog = requests.get('https://m.weibo.cn/detail/' + item['mblog']['idstr'], headers=headers)
res = re.search('"text":.*"', blog.text)
try:
blog_text = re.sub('<.*?>', '', res.group())
#正文純文字無代碼時捕獲異常,繼續後續流程
except AttributeError:
blog_text = res.group()
#
去除首尾用來定位匹配的字符串
final_text = list(re.sub(r'"text":\s"?', '', blog_text))
final_text[-1] = ''
final_text = ''.join(final_text)
獲取多頁微博
使用上述請求參數爬取微博時存在一個問題,即微博服務器只會返回10條微博數據。通過向下滑動頁面,使用開發者工具檢測請求,可以看到下拉頁面加載時,多出了page_type
和page
參數。
首先猜測page
參數代表頁碼,在請求參數字典中加入這兩個參數,遍歷完response報文中的cards列表後,對page
參數執行遞增操作,發現可以獲取到新的頁面微博列表。
實際運行一下程序,發現最多隻能抓取2000條左右的微博數據,之後返回的json數據中的ok鍵值對的值爲0,沒有微博列表數據。
猜測需要增大page_type
,對page_type
參數進行測試,當page
不變時,改變page_type
參數,對結果無影響,但是增大’page_type’之後,page
參數可以繼續獲取微博數據,故page_type
決定了你能獲取的最大微博數目。
所以當返回的json文件ok值爲0,對page_type
和page
值進行遞增,實現抓取大規模微博數據。
當目標微博數量在2000以下時,則只需要遞增page
值。
反爬蟲機制應對處理
實際測試中發現微博對爬蟲有着很多限制:
- 首先,如果無限制爬取網頁,很快會被微博服務器發現,會拒絕返回有效數據,提示無內容。所以需要在循環中加入睡眠時間,降低爬取速度。經過測試,1-4秒內隨機請求一次即可避坑。
- 其次,需要在get請求中設置headers參數中的user-agent,通過觀察開發者工具中的headers即可獲取該參數。否在會觸發418狀態碼錯誤,即被識別出爬蟲程序。
- 還有,有些情況下,刷新頁碼後會獲取到相同的微博,需要對比微博id,及時跳過
- 最後,對於每個ip地址,微博似乎限制了其每小時最多能獲取的微博數量,會直接拒絕訪問,拋出拒絕訪問錯誤。這種情況下只能更換ip地址或者等待一段時間。
爬蟲完整代碼
將捕獲的文本存入文本文檔,代碼如下:
import random
import re
import traceback
from urllib.parse import urlencode
import json
import time
import requests
# 自定義異常類,用於跳出多重循環
class Getoutofloop(Exception):
pass
# 獲取微博列表的url地址
url = 'https://m.weibo.cn/api/container/getIndex?'
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36'}
parameter = {
'uid': '2803301701',
't': '0',
'luicode': '10000011',
'lfid': '100103type=1&q=人民日報',
'type': 'uid',
'value': '2803301701',
'containerid': '1076032803301701',
'page_type': '03',
'page': '1'
}
txt = open("spider.txt", 'w', encoding='utf-8')
i = 1
j = 1
former = ''
while True:
# 人爲設定循環最大次數
if j >= 5000:
print("爬取結束,i = {},j = {}".format(i, j))
# 用於捕獲各種異常,保證讀取到的數據能正常存入文件
try:
respones = requests.get(url + urlencode(parameter), headers=headers)
time.sleep(random.uniform(1, 4))
# 運行狀態查看,可以省略
print('status:', respones.status_code)
if respones.status_code != 200:
print('爬蟲暴露了!')
print('status:', respones.status_code)
break
data = json.loads(respones.text)
# 返回ok值爲0,改變page_type值
if data['ok'] == 0:
parameter['page_type'] = str(int(parameter['page_type']) + 1)
parameter['page'] = str(int(parameter['page']) + 1)
i = i + 1
j = j + 1
print("i=", i)
print("j=", j)
continue
if data['ok'] == 1:
# 循環當前微博列表
for item in data['data']['cards']:
if 'mblog' not in item:
continue
# 打印微博id,可以忽略
print(item['mblog']['mid'])
if former == item['mblog']['mid']:
break
blog = requests.get('https://m.weibo.cn/detail/' + item['mblog']['mid'], headers=headers)
# 請求狀態碼不正常,則直接結束程序
if blog.status_code != 200:
print("爬蟲被限制了")
raise Getoutofloop()
res = re.search('"text":.*"', blog.text)
# 當前頁面沒找到文本的異常處理
try:
blog_text = re.sub('<.*?>', '', res.group())
except AttributeError:
print('找不到文本')
print(blog.text)
continue
# 正則表達式提前文字
final_text = list(re.sub(r'"text":"?', '', blog_text))
final_text[-1] = ''
final_text = ''.join(final_text)
txt.write(final_text + '\n')
former = item['mblog']['mid']
else:
print('failed')
break
j = j + 1
parameter['page'] = str(int(parameter['page']) + 1)
except BaseException as err:
print(type(err))
traceback.print_exc()
print('no response')
break
txt.close()
詞雲圖生成
詞雲圖生成部分比較簡單,用到第三方中文分詞庫jieba分詞,再用worldcloud庫生成詞雲即可,需要事先準備好背景圖片,生成詞雲字體顏色從圖片背景中獲取自動生成,代碼如下:
import jieba
import matplotlib
import matplotlib.pyplot as plt
from wordcloud import WordCloud, STOPWORDS, ImageColorGenerator
fin1 = open("文本.txt", "r", encoding='UTF-8')
def split_word(fin):
text = ''
# 讀取文件,去除換行符
for line in fin.readlines():
line = line.strip('\n')
# 分詞並存入text,注意用空格隔開
text += " ".join(jieba.cut(line))
# 設置背景圖
background_picture = "背景圖片.jpg"
background_Image = plt.imread(background_picture)
wc = WordCloud(
background_color='white',
mask=background_Image,
# 設置字體
font_path=r'C:\Windows\Fonts\simhei.ttf',
# 設置詞雲中的詞語數量
max_words=int(100),
# 分詞用到的停止詞,默認即可,也可以自定義
stopwords=STOPWORDS,
# 最大字號
max_font_size=400,
# 字體顏色種類
random_state=10
)
wc.generate_from_text(text)
wc.recolor(color_func=ImageColorGenerator(background_Image))
plt.imshow(wc)
plt.axis("off")
plt.show()
split_word(fin1)
這裏是自制的中國地圖背景圖:
成果展示
應用上述程序分析了2020年3月底到5月初的人民日報微博,生成詞雲圖如下:
可以非常直觀的看到,整個四月份,新館肺炎依然牢牢佔據人民日報的關注,並且關注點主要放在了境外輸入的確診病例上。
我們修改程序,爬取2017年12月至2020年5月份的兩萬多條微博進行分析,結果如下:
可以看到時間跨度拉長後,境外輸入佔比大大降低,而新冠肺炎確診病例成爲主要高頻詞,說明了新冠疫情可以算得上是近三年以來中國社會發生的最佔據公共注意力的大事件。