爬蟲系列之丁香園論壇的所有回復用戶數據(下)

  • 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哦!

Tags: