python传输大文本实战分享
最近开发网站有一个需求是将大量数据库的内容导出下载,大小少则几MB,多则上百MB,由于以前从没做过类似的需求,刚开始我直接将导出的字符串通过ajax传到页面上,然后进行一些处理,然后导出。
但我很快就发现,小一点的文本还好,文本较大时要花费大量时间传输,比如大小100MB的文本,下载速度2MB/s的话,要下载50s,而且因为是在ajax时传输,用户不知道传输的进度,只能干等着,体验会非常差。
这时我想到了gzip,以前在看一些网站优化的文章时看到过它,但从来没有深入了解与实际应用,看了一下它的介绍,发现它压缩大量文本的能力很强,于是决定试一试。
这里我用的是python2.7的tornado框架,和后台的API交互获取数据,这样实际上是前后端分离的模式, 你可能会问为什么不用node.js,其实我是非常想用node.js的,一来可以增强自己的js水平,二来熟悉node.js的话对以后的前端学习与开发也是很有用的,毕竟许多前端工具都需要用到node.js。
不过团队的人用的都是python,带我们的学长也明确要求用tornado,更重要的是自己目前水平还不够。在我看来,要在团队中推行新的技术栈,首先自己得弄明白,而且得能做出来才行,要不然就是坑队友。当然,多学几门语言也没什么坏处,吊死在一门语言上也不是太好的做法。
回到刚才的问题,python2.7下有gizp模块,可以直接导入使用
import gzip with gzip.open('filename', 'wb', 5) as f: f.write('content')
gzip.open()类似python原生的open(),第一个参数是文件名,第二个参数是读写模式,这里用的是'wb',表示以二进制格式写入文件,第三个参数是压缩等级,从0到9数字越大,压缩率越大,但是CPU占用会越高,消耗时间也越长,设置为0的话就是不压缩。
刚开始,我想将内容压缩后直接通过ajax发送到页面上,那么这里我们发送的应该是字符串,但在这里用gzip压缩得到的是文件,怎么办呢?
通过对gzip的文档的查阅,我发现gzip可以直接对文件对象进行操作,而不必非得操作真实存在的文件,在python中StringIO模块提供了直接在内存中读写字符串的方法,这样可以把字符串当作文件操作,用在这里正合适,于是便有以下代码:
import gzip import cStringIO output = cStringIO.StringIO() f = gzip.GzipFile('a.txt.gz','wb', 9, output) f.write('Hello World!\n') f.close() print output.getvalue()
这里使用的cStringIO模块,其与StringIO模块相同,而且效率更好,除了以下这两点与StringIO不同:
cStringIO.StringIO不能作为基类被继承;
创建cStringIO.StringIO对象时,如果初始化函数提供了初始化数据,新生成的对象是只读的。
以上代码直接构造gzip.GzipFile类,其中参数相比gzip.open(),多了几个可选的参数,其中一个参数类型为fileobj,关于这个参数的介绍是这样的
The new class instance is based on fileobj, which can be a regular file, a StringIO object, or any other object which simulates a file. It defaults to None, in which case filename is opened to provide a file object.
所以可以直接对一个StringIO实例操作,与直接操作文件方式类似,将内容写入StringIO实例。这里StringIO提供了getvalue()方法,可以直接获取其中的数据,也可以通过read(),但要注意需要使用seek(0)来将文件指针移动到文件的起始位置,不然将获得空的字符串。
获取压缩后的二进制数据后便可以发送到页面上,现代浏览器都支持gzip类型数据的解压缩,只要在响应的http报文头部中添加Content-Encoding:gzip字段,就可以在浏览器端进行解压缩。
但在这里我又遇到了新的问题,大量文本的解压缩操作会占用cpu比较高,我用的笔记本电脑风扇狂转,操作也变得卡顿起来,而且解压缩后还要读取这段数据,也要消耗不断的时间。我尝试降低压缩等级,从9降到了6,后来又降到了1,发现压缩的文件只是变大了少许,但cpu占用情况有所改良,不过效率还是非常低下。
想了很久之后,我感觉自己走了歪路,试想,把一段几十MB甚至超过100MB的文本直接传到页面上进行解析,20M的网络单传输就要花上几十秒,就算是经过压缩也要花不少时间,再加上对如此大量的文本的解析,客户端将会出现高内存、CPU占用导致的卡顿,如果是经过压缩的文本卡顿会更加严重。总之,我感觉我的出发点就是错误的。
于是我决定换一种方式,将文本在服务器端直接处理完成,通过文件的形式让用户直接开始下载。但我之前从来没有做过文件下载这样的功能,所以遇到了两个问题:
生成的文件名需要固定,如何防止与其他用户生成的文件冲突。
生成文件后需要保存一段时间再删除。
原来通过ajax直接将数据传到页面上,哪个用户发请求就把响应的文本传给谁,逻辑比较简单,这也是我一开始的想法,不过事实证明这种方法相当低效。
现在用文件下载的方式,就需要考虑以上两个问题了,对第一个问题,我的解决方案是每次生成一个名称唯一的文件夹,然后将这个文件的路径发到页面上进行进一步的下载操作,具体如何操作呢?
这里我使用的的是uuid,uuid是由一组32位数的16进制数字所构成,其数目非常庞大,两个uuid发生碰撞的几率很小,但不为0。
python中的uuid模块对此进行了实现,其有uuid1、uuid3、uuid4、uuid5这4个生成uuid的方法,在这里我使用的是uuid4,其使用伪随机数生成uuid,将uuid作为文件夹名,然后文件存入文件夹中,并将路径发送给到页面上,这样第一个问题就解决了;
那么接下来就是第二个问题,用户发送请求生成文本文件后,会有确认下载的页面,这样需要文件保存一段时间,但不及时删除的话又会造成硬盘空间的浪费。我的思路是设定一个过期时间,每次对该页面有新的请求时,系统都会检查留存的文件夹的创建时间,若当前时间与创建时间的差值大于过期时间,则删掉该文件夹,这里用到了glob模块与shutil模块,代码如下:
temp_folder = './static/temp' # 删除temp中过期的文件夹 folders = glob.glob('{0}/*'.format(temp_folder)) time_now = time.time() for folder in folders: if time_now - os.path.getctime(folder) > 3 * 60: # 过期时间3分钟 shutil.rmtree(folder)
这样两个问题就解决了,然后文本文件再采用gzip压缩的方式,直接将压缩包发给用户,最后效率提升了5倍以上!
通过这次的经历,我学到了很多:gzip处理文本、uuid的使用、实现临时文件的下载等。因为以前没有接触过类似场景,自己主要是习惯性的把数据直接传到页面上,直接放到内存里处理,现在认识到了这种方法的不足,学到了更好的方法,希望自己以后考虑问题时更加细心,积极寻找最优解,水平能更上一层楼。
上一篇: FileChannel 文件通道