Python:多进程、多线程及其爬虫应用

发布时间:2022-04-27 阅读 698

Stata连享会   主页 || 视频 || 推文 || 知乎 || Bilibili 站

温馨提示: 定期 清理浏览器缓存,可以获得最佳浏览体验。

New! lianxh 命令发布了:
随时搜索推文、Stata 资源。安装:
. ssc install lianxh
详情参见帮助文件 (有惊喜):
. help lianxh
连享会新命令:cnssc, ihelp, rdbalance, gitee, installpkg

课程详情 https://gitee.com/lianxh/Course

⛳ Stata 系列推文:

PDF下载 - 推文合集

作者: 王颖 (四川大学)
邮箱: wangyingchn@outlook.com


目录


0. 前言

在执行 Python 任务的过程中,我们通常会想提高程序的效率。尤其在处理大量任务的时候,提高程序的效率可以节省大量的时间。

以数据爬取为例,爬虫的基本流程都是,请求数据 → 解析数据 → 存储数据。如果有一万条数据,这个流程就要重复执行一万次,而且是线性的执行。就像车流一样,如果只有一条车道,一万辆车排队通过就需要一万次。如果有了双车道,就可以减少一半的时间,如果有了四车道,时间可以再减少一半。

那我们在程序运行中,能否想办法多开几条车道呢?在 Python 中,可以通过开启多进程或者多线程,来提高程序运行的效率。我们将对 Python 中的多进程、多线程进行简单的介绍,并主要介绍多线程在 Python 爬虫中的应用。

1. 进程与线程

先简单的介绍一下进程与线程的含义和两者的区别。

进程(Process),是资源单位。 要运行一个程序时,操作系统会分配一部分内存空间和资源来运行这个程序,这就是进程。 比如电脑中运行 Word、Excel、微信、QQ 等,每个程序都是一个进程。

线程(Thread)是执行单元。每个进程要都需要执行,这个执行就由线程来完成。

打个比方,进程就好像一家公司,里面包括各种资源,比如办公室、办公桌、电脑等, 而线程就是公司里的员工,来执行具体的任务。

如果一家公司没有员工,那公司就无法运营。 同理,每个进程中至少会有一个线程。只要启动程序,进程中就默认会有一个主线程。 当然,为了提高效率,一个进程也可能有多个线程去执行(多招几个员工)。

进程和线程之间的简单区别如下表:

进程 线程
简单定义 资源单位 执行单位
资源占用
资源共享 进程间的资源相互独立 同一进程下的线程间共享资源
互相通信 进程间只能间接交流 线程间可以直接交流,一个线程可以控制同一进程下的其他线程
可靠程度 进程间不会互相影响 一个线程挂掉会导致整个进程挂掉

  1. 在 Python 实际应用中,更多使用多线程,因为消耗资源更少,且线程间共享内存、变量等,互相交流很容易。
  2. 与多进程、多线程相关的概念还有全局解释器锁(GIL)、互斥锁、线程守护等
  3. 还有其他方法也涉及提高程序运行效率,如队列、协程等。

2. Python 中的多进程

我们通过一个简单的虚拟任务,来看看在 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

可以看到,正常执行任务时,第一个任务结束后才执行第二个任务,任务的总耗时等于两个任务的耗时总和。而使用多进程时,两个任务可以同时进行,此时任务的总耗时不再是两个任务的耗时总和。可见,使用多进程确实可以提高程序的运行效率。

3. Python 中的多线程

接下来我们来看看 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()

同样的,从图中的运行结果可以看到,多线程也可以提高程序的运行效率。

多线程执行
多线程执行

4. 多线程在爬虫中的应用

在爬虫任务中使用多进程或多线程可以极大的提高爬虫的效率。通常我们更经常使用多线程来提高效率。因此我们将介绍多线程在爬虫中的应用,多进程的使用是类似的。

4.1 线程池

在前面的任务中,我们通过 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 外面的代码,会在所有线程完成后才执行

4.2 爬取静态网站(历史天气)

接下来,我们用上次 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)  # 传入要执行的函数及其参数

4.3 爬取动态网站( bilibili 视频评论)

同样的,在爬取动态网站 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)  # 传入要执行的函数及其参数

5. 参考资料

6. 相关推文

Note:产生如下推文列表的 Stata 命令为:
lianxh 爬
安装最新版 lianxh 命令:
ssc install lianxh, replace

相关课程

免费公开课

最新课程-直播课

专题 嘉宾 直播/回看视频
最新专题 文本分析、机器学习、效率专题、生存分析等
研究设计 连玉君 我的特斯拉-实证研究设计-幻灯片-
面板模型 连玉君 动态面板模型-幻灯片-
面板模型 连玉君 直击面板数据模型 [免费公开课,2小时]
  • Note: 部分课程的资料,PPT 等可以前往 连享会-直播课 主页查看,下载。

课程主页

课程主页

关于我们

  • Stata连享会 由中山大学连玉君老师团队创办,定期分享实证分析经验。
  • 连享会-主页知乎专栏,700+ 推文,实证分析不再抓狂。直播间 有很多视频课程,可以随时观看。
  • 公众号关键词搜索/回复 功能已经上线。大家可以在公众号左下角点击键盘图标,输入简要关键词,以便快速呈现历史推文,获取工具软件和数据下载。常见关键词:课程, 直播, 视频, 客服, 模型设定, 研究设计, stata, plus, 绘图, 编程, 面板, 论文重现, 可视化, RDD, DID, PSM, 合成控制法

连享会小程序:扫一扫,看推文,看视频……

扫码加入连享会微信群,提问交流更方便

✏ 连享会-常见问题解答:
https://gitee.com/lianxh/Course/wikis

New! lianxh 命令发布了:
随时搜索连享会推文、Stata 资源,安装命令如下:
. ssc install lianxh
使用详情参见帮助文件 (有惊喜):
. help lianxh