爬蟲系列之丁香園論壇的所有回復用戶數據(下)
- 2019 年 10 月 5 日
- 筆記

目錄
0.寫在前面
1.原理分析
2.程式碼實現
2.1 爬取bbs
2.2 用戶個人主頁
2.3 bbs與用戶主頁聯合使用
0.寫在前面
【上一節直通點】爬蟲系列之爬取丁香園用戶主頁
【高能預警】本節你可以學會什麼?
- 第一:lxml及xpath使用
- 第二:模擬登陸處理
- 第三:多頁面處理
- 第四:MongoDB存儲
- 第五:使用pandas存儲數據到csv
- 第六:數據下載
- 第七:列表、字典處理
- 第八:面相對象思想
- 第九:正則啊等等。。。。
【異常處理】
對於上一節的爬蟲,只是做了個簡單的數據爬取及存儲,但是當遇見不同的個人主頁時,程式碼就會報錯,數據就會錯落,為了更好的解決這個問題,本節即對上節程式碼進行優化及異常處理。
【bbs數據提取】
除了對程式碼異常處理之外,還需要使得我們的程式碼更加智慧化,我們本次實現的功能是:隨便傳入一個類似的bbs鏈接,即可獲取到當前bbs裡面的所有的回復用戶的用戶數據,從這個需求中我們提取中兩個關鍵點。
- 第一:爬取bbs裡面的回復用戶;
- 第二:爬取用戶數據;
第二點上節基本上已經實現,那麼重點放在第一點的講解及異常處理等。好了,廢話不多說,開始實戰!
【項目目錄】
├─data 這裡面存放每個用戶的頭像 ├─each 這裡面存放每個用戶的csv數據 ├─raw_demo dxy_raw_profile.py # 上一節的爬取用戶主頁程式碼 all.csv # 這個是生成的文件,程式碼運行後才有,是對each裡面的所有csv文件的集合 bbs_genspider.py # bbs論壇程式碼 dxy_profile.py # 用戶主頁程式碼 最後運行只需運行這個即可,記得替換raw_id = '3927842'
【項目使用】
根據上述項目目錄修改raw_id,除此之外,需要本地安裝mongodb,並創建資料庫名為test,collection為dxy,記得運行前修改bbs_genspider.py裡面的cookie,最後運行dxy_profile.py即可。
1.原理分析
先來看一下我們爬取的網站長什麼樣子?
- bbs論壇

bbs論壇圖
- 用戶個人主頁

用戶個人主頁圖
以上就是我們要爬取的目標站點。
對於第一張圖,紅色框圖標註出來都就是我們想要爬取的目標用戶,除了我標註的,bbs還有其他人的回復,由於上圖的樓主回復的太多,圖片截不出後面其他回復用戶,大家可以根據圖一中的url進行檢驗。
【第一個問題】
那麼現在問題來了,是不是我直接爬取那個bbs(如圖1)的url,定位到左側邊欄的每一個用戶,找到對應的用戶名就可以了?
答:答案是對的,確實如此解決,我們上一節知道如圖2所示,用戶主頁為http://i.dxy.cn/profile/用戶名,那麼只要替換不同的用戶名,便可以實現多個用戶的爬取,哈哈,終於有數據了~~~
現在是時候輪到bbs發話了,那麼問題又來了,如何從bbs中爬取相應的用戶名呢?不著急,我們看圖說話!

爬取用戶名圖
【第二個問題】
看出什麼端倪了?
答:只要我們利用xpth定位到a標籤的text()值,便可以迎刃而解了~~~
【第三個問題】
最後程式碼爬取完,發現怎麼數據這麼少呢?
答:結果發現是用戶未登陸,那麼你只能爬到幾個數據,可是我要做研究啊,沒數據,怎麼搞,接下來就是模擬登陸上場了,這裡的模擬登陸沒有那種高大上的selenuim,也沒有Session(), 而是採用了最為低調的requests添加cookie方式。
【第四個問題】
最後會發現還是覺得數據少啊,發現這個bbs有多頁面,這個又該如何處理呢?
答:那麼我們不妨嘗試一下,點擊下一頁,看有什麼規律嘛,果不其然,被我瞄到了~~~我們看圖說話
注意:下圖中的鏈接為bbs鏈接!

