Python 3 爬虫|第3章:同步阻塞下载

  • 原创
  • Madman
  • /
  • /
  • 2
  • 2543 次阅读

Python 3 爬虫-min.png

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 安装一些需要用到的模块:

pip install requests

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 虚拟机上创建出这个文件:

# cd /usr/share/nginx/html
# ls flags > flags.txt

我们可以读取这个文件,用每一行的图片名称组合成图片的 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

未经允许不得转载: LIFE & SHARE - 王颜公子 » Python 3 爬虫|第3章:同步阻塞下载

分享

作者

作者头像

Madman

如需 Linux / Python 相关问题付费解答,请按如下方式联系我

2 条评论

Qing Yuan
Qing Yuan

费了半天劲收费。。。

Madman
Madman Qing Yuan Author

主要是云服务器需要年费,请谅解