Stata连享会 主页 || 视频 || 推文 || 知乎 || Bilibili 站
温馨提示: 定期 清理浏览器缓存,可以获得最佳浏览体验。
New!
lianxh
命令发布了:
随时搜索推文、Stata 资源。安装:
. ssc install lianxh
详情参见帮助文件 (有惊喜):
. help lianxh
连享会新命令:cnssc
,ihelp
,rdbalance
,gitee
,installpkg
⛳ Stata 系列推文:
作者: 王颖 (四川大学)
邮箱: wangyingchn@outlook.com
目录
在执行 Python 任务的过程中,我们通常会想提高程序的效率。尤其在处理大量任务的时候,提高程序的效率可以节省大量的时间。
以数据爬取为例,爬虫的基本流程都是,请求数据 → 解析数据 → 存储数据。如果有一万条数据,这个流程就要重复执行一万次,而且是线性的执行。就像车流一样,如果只有一条车道,一万辆车排队通过就需要一万次。如果有了双车道,就可以减少一半的时间,如果有了四车道,时间可以再减少一半。
那我们在程序运行中,能否想办法多开几条车道呢?在 Python 中,可以通过开启多进程或者多线程,来提高程序运行的效率。我们将对 Python 中的多进程、多线程进行简单的介绍,并主要介绍多线程在 Python 爬虫中的应用。
先简单的介绍一下进程与线程的含义和两者的区别。
进程(Process),是资源单位。 要运行一个程序时,操作系统会分配一部分内存空间和资源来运行这个程序,这就是进程。 比如电脑中运行 Word、Excel、微信、QQ 等,每个程序都是一个进程。
线程(Thread)是执行单元。每个进程要都需要执行,这个执行就由线程来完成。
打个比方,进程就好像一家公司,里面包括各种资源,比如办公室、办公桌、电脑等, 而线程就是公司里的员工,来执行具体的任务。
如果一家公司没有员工,那公司就无法运营。 同理,每个进程中至少会有一个线程。只要启动程序,进程中就默认会有一个主线程。 当然,为了提高效率,一个进程也可能有多个线程去执行(多招几个员工)。
进程和线程之间的简单区别如下表:
进程 | 线程 | |
---|---|---|
简单定义 | 资源单位 | 执行单位 |
资源占用 | 多 | 少 |
资源共享 | 进程间的资源相互独立 | 同一进程下的线程间共享资源 |
互相通信 | 进程间只能间接交流 | 线程间可以直接交流,一个线程可以控制同一进程下的其他线程 |
可靠程度 | 进程间不会互相影响 | 一个线程挂掉会导致整个进程挂掉 |
在 Python 实际应用中,更多使用多线程,因为消耗资源更少,且线程间共享内存、变量等,互相交流很容易。 与多进程、多线程相关的概念还有全局解释器锁(GIL)、互斥锁、线程守护等 还有其他方法也涉及提高程序运行效率,如队列、协程等。
我们通过一个简单的虚拟任务,来看看在 Python 中如何使用多进程。以及多进程执行和正常执行在任务耗时上的区别。
# -*- coding: utf-8 -*-
# Author: W.Y.
# Email: wangyingchn@outlook.com
# Date: 2022/4/24
import time # 时间模块
from random import randint # 生成随机数
from multiprocessing import Process # 多进程
# 定义一个虚拟任务
def random_task(name):
task_time = randint(3, 8) # 任务随机执行 3 至 8 秒
time.sleep(task_time)
print(f'{name}执行时间{task_time}s')
# 正常执行两个任务
def main():
start_main = time.time()
random_task('正常执行任务1')
random_task('正常执行任务2')
end_main = time.time()
print(f'正常执行总耗时{end_main - start_main}s')
# 使用多进程执行两个任务
def multi_process():
start_process = time.time()
t1 = Process(target=random_task, args=('多进程执行任务1', )) # 创建新的进程,注意此处 args 必须为元组
t1.start() # 告诉进程可以开始执行,具体执行开始时间由 CPU 决定
t2 = Process(target=random_task, args=('多进程执行任务2', ))
t2.start()
t1.join() # join() 方法,等待进程执行结束再往下执行主程序(为了统计时间)
t2.join()
end_process = time.time()
print(f'多进程执行总耗时{end_process - start_process}s')
if __name__ == '__main__':
print('-'*30)
main()
print('-'*30)
multi_process()
程序运行的结果如下:
------------------------------
正常执行任务1执行时间 7s
正常执行任务2执行时间 5s
正常执行总耗时 12.014511823654175 s
------------------------------
多进程执行任务 2 执行时间 3 s
多进程执行任务1执行时间 5 s
多进程执行总耗时5. 3230578899383545 s
可以看到,正常执行任务时,第一个任务结束后才执行第二个任务,任务的总耗时等于两个任务的耗时总和。而使用多进程时,两个任务可以同时进行,此时任务的总耗时不再是两个任务的耗时总和。可见,使用多进程确实可以提高程序的运行效率。
接下来我们来看看 Python 中的多线程。 虽然进程和线程的本质完全不同,但是在 Python 中启用多进程和多线程的方式是一样的,只是需要使用不同的模块。
# -*- coding: utf-8 -*-
# Author: W.Y.
# Email: wangyingchn@outlook.com
# Date: 2022/4/24
import time # 时间模块
from random import randint # 生成随机数
from threading import Thread # 多线程
# 定义一个虚拟任务
def random_task(name):
task_time = randint(3, 8) # 任务随机执行 3 至 8 秒
time.sleep(task_time)
print(f'{name}执行时间{task_time}s')
# 正常执行两个任务
def main():
start_main = time.time()
random_task('正常执行任务1')
random_task('正常执行任务2')
end_main = time.time()
print(f'正常执行总耗时{end_main - start_main}s')
# 使用多线程执行两个任务
def multi_thread():
start_thread = time.time()
t1 = Thread(target=random_task, args=('多线程执行任务1', )) # 创建新的线程,注意此处 args 必须为元组
t1.start() # 告诉线程可以开始执行,具体执行开始时间由 CPU 决定
t2 = Thread(target=random_task, args=('多线程执行任务2', ))
t2.start()
t1.join() # join() 方法,等待线程执行结束再往下执行主程序(为了统计时间)
t2.join()
end_thread = time.time()
print(f'多线程执行总耗时{end_thread - start_thread}s')
if __name__ == '__main__':
print('-'*30)
main()
print('-'*30)
multi_thread()
同样的,从图中的运行结果可以看到,多线程也可以提高程序的运行效率。
在爬虫任务中使用多进程或多线程可以极大的提高爬虫的效率。通常我们更经常使用多线程来提高效率。因此我们将介绍多线程在爬虫中的应用,多进程的使用是类似的。
在前面的任务中,我们通过 Thread()
来开启新的线程,那如果我们想一次性多开启一些线程怎么办呢?
这时可以通过线程池来一次性开辟一些线程,把任务提交给线程池,由线程池来调度任务的具体执行。具体开辟多少个线程可以根据 CPU 的情况来决定。
线程池的基本用法如下:
from concurrent.futures import ThreadPoolExecutor
def func(name):
for _ in range(1000):
print(name, _)
if __name__ == '__main__':
with ThreadPoolExecutor(50) as t: # 一次性开辟 50 个线程
for i in range(100):
t.submit(func, name=f"线程{i}")
print("执行完毕") # with 外面的代码,会在所有线程完成后才执行
接下来,我们用上次 Python 爬虫的两个案例,来看看多进程在爬虫实战中的应用。首先是爬取静态网站历史天气的案例。
在这个例子中,我们通过 generate_urls()
函数一次性生成所有的网址。
正常执行时,我们通过一个 for
循环,逐次的执行爬取任务。使用多线程时,我们通过线程池一次性构造很多线程,然后把爬取任务分配给各个线程去执行。
from concurrent.futures import ThreadPoolExecutor
# 定义爬取函数
def crawler(url, save_path):
response = get_response(url)
results = parse_data(response)
save_data(results, save_path)
print(f'成功爬取数据:{url}')
if __name__ == '__main__':
urls = generate_urls()
save_file = 'weather.csv'
# 正常执行
# for u in urls: # 在网址中循环
# time.sleep(2) # 每次爬取休息 2 秒,以免太过频繁的请求
# crawler(u, save_file) # 进行爬取
# 多线程执行
with ThreadPoolExecutor(5) as t:
for u in urls:
t.submit(crawler, url=u, save_path=save_file) # 传入要执行的函数及其参数
同样的,在爬取动态网站 bilibili 视频评论的案例中,我们通过页面翻页循环来执行爬取任务。
正常执行时,我们通过 for
循环逐次翻页进行爬取。而多线程时,我们通过线程池构造多个线程,通过线程池进行多线程爬取。
from concurrent.futures import ThreadPoolExecutor
# 定义爬取函数
def crawler(page, save_path):
response = get_response(page)
comments = parse_data(response)
save_data(comments, save_path)
print(f'成功爬取第{page+1}页')
if __name__ == '__main__':
save_file = 'bilibili.csv'
total_counts = 1000
# 正常执行
# for p in range(total_counts//20 + 1):
# crawler(p, save_file)
# 多线程执行
with ThreadPoolExecutor(5) as t:
for p in range(total_counts//20 + 1):
t.submit(crawler, page=p, save_path=save_file) # 传入要执行的函数及其参数
Note:产生如下推文列表的 Stata 命令为:
lianxh 爬
安装最新版lianxh
命令:
ssc install lianxh, replace
免费公开课
最新课程-直播课
专题 | 嘉宾 | 直播/回看视频 |
---|---|---|
⭐ 最新专题 | 文本分析、机器学习、效率专题、生存分析等 | |
研究设计 | 连玉君 | 我的特斯拉-实证研究设计,-幻灯片- |
面板模型 | 连玉君 | 动态面板模型,-幻灯片- |
面板模型 | 连玉君 | 直击面板数据模型 [免费公开课,2小时] |
⛳ 课程主页
⛳ 课程主页
关于我们
课程, 直播, 视频, 客服, 模型设定, 研究设计, stata, plus, 绘图, 编程, 面板, 论文重现, 可视化, RDD, DID, PSM, 合成控制法
等
连享会小程序:扫一扫,看推文,看视频……
扫码加入连享会微信群,提问交流更方便
✏ 连享会-常见问题解答:
✨ https://gitee.com/lianxh/Course/wikis
New!
lianxh
命令发布了:
随时搜索连享会推文、Stata 资源,安装命令如下:
. ssc install lianxh
使用详情参见帮助文件 (有惊喜):
. help lianxh