8_future_threading_multiprocessing的封装(线程进程的高级api)


resource

future 中文文档

使用future处理并发

future指一种对象,表示异步执行的操作。这个概念的作用很大,是concurrent.futures模块和asyncio包 的基础

另外我想说的是 我们之前已经学了 threadingmultiprocessing ,而future实现了对threading和multiprocessing的更高级的抽象,也就是说 concurrent本质上都是对threadingmutiprocessing的封装。

思考一下为啥要封装? 想想我们为啥要写函数? 以及类?

方便调用 以及复用代码,管理代码。

封装之后肯定有 更多方便的 特性。帮助我们更好的使用线程和进程

例子(本章基本完全来自 fluntpython 这本书。)

为了高效处理网络I/O,需要使用并发,因为网络有很高的延迟,所以为了不浪费CPU周期去等待,最好在收到网络响应之前做些其他的事。
为了通过代码说明这一点,我写了三个示例程序,从网上下载20个国家的国旗图像。第一个示例程序flags.py是依序下载的:下载完一个图像,并将其保存在硬盘中之后,才请求下一个图像。另外两个脚本是并发下载的:几乎同时请求所有图像,每下载完一个文件就保存一个文件。flags_threadpool.py脚本使用concurrent.futures模块,而flags_asyncio.py 脚本使用 asyncio 包。

依序下载 flags.py

flags.py:依序下载的脚本;另外两个脚本会重用其中几个函数

"""Download flags of top 20 countries by population
Sequential version
Sample run::
    $ python3 flags.py
    BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN
    20 flags downloaded in 10.16s
"""
# BEGIN FLAGS_PY
import os
import time
import sys

import requests  
# 导入requests库。这个库不在标准库中,因此依照惯例,在导入标准库中的模块(os、time和sys)之后导入,
# 而且使用一个空行分隔开。

POP20_CC = ('CN IN US ID BR PK NG BD RU JP '
            'MX PH VN ET EG DE IR TR CD FR').split()  
# 列出人口最多的20个国家的ISO 3166国家代码,按照人口数量降序排列。

BASE_URL = 'http://flupy.org/data/flags'  
# 获取国旗图像的网站

DEST_DIR = './img/'
# 保存图像的本地目录。


def save_flag(img, filename):  
# 把img(字节序列)保存到DEST_DIR目录中,命名为filename。
    path = os.path.join(DEST_DIR, filename)
    try:
        with open(path, "wb") as f:
            f.write(img)
    except FileNotFoundError:
        os.mkdir(DEST_DIR)
        with open(path, "wb") as f:
            f.write(img)

def get_flag(cc):  
# 指定国家代码,构建URL,然后下载图像,返回响应中的二进制内容
    url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
    resp = requests.get(url)
    return resp.content


def show(text):  
# 显示一个字符串,然后刷新sys.stdout,这样能在一行消息中看到进度。在Python中得这么做,
# 因为正常情况下,遇到换行才会刷新stdout缓冲
    print(text, end=' ')
    sys.stdout.flush()


def download_many(cc_list):  
# download_many是与并发实现比较的关键函数。
    for cc in sorted(cc_list):  
    # 按字母表顺序迭代国家代码列表,明确表明输出的顺序与输入一致。返回下载的国旗数量。
        image = get_flag(cc)
        show(cc)
        save_flag(image, cc.lower() + '.gif')

    return len(cc_list)


def main(download_many):  
# main函数记录并报告运行download_many函数之后的耗时
    t0 = time.time()
    count = download_many(POP20_CC)
    elapsed = time.time() - t0
    msg = '\n{} flags downloaded in {:.2f}s'
    print(msg.format(count, elapsed))


if __name__ == '__main__':
    main(download_many)  
    # main函数必须调用执行下载的函数;我们把download_many函数当作参数传给main函数,
    # 这样main函数可以用作库函数,在后面的示例中接收download_many函数的其他实现。
# END FLAGS_PY


BD BR CD CN DE EG ET FR ID IN IR JP MX NG PH PK RU TR US VN 
20 flags downloaded in 44.47s
# 经测试这段代码在我的电脑上 耗时 44.47s

异步下载 flags_threadpool.py

flags_threadpool.py:使用futures.ThreadPoolExecutor类实现多线程下载的脚本

需要关注的就是这两个

其他的都是一些基础的爬虫代码,熟悉一下 requests 库的基本api 就完全没问题

  • futures.ThreadPoolExecutor

  • executor.map

"""Download flags of top 20 countries by population
ThreadPoolExecutor version
Sample run::
    $ python3 flags_threadpool.py
    BD retrieved.
    EG retrieved.
    CN retrieved.
    ...
    PH retrieved.
    US retrieved.
    IR retrieved.
    20 flags downloaded in 0.93s
"""
# BEGIN FLAGS_THREADPOOL
from concurrent import futures

# from flags import save_flag, get_flag, show, main  
# 重用flags模块(见flags.py)中的几个函数。

MAX_WORKERS = 20  # 设定ThreadPoolExecutor类最多使用几个线程。


def download_one(cc):  # 下载一个图像的函数;这是在各个线程中执行的函数。
    image = get_flag(cc)
    show(cc)
    save_flag(image, cc.lower() + '.gif')
    return cc


def download_many(cc_list):
    workers = min(MAX_WORKERS, len(cc_list))  
    # 设定工作的线程数量:使用允许的最大值(MAX_WORKERS)与要处理的数量之间较小的那个值,
    # 以免创建多余的线程。
    with futures.ThreadPoolExecutor(workers) as executor:  
    # 使用工作的线程数实例化ThreadPoolExecutor类;
    # executor.__exit__方法会调用executor.shutdown(wait=True)方法,
    # 它会在所有线程都执行完毕前阻塞线程
        res = executor.map(download_one, sorted(cc_list))  
        # map方法的作用与内置的map函数类似,不过download_one函数会在多个线程中并发调用;
        # map方法返回一个生成器,因此可以迭代,获取各个函数返回的值。

    return len(list(res)) 
     # 返回获取的结果数量;如果有线程抛出异常,异常会在这里抛出,
     # 这与隐式调用next( )函数从迭代器中获取相应的返回值一样。


if __name__ == '__main__':
    main(download_many)  
    # 调用flags模块中的main函数,传入download_many函数的增强版。
# END FLAGS_THREADPOOL

NG US FR JP IR PK CD DE EG BR MX CN PH ET VN BD RU ID IN TR 
20 flags downloaded in 22.82s
经测试 这段代码在我的电脑 上运行 耗时 22.82s

注意,这段代码的download_one函数其实是flags.pydownload_many函数的for循环体。编写并发代码时经常这样重构:把依序执行的for循环体改成函数,以便并发调用。


flags.py中被重用的函数

下面几个函数是flags.py中被重用的函数以及几个变量。

