爬蟲試手——百度貼吧爬蟲

自學python有一段時間了,做過的東西還不多,最近開始研究爬蟲,想自己寫一個爬百度貼吧的帖子內容,然後對帖子做分詞和詞頻統計,看看這個吧熱議的關鍵詞都有哪些。百度了好多資料和視頻,學到了不少東西,但也生出了一些問題:

1、http請求用python自帶的urllib,也可以用requests,哪個更好用?

2、html解析可以用正則表達式,也可以用xpath,哪個效率更高?

根據網上資料的說法,requests相對更好用,因爲很多功能已經封裝好了,性能上與urllib也沒什麼區別,而正則表達式通常要比xpath效率更高。不過實踐出真知,分別用兩種方式寫出來然後對比一下。爬取的目標是我很喜歡的一個遊戲——英雄無敵3的貼吧,從第10頁爬到30頁,只爬帖子、回帖以及樓中樓內容的文字部分。首先用建議初學者使用的urllib加正則表達式寫了一版:

# -*- coding: utf-8 -*-

from urllib import request

import re

import queue

import os

import math

import threading

from time import sleep

import datetime


baseurl="https://tieba.baidu.com" #貼吧頁面url的通用前綴

q=queue.Queue() #保存帖子鏈接的隊列

MAX_WAIT=10 #解析線程的最大等待時間

reg=re.compile('<[^>]*>') #去除html標籤的正則表達式


#封裝的獲取html字符串的函數

def get_html(url):

    response=request.urlopen(url)

    html=response.read().decode('utf-8')

    return html


#採集url的線程,thnum線程id,startpage開始採集的頁數,step單個線程採集頁數間隔(與線程個數相同),maxpage採集結束的頁數,url採集的貼吧的url後綴

class getlinkthread(threading.Thread):

    def __init__(self,thnum,startpage,step,maxpage,url):

        threading.Thread.__init__(self)

        self.thnum=thnum

        self.startpage=startpage

        self.step=step

        self.maxpage=maxpage

        self.url=url

    def run(self):

        mm=math.ceil((self.maxpage-self.startpage)/self.step) #計算循環的範圍

        for i in range(0,mm):

            startnum=self.startpage+self.thnum+i*self.step #開始頁數

            tempurl=baseurl+self.url+"&pn="+str(startnum*50) #構造每一頁的url

            print("Thread %s is getting %s"%(self.thnum,tempurl))

            try:

                temphtml=get_html(tempurl)

                turls = re.findall(r'rel="noreferrer" href="(/p/[0-9]*?)"', temphtml,re.S) #獲取當前頁的所有帖子鏈接

                for tu in turls: #入隊列

                    q.put(tu)

            except:

                print("%s get failed"%(tempurl))

                pass

            sleep(1)


#解析url的線程,thrnum線程id,barname貼吧名,用來構造文件保存路徑

