一、對微博頁面的分析
(一)對微博網頁端的分析
- 首先,我們開啟微博,發現從電腦端開啟微博,網址為:Sina Visitor System
- 我們搜尋關鍵字:巴以衝突,會發現其對應的 URL:巴以衝突
(1)URL 編碼/解碼
透過對 URL 進行分析,不難發現我們輸入的是中文“巴以衝突”,但是真實的連結卻不含中文,這是因為連結中的中文被編碼了。我們將複製來的 URL 進行解碼操作便可以得知。
在巴以衝突這個頁面裡面可以看到高階搜尋,開啟高階搜尋後發現可以對微博的釋出時間進行篩選,還可以對微博型別、微博包含的內容進行篩選。 一開始,想的便是從這下手,非常方便爬取指定時間內指定話題下的微博內容。
(2)抓包分析請求網址/請求方法/響應內容
接著,開啟開發者工具,對抓包進行分析,我們可以看到請求網址發生了變化, 請求網址
為: https://weibo.com/ajax/side/search?q=%E5%B7%B4%E4%BB%A5%E5%86%B2%E7%AA%81, 請求方法
是: GET
點開預覽、檢視相應內容,可以發現該請求網址返回的 json 檔案內容對應的就是頁面中載入的微博內容。
於是,便開始在 pycharm 中編寫請求程式碼
import requests # 匯入requests庫,用於傳送HTTP請求
url = 'https://weibo.com/ajax/side/search?q=%E5%B7%B4%E4%BB%A5%E5%86%B2%E7%AA%81'
print(url)
response = requests.get(url=url) # 傳送HTTP GET請求
print(f'響應狀態碼是:{response.status_code}') # 如果響應狀態碼為200(成功)
print(response.json())
(二)網頁端的侷限性(cookie 、微博數量問題)
雖然狀態碼返回 200 表示成功,但是 json 檔案裡面只有很少的 50 條微博資料,這對於爬蟲而言是非常少的資料。但是,當我們向下滑動想要進一步探究、檢視更多資料時,會發現這時候微博官方不給我們檢視,要求我們登陸賬號後才能檢視。
如果需要登陸賬號才能檢視更多微博內容,那麼意味著在爬蟲裡面傳送 http 請求時需要使用到賬號的 cookie,又考慮到網站肯定存在對爬蟲的檢測,如果使用 cookie 的話,肯定會被封禁的,這不僅僅會影響爬取微博資料的效率,還會造成短時間內無法開啟網站。
因此,這時候便不再嘗試從當前網址下手爬取資料。
(三)微博手機端的分析
便開始在網上查閱相關的資料,想要找到一個無需 cookie 便能爬取微博資料內容,同時又能突破只能檢視 50 條資料的侷限性。最終,在某網頁上面有網友提了一嘴,說:“在手機端介面爬取微博資料,比在網頁端爬取更加方便、侷限性相對來說更小”。 於是,便開始準備從手機端網址開始下手,先嚐試著驗證下網友說的是否正確。 手機端的網址是:https://m.weibo.cn
在搜尋框內搜尋巴以衝突,找到其對應的 URL:https://m.weibo.cn/search?containerid=100103type%3D1%26q%3D%E5%B7%B4%E4%BB%A5%E5%86%B2%E7%AA%81
(1)URL 編碼/解碼
不難發現,此處的中文仍然進行了編碼操作,我們需要對連結進行解碼檢視是否為原來的 URL。
(2)抓包分析
開啟開發者工具,開始對網頁抓包進行分析,我們在 Fetch/XHR 裡面可以找到 https 請求, 請求網址是:https://m.weibo.cn/api/container/getIndex?containerid=100103type%3D1%26q%3D%E5%B7%B4%E4%BB%A5%E5%86%B2%E7%AA%81&page_type=searchall, 請求方法是:GET, 請求引數是:containerid=100103type%3D1%26q%3D%E5%B7%B4%E4%BB%A5%E5%86%B2%E7%AA%81&page_type=searchall
(3)請求引數分析
由於微博每頁都有數量限制,因此當下滑到一定程度時,又有新的微博內容顯示,因此可以得知網頁資料是透過ajax檔案格式載入出來的。所以,找到其中的請求引數,發現存在page引數,這裡 page的引數為 2,就是代表第二頁。這個時候不難猜測出從本頁面下手,並不存在微博內容數量的限制,我們只需要設定好 page 引數即可。
於是便開始寫相關的程式碼,首先寫好請求引數 params
params = {
'containerid': f'100103type=1&q=#{keyword}#',
'page_type': 'searchall',
'page': page
}
(4)json 內容分析
接著我們開啟請求網址https://m.weibo.cn/api/container/getIndex?containerid=100103type%3D1%26q%3D%E5%B7%B4%E4%BB%A5%E5%86%B2%E7%AA%81&page_type=searchall,發現下面微博資料仍然是以 json 格式顯示的,因此我們需要對 json 檔案內容進行分析。
首先,我們將 json 檔案內容複製到 json 格式化檢驗裡,發現 返回的 json 是正確的 json 檔案
為了方便對 json 檔案內容的分析,我們選取 json 檢視對 json 進行視覺化分析。 不難發現,微博內容存在於 json/data/cards/裡面,下圖中的 0~22 均代表一條條微博內容等等資料。
(5)檢視/提取 json 有效資訊
結合網頁內容對 cards 下面的內容進行分析,我們可以發現: -->在 mblog 中存在判斷微博是否為長文字一項 isLongText : true 其他有用內容如下:
Json引數分析
'wid': item.get('id'), # 微博ID
'user_name': item.get('user').get('screen_name'), # 微博釋出者名稱
'user_id': item.get('user').get('id'), # 微博釋出者ID
'gender': item.get('user').get('gender'), # 微博釋出者性別
'publish_time': time_formater(item.get('created_at')), # 微博釋出時間
'source': item.get('source'), # 微博釋出來源
'status_province': item.get('status_province'), # 微博釋出者所在省份
'text': pq(item.get("text")).text(), # 僅提取內容中的文字
'like_count': item.get('attitudes_count'), # 點贊數
'comment_count': item.get('comments_count'), # 評論數
'forward_count': item.get('reposts_count'), # 轉發數
到了這裡,我驚訝的發現爬取時並不需要使用者的 cookie,可以證明網友的某些說法是正確的。
二、程式碼實現過程:
(1)導包
import requests # 匯入requests庫,用於傳送HTTP請求
from urllib.parse import urlencode # 匯入urlencode函式,用於構建URL引數
import time # 匯入time模組,用於新增時間延遲
import random # 匯入random模組,用於生成隨機數
from pyquery import PyQuery as pq # 匯入PyQuery庫,用於解析HTML和XML
from datetime import datetime # 匯入datetime模組,用於處理日期和時間
import os
import csv
(2)設定基礎的 URL 以及請求引數 params
# 設定代理等(新浪微博的資料是用ajax非同步下拉載入的,network->xhr)
host = 'm.weibo.cn' # 設定主機地址
base_url = 'https://%s/api/container/getIndex?' % host # 基礎URL,用於構建API請求URL
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36' # 設定使用者代理資訊
# 設定請求頭
headers = {
'Host': host, # 設定請求頭中的Host欄位
'keep': 'close', # 設定請求頭中的keep欄位
# 話題巴以衝突下的URL對應的Referer
# 'Referer': 'https://m.weibo.cn/search?containerid=100103type%3D1%26q%3D%E5%B7%B4%E4%BB%A5%E5%86%B2%E7%AA%81', #
'User-Agent': user_agent # 設定請求頭中的User-Agent欄位
}
(3)將微博的時間格式轉換為標準的日期時間格式
# 用於將微博的時間格式轉換為標準的日期時間格式
def time_formater(input_time_str):
input_format = '%a %b %d %H:%M:%S %z %Y' # 輸入時間的格式
output_format = '%Y-%m-%d %H:%M:%S' # 輸出時間的格式
return datetime.strptime(input_time_str, input_format).strftime(output_format)
(4)按頁數 page 抓取微博內容資料
# 按頁數抓取資料
def get_single_page(page, keyword):
# https://m.weibo.cn/api/container/getIndex?containerid=100103type=1&q=巴以衝突&page_type=searchall&page=1
# 構建請求引數
params = {
'containerid': f'100103type=1&q=#{keyword}#',
'page_type': 'searchall',
'page': page
}
url = base_url + urlencode(params) # 將輸入的中文關鍵詞編碼,構建出完整的API請求URL
print(url) # 列印請求的URL
error_times = 3 # 設定錯誤嘗試次數
while True:
response = requests.get(url, headers=headers) # 傳送HTTP GET請求
if response.status_code == 200: # 如果響應狀態碼為200(成功)
if len(response.json().get('data').get('cards')) > 0: # 檢查是否有資料
return response.json() # 返回JSON響應資料
time.sleep(3) # 等待3秒
error_times += 1 # 錯誤嘗試次數增加
if error_times > 3: # 如果連續出錯次數超過3次
return None # 返回空值
(5)定義長文字微博內容的爬取
# 長文字爬取程式碼段
def getLongText(lid): # 根據長文字的ID獲取長文字內容
# 長文字請求頭
headers_longtext = {
'Host': host,
'Referer': 'https://m.weibo.cn/status/' + lid,
'User-Agent': user_agent
}
params = {
'id': lid
}
url = 'https://m.weibo.cn/statuses/extend?' + urlencode(params) # 構建獲取長文字內容的URL
try:
response = requests.get(url, headers=headers_longtext) # 傳送HTTP GET請求
if response.status_code == 200: # 如果響應狀態碼為200(成功)
jsondata = response.json() # 解析JSON響應資料
tmp = jsondata.get('data') # 獲取長文字資料
return pq(tmp.get("longTextContent")).text() # 解析長文字內容
except:
pass
(6)對 json 中的有效資訊進行提取
# 修改後的頁面爬取解析函式
def parse_page(json_data):
global count # 使用全域性變數count
items = json_data.get('data').get('cards') # 獲取JSON資料中的卡片列表
for index, item in enumerate(items):
if item.get('card_type') == 7:
print('導語')
continue
elif item.get('card_type') == 8 or (item.get('card_type') == 11 and item.get('card_group') is None):
continue
if item.get('mblog', None):
item = item.get('mblog')
else:
item = item.get('card_group')[0].get('mblog')
if item:
if item.get('isLongText') is False: # 不是長文字
data = {
'wid': item.get('id'), # 微博ID
'user_name': item.get('user').get('screen_name'), # 微博釋出者名稱
'user_id': item.get('user').get('id'), # 微博釋出者ID
'gender': item.get('user').get('gender'), # 微博釋出者性別
'publish_time': time_formater(item.get('created_at')), # 微博釋出時間
'source': item.get('source'), # 微博釋出來源
'status_province': item.get('status_province'), # 微博釋出者所在省份
'text': pq(item.get("text")).text(), # 僅提取內容中的文字
'like_count': item.get('attitudes_count'), # 點贊數
'comment_count': item.get('comments_count'), # 評論數
'forward_count': item.get('reposts_count'), # 轉發數
}
else: # 長文字涉及文字的展開
tmp = getLongText(item.get('id')) # 呼叫函式獲取長文字內容
data = {
'wid': item.get('id'), # 微博ID
'user_name': item.get('user').get('screen_name'), # 微博釋出者名稱
'user_id': item.get('user').get('id'), # 微博釋出者ID
'gender': item.get('user').get('gender'), # 微博釋出者性別
'publish_time': time_formater(item.get('created_at')), # 微博釋出時間
'source': item.get('source'), # 微博釋出來源
'text': tmp, # 僅提取內容中的文字
'status_province': item.get('status_province'), # 微博釋出者所在省份
'like_count': item.get('attitudes_count'), # 點贊數
'comment_count': item.get('comments_count'), # 評論數
'forward_count': item.get('reposts_count'), # 轉發數
}
count += 1
print(f'total count: {count}') # 列印總計數
yield data # 返回資料
(7)將爬取到的內容儲存到 csv 檔案內
if __name__ == '__main__':
keyword = '巴以衝突' # 設定關鍵詞
result_file = f'10月26日{keyword}話題.csv' # 設定結果檔名
if not os.path.exists(result_file):
with open(result_file, mode='w', encoding='utf-8-sig', newline='') as f:
writer = csv.writer(f)
writer.writerow(['微博ID', '微博釋出者名稱', '微博釋出者ID', '微博釋出者性別',
'微博釋出時間', '微博釋出來源', '微博內容', '微博釋出者所在省份', '微博點贊數量', '微博評論數量',
'微博轉發量']) # 寫入CSV檔案的標題行
temp_data = [] # 用於臨時儲存資料的列表
empty_times = 0 # 空資料的連續次數
for page in range(1, 50000): # 迴圈抓取多頁資料
print(f'page: {page}')
json_data = get_single_page(page, keyword) # 獲取單頁資料
if json_data == None: # 如果資料為空
print('json is none')
break
if len(json_data.get('data').get('cards')) <= 0: # 檢查是否有資料
empty_times += 1
else:
empty_times = 0
if empty_times > 3: # 如果連續空資料超過3次
print('\n\n consist empty over 3 times \n\n')
break
for result in parse_page(json_data): # 解析並處理頁面資料
temp_data.append(result) # 將資料新增到臨時列表
if page % save_per_n_page == 0: # 每隔一定頁數儲存一次資料
with open(result_file, mode='a+', encoding='utf-8-sig', newline='') as f:
writer = csv.writer(f)
for d in temp_data:
# 將爬取到的資料寫入CSV檔案
writer.writerow(
[d['wid'],
d['user_name'],
d['user_id'],
d['gender'],
d['publish_time'],
d['source'],
d['text'],
d['status_province'],
d['like_count'],
d['comment_count'],
d['forward_count']])
print(f'\n\n------cur turn write {len(temp_data)} rows to csv------\n\n') # 列印儲存資料的資訊
temp_data = [] # 清空臨時資料列表
time.sleep(random.randint(4, 8)) # 隨機等待一段時間,模擬人的操作
(8)對csv資料進行處理
開啟 csv 檔案後會發現,存在重複的微博內容。 因為微博都有自己獨有的 ID,故從 ID 下手對重複值進行刪除處理。 在 Jupyter notebook 裡面進行資料處理分析
import pandas as pd
# 讀取CSV檔案
df = pd.read_csv('10月26日巴以衝突話題.csv')
# 檢測並刪除重複值
df.drop_duplicates(subset='微博ID', keep='first', inplace=True)
# 儲存處理後的結果到新的CSV檔案
df.to_csv('處理後的內容.csv', index=False)
三、完整的程式碼
完整爬蟲程式碼(註釋由gpt自動生成)
"""
這段Python程式碼是一個用於爬取新浪微博資料的指令碼。它使用了多個Python庫來實現不同功能,包括髮送HTTP請求、解析HTML和XML、處理日期和時間等。
以下是程式碼的主要功能和結構:
1. 匯入所需的Python庫:
- `requests`: 用於傳送HTTP請求。
- `urllib.parse`: 用於構建URL引數。
- `time`: 用於新增時間延遲。
- `random`: 用於生成隨機數。
- `pyquery`: 用於解析HTML和XML。
- `datetime`: 用於處理日期和時間。
- `os`:用於檔案操作。
- `csv`:用於讀寫CSV檔案。
2. 設定一些常量和請求頭資訊,包括主機地址、基礎URL、使用者代理資訊、請求頭等。
3. 定義了一個`time_formater`函式,用於將微博的時間格式轉換為標準的日期時間格式。
4. 定義了一個`get_single_page`函式,用於按頁數抓取資料,構建API請求URL,併傳送HTTP GET請求。它還包含了錯誤重試邏輯。
5. 定義了一個`getLongText`函式,用於根據長文字的ID獲取長文字內容。這部分程式碼涉及長文字的展開。
6. 定義了一個`parse_page`函式,用於解析頁面返回的JSON資料,提取所需的資訊,並生成資料字典。
7. 主程式部分包括以下功能:
- 設定關鍵詞(`keyword`)和結果檔名(`result_file`)。
- 開啟結果檔案(CSV),如果檔案不存在,則建立檔案並寫入標題行。
- 定義臨時資料列表`temp_data`,用於儲存資料。
- 進行迴圈,抓取多頁資料,解析並處理頁面資料,然後將資料寫入CSV檔案。
- 在每隔一定頁數儲存一次資料到CSV檔案。
- 隨機等待一段時間以模擬人的操作。
總體來說,這段程式碼的主要目的是爬取新浪微博中與特定關鍵詞相關的微博資料,並將其儲存到CSV檔案中。
它處理了長文字的展開以及一些錯誤重試邏輯。需要注意的是,爬取網站資料時應遵守網站的使用政策和法律法規。
"""
import requests # 匯入requests庫,用於傳送HTTP請求
from urllib.parse import urlencode # 匯入urlencode函式,用於構建URL引數
import time # 匯入time模組,用於新增時間延遲
import random # 匯入random模組,用於生成隨機數
from pyquery import PyQuery as pq # 匯入PyQuery庫,用於解析HTML和XML
from datetime import datetime # 匯入datetime模組,用於處理日期和時間
import os
import csv
# 設定代理等(新浪微博的資料是用ajax非同步下拉載入的,network->xhr)
host = 'm.weibo.cn' # 設定主機地址
base_url = 'https://%s/api/container/getIndex?' % host # 基礎URL,用於構建API請求URL
user_agent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36' # 設定使用者代理資訊
# 設定請求頭
headers = {
'Host': host, # 設定請求頭中的Host欄位
'keep': 'close', # 設定請求頭中的keep欄位
# 話題巴以衝突下的URL對應的Referer
'Referer': 'https://m.weibo.cn/search?containerid=100103type%3D1%26q%3D%E5%B7%B4%E4%BB%A5%E5%86%B2%E7%AA%81', #
'User-Agent': user_agent # 設定請求頭中的User-Agent欄位
}
save_per_n_page = 1 # 每隔多少頁儲存一次資料
# 用於將微博的時間格式轉換為標準的日期時間格式
def time_formater(input_time_str):
input_format = '%a %b %d %H:%M:%S %z %Y' # 輸入時間的格式
output_format = '%Y-%m-%d %H:%M:%S' # 輸出時間的格式
return datetime.strptime(input_time_str, input_format).strftime(output_format)
# 按頁數抓取資料
def get_single_page(page, keyword):
# https://m.weibo.cn/api/container/getIndex?containerid=100103type=1&q=巴以衝突&page_type=searchall&page=1
# 構建請求引數
params = {
'containerid': f'100103type=1&q=#{keyword}#',
'page_type': 'searchall',
'page': page
}
url = base_url + urlencode(params) # 將輸入的中文關鍵詞編碼,構建出完整的API請求URL
print(url) # 列印請求的URL
error_times = 3 # 設定錯誤嘗試次數
while True:
response = requests.get(url, headers=headers) # 傳送HTTP GET請求
if response.status_code == 200: # 如果響應狀態碼為200(成功)
if len(response.json().get('data').get('cards')) > 0: # 檢查是否有資料
return response.json() # 返回JSON響應資料
time.sleep(3) # 等待3秒
error_times += 1 # 錯誤嘗試次數增加
if error_times > 3: # 如果連續出錯次數超過3次
return None # 返回空值
# 長文字爬取程式碼段
def getLongText(lid): # 根據長文字的ID獲取長文字內容
# 長文字請求頭
headers_longtext = {
'Host': host,
'Referer': 'https://m.weibo.cn/status/' + lid,
'User-Agent': user_agent
}
params = {
'id': lid
}
url = 'https://m.weibo.cn/statuses/extend?' + urlencode(params) # 構建獲取長文字內容的URL
try:
response = requests.get(url, headers=headers_longtext) # 傳送HTTP GET請求
if response.status_code == 200: # 如果響應狀態碼為200(成功)
jsondata = response.json() # 解析JSON響應資料
tmp = jsondata.get('data') # 獲取長文字資料
return pq(tmp.get("longTextContent")).text() # 解析長文字內容
except:
pass
# 解析頁面返回的JSON資料
count = 0 # 計數器,用於記錄爬取的資料數量
# 修改後的頁面爬取解析函式
def parse_page(json_data):
global count # 使用全域性變數count
items = json_data.get('data').get('cards') # 獲取JSON資料中的卡片列表
for index, item in enumerate(items):
if item.get('card_type') == 7:
print('導語')
continue
elif item.get('card_type') == 8 or (item.get('card_type') == 11 and item.get('card_group') is None):
continue
if item.get('mblog', None):
item = item.get('mblog')
else:
item = item.get('card_group')[0].get('mblog')
if item:
if item.get('isLongText') is False: # 不是長文字
data = {
'wid': item.get('id'), # 微博ID
'user_name': item.get('user').get('screen_name'), # 微博釋出者名稱
'user_id': item.get('user').get('id'), # 微博釋出者ID
'gender': item.get('user').get('gender'), # 微博釋出者性別
'publish_time': time_formater(item.get('created_at')), # 微博釋出時間
'source': item.get('source'), # 微博釋出來源
'status_province': item.get('status_province'), # 微博釋出者所在省份
'text': pq(item.get("text")).text(), # 僅提取內容中的文字
'like_count': item.get('attitudes_count'), # 點贊數
'comment_count': item.get('comments_count'), # 評論數
'forward_count': item.get('reposts_count'), # 轉發數
}
else: # 長文字涉及文字的展開
tmp = getLongText(item.get('id')) # 呼叫函式獲取長文字內容
data = {
'wid': item.get('id'), # 微博ID
'user_name': item.get('user').get('screen_name'), # 微博釋出者名稱
'user_id': item.get('user').get('id'), # 微博釋出者ID
'gender': item.get('user').get('gender'), # 微博釋出者性別
'publish_time': time_formater(item.get('created_at')), # 微博釋出時間
'source': item.get('source'), # 微博釋出來源
'text': tmp, # 僅提取內容中的文字
'status_province': item.get('status_province'), # 微博釋出者所在省份
'like_count': item.get('attitudes_count'), # 點贊數
'comment_count': item.get('comments_count'), # 評論數
'forward_count': item.get('reposts_count'), # 轉發數
}
count += 1
print(f'total count: {count}') # 列印總計數
yield data # 返回資料
if __name__ == '__main__':
keyword = '巴以衝突' # 設定關鍵詞
result_file = f'10月26日{keyword}話題.csv' # 設定結果檔名
if not os.path.exists(result_file):
with open(result_file, mode='w', encoding='utf-8-sig', newline='') as f:
writer = csv.writer(f)
writer.writerow(['微博ID', '微博釋出者名稱', '微博釋出者ID', '微博釋出者性別',
'微博釋出時間', '微博釋出來源', '微博內容', '微博釋出者所在省份', '微博點贊數量', '微博評論數量',
'微博轉發量']) # 寫入CSV檔案的標題行
temp_data = [] # 用於臨時儲存資料的列表
empty_times = 0 # 空資料的連續次數
for page in range(1, 50000): # 迴圈抓取多頁資料
print(f'page: {page}')
json_data = get_single_page(page, keyword) # 獲取單頁資料
if json_data == None: # 如果資料為空
print('json is none')
break
if len(json_data.get('data').get('cards')) <= 0: # 檢查是否有資料
empty_times += 1
else:
empty_times = 0
if empty_times > 3: # 如果連續空資料超過3次
print('\n\n consist empty over 3 times \n\n')
break
for result in parse_page(json_data): # 解析並處理頁面資料
temp_data.append(result) # 將資料新增到臨時列表
if page % save_per_n_page == 0: # 每隔一定頁數儲存一次資料
with open(result_file, mode='a+', encoding='utf-8-sig', newline='') as f:
writer = csv.writer(f)
for d in temp_data:
# 將爬取到的資料寫入CSV檔案
writer.writerow(
[d['wid'],
d['user_name'],
d['user_id'],
d['gender'],
d['publish_time'],
d['source'],
d['text'],
d['status_province'],
d['like_count'],
d['comment_count'],
d['forward_count']])
print(f'\n\n------cur turn write {len(temp_data)} rows to csv------\n\n') # 列印儲存資料的資訊
temp_data = [] # 清空臨時資料列表
time.sleep(random.randint(4, 8)) # 隨機等待一段時間,模擬人的操作