放到这里来。 避免看上面的代码被干扰到。

def save_flag(img, filename):  
# 把img(字节序列)保存到DEST_DIR目录中,命名为filename。
    path = os.path.join(DEST_DIR, filename)
    try:
        with open(path, "wb") as f:
            f.write(img)
    except FileNotFoundError:
        os.mkdir(DEST_DIR)
        with open(path, "wb") as f:
            f.write(img)


def get_flag(cc):  
# 指定国家代码,构建URL,然后下载图像,返回响应中的二进制内容
    url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
    resp = requests.get(url)
    return resp.content


def show(text):  
# 显示一个字符串,然后刷新sys.stdout,这样能在一行消息中看到进度。
# 在Python中得这么做,因为正常情况下,遇到换行才会刷新stdout缓冲
    print(text, end=' ')
    sys.stdout.flush()
    
def main(download_many):  
# main函数记录并报告运行download_many函数之后的耗时
    t0 = time.time()
    count = download_many(POP20_CC)
    elapsed = time.time() - t0
    msg = '\n{} flags downloaded in {:.2f}s'
    print(msg.format(count, elapsed))
    
POP20_CC = ('CN IN US ID BR PK NG BD RU JP '
            'MX PH VN ET EG DE IR TR CD FR').split() 

DEST_DIR = './img/'

BASE_URL = 'http://flupy.org/data/flags'

flags_threadpool_ac.py:把download_many函数中的executor.map方法换成executor.submit方法和futures.as_completed函数

  • executor.map

替换为

  • executor.submit
  • futures.as_completed

也就是说关注这两个 api 即可


def save_flag(img, filename):  
# 把img(字节序列)保存到DEST_DIR目录中,命名为filename。
    path = os.path.join(DEST_DIR, filename)
    try:
        with open(path, "wb") as f:
            f.write(img)
    except FileNotFoundError:
        os.mkdir(DEST_DIR)
        with open(path, "wb") as f:
            f.write(img)


def get_flag(cc):  
# 指定国家代码,构建URL,然后下载图像,返回响应中的二进制内容
    url = '{}/{cc}/{cc}.gif'.format(BASE_URL, cc=cc.lower())
    resp = requests.get(url)
    return resp.content


def show(text):  
	# 显示一个字符串,然后刷新sys.stdout,这样能在一行消息中看到进度。
	# 在Python中得这么做,因为正常情况下,遇到换行才会刷新stdout缓冲
    print(text, end=' ')
    sys.stdout.flush()
    
def main(download_many):  
# main函数记录并报告运行download_many函数之后的耗时
    t0 = time.time()
    count = download_many(POP20_CC)
    elapsed = time.time() - t0
    msg = '\n{} flags downloaded in {:.2f}s'
    print(msg.format(count, elapsed))
    
POP20_CC = ('CN IN US ID BR PK NG BD RU JP '
            'MX PH VN ET EG DE IR TR CD FR').split() 

DEST_DIR = './img/'

BASE_URL = 'http://flupy.org/data/flags'

"""Download flags of top 20 countries by population
ThreadPoolExecutor version 2, with ``as_completed``.
Sample run::
    $ python3 flags_threadpool.py
    BD retrieved.
    EG retrieved.
    CN retrieved.
    ...
    PH retrieved.
    US retrieved.
    IR retrieved.
    20 flags downloaded in 0.93s
"""
from concurrent import futures
import os
import time
import sys
import requests
# from flags import save_flag, get_flag, show, main

MAX_WORKERS = 20


def download_one(cc):
    image = get_flag(cc)
    show(cc)
    save_flag(image, cc.lower() + '.gif')
    return cc


# BEGIN FLAGS_THREADPOOL_AS_COMPLETED
def download_many(cc_list):
    cc_list = cc_list[:5] 
    # 这次演示只使用人口最多的5个国家。
    with futures.ThreadPoolExecutor(max_workers=3) as executor:  
    # 把max_workers硬编码为3,以便在输出中观察待完成的future。
        to_do = []
        for cc in sorted(cc_list):  
        # 按照字母表顺序迭代国家代码,明确表明输出的顺序与输入一致。
            future = executor.submit(download_one, cc)  
            # executor.submit方法排定可调用对象的执行时间,然后返回一个future,表示这个待执行的操作。
            to_do.append(future)  
            # 存储各个future,后面传给as_completed函数。
            msg = 'Scheduled for {}: {}'
            print(msg.format(cc, future))  
            # 显示一个消息,包含国家代码和对应的future。

        results = []
        for future in futures.as_completed(to_do):  
        # as_completed函数在future运行结束后产出future。
            res = future.result()  
            # 获取该future的结果。
            msg = '{} result: {!r}'
            print(msg.format(future, res)) 
            # 显示future及其结果
            results.append(res)

    return len(results)
# END FLAGS_THREADPOOL_AS_COMPLETED

if __name__ == '__main__':
    main(download_many)


注意,在这个示例中调用future.result()方法绝不会阻塞,因为futureas_completed函数产出。

阻塞型 I/O 和 GIL

执行耗时的任务时,可以使用一个内置的函数或一个使用C语言编写的扩展释放GIL。

标准库中所有执行阻塞型I/O操作的函数,在等待操作系统返回结果时都会释放GIL。这意味着在Python语言这个层次上可以使用多线程,而I/O密集型Python程序能从中受益 :一个Python线程等待网络响应时,阻塞型I/O函数会释放GIL,再运行一个线程

time.sleep()函数也会释放GIL。因此,尽管有GIL,Python线程还是能在I/O密集型应用中发挥作用

使用 concurrent.futures 模块启动进程

ProcessPoolExecutorThreadPoolExecutor类都实现了通用的Executor接口,因此使用concurrent.futures模块能特别轻松地把基于线程的方案转成基于进程的方案。

def download_many(cc_list):
    workers = min(MAX_WORKERS, len(cc_list))
    with futures.ThreadPoolExecutor(workers) as executor:
    # 我们需要传入 需要开启的线程数

改成:

def download_many(cc_list):
    with futures.ProcessPoolExecutor() as executor:
    # 这个futures.ProcessPoolExecutor 一般无需传入参数。 他的默认参数是 os.cpu_count() 你的cpu核心数

ProcessPoolExecutor的价值体现在CPU密集型作业上

所以针对网络IO 提升并不大。

实验 Executor.map 方法


from time import sleep, strftime
from concurrent import futures
def display(*args): 
# 这个函数的作用很简单,把传入的参数打印出来,并在前面加上[HH:MM:SS]格式的时间戳
    print(strftime('[%H:%M:%S]'), end=' ')
    print(*args)