class parselinkthread(threading.Thread):

    def __init__(self,thrnum,barname):

        threading.Thread.__init__(self)

        self.thrnum=thrnum

        self.barname=barname

    def run(self):

        waittime=0

        while True:

            if q.empty() and waittime<MAX_WAIT: #隊列爲空且等待沒有超過MAX_WAIT時,繼續等待

                sleep(1)

                waittime=waittime+1

                print("Thr %s wait for %s secs"%(self.thrnum,waittime))

            elif waittime>=MAX_WAIT: #等待超過MAX_WAIT時,線程退出

                print("Thr %s quit"%(self.thrnum))

                break

            else: #隊列不爲空時,重置等待時間,從隊列中取帖子url,進行解析

                waittime=0

                item=q.get()

                self.dotask(item)

    def dotask(self,item):

        print("Thr %s is collecting %s"%(self.thrnum,item))

        self.savepost(item,self.barname)

    #抓取一頁的內容,包括帖子及樓中樓,入參爲頁面url和帖子id,返回值爲帖子的內容字符串

    def getpagestr(self,url,tid):

        html=get_html(url)

        result1 = re.findall(r'class="d_post_content j_d_post_content ">(.*?)</div>', html,re.S)

        result2 = re.findall(r'class="j_lzl_r p_reply" data-field=\'{(.*?)}\'', html,re.S)

        pagestr=""

        for res in result1:

            pagestr=pagestr+reg.sub('',res)+"\n"  #先整合帖子內容

        for res in result2:

            if 'null' not in res:  #若有樓中樓,即層數不爲null

                pid=res.split(",")[0].split(":")[1]  #樓中樓id

                numreply=int(res.split(",")[1].split(":")[1])  #樓中樓層數

                tpage=math.ceil(numreply/10) #計算樓中樓頁數,每頁10條,用於遍歷樓中樓的每一頁

                for i in range(1,tpage+1):

                    replyurl="https://tieba.baidu.com/p/comment?tid="+tid+"&pid="+pid+"&pn="+str(i) #構造樓中樓url

                    htmlreply=get_html(replyurl)

                    replyresult=re.findall(r'<span class="lzl_content_main">(.*?)</span>', htmlreply,re.S) #獲取樓中樓的評論內容

                    for reply in replyresult:

                        pagestr=pagestr+reg.sub('',reply)+"\n"

        return pagestr

    #爬取一個帖子,入參爲帖子後綴url,以及貼吧名

    def savepost(self,url,barname):

        tid=url.replace("/p/","")

        filename = "E:/tieba/"+barname+"/"+tid+".txt" #文件保存路徑

        if os.path.exists(filename): #判斷是否已經爬取過當前帖子

            return

        print(baseurl+url)

        try:

            html=get_html(baseurl+url)

            findreault = re.findall(r'([0-9]*)</span>頁', html,re.S) #獲取當前帖子頁數

            numpage=findreault[0]

            poststr=self.getpagestr(baseurl+url,tid) #獲取第一頁

            if int(numpage)>1:

                for i in range(2,int(numpage)+1): 

                    tempurl=baseurl+url+"?pn="+str(i) #構造每一頁的url,循環獲取每一頁

                    pagestr=self.getpagestr(tempurl,tid)

                    poststr=poststr+pagestr

            with open(filename,'w',encoding="utf-8") as f: #寫文件

                f.write(poststr)

        except:

            print("get %s failed"%(baseurl+url))

            pass


if __name__ == '__main__':

    starttime = datetime.datetime.now()

    testurl="/f?kw=%E8%8B%B1%E9%9B%84%E6%97%A0%E6%95%8C3&fr=index&fp=0&ie=utf-8"

    barname="英雄無敵3"

    html=get_html(baseurl+testurl)

    numpost=re.findall(r'共有主題數<span class="red_text">([0-9]*?)</span>個', html,re.S)[0] #獲取帖子總數

    numpage=math.ceil(int(numpost)/50) #計算頁數

    path = "E:/tieba/"+barname

    folder=os.path.exists(path)

    if not folder:

        os.makedirs(path)

    for i in range(3): #創建獲取帖子鏈接的線程

        t=getlinkthread(i,10,3,30,testurl)

        t.start()

    for j in range(3): #創建解析帖子鏈接的線程

        t1=parselinkthread(j,barname)

        t1.start()

    t1.join()

    endtime = datetime.datetime.now()

    print(endtime-starttime)

然後用requests加xpath寫了一版:

# -*- coding: utf-8 -*-

import requests

from lxml import etree

import re

import queue

import os

import math

import threading

import datetime

from time import sleep


baseurl="https://tieba.baidu.com" #貼吧頁面url的通用前綴

q=queue.Queue() #保存帖子鏈接的隊列

MAX_WAIT=10 #解析線程的最大等待時間

reg=re.compile('<[^>]*>') #去除html標籤的正則表達式


#封裝的獲取etree對象的函數

def get_url_text(url):

    response=requests.get(url)

    return etree.HTML(response.text)


#封裝的獲取json對象的函數

def get_url_json(url):

    response=requests.get(url)

    return response.json()


