Python 3 爬虫|第3章:同步阻塞下载
Synopsis: 从这一篇开始,将介绍如何用 Python 3 实现网络爬虫,多任务快速抓取你想要的数据。每一次 HTTP 请求/响应都要经过 TCP 连接、客户端发送请求数据、服务端分多次返回响应数据,这个过程中,客户端的 CPU 在等待网络 I/O 时会阻塞。Python 依序下载是一个主线程依次等待每个网络 I/O 完成,而多线程是多个线程并发(不是并行)等待多个网络 I/O,当一个线程因为等待网络 I/O 而阻塞时,会自动切换到另一个线程继续执行,而不是 CPU 浪费时间阻塞在唯一的线程上。所以 I/O 密集型适合用多线程,像加解密、源代码编译等 CPU 密集型适合用多进程(并行)
代码已上传到 https://github.com/wangy8961/python3-concurrency ,欢迎 star
1. 搭建测试环境
由于我们需要进行多次大并发的测试,建议先不爬取互联网上的数据,因为那样可能会造成 DDoS( Distributed Denial of Service)
。我们可以在本地局域网中,搭建一个 Web 服务器,提供静态资源
1.1 安装CentOS
参考 定制 CentOS 7 全自动安装 ISO,使用 VMware 安装一台 CentOS-7.3 虚拟机
1.2 配置Nginx
(1) 安装
1. 添加repo源 # vi /etc/yum.repos.d/nginx.repo 内容如下: [nginx] name=nginx repo baseurl=http://nginx.org/packages/centos/7/$basearch/ gpgcheck=0 enabled=1 2. 安装 # yum install -y nginx 3. 启动 # systemctl start nginx # systemctl enable nginx
(2) 提供图片
下载本文附件 flags.tar.gz
,可以通过 Xftp
上传到 CentOS 虚拟机上,解压到 /usr/share/nginx/html
目录下:
# cd /usr/share/nginx/html # ls 50x.html flags.tar.gz index.html # tar xf flags.tar.gz # ls 50x.html flags flags.tar.gz index.html
假设 CentOS 的 IP 为 192.168.40.121,此时你在 Win10 主机上,通过 Chrome 浏览器访问 http://192.168.40.121/flags/cn.gif
,应该能看到中国的国旗
1.3 模拟网络延时
由于局域网内各 PC 之间网络延时非常低,一般低于 1 ms,你可以在 Win10 主机上 ping 虚拟机试试。在互联网中,延时比这高得多,为了模拟真实的并发情况,我们需要设置 CentOS 虚拟机网卡将数据包送出的速率,假设你的 Win10 主机与 CentOS 虚拟机上的 ens160
网卡连接:
1. 查询网卡的配置 # tc qdisc ls dev ens160 2. 增加100ms延时 # tc qdisc add dev ens160 root netem delay 100ms 3. 删除规则(本文测试完成后才执行) # tc qdisc del dev ens160 root
此时,你在 Win10 主机上 ping 虚拟机发现延时为 100 ms 左右
2. 依序下载
2.1 安装依赖包
我们在 Win10 主机上需要先安装 Python 3.6
,并用 pip
安装一些需要用到的模块:
2.2 准备日志模块
创建代码目录,假设叫 python3-concurrency
,为方便观察并发效果,使用内置的 logging
模块来记录日志,创建 logger.py
文件:
import os import time import logging ### # 1. 创建logger实例,如果参数为空则返回 root logger ### logger = logging.getLogger('aiotest') # 设置总日志级别, 也可以给不同的handler设置不同的日志级别 logger.setLevel(logging.DEBUG) ### # 2. 创建Handler, 输出日志到控制台和文件 ### # 控制台日志和日志文件使用同一个Formatter formatter = logging.Formatter( '%(asctime)s - %(filename)s[line:%(lineno)d] - <%(threadName)s %(thread)d>' + '- <Process %(process)d> - %(levelname)s: %(message)s' ) # 日志文件FileHandler basedir = os.path.abspath(os.path.dirname(__file__)) log_dest = os.path.join(basedir, 'logs') # 日志文件所在目录 if not os.path.isdir(log_dest): os.mkdir(log_dest) filename = time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime(time.time())) + '.log' # 日志文件名,以当前时间命名 file_handler = logging.FileHandler(os.path.join(log_dest, filename)) # 创建日志文件handler file_handler.setFormatter(formatter) # 设置Formatter file_handler.setLevel(logging.INFO) # 单独设置日志文件的日志级别 # 控制台日志StreamHandler stream_handler = logging.StreamHandler() stream_handler.setFormatter(formatter) # stream_handler.setLevel(logging.INFO) # 单独设置控制台日志的日志级别,注释掉则使用总日志级别 ### # 3. 将handler添加到logger中 ### logger.addHandler(file_handler) logger.addHandler(stream_handler)
2.3 分析程序结构
下载本文附件 flags.txt
保存到代码目录 python3-concurrency
下,里面是 194 个国旗图片的名字,你也可以自己在 CentOS 虚拟机上创建出这个文件:
我们可以读取这个文件,用每一行的图片名称组合成图片的 URL 地址,然后用 requests.get()
请求它,下载到本地
下载一张图片的过程包括: 1. 获取图片URL 2. 请求图片URL 3. 保存到本地
我们创建一个 common.py
模块,将这三个步骤写到这里面,因为后续的几篇文章将会介绍多进程和多线程,就可以重用这些函数:
import os import time import requests from logger import logger basepath = os.path.abspath(os.path.dirname(__file__)) # 当前模块文件的根目录 def setup_down_path(): '''设置图片下载后的保存位置,所有图片放在同一个目录下''' down_path = os.path.join(basepath, 'downloads') if not os.path.isdir(down_path): os.mkdir(down_path) logger.info('Create download path {}'.format(down_path)) return down_path def get_links(): '''获取所有图片的下载链接''' with open(os.path.join(basepath, 'flags.txt')) as f: # 图片名都保存在这个文件中,每行一个图片名 return ['http://192.168.40.121/flags/' + flag.strip() for flag in f.readlines()] def download_one(image): # 为什么设计成接收一个字典参数,而不是三个位置参数? 方便后续多线程时concurrent.futures.ThreadPoolExecutor.map() '''下载一张图片 :param image: 字典,包括图片的保存目录、图片的序号、图片的URL ''' logger.info('Downloading No.{} [{}]'.format(image['linkno'], image['link'])) t0 = time.time() resp = requests.get(image['link']) filename = os.path.split(image['link'])[1] with open(os.path.join(image['path'], filename), 'wb') as f: f.write(resp.content) # resp.content是bytes类型,而resp.text是str类型 t1 = time.time() logger.info('Task No.{} [{}] runs {} seconds.'.format(image['linkno'], image['link'], t1 - t0))
然后创建依序下载 sequential.py
模块:
import time from common import setup_down_path, get_links, download_one from logger import logger def download_many(): '''依序下载所有图片,同步阻塞''' down_path = setup_down_path() links = get_links() for linkno, link in enumerate(links, 1): image = { 'path': down_path, 'linkno': linkno, # 图片序号,方便日志输出时,正在下载哪一张 'link': link } download_one(image) return len(links) if __name__ == '__main__': t0 = time.time() count = download_many() msg = '{} flags downloaded in {} seconds.' logger.info(msg.format(count, time.time() - t0))
多次运行 python sequential.py
测试,取平均值后,下载 194 张国旗图片,总用时 50 秒
左右
依序下载脚本将作为后续多任务脚本的基准,下一篇将介绍 多进程
下载
代码已上传到 https://github.com/wangy8961/python3-concurrency ,欢迎 star
2 条评论
评论者的用户名
评论时间Qing Yuan
2019-01-24T11:28:25Z费了半天劲收费。。。
Madman Qing Yuan Author
2019-01-25T06:35:38Z主要是云服务器需要年费,请谅解