Python初學者之網絡爬蟲

聲明:本文內容和涉及到的代碼僅限於我的學習,任何人不得做爲商業用途。html

本文將介紹我最近在學習Python過程當中寫的一個爬蟲程序,將力爭作到不須要有任何Python基礎的程序員都能讀懂。讀者也能夠先跳到文章末尾看最終收集的數據效果和完整代碼。python

1. 確立目標需求

本次練習Python爬蟲的目標需求爲如下兩點:mysql

1) 收集huajiao.com上的人氣主播信息:每位主播的關注數,粉絲數,贊數,經驗值等數據

2) 收集每位人氣主播的直播歷史數據,包括每次直播的開播時間,觀看人數,贊數等數據

2. 確立邏輯步驟

首先經過瀏覽器查看www.huajiao.com網站上的各個頁面,分析它的網站結構。獲得以下信息:git

1) 每個導航項列出的都是直播列表,而非主播的我的主頁列表

以「熱門推薦」爲例,以下圖,每一個直播頁面的url格式爲http://www.huajiao.com/l/liveId, 這裏的liveId惟一標識一個直播,好比http://www.huajiao.com/l/52860333
直播列表程序員

2) 在直播頁上有主播的用戶ID和暱稱等信息

經過點擊用戶暱稱能夠進入主播的我的主頁
直播頁github

3) 在主播我的主頁上有更加完整的我的信息

更加完整的我的信息包括關注數,粉絲數,贊數,經驗值等數據;也有主播的直播歷史數據,以下圖,每一個主播我的主頁的url格式爲http://www.huajiao.com/user/userId, 這裏的userId惟一標識一個主播用戶,好比http://www.huajiao.com/user/50647288
主播我的主頁web

4) 程序邏輯

經過以上的分析,爬蟲能夠從直播列表頁入手,獲取到全部的直播url中的直播id,即上文提到的liveId;
拿到直播id後就能夠進入直播頁獲取用戶id,即前面提到的userId,
有了userId後就能夠進入主播我的主頁,在我的主頁上有主播完整的我的信息和直播歷史信息。
具體步驟以下:ajax

  • a):抓取直播列表頁的html, 我選取的是」熱門推薦」頁面http://www.huajiao.com/category/1000
  • b):從獲取到的「熱門推薦」頁面的html中過濾出全部的直播地址,http://www.huajiao.com/l/liveId
  • c):經過直播id抓取直播頁面的html, 並過濾出主播的userId
  • d):經過userId抓取主播的我的主頁,過濾出關注數,粉絲數,贊數,經驗值;過濾出直播歷史數據。
  • e):將用戶數據和直播歷史數據寫入mysql保存

以上是根據觀察網站頁面,直觀上得出的一個爬蟲邏輯,但實際在開發過程當中,還要考慮更多,好比:sql

  • a)爬蟲要定時執行,對於已經採集到的數據,採起何種更新策略
  • b)直播歷史數據須要請求相應的ajax接口,對收到的數據進行json解碼分析
  • c)主播暱稱包含emoji表情,若是數據庫使用經常使用的編碼」utf8″則會寫入報錯
  • d)過濾直播地址來獲取直播id時,須要使用到正則匹配,我使用的是Python庫」re」
  • e)分析html,我使用的是」BeautifulSoup」
  • f)讀寫mysql,我使用的是」pymysql」

如上邏輯步驟分析清楚後,就是編碼了,利用Python來實現以上的邏輯步驟。數據庫

3. Python編碼

1) 數據表設計

數據表設計
其中Tbl_Huajiao_User用於存儲主播的我的數據,Tbl_Huajiao_Live用於存儲主播的歷史直播數據,其中字段FScrapedTime是每次記錄更新的時間,依靠此字段能夠實現簡單的更新策略。

2) 從直播列表頁過濾出直播Id列表

# filter out live ids from a url
 def filterLiveIds(url):
     html = urlopen(url)
     liveIds = set()
     bsObj = BeautifulSoup(html, "html.parser")
     for link in bsObj.findAll("a", href=re.compile("^(/l/)")):
         if 'href' in link.attrs:
             newPage = link.attrs['href']
             liveId = re.findall("[0-9]+", newPage)
             liveIds.add(liveId[0])
     return liveIds

