Python:如何在读取文件时忽略 #注释行?

49

在Python中,我刚刚从文本文件中读取了一行,我想知道如何编写代码来忽略以井号#开头的注释。

我认为应该像这样:

for 
   if line !contain #
      then ...process line
   else end for loop 

但我是Python的新手,不熟悉语法。

10个回答

68
你可以使用 startswith()。 例如:
for line in open("file"):
    li=line.strip()
    if not li.startswith("#"):
        print line.rstrip()

4
忽略前导空格,判断一行文本是否以 "#" 开头: if not line.strip().startswith("#") - exhuma
12
你的代码中有for line in open("file"):这行,它会保留一个打开的文件句柄。当你完成后,你应该保持 open("file") 的返回值并显式地调用 close() 方法或者使用 with 语句(参见http://docs.python.org/library/stdtypes.html#file.close)。 - Mike Mazur
5
不应该这样。当遇到文件结束符时,for循环会隐式地调用StopIteration。 - ghostdog74
6
至少对于CPython来说,这并不会留下一个打开的文件句柄。当对文件对象的最后一个引用消失时,文件对象将被垃圾回收,在那时文件将关闭。Jython(在Java虚拟机上运行)可能会有所不同。如果你使用的是具有“with”语句的现代Python版本,那么使用“with open(“filename”)as f:”被认为是非常好的形式,然后通过“f”(或任何其他你选择的变量名)引用文件对象。“with”确保文件被关闭,无论出现什么情况,即使面临异常的情况也是如此。 - steveha
它不应该是“print li.rstrip()”而不是“print li”吗?目前它正在打印已剥离的行,这将删除前导和尾随空格。 - R Hyde
显示剩余2条评论

48

建议你看到 # 字符时不要忽略整行内容,只需要忽略该字符后面的内容即可。你可以使用字符串方法函数partition轻松实现:

with open("filename") as f:
    for line in f:
        line = line.partition('#')[0]
        line = line.rstrip()
        # ... do something with line ...

partition 返回一个元组:分隔符之前的所有内容、分隔符本身和分隔符之后的所有内容。因此,通过使用索引 [0],我们仅获取分隔符之前的部分。

编辑: 如果您正在使用不带有partition()函数的Python版本,则可以使用以下代码:

with open("filename") as f:
    for line in f:
        line = line.split('#', 1)[0]
        line = line.rstrip()
        # ... do something with line ...
这将根据 '#' 字符将字符串拆分,然后保留拆分之前的所有内容。参数1使得.split()方法在一次拆分后停止;由于我们仅获取0号子字符串(通过使用[0]索引),因此没有1参数也会得到相同的答案,但是这样可能会更快一些。(由于@gnr的评论,从我的原始代码简化。我的原始代码没有好的理由变得混乱;感谢@gnr。)
您还可以编写自己的版本的partition()。下面是一个名为part()的示例:
def part(s, s_part):
    i0 = s.find(s_part)
    i1 = i0 + len(s_part)
    return (s[:i0], s[i0:i1], s[i1:])
@dalle指出'#'可以出现在字符串中。正确处理这种情况并不容易,因此我忽略了它,但我应该说一些东西。
如果您的输入文件对引号包含的规则足够简单,那么这并不难。如果您接受任何合法的Python引号字符串,那么就很难了,因为有单引号、双引号、带反斜杠转义行末的多行引用、使用单引号或双引号的三重引用字符串,甚至是原始字符串!正确处理所有这些的唯一可能方法是使用复杂的状态机。
但是,如果我们只限制自己使用简单的引号包含的字符串,我们可以使用简单的状态机来处理它。我们甚至可以允许在字符串中使用反斜杠引用的双引号。
c_backslash = '\\'
c_dquote = '"'
c_comment = '#'


def chop_comment(line):
    # a little state machine with two state varaibles:
    in_quote = False  # whether we are in a quoted string right now
    backslash_escape = False  # true if we just saw a backslash

    for i, ch in enumerate(line):
        if not in_quote and ch == c_comment:
            # not in a quote, saw a '#', it's a comment.  Chop it and return!
            return line[:i]
        elif backslash_escape:
            # we must have just seen a backslash; reset that flag and continue
            backslash_escape = False
        elif in_quote and ch == c_backslash:
            # we are in a quote and we see a backslash; escape next char
            backslash_escape = True
        elif ch == c_dquote:
            in_quote = not in_quote

    return line

虽然我不想在一个标签为“初学者”的问题中变得复杂,但这个状态机相当简单,并且我希望它会很有趣。


2
真的,但如果你想要正确性,那么你可能也需要关注引用的 #。 - dalle
1
需要提醒OP的是,在旧版本中可能没有分区功能。 - ghostdog74
哦,糟糕,没错:partition() 只存在于 Python 2.5 及以上版本。我会编辑我的回答并添加另一种解决方案。 - steveha
有什么理由你不能在这里使用split而不是partition吗? - gnr
@gnr,这里没有什么特别的原因,使用str.split()可能是更好的选择。我会使用可选的maxsplit参数,将其设置为1,这样它就会在找到一个#字符后停止,像这样:line.split('#', 1)[0]。我真的很喜欢str.partition(),并且我倾向于使用它,但是str.split()在旧版本的Python中也可以使用。str.partition()还会返回中间字符串,但我们在这里不需要它,所以没有理由使用它。如果str.partition()不可用,我会编辑答案建议使用str.split() - steveha

9
我有些晚了才来,但处理Shell风格(或Python风格)的 # 注释问题是非常普遍的。每次读取文本文件时我都会使用一些代码。
问题是它不能正确处理带引号或已转义注释。但对于简单情况很实用且易于操作。
for line in whatever:
    line = line.split('#',1)[0].strip()
    if not line:
        continue
    # process line

更为健壮的解决方案是使用shlex

import shlex
for line in instream:
    lex = shlex.shlex(line)
    lex.whitespace = '' # if you want to strip newlines, use '\n'
    line = ''.join(list(lex))
    if not line:
        continue
    # process decommented line

这个shlex方法不仅正确处理引号和转义,还添加了很多很酷的功能(比如如果需要,可以让文件引用其他文件)。我还没有在大文件上测试过它的速度,但对于小文件来说非常快。

当您还将每个输入行拆分为字段(按空格)时,通常情况更加简单:

import shlex
for line in instream:
    fields = shlex.split(line, comments=True)
    if not fields:
        continue
    # process list of fields 

这篇文章值得更多的点赞!即使没有使用shlex的解决方案也比被接受的答案更全面(对于简单的用例来说,被接受的答案还是可以的,但问题在于如果你告诉用户“你可以在里面放注释,但只有在行首”,你会第一个忘记这个限制)。 - dlukes

8
这是最简短的形式:
for line in open(filename):
  if line.startswith('#'):
    continue
  # PROCESS LINE HERE

对于字符串,startswith() 方法如果以传入的字符串开头则返回 True。

这在一些场景下还可以使用,比如在 shell 脚本中。但其有两个问题。首先,它没有指定如何打开文件。默认的文件打开模式是 'r',这意味着“以二进制模式读取文件”。由于你期望的是一个文本文件,最好使用 'rt' 打开它。虽然在类 UNIX 系统上这种区别无关紧要,但在 Windows (和早期的 Mac OS X) 上很重要。

第二个问题是打开的文件句柄。open() 函数返回一个文件对象,通常情况下,在使用完文件后应当关闭文件。为此,请在该对象上调用 close() 方法。现在,Python 可能会在某个时间点自动完成此操作,因为 Python 对象是引用计数的,当一个对象的引用计数降至 0 时,它将被释放,并且在 Python 调用其析构函数(一个名为 __del__ 的特殊方法)后的某个时间点。需要注意的是,我说过“可能”:Python 经常不会真正地调用那些在线程结束前引用计数降至 0 的对象的析构函数。我想这是因为它着急完成。

对于短暂的 shell 脚本,特别是文件对象,这没有问题。当程序结束时,操作系统会自动清理掉任何打开的文件句柄。但是如果你打开了文件,读取了内容,然后开始长时间计算而没有显式关闭文件句柄,Python 在计算期间可能会保持文件句柄处于打开状态。这是不好的做法。

此版本适用于 Python 的任何 2.x 版本,并修复了我上述的两个问题:

f = open(file, 'rt')
for line in f:
  if line.startswith('#'):
    continue
  # PROCESS LINE HERE
f.close()

这是适用于旧版本Python的最佳通用形式。

正如steveha所建议的,使用“with”语句现在被认为是最佳实践。如果您正在使用2.6或更高版本,则应以以下方式编写:

with open(filename, 'rt') as f:
  for line in f:
    if line.startswith('#'):
      continue
    # PROCESS LINE HERE

"with"语句会为您清理文件句柄。
在您的问题中,您说“以#开头的行”,所以这就是我在这里向您展示的。如果您想要过滤掉以可选空格开头,然后是'#'的行,您应该在查找'#'之前去除空格。在这种情况下,您应该更改以下内容:
    if line.startswith('#'):

转换为:

    if line.lstrip().startswith('#'):

在Python中,字符串是不可变的,因此这不会改变line的值。 lstrip()方法返回一个将其所有前导空格都删除的字符串副本。

Python有一个坏习惯,即在程序快要结束时引用计数降为零的对象实际上不会调用析构函数。您有这个说法的证据吗? - gotgenes
不能保证解释器退出时仍存在的对象会调用__del__()方法。 - Larry Hastings

5

我知道这是一个旧的线程,但这是一个生成器函数,我为自己的目的使用它。它会删除注释,无论注释出现在哪一行,还会删除前导/尾随空格和空白行。以下是源代码:

# Comment line 1
# Comment line 2

# host01  # This host commented out.
host02  # This host not commented out.
host03
  host04  # Oops! Included leading whitespace in error!
  

将产生:

host02
host03
host04

这里有一份记录代码,其中包含演示:

def strip_comments(item, *, token='#'):
    """Generator. Strips comments and whitespace from input lines.
    
    This generator strips comments, leading/trailing whitespace, and
    blank lines from its input.
    
    Arguments:
        item (obj):  Object to strip comments from.
        token (str, optional):  Comment delimiter.  Defaults to ``#``.
    
    Yields:
        str:  Next uncommented non-blank line from ``item`` with
            comments and leading/trailing whitespace stripped.
    
    """
    
    for line in item:
        s = line.split(token, 1)[0].strip()
        if s:
            yield s
    
    
if __name__ == '__main__':
    HOSTS = """# Comment line 1
    # Comment line 2

    # host01  # This host commented out.
    host02  # This host not commented out.
    host03
      host04  # Oops! Included leading whitespace in error!""".split('\n')

    
    hosts = strip_comments(HOSTS)
    print('\n'.join(h for h in hosts))

常规用法是从文件中删除注释(例如,像上面我的示例中的hosts文件)。如果是这种情况,则上述代码的尾部将被修改为:

if __name__ == '__main__':
    with open('aa.txt', 'r') as f:
        hosts = strip_comments(f)

        for host in hosts:
            print('\'%s\'' % host)

1
感谢您的发现,@MilovanTomašević!我简直不敢相信六年前我没有发现这个问题,更不敢相信自那以后竟然没有人发现它! - Deacon
1
啊 @Deacon,我们在这里互相帮助。 :) - Milovan Tomašević

