更加符合Python风格的跳过标题行的方法

9

有没有更简短(也许更符合Python风格)的方法打开文本文件并读取不是以注释字符开头的行?

换句话说,有没有更简洁的方法实现这个功能:

fin = open("data.txt")
line = fin.readline()
while line.startswith("#"):
    line = fin.readline()

https://dev59.com/l3I-5IYBdhLWcg3wu7BU#1706204 - ghostdog74
11
“Shorter”并不一定“Pythonic”。你所拥有的非常整洁、清晰和自明。将其压缩为晦涩难懂的一行代码并不总是有助于促进Pythonicity。虽然我很喜欢itertools,但有时它的函数式方法会让我停下来挠头。Pythonic的代码应该需要很少或者不需要思考就能理解。如果我必须投票选择另一种形式,并称其更加Pythonic,那么我会选择Jim Dennis的列表推导式解决方案。 - PaulMcG
9个回答

16

在我学习Python的过程中,我认为这是最Pythonic的方式:

def iscomment(s):
   return s.startswith('#')

from itertools import dropwhile
with open(filename, 'r') as f:
    for line in dropwhile(iscomment, f):
       # do something with line

要跳过文件顶部所有以#开头的行。为了跳过所有以#开头的行:

from itertools import ifilterfalse
with open(filename, 'r') as f:
    for line in ifilterfalse(iscomment, f):
       # do something with line

对我来说,这几乎就是关于可读性的全部内容了;从功能上讲,以下两种方式几乎没有区别:

for line in ifilterfalse(iscomment, f))

并且

for line in (x for x in f if not x.startswith('#'))

将测试分解成自己的函数可以使代码的意图更清晰;这也意味着,如果您对评论的定义发生变化,您只需更改一个位置。


那些 while 应该改成 with,对吗? - Autoplectic
这适用于Python 2,对于Python 3,您应该使用filterfalse而不是ifilterfalse - Jadim

14
for line in open('data.txt'):
    if line.startswith('#'):
        continue
    # work with line

当然,如果你的注释行只在文件开头,那么你可以使用一些优化。


+1 清晰明了。如果有更多的条件来过滤行,您只需像这样添加下一个检查即可保持清晰。不像堆叠过滤器。 - Tomek Szpakowicz

10
from itertools import dropwhile
for line in dropwhile(lambda line: line.startswith('#'), file('data.txt')):
    pass

6
如果你想过滤掉 所有 的注释行(不仅仅是文件开头的注释):
for line in file("data.txt"):
  if not line.startswith("#"):
    # process line

如果你只想跳过开头的那些元素,请参考ephemient使用itertools.dropwhile的答案。


5

您可以使用生成器函数

def readlines(filename):
    fin = open(filename)
    for line in fin:
        if not line.startswith("#"):
            yield line

并像这样使用它

for line in readlines("data.txt"):
    # do things
    pass

根据文件来源的不同,您可能还需要在使用startswith()检查之前对行进行strip()操作。我曾经不得不在几个月后调试这样的脚本,因为有人在'#'前放了几个空格字符。


1
这会过滤掉所有以#开头的行,而不仅仅是文件开头(“head”)的那些——OP对所需行为并不完全清楚。 - ephemient
此外,您可以使用生成器表达式:for line in (line for line in open('data.txt') if not line.startswith('#')): - ephemient
请查看我的答案,该答案只会删除文件开头的#行,而不是整个文件。 - steveha

5
作为实际问题,如果我知道我正在处理大小合理的文本文件(任何可以轻松放入内存的文件),那么我可能会选择以下方法:
f = open("data.txt")
lines = [ x for x in f.readlines() if x[0] != "#" ]

...读取整个文件并过滤掉以井号开头的所有行。

正如其他人指出的那样,我们可能希望忽略在井号之前出现的前导空格,就像这样:

lines = [ x for x in f.readlines() if not x.lstrip().startswith("#") ]

我喜欢这个的简洁性。

这假设我们想要剥离所有注释行。

我们也可以使用以下方法“截断”每个字符串结尾的最后几个字符(几乎总是换行符):

lines = [ x[:-1] for x in ... ]

假设我们不担心文件的最后一行缺少换行符这个臭名昭著的模糊问题。(只有在 EOF 时,从 .readlines() 或相关文件对象方法读取的行才可能没有以换行符结尾。)
在相当新的 Python 版本中,可以使用条件表达式来去掉行末的换行符(仅限换行符),如下所示:
lines = [ x[:-1] if x[-1]=='\n' else x for x in ... ]