關於python中如何定義函數,直接看以上代碼就能夠了,使用」def」和冒號,沒有大括號。其中urlopen(url)是python的庫函數,須要作import, 以下:

from urllib.request import urlopen

其中BeautifulSoup是一個第三方Python庫,經過它就能夠方便的解析html代碼了,經過它的findAll()方法找出全部的a標籤,而且這個方法支持正則,因此在它的參數裏我傳入了一個正則re.compile(「^(/l/)」)來表示尋找一」/l/」開頭的全部連接地址,bsObj.findAll(「a」, href=re.compile(「^(/l/)」))的結果是一個列表,故使用for循環來遍歷列表內的元素,在遍歷過程當中經過使用正則re.findall(「[0-9]+」, newPage)匹配出liveId, 並臨時保存在liveIds中,並將liveIds返回給調用者。

3) 從直播頁過濾出主播id

# get user id from live page
 def getUserId(liveId):
     html = urlopen("http://www.huajiao.com/" + "l/" + str(liveId))
     bsObj = BeautifulSoup(html, "html.parser")
     text = bsObj.title.get_text()
     res = re.findall("[0-9]+", text)
     return res[0]

這裏仍是使用BeautifulSoup分析直播頁的html結構,使用bsObj.title.get_text()獲取到主播Id的文本信息後,經過正則獲取到最終的userId

4) 經過userId進入主播我的主頁獲取我的信息

#get user data from user page
def getUserData(userId):
     html = urlopen("http://www.huajiao.com/user/" + str(userId))
     bsObj = BeautifulSoup(html, "html.parser")
     data = dict()
     try:
         userInfoObj = bsObj.find("div", {"id":"userInfo"})
         data['FAvatar'] = userInfoObj.find("div", {"class": "avatar"}).img.attrs['src']
         userId = userInfoObj.find("p", {"class":"user_id"}).get_text()
         data['FUserId'] = re.findall("[0-9]+", userId)[0]
         tmp = userInfoObj.h3.get_text('|', strip=True).split('|')
         #print(tmp[0].encode("utf-8"))
         data['FUserName'] = tmp[0]
         data['FLevel'] = tmp[1]
         tmp = userInfoObj.find("ul", {"class":"clearfix"}).get_text('|', strip=True).split('|')
         data['FFollow'] = tmp[0]
         data['FFollowed'] = tmp[2]
         data['FSupported'] = tmp[4]
         data['FExperience'] = tmp[6]
         return data
     except AttributeError:
         #traceback.print_exc()
         print(str(userId) + ":html parse error in getUserData()")
         return 0

以上使用了python的try-except的異常處理機制,由於在使用BeautifulSoup分析html數據時,有時候會由於沒有某個對象而報錯,對於這種報錯須要處理,不然整個程序就會中止執行,這裏咱們打印出了日誌,在日誌中記錄了相應的userId。固然這裏仍是主要用到了BeautifulSoup便捷的功能,好比其中的get_text()方法,可以將多個標籤的文本抽取出來而且可以制定文本的分隔符,和對空格等字符進行過濾。

5) 將獲取的我的信息寫入mysql

# update user data
 def replaceUserData(data):
     conn = getMysqlConn()
     cur = conn.cursor()
     try:
         cur.execute("USE wanghong")
         cur.execute("set names utf8mb4")
         cur.execute("REPLACE INTO Tbl_Huajiao_User(FUserId,FUserName, FLevel, FFollow,FFollowed,FSupported,FExperience,FAvatar,FScrapedTime) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s)",                    (int(data['FUserId']), data['FUserName'],int(data['FLevel']),int(data['FFollow']),int(data['FFollowed']), int(data['FSupported']), int(data['FExperience']), data['FAvatar'],getNowTime())
         )
         conn.commit()
     except pymysql.err.InternalError as e:
         print(e)