#封裝的通過xpath解析的函數

def parse_html(html,xpathstr):

    result = html.xpath(xpathstr)

    return result


#採集url的線程,thnum線程id,startpage開始採集的頁數,step單個線程採集頁數間隔(與線程個數相同),maxpage採集結束的頁數,url採集的貼吧的url後綴

class getlinkthread(threading.Thread):

    def __init__(self,thnum,startpage,step,maxpage,url):

        threading.Thread.__init__(self)

        self.thnum=thnum

        self.startpage=startpage

        self.step=step

        self.maxpage=maxpage

        self.url=url

    def run(self):

        mm=math.ceil((self.maxpage-self.startpage)/self.step) #計算循環的範圍

        for i in range(0,mm):

            startnum=self.startpage+self.thnum+i*self.step #開始頁數

            tempurl=baseurl+self.url+"&pn="+str(startnum*50) #構造每一頁的url

            print("Thread %s is getting %s"%(self.thnum,tempurl))

            try:

                temphtml=get_url_text(tempurl)

                turls = parse_html(temphtml, '//*[@class="threadlist_title pull_left j_th_tit "]/a/@href') #通過xpath解析,獲取當前頁所有帖子的url後綴

                for tu in turls: #入隊列

                    q.put(tu)

            except:

                print("%s get failed"%(tempurl))

                pass

            sleep(1)


#解析url的線程,thrnum線程id,barname貼吧名,用來構造文件保存路徑

class parselinkthread(threading.Thread):

    def __init__(self,thrnum,barname):

        threading.Thread.__init__(self)

        self.thrnum=thrnum

        self.barname=barname

    def run(self):

        waittime=0

        while True:

            if q.empty() and waittime<MAX_WAIT: #隊列爲空且等待沒有超過MAX_WAIT時,繼續等待

                sleep(1)

                waittime=waittime+1

                print("Thr %s wait for %s secs"%(self.thrnum,waittime))

            elif waittime>=MAX_WAIT: #等待超過MAX_WAIT時,線程退出

                print("Thr %s quit"%(self.thrnum))

                break

            else: #隊列不爲空時,重置等待時間,從隊列中取帖子url,進行解析

                waittime=0

                item=q.get()

                self.dotask(item)

    def dotask(self,item):

        print("Thr %s is collecting %s"%(self.thrnum,item))

        tid=item.replace("/p/","") #獲取帖子的id,後面構造樓中樓url以及保存文件時用到

        filename = "E:/tieba/"+barname+"/"+tid+".txt" #文件保存路徑

        if os.path.exists(filename): #判斷是否已經爬取過當前帖子

            return

        print(baseurl+item)

        try:

            html=get_url_text(baseurl+item)

            findreault = parse_html(html, '//*[@id="thread_theme_5"]/div[1]/ul/li[2]/span[2]/text()') #獲取當前帖子頁數

            numpage=int(findreault[0])

            poststr=self.getpagestr(baseurl+item,tid,1) #獲取第一頁的內容

            if numpage>1:

                for i in range(2,numpage+1): 

                    tempurl=baseurl+item+"?pn="+str(i) #構造每一頁的url,循環獲取每一頁

                    pagestr=self.getpagestr(tempurl,tid,i)

                    poststr=poststr+pagestr

            poststr= reg.sub('',poststr) #正則表達式去除html標籤

            with open(filename,'w',encoding="utf-8") as f: #寫文件

                f.write(poststr)

        except:

            print("Thr %s get %s failed"%(self.thrnum,baseurl+item))

            pass

    #抓取一頁的內容,包括帖子及樓中樓,入參爲頁面url和帖子id,返回值爲帖子的內容字符串

    def getpagestr(self,url,tid,pagenum):

        html=get_url_text(url)

        lzlurl=baseurl+"/p/totalComment?tid="+tid+"&pn="+str(pagenum)+"&see_lz=0" #構造樓中樓url

        jsonstr=get_url_json(lzlurl) #正常一頁能看到的樓中樓的內容返回爲json格式,如果有樓中樓層數大於10的,需要通過其他格式的url獲取樓中樓10層以後的內容

        result1 = parse_html(html,'//*[@class="d_post_content j_d_post_content "]/text()') #xpath解析返回樓中樓內容

        pagestr=""

        for res in result1:

            pagestr=pagestr+res+"\n"  #先整合帖子內容

        if jsonstr['data']['comment_list']!=[]: #如果某頁沒有樓中樓,返回是空的list,不加判斷的話會報錯

            for key,val in jsonstr['data']['comment_list'].items(): #循環獲取每層樓中樓的內容,key是樓中樓id,val爲包含樓中樓層數、內容等信息的字典

                lzlid=key

                lzlnum=int(val['comment_num'])

                tpage=math.ceil(lzlnum/10) #計算樓中樓的頁數

                for cominfo in val['comment_info']:

                    pagestr=pagestr+cominfo['content']+"\n"

                if tpage>1: #樓中樓超過1頁時,需要構造第二頁及以後的樓中樓url

                    for i in range(1,tpage+1):

                        replyurl="https://tieba.baidu.com/p/comment?tid="+tid+"&pid="+lzlid+"&pn="+str(i) #構造樓中樓url

                        htmlreply=get_url_text(replyurl)

                        replyresult=parse_html(htmlreply, '/html/body/li/div/span/text()') #獲取樓中樓的評論內容

                        for reply in replyresult:

                            pagestr=pagestr+reply+"\n"

        return pagestr