def loiter(n):  
# loiter函数什么也没做,只是在开始时显示一个消息,然后休眠n秒,最后在结束时再显示一个消息;消息使用制表符缩进,缩进的量由n的值确定
    msg = '{}loiter({}): doing nothing for {}s...'
    display(msg.format('\t'*n, n, n))
    sleep(n)
    msg = '{}loiter({}): done.'
    display(msg.format('\t'*n, n))
    return n * 10  
    # loiter函数返回n * 10,以便让我们了解收集结果的方式
def main():
    display('Script starting.')
    executor = futures.ThreadPoolExecutor(max_workers=3)  
    # 创建ThreadPoolExecutor实例,有3个线程。
    results = executor.map(loiter, range(5))  
    # 把五个任务提交给executor(因为只有3个线程,所以只有3个任务会立即开始:loiter(0)、loiter(1)和loiter(2));这是非阻塞调用。
    display('results:', results)  
    # 立即显示调用executor.map方法的结果:一个生成器
    display('Waiting for individual results:')
    for i, result in enumerate(results): 
    # for循环中的enumerate函数会隐式调用next(results),这个函数又会在(内部)表示第一个任务(loiter(0))的_f future上调用_f.result() 方法。result方法会阻塞,直到 future 运行结束,因此这个循环每次迭代时都要等待下一个结果做好准备
        display('result {}: {}'.format(i, result))
main()


运行结果

[14:49:31] Script starting.
# 这次运行从 14:49:31 开始
[14:49:31] loiter(0): doing nothing for 0s...
# 第一个线程执行loiter(0),因此休眠0秒,甚至会在第二个线程开始之前就结束,不过具体情况因人而异。
[14:49:31] loiter(0): done.
[14:49:31] 	loiter(1): doing nothing for 1s...
# loiter(1)和loiter(2)立即开始(因为线程池中有三个线程,可以并发运行三个函数)。
[14:49:31] 		loiter(2): doing nothing for 2s...
[14:49:31] results: <generator object Executor.map.<locals>.result_iterator at 0x05BBF360>
# 这一行表明,executor.map方法返回的结果(results)是生成器;不管有多少任务,也不管max_workers的值是多少,目前不会阻塞。
[14:49:31] Waiting for individual results:
[14:49:31] result 0: 0
# 此时执行过程可能阻塞,具体情况取决于传给loiter函数的参数:results生成器的__next__方法必须等到第一个future运行结束。此时不会阻塞,因为loiter(0)在循环开始前结束。注意,这一点之前的所有事件都在同一刻发生——14:49:31。
[14:49:31] 			loiter(3): doing nothing for 3s...
# loiter(0)运行结束了,第一个职程可以启动第四个线程,运行loiter(3)。
[14:49:32] 	loiter(1): done.
# 一秒钟后,即14:49:32,loiter(1)运行完毕。这个线程闲置,可以开始运行loiter(4)。
[14:49:32][14:49:32] result 1: 10
 				loiter(4): doing nothing for 4s...
 # 显示loiter(1)的结果:10。现在,for循环会阻塞,等待loiter(2)的结果。
[14:49:33] 		loiter(2): done.
[14:49:33] result 2: 20
# 同上:loiter(2)运行结束,显示结果;loiter(3)也一样。
[14:49:34] 			loiter(3): done.
[14:49:34] result 3: 30
[14:49:36] 				loiter(4): done.
# 2秒钟后loiter(4)运行结束,因为loiter(4)在14:49:32时开始,休眠了4秒。
[14:49:36] result 4: 40

:这个函数(executor.map)返回结果的顺序与调用开始的顺序一致

如果第一个调用生成结果用时10秒,而其他调用只用1秒,代码会阻塞10秒,获取map方法返回的生成器产出的第一个结果。

在此之后,获取后续结果时不会阻塞,因为后续的调用已经结束。如果必须等到获取所有结果后再处理,这种行为没问题;

不过,通常更可取的方式是,不管提交的顺序,只要有结果就获取。为此,要把Executor.submit方法和futures.as_completed函数结合起来使用

executor.submitfutures.as_completed这个组合比executor.map 更灵活,因为submit方法能处理不同的可调用对象和参数,而executor.map只能处理参数不同的同一个可调用对象

传给futures.as_completed函数的future集合可以来自多个Executor实例,例如一些由ThreadPoolExecutor实例创建,另一些由ProcessPoolExecutor实例创建。


显示下载进度并处理错误

需要配置 nginx 服务器

如果要尝试使用flags2*.py该目录中的图像下载示例需要配置 服务器

标志下载示例旨在比较从Web查找和下载文件的不同方法的性能。但是,我们不想在测试时以每秒多个请求的速度访问公共服务器,我们希望能够模拟高延迟和随机网络错误。

对于此设置,我选择Nginx作为HTTP服务器,因为它配置起来非常快捷,容易,而选择了Vaurien代理,因为它是由Mozilla设计的,目的是引入延迟和网络错误以测试Web服务。

将这些文件解压缩到flags/目录并配置了Nginx之后,您就可以尝试使用flags2*.py示例,而无需访问网络。

图片文件下载链接。

https://raw.githubusercontent.com/onepisYa/fluentpython/master/17-futures/countries/flags.zip

测试服务器配置

1. Unpack test data(解压测试数据)

本节中的简介适用于使用命令行的 GNU / LinuxOSXWindows用户使用GUI 进行解压安装 ,进行相同的操作应该没有困难。

解压 countries/ 目录:

$ unzip flags.zip
... many lines omitted ...
creating: flags/zw/
inflating: flags/zw/metadata.json
inflating: flags/zw/zw.gif

验证在194个目录countries/flags/,每个目录都有一个.gif和一个metadata.json文件:

  $ ls flags | wc -w
  194
  $ find flags | grep .gif | wc -l
  194
  $ find flags | grep .json | wc -l
  194
  $ ls flags/ad
  ad.gif      metadata.json
2. Install Nginx(安装Nginx)

下载并安装Nginx。这里使用的是1.6.2版

  • Download page: http://nginx.org/en/download.html

  • Beginner’s guide: http://nginx.org/en/docs/beginners_guide.html

windows直接下载下来解压就好了,最多再添加一下环境变量

在这里插入图片描述

3. Configure Nginx(配置Nginx)

编辑 nginx.conf 文件 设置 端口和文档根目录。 您可以通过运行确定使用哪个 nginx.conf

$ nginx -V

输出以下内容开头

nginx version: nginx/1.6.2
built by clang 6.0 (clang-600.0.51) (based on LLVM 3.5svn)
TLS SNI support enabled
configure arguments:...

configure参数中,您将看到 --conf-path=。那就是您要编辑的文件。

其中的大多数内容nginx.conf都在带有http大括号并标记为大括号的块内。在该块内可以有多个标记为的块server。添加另一个server像这样的块:

  server {
      listen       8001;

      location /flags/ {
          root   /full-path-to.../countries/;
      }
  }

这是我的配置 要写全路径

    server {
        listen       8001;

        location /flags/ {
            root   F:/bai/Pictures/Img/;
        }
    }

