多线程爬虫入门及问题解决(爬取表情包)
时间:2022-07-22
本文章向大家介绍多线程爬虫入门及问题解决(爬取表情包),主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
文章目录
- 实验要求
- 代码实现
- 问题分析
- 问题解决
- 最终答案
- 时间分析
实验要求
使用多线程爬虫技术,爬取目标网站中的图片并保存到本地。
代码实现
使用生产者消费者的设计模式 + 多线程技术。
首先要明确地概念:
- 线程与线程之间是轮流执行的,每个线程都有一个时间片;
- 主线程就是整个函数的执行顺序;
-
join
函数的意思是让调用者优先执行,也可以理解为线程阻塞,即阻塞其他线程,直到该线程执行完毕或者终止。
"""
@author: shoo Wang
@contact: wangsuoo@foxmail.com
@file: demo02.py
@time: 2020/5/19 0019
"""
# 多线程爬虫
import requests as req
from lxml import etree
import urllib
import threading
import time
lock = threading.Lock()
IMG_LIST = []
URL_LIST = []
# 生成爬取目标网站的链接
def getUrlList(num):
base_url = 'https://www.doutula.com/photo/list/?page='
for i in range(1, num + 1):
x = base_url + str(i)
URL_LIST.append(x)
# 获取网站的文本数据 url 为单个页面
def product():
while URL_LIST:
# 拿到这一页的地址
url = URL_LIST.pop()
html = etree.HTML(urlToText(url))
img_url = html.xpath('//div[@class="page-content text-center"]//img/@data-original')
# 获取线程锁
lock.acquire()
print('生产者将一页中的图片地址放进资源池')
for img in img_url:
# 对于每一页中的每一个图片都把他放进去
IMG_LIST.append(img)
lock.release()
# 将页的地址转为 text 格式
def urlToText(url):
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.116 Safari/537.36'}
resp = req.get(url, headers=header)
resp.encoding = 'utf-8'
return resp.text
def customer():
global flag
while True:
if len(IMG_LIST) is 0:
pass
else:
# 获取线程锁
lock.acquire()
img = IMG_LIST.pop()
# 改完之后释放锁
lock.release()
fileName = img.split('/')[-1]
path = './image/' + fileName
urllib.request.urlretrieve(img, path)
def run_time(func):
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
end = time.time()
print('爬取成功! 请查阅当前目录下的 image 文件夹, 耗时: ', end-start, 's')
return wrapper()
@run_time
def run():
# 爬取的页数
getUrlList(2)
thrPros = []
thrCuss = []
for i in range(2):
thrPro = threading.Thread(target=product)
thrPro.start()
thrPros.append(thrPro)
for i in range(8):
thrCus = threading.Thread(target=customer)
thrCus.start()
thrCuss.append(thrCus)
for thPro in thrPros:
thPro.join()
for thCus in thrCuss:
thCus.join()
程序执行,过一会之后图片会爬取成功,但是这里是有问题的,下面会分析。
问题分析
这一段代码有问题:
def customer():
global flag
while True:
if len(IMG_LIST) is 0:
pass
else:
# 获取线程锁
lock.acquire()
img = IMG_LIST.pop()
# 改完之后释放锁
lock.release()
fileName = img.split('/')[-1]
path = './image/' + fileName
urllib.request.urlretrieve(img, path)
我们写这段代码的初衷是因为刚开始的时候 IMG_LIST
可能是 0 ,因为生产者还没来得及做,所以让消费者等一下生产者,所以 continue
,但是设想一下到最后的时候,消费者已经消耗完所有的 IMG_LIST
资源了。IMG_LIST
确实为空了,他还在这里不停的循环。
问题解决
我们可以添加一个 falg
变量来记录生产者是不是生产完了。刚开始的时候设置为 0
,这样就还是 continue
。
只需要保证在设置 flag
变量的时候,IMG_LIST
不为空即可,因为如果他不为空,那么无论 flag
的值为何值都不会影响程序的执行,因为这个 if
语句进不去:
那我们如何控制呢?
因为在多线程执行的时候,我们无法保证 IMG_LIST
不为空,因为当生产者刚刚生产完了资源放进资源池中,可能瞬间就被消费者消耗掉了,所以我们要做的就是让其它线程先等生产者执行完了再执行,这个时候就要使用 join
方法了,他的意思是让调用它的线程独占 CPU
资源,只有等他执行完了其他线程才能执行。
所以主线程也被阻塞了,当消费者线程的 join
方法结束的时候,我们肯定可以保证 IMG_LIST
不为空,所以可以放心的将 flag
置为1
,这样的话如果下一次消费者消耗完了资源池中的数据,就说明所有的生产者已经生产完了,然后又被消耗完了,所以是真的没有了,那么这个 flag
就起作用了。
最终答案
"""
@author: shoo Wang
@contact: wangsuoo@foxmail.com
@file: demo02.py
@time: 2020/5/19 0019
"""
# 多线程爬虫
import requests as req
from lxml import etree
import urllib
import threading
import time
lock = threading.Lock()
IMG_LIST = []
URL_LIST = []
flag = 0
# 生成爬取目标网站的链接
def getUrlList(num):
base_url = 'https://www.doutula.com/photo/list/?page='
for i in range(1, num + 1):
x = base_url + str(i)
URL_LIST.append(x)
# 获取网站的文本数据 url 为单个页面
def product():
while URL_LIST:
# 拿到这一页的地址
url = URL_LIST.pop()
html = etree.HTML(urlToText(url))
img_url = html.xpath('//div[@class="page-content text-center"]//img/@data-original')
# 获取线程锁
lock.acquire()
print('生产者将一页中的图片地址放进资源池')
for img in img_url:
# 对于每一页中的每一个图片都把他放进去
IMG_LIST.append(img)
lock.release()
# 将页的地址转为 text 格式
def urlToText(url):
header = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.116 Safari/537.36'}
resp = req.get(url, headers=header)
resp.encoding = 'utf-8'
return resp.text
def customer():
global flag
while True:
if len(IMG_LIST) is 0:
if flag:
print('资源池中的数据已取完')
break
pass
else:
# 获取线程锁
lock.acquire()
img = IMG_LIST.pop()
# 改完之后释放锁
lock.release()
fileName = img.split('/')[-1]
path = './image/' + fileName
urllib.request.urlretrieve(img, path)
def run_time(func):
def wrapper(*args, **kwargs):
start = time.time()
func(*args, **kwargs)
end = time.time()
print('爬取成功! 请查阅当前目录下的 image 文件夹, 耗时: ', end-start, 's')
return wrapper()
@run_time
def run():
# 爬取的页数
getUrlList(2)
thrPros = []
thrCuss = []
for i in range(2):
thrPro = threading.Thread(target=product)
thrPro.start()
thrPros.append(thrPro)
for i in range(8):
thrCus = threading.Thread(target=customer)
thrCus.start()
thrCuss.append(thrCus)
for thPro in thrPros:
thPro.join()
'''
? 为什么 flag 要放在这里:
- 他前面的是生产者线程的join方法,该方法执行完了则代表生产者线程肯定全部执行完了
- 因为 join 相当于线程阻塞,只执行该线程,不执行其他线程
- 肯定执行完了之后那 IMG 中肯定有数据,那么同时在执行的消费者肯定可以直接获取到
- 如果这个时候还是空,那只有可能是他自己消耗完了,因为人家刚刚给你制造了那么多 IMG,不可能没有
- 如果放在生产者的 join 方法前面的话,则可能生产者还没来得及制造资源
'''
global flag
flag = 1
for thCus in thrCuss:
thCus.join()
时间分析
下面是我以爬取10页数据做的实验,使用控制变量法:
- 图一和图二是在保持消费者的线程数不变的情况下,改变生产者的线程数量
从2到5
,通过结果可以发现,两者的时间几乎没有差别,仅仅是少了0.04
秒,但是这说明真正制约时间的不是生产者; - 图二和图三是在保持生产者的线程数不变的情况下,改变消费者的线程数量
从8到4
,降了一半,通过结果可以发现,两者的时间相差很大后者为前者的3倍
时间,这说明真正限制时间的是消费者的执行速度,所以应该相对于生产者分配更多的线程数量。
- 脑科学发展的助推器
- BFIThumb:WordPress 中替代TimThumb 进行裁图的选择
- jquery 操作ajax 相关方法
- SQL SERVER 2008 Hierarchyid数据类型
- Html5 学习利器 Web Standards Update for Microsoft Visual Studio 2010 SP1
- MongoDB 客户端 MongoVue
- HttpClient介绍
- 10个使用 Foundation 框架开发的WordPress 主题推荐
- jQuery 效果使用
- 几款更换WordPress 后台UI 的插件推荐
- 入门:构建简单的Web API
- WordPress 编辑器快捷键——让写作来得更方便些吧!
- ASP.NET Web API: 宿主(Hosting)
- 在 Windows Phone上使用QQConnect OAuth2
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 在Laravel5中正确设置文件权限的方法
- asp函数split()对应php函数explode()
- php获取目录下所有文件及目录(多种方法)(推荐)
- Python基于Twilio及腾讯云实现国际国内短信接口
- PHP __call()方法实现委托示例
- PHP中rename()函数的妙用讲解
- php使用mysqli和pdo扩展,测试对比mysql数据库的执行效率完整示例
- php实现小程序支付完整版
- Yii2框架视图(View)操作及Layout的使用方法分析
- php实现单笔转账到支付宝功能
- PHP使用Redis实现Session共享的实现示例
- windows10在visual studio2019下配置使用openCV4.3.0
- PHP5.0 TIDY_PARSE_FILE缓冲区溢出漏洞的解决方案
- Python爬虫爬取新闻资讯案例详解
- Python代码需要缩进吗