Python3爬虫系列03 (实验) - 同步阻塞下载

  • 原创
  • Madman
  • /
  • 2018-10-03 11:08
  • /
  • 0
  • 321 次阅读

spider 03-min.jpg

Synopsis: 从这一篇开始,将介绍如何用Python3实现网络爬虫,多任务快速抓取你想要的数据。每一次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.3全自动安装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之间网络延时非常低,一般低于1ms,你可以在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虚拟机发现延时为100ms左右

2. 依序下载

2.1 安装依赖包

我们在Win10主机上需要先安装Python3.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 - 王颜公子 » Python3爬虫系列03 (实验) - 同步阻塞下载

分享

作者

作者头像

Madman

如果博文内容有误或其它任何问题,欢迎留言评论,我会尽快回复; 或者通过QQ、微信等联系我

0 条评论

暂时还没有评论.

发表评论前请先登录