编辑nginx.conf后,必须启动服务器(如果服务器未运行)或告诉服务器重新加载配置文件:

这个命令 如果你配置了环境变量 那么 就可以任何地方 使用。

如果像我一样不喜欢配置环境变量 那么 就cd 到 你 有 nginx.exe 的那个目录,再使用 nginx 命令 启动

  $ nginx  # to start, if necessary
  $ nginx -s reload  # 重启配置

要测试配置,请在浏览器中打开URL http://localhost:8001/flags/ad/ad.gif。您应该看到安道尔的蓝色,黄色和红色标志。

如果测试失败,请仔细检查刚刚描述的过程,并参考Nginx文档。

windows 配置文件 就在 conf 文件夹下

在这里插入图片描述
在这里插入图片描述
关于 flags里面的图片

在这里插入图片描述

在这里插入图片描述
配置成功之后 输入http://localhost:8001/flags/cn/cn.gif 就可以访问到 国旗

在这里插入图片描述


需要 cd 到 有 py文件的目录

此时,您可以flags_*2.py通过提供--server LOCAL 命令行选项针对Nginx安装运行示例。


  $ python3 flags2_threadpool.py -s LOCAL
  
  LOCAL site: http://localhost:8001/flags
  Searching for 20 flags: from BD to VN
  20 concurrent connections will be used.
  --------------------
  20 flags downloaded.
  Elapsed time: 0.09s

这是我的运行截图,花费了 2.13s 下载了 20 个国旗。

在这里插入图片描述

请注意,Nginx是如此之快,以至于您不会在顺序版本和并发版本之间看到多少运行时间差异。为了通过模拟的网络滞后进行更实际的测试,我们需要设置Vaurien代理。

不使用 也没有关系。

其实我们 前面已经学会了 如何使用 executor.mapexecutor.submitfutures.as_completed

其他的我们完全可以自己搞定,当然我们也可以学学 fluntpython 作者的一些 编码习惯,以及编码方式,看看大神是如何思考的。

4. Install and run Vaurien(安装和运行Vaurien)

 $ pip install vaurien
 
  Downloading/unpacking vaurien
    Downloading vaurien-1.9.tar.gz (50kB): 50kB downloaded
  ...many lines and a few minutes later...

  Successfully installed vaurien cornice gevent statsd-client vaurienclient
  greenlet http-parser pyramid simplejson requests zope.interface
  translationstring PasteDeploy WebOb repoze.lru zope.deprecation venusian
  Cleaning up...

执行下面命令

#!/bin/bash
不过这个命令好像要 python2 才能使用。

我尝试用 2to3 转换但是好像依然无法使用 

实在不行 也别纠结。 就别加延迟好了。 或者在结束 直接 加个 0.5 的 time.sleep() 好了。


vaurien --protocol http --backend localhost:8001 --proxy localhost:8002  --behavior 100:delay --behavior-delay-sleep .5
        
        


tqdm 进度条 简单使用

需要安装 因为是第三方库

安装方式 => pip install tqdm

import time
from tqdm import tqdm
for i in tqdm(range(100)):
    time.sleep(0.1)

tqdm函数的实现方式也很有趣:能处理任何可迭代的对象,生成一个迭代器;使用这个迭代器时,显示进度条和完成全部迭代预计的剩余时间。为了计算预计剩余时间,tqdm函数要获取一个能使用len函数确定大小的可迭代对象,或者在第二个参数中指定预期的元素数量。

在这里插入图片描述

progress 进度条使用

更加简单的 进度条库。

两个都可以 不过我还是推荐 用 tqdm

from progress.bar import Bar

bar = Bar('Loading', fill='#', suffix='%(percent)d%% 耗时:%(elapsed)ds 剩余: %(eta)ds')
import time

for i in range(100):
    # Do some work
    time.sleep(0.1)
    bar.next()
bar.finish()

我们必须使用futures.as_completed函数,这样tqdm函数才能在每个future运行结束后更新进度。

flags2_common.py

country_codes.txt

在这里插入图片描述
我把 save_flag 改造了一下。 如果没有这个文件夹 那就新建一个。

不然的话 朋友们 要 测试的话, 就要自己建立文件夹了。

在这里插入图片描述


"""Utilities for second set of flag examples.
flags2_common.py

这个模块中包含所有flags2示例通用的函数和设置,例如main函数,负责解析命令行参数、计时和报告结果。

其实与本节没有 直接的联系,仅仅提供支持

意思就是 后面的 代码 需要 重用 这个 文件 中的 函数

需要导入 它。
"""

import os
import time
import sys
import string
import argparse
from collections import namedtuple
from enum import Enum


Result = namedtuple('Result', 'status data')
# 生成一个 Enum Status对象  第二个参数 用空格 分开  就是三个 状态
# 一个是 ok  一个是  not_found 一个 是 error
HTTPStatus = Enum('Status', 'ok not_found error')
# 一个 旗子的名字的列表
POP20_CC = ('CN IN US ID BR PK NG BD RU JP '
            'MX PH VN ET EG DE IR TR CD FR').split()
# 线程数 默认
DEFAULT_CONCUR_REQ = 1
# 最大线程数
MAX_CONCUR_REQ = 1

# 命令行参数
# 不过由于我的 py3 无法使用 那个代理。
# 所以无法开启延迟,我就没有测试 延迟 和错误
SERVERS = {
    'REMOTE': 'http://flupy.org/data/flags',
    'LOCAL':  'http://localhost:8001/flags',
    'DELAY':  'http://localhost:8002/flags',
    'ERROR':  'http://localhost:8003/flags',
}
# 默认服务 是 LOCAL
DEFAULT_SERVER = 'LOCAL'

# 默认下载地址
DEST_DIR = 'downloads/'
# 所有旗子的名字的 txt 文件
COUNTRY_CODES_FILE = 'country_codes.txt'

# 保存文件的 旗子的函数
def save_flag(img, filename):
    path = os.path.join(DEST_DIR, filename)
    try:
        with open(path, 'wb') as fp:
            fp.write(img)
    except FileNotFoundError:
        os.mkdir(DEST_DIR)
        with open(path, 'wb') as fp:
            fp.write(img)

# 输出初始化报告
def initial_report(cc_list, actual_req, server_label):
    if len(cc_list) <= 10:
        cc_msg = ', '.join(cc_list)
    else:
        cc_msg = 'from {} to {}'.format(cc_list[0], cc_list[-1])
    print('{} site: {}'.format(server_label, SERVERS[server_label]))
    msg = 'Searching for {} flag{}: {}'
    plural = 's' if len(cc_list) != 1 else ''
    print(msg.format(len(cc_list), plural, cc_msg))
    plural = 's' if actual_req != 1 else ''
    msg = '{} concurrent connection{} will be used.'
    print(msg.format(actual_req, plural))

