多线程案例------多线程爬取小说
- 生产者------------------产生URL
- 消费者兼生产者------下载小说
- 消费者------------------合并小说
- 主函数------------------函数入口
注意事项
这里我们使用了队列queue来储存URL,需要提取导入一下队列,我们在主函数中让队列实例化,指定大小使用maxsize参数
代码展示
python
# 导入库
import re
import time
import requests
import threading
import queue
import os
from lxml import etree
lock = threading.Lock()
end_work = False # 判断生产者是否完成工作
novel_title = ''
total_numbers = 0 # 给合并板块的线程一个参考的范围,这里是为了告诉它一共有多少个文件
# 生产者------------产生URL
def get_url(index_url,q):
global novel_title,end_work,total_numbers
# 从目录页获取总页数和第一页的网址格式
rt = requests.get(url=index_url)
# print(rt.text)
html = etree.HTML(rt.text)
# 创建文件夹,放小说
novel_title = html.xpath('//div[@class="infotitle"]//h1//text()')[1] # 拿到小说名称
if not os.path.exists(f'/{novel_title}'): # 创建小说文件夹
os.mkdir(f'/{novel_title}')
result1 = html.xpath('//div[@class="tag"]/a/@href') # 得到页数所在的标签
Total_pages = re.findall('/read_(\\d+).html',result1[0])[0] # 获取标签里面的页数信息
# Total_pages = 10 # 为了展示效果,这个Total_pages暂且设为10 ,把这条屏蔽就是全部小说了
total_numbers = Total_pages # 把总页数传给合并线程
# print(Total_pages)
for link in range(1, int(Total_pages) + 1): # 因为是左闭右开,所以右边加一
url = re.sub(f'/read_.*',f'/read_{link}.html',result1[0])
q.put(url)
# print(url)
# 完成所有URL的制作
print('生产者URL完成工作')
end_work = True
# 生产者兼消费者------------下载URL里面的小说到本地
def download_novel(q):
global novel_title,end_work
while not end_work: # 生产者URL还没生成完毕
time.sleep(0.5)
while True:
if q.empty() and end_work:
return
lock.acquire()
url = q.get()
lock.release()
pages = re.findall('/read_(\\d+).html', url)[0] # 获取小说的章节
rt = requests.get(url=url)
# print(rt.text)
html = etree.HTML(rt.text)
result2 = html.xpath('//div[@class="content"]//text()') # 得到小说文章
result2 = "".join(result2) # 字符串拼接
result2 = result2.replace('\u3000\u3000','\n') # 文本处理完毕
# 写入文件
title = html.xpath('//div[@class="title"]//a/text()')[0]
with open(f'/{novel_title}/{pages}.text','w',encoding='utf-8') as f:
f.write(str(title)+'\n' + result2 + '\n\n')
print(f'{threading.current_thread().name}已下载......{title}')
# 消费者------合并小说 # 其实在上面那个板块里面,把小说以追加的形式写在一个文件里面也能完成合并,这里是为了展示代码能力
def Combined_novel():
global total_numbers,novel_title,end_work
while not end_work:
time.sleep(0.5)
num = 1
fp = open(f'/{novel_title}/{novel_title}.text','w',encoding='utf-8') # 以追加的方式创建合并小说集,如果文件不存在就会新建一个文件
fp.close()
fp = open(f'/{novel_title}/{novel_title}.text','a',encoding='utf-8')
while True:
if int(num) > int(total_numbers):
break
if os.path.exists(f"/{novel_title}/{num}.text"):
with open(f"/{novel_title}/{num}.text",'r',encoding='utf-8') as f:
text = f.read()
fp.write(text)
num += 1
print(f'已合并......章节{num-1}')
os.remove(f"/{novel_title}/{num-1}.text")
print(f'已删除......章节{num-1}')
else:
print(f'未发现......章节{num}')
time.sleep(1) # 等待小说下载
fp.close()
print('已完成全部文件合并')
# 函数入口
def main():
q = queue.Queue(maxsize=2000) # 这里是创建了一个空队列,设置的大小是2000
index_url ='https://www.80down.cc/novel/160651/' # 如果想下载别的小说,只需要修改这个地方就可以了
# 当然,是这个网站里面的小说 ,其他网站的小说,可以按照这个格式爬取
th1 = threading.Thread(target=get_url,args=(index_url,q,))
th1.start()
for i in range(4):
th2 = threading.Thread(target=download_novel,args=(q,),name=f'生产者{i}号')
th2.start()
th3 = threading.Thread(target=Combined_novel)
th3.start()
if __name__ == '__main__':
main()
引入的库
首先介绍引入的库,还有它在代码当中发挥的作用
- re 这个是正则表达式,负责从字符串中提取信息,在代码中提取了小说的总页数
- time 这个是时间模块,在程序中是为了等待其他的线程完成工作
- requests 这个是网络请求模块,可以模拟浏览器向网页发送请求
- threading 这个是多线程模块,有了这个模块才可以开启多线程,在主函数中应用
- queue 这是队列模块,负责存储和出取URL
- os 这个是系统模块,程序中负责创建文件夹,检测文件夹是否存在
- from lxml import etree 这个就是xPath语法的库,不懂的看我前几篇文章
- lock = threading.Lock() 这个是多线程上锁的模块,主要在下载小说的线程中应用
全局变量
- end_work 判断生产URL是否完成工作的变量,防止下载小说的线程太快造成程序崩溃,而且这个制作URL的线程很快
- novel_title 这个是储存小说书名的,在下载URL时创建文件夹使用的,还有下载小说时存临时文件用,以及合并文件的路径
- total_numbers 告知合并小说的线程,一共有多少个文件需要合并
生产者------------产生URL
和之前一样,这种比较简易的网站在设计网址的时候都是只改最后面的数字充当页数,比如/read/289414_1.html
这个下划线后面的1就是第一页,根据这个规律,可以很轻松的模拟出其他的页面链接
所以,根据小说的目录页,获取第一页小说的网址,再获取小说的总页数,这个板块对网址的请求就没用了,剩下的就是设计链接了
在这里我随手就创建了存放小说的文件夹,毕竟目录页可以很轻松的获取小说的名称啦
result1就是获取总章节的页数存到Total_pages里面,这个就是全局变量第三个的来源
后面就是把设计好的链接放到队列里面去
生产者兼消费者------------下载URL里面的小说到本地
这个板块引用了两个全局变量,一个是小说的名称用来下载小说时确认路径的
一个是上面那个生产者的结束判定,毕竟要把URL先存到队列里面去才能拿出来本来是只需要让这个线程等一秒就够了,但是制作URL实在是太快了,这个判断主要作为消费者合并小说的开始标志了
我们使用一共while循环,一直循环下载小说
这里我们从队列里面拿链接的时候要锁一下,一个线程拿链接的过程其他线程不能拿,防止多个线程拿同一个链接
取完链接就是线程自己的事了,互不干扰,就能解锁了
获取一下小说的章节,便于后面保存小说时使用
再接着就是使用xPath语法获取小说的内容,这时候的内容是一个列表,使用字符串拼接函数join把列表合并
发现每句话之间都有一个\u3000\u3000的字样,我们不希望看到这个东西,使用字符串替换函数replace把他们全换成\n换行符
这时候打印一下就会发现,小说的内容已经是分行显示了且中间没有空行,很完美
最后把小说保存在本地
消费者------合并小说
这个板块可有可无,我们在上面保存文件的时候完全可以把他们保存在一个文件里面,不过由于每个线程的差异,会导致某些章节的位置错乱,把这两个板块分离还是有点好处的
这个板块就不需要使用requests库了
完全就是文件操作,读取一个个单篇的小说,再按顺序保存到一个文件中去
这里的读取使用只读r的方式
保存文件使用追加的方式a,我们使用追加的方式打开文件时,按理来说,文件不存在应该会新建一个文件,但是程序报错了
这里我使用了先以只写的方式w创建文件,再关闭文件,再使用追加的方式打开文件,就解决的这个问题
在读取小说的时候,我们要按照顺序读取,所以还要使用os库判断一下小说的章节存不存在,如果存在就读取追加,然后num+1
这里的num就是我控制小说章节顺序的依据,我在前面设计保存小说的时候就是以章节数字命名,所以无论下载时间的前后都不影响按顺序读取小说
读取完的小说没有什么价值了,就把读取后的章节删除
毕竟看小说还是喜欢一直连续的看,不喜欢一直关闭打开文件吧
主函数------函数入口
没什么好说的
只有一个队列的实例化,还有多线程的启动
这里只有板块二的工作更能证明多线程的优点
还是希望读到这里的小伙伴可以跟我交流一下哪里有疑惑,大家需要互相成长,毕竟金无足赤,人无完人
完~~