這裏使用了Python第三方庫pymysql進行mysql的讀寫操做,而指定編碼utf8mb4,也就是爲了不文章開始提到的一個問題,關於emoji表情符,若是數據庫使用經常使用的編碼」CHARSET=utf8 COLLATE=utf8_general_ci」則會寫入報錯,注意上面sql語句裏也聲明瞭utf8mb4字符集和編碼。
這裏沒有使用mysql的「INSERT」,而是使用了「REPLACE」,是當包含一樣的FUserId的一條記錄被寫入時將替換原來的記錄,這樣可以保證爬蟲定時更新到最新的數據。

6) 獲取某主播的直播歷史數據

#get user history lives
def getUserLives(userId):
     try:
         url = "http://webh.huajiao.com/User/getUserFeeds?fmt=json&uid=" + str(userId)
         html = urlopen(url).read().decode('utf-8')
         jsonData = json.loads(html)
         if jsonData['errno'] != 0:
             print(str(userId) + "error occured in getUserFeeds for: " + jsonData['msg'])
             return 0

         return jsonData['data']['feeds']
 except Exception as e:
     print(e)
     return 0

前面說到,獲取直播歷史數據是經過直接請求ajax接口地址的,代碼中的url即爲接口地址,這是經過瀏覽器的調試工具得到的。這裏用到了json的解碼。

7) 將主播的直播歷史數據寫入Mysql

這裏和以上第5項相似,就不詳述了,讀者能夠在文章末尾的github地址獲取完整的代碼

8) 定義骨架函數

#spider user ids
def spiderUserDatas():
     for liveId in getLiveIdsFromRecommendPage():
         userId = getUserId(liveId)
         userData = getUserData(userId)
         if userData:
             replaceUserData(userData)
     return 1

#spider user lives
def spiderUserLives():
     userIds = selectUserIds(100)
     for userId in userIds:
         liveDatas = getUserLives(userId[0])
         for liveData in liveDatas:
             liveData['feed']['FUserId'] = userId[0]
             replaceUserLive(liveData['feed'])

     return 1

所謂的骨架函數,就是控制單個小的功能函數,實現循環邏輯,一頁一頁的去採集數據。
spiderUserDatas()的邏輯:拿到liveId列表後,循環遍歷的去取每個liveId對應的userId,進而渠道userData並寫入mysql;
spiderUserLives()的邏輯:從mysql中選出上次爬蟲時間最晚的100個userId, 循環遍歷地去取每個user的直播歷史數據並寫入mysql;

9) 定義入口函數和命令行參數

def main(argv):
      if len(argv) < 2:
         print("Usage: python3 huajiao.py [spiderUserDatas|spiderUserLives]")
         exit()
     if (argv[1] == 'spiderUserDatas'):
         spiderUserDatas()
     elif (argv[1] == 'spiderUserLives'):
         spiderUserLives()
     elif (argv[1] == 'getUserCount'):
         print(getUserCount())
     elif (argv[1] == 'getLiveCount'):
         print(getLiveCount())
     else:
         print("Usage: python3 huajiao.py [spiderUserDatas|spiderUserLives|getUserCount|getLiveCount]")
if  __name__ == '__main__':
     main(sys.argv)

首先,要命名python在命令行模式下如何接收參數,經過sys.argv;
再有__name__的含義,若是文件被執行,則__name__的值爲」__main__」;
這樣經過以上代碼就能夠實現命令行調用和參數處理了。

好比要爬取主播的我的信息,則執行:

python3 huajiao.py spiderUserDatas

好比查看爬取了多少條用戶數據信息,則執行:

python3 huajiao.py getUserCount

10) 加入crontab

*/1 * * * * python3 /root/PythonPractice/spiderWanghong/huajiao.py spiderUserDatas >> /tmp/huajiao.py_spiderUserDatas.log
*/1 * * * * python3 /root/PythonPractice/spiderWanghong/huajiao.py spiderUserLives >> /tmp/huajiao.py_spiderUserLives.log

4. 目標需求達成

主播數據
主播數據

直播歷史數據
直播歷史數據

5. 待改進項和後續計劃

對mysql的讀寫部分進行優化,如今寫的比較臃腫

對其餘直播網站進行分析並收集數據

將各個直播網站的數據進行聚合

6. 代碼地址

https://github.com/octans/PythonPractice/tree/master/spiderWanghong

歡迎關注代碼細語公衆號

相關文章
相關標籤/搜索