# 输出最终报告的函数
def final_report(cc_list, counter, start_time):
	# 计算消耗的时间
    elapsed = time.time() - start_time
    print('-' * 20)
    msg = '{} flag{} downloaded.'
    # 如果 等于1 那么 就不在 flag 后面加 s 否则就加 s
    plural = 's' if counter[HTTPStatus.ok] != 1 else ''
    print(msg.format(counter[HTTPStatus.ok], plural))
    if counter[HTTPStatus.not_found]:
        print(counter[HTTPStatus.not_found], 'not found.')
    if counter[HTTPStatus.error]:
        plural = 's' if counter[HTTPStatus.error] != 1 else ''
        print('{} error{}.'.format(counter[HTTPStatus.error], plural))
    print('Elapsed time: {:.2f}s'.format(elapsed))

# 生成更多的 旗子名字的 列表
def expand_cc_args(every_cc, all_cc, cc_args, limit):
    codes = set()
    A_Z = string.ascii_uppercase
    if every_cc:
        codes.update(a+b for a in A_Z for b in A_Z)
    elif all_cc:
        with open(COUNTRY_CODES_FILE) as fp:
            text = fp.read()
        codes.update(text.split())
    else:
        for cc in (c.upper() for c in cc_args):
            if len(cc) == 1 and cc in A_Z:
                codes.update(cc+c for c in A_Z)
            elif len(cc) == 2 and all(c in A_Z for c in cc):
                codes.add(cc)
            else:
                msg = 'each CC argument must be A to Z or AA to ZZ.'
                raise ValueError('*** Usage error: '+msg)
    return sorted(codes)[:limit]

# 此函数用于处理命令行参数
def process_args(default_concur_req):
    server_options = ', '.join(sorted(SERVERS))
    parser = argparse.ArgumentParser(
                description='Download flags for country codes. '
                'Default: top 20 countries by population.')
    parser.add_argument('cc', metavar='CC', nargs='*',
                help='country code or 1st letter (eg. B for BA...BZ)')
    parser.add_argument('-a', '--all', action='store_true',
                help='get all available flags (AD to ZW)')
    parser.add_argument('-e', '--every', action='store_true',
                help='get flags for every possible code (AA...ZZ)')
    parser.add_argument('-l', '--limit', metavar='N', type=int,
                help='limit to N first codes', default=sys.maxsize)
    parser.add_argument('-m', '--max_req', metavar='CONCURRENT', type=int,
                default=default_concur_req,
                help='maximum concurrent requests (default={})'
                      .format(default_concur_req))
    parser.add_argument('-s', '--server', metavar='LABEL',
                default=DEFAULT_SERVER,
                help='Server to hit; one of {} (default={})'
                      .format(server_options, DEFAULT_SERVER))
    parser.add_argument('-v', '--verbose', action='store_true',
                help='output detailed progress info')
    args = parser.parse_args()
    if args.max_req < 1:
        print('*** Usage error: --max_req CONCURRENT must be >= 1')
        parser.print_usage()
        sys.exit(1)
    if args.limit < 1:
        print('*** Usage error: --limit N must be >= 1')
        parser.print_usage()
        sys.exit(1)
    args.server = args.server.upper()
    if args.server not in SERVERS:
        print('*** Usage error: --server LABEL must be one of',
              server_options)
        parser.print_usage()
        sys.exit(1)
    try:
        cc_list = expand_cc_args(args.every, args.all, args.cc, args.limit)
    except ValueError as exc:
        print(exc.args[0])
        parser.print_usage()
        sys.exit(1)

    if not cc_list:
        cc_list = sorted(POP20_CC)
    return args, cc_list

# 主函数 下载函数传递进去,然后 默认线程和 最大线程
# 调用 命令行参数 ,然后提取出来 解包 给 args 和  cc_list
# 并调用开始的 初始化报告
# 在中间调用 download_many 函数
# 以及在结束后调用 最后的报告函数 
def main(download_many, default_concur_req, max_concur_req):
    args, cc_list = process_args(default_concur_req)
    actual_req = min(args.max_req, max_concur_req, len(cc_list))
    initial_report(cc_list, actual_req, args.server)
    base_url = SERVERS[args.server]
    t0 = time.time()
    counter = download_many(cc_list, base_url, args.verbose, actual_req)
    assert sum(counter.values()) == len(cc_list), \
        'some downloads are unaccounted for'
    final_report(cc_list, counter, t0)

flags2_sequential.py

这个脚本讲解了 如何 处理异常

以及集成 tqdm 进度条

Enum模块文档

collections 模块文档

Enum一般我们生成常量 就是用这个 模块

可以看到我 准备 修改 他的时候 报错了。

在这里插入图片描述

关于 namedtuple 的使用
在这里插入图片描述

"""Download flags of countries (with error handling).
Sequential version
Sample run::
    $ python3 flags2_sequential.py -s LOCAL
    
flags2_sequential.py 

这个脚本是依序下载的脚本 

import 导入的函数 依赖于 前面的 flags2_common.py

这个 依序下载 总共就 写了 三个函数 

一个  download_one , download_many , get_flag

其他的都是 调用的 flags2_common.py 的东西。

"""

import collections

import requests
import tqdm

from flags2_common import main, save_flag, HTTPStatus, Result
# Result 是从 flag2_common 导入的一个变量  nametuple 类型
# HTTPStatus 是一个 Enum 类型 的  变量
# main 和 save_flag 是 函数 。 

DEFAULT_CONCUR_REQ = 1
# 这里设置了 默认的线程
MAX_CONCUR_REQ = 1
# 最大线程 
# 这里都设置为 1 所以  这是 依序下载的 

# BEGIN FLAGS2_BASIC_HTTP_FUNCTIONS
def get_flag(base_url, cc):
    url = '{}/{cc}/{cc}.gif'.format(base_url, cc=cc.lower())
    resp = requests.get(url)
    # requests.get 发送请求
    if resp.status_code != 200:  
    # 其实直接写  resp.raise_for_status() 不判断 也会自动抛出异常的
    # get_flag函数没有处理错误,当HTTP代码不是200时,
    # 使用requests.Response.raise_for_status方法抛出异常。
        resp.raise_for_status()
    return resp.content


def download_one(cc, base_url, verbose=False):
    try:
        image = get_flag(base_url, cc)
    except requests.exceptions.HTTPError as exc:  
    # download_one函数捕获requests.exceptions.HTTPError异常,特别处理HTTP 404错误……
        res = exc.response
        if res.status_code == 404:
            status = HTTPStatus.not_found  
            # 把局部变量status设为HTTPStatus.not_found,
            # HTTPStatus是从flags2_common模块)中导入的Enum对象。
            msg = 'not found'
        else:  
        # 重新抛出其他HTTPError异常;这些异常会向上冒泡,传给调用方。
            raise
    else:
    # else 代表没报错,所以执行 保存 
        save_flag(image, cc.lower() + '.gif')
        status = HTTPStatus.ok
        msg = 'OK'

    if verbose:  
    # 如果在命令行中设定了-v/--verbose选项,显示国家代码和状态消息;这就是详细模式中看到的进度信息。
        print(cc, msg)
	# 将 状态 和 旗子名字传进去
	# 状态时 status , 而 data 就是 cc  旗子的名字
    return Result(status, cc)  
    # download_one函数的返回值是一个namedtuple——Result,
    # 其中有个status字段,其值是HTTPStatus.not_found或HTTPStatus.ok。
