您好,登錄后才能下訂單哦!
自學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這個文章的方法,安裝成功
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。