自學(xué)python有一段時間了,做過的東西還不多,最近開始研究爬蟲,想自己寫一個爬百度貼吧的帖子內(nèi)容,然后對帖子做分詞和詞頻統(tǒng)計,看看這個吧熱議的關(guān)鍵詞都有哪些。百度了好多資料和視頻,學(xué)到了不少東西,但也生出了一些問題:
成都創(chuàng)新互聯(lián)是一家專注于做網(wǎng)站、網(wǎng)站制作與策劃設(shè)計,武陵源網(wǎng)站建設(shè)哪家好?成都創(chuàng)新互聯(lián)做網(wǎng)站,專注于網(wǎng)站建設(shè)十多年,網(wǎng)設(shè)計領(lǐng)域的專業(yè)建站公司;建站業(yè)務(wù)涵蓋:武陵源等地區(qū)。武陵源做網(wǎng)站價格咨詢:18980820575
1、http請求用python自帶的urllib,也可以用requests,哪個更好用?
2、html解析可以用正則表達式,也可以用xpath,哪個效率更高?
根據(jù)網(wǎng)上資料的說法,requests相對更好用,因為很多功能已經(jīng)封裝好了,性能上與urllib也沒什么區(qū)別,而正則表達式通常要比xpath效率更高。不過實踐出真知,分別用兩種方式寫出來然后對比一下。爬取的目標(biāo)是我很喜歡的一個游戲——英雄無敵3的貼吧,從第10頁爬到30頁,只爬帖子、回帖以及樓中樓內(nèi)容的文字部分。首先用建議初學(xué)者使用的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標(biāo)簽的正則表達式
#封裝的獲取html字符串的函數(shù)
def get_html(url):
response=request.urlopen(url)
html=response.read().decode('utf-8')
return html
#采集url的線程,thnum線程id,startpage開始采集的頁數(shù),step單個線程采集頁數(shù)間隔(與線程個數(shù)相同),maxpage采集結(jié)束的頁數(shù),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) #計算循環(huán)的范圍
for i in range(0,mm):
startnum=self.startpage+self.thnum+i*self.step #開始頁數(shù)
tempurl=baseurl+self.url+"&pn="+str(startnum*50) #構(gòu)造每一頁的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) #獲取當(dāng)前頁的所有帖子鏈接
for tu in turls: #入隊列
q.put(tu)
except:
print("%s get failed"%(tempurl))
pass
sleep(1)
#解析url的線程,thrnum線程id,barname貼吧名,用來構(gòu)造文件保存路徑
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 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) #抓取一頁的內(nèi)容,包括帖子及樓中樓,入?yún)轫撁鎢rl和帖子id,返回值為帖子的內(nèi)容字符串 def getpagestr(self,url,tid): html=get_html(url) result1 = re.findall(r'class="d_post_content j_d_post_content ">(.*?)
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" #先整合帖子內(nèi)容
for res in result2:
if 'null' not in res: #若有樓中樓,即層數(shù)不為null
pid=res.split(",")[0].split(":")[1] #樓中樓id
numreply=int(res.split(",")[1].split(":")[1]) #樓中樓層數(shù)
tpage=math.ceil(numreply/10) #計算樓中樓頁數(shù),每頁10條,用于遍歷樓中樓的每一頁
for i in range(1,tpage+1):
replyurl="https://tieba.baidu.com/p/comment?tid="+tid+"&pid="+pid+"&pn="+str(i) #構(gòu)造樓中樓url
htmlreply=get_html(replyurl)
replyresult=re.findall(r'(.*?)', htmlreply,re.S) #獲取樓中樓的評論內(nèi)容
for reply in replyresult:
pagestr=pagestr+reg.sub('',reply)+"\n"
return pagestr
#爬取一個帖子,入?yún)樘雍缶Yurl,以及貼吧名
def savepost(self,url,barname):
tid=url.replace("/p/","")
filename = "E:/tieba/"+barname+"/"+tid+".txt" #文件保存路徑
if os.path.exists(filename): #判斷是否已經(jīng)爬取過當(dāng)前帖子
return
print(baseurl+url)
try:
html=get_html(baseurl+url)
findreault = re.findall(r'([0-9]*)頁', html,re.S) #獲取當(dāng)前帖子頁數(shù)
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) #構(gòu)造每一頁的url,循環(huán)獲取每一頁
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'共有主題數(shù)([0-9]*?)個', html,re.S)[0] #獲取帖子總數(shù)
numpage=math.ceil(int(numpost)/50) #計算頁數(shù)
path = "E:/tieba/"+barname
folder=os.path.exists(path)
if not folder:
os.makedirs(path)
for i in range(3): #創(chuàng)建獲取帖子鏈接的線程
t=getlinkthread(i,10,3,30,testurl)
t.start()
for j in range(3): #創(chuàng)建解析帖子鏈接的線程
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標(biāo)簽的正則表達式
#封裝的獲取etree對象的函數(shù)
def get_url_text(url):
response=requests.get(url)
return etree.HTML(response.text)
#封裝的獲取json對象的函數(shù)
def get_url_json(url):
response=requests.get(url)
return response.json()
#封裝的通過xpath解析的函數(shù)
def parse_html(html,xpathstr):
result = html.xpath(xpathstr)
return result
#采集url的線程,thnum線程id,startpage開始采集的頁數(shù),step單個線程采集頁數(shù)間隔(與線程個數(shù)相同),maxpage采集結(jié)束的頁數(shù),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) #計算循環(huán)的范圍
for i in range(0,mm):
startnum=self.startpage+self.thnum+i*self.step #開始頁數(shù)
tempurl=baseurl+self.url+"&pn="+str(startnum*50) #構(gòu)造每一頁的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解析,獲取當(dāng)前頁所有帖子的url后綴
for tu in turls: #入隊列
q.put(tu)
except:
print("%s get failed"%(tempurl))
pass
sleep(1)
#解析url的線程,thrnum線程id,barname貼吧名,用來構(gòu)造文件保存路徑
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 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,后面構(gòu)造樓中樓url以及保存文件時用到 filename = "E:/tieba/"+barname+"/"+tid+".txt" #文件保存路徑 if os.path.exists(filename): #判斷是否已經(jīng)爬取過當(dāng)前帖子 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()') #獲取當(dāng)前帖子頁數(shù) numpage=int(findreault[0]) poststr=self.getpagestr(baseurl+item,tid,1) #獲取第一頁的內(nèi)容 if numpage>1: for i in range(2,numpage+1): tempurl=baseurl+item+"?pn="+str(i) #構(gòu)造每一頁的url,循環(huán)獲取每一頁 pagestr=self.getpagestr(tempurl,tid,i) poststr=poststr+pagestr poststr= reg.sub('',poststr) #正則表達式去除html標(biāo)簽 with open(filename,'w',encoding="utf-8") as f: #寫文件 f.write(poststr) except: print("Thr %s get %s failed"%(self.thrnum,baseurl+item)) pass #抓取一頁的內(nèi)容,包括帖子及樓中樓,入?yún)轫撁鎢rl和帖子id,返回值為帖子的內(nèi)容字符串 def getpagestr(self,url,tid,pagenum): html=get_url_text(url) lzlurl=baseurl+"/p/totalComment?tid="+tid+"&pn="+str(pagenum)+"&see_lz=0" #構(gòu)造樓中樓url jsonstr=get_url_json(lzlurl) #正常一頁能看到的樓中樓的內(nèi)容返回為json格式,如果有樓中樓層數(shù)大于10的,需要通過其他格式的url獲取樓中樓10層以后的內(nèi)容 result1 = parse_html(html,'//*[@class="d_post_content j_d_post_content "]/text()') #xpath解析返回樓中樓內(nèi)容 pagestr="" for res in result1: pagestr=pagestr+res+"\n" #先整合帖子內(nèi)容 if jsonstr['data']['comment_list']!=[]: #如果某頁沒有樓中樓,返回是空的list,不加判斷的話會報錯 for key,val in jsonstr['data']['comment_list'].items(): #循環(huán)獲取每層樓中樓的內(nèi)容,key是樓中樓id,val為包含樓中樓層數(shù)、內(nèi)容等信息的字典 lzlid=key lzlnum=int(val['comment_num']) tpage=math.ceil(lzlnum/10) #計算樓中樓的頁數(shù) for cominfo in val['comment_info']: pagestr=pagestr+cominfo['content']+"\n" if tpage>1: #樓中樓超過1頁時,需要構(gòu)造第二頁及以后的樓中樓url for i in range(1,tpage+1): replyurl="https://tieba.baidu.com/p/comment?tid="+tid+"&pid="+lzlid+"&pn="+str(i) #構(gòu)造樓中樓url htmlreply=get_url_text(replyurl) replyresult=parse_html(htmlreply, '/html/body/li/div/span/text()') #獲取樓中樓的評論內(nèi)容 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()') #獲取當(dāng)前帖子頁數(shù) numpost=int(findreault[0]) numpage=math.ceil(int(numpost)/50) #計算頁數(shù) path = "E:/tieba/"+barname folder=os.path.exists(path) if not folder: os.makedirs(path) for i in range(3): #創(chuàng)建獲取帖子鏈接的線程 t=getlinkthread(i,10,3,30,testurl) t.start() for j in range(3): #創(chuàng)建解析帖子鏈接的線程 t1=parselinkthread(j,barname) t1.start() t1.join() endtime = datetime.datetime.now() print(endtime-starttime) 執(zhí)行的結(jié)果: 方法1:urllib+正則執(zhí)行時間:0:32:22.223089,爬下來984個帖子,失敗9個帖子 方法2:requests+xpath執(zhí)行時間:0:21:42.239483,爬下來993個帖子,失敗0個帖子 結(jié)果與經(jīng)驗不同!后來想了一下,可能是因為對樓中樓的爬取方式不同,方法1中對每一個樓中樓每一頁都要請求一次url,因為當(dāng)時不會用瀏覽器F12工具,樓中樓的url格式是百度查到的。。。在寫方法2時用F12工具抓到了第一頁樓中樓的url,就是返回json的那個,這樣如果樓中樓層數(shù)不超過10的話,每一頁帖子的樓中樓只需要請求一次,只有超過10層的樓中樓才需要用方法1中的url進行爬取,這樣效率就高了許多。這樣看來,這個測試不是很合理。 分享一點經(jīng)驗: 1、就個人感覺來說,正則比xpath好用,只要找到html中的特定格式就行了,不過似乎容錯差一點,方法1失敗的9個帖子可能就是因為個別帖子html格式與其他不同導(dǎo)致正則匹配不到; 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這個文章的方法,安裝成功
分享名稱:爬蟲試手——百度貼吧爬蟲
當(dāng)前地址:http://weahome.cn/article/ihchcp.html