# END FLAGS2_BASIC_HTTP_FUNCTIONS

# BEGIN FLAGS2_DOWNLOAD_MANY_SEQUENTIAL
def download_many(cc_list, base_url, verbose, max_req):
    counter = collections.Counter()  
    # 这个Counter实例用于统计不同的下载状态:
    # HTTPStatus.ok、HTTPStatus.not_found或HTTPStatus.error。
    cc_iter = sorted(cc_list)  
    # 按字母顺序传入的国家代码列表,
    # 保存在cc_iter变量中
    if not verbose:
    	# 如果不是 详细模式 就使用进度条
        cc_iter = tqdm.tqdm(cc_iter)  
        # 如果不是详细模式,把cc_iter传给tqdm函数,返回一个迭代器,
        # 产出cc_iter中的元素,还会显示进度条动画。
    for cc in cc_iter:  # 这个for循环迭代cc_iter
        try:
        	# 保存图片 并且将 状态 和对应的旗子 名字放入 namedtuple 返回给 res变量
            res = download_one(cc, base_url, verbose)  
            # 不断调用download_one函数,执行下载。
        except requests.exceptions.HTTPError as exc:  
        # 处理get_flag函数抛出的与HTTP有关的且download_one函数没有处理的异常。
            error_msg = 'HTTP error {res.status_code} - {res.reason}'
            error_msg = error_msg.format(res=exc.response)
        except requests.exceptions.ConnectionError as exc:  
        # 处理其他与网络有关的异常。其他异常会中止这个脚本,
        # 因为调用download_many函数的flags2_common.main函数中没有try/except块。
            error_msg = 'Connection error'
        else:  
        # 如果没有异常从download_one函数中逃出,
        # 从download_one函数返回的namedtuple(HTTPStatus)中获取status。
            error_msg = ''
            status = res.status

        if error_msg:
            status = HTTPStatus.error  
            # 如果有错误,把局部变量status设为相应的状态。
        counter[status] += 1  
        # 以HTTPStatus(一个Enum)中的值为键,增加计数器
        if verbose and error_msg: 
        # 如果是详细模式,而且有错误,显示带有当前国家代码的错误消息
            print('*** Error for {}: {}'.format(cc, error_msg))

    return counter  
    # 返回counter对象,以便main函数能在最终的报告中显示数量。
# END FLAGS2_DOWNLOAD_MANY_SEQUENTIAL

if __name__ == '__main__':
    main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)

flags2_threadpool.py

不能忽略的选项是-s/--server:用于选择测试时使用的HTTP服务器和基URL。这个选项的值可以设为下述4个字符串(不区分大小写),用于确定脚本从哪里下载国旗。

  • LOCAL
    使用http://localhost:8001/flags;这是默认值。你应该配置一个本地HTTP服务器,响应8001端口的请求。我测试时使用Nginx。
  • REMOTE
    使用http://flupy.org/data/flags;这是fluntpython搭建的公开网站,托管在一个共享服务器中。请不要使用太多并发请求访问这个网站。flupy.org域名由Cloudflare CDN的一个免费账户管理,因此第一次下载时会发现很慢,不过一旦CDN有了缓存,速度就会变快。

在这里插入图片描述

"""Download flags of countries (with error handling).
这个 多线程的函数 只是写了一下 download_many 函数
然后重用了  download_one 函数
main 函数
HTTPStatus 变量
"""

# BEGIN FLAGS2_THREADPOOL
import collections
from concurrent import futures

import requests
import tqdm  # 导入显示进度条的库

from flags2_common import main, HTTPStatus
# 从flags2_common.py 模块中导入一个函数和一个Enum。
from flags2_sequential import download_one
# 重用flags2_sequential.py 模块 里的download_one函数。

DEFAULT_CONCUR_REQ = 30
# 如果没有在命令行中指定-m/--max_req选项,
# 使用这个值作为并发请求数的最大值,也就是线程池的大小;真实的数量可能会比这少,例如下载的国旗数量较少。
MAX_CONCUR_REQ = 1000
# 不管要下载多少国旗,也不管-m/--max_req命令行选项的值是多少,
# MAX_CONCUR_REQ会限制最大的并发请求数;这是一项安全预防措施。


def download_many(cc_list, base_url, verbose, concur_req):
    counter = collections.Counter()
    with futures.ThreadPoolExecutor(max_workers=concur_req) as executor:
        # 把max_workers设为concur_req,创建ThreadPoolExecutor实例;
        # main函数会把下面这三个值中最小的那个赋值给concur_req:MAX_CONCUR_REQ、cc_list的长度、
        # -m/--max_req命令行选项的值。这样能避免创建超过所需的线程
        to_do_map = {}
        # 这个字典把各个Future实例(表示一次下载)映射到相应的国家代码上,在处理错误时使用
        for cc in sorted(cc_list):
            # 按字母顺序迭代国家代码列表。结果的顺序主要由HTTP响应的时间长短决定,
            # 不过,如果线程池的大小(由concur_req设定)比len(cc_list)小得多,
            # 可能会发现有按字母顺序批量下载的情况
            future = executor.submit(download_one, cc, base_url, verbose)
            # 每次调用executor.submit方法排定一个可调用对象的执行时间,
            # 然后返回一个Future实例。第一个参数是可调用的对象,
            # 其余的参数是传给可调用对象的参数。
            to_do_map[future] = cc
            # 把返回的future和国家代码存储在字典中。
        done_iter = futures.as_completed(to_do_map)
        # futures.as_completed函数返回一个迭代器,在future运行结束后产出future
        if not verbose:
            # 进度条
            done_iter = tqdm.tqdm(done_iter, total=len(cc_list))
            # 如果不是详细模式,把as_completed函数返回的结果传给tqdm函数,显示进度条;
            # 因为done_iter没有len函数,所以我们必须通过total=参数告诉tqdm函数预期的元素数量,
            # 这样tqdm才能预计剩余的工作量。
        for future in done_iter:
            # 迭代运行结束后的future。
            try:
                res = future.result()
                # 在future上调用result方法,要么返回可调用对象的返回值,
                # 要么抛出可调用的对象在执行过程中捕获的异常。这个方法可能会阻塞,等待确定结果;
                # 不过,在这个示例中不会阻塞,因为as_completed函数只返回已经运行结束的future。
            except requests.exceptions.HTTPError as exc:
                # 处理可能出现的异常;这个函数余下的代码与依序下载版download_many函数一样 不过下一点除外。
                error_msg = 'HTTP {res.status_code} - {res.reason}'
                error_msg = error_msg.format(res=exc.response)
            except requests.exceptions.ConnectionError as exc:
                print(exc)
                error_msg = 'Connection error'
            else:
                error_msg = ''
                status = res.status

            if error_msg:
                status = HTTPStatus.error
            counter[status] += 1
            if verbose and error_msg:
                cc = to_do_map[future]
                # 为了给错误消息提供上下文,以当前的future为键,从to_do_map中获取国家代码。
                # 在依序下载版中无须这么做,因为那一版迭代的是国家代码,所以知道当前国家的代码;
                # 而这里迭代的是future
                print('*** Error for {}: {}'.format(cc, error_msg))

    return counter


