自学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这个文章的方法,安装成功
原文地址:http://blog.51cto.com/13904513/2153384