Python中如何连接文本文件?

231

我有一个包含20个文件名的列表,例如['file1.txt','file2.txt',...]。我想编写一个Python脚本将这些文件连接成一个新文件。 我可以通过f = open(...)打开每个文件,通过调用f.readline()逐行读取,并将每行写入新文件中。但对我来说,这似乎不太“优雅”,特别是我必须逐行读写的那部分。

在Python中有更加“优雅”的方式吗?


9
不是用Python,但在Shell脚本中,你可以像这样做:cat file1.txt file2.txt file3.txt ... > output.txt。在Python中,如果你不喜欢readline(),总有readlines()或者简单的read()可供选择。 - jedwards
1
@jedwards 只需使用 subprocess 模块运行 cat file1.txt file2.txt file3.txt 命令,就完成了。但我不确定 cat 在 Windows 上是否可用。 - Ashwini Chaudhary
7
作为注释,你所描述的读取文件的方式是一个糟糕的方法。请使用“with”语句确保文件正确关闭,并在迭代文件以获取行时,使用循环而非使用“f.readline()”。 - Gareth Latty
@jedwards 当文本文件是Unicode时,cat命令无法正常工作。 - Avi Cohen
实际分析 https://waymoot.org/home/python_string/ - nu everest
12个回答

320

这应该可以解决问题。

对于大文件:

filenames = ['file1.txt', 'file2.txt', ...]
with open('path/to/output/file', 'w') as outfile:
    for fname in filenames:
        with open(fname) as infile:
            for line in infile:
                outfile.write(line)

对于小文件:

filenames = ['file1.txt', 'file2.txt', ...]
with open('path/to/output/file', 'w') as outfile:
    for fname in filenames:
        with open(fname) as infile:
            outfile.write(infile.read())

...还有另一个我认为很有趣的:

filenames = ['file1.txt', 'file2.txt', ...]
with open('path/to/output/file', 'w') as outfile:
    for line in itertools.chain.from_iterable(itertools.imap(open, filnames)):
        outfile.write(line)

不幸的是,这种方法会留下一些未关闭的文件描述符,尽管这些应该由垃圾收集器自动处理。我只是觉得很有趣。


14
对于大文件来说,这将非常低效地使用内存。 - Gareth Latty
2
我们认为什么样的文件才算是“大”文件? - Dee
5
一个文件非常大,其内容无法全部存入主内存。 - inspectorG4dget
3
为什么要解码和重新编码整个内容?查找换行符和所有不必要的东西,当所需的只是将文件连接起来。下面的shutil.copyfileobj答案会更快。 - flying sheep
15
再次强调:这个答案是错误的,shutil.copyfileobj 是正确的答案。 - Paul Crowley
显示剩余15条评论

277

使用shutil.copyfileobj

它会自动为您逐块读取输入文件,这更加高效,并且即使一些输入文件太大无法放入内存,也能正常工作:

import shutil

with open('output_file.txt','wb') as wfd:
    for f in ['seg1.txt','seg2.txt','seg3.txt']:
        with open(f,'rb') as fd:
            shutil.copyfileobj(fd, wfd)

5
我将for语句替换为包含目录中所有文件的语句,但我的output_file开始在很短的时间内快速增长,像100GB左右。 - R__raki__
26
请注意,如果文件中没有行尾符,则会将每个文件的最后一行与下一个文件的第一行合并。在我的情况下,使用这段代码后得到了完全损坏的结果。我在copyfileobj之后添加了wfd.write(b"\n")来获得正常的结果。 - Thelambofgoat
8
我认为在这种情况下,这不是纯粹的串联,但无论如何,适合您的需要就好。 - HelloGoodbye
1
这绝对是最好的答案! - Kai Petzke
1
这非常快,正如我所需。是的,它不会在“两个文件结束和开始之间”添加新行,这正是我需要的。所以不要更新它:D - Adnan Ali

66

这正是fileinput的用途:

import fileinput
with open(outfilename, 'w') as fout, fileinput.input(filenames) as fin:
    for line in fin:
        fout.write(line)

对于这种用例,它实际上并不比手动迭代文件简单多少,但在其他情况下,拥有一个单个迭代器,它可以像单个文件一样迭代所有文件,非常方便。(另外,fileinput 在完成后立即关闭每个文件的事实意味着没有必要使用 withclose,但这只是节省了一行代码,不算太大的问题。)

fileinput 还有一些其他很棒的功能,比如通过过滤每一行来对文件进行原地修改的能力。


正如评论中所指出的,并在另一篇文章中讨论的那样,fileinput 对于 Python 2.7 将无法按预期工作。这里稍作修改以使代码符合 Python 2.7。

with open('outfilename', 'w') as fout:
    fin = fileinput.input(filenames)
    for line in fin:
        fout.write(line)
    fin.close()

1
@abament 我认为在这种情况下,for line in fileinput.input()并不是最佳选择:OP想要连接文件,而不是逐行读取它们,这是一个理论上更长的执行过程。 - eyquem
1
@eyquem:执行起来并不需要很长时间。正如你自己指出的那样,基于行的解决方案不是一次读取一个字符;它们会读取块并从缓冲区中提取行。I/O 时间将完全淹没行解析时间,因此只要实现者在缓冲处理上没有做什么极其愚蠢的事情,它就会像猜测一个好的缓冲区大小一样快(如果你认为 10000 是一个好的选择),甚至可能更快。 - abarnert
1
@abarnert 不,10000不是一个好的选择。它确实是一个非常糟糕的选择,因为它不是2的幂,并且它的大小非常小。更好的大小将是2097152(2 ** 21),16777216(2 ** 24)甚至134217728(2 ** 27),为什么不呢?在4 GB的RAM中,128 MB微不足道。 - eyquem
巨大的缓冲区并没有太大的帮助。事实上,如果你读取的数据超过了操作系统典型的预读缓存大小,你最终会在等待数据时浪费时间,而本可以进行写入操作。此外,运行十几个应用程序,它们都认为128MB不算什么,突然间你的系统就开始交换内存并变得非常缓慢。测试这些东西真的非常容易,所以试一试吧。 - abarnert
2
示例代码不适用于Python 2.7.10及更高版本:https://dev59.com/el0Z5IYBdhLWcg3w_0d8 - CnrL
显示剩余5条评论

