Last Updated on

原本,我觉得readreadlinereadlines比较简单,没什么好说的,本没打算要单独说一说的,但是在一次面试的时候,面试官问到了这个问题,但我并没有回答的很好,在面对大文件时的处理,没有给出很好的回答,所以这里单独来研究研究,并好好说一下这三个的方法。

首先,这三个方法都是Python中对文件的操作。可以通过with open(...) as f: 打开文件并操作文件。

正文

首先,先说一下这几个函数的作用的用法:

  • read(size=-1),返回文件的全部内容,返回的数据类型根据打开的方法来定,size默认为-1,表示返回全部内容,指定size,则返回size个字符长度的内容。
  • readline(size=-1),size指定返回当前行size长度的字符,当指定size时,功能跟read()相同,从当前位置返回指定长度的字符。默认为-1,表示返回当前行全部内容,也就是读取字符直到换行符,返回的内容包括换行符。
  • readlines(size=-1),同上,size为字符长度,当指定size时,会返回指定长度的字符所在的所有行的全部内容,并放到列表中返回。当默认为-1时,返回此文件所有行的全部数据,并放到列表中返回。返回的数据包括换行符,就是readline的多次结果的列表。

如上,三个函数都是常见的返回文件内容的函数,这只是常见的我们对于这几个函数的认识,下面我们就来更深入的研究一下这几个方法。

1. size参数

以上三个函数都有一个共同点,都可以传入一个size的参数,用于指定返回字符的长度。这个在不同的函数中作用不同。

read()中,不分行,全部内容可看着一个整体,指定size则返回这个整体的指定长度的内容。

readline()中,是返回当前行的,指定size长度的内容,如果指定的长度超过当前行的总长度,则最多只会返回当前行的全部内容,并不会返回下一行的内容,可以理解为此方法在遇到换行符后,就会停止,不会再返回换行符后面的内容。

readlines()中,当size的长度没有超过当前行时,列表中都只返回当前行的全部内容,如果size超过当前行小于第二行结尾,则会返回当前行和下一行两行的全部数据,以此类推返回更多行数据。

以上时size参数在这几个函数中的使用注意事项,单说可能不好理解,举个例子,假设我们有一个文件filename,文件内容如下:

123
456
789

下面我们举例看看这几个的参数size的使用:

with open('filename', 'rb') as f:
    print(f.read(5))

# 输出: b'123\n45'

with open('filename', 'rb') as f:
    print(f.readline(5))

# 输出: b'123\n'

with open('filename', 'rb') as f:
    print(f.readlines(5))

# 输出: [b'123\n', b'456\n']

如上,大致就能明白一些size参数的用法。

2. 指针的概念

在操作文件时,内部其实有一个指针,当开开文件时,默认指针在文件最开头,当用readreadlinereadlines等函数读取文件内容后,指针会进行相应的移动,且这写函数在读取文件内容时,则是从该指针的位置进行读取,只能返回指针之后的内容,而不是每次都从头读取。

要想搞明白文件操作,就必须理解这个指针的概念,在Python中可以通过tell()方法返回指针当前的位置。

看下面例子:

with open('filename', 'rb') as f:
    print(f.tell())
    print(f.read(2))
    print(f.tell())
    print(f.read(5))
    print(f.tell())

# 输出:
0
b'12'
2
b'3\n456'
7

可以看到,read函数在读取了内容后,指针会有一个相应的移动,指针位置为2。

然后我们再看一下readline()

with open('filename', 'rb') as f:
    print(f.tell())
    print(f.readline(2))
    print(f.tell())
    print(f.readline(5))
    print(f.tell())

# 输出:
0
b'12'
2
b'3\n'
4

如上可以看出,readline()最多只会输出当前行的内容,指针同样只移动到当前行的末尾就结束了。

在看一下readlines():

with open('filename', 'rb') as f:
    print(f.tell())
    print(f.readlines(2))
    print(f.tell())
    print(f.readlines(5))
    print(f.tell())

# 输出:
0
[b'123\n']
4
[b'456\n', b'789']
11

