获取文本文件第一行和最后一行的最有效方法是什么?


74

我有一个文本文件,每行包含一个时间戳。我的目标是找到时间范围。所有时间都井井有条,因此第一行将是最早的时间,最后一行将是最新的时间。我只需要第一行和最后一行。在python中获取这些行的最有效方法是什么?

注意:这些文件的长度相对较大,每个文件大约1-2百万行,我必须对几百个文件执行此操作。

Answers:


62

io模块的文档

with open(fname, 'rb') as fh:
    first = next(fh).decode()

    fh.seek(-1024, 2)
    last = fh.readlines()[-1].decode()

此处的变量值为1024:它表示平均字符串长度。例如,我仅选择1024。如果您估算出平均线长,则可以将该值乘以2。

由于您不知道行长的可能上限,因此显而易见的解决方案是遍历文件:

for line in fh:
    pass
last = line

您无需费心可以使用的二进制标志open(fname)

预计到达时间(ETA):由于您要处理的文件很多,因此可以使用创建数十个文件的示例,random.sample并在其上运行此代码以确定最后一行的长度。先验地获得较大的位置偏移值(假设为1 MB)。这将帮助您估算整个运行的价值。


只要这些行的长度不超过1024个字符。
FogleBird 2010年

无法保证这些行的长度不能超过1024个字符,除了行上的时间戳之外,还可能存在其他一些垃圾。
pasbino 2010年

@pasbino:你有一些上限?
SilentGhost

18
使用fh.seek(-1024, os.SEEK_END)而不是fh.seek(-1024, 2)增加可读性。
2014年

2
以下是不正确的:您不必费心只可以使用的二进制标志open(fname)b旗帜开头至关重要。如果你使用open(fname)的不是open(fname, 'rb')会得到 io.UnsupportedOperation:不能做非零最终相对寻求
patryk.beza

87

要读取文件的第一行和最后一行,您可以...

  • 打开文件,...
  • ...使用内置阅读第一行 readline(),...
  • ...查找(移动光标)到文件末尾,...
  • ...向后退一步,直到遇到停产(换行符)并...
  • ...从那里读最后一行。
def readlastline(f):
    f.seek(-2, 2)              # Jump to the second last byte.
    while f.read(1) != b"\n":  # Until EOL is found ...
        f.seek(-2, 1)          # ... jump back, over the read byte plus one more.
    return f.read()            # Read all data from this point on.
    
with open(file, "rb") as f:
    first = f.readline()
    last = readlastline(f)

跳到第二个直接倒数个字节,以防止尾随换行符导致返回空行*。

每次读取一个字节时,当前偏移量将向前推一,因此一次后退一步将完成两个字节,越过最近读取的字节和下一个要读取的字节。

whence传递给的参数fseek(offset, whence=0)表示fseek应该寻找offset相对于...的位置字节

*可以预期,大多数应用程序(包括print和)的默认行为echo是在写入的每一行后附加一个,并且对缺少尾随换行符的行没有影响。


效率

每个1-2百万行,我必须为数百个文件执行此操作。

我对这种方法进行了计时,并将其与最佳答案进行了比较。

10k iterations processing a file of 6k lines totalling 200kB: 1.62s vs 6.92s.
100 iterations processing a file of 6k lines totalling 1.3GB: 8.93s vs 86.95.

数以百万计的行会增加差很多了。

用于计时的Exakt代码:

with open(file, "rb") as f:
    first = f.readline()     # Read and store the first line.
    for last in f: pass      # Read all lines, keep final value.

修正案

一种更复杂,更难阅读的变体,用于解决此后提出的评论和问题。

  • 解析空文件时返回空字符串,由comment引发。
  • 未找到由comment引发的分隔符时,返回所有内容。
  • 避免使用相对偏移量来支持文本模式(由注释引起)
  • UTF16 / UTF32 hack,通过注释注明。

还增加了对多字节定界符的支持readlast(b'X<br>Y', b'<br>', fixed=False)

请注意,由于文本模式需要非相对偏移量,因此这种变化对于大型文件来说确实很慢。根据您的需要进行修改,或者根本不使用它,因为最好使用f.readlines()[-1]在文本模式下打开的文件。

#!/bin/python3

from os import SEEK_END

