批评 Python 的人通常都会说 Python 的多线程编程太困难了,众所周知的全局解释器锁(Global Interpreter Lock,或称 GIL)使得多个线程的 Python 代码无法同时运行。因此,如果你并非 Python 开发者,而是从其他语言如 C++ 或者 Java 转过来的话,你会觉得 Python 的多线程模块并没有以你期望的方式工作。但必须澄清的是,只要以一些特定的方式,我们仍然能够编写出并发或者并行的 Python 代码,并对性能产生完全不同的影响。如果你还不理解什么是并发和并行,建议你百度或者 Google 或者 Wiki 一下。
在这篇阐述 Python 并发与并行编程的入门教程里,我们将写一小段从 Imgur 下载最受欢迎的图片的 Python 程序。我们将分别使用顺序下载图片和同时下载多张图片的版本。在此之前,你需要先注册一个 Imgur 应用。如果你还没有 Imgur 账号,请先注册一个。
这篇教程的 Python 代码在 3.4.2 中测试通过。但只需一些小的改动就能在 Python 2中运行。两个 Python 版本的主要区别是 urllib2 这个模块。
注:考虑到国内严酷的上网环境,译者测试原作的代码时直接卡在了注册 Imgur 账号这一步。因此为了方便起见,译者替换了图片爬取资源。一开始使用的某生产商提供的图片 API ,但不知道是网络原因还是其他原因导致程序在读取最后一张图片时无法退出。所以译者一怒之下采取了原始爬虫法,参考着 requests 和 beautifulsoup4 的文档爬取了某头条 253 张图片,以为示例。译文中的代码替换为译者使用的代码,如需原始代码请参考原文 Python Multithreading Tutorial: Concurrency and Parallelism 。
Python 多线程起步
首先让我们来创建一个名为 download.py
的模块。这个文件包含所有抓取和下载所需图片的函数。我们将全部功能分割成如下三个函数:
- get_links
- download_link
- setup_download_dir
第三个函数,setup_download_dir
将会创建一个存放下载的图片的目录,如果这个目录不存在的话。
我们首先结合 requests 和 beautifulsoup4 解析出网页中的全部图片链接。下载图片的任务非常简单,只要通过图片的 URL 抓取图片并写入文件即可。
代码看起来像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
download.py import json import os import requests from itertools import chain from pathlib import Path from bs4 import BeautifulSoup # 结合 requests 和 bs4 解析出网页中的全部图片链接,返回一个包含全部图片链接的列表 def get_links(url): req = requests.get(url) soup = BeautifulSoup(req.text, "html.parser") return [img.attrs.get('data-src') for img in soup.find_all('div', class_='img-wrap') if img.attrs.get('data-src') is not None] # 把图片下载到本地 def download_link(directory, link): img_name = '{}.jpg'.format(os.path.basename(link)) download_path = directory / img_name r = requests.get(link) with download_path.open('wb') as fd: fd.write(r.content) # 设置文件夹,文件夹名为传入的 directory 参数,若不存在会自动创建 def setup_download_dir(directory): download_dir = Path(directory) if not download_dir.exists(): download_dir.mkdir() return download_dir |
接下来我们写一个使用这些函数一张张下载图片的模块。我们把它命名为single.py
。我们的第一个简单版本的 图片下载器将包含一个主函数。它会调用 setup_download_dir
创建下载目录。然后,它会使用 get_links
方法抓取一系列图片的链接,由于单个网页的图片较少,这里抓取了 5 个网页的图片链接并把它们组合成一个列表。最后调用 download_link
方法将全部图片写入磁盘。这是 single.py
的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
single.py from time import time from itertools import chain from download import setup_download_dir, get_links, download_link def main(): ts = time() url1 = 'http://www.toutiao.com/a6333981316853907714' url2 = 'http://www.toutiao.com/a6334459308533350658' url3 = 'http://www.toutiao.com/a6313664289211924737' url4 = 'http://www.toutiao.com/a6334337170774458625' url5 = 'http://www.toutiao.com/a6334486705982996738' download_dir = setup_download_dir('single_imgs') links = list(chain( get_links(url1), get_links(url2), get_links(url3), get_links(url4), get_links(url5), )) for link in links: download_link(download_dir, link) print('一共下载了 {} 张图片'.format(len(links))) print('Took {}s'.format(time() - ts)) if __name__ == '__main__': main() """ 一共下载了 253 张图片 ݜ。但必须澄清的是,只要以一些特定的方式,我们仍然能够编写出并发或者并行的 Python 代码,并对性能产生完全不同的影响。如果你还不理解什么是并发和并行,建议你百度或者 Google 或者 Wiki 一下。
在这篇阐述 Python 并发与并行编程的入门教程里,我们将写一小段从 Imgur 下载最受欢迎的图片的 Python 程序。我们将分别使用顺序下载图片和同时下载多张图片的版本。在此之前,你需要先注册一个 Imgur 应用。如果你还没有 Imgur 账号,请先注册一个。 这篇教程的 Python 代码在 3.4.2 中测试通过。但只需一些小的改动就能在 Python 2中运行。两个 Python 版本的主要区别是 urllib2 这个模块。 注:考虑到国内严酷的上网环境,译者测试原作的代码时直接卡在了注册 Imgur 账号这一步。因此为了方便起见,译者替换了图片爬取资源。一开始使用的某生产商提供的图片 API ,但不知道是网络原因还是其他原因导致程序在读取最后一张图片时无法退出。所以译者一怒之下采取了原始爬虫法,参考着 requests 和 beautifulsoup4 的文档爬取了某头条 253 张图片,以为示例。译文中的代码替换为译者使用的代码,如需原始代码请参考原文 Python Multithreading Tutorial: Concurrency and Parallelism 。 Python 多线程起步首先让我们来创建一个名为
第三个函数, 我们首先结合 requests 和 beautifulsoup4 解析出网页中的全部图片链接。下载图片的任务非常简单,只要通过图片的 URL 抓取图片并写入文件即可。 代码看起来像这样:
接下来我们写一个使用这些函数一张张下载图片的模块。我们把它命名为
在我的笔记本上,这段脚本花费了 166 秒下载 253 张图片。请注意花费的时间因网络的不同会有所差异。166 秒不算太长。但如果我们要下载更多的图片呢?2530 张而不是 253 张。平均下载一张图片花费约 1.5 秒,那么 2530 张图片将花费约 28 分钟。25300 张图片将要 280 分钟。但好消息是通过使用并发和并行技术,其将显著提升下载速度。 接下来的代码示例只给出为了实现并发或者并行功能而新增的代码。为了方便起见,全部的 python 脚本可以在 这个GitHub的仓库 获取。(注:这是原作者的 GitHub 仓库,是下载 Imgur 图片的代码,本文的代码存放在这:concurrency-parallelism-demo |