码迷,mamicode.com
首页 > 编程语言 > 详细

Python 10min 面试题解析丨Python实现多连接下载器

时间:2016-05-05 22:49:39      阅读:393      评论:0      收藏:0      [点我收藏+]

标签:python|多连接

技术分享

作者:蜗牛 shengxinjing (woniuppp) · GitHub


今天群里看到有人问关于 Python 多线程写文件的问题,联想到这是 Reboot 的架构师班的入学题.我想了一下,感觉坑和考察的点还挺多的,可以当成一个面试题来问,简单说一下我的想法和思路吧,涉及的代码和注释在 GitHub 上 (https://github.com/shengxinjing/my_blog/blob/master/downloader/downloader.py)


当年的网络蚂蚁“多点同时下载、并支持断点续传”的奥秘就在这道面试题里了。


1998年,中国互联网的主旋律是下载。国内互联网超过90%的流量,来自于一群早期的电脑发烧友,从有限的中国出口带宽里面,不顾一切地下载来自国外的各种软件。33.6的Modem最可能碰到的问题是掉线,辛苦下载了2-3个小时的20M文件,又要重新下载。


所以使用人数最高的软件,不是ICQ(聊天软件),也不是Foxmail(邮件客户端),甚至不是Netscape(网景浏览器),而是下载软件,但是国外老牌的下载软件放到国内一旦断线,网速慢和重新下载便让人极为烦躁。

很快,一个符合中国国情的下载软件“网络蚂蚁”崭露头角。网络蚂蚁是世界第一款使用了多点同时下载、并支持断点续传的软件。


From: 创业故事:还记得“网络蚂蚁”吗?


本文需要一定的 Python 基础,希望大家对下面几个知识点有所了解:

  1. Python文件处理,open write

  2. 简单了解http协议头信息

  3. os,sys模块

  4. threading模块多线程

  5. requests模块发请求


题目既然是多线程下载,首先要解决的就是下载问题,为了方便测试,我们先不用QQ安装包这么大的,直接用 PC大大英明神武又很内涵的头像举例,大概是这个样子:

技术分享

下载


Python 的 requests 模块很好的封装了 http 请求,我们选择用它来发送 http 的get 请求,然后写入本地文件即可(关于 requests 和 http,以及 Python 处理文件的具体介绍,可以百度或者持续关注,后面我会写),思路既然清楚了,代码就呼之欲出了:


# 简单粗暴的下载
import requests

res=requests.get(‘
with open(‘pc.jpg‘,‘w‘) as f:
    f.write(res.content)

运行完上面的代码,文件夹下面多了个pc.jpg 就是你想要的图片了。


上面代码功能太少了,注意 ,我们的要求是多线程下载,这种简单粗暴的下载完全不符合要求,所谓多线程,你可以理解为仓库里有很多很多袋奥利奥饼干,老板让我去都搬到公司来放好,而且要按照原顺序放好。


上面的代码,大概就是我一个人去仓库,把所有奥利奥一次性拿回来,大概流程如下(图不清请戳大)。


技术分享


我们如果要完成题目多线程的要求,首先就要把任务拆解,拆成几个子任务,子任务之间可以并行执行,并且执行结果可以汇总成最终结果。


拆解任务


为了完成这个任务,我们首先要知道数据到底有多大,然后把数据分块去取就OK啦,我们要对http协议有一个很好的了解


  • 用head方法请求数据,返回只有http头信息,没有主题部分

    • 我们从头信息Content-length的值,知道资源的大小,比如有50字节

  • 比如我们要分四个线程,每个线程去取大概1/4即可

    • 50/4=12,所以前几个线程每人取12个字节,最后一个现成取剩下的即可

  • 每个线程取到相应的内容,文件中seek到相应的位置再写入即可

    • file.seek

  • 为了方便理解,一开始我们先用单线程的跑通 流程图大概如下(图不清戳大)

技术分享


思路清晰了,代码也就呼之欲出了,我们先测试一下range头信息:


http头信息中的Range信息,用于请求头中,指定第一个字节的位置和最后一个字节的位置,如1-12,如果省略第二个数,就认为取到最后,比如36-


# range测试代码
import requests
# http头信息,指定获取前15000个字节
headers={‘Range‘:‘Bytes=0-15000‘,‘Accept-Encoding‘:‘*‘}
res=requests.get(‘

with open(‘pc.jpg‘,‘w‘) as f:
    f.write(res.content)


我们得到了头像的前15000个字节,如下图,目测range是对的:

技术分享


继续丰富我们的代码:

  • 要先用requests.head方法去获取数据的长度

  • 确认开几个线程后,给每个线程确认要获取的数据区间,即Range字段的值

  • seek写文件

  • 功能比较复杂了,我们需要用面向对象来组织一下代码

  • 先写单线程,逐步优化

  • 代码呼之欲出了:

import requests
# 下载器的类
class downloader:
    # 构造函数
    def __init__(self):
        # 要下载的数据连接
        self.url=‘http://51reboot.com/src/blogimg/pc.jpg‘
        # 要开的线程数
        self.num=8
        # 存储文件的名字,从url最后面取
        self.name=self.url.split(‘/‘)[-1]
        # head方法去请求url
        r = requests.head(self.url)
        # headers中取出数据的长度
        self.total = int(r.headers[‘Content-Length‘])
        print type(‘total is %s‘ % (self.total))
    def get_range(self):
        ranges=[]
        # 比如total是50,线程数是4个。offset就是12
        offset = int(self.total/self.num)
        for i in  range(self.num):
            if i==self.num-1:
                # 最后一个线程,不指定结束位置,取到最后
                ranges.append((i*offset,‘‘))
            else:
                # 没个线程取得区间
                ranges.append((i*offset,(i+1)*offset))
        # range大概是[(0,12),(12,24),(25,36),(36,‘‘)]
        return ranges
    def run(self):

        f = open(self.name,‘w‘)
        for ran in self.get_range():
            # 拼出Range参数 获取分片数据
            r = requests.get(self.url,headers={‘Range‘:‘Bytes=%s-%s‘ % ran,‘Accept-Encoding‘:‘*‘})
            # seek到相应位置
            f.seek(ran[0])
            # 写数据
            f.write(r.content)
        f.close()

if __name__==‘__main__‘:
    down = downloader()
    down.run()


多线程


多线程和多进程是啥在这就不多说了,要说明白还得专门写个文章,大家知道


  • threading 模块是专门解决多线程的问题就OK了,大概的使用方法如下,更详细的请百度或者关注后续文章:

  • threading.Thread创建线程,设置处理函数

  • start启动

  • setDaemon 设置守护进程

  • join设置线程等待

  • 代码如下:

import requests
import threading

class downloader:
    def __init__(self):
        self.url=‘http://51reboot.com/src/blogimg/pc.jpg‘
        self.num=8
        self.name=self.url.split(‘/‘)[-1]
        r = requests.head(self.url)
        self.total = int(r.headers[‘Content-Length‘])
        print ‘total is %s‘ % (self.total)
    def get_range(self):
        ranges=[]
        offset = int(self.total/self.num)
        for i in  range(self.num):
            if i==self.num-1:
                ranges.append((i*offset,‘‘))
            else:
                ranges.append((i*offset,(i+1)*offset))
        return ranges
    def download(self,start,end):
        headers={‘Range‘:‘Bytes=%s-%s‘ % (start,end),‘Accept-Encoding‘:‘*‘}
        res = requests.get(self.url,headers=headers)
        print ‘%s:%s download success‘%(start,end)
        self.fd.seek(start)
        self.fd.write(res.content)
    def run(self):
        self.fd =  open(self.name,‘w‘)
        thread_list = []
        n = 0
        for ran in self.get_range():
            start,end = ran
            print ‘thread %d start:%s,end:%s‘%(n,start,end)
            n+=1
            thread = threading.Thread(target=self.download,args=(start,end))
            thread.start()
            thread_list.append(thread)
        for i in thread_list:
            i.join()
        print ‘download %s load success‘%(self.name)
        self.fd.close()
if __name__==‘__main__‘:
    down = downloader()
    down.run()


执行 Python downloader效果如下:

total is 21520
thread 0 start:0,end:2690
thread 1 start:2690,end:5380
thread 2 start:5380,end:8070
thread 3 start:8070,end:10760
thread 4 start:10760,end:13450
thread 5 start:13450,end:16140
thread 6 start:16140,end:18830
thread 7 start:18830,end:
0:2690 is end
2690:5380 is end
13450:16140 is end
10760:13450 is end
5380:8070 is end
8070:10760 is end
18830: is end
16140:18830 is end
download pc.jpg load success


run函数做了修改,加了多线程的东西,加了一个download函数专门用来下载数据块,这俩函数详细解释如下:

def download(self,start,end):
    #拼接Range字段,accept字段支持所有编码
    headers={‘Range‘:‘Bytes=%s-%s‘ % (start,end),‘Accept-Encoding‘:‘*‘}
    res = requests.get(self.url,headers=headers)
    print ‘%s:%s download success‘%(start,end)
    #seek到start位置
    self.fd.seek(start)
    self.fd.write(res.content)
def run(self):
    # 保存文件打开对象
    self.fd =  open(self.name,‘w‘)
    thread_list = []
    #一个数字,用来标记打印每个线程
    n = 0
    for ran in self.get_range():
        start,end = ran
        #打印信息
        print ‘thread %d start:%s,end:%s‘%(n,start,end)
        n+=1
        #创建线程 传参,处理函数为download
        thread = threading.Thread(target=self.download,args=(start,end))
        #启动
        thread.start()
        thread_list.append(thread)
    for i in thread_list:
        # 设置等待
        i.join()
    print ‘download %s load success‘%(self.name)
    #关闭文件
    self.fd.close()


持续可以优化的点:

  • 一个文件描述符多个进程用会出问题

  • 建议用os.dup复制文件描述符和os.fdopen来打开处理文件

  • 要下载的资源地址和线程数,应该做成命令行传进来的

  • 用sys.argv获取命令行参数

  • 支持python downloader.py url num这种写法

  • 参数数量不对或者格式不对时报错

  • 各种容错处理

  • 正所谓女人的迪奥,男人的奥利奥,这篇文章,你值得拥有


大概就是这样了,我也是正在学习Python,文章仅代表我个人看法,有错误不可避免。欢迎大家留言指正和交流哦。

==========================================

欢迎关注Reboot教育  运维自动化班(5月8日开班)

课程表:http://www.51reboot.com/course/devops/

上课形式:面授班 / 网络直播班

QQ:979950755

交流群:238757010

本文出自 “Reboot运维开发” 博客,请务必保留此出处http://opsdev.blog.51cto.com/2180875/1770520

Python 10min 面试题解析丨Python实现多连接下载器

标签:python|多连接

原文地址:http://opsdev.blog.51cto.com/2180875/1770520

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!