if __name__ == '__main__':
    main(download_many, DEFAULT_CONCUR_REQ, MAX_CONCUR_REQ)
# END FLAGS2_THREADPOOL

用到了一个对futures.as_completed 函数特别有用惯用法:构建一个字典,把各个future映射到其他数据(future运行结束后可能有用)上。这里,在to_do_map中,我们把各个future映射到对应的国家代码上。这样,尽管future生成的结果顺序已经乱了,依然便于使用结果做后续处理。

因此我觉得concurrent.futures包很棒,它把线程、进程和队列视作服务的基础设施,不用自己动手直接处理。当然,这个包针对的是简单的作业,也就是所谓的“高度并行”问题。编写应用(而非操作系统或数据库服务器)时,遇到的大部分并发问题都属于这一种(高度并行)。

JavaScript中,只能通过回调式异步编程实现并发。我提到这些是因为,RubyJavaScript是最能直接与Python竞争的通用动态编程语言



改写 executor.map 下载表情包

import os
import re
import string
import requests
from lxml import etree
import time
from concurrent import futures
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64)',
    'Referer': 'http://www.doutula.com/'
}
PAT = string.punctuation
SAVEPATH = "".join([".", os.sep, "img"])
page_url_template = 'http://www.doutula.com/photo/list/?page={}'
get = requests.get


def gen_html_obj(url):
    try:
        r = get(url, headers=HEADERS, timeout=30)
        r.raise_for_status()  # 如果状态不是200 引发 异常
        r.encoding = "utf-8"  # 设置编码
        html = etree.HTML(r.text)
        return html
    except BaseException as exc:
        print(exc)


def save_(img_url, filename):
    res = get(img_url, headers=HEADERS).content
    save_name = "".join([SAVEPATH, os.sep, filename])
    try:
        with open(save_name, "wb") as f:
            f.write(res)
    except FileNotFoundError:
        os.mkdir(SAVEPATH)
        with open(save_name, "wb") as f:
            f.write(res)
    # print(img_url)


def handle_img_node(img):
    """
    从图片节点提取属性
    构造文件名字,以及获取图片url
    """
    img_url = img.get('data-original')
    # 用get方法获取data-original属性 的值 ,也就是下载链接

    alt = img.get('alt')
    alt = re.sub(fr'[{PAT}]',  '', alt)  # 把特殊字符给替换掉
    suffix = os.path.splitext(img_url)[1]  # 提取后缀
    filename = alt + suffix  # 组合图片名字
    return img_url, filename


def handle_html(html):
    # 获取 下面class 不等于 gif 的图片
    if html is not None:
        imgs = html.xpath(
            "//div[@class='page-content text-center']//img[@class!='gif']")
        img_urls=[]
        filenames=[]
        for img in imgs:  # data-original
            img_url, filename = handle_img_node(img)
            # 如果想要让save_ 函数下载某张图片 也 并发运行那么 也是 和外层一样的操作。
            # 开启更多的线程即可
            # save_(img_url, filename)  # 调用 save_ 发送请求 并保存图片
            img_urls.append(img_url)
            filenames.append(filename)
        
        with futures.ThreadPoolExecutor(20) as executor:
        # 使用工作的线程数实例化ThreadPoolExecutor类;executor.__exit__方法会调用executor.shutdown(wait=True)方法,它会在所有线程都执行完毕前阻塞线程
            res=executor.map(save_, img_urls,filenames)
            list(res)


def download_one(x):
    url = page_url_template.format(x)
    html = gen_html_obj(url)
    handle_html(html)


def download_many(x):
    MAX_WORKERS = 20
    workers = min(MAX_WORKERS, x)
    # 设定工作的线程数量:使用允许的最大值(MAX_WORKERS)与要处理的数量之间较小的那个值,以免创建多余的线程。
    with futures.ThreadPoolExecutor(workers) as executor:
        # 使用工作的线程数实例化ThreadPoolExecutor类;executor.__exit__方法会调用executor.shutdown(wait=True)方法,它会在所有线程都执行完毕前阻塞线程
        res=executor.map(download_one, range(1, x + 1))
        # map方法的作用与内置的map函数类似,不过download_one函数会在多个线程中并发调用;map方法返回一个生成器,因此可以迭代,获取各个函数返回的值。
        # 和前面讲的类似的是
        # 这个代码也是 多页同时下载
        # 而不是多个图片一起下载
        # 再加多页同时下载。
        # 但是不同的是这个程序会阻塞到完全下载完毕。
        # 所以显示的耗时是正确的。
        list(res) # 因为是一个生成器 所以 要 next 或者 list 取出来 会执行内部的代码 哦 


def main(func, num):
    start = time.time()
    func(num)
    print("耗时 -> ", time.time()-start, "秒")


if __name__ == "__main__":
    main(download_many, 2)  # 下载二页的表情包


    

说来可能大家不信 我 用这个代码 测试 2 页 。 下载了 138 张图片。

耗时3秒多一点

因为我图片下载 开了 20 线程 2 页页面请求 开了 两个线程

改写为 submitas_completed 下载表情包


import os
import re
import string
import requests
from lxml import etree
import time
from concurrent import futures
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64)',
    'Referer': 'http://www.doutula.com/'
}
PAT = string.punctuation
SAVEPATH = "".join([".", os.sep, "img"])
page_url_template = 'http://www.doutula.com/photo/list/?page={}'
get = requests.get


def gen_html_obj(url):
    try:
        r = get(url, headers=HEADERS, timeout=30)
        r.raise_for_status()  # 如果状态不是200 引发 异常
        r.encoding = "utf-8"  # 设置编码
        html = etree.HTML(r.text)
        return html
    except BaseException as exc:
        print(exc)


