Python读取大文件的"坑“与内存占用检测

1.read()与readlines()

with open(file_path, 'rb') as f:
    sha1Obj.update(f.read())
or
with open(file_path, 'rb') as f:
    for line in f.readlines():
        print(line)

这对方法在读取小文件时确实不会产生什么异常,但是一旦读取大文件,很容易会产生MemoryError,也就是内存溢出的问题。

Why Memory Error?

我们首先来看看这两个方法:

当默认参数size=-1时,read方法会读取直到EOF,当文件大小大于可用内存时,自然会发生内存溢出的错误。

read方法

read([size])方法从文件当前位置起读取size个字节,若无参数size,则表示读取至文件结束为止,它范围为字符串对象;同样的,readlines会构造一个list。list而不是iter,所以所有的内容都会保存在内存之上,同样也会发生内存溢出的错误。

readlines方法

该方法每次读出一行内容,所以,读取时占用内存小,比较适合大文件,该方法返回一个字符串对象。

2.正确的用法

在实际运行的系统之中如果写出上述代码是十分危险的,这种”坑“十分隐蔽。所以接下来我们来了解一下正确用,正确的用法也很简单,依照API之中对函数的描述来进行对应的编码就OK了;如果是二进制文件推荐用如下这种写法,可以自己指定缓冲区有多少byte。显然缓冲区越大,读取速度越快。

with open(file_path, 'rb') as f:
    while True:
        buf = f.read(1024)
        if buf:    
            sha1Obj.update(buf)
        else:
            break

而如果是文本文件,则可以用readline方法或直接迭代文件(python这里封装了一个语法糖,二者的内生逻辑一致,不过显然迭代文件的写法更pythonic )每次读取一行,效率是比较低的。笔者简单测试了一下,在3G文件之下,大概性能和前者差了20%。

with open(file_path, 'rb') as f:
    while True:
        line = f.readline()
        if buf:    
            print(line)
        else:
            break
with open(file_path, 'rb') as f:
    for line in f:
        print(line)

3.内存检测工具的介绍

对于python代码的内存占用问题,对于代码进行内存监控十分必要。这里笔者这里推荐两个小工具来检测python代码的内存占用。

memory_profiler

首先先用pip安装memory_profiler

`pip ``install` `memory_profile`

memory_profiler是利用python的装饰器工作的,所以我们需要在进行测试的函数上添加装饰器。

from hashlib import sha1
import sys
@profile
def my_func():
    sha1Obj = sha1()
    with open(sys.argv[1], 'rb') as f:
        while True:
            buf = f.read(10 * 1024 * 1024)
            if buf:
                sha1Obj.update(buf)
            else:
                break
    print(sha1Obj.hexdigest())
if __name__ == '__main__':
    my_func()

之后在运行代码时加上 -m memory_profiler,就可以了解函数每一步代码的内存占用了

(django_test) E:\hd\filetransfer\apps\dicom>python -m memory_profiler tests.py
100000
Filename: tests.py

Line #    Mem usage    Increment   Line Contents
================================================
    17     16.9 MiB     16.9 MiB   @profile
    18                             def test1():
    19     16.9 MiB      0.0 MiB       c = 0
    20     16.9 MiB      0.0 MiB       for itme in range(100000):
    21     16.9 MiB      0.0 MiB           c+=1
    22     16.9 MiB      0.0 MiB       print(c)
Mem usage:内存占用情况
Increment:执行该行代码后新增的内存

guppy

依样画葫芦,仍然是通过pip先安装guppy

pip install guppy3

之后可以在代码之中利用guppy直接打印出对应各种python类型(list、tuple、dict等)分别创建了多少对象,占用了多少内存

from guppy import hpy
import random
def my_func():
    result = []
    def _get():
        return chr(random.randint(0,100))
    for i in range(10):
        mk = [_get() for i in range(10)]
        result.extend(mk)
    return "".join(result)
if __name__ == '__main__':
    profile = hpy()
    print(profile.iso(my_func()))
Partition of a set of 1 object. Total size = 149 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0      1 100      149 100       149 100 str
from guppy import hpy
def my_func1():
    hxx = hpy()
    heap = hxx.heap()
    byrcs = hxx.heap().byrcs
    print(heap)
if __name__ == '__main__':
    my_func1()
Partition of a set of 56630 objects. Total size = 6560442 bytes.
 Index  Count   %     Size   % Cumulative  % Kind (class / dict of class)
     0  17121  30  1599115  24   1599115  24 str
     1  12111  21   850344  13   2449459  37 tuple
     2   3986   7   704022  11   3153481  48 types.CodeType
     3   7887  14   571777   9   3725258  57 bytes
     4   3904   7   530944   8   4256202  65 function
     5    633   1   523624   8   4779826  73 type
     6    633   1   348536   5   5128362  78 dict of type
     7    156   0   269840   4   5398202  82 dict of module
     8    514   1   194280   3   5592482  85 dict (no owner)
     9     67   0    91272   1   5683754  87 set
<186 more rows. Type e.g. '_.more' to view.>

Process finished with exit code 0

可以看到打印出对应的内存占用数据:

通过上述两种工具guppy与memory_profiler可以很好地来监控python代码运行时的内存占用问题。