可以看到,readlines()比较不一样,会返回当前指针所处行的所有内容,并将指针移动到行末尾,再此调用时就从指针的下一个字符开始,并且由于第二次调用时长度已经到了第三行,所以会返回两行的内容,指针相应的也就移动到了第三行的末尾。

指针在文件操作时,非常重要,还可以通过seek()方法手动移动指针。

3. 大文件的读取

在用Python操作大文件时,这些方法默认都会将数据读取出来存放到内存中,所以若文件非常大,就会占用过大的内存,会导致资源不足,打开的速度慢等等问题。

所以我们来研究分析一下这几个方法在操作大文件时的情况。

read()方法必须指定size大小,否则不可取,默认会读取所有文件放到内存中。

import sys

with open('filename', 'rb') as f:
    s = f.read()
    print(sys.getsizeof(s))

# 输出:
31779429

如上,如果filename是个31.8M的文件,在read后,则会占用相同大小的内存空间内。


readline() 只读取一行数据,所以是可用的。这样不会导致内存占用过大。

readlines() 会读取所有数据行并放入列表中,同read() 一样,会占用大量内存。

import sys

with open('filename', 'rb') as f:
    s = f.readlines()
    qq = 0
    for i in s:
        qq += sys.getsizeof(i)
    print(qq)

# 输出
31799196

可以看到readlinesread一样,占用大量内存。

那么在面对大文件,或不知道大小的文件时,该如何进行操作呢?下面列举几种方法来实现:

1. 文件操作句柄本身可迭代

当我们用with open开发一个文件句柄时,此文件句柄本身就是可迭代的,可以使用for循环逐行输出,如下示例:

with open('filename', 'rb') as f:
    for i in f:
        print(i)

# 输出:
b'123\n'
b'456\n'
b'789'

优点:简单,逐行输出,不会占用过多内存。

缺点:对于一些单行过大的文件,或本身就只有一行的大文件,这种方法就不可用了。

2. 分块输出

将大文件,自定义一个块的大小,然后一次只输出这么大的数据量,然后不断循环输出,我们可以通过yield自定义一个生成器函数,然后迭代生成器,进行文件读取,如下所示:

def load_big_file(fileobj, block=1024):
    while True:
        s = fileobj.read(block)
        if not s:
            break
        yield s


with open('filename', 'rb') as f:
    for i in load_big_file(f):
        print(i)

# 输出:
b'123\n'
b'456\n'
b'789'        

如上,我们定义了一个生成器函数load_big_file,此文件返回一个生成器,用for迭代此生成器,即可得到值。这样也可以解决读取大文件的问题。

看到上面,感觉好像很厉害,对不对,那么我们思考一下,我们一般会对大文件做什么操作?直接读取,有用处吗?没用处!所以继续

在运维工作中,最常见的大文件需要操作的就是日志文件,那么对于日志文件,我们一般可能会进行匹配,筛选,返回日志最后最新的n行内容等。

那么如何实现这些常见操作呢?

在日志文件中,每行的数据量不会过大,在进行匹配或者过滤时,则可以通过第一种方法,直接迭代,每行使用re进行正则匹配,实现匹配,筛选。

那么返回最后几行的话,怎么实现呢?则可以通过文件指针的前向移动,来实现。看下面示例:

def tail(filepath, n, block=-1024):
    with open(filepath, 'rb') as f:
        f.seek(0,2)
        filesize = f.tell()
        while True:
            if filesize >= abs(block):
                f.seek(block, 2)
                s = f.readlines()
                if len(s) > n:
                    return s[-n:]
                    break
                else:
                    block *= 2
            else:
                block = -filesize

此方法就实现了返回文件最后几行的功能,但是有一点需要特别注意:

只有用b二进制的方式打开文件,才可以从后往前读!!!seek()方法才能接受负数参数。

OK,Python的readreadlinereadlines三种方法大致就说到这里,只要理解了指针和方法功能,就能在工作中灵活使用。

有任何问题,欢迎留言。