...这就是我为了易读性而能想到的最复杂的列表推导式。

如果我们担心文件过大(或内存受限)会影响性能或稳定性,而且我们使用的Python版本足够新以支持生成器表达式(这比我在此处使用的列表推导式更近期添加到语言中),那么我们可以使用:

for line in (x[:-1] if x[-1]=='\n' else x for x in
  f.readlines() if x.lstrip().startswith('#')):

    # do stuff with each line

...在代码提交一年后,我不认为其他人能够在一行中解析出这个限制。

如果意图仅是跳过“头”行,则我认为最好的方法是:

f = open('data.txt')
for line in f:
    if line.lstrip().startswith('#'):
        continue

... 干完就完了。


4
你可以制作一个循环遍历文件的生成器,跳过那些行:
fin = open("data.txt")
fileiter = (l for l in fin if not l.startswith('#'))

for line in fileiter:
   ...

2
你可以这样做:
def drop(n, seq):
    for i, x in enumerate(seq):
        if i >= n:
            yield x

然后说

for line in drop(1, file(filename)):
    # whatever

2

我喜欢@iWerner提出的生成器函数的想法。对他的代码进行小改动,就可以实现问题所要求的功能。

def readlines(filename):
    f = open(filename)
    # discard first lines that start with '#'
    for line in f:
        if not line.lstrip().startswith("#"):
            break
    yield line

    for line in f:
        yield line

并像这样使用它

for line in readlines("data.txt"):
    # do things
    pass

但这里有一种不同的方法。这几乎非常简单。思路是我们打开文件并获取一个文件对象,可以将其用作迭代器。然后我们从迭代器中取出不需要的行,只返回迭代器。如果我们总是知道要跳过多少行,那么这将是理想的。问题在于我们不知道需要跳过多少行;我们只需要拉出行并查看它们。一旦我们拉出了一行,就无法将其放回到迭代器中。
所以:打开迭代器,拉出行并计算前导 '#' 字符的数量;然后使用 .seek() 方法倒回文件,再次拉出正确的行数,并返回迭代器。
我喜欢这个方法的一件事:您会得到实际的文件对象,带有所有的方法;您可以直接使用它而不是 open() ,它将在所有情况下都起作用。我将函数重命名为 open_my_text() 以反映这一点。
def open_my_text(filename):
    f = open(filename, "rt")
    # count number of lines that start with '#'
    count = 0
    for line in f:
        if not line.lstrip().startswith("#"):
            break
        count += 1

    # rewind file, and discard lines counted above
    f.seek(0)
    for _ in range(count):
        f.readline()

    # return file object with comment lines pre-skipped
    return f

我可以使用f.next()(对于Python 2.x)或next(f)(对于Python 3.x),而不是f.readline(),但我想编写一个可移植到任何Python的代码。

编辑:好吧,我知道没人关心我,也不会因此获得任何点赞,但我已经重写了我的答案,使其更加优雅。

你不能将一行放回迭代器中。但是,你可以两次打开文件,并获得两个迭代器;由于文件缓存的工作方式,第二个迭代器几乎是免费的。如果我们想象一个在顶部有一兆字节的“#”行的文件,则此版本将大大优于调用f.seek(0)的先前版本。

def open_my_text(filename):
    # open the same file twice to get two file objects
    # (We are opening the file read-only so this is safe.)
    ftemp = open(filename, "rt")
    f = open(filename, "rt")

    # use ftemp to look at lines, then discard from f
    for line in ftemp:
        if not line.lstrip().startswith("#"):
            break
        f.readline()

    # return file object with comment lines pre-skipped
    return f

这个版本比之前的版本好得多,它仍然返回一个完整的文件对象,包括所有的方法。


1
在循环中,为什么不使用 f.tell() 来保存文件中实际的位置,而不是使用计数器?将 count = 0 替换为 loc = 0,将 count += 1 替换为 loc = f.tell(),将 f.seek(0) 替换为 f.seek(loc),并完全删除 for _ in range(count) 循环。 - PaulMcG
我喜欢这个建议,但我刚试了一下,它不起作用。.tell()方法与迭代器不匹配;我的短测试文件完全被吞掉了,每次调用.tell()都返回文件末尾。如果.tell()能够跟踪迭代器,我绝对会按照你的方式去做;这样更加简洁。我的代码比较混乱,但它的优点是实际上可以工作... :-) - steveha

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接