并发和并行
做并发编程之前,必须首先理解什么是并发,什么是并行,什么是并发编程,什么是并行编程。
并发(concurrency)和并行(parallellism)的区别:
- 解释一:并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
- 解释二:并行是在不同实体上的多个事件,并发是在同一实体上的多个事件。
- 解释三:并行是在多台处理器上同时处理多个任务,如hadoop分布式集群。并发是在一台处理器上“同时”处理多个任务。
例子
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。
并行的关键是你有同时处理多个任务的能力。
所以我认为它们最关键的点就是:是否是『同时』。
进程,线程,协程
区别
- 进程 工厂生产线
- 线程 工人
- 协程 充分的利用工人干别的事情
单线程爬取腾讯招牌网站信息
目标网站(https://careers.tencent.com/search.html?index=2&keyword=python)
操作亮点
- 在使用request请求方法时,使用params参数进行传参,在输入网址的时候,可以不用写这么长
import time
import requests
from user_agent import headers # 没有自己的请求头包需要自己找请求头headers
import cchardet
from retrying import retry
import json
import jsonpath
# https://careers.tencent.com/tencentcareer/api/post/Query?&keyword=python&pageIndex=5&pageSize=10
class Tenxun(object):
def __init__(self):
self.url = 'https://careers.tencent.com/tencentcareer/api/post/Query?'
self.proxies = {'http':'121.237.88.178:3000'}
def get_url(self):
'''生成url列表'''
# 网址的其他参数靠传参的方式生成
params = []
for i in range(1,5):
param = {
'keyword':'python',
'pageIndex': i,
'pageSize':'10'
}
params.append(param)
return params
@retry(stop_max_attempt_number = 3)
def send(self,params):
'''发送请求'''
time.sleep(1)
try:
res = requests.get(url=self.url,
headers=headers,
proxies=self.proxies,
params=params,
timeout=4).content
encoding = cchardet.detect(res)['encoding']
return res.decode(encoding)
except Exception as e:
print(e)
def clean(self,data):
'''清洗数据'''
# 找到的是json数据 使用jsonpath解析
# json数据转换python数据
dict_data = json.loads(data)
# 解析
ret = jsonpath.jsonpath(dict_data,'$..RecruitPostName')
return ret
def save(self,data):
'''保存文件'''
print(data)
def start_func(self):
'''执行任务'''
# 1 构造请求参数
param = self.get_url()
# 需要遍历取出
for params in param:
# 2 发送请求
data = self.send(params=params)
# 3 清洗数据
ret_name = self.clean(data)
# 4 保存数据
self.save(ret_name)
def run(self):
'''代码执行时间'''
start_time = time.time()
self.start_func()
end_time = time.time()
print('耗时:%s' % (end_time-start_time))
if __name__ == '__main__':
Tenxun().run()
线程和队列的关系
首先得有队列,然后才能进行队列元素抽取和存放
import time
import requests
from user_agent import headers
import cchardet
from retrying import retry
import json
import jsonpath
from queue import Queue
import threading
# https://careers.tencent.com/tencentcareer/api/post/Query?&keyword=python&pageIndex=5&pageSize=10
class Tenxun(object):
def __init__(self):
self.url = 'https://careers.tencent.com/tencentcareer/api/post/Query?'
self.proxies = {'http':'121.237.88.178:3000'}
# 创建url队列
self.url_q = Queue()
# 创建相应队列
self.res_q = Queue()
# 创建数据队列
self.data_q = Queue()
def get_url(self):
'''生成url列表'''
# 网址的其他参数靠传参的方式生成
# 将params的数据添加到url_q队列中,不需要创建空列表
# params = []
for i in range(1,5):
param = {
'keyword':'python',
'pageIndex': i,
'pageSize':'10'
}
# params.append(param)
# 将数据使用put方法传进url队列url_q
self.url_q.put(param)
print('添加url队列')
# return params
@retry(stop_max_attempt_number = 3)
def send(self):
'''发送请求'''
time.sleep(1)
while True:
# 使用get方法获取url队列url_q中的网址参数(出队列),一次获取一个
params = self.url_q.get()
try:
res = requests.get(url=self.url,
headers=headers,
proxies=self.proxies,
params=params,
timeout=4).content
encoding = cchardet.detect(res)['encoding']
# return res.decode(encoding)
# 将数据使用put方法传进响应队列res_q
self.res_q.put(res.decode(encoding))
print('响应数据添加进响应队列')
# 每次完成获取之后,计数器减一,
# 但是get方法不自带计数器,需要自己创建,让计数器自己减一
# 计数器减一的写法
self.url_q.task_done()
print('url队列计数器减一')
except Exception as e:
print(e)
def clean(self):
'''清洗数据'''
while True:
# 使用get方法获取响应队列res_q中的网址参数(出队列),一次获取一个
data = self.res_q.get()
# 找到的是json数据 使用jsonpath解析
# json数据转换python数据
dict_data = json.loads(data)
# 解析
ret = jsonpath.jsonpath(dict_data,'$..RecruitPostName')
# return ret
# 将数据使用put方法传进数据队列data_q
self.data_q.put(ret)
print('解析清理的数据添加进清理数据队列')
# 计数器减一
self.res_q.task_done()
print('响应队列计数器减一')
def save(self):
'''保存文件'''
while True:
# 使用get方法获取数据队列data_q中的网址参数(出队列),一次获取一个
data = self.data_q.get()
print('拿出数据')
# print(data)
# 计数器减一
self.data_q.task_done()
print('清理数据队列计数器减一,完成任务')
def start_func(self):
'''执行任务'''
# 存放线程的列表
th_lists = []
# 0 创建构造请求参数的子线程
th_url = threading.Thread(target=self.get_url)
print('创建子线程1')
th_lists.append(th_url)
# 创建发送请求参数的线程
# 发送时可以一次开启两条子线程
for i in range(2):
th_send = threading.Thread(target=self.send)
print('创建子线程'+str(i+2))
th_lists.append(th_send)
# 创建清洗数据的子线程
th_clean = threading.Thread(target=self.clean)
print('创建子线程3')
th_lists.append(th_clean)
# 创建保存数据的子线程
th_save = threading.Thread(target=self.save)
print('创建子线程4')
th_lists.append(th_save)
# 1 构造请求参数
for th in th_lists:
# 是setDaemon,不是setDeamon!!
th.setDaemon(True) # 开启线程守护,主线程等待子线程
th.start() # 启动
# 队列用于阻塞主线程,因为还有数据没有处理,不能结束
for i in [self.url_q,self.res_q,self.data_q]:
print('阻塞前')
i.join()
print('阻塞后')
def run(self):
'''代码执行时间'''
start_time = time.time()
self.start_func()
end_time = time.time()
print('耗时:%s' % (end_time-start_time))
if __name__ == '__main__':
Tenxun().run()
疑问
怎么才能知道他阻不阻塞