5

最近我发现生成器函数非常适合这个任务。我曾经使用类似的函数来跳过注释行、空行等。

我的函数定义如下:

def skip_comments(file):
    for line in file:
        if not line.strip().startswith('#'):
            yield line

那样的话,我就可以直接执行。
f = open('testfile')
for line in skip_comments(f):
    print line

这段代码可以在我所有的程序中重复使用,我还可以添加任何需要的处理、日志记录等。


3
更紧凑的过滤表达式也可以像这样:

for line in (l for l in open(filename) if not l.startswith('#')):
    # do something with line

(l for ... ) 被称为 "生成器表达式",它在迭代文件时充当包装迭代器,可以过滤掉所有不需要的行。不要将其与方括号中的相同内容 [l for ... ] 混淆,这是一个 "列表推导式",它会首先将文件中的所有行读入内存,然后再开始迭代。

有时您可能希望让代码更加易读:

lines = open(filename)
lines = (l for l in lines if ... )
# more filters and mappings you might want
for line in lines:
    # do something with line

所有过滤器都将在一次迭代中即时执行。


2
使用正则表达式re.compile("^(?:\s+)*#|(?:\s+)")跳过换行和注释。

1

我倾向于使用

for line  in lines:
    if '#' not in line:
        #do something

这将忽略整行,尽管包括rpartition的答案得到了我的赞同,因为它可以包含#之前的任何信息


1
消除注释的好方法,既适用于行内注释,也适用于单独一行的注释。
def clear_coments(f):
    new_text = ''
    for line in f.readlines():
        if "#" in line: line = line.split("#")[0]

        new_text += line

    return new_text

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