if __name__ == '__main__':

    starttime = datetime.datetime.now()

    testurl="/f?ie=utf-8&kw=%E8%8B%B1%E9%9B%84%E6%97%A0%E6%95%8C3&fr=search"

    barname="英雄無敵3"

    html=get_url_text(baseurl+testurl)

    findreault = parse_html(html, '//*[@class="th_footer_l"]/span[1]/text()') #獲取當前帖子頁數

    numpost=int(findreault[0])

    numpage=math.ceil(int(numpost)/50) #計算頁數

    path = "E:/tieba/"+barname

    folder=os.path.exists(path)

    if not folder:

        os.makedirs(path)

    for i in range(3): #創建獲取帖子鏈接的線程

        t=getlinkthread(i,10,3,30,testurl)

        t.start()

    for j in range(3): #創建解析帖子鏈接的線程

        t1=parselinkthread(j,barname)

        t1.start()

    t1.join()

    endtime = datetime.datetime.now()

    print(endtime-starttime)


執行的結果:

方法1:urllib+正則執行時間:0:32:22.223089,爬下來984個帖子,失敗9個帖子

方法2:requests+xpath執行時間:0:21:42.239483,爬下來993個帖子,失敗0個帖子

結果與經驗不同!後來想了一下,可能是因爲對樓中樓的爬取方式不同,方法1中對每一個樓中樓每一頁都要請求一次url,因爲當時不會用瀏覽器F12工具,樓中樓的url格式是百度查到的。。。在寫方法2時用F12工具抓到了第一頁樓中樓的url,就是返回json的那個,這樣如果樓中樓層數不超過10的話,每一頁帖子的樓中樓只需要請求一次,只有超過10層的樓中樓才需要用方法1中的url進行爬取,這樣效率就高了許多。這樣看來,這個測試不是很合理。

分享一點經驗:

1、就個人感覺來說,正則比xpath好用,只要找到html中的特定格式就行了,不過似乎容錯差一點,方法1失敗的9個帖子可能就是因爲個別帖子html格式與其他不同導致正則匹配不到;

2、requests比urllib好用,尤其對於返回json格式的url,字典操作感覺比返回字符串做正則匹配要方便;

3、pip裝lxml的時候報錯,提示Cannot open include file: 'libxml/xpath.h': No such file or directory,以及沒有安裝libxml2,後來百度到https://www.cnblogs.com/caochuangui/p/5980469.html這個文章的方法,安裝成功

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章