bbs鏈接圖
每變動一頁,會發現在id後面多了個?ppg=6,我們會發現此處的6正是頁數,我們再來看一張圖:

多頁面分析圖
那麼只需要通過xpath定位到<div class="num">下面的最後一個a標籤即可,這個規律為從打開多個bbs鏈接中發現的!
【第五個問題】
但是當單頁面時候,它就不會有這個<div class="num">,更不會有a標籤,此時程式碼肯定報錯,那麼這個怎麼能難倒我呢?
答:直接做個異常處理,然後設置page為1即可解決!
2.程式碼實現
2.1 爬取bbs
- bbs的url前綴
base_url = 'http://www.dxy.cn/bbs/topic/' # 在類外面
- 封裝成類
class bbs_genspider(object): def __init__(self, id): self.url = base_url + id # 與base_url拼接成真實地址
- 獲取html頁面(注意填寫自己的cookie!)
def get_html(self): headers = { 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', 'Accept-Encoding': 'gzip, deflate', 'Accept-Language': 'zh-CN,zh;q=0.9', 'Cache-Control': 'max-age=0', 'Connection': 'keep-alive', 'Cookie': '填寫自己的cookie', 'Host': 'www.dxy.cn', 'Referer': 'https://auth.dxy.cn/', 'Upgrade-Insecure-Requests': '1', 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36', } req = requests.get(self.url, headers=headers).text return req注意事項:填寫自己的cookie,填寫的是登陸成功後的cookie!
xpath解析
import requests from lxml import etree def get_BbsInfo(self): raw_html = self.get_html() selector = etree.HTML(raw_html) # 提取bbs標題 bbs_title = selector.xpath('//table[@class="title tbfixed"]/tbody/tr/th/h1/text()')[0] bbs_title = bbs_title.strip() # 去除字元串左右的空格 print(bbs_title) # 頭像 bbs_other_avater = selector.xpath('//td[@class="tbs"]//div[@class="avatar"]/div/span/a/img/@src') print(bbs_other_avater) # 用戶名 bbs_other_id = selector.xpath('//td[@class="tbs"]//div[@class="auth"]//a/text()') print(bbs_other_id) # 處理單頁面異常! try: page = selector.xpath('//div[@class="pages"]/div[@class="num"]/a[last()]/text()')[0] print(page) except IndexError as e: page = 1 ''' 到這裡,我們得到了當前bbs的所有回復用戶的資訊 bbs_other_avater 用戶的頭像地址 bbs_other_id 用戶的用戶名 page 頁面數量 ''' return bbs_other_id,bbs_other_avater,page注意事項:此處返回的數據為當前bbs的所有回復用戶,那麼問題來了,既然是回復用戶,當然有些用戶回復了很多次,相當於此處獲得的是最原始的所有回復用戶數據,那麼如何去重呢?請往下看!
- 獲取所有頁面的Url
# 獲取所有頁面的Url def get_AllPageUrl(self, raw_id): bbs = bbs_genspider(raw_id) bbs_other_id, bbs_other_avater, page = bbs.get_BbsInfo() page_list = [] for i in range(1, int(page) + 1): page_url = raw_id + '?ppg=' + str(i) page_list.append(page_url) return page_list
- bbs用戶數據去重
# 刪除重複的用戶 def del_common(self, raw_id): page_list = self.get_AllPageUrl(raw_id) data_bbs = {} for url in page_list: bbs = bbs_genspider(url) bbs_id, bbs_avater, page = bbs.get_BbsInfo() bbs_data = dict(zip(bbs_id, bbs_avater)) # 兩列表合併成字典 for key in bbs_data: if key not in data_bbs: data_bbs[key] = bbs_data[key] bbs_id = [] bbs_avater = [] for key in data_bbs: bbs_id.append(key) bbs_avater.append(data_bbs[key]) return bbs_id, bbs_avater 注意事項:get_BbsInfo()返回的id及avater都是列表,那麼此處的去重就是解決上述問題的去重,也就是說list去重操作。這裡採用的演算法思想是通過zip()函數將兩個list合併成字典,key為id,value為avater,那麼現在問題就變為了字典去重,先定義一個新的字典,如果當前添加的數據不在新的字典裡面,就往裡面加,最後就得到了一個去重後的字典,對字典進行拆分為兩個列表即可,返回後的兩個列表就是我們最後所需要的數據。
為什麼獲取了頭像地址呢,原因很簡單,想下載到本地以供往後頭像的選擇!
【說在最後】
上述所有方法全部封裝在類裡面即可。
- 測試
raw_id = '12345' bbs = bbs_genspider(raw_id) bbs_id,bbs_avater = bbs.del_common(raw_id) print("----------------------------------") print(bbs_id) print(len(bbs_id)) print(bbs_avater) print(len(bbs_avater))
測試結果圖
上述測試的是單頁面,然後打開www.dxy.cn/bbs/thread/12345,自己數一下(記得去重),便跟上述列印的結果數量3一樣!到這裡便測試成功了!
2.2 用戶個人主頁
用戶個人主頁功能在上一節做了詳細解釋,這一節主要側重於異常的處理及如何調用bbs程式碼,將bbs程式碼與個人主頁程式碼結合使用。
上一節保持不變程式碼如下:
【基本框架】
import requests from lxml import etree import pymongo import pandas as pd from urllib.request import urlretrieve from bbs_genspider import bbs_genspider base_url = 'http://i.dxy.cn/profile/' MONGO_URI = 'localhost' MONGO_DB = 'test' # 定義資料庫 MONGO_COLLECTION = 'dxy' # 定義資料庫表 class dxy_spider(object): # 初始化 def __init__(self, user_id, mongo_uri, mongo_db): self.url = base_url + user_id self.client = pymongo.MongoClient(mongo_uri) self.db = self.client[mongo_db] # 獲取html def get_html(self): headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.106 Safari/537.36', } req = requests.get(self.url, headers=headers).text # print(req) return req def Save_MongoDB(self,userinfo): self.db[MONGO_COLLECTION].insert(userinfo) self.client.close() # 每個用戶資訊存儲至csv文件 def Sava_Excel(self, userinfo): key_list = [] value_list = [] for key, value in userinfo.items(): key_list.append(key) value_list.append(value) key_list.insert(0, '用戶名') # 增加用戶名列 value_list.insert(0, user) # 增加用戶名 data = pd.DataFrame(data=[value_list],columns=key_list) print(data) ''' 表示以用戶名命名csv文件,並去掉DataFame序列化後的index列(這就是index=False的意思),並以utf_8_sig編碼, 防止中文亂碼。 注意:一定要先用pandas的DataFrame序列化後,方可使用to_csv方法導出csv文件! ''' data.to_csv('./each/' + user + '.csv', encoding='utf_8_sig', index=False) 注意:與上節相比,唯一變動的是data.to_csv('./' + user + '.csv', encoding='utf-8', index=False),修改為data.to_csv('./each/' + user + '.csv', encoding='utf_8_sig', index=False),防止有些電腦打開csv文件亂碼!
大改動如下:
【異常處理】
語法:
try: xxxx except Exception as e: xxx
實戰:
def get_UserInfo(self): raw_html = self.get_html() selector = etree.HTML(raw_html) key_list = [] value_list = [] force_fan_dd_key = selector.xpath('//div[@class="follows-fans clearfix"]//p/text()') force_fan_dd_value = selector.xpath('//div[@class="follows-fans clearfix"]//p/a/text()') if '關注' in force_fan_dd_key: for each in force_fan_dd_key: key_list.append(each) for each in force_fan_dd_value: value_list.append(each) else: key_list = ['關注', '粉絲', '丁當'] value_list = ['無', '無', '無'] UserInfo_dict = dict(zip(key_list, value_list)) # 兩個list合併為dict # print(UserInfo_dict) # {'關注': '28', '粉絲': '90', '丁當': '1128'} try: user_home = selector.xpath('//p[@class="details-wrap__items"]/text()')[0] # user_home = user_home.replace(',','') # 去掉逗號,否則使用MongoDB可視化工具導出csv文件報錯! user_home = user_home.replace(',', ',') # 改變英文逗號為中文逗號,否則使用MongoDB可視化工具導出csv文件報錯! print(user_home) UserInfo_dict['地址'] = user_home except IndexError as e: UserInfo_dict['地址'] = '無' print('地址缺少,報錯!') try: user_profile = selector.xpath('//p[@class="details-wrap__items details-wrap__last-item"]/text()')[0] user_profile = user_profile.replace(',', ',') # 改變英文逗號為中文逗號,否則使用MongoDB可視化工具導出csv文件報錯! UserInfo_dict['座右銘'] = user_profile except IndexError as e: UserInfo_dict['座右銘'] = '無' print('座右銘缺少,報錯!') # 帖子被瀏覽 try: article_browser = selector.xpath( '//li[@class="statistics-wrap__items statistics-wrap__item-topic fl"]/p/text()') UserInfo_dict[article_browser[0]] = article_browser[1] except IndexError as e: UserInfo_dict['帖子被瀏覽'] = '0次' print('帖子被瀏覽缺少,報錯!') # 帖子被投票 try: article_vote = selector.xpath( '//li[@class="statistics-wrap__items statistics-wrap__item-vote fl"]/p/text()') UserInfo_dict[article_vote[0]] = article_vote[1] except IndexError as e: UserInfo_dict['帖子被投票'] = '0次' print('帖子被投票缺少,報錯!') # 帖子被收藏 try: article_collect = selector.xpath( '//li[@class="statistics-wrap__items statistics-wrap__item-fav fl"]/p/text()') UserInfo_dict[article_collect[0]] = article_collect[1] except IndexError as e: UserInfo_dict['帖子被收藏'] = '0次' print('帖子被收藏缺少,報錯!') # 在線時長共 try: onlie_time = selector.xpath( '//li[@class="statistics-wrap__items statistics-wrap__item-time fl"]/p/text()') UserInfo_dict[onlie_time[0]] = onlie_time[1] except IndexError as e: UserInfo_dict['在線時長共'] = '0次' print('在線時長共缺少,報錯!') return UserInfo_dict【解釋】
這裡採用異常處理原因如下:

異常結果圖
上圖我們發現壹刀的數據亂了,那麼我們現在進入這個人的主頁來看。瀏覽器輸入http://i.dxy.cn/profile/壹刀,會發現重定向進入http://i.dxy.cn/lizhanqiang這個頁面,如下圖所示:

壹刀主頁圖
會發現這個已經不是我們想要的用戶頁面圖了。。。所以自然那些數據爬取不到,就會報錯。。這樣就得加異常處理!如下圖所示為加異常處理後的結果,會發現已經對齊!

正常結果圖
異常處理前,數據不對齊,還有另外一種就是這個人的用戶主頁沒了。。。對,沒錯就是404。。。
如:瀏覽器打開http://i.dxy.cn/ymyl,會看到下圖所示頁面,直接404了,自然沒數據,沒數據,自然報錯,這就是異常處理的作用!

404頁面圖
2.3 bbs與用戶主頁聯合使用
【下載頭像】
def DownLoadUserAvater(self, bbs_avater, bbs_id): urlretrieve(bbs_avater, './data/{0}.jpg'.format(bbs_id))【程式調用】
if __name__ == '__main__': raw_id = '3927842' bbs = bbs_genspider(raw_id) bbs_id,bbs_avater = bbs.del_common(raw_id) print("----------------------------------") print(bbs_id) print(len(bbs_id)) print(bbs_avater) print(len(bbs_avater)) i = 0 for user in bbs_id: dxy = dxy_spider(user, MONGO_URI, MONGO_DB) userinfo = dxy.get_UserInfo() print('-----------') print(userinfo) dxy.Save_MongoDB(userinfo) dxy.DownLoadUserAvater(bbs_avater[i], user) dxy.Sava_Excel(userinfo) # 合併each裡面所有的單個用戶數據,並存儲至all.csv df = pd.read_csv('./each/' + user + '.csv', engine='python', encoding='utf_8_sig') if i == 0: df.to_csv('all.csv', encoding="utf_8_sig", index=False, mode='a+', header=True) else: df.to_csv('all.csv', encoding="utf_8_sig", index=False, mode='a+', header=False) i += 1【效果呈現】
- 終端log

用戶名結果圖

頭像鏈接結果圖
- 資料庫

- 本地

MongoDB數據存儲結果圖

本地頭像數據圖

本地單個用戶數據圖
最後的最後,如果您覺得這篇文章有幫助,請點贊一下,想看源碼的朋友,可以點擊閱讀原文,查看即可,不要忘記star哦!