8
outfile.write(infile.read()) # time: 2.1085190773010254s
shutil.copyfileobj(fd, wfd, 1024*1024*10) # time: 0.60599684715271s

一项简单的基准测试显示shutil表现更好。

7
我不知道是否优雅,但是这个方法可行:

    import glob
    import os
    for f in glob.glob("file*.txt"):
         os.system("cat "+f+" >> OutFile.txt")

9
你甚至可以避免循环:import os; os.system("cat file*.txt >> OutFile.txt") - lib
15
不跨平台且文件名中带有空格会导致错误。 - flying sheep
6
这是不安全的;另外,cat可以接受一个文件列表,所以不需要重复调用它。你可以通过调用subprocess.check_call而不是os.system来轻松使其更安全。 - Clément

6
如果你不是在Windows上工作,UNIX命令有什么问题吗?使用ls | xargs cat | tee output.txt可以完成任务(如果需要,你可以用subprocess从python中调用它)。

37
因为这是一个关于Python的问题。 - ObscureRobot
5
总的来说没有什么问题,但这个答案有误(不要将ls的输出传递给xargs,直接将文件列表传递给cat:cat * | tee output.txt)。 - Clément
如果能够同时插入文件名就更好了。 - Deqing
@Deqing 为了指定输入文件名,您可以使用 cat file1.txt file2.txt | tee output.txt - GoTrained
1
你可以通过在命令的末尾添加 1> /dev/null 来禁用发送到 stdout(在终端中打印)的功能。 - GoTrained
如果文件名中包含空格或其他模糊字符,这个解决方案将如何保持稳定? - HelloGoodbye

6
如果目录中有大量文件,使用 glob2 生成文件名列表可能比手动编写更好。
import glob2

filenames = glob2.glob('*.txt')  # list of all .txt files in the directory

with open('outfile.txt', 'w') as f:
    for file in filenames:
        with open(file) as infile:
            f.write(infile.read()+'\n')

1
这与问题有什么关系?为什么要使用glob2而不是glob模块或pathlib中的通配符功能? - AMC
非常好且完整的Python代码。运行得非常出色。谢谢。 - Just Me

3

以下是对@inspectorG4dget答案(截至2016年3月29日最佳答案)的替代方案。我测试了三个436MB的文件。

@inspectorG4dget的解决方案:162秒

以下解决方案:125秒

from subprocess import Popen
filenames = ['file1.txt', 'file2.txt', 'file3.txt']
fbatch = open('batch.bat','w')
str ="type "
for f in filenames:
    str+= f + " "
fbatch.write(str + " > file4results.txt")
fbatch.close()
p = Popen("batch.bat", cwd=r"Drive:\Path\to\folder")
stdout, stderr = p.communicate()

这个想法是创建一个批处理文件并执行它,利用“老旧的好技术”。它是半Python,但速度更快。适用于Windows。


2

12
这将产生一个很长的字符串,取决于文件的大小,可能会比可用内存还要大。由于Python提供了易于访问文件的惰性方式,因此这是一个不好的主意。 - Gareth Latty

2
如果文件不是巨大的话:
with open('newfile.txt','wb') as newf:
    for filename in list_of_files:
        with open(filename,'rb') as hf:
            newf.write(hf.read())
            # newf.write('\n\n\n')   if you want to introduce
            # some blank lines between the contents of the copied files

如果文件太大无法完全读取并保存在RAM中,则算法必须有所不同,需要使用固定长度的块通过循环读取每个要复制的文件,例如使用read(10000)

@Lattyware 因为我相当确定执行速度更快。顺便说一下,实际上,即使代码按行读取文件,文件也会被分块读取,这些块被放入缓存中,每行都会一个接一个地读取。更好的做法是将读取块的长度设置为缓存大小。但我不知道如何确定此缓存的大小。 - eyquem
那是在CPython中的实现,但这并不是保证的。这样优化是一个坏主意,因为虽然它可能在某些系统上有效,但在其他系统上可能不起作用。 - Gareth Latty
1
当然,逐行阅读是有缓存的。这也正是它不会慢太多的原因。(事实上,在某些情况下,它可能甚至略微更快,因为将Python移植到您的平台的人选择了一个比10000更好的块大小。)如果这个真的很重要,您必须对不同的实现进行分析。但99.99...%的时间,两种方式都足够快,或者实际磁盘I/O是最慢的部分,不管你的代码怎么做都没关系。 - abarnert
此外,如果您确实需要手动优化缓冲区,您将需要使用 os.openos.read,因为普通的 open 使用 Python 对 C 的 stdio 的包装器,这意味着会有1或2个额外的缓冲区妨碍您。 - abarnert
@eyquem: 读和写都是带缓冲的。所以当你调用 outf.write(line) 时,它不会重新写入磁盘块以只写入这80个字符; 这80个字符进入缓冲区,如果缓冲区现在超过8KB,则写入前8KB。 如果3MB比8KB更快,则会使用3MB缓冲区。 所以读取和写入3MB块之间唯一的区别在于您还需要进行一些RAM工作和字符串处理 - 这比磁盘快得多,因此通常并不重要。 - abarnert
显示剩余12条评论

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