def readlast(f, sep, fixed=True):
    r"""Read the last segment from a file-like object.

    :param f: File to read last line from.
    :type  f: file-like object
    :param sep: Segment separator (delimiter).
    :type  sep: bytes, str
    :param fixed: Treat data in ``f`` as a chain of fixed size blocks.
    :type  fixed: bool
    :returns: Last line of file.
    :rtype: bytes, str
    """
    bs   = len(sep)
    step = bs if fixed else 1
    if not bs:
        raise ValueError("Zero-length separator.")
    try:
        o = f.seek(0, SEEK_END)
        o = f.seek(o-bs-step)    # - Ignore trailing delimiter 'sep'.
        while f.read(bs) != sep: # - Until reaching 'sep': Read sep-sized block
            o = f.seek(o-step)   #  and then seek to the block to read next.
    except (OSError,ValueError): # - Beginning of file reached.
        f.seek(0)
    return f.read()

def test_readlast():
    from io import BytesIO, StringIO
    
    # Text mode.
    f = StringIO("first\nlast\n")
    assert readlast(f, "\n") == "last\n"
    
    # Bytes.
    f = BytesIO(b'first|last')
    assert readlast(f, b'|') == b'last'
    
    # Bytes, UTF-8.
    f = BytesIO("X\nY\n".encode("utf-8"))
    assert readlast(f, b'\n').decode() == "Y\n"
    
    # Bytes, UTF-16.
    f = BytesIO("X\nY\n".encode("utf-16"))
    assert readlast(f, b'\n\x00').decode('utf-16') == "Y\n"
  
    # Bytes, UTF-32.
    f = BytesIO("X\nY\n".encode("utf-32"))
    assert readlast(f, b'\n\x00\x00\x00').decode('utf-32') == "Y\n"
    
    # Multichar delimiter.
    f = StringIO("X<br>Y")
    assert readlast(f, "<br>", fixed=False) == "Y"
    
    # Make sure you use the correct delimiters.
    seps = { 'utf8': b'\n', 'utf16': b'\n\x00', 'utf32': b'\n\x00\x00\x00' }
    assert "\n".encode('utf8' )     == seps['utf8']
    assert "\n".encode('utf16')[2:] == seps['utf16']
    assert "\n".encode('utf32')[4:] == seps['utf32']
    
    # Edge cases.
    edges = (
        # Text , Match
        (""    , ""  ), # Empty file, empty string.
        ("X"   , "X" ), # No delimiter, full content.
        ("\n"  , "\n"),
        ("\n\n", "\n"),
        # UTF16/32 encoded U+270A (b"\n\x00\n'\n\x00"/utf16)
        (b'\n\xe2\x9c\x8a\n'.decode(), b'\xe2\x9c\x8a\n'.decode()),
    )
    for txt, match in edges:
        for enc,sep in seps.items():
            assert readlast(BytesIO(txt.encode(enc)), sep).decode(enc) == match

if __name__ == "__main__":
    import sys
    for path in sys.argv[1:]:
        with open(path) as f:
            print(f.readline()    , end="")
            print(readlast(f,"\n"), end="")

4
这是最简洁的解决方案,我喜欢它。关于不猜测块大小的好处是,它可以与较小的测试文件一起很好地工作。我添加了几行,并将其包装在我喜欢的函数中tail_n
MarkHu 2014年

1
我喜欢它在纸上,但无法使用。File "mapper1.2.2.py", line 17, in get_last_line f.seek(-2, 2) IOError: [Errno 22] Invalid argument
卢瓦克

2
没关系,文件为空,derp。无论如何,最好的答案。+1
卢瓦克

2
根据此评论作为答案,while f.read(1) != "\n":应为while f.read(1) != b"\n":
Artjom B.

4
还要记录一下:如果获取到异常io.UnsupportedOperation: can't do nonzero end-relative seeks,则必须分两个步骤进行操作:首先找到文件的长度,然后添加偏移量,然后将其传递给f.seek(size+offset,os.SEEK_SET)
AnotherParker

25

这是SilentGhost答案的修改版本,可以满足您的需求。

with open(fname, 'rb') as fh:
    first = next(fh)
    offs = -100
    while True:
        fh.seek(offs, 2)
        lines = fh.readlines()
        if len(lines)>1:
            last = lines[-1]
            break
        offs *= 2
    print first
    print last

此处不需要线长的上限。


10

可以使用Unix命令吗?我认为使用head -1tail -n 1可能是最有效的方法。另外,您也可以使用简单的fid.readline()方法获取第一行和fid.readlines()[-1],但这可能会占用太多内存。


嗯,创建一个子进程来执行这些命令是最有效的方法吗?
pasbino 2010年

10
如果您确实有unix,则os.popen("tail -n 1 %s" % filename).read()可以很好地获得最后一行。
Michael Dunn 2010年

1
头-1和尾-1的+1。fid.readlines()[-1]对于大型文件不是一个好的解决方案。
Joao Figueiredo

