文章目录
- 前言
- 代码
- 引入库
- 定义线程类
- 开始线程
- 进度条
- 选择请求头
- 发起请求
- 查找列表
- 获取列表
- 查找元素
- 写入文件
- 主函数
- 执行
- 排序
- 结果
- 总结
前言
对Python爬虫练习(一)作出改进,增加了多线程,使用re代替BeautifulSoup查找,提高爬取速度,改进了进度条,及时捕获异常,防止中途换行。
代码
引入库
import requests
from bs4 import BeautifulSoup
import re
import threading #线程
import queue #队列
import time
import random
定义线程类
创建线程后执行WriteItem()函数查找元素并写入文件。
class myThread(threading.Thread):
def __init__(self,name,q):
threading.Thread.__init__(self)
self.name=name
self.q=q
def run(self):
WriteItem(self.name,self.q)
开始线程
创建10个线程,若URL列表小于10,则创建列表长度个线程。
def StartThread():
workqueue=queue.Queue(number)
for i in range(len(ls)):
workqueue.put(ls[i])
for i in range(min(10,len(ls))):
thread=myThread('T-'+str(i+1),workqueue)
thread.start()
threads.append(thread)
for thread in threads:
thread.join()
进度条
用来展示进度,接收info信息,展示线程名称、状态和捕获到的异常。
def Print(i,info):
global start
p=round(30*i/len(ls))
p1='█'*p
p2='—'*(30-p)
dur=time.perf_counter()-start
print("\r{}{} {}/{}".format(p1,p2,i,len(ls)),end=' ')
print("{:.1%}".format(i/len(ls)),end=' ')
print("用时{:.2f}s".format(dur),end=' ')
print("状态:{}".format(info),end=' ')
选择请求头
储存请求头。
def GetAgent():
agent=[ 'Opera/8.0 (Windows NT 5.1; U; en)',\
'UCWEB7.0.2.37/28/999',\
'NOKIA5700/ UCWEB7.0.2.37/28/999',\
'Openwave/ UCWEB7.0.2.37/28/999']
return agent
发起请求
返回响应,若超时,则返回空。
def GetHtml(url):
try:
r=requests.get(url,headers={'User-Agent':random.choice(GetAgent())},timeout=30)
r.raise_for_status()
return r.text
except:
return ''
查找列表
返回URL列表。
def FindList(number,demo):
soup=BeautifulSoup(demo,'html.parser')
flag=0
for tag in soup('a'):
try:
s=re.match(r'[\u4e00-\u9fff]{2,5}',tag.attrs['title']).group(0)
if s=='妙蛙种子':
flag=1
if re.match(r'第.世代',s):
continue
if flag:
ls.append('https://wiki.52poke.com/wiki/'+s)
if len(ls)==number:
break
except:
continue
获取列表
尝试三次,若列表获取失败,抛出异常,结束程序。
def GetList(number):
url="https://wiki.52poke.com/wiki/宝可梦列表%EF%BC%88按全国图鉴编号%EF%BC%89/简单版"
for i in range(3):
FindList(number,GetHtml(url))
if len(ls)==number:
print('列表提取成功')
break
else:
print('列表提取失败{}/3'.format(str(i+1)))
else:
raise Exception('列表提取失败')
查找元素
用re替代BeautifulSoup,减少查找时间,分别进行try-except,查找失败则返回"–"。
def FindItem(demo):
try:
num=re.search(r'title="宝可梦列表(按全国图鉴编号)">#[0-9]{3}',demo).group(0)
num=num.split(">")[1]
except:
num="--"
try:
atrs=re.search(r'roundy a-r at-c .+',demo).group(0).split()
atr1=atrs[3].split('-')[-1]
atr2=atrs[4][:-1].split('-')[-1]
if atr1==atr2:
atr=atr1
else :
atr=atr1+','+atr2
except:
atr="--"
try:
cla=re.search(r"<td class=\"roundy b.+ bgwhite bw.+\">[\u4e00-\u9fff]{1,4}宝可梦",demo).group(0)
cla=cla.split(">")[1]
except Exception as e:
cla="--"
print(e)
return num,atr,cla
写入文件
多线程查找,写入,使用线程锁将公共资源(全局变量,输出,写入)夹住,通过sleep控制爬取频率,将连接失败和查找失败的异常分开,记录未成功写入的URL链接,通过进度条函数展示信息。
def WriteItem (name,q):
global f,suc,count
while True:
try:
url=q.get(timeout=2)
except:
break
try:
demo=GetHtml(url)
if demo=='':
raise Exception('连接失败')
num,atr,cla=FindItem(demo)
if atr=='--' and cla=='--':
raise Exception('查找失败')
nam=url.split('/')[-1]
pa='{0:{4:}<4}{1:{4:}^10}{2:{4:}^9}{3:{4:}^10}'
lock.acquire()
f.write('\n'+pa.format(num,nam,atr,cla,chr(12288)))
suc+=1
count+=1
info='{}:查找成功,已写入{}项'.format(name,suc)+' '*5
Print(count,info)
lock.release()
time.sleep(random.random()*5)
except Exception as e:
lock.acquire()
lt.append(url)
count+=1
info=name+':Error:'+str(e)+' '*5
Print(count,info)
lock.release()
主函数
每轮查找过后,重新查找失败的链接,直到所有链接成功或完成五轮查找。
def main():
global f,suc,sucs,count,ls,lt
pa='{0:{4:}<4}{1:{4:}^10}{2:{4:}^9}{3:{4:}^10}'
#写入文件
f.write(pa.format('序号','名称','属性','分类',chr(12288)))
for i in range(5):
print("第{}次查找".format(i+1))
StartThread()
print('')
sucs+=suc
if len(lt)==0:
break
ls=lt
lt=[]
suc=0
count=0
执行
初始化全局变量,先获得URL列表,再执行主函数。
number=eval(input("输入查找数量:"))
start=time.perf_counter() #开始计时
lock=threading.Lock() #定义线程锁
ls=[] #URL列表
lt=[] #查找失败的URL
threads=[] #线程列表
suc=0 #单轮查找成功的数量
sucs=0 #查找成功的总量
count=0 #查找进度
GetList(number)
path='C:/Users/lenovo/Desktop/py/其他/pokemon-m.txt'
f=open(path,'w')
main()
f.close()
dur=time.perf_counter()-start
print('程序结束')
print('共写入{}项,失败{}项,用时{:.2f}s'.format(sucs,number-sucs,dur))
time.sleep(10)
排序
多线程爬取返回的结果为乱序,重新排序的方法与Python爬虫练习(二)中的相同,不再赘述。
结果
运行情况
运行结果
排序结果
总结
requests使用多线程之后效率得到了很大的提升,使用多线程时应注意用线程锁将公共资源夹起,防止各线程之间相互干扰。使用多线程在爬取多页面时速度可以与scrapy相当,但应注意爬取频率,过高的频率可能导致查找失败甚至ip被封,在调试时我就被封了三个小时。线程数目的选择对爬虫效率也有影响。