在python3中,主线程主进程结束,子进程不会结束,为了能够让主线程回收子线程,可以把子线程设置为守护线程,即该线程不重要,主线程结束,子线程结束:
举个例子:
import time
import threading
def test():
while True:
print('测试线程守护!!',threading.currentThread())
time.sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=test)
t2 = threading.Thread(target=test)
t1.start()
t2.start()
执行以上代码,得出的结果是交替打印出两个线程,由于线程执行的函数为死循环,所以子线程也会陷入死循环,不管你怎么按ctrl+c也无法停止线程。如下:
(venv) E:\test>python test.py
测试线程守护!! <Thread(Thread-1, started 9652)>
测试线程守护!! <Thread(Thread-2, started 2728)>
测试线程守护!! <Thread(Thread-1, started 9652)>
测试线程守护!! <Thread(Thread-2, started 2728)>
测试线程守护!! <Thread(Thread-1, started 9652)>
测试线程守护!! <Thread(Thread-2, started 2728)>
测试线程守护!! <Thread(Thread-1, started 9652)>
测试线程守护!! <Thread(Thread-2, started 2728)>
测试线程守护!! <Thread(Thread-1, started 9652)>
测试线程守护!! <Thread(Thread-2, started 2728)>
测试线程守护!! <Thread(Thread-1, started 9652)>
测试线程守护!! <Thread(Thread-2, started 2728)>
测试线程守护!! <Thread(Thread-1, started 9652)>
测试线程守护!! <Thread(Thread-2, started 2728)>
那么怎样才能可以避免这种问题呢?或者说,怎样才能在主线程退出的时候,子线程也自动退出呢?没错就是使用线程守护,也就是等到主线程执行完,子线程也强制停止,换句话说就是子线程会随着主线程一起结束。使用案例:
t1 = threading.Thread(targe=func,args=(,))
t1.setDaemon(True)
t1.start() #此时线程才会启动
优化前面死循环不能停止的线程,源码如下:
import time
import threading
def test():
while True:
print('测试线程守护!!',threading.currentThread())
time.sleep(1)
if __name__ == '__main__':
t1 = threading.Thread(target=test)
t2 = threading.Thread(target=test)
t1.setDaemon(True)
t1.start()
t2.setDaemon(True)
t2.start()
输出结果:
(venv) E:\test>python test.py
测试线程守护!! <Thread(Thread-1, started daemon 5884)>
测试线程守护!! <Thread(Thread-2, started daemon 7112)>
可以看到,运行一次就直接退出了,因为主线程已经执行完了,确实是主线程已经结束了,正因为设置了守护线程,所以这时候子线程也一并退出了。
from queue import Queue
q = Queue(maxsize=100)
item = {}
q.put_nowait(item) #不等待直接放,队列满的时候会报错
q.put(item) #放入数据,队列满的时候会等待
q.get_nowait() #不等待直接取,队列空的时候会报错
q.get() #取出数据,队列为空的时候会等待
q.qsize() #获取队列中现存数据的个数
q.join() #队列中维持了一个计数,计数不为0时候让主线程阻塞等待,队列计数为0的时候才会继续往后执行
q.task_done() # put的时候计数+1,get不会-1,get需要和task_done 一起使用才会-1
## 队列可用于线程间的数据通讯
from queue import Queue
import threading
q = Queue()
def add_to_queue():
for i in range(0, 100):
print("存入队列: {}".format(i))
q.put(i)
def get_from_queue():
# 但是在我们获取队列元素的时候, 我们并不知道队列中放了几个元素,
# 这个时候我们就会使用while的死循环来获取,知道取完为止
# for i in range(0, 100):
while True:
print("从队列中取出: {}".format(q.get()))
q.task_done()
# 创建线程
t = threading.Thread(target=add_to_queue)
# 设置为守护线程
t.setDaemon(True)
# 启动线程
t.start()
t = threading.Thread(target=get_from_queue)
t.setDaemon(True)
t.start()
# 队列加入主线线程, 等待队列中任务完成为止
q.join()
装饰器的作用: 不改变原有函数,给函数添加额外功能
# 定义装饰一个死循环执行任务的装饰器
def run_forever(func):
def forever(obj):
while True:
func(obj)
return forever
# 使用装饰器
@run_forever # 等同于 run_forever(run)
def run(obj):
print("人生苦短,我用Python {}".format(obj))
# 执行函数 --死循环
run('Hpayy')
多线程爬虫源码:
import threading
import requests
from lxml import etree
import json
from queue import Queue
# 多线程思路:
# 把所有待采集得链接url放进一个url队列里面
# 在请求页面得过程中,以多线程得方式从队列中取出url,请求结束后要执行task_done()方法来释放队列避免重复采集
# 为了更快得处理相应后数据(页面数据),也可以把每个页面获得得页面源码放在相应队列中,一样处理完得task_done()来清队列
# 最后得保存数据也可以开一个队列用于保存
# 可以设置线程数量来处理数据
# 由于请求、处理数据需要较多次数的轮循,所以可以直接写个修饰器函数用于循环某个函数
def run_forever(func):
def wrapper(obj):
while True:
func(obj)
return wrapper
class CrawlData:
def __init__(self):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'
}
self.url_pattern = 'https://www.qiushibaike.com/8hr/page/{}/'
self.url_queue = Queue() # url 队列
self.page_queue= Queue() # 页面 队列
self.data_queue = Queue() # 数据 队列
# 把待采集的url放入队列
def add_url_to_queue(self):
for i in range(1, 14):
self.url_queue.put(self.url_pattern.format(i))
@run_forever
def add_page_to_queue(self):
url = self.url_queue.get() # 从队列中获取一个url,get不会做-1操作
result = requests.get(url, headers=self.headers)
if result.status_code != 200:
self.url_queue.put(url)
else:
self.page_queue.put(result.text)
#结束当前url任务
self.url_queue.task_done()
@run_forever
def add_data_to_queue(self):
# 从队列中获取一个页面数据出来待处理
page = self.page_queue.get()
element = etree.HTML(page)
div_s = element.xpath('//div[@class="recommend-article"]/ul/li')
# 遍历分组列表, 再使用xpath获取内容
dz_list = []
for div in div_s:
item = {}
# 每个段子包含发送人头像URL, 昵称, 性别, 段子内容, 好笑数,评论数
# 头像URL
item['head_url'] = self.get_first_element(div.xpath('.//a[@class="recmd-user"]/img/@src'))
if item['head_url'] is not None:
item['head_url'] = 'http:' + item['head_url']
# 昵称
item['author_name'] = self.get_first_element(div.xpath('.//a[@class="recmd-user"]/span/text()'))
dz_list.append(item)
# 解析好得数据放入队列
self.data_queue.put(dz_list)
# 结束当前页面数据任务
self.page_queue.task_done()
def get_first_element(self, list):
'''获取列表中第一个元素,如果是空列表就返回None'''
return list[0] if len(list) != 0 else None
# 保存数据
@run_forever
def save_data(self):
# 从已有的数据中获取一个处理后的数据进行存储
data = self.data_queue.get()
with open('qiubai.json', 'a', encoding='utf8') as f:
for dt in data:
json.dump(dt, f, ensure_ascii=False)
f.write('\n')
self.data_queue.task_done() # 结束当前数据任务
def run_count_use(self, func, count):
# 开启多个线程处理
for i in range(count):
t = threading.Thread(target=func)
t.setDaemon(True)
t.start()
def run(self):
# 把待处理的url加入队列
url_t = threading.Thread(target=self.add_url_to_queue)
url_t.start()
# 开启获取页面数据线程
self.run_count_use(self.add_page_to_queue, 3)
# 开启处理解析页面数据
self.run_count_use(self.add_data_to_queue, 2)
# 开启数据存储线程
self.run_count_use(self.save_data, 2)
# 使用队列join方法,等待队列数据处理结束
self.url_queue.join() # 等待url队列为空时才会往下继续
self.page_queue.join() # 等待页面队列为空时才会往下继续
self.data_queue.join() # 等待数据队列为空时才会往下继续
if __name__ == '__main__':
import time
start = time.time()
CrawlData().run()
end = time.time()
print('总共用时:{}'.format((end-start)))
实现步骤
from multiprocessing import Pool
import json
import requests
import os
import time
from lxml import etree
class CrawlData:
def __init__(self, url):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.139 Safari/537.36'
}
self.url = url
def get_page_data(self):
print('当前子进程{}'.format(os.getpid()))
page_data = requests.get(self.url, headers=self.headers)
element = etree.HTML(page_data.text)
div_s = element.xpath('//div[@class="recommend-article"]/ul/li')
# 遍历分组列表, 再使用xpath获取内容
dz_list = []
for div in div_s:
item = {}
item['head_url'] = self.get_first_element(div.xpath('.//a[@class="recmd-user"]/img/@src'))
if item['head_url'] is not None:
item['head_url'] = 'http:' + item['head_url']
# 昵称
item['author_name'] = self.get_first_element(div.xpath('.//a[@class="recmd-user"]/span/text()'))
dz_list.append(item)
self.save_data(dz_list)
def get_first_element(self, list):
'''获取列表中第一个元素,如果是空列表就返回None'''
return list[0] if len(list) != 0 else None
def save_data(self, data):
# 从已有的数据中获取一个处理后的数据进行存储
with open('qiubai_process.json', 'a', encoding='utf8') as f:
for dt in data:
json.dump(dt, f, ensure_ascii=False)
f.write('\n')
# 采集入口函数
def crawl(url):
CrawlData(url).get_page_data()
if __name__ == '__main__':
start = time.time()
url_pattern = 'https://www.qiushibaike.com/8hr/page/{}/'
p = Pool(4) # 不传参默认以最大cpu数作为进程数
for i in range(1,14):
url = url_pattern.format(str(i))
p.apply_async(crawl, args=(url,))
p.close()
p.join()
end = time.time()
print(f'总共用时:{(end-start)}')
运行结果:
当前子进程10356
当前子进程10356
当前子进程10356
当前子进程10356
当前子进程10356
当前子进程10356
当前子进程10356
当前子进程10356
当前子进程10356
当前子进程10356
当前子进程10356
当前子进程10356
当前子进程10356
总共用时:13.325877666473389
当前子进程3320
当前子进程9492
当前子进程3320
当前子进程9492
当前子进程9492
当前子进程3320
当前子进程9492
当前子进程3320
当前子进程3320
当前子进程9492
当前子进程3320
当前子进程9492
当前子进程3320
总共用时:1.3394155502319336
当前子进程10908
当前子进程9124
当前子进程3148
当前子进程10908
当前子进程9124
当前子进程3148
当前子进程3148
当前子进程9124
当前子进程10908
当前子进程9124
当前子进程3148
当前子进程10908
当前子进程3148
总共用时:1.1449363231658936
当前子进程11208
当前子进程7944
当前子进程9188
当前子进程9916
当前子进程9916
当前子进程11208
当前子进程9188
当前子进程7944
当前子进程9916
当前子进程7944
当前子进程11208
当前子进程9188
当前子进程11208
总共用时:0.9803769588470459
采集知乎一定要登录才可以,否则无法正常获取数据,本文采用cookies携带登录信息请求数据,具体之前文章《Python爬虫之利用cookies跳过登陆验证码》已经介绍过。其中cookies一定要用等登陆后从浏览器里面获取,并用于请求中,不然会报以下错误:
{"code":"100010","'message'":"Unsupported auth type oauth"}
from multiprocessing import Pool
import json
import os
import requests
import time
from queue import Queue
class CrawlZH(object):
def __init__(self, url, cookies):
self.url = url
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36'
}
self.cookies = cookies
def get_json(self):
session = requests.session()
session.headers = self.headers
requests.utils.add_dict_to_cookiejar(session.cookies, self.cookies)
response = session.get(self.url)
result = json.loads(response.text)
print("当前子进程-{}运行结果:{}".format(os.getpid(), len(result['data'])))
def main(url, cookies):
# print('当前子进程:{}'.format(os.getpid()))
CrawlZH(url, cookies).get_json()
if __name__ == '__main__':
# 这里cookies是你正常的登陆后从浏览器里面复制出来
cookies = { ...
}
url_pattern = 'https://www.zhihu.com/api/v3/feed/topstory/recommend?desktop=true&page_number={}&limit=6'
# 开启多进程采集 start ===
print('母进程:{}'.format(os.getpid()))
start = time.time()
p = Pool()
for page_number in range(1, 11):
url = url_pattern.format(str(page_number))
p.apply_async(main, args=(url, cookies,))
p.close()
p.join()
end = time.time()
print(f'总共耗时:{(end-start)}')
# 多进程 end ====
# 多线程采集 start ===
start = time.time()
CrawlZHThread(cookies).run()
end = time.time()
print(f'总共耗时:{(end-start)}')
# 多线程 end ====
结果:
多进程结果:
母进程:5024
当前子进程-6372运行结果:6
当前子进程-12984运行结果:6
当前子进程-4536运行结果:6
当前子进程-12924运行结果:6
当前子进程-12984运行结果:6
当前子进程-6372运行结果:6
当前子进程-12924运行结果:6
当前子进程-4536运行结果:6
当前子进程-12984运行结果:6
当前子进程-6372运行结果:6
总共耗时:2.6678614616394043
# 简单 修饰器 用于无限循环
def run_forever(func):
def wrapper(obj):
while True:
func(obj)
return wrapper
class CrawlZHThread(object):
def __init__(self, cookies):
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36'
}
self.cookies = cookies
self.url_pattern = 'https://www.zhihu.com/api/v3/feed/topstory/recommend?desktop=true&page_number={}&limit=6'
self.url_queue = Queue()
def add_url_to_queue(self):
for i in range(1, 11):
self.url_queue.put(self.url_pattern.format(str(i)))
def get_json(self):
while True:
url = self.url_queue.get()
session = requests.session()
session.headers = self.headers
requests.utils.add_dict_to_cookiejar(session.cookies, self.cookies)
response = session.get(url)
result = json.loads(response.text)
print("当前子线程-{}运行结果:{}".format(threading.current_thread().name, len(result['data'])))
self.url_queue.task_done()
@run_forever
def get_json_01(self):
url = self.url_queue.get()
session = requests.session()
session.headers = self.headers
requests.utils.add_dict_to_cookiejar(session.cookies, self.cookies)
response = session.get(url)
result = json.loads(response.text)
print("当前子线程-{}运行结果:{}".format(threading.current_thread().name, len(result['data'])))
self.url_queue.task_done()
def run(self):
url_t = threading.Thread(target=self.add_url_to_queue)
url_t.start()
# 开启多线程
for i in range(4):
t = threading.Thread(target=self.get_json)
t.setDaemon(True)
t.start()
self.url_queue.join()
if __name__ == '__main__':
# 这里cookies是你正常的登陆后从浏览器里面复制出来
cookies = { ...
}
# 多线程采集 start ===
start = time.time()
CrawlZHThread(cookies).run()
end = time.time()
print(f'总共耗时:{(end-start)}')
# 多线程 end ====
结果:
多线程结果:
当前子线程-Thread-3运行结果:6
当前子线程-Thread-4运行结果:6
当前子线程-Thread-5运行结果:6
当前子线程-Thread-2运行结果:6
当前子线程-Thread-3运行结果:6
当前子线程-Thread-4运行结果:6
当前子线程-Thread-5运行结果:6
当前子线程-Thread-2运行结果:6
当前子线程-Thread-3运行结果:6
当前子线程-Thread-4运行结果:6
总共耗时:2.4344863891601562
其中get_json与get_json_01结果是一样的,只不过一个用了修饰器,用修饰器可以提高可读性
对比以上两种采集方式,时间差不多,我只做获取页面数据,没有做后续数据处理,所以没有显著差异!!
注意本文的创作时间,后期页面源码调整,可能将采集不到数据,也属正常现象,本文主要目的是提供多进程、多线程爬虫的思想逻辑,仅供参考!!