def save_(img_url, filename):
    res = get(img_url, headers=HEADERS).content
    save_name = "".join([SAVEPATH, os.sep, filename])
    try:
        with open(save_name, "wb") as f:
            f.write(res)
    except FileNotFoundError:
        os.mkdir(SAVEPATH)
        with open(save_name, "wb") as f:
            f.write(res)
    # print(img_url)


def handle_img_node(img):
    """
    从图片节点提取属性
    构造文件名字,以及获取图片url
    """
    img_url = img.get('data-original')
    # 用get方法获取data-original属性 的值 ,也就是下载链接

    alt = img.get('alt')
    alt = re.sub(fr'[{PAT}]',  '', alt)  # 把特殊字符给替换掉
    suffix = os.path.splitext(img_url)[1]  # 提取后缀
    filename = alt + suffix  # 组合图片名字
    return img_url, filename


def handle_html(html):
    # 获取 下面class 不等于 gif 的图片
    if html is not None:
        imgs = html.xpath(
            "//div[@class='page-content text-center']//img[@class!='gif']")
        img_urls=[]
        filenames=[]
        for img in imgs:  # data-original
            img_url, filename = handle_img_node(img)
            # 如果想要让save_ 函数下载某张图片 也 并发运行那么 也是 和外层一样的操作。
            # 开启更多的线程即可
            # save_(img_url, filename)  # 调用 save_ 发送请求 并保存图片
            img_urls.append(img_url)
            filenames.append(filename)
        
        with futures.ThreadPoolExecutor(20) as executor:
            to_do=[]
            for args in zip(img_urls,filenames):
                future = executor.submit(save_,*args)
                to_do.append(future)

            results = []
            for future in futures.as_completed(to_do):
                res = future.result()  # 获取该future的结果。
                results.append(res)



def download_one(x):
    url = page_url_template.format(x)
    html = gen_html_obj(url)
    handle_html(html)


def download_many(x):
    MAX_WORKERS = 20
    workers = min(MAX_WORKERS, x)
    # 设定工作的线程数量:使用允许的最大值(MAX_WORKERS)与要处理的数量之间较小的那个值,以免创建多余的线程。
    with futures.ThreadPoolExecutor(workers) as executor:
        # 使用工作的线程数实例化ThreadPoolExecutor类;executor.__exit__方法会调用executor.shutdown(wait=True)方法,它会在所有线程都执行完毕前阻塞线程abs
        to_do=[]
        for page in range(1, x + 1):
            future = executor.submit(download_one, page)
            to_do.append(future)
        
        results = []
        for future in futures.as_completed(to_do):
            res = future.result()  # 获取该future的结果。
            results.append(res)

def main(func, num):
    start = time.time()
    func(num)
    print("耗时 -> ", time.time()-start, "秒")


if __name__ == "__main__":
    main(download_many, 2)  # 下载两页的表情包

我们也来给表情包下载的这个例子加进度条和 异常处理。

运行截图
在这里插入图片描述

import os
import re
import string
import requests
from lxml import etree
import time
from concurrent import futures
import tqdm  # 导入显示进度条的库
HEADERS = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64)',
    'Referer': 'http://www.doutula.com/'
}
PAT = string.punctuation
SAVEPATH = "".join([".", os.sep, "img"])
page_url_template = 'http://www.doutula.com/photo/list/?page={}'
get = requests.get


def gen_html_obj(url):
    try:
        r = get(url, headers=HEADERS, timeout=30)
        r.raise_for_status()  # 如果状态不是200 引发 异常
        r.encoding = "utf-8"  # 设置编码
        html = etree.HTML(r.text)
        return html
    except requests.exceptions.HTTPError as exc:
        print(exc)
    except requests.exceptions.ConnectionError as exc:
        print(exc)


def save_(img_url, filename):
    res = get(img_url, headers=HEADERS).content
    save_name = "".join([SAVEPATH, os.sep, filename])
    try:
        with open(save_name, "wb") as f:
            f.write(res)
    except FileNotFoundError:
        print(f"创建 {SAVEPATH} 文件夹")
        os.mkdir(SAVEPATH)
        with open(save_name, "wb") as f:
            f.write(res)
    # print(img_url)


def handle_img_node(img):
    """
    从图片节点提取属性
    构造文件名字,以及获取图片url
    """
    img_url = img.get('data-original')
    # 用get方法获取data-original属性 的值 ,也就是下载链接

    alt = img.get('alt')
    alt = re.sub(fr'[{PAT}]',  '', alt)  # 把特殊字符给替换掉
    suffix = os.path.splitext(img_url)[1]  # 提取后缀
    filename = alt + suffix  # 组合图片名字
    return img_url, filename


def handle_html(html):
    # 获取 下面class 不等于 gif 的图片
    if html is not None:
        imgs = html.xpath(
            "//div[@class='page-content text-center']//img[@class!='gif']")
        img_urls = []
        filenames = []
        for img in imgs:  # data-original
            img_url, filename = handle_img_node(img)
            # 如果想要让save_ 函数下载某张图片 也 并发运行那么 也是 和外层一样的操作。
            # 开启更多的线程即可
            # save_(img_url, filename)  # 调用 save_ 发送请求 并保存图片
            img_urls.append(img_url)
            filenames.append(filename)

        with futures.ThreadPoolExecutor(20) as executor:
            to_do = {}
            for args in zip(img_urls, filenames):
                future = executor.submit(save_, *args)
                to_do[future] = args

            try:
                results = futures.as_completed(to_do)
                results = tqdm.tqdm(results, total=len(img_urls), ascii=True, desc="download_img")
                for future in results:
                    future.result()  # 获取该future的结果。

            except requests.exceptions.HTTPError as exc:
                error_msg = 'HTTP {res.status_code} - {res.reason}'
                error_msg = error_msg.format(res=exc.response)
                print(f"Error for {to_do[future]} : {error_msg}")


def download_one(x):
    url = page_url_template.format(x)
    html = gen_html_obj(url)
    handle_html(html)


def download_many(x):
    MAX_WORKERS = 20
    workers = min(MAX_WORKERS, x)
    # 设定工作的线程数量:使用允许的最大值(MAX_WORKERS)与要处理的数量之间较小的那个值,以免创建多余的线程。
    with futures.ThreadPoolExecutor(workers) as executor:
        # 使用工作的线程数实例化ThreadPoolExecutor类;executor.__exit__方法会调用executor.shutdown(wait=True)方法,它会在所有线程都执行完毕前阻塞线程abs
        to_do = []
        for page in range(1, x + 1):
            future = executor.submit(download_one, page)
            to_do.append(future)

        results = futures.as_completed(to_do)
        results = tqdm.tqdm(results, total=len(range(1, x + 1)), ascii=True, desc="request_page")
        for future in results:
            future.result()  # 获取该future的结果。


def main(func, num):
    start = time.time()
    func(num)
    print("耗时 -> ", time.time()-start, "秒")


if __name__ == "__main__":
    main(download_many, 2)  # 下载两页的表情包

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值