今天群里看到有人问关于python多线程写文件的问题,联想到这是reboot的架构师班的入学题,我想了一下,感觉坑和考察的点还挺多,可以当成一个面试题来问,简单说一下我的想法和思路吧,涉及的代码和注释在github 跪求star

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

python文件处理,open write
简单了解http协议头信息
os,sys模块
threading模块多进程
requests模块发请求

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

下载

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

# 简单粗暴的下载
import requests res=requests.get('http://51reboot.com/src/blogimg/pc.jpg')
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('http://51reboot.com/src/blogimg/pc.jpg',headers=headers) 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)
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:
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)
#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,文章代表我个人看法,有错误不可避免,欢迎大家指正,共同学习,本文完整代码在github,跪求大家star

python10min系列之多线程下载器的更多相关文章

  1. <基于Qt与POSIX线程>多线程下载器的简易搭建

    原创博客,转载请联系博主! 本项目已托管到本人Git远程库:https://github.com/yue9944882/Snow 项目目标  Major Functionality 开发环境:  Ce ...

  2. 06-python进阶-多线程下载器练手

    我们需要用python 写一个多线程的下载器 我们要先获取这个文件的大小 然后将其分片 然后启动多线程 分别去下载 然后将其拼接起来 #!/usr/bin/env python#coding:utf- ...

  3. Java多线程下载器FileDownloader(支持断点续传、代理等功能)

    前言 在我的任务清单中,很早就有了一个文件下载器,但一直忙着没空去写.最近刚好放假,便抽了些时间完成了下文中的这个下载器. 介绍 同样的,还是先上效果图吧. Jar包地址位于 FileDownload ...

  4. Android版多线程下载器核心代码分享

    首先给大家分享多线程下载核心类: package com.example.urltest; import java.io.IOException; import java.io.InputStream ...

  5. java编写的Http协议的多线程下载器

    断点下载器还在实现中...... //////////////////////////////////界面/////////////////////////////////////////// pac ...

  6. Ubuntu下的图形化多线程下载器XDM

    目录 1.下载 2.安装 3.浏览器支持 使用Ubuntu下载东西经常过于缓慢,因此需要多进程下载器. 1.下载 下载链接:http://xdman.sourceforge.net/#download ...

  7. Linux下的多线程下载工具mwget

    之前在做项目的时候,遇到一个难题,需要一个多线程下载器,于是阴差阳错的看到了这款工具--mwget,之所以是阴差阳错,是因为mwget的多线程下载功能,并不是我们想要的多线程. wget大家都知道吧, ...

  8. Chrome开启多线程下载

    Chrome多线程下载也和标签页预览一样属于Google测试中的功能,可通过在地址栏输入chrome://flags/,然后在搜索框中输入Parallel downloading,选择enabled, ...

  9. Http系列:断点续传与多线程下载

    前言 当下载电影时,我常常会想中断下载后,为什么点击开始时会在中断的地方继续下载呢?又或者在看在线电影时,为什么可以按着播放条拖动就能看到想看的片段呢? http的range请求将解决以上困惑. 多线 ...

随机推荐

  1. Unity 3d 刚体

    1.起始的设置如下图: 这是我们运行游戏,方块并不会往下掉. 2.选中CUBE,然后添加刚体                 此时再运行,会发现场景的方块会自动掉在地上. 3.我们来看一下刚体的属性 ...

  2. 如何在其他类中实现继承自CFormView类的对象

    今天项目开发中,我们创建了一个对话框资源,并创建了一个派生自CFormView的类(假设为CMyClassDlg)来管理它. CMyClassDlg.h #pragma once // CMyClas ...

  3. 读写分离提高 SQL Server 并发性

    转自:http://www.canway.net/Lists/CanwayOriginalArticels/DispForm.aspx?ID=476 在一些大型的网站或者应用中,单台的SQL Serv ...

  4. 编写可维护的JS 06

    7.事件处理 //典型用法 function handlerClick(event){ var popup = document.getElementById('popup'); popup.styl ...

  5. CSS3六边形

    <!DOCTYPE html> <!-- saved from url=(0043)http://dbox.whosemind.net/demo/liufang.html --> ...

  6. 解决Metadata file does not match checksum错误

    1.清空缓存执行: # yum clean all 先把就的缓存数据都去掉. 2.下载metadata和校验数据先进入yum对应的目录,再下载: # cd /var/cache/yum/rpmforg ...

  7. 实现单例模式C++版本

    还是先看最简单的C++单例模式 class CSingleton { private: CSingleton(){} static CSingleton *pInstance; public: sta ...

  8. Spring学习之常用注解(转)

    使用注解来构造IoC容器 用注解来向Spring容器注册Bean.需要在applicationContext.xml中注册<context:component-scan base-package ...

  9. 兼容现有jQuery API的轻量级JavaScript库:Zepo

    Zepo是一个JavaScript框架,其特点是兼容现有jQuery API的同时,自身体积十分小:它与jQuery有着类似的API.如果你会jQuery,那么也就会使用Zepto了. $('div' ...

  10. Servlet基础知识(四)——Servlet过滤器Filter

    一.什么是过滤器: 政府大楼的安检保安,它既能对进入政府大楼的人员进行检查,只允许检查符合要求的进入:同时他也负责对出大楼的人进行检查,看他带出的东西是否符合要求. 同样的,Servlet中的过滤器既 ...