从文件中读取,或者从标准输入中读取。

84

我写了一个使用 getopt 解析命令行参数的命令行实用程序。我还想让文件名成为可选参数,就像在其他实用程序(如 grep、cut 等)中一样。因此,我希望它具有以下用法:

tool -d character -f integer [filename]

如何实现以下功能?

  • 如果给定了文件名,则从文件中读取。
  • 如果未指定文件名,则从标准输入中读取。

2
请参考以下链接:http://unix.stackexchange.com/questions/47098/how-do-i-make-python-programs-behave-like-proper-unix-tools/47543#47543 - magnetar
9个回答

99

fileinput 模块可能可以满足您的需求 - 假设非选项参数在 args 中,则:

import fileinput
for line in fileinput.input(args):
    print line
如果args为空,则fileinput.input()将从标准输入读取;否则,它会类似于Perl的while(<>),依次从每个文件中读取。

这也是一个很好的答案,但不太具有普适性。如果合适的话,我会记得下次使用fileinput。 - Ryan R. Rosario
没错,但如果你正在使用 getargs(就像 OP 一样),那么你可能只想传递剩余的参数而不是 sys.argv[1:](这是默认值)。 - SimonJ
4
fileinput是一个奇怪且令人烦恼的API,它强制你在命令行上使用带标记的参数。 - ctpenrose
2
@ctpenrose 这不是fileinput设计的问题:区分作为输入文件名称的参数和其他参数是问题域固有的问题。Fileinput(特别是使用argparse)简化了使用一种常见模式来解决此问题,您可以选择使用或不使用,但如果有其他方法来进行区分,则可以将sys.argv的一个切片(或完全不同的名称数组)发送到fileinput.input() - 如果您明确传递数组,则无需放入虚假的sys.argv [0]。 - sdenham
1
如果 args一个空序列,那么它将从标准输入读取。如果它是 None,那么就好像它没有被提供一样;即,fileinput.input 将对命令行进行自己的解析,并将每个标记视为要打开的文件名。 - Karl Knechtel

77

简单来说:

import sys
# parse command line
if file_name_given:
    inf = open(file_name_given)
else:
    inf = sys.stdin

在这个阶段,你可以使用inf从文件中读取内容。根据是否指定了文件名,这将从指定的文件或标准输入流(stdin)中读取。

当你需要关闭文件时,你可以执行以下操作:

if inf is not sys.stdin:
    inf.close()

但在大多数情况下,如果你已经不需要使用 sys.stdin ,关闭它是无害的。


@thefourtheye:是的,这两个函数都可以从文件或sys.stdin中读取。 - Greg Hewgill
3
我找到了另一种解决这个问题的方法,并在这里写了博客 http://dfourtheye.blogspot.in/2013/05/python-equivalent-of-cs-freopen.html,同时也给这个问题添加了一个答案。 - thefourtheye
1
@thefourtheye已删除了他们的回答;你可能不需要点击进入博客来发现sys.stdin = open(file_name) - tripleee

21

我更喜欢使用“-”表示应该从标准输入读取,这更加明确:

import sys
with open(sys.argv[1], 'r') if sys.argv[1] != "-" else sys.stdin as f:
    pass # do something here

4
您的解决方案将关闭 sys.stdin,因此在 with 语句之后调用 input 函数将引发 ValueError 异常。 - Timofei Bondarev
5
可能是真的,但在脚本中输入通常只用一次。这是一个有用的结构。 - WestCoastProjects
1
小细节:应该是 sys.argv[1] != "-" 而不是 sys.argv[1] is not "-" - janto

21

我喜欢使用上下文管理器的通用语言风格,但是(过于)简单的解决方案会在离开with语句时关闭sys.stdin,而我想避免这种情况。

这个答案中借鉴了一个解决方法:

import sys
import contextlib

@contextlib.contextmanager
def _smart_open(filename, mode='Ur'):
    if filename == '-':
        if mode is None or mode == '' or 'r' in mode:
            fh = sys.stdin
        else:
            fh = sys.stdout
    else:
        fh = open(filename, mode)
    try:
        yield fh
    finally:
        if filename != '-':
            fh.close()
    
if __name__ == '__main__':
    args = sys.argv[1:]
    if args == []:
        args = ['-']
    for filearg in args:
        with _smart_open(filearg) as handle:
            do_stuff(handle)

我想你可以通过使用os.dup()来实现类似的效果,但是我编写的代码变得更加复杂和神奇,而上面的方法有点笨重但非常直接。


非常感谢!这正是我一直在寻找的。解决方案非常清晰和直接。 - edison
argparse.FileType变得太烦人(对我来说经常发生),以下代码也非常有用。 - travc

14

为了使用Python的with语句,可以使用以下代码:

import sys
with open(sys.argv[1], 'r') if len(sys.argv) > 1 else sys.stdin as f:
    # read data using f
    # ......

你的解决方案将关闭sys.stdin,因此在with语句之后调用input函数将引发ValueError - Timofei Bondarev

13

虽然不是直接的答案,但与问题相关。

通常,在编写Python脚本时,您可以使用argparse包。如果是这种情况,您可以使用:

parser = argparse.ArgumentParser()
parser.add_argument('infile', nargs='?', type=argparse.FileType('r'), default=sys.stdin)
'?'。如果可能,将从命令行消耗一个参数,并作为单个项目生成。如果没有命令行参数,则将产生默认值。
在这里,我们将默认设置为sys.stdin;因此,如果存在文件,它将读取该文件,否则它将从stdin获取输入“注意:在上面的示例中,我们使用位置参数”
更多信息请访问:https://docs.python.org/2/library/argparse.html#nargs

8

使用 argparse(它也是标准库的一部分)并且使用默认值为 stdin 的 argparse.FileType

import  argparse, sys

p = argparse.ArgumentParser()
p.add_argument('input', nargs='?',
  type=argparse.FileType(), default=sys.stdin)
args = p.parse_args()

print(args.input.readlines())

这将不允许您为stdin指定编码和其他参数; 但是,如果您想这样做,需要使参数非可选,并在将-作为参数时让FileType处理stdin:
p.add_argument('input', type=FileType(encoding='UTF-8'))

请注意,后一种情况不支持二进制模式 ('b') I/O。如果您只需要这个,可以使用上面的默认参数技术,但要提取二进制 I/O 对象,例如 default=sys.stdout.buffer 用于 stdout。然而,如果用户仍然指定了 -,这仍然会出错。(对于 -,stdin/stdout 总是被包装在一个 TextIOWrapper 中。)
如果你希望它能与 - 一起工作,或者有任何其他需要提供的打开文件时的参数,你可以修复参数,如果它被错误地包装了。
p.add_argument('output', type=argparse.FileType('wb'))
args = p.parse_args()
if hasattr(args.output, 'buffer'):
    #   If the argument was '-', FileType('wb') ignores the 'b' when
    #   wrapping stdout. Fix that by grabbing the underlying binary writer.
    args.output = args.output.buffer

(感谢medhat提到了add_argument()type参数。)

3
一个KISS解决方案是:
if file == "-":
    content = sys.stdin.read()
else:
    with open(file) as f:
        content = f.read()
print(content)   # Or whatever you want to do with the content of the file.

1
如果你想要一个文件行的数组,可以使用以下代码:sys.stdin.readlines() 或者 f.readlines() - 8c6b5df0d16ade6c

1

类似这样:

if input_from_file:
    f = open(file_name, "rt")
else:
    f = sys.stdin
inL = f.readline()
while inL:
    print inL.rstrip()
    inL = f.readline()

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