python上一篇写了怎样获取整本小说并写入文件,但是速度实在太慢,这一篇我们使用多线程来提高爬取速度 一:多线程模块 threading创建线程 创建线程有两种方法
第一种是继承threading.Thread类,并重写它的init和run函数
代码如下:
import threading
class gettext(threading.Thread):
def __init__(self,threadName):
#调用父类的构造函数
threading.Thread.__init__(self)
self.threadName = threadName
def run(self):
print(self.threadName)
这里的self是一个指向当前对象的指针,用它来进行初始化和调用成员变量 将类定义好后,就可以创建线程了,代码如下: #保存所有线程的列表
threads = []
#创建5个线程
for i in range(5):
#创建一个线程
thread = gettext('Thread-'+str(i))
#将创建好的线程添加到线程列表
threads.append(thread)
#启动线程
thread.start()
#等待所有线程结束
for t in threads:
t.join()
print('线程结束')
运行结果:

这和预期的输出有点不同:

这是由于线程不同步造成的,在程序中输出流只有一个,但是线程却有多个,而且线程又是同时运行的,所以就会出现多个线程同时访问同一个资源的情况。 所以在使用多线程的时候,我们要对共有资源的访问加以限制,使得同时只能有一个线程访问。就像一把锁一样,在有线程访问资源的时候把资源锁起来不让其他线程访问,等到当前线程结束对共有资源的操作后再把锁打开,让其他线程可以访问。 如果多个线程共同对某个数据修改,则可能出现不可预料的结果,为了保证数据的正确性,需要对多个线程进行同步。 使用 Thread 对象的 Lock 和 Rlock 可以实现简单的线程同步,这两个对象都有 acquire 方法和 release
方法,对于那些需要每次只允许一个线程操作的数据,可以将其操作放到 acquire 和 release 方法之间。如下: 多线程的优势在于可以同时运行多个任务(至少感觉起来是这样)。但是当线程需要共享数据时,可能存在数据不同步的问题。 考虑这样一种情况:一个列表里所有元素都是0,线程”set”从后向前把所有元素改成1,而线程”print”负责从前往后读取列表并打印。 那么,可能线程”set”开始改的时候,线程”print”便来打印列表了,输出就成了一半0一半1,这就是数据的不同步。为了避免这种情况,引入了锁的概念。 锁有两种状态——锁定和未锁定。每当一个线程比如”set”要访问共享数据时,必须先获得锁定;如果已经有别的线程比如”print”获得锁定了,那么就让线程”set”暂停,也就是同步阻塞;等到线程”print”访问完毕,释放锁以后,再让线程”set”继续。 经过这样的处理,打印列表时要么全部输出0,要么全部输出1,不会再出现一半0一半1的尴尬场面。
这里的锁就是threading的Lock方法,代码如下: import threading
class gettext(threading.Thread):
def __init__(self,threadName,lock):
threading.Thread.__init__(self)
self.threadName = threadName
self.lock = lock
def run(self):
#把共有资源锁起来
self.lock.acquire()
print(self.threadName)
#操作完成后打开锁
self.lock.release()
#保存所有线程的列表
threads = []
#创建锁
lock = threading.RLock()
#创建5个线程
for i in range(5):
#创建一个线程
thread = gettext('Thread-'+str(i),lock)
#将创建好的线程添加到线程列表
threads.append(thread)
#启动线程
thread.start()
#等待所有线程结束
for t in threads:
t.join()
print('线程结束')
这样输出就和预期的一样了 第二种创建线程的方法称为函数式, 这个非常简单,详情请查看:菜鸟教程 | | python3 多线程 接下来开始敲代码 二:多线程获取单本小说首先我们要将每个章节进行编号(因为多线程并不会按照列表的顺序来获取章节,所以我们将每个章节进行编号,写入文件的时候就不会乱序了) 将获取章节列表部分的代码修改为: '''***************************获取章节列表**********************************'''
chapter_list = []
i = 1
while 1:
#获取网页
#更改编码
#获得BeautifulSoup对象
#获取章节列表
#url + str(i)第i页的url
r = requests.get(url + str(i),params=re_header)
r.encoding = 'gbk'
soup = BeautifulSoup(r.text,"html.parser")
i+=1
print(url + str(i))
temp_list = soup.select('.last9 a')
for t in range(len(temp_list)):
temp_list[t] = temp_list[t]['href']
del temp_list[0]
if(len(temp_list)==0):
break
chapter_list.extend(temp_list)
for i in range(len(chapter_list)):
chapter_list[i] = 'http://m.' + chapter_list[i]
#对每个章节编号
chapter_list[i] = [i,chapter_list[i]]
print(chapter_list[i])
后面的代码也要进行相应的修改,现在章节链接是chapter_list[ i ][1] 线程类的代码,这里本来需要用queue队列的,但是那样会使得程序运行变慢,所以我直接用了列表的pop方法,先把chapter_list列表用reverse方法反向,然后再用pop方法逐个取出,效果和queue队列一样,还省去了put的过程,代码如下: 线程类和储存类的定义:class gettext(threading.Thread):
def __init__(self,chapter_list,book,lock,folock,re_header):
threading.Thread.__init__(self)
self.chapter_list = chapter_list
self.book = book
self.lock = lock
self.folock = folock
self.re_header = re_header
self.exitflag = False
def run(self):
while not self.exitflag:
#把共有资源锁起来
self.lock.acquire()
if len(self.chapter_list) != 0:
data = self.chapter_list.pop()
#获得链接后打开锁
self.lock.release()
#获取第一页页面
r = requests.get(data[1],params=re_header)
r.encoding = 'gbk'
soup = BeautifulSoup(r.text,"html.parser")
#这个网站把每个章节分为两页,要分两次获取
#获取章节名和第一页的内容
title = soup.select('.nr_title')[0].text
content_1 = soup.select('#nr1')[0].text
#获取第二页页面
r = requests.get(data[1].replace('.html','_2.html'),params=re_header)
r.encoding = 'gbk'
soup = BeautifulSoup(r.text,"html.parser")
#第二部分章节内容
content_2 = soup.select('#nr1')[0].text
#拼接两部分内容 详细请搜索字符串join方法
str1 = ''
chapter_content = str1.join([content_1,content_2])
self.lock.acquire()
#将章节内容放进储存对象中
self.book.put(data[0],title,chapter_content)
print(title)
self.lock.release()
else:
#chapter_list长度为0时退出线程
self.exitflag = True
self.lock.release()
#定义一个storage类用来暂时储存小说内容
class storage:
def __init__(self):
self.content = []
def put(self,index,title,content):
self.content.extend([[index,title,content]])
线程的创建和启动:#反向列表
chapter_list.reverse()
'''*************************************线程*******************************'''
#保存小说内容的对象
book = storage()
#保存所有线程的列表
threads = []
#创建锁
lock = threading.RLock()
folock = threading.RLock()
#创建10个线程
for i in range(10):
#创建一个线程
thread = gettext(chapter_list,book,lock,folock,re_header)
#将创建好的线程添加到线程列表
threads.append(thread)
#启动线程
thread.start()
#等待所有线程结束
for t in threads:
t.join()
print('线程结束')
当线程全部结束时,小说的内容就保存在book对象中了,不过book对象中的章节全是乱序的,所以我们要根据每个章节的编号对book内的内容重新进行一次排序,这里我们用到的是桶排序: #定义一个桶数组
novel = [['a','a']]*len(book.content)
for t in book.content:
index = t[0]
#因为python中列表是按引用传递的,所以这里我们传递的只是地址
novel[index][0] = t
排序好后写入文件 #打开/创建文件
fo = open('1.txt','wb')
for t in novel:
title = t[1]
chapter_content = t[2]
#写入章节名和内容
fo.write((title).encode('utf-8'))
fo.write((chapter_content).encode('utf-8'))
#打印提示
print(title + '已下载')
#关闭文件
fo.close()
print('下载成功')
因为网站服务器的缘故,访问的速度太快的话,有少量的请求会失败,所以我们还要在获取请求的时候对请求失败的情况做出处理。这里用到状态响应码r.status_code,当成功访问时状态响应码为200。这里只修改了gettext类的部分代码: class gettext(threading.Thread):
def __init__(self,chapter_list,book,lock,folock,re_header):
threading.Thread.__init__(self)
self.chapter_list = chapter_list
self.book = book
self.lock = lock
self.folock = folock
self.re_header = re_header
self.exitflag = False
def run(self):
while not self.exitflag:
#把共有资源锁起来
self.lock.acquire()
if len(self.chapter_list) != 0:
data = self.chapter_list.pop()
#获得链接后打开锁
self.lock.release()
#获取第一页页面
while 1:
r = requests.get(data[1],params=re_header)
#成功获得页面跳出循环,否则继续
if r.status_code == 200:
break
r.encoding = 'gbk'
soup = BeautifulSoup(r.text,"html.parser")
#这个网站把每个章节分为两页,要分两次获取
#获取章节名和第一页的内容
title = soup.select('.nr_title')[0].text
content_1 = soup.select('#nr1')[0].text
time.sleep(0.1)
#获取第二页页面
while 1:
r = requests.get(data[1].replace('.html','_2.html'),params=re_header)
#成功获得页面跳出循环,否则继续
if r.status_code == 200:
break
r.encoding = 'gbk'
soup = BeautifulSoup(r.text,"html.parser")
#第二部分章节内容
content_2 = soup.select('#nr1')[0].text
#拼接两部分内容 详细请搜索字符串join方法
str1 = ''
chapter_content = str1.join([content_1,content_2])
self.folock.acquire()
#将章节内容放进储存对象中
self.book.put(data[0],title,chapter_content)
print(title)
self.folock.release()
else:
#chapter_list长度为0时退出线程
self.exitflag = True
self.lock.release()
|