os.popen("tail -n 1 %s" % filename).read()->自2.6版起弃用
LarsVegas

6

这是我的解决方案,也与Python3兼容。它还可以处理边境案件,但缺少utf-16支持:

def tail(filepath):
    """
    @author Marco Sulla (marcosullaroma@gmail.com)
    @date May 31, 2016
    """

    try:
        filepath.is_file
        fp = str(filepath)
    except AttributeError:
        fp = filepath

    with open(fp, "rb") as f:
        size = os.stat(fp).st_size
        start_pos = 0 if size - 1 < 0 else size - 1

        if start_pos != 0:
            f.seek(start_pos)
            char = f.read(1)

            if char == b"\n":
                start_pos -= 1
                f.seek(start_pos)

            if start_pos == 0:
                f.seek(start_pos)
            else:
                char = ""

                for pos in range(start_pos, -1, -1):
                    f.seek(pos)

                    char = f.read(1)

                    if char == b"\n":
                        break

        return f.readline()

它是由ispired Trasp的回答AnotherParker的评论


4

首先以读取模式打开文件,然后使用readlines()方法逐行读取所有行存储在列表中,现在您可以使用列表切片来获取文件的第一行和最后一行。

    a=open('file.txt','rb')
    lines = a.readlines()
    if lines:
        first_line = lines[:1]
        last_line = lines[-1]

1
我正是在搜索这个,我不需要第一行和最后一行,因此lines [1,-2]给出了标题和页脚之间的文本。
guneysus

4
此选项无法处理空文件。
Un33k 2014年

8
并崩溃导致非常大的文件
akarapatis 2014年

4
w=open(file.txt, 'r')
print ('first line is : ',w.readline())
for line in w:  
    x= line
print ('last line is : ',x)
w.close()

for环通过线运行,并x获得在最后一次迭代的最后一行。


这应该是公认的答案。我不知道为什么其他答案中所有这些都与低级io混在一起了?
GreenAsJade

3
@GreenAsJade我的理解是,“四处走动”是为了避免从头到尾读取整个文件。在大文件上这可能效率不高。
bli

3
with open("myfile.txt") as f:
    lines = f.readlines()
    first_row = lines[0]
    print first_row
    last_row = lines[-1]
    print last_row

您能解释一下为什么您的解决方案会更好吗?
祖鲁语

嗨,我发现自己也有必要删除文本文件中最后一行的最后一个逗号,因此我设法轻松地找到了它。我当时想分享。该解决方案简单,实用且即时,但我不知道它在效率方面是否最快。您能告诉我些什么?
Riccardo Volpe 2015年

好吧,它必须读取并处理整个文件,因此这似乎是最不高效的方法。
rakslice 2015年

好的...所以,如果您不知道字符串的长度,那将是最好的一种方法?我需要尝试另一个(stackoverflow.com/a/3346492/2149425)。谢谢!
Riccardo Volpe 2015年

1
使用f.readlines()[-1]新变量的insead。0 =第一行1 =第二行-1 =最后一行-2 =最后一行之前的一行...
BladeMight '16

2

这是@Trasp答案的扩展,它具有其他逻辑来处理只有一行的文件的特殊情况。如果您反复想要读取不断更新的文件的最后一行,则处理这种情况可能很有用。没有这个,如果您尝试获取刚刚创建的文件的最后一行,并且只有一行,IOError: [Errno 22] Invalid argument则会引发该错误。

def tail(filepath):
    with open(filepath, "rb") as f:
        first = f.readline()      # Read the first line.
        f.seek(-2, 2)             # Jump to the second last byte.
        while f.read(1) != b"\n": # Until EOL is found...
            try:
                f.seek(-2, 1)     # ...jump back the read byte plus one more.
            except IOError:
                f.seek(-1, 1)
                if f.tell() == 0:
                    break
        last = f.readline()       # Read last line.
    return last

2

没有人提到使用反向:

f=open(file,"r")
r=reversed(f.readlines())
last_line_of_file = r.next()

5
.readlines()将一次性将文件中的所有行读入内存-这不是解决此问题的方法
Steve Mayne


1
with open(filename, "rb") as f:#Needs to be in binary mode for the seek from the end to work
    first = f.readline()
    if f.read(1) == '':
        return first
    f.seek(-2, 2)  # Jump to the second last byte.
    while f.read(1) != b"\n":  # Until EOL is found...
        f.seek(-2, 1)  # ...jump back the read byte plus one more.
    last = f.readline()  # Read last line.
    return last

上面的答案是上面的答案的修改版本,用于处理文件中只有一行的情况

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.