如何使用Python删除文本文件中的特定行?

214
假设我有一个充满昵称的文本文件。如何使用Python从该文件中删除特定的昵称?

1
尝试使用@j-f-sebastian在此处(https://dev59.com/jHLYa4cB1Zd3GeqPTx4U#16563027)描述的`fileinput`。它似乎允许您通过临时文件逐行工作,并使用简单的`for`语法完成所有操作。 - Kevin
18个回答

277

首先,打开文件并获取文件中的所有行。然后以写模式重新打开文件,并将除了要删除的那一行之外的行写回文件:

with open("yourfile.txt", "r") as f:
    lines = f.readlines()
with open("yourfile.txt", "w") as f:
    for line in lines:
        if line.strip("\n") != "nickname_to_delete":
            f.write(line)

你需要在比较时使用strip("\n")函数来移除换行符,因为如果你的文件没有以换行符结尾,则最后一个line也不会有。


4
为什么我们必须要打开和关闭它两次? - Ooker
6
你需要打开文件两次(并在之间关闭它),因为在第一次打开时,由于你只是读取文件中当前的行,所以它是“只读”的。然后你关闭它并重新以“写入模式”打开它,在这种模式下文件是可写的,你可以替换掉文件中除去你想要删除的那一行之外的内容。 - Devin
8
为什么Python不允许我们在一行中完成这个操作? - Ooker
11
当你阅读一行时,尝试想象一个光标沿着被阅读的行移动。一旦该行已被阅读,光标现在已经超过了它。当你试图写入文件时,你会写在光标当前的位置。通过重新打开文件,你可以重置光标。 - Waddas
9
只需打开文件一次即可完成此任务...但需要以'r+'模式打开,并且您需要调用file.seek(0)(将光标移动到开头)和file.truncate()(使现有内容无效)然后才能继续重写它。 - Joshua Clayton
显示剩余2条评论

141

这个问题的解决方案只需要一个打开:

with open("target.txt", "r+") as f:
    d = f.readlines()
    f.seek(0)
    for i in d:
        if i != "line you want to remove...":
            f.write(i)
    f.truncate()

这个解决方案以读写模式("r+")打开文件,并使用seek重置f指针,然后使用truncate删除最后一次写入之后的所有内容。


2
这对我非常有效,因为我必须同时使用lockfile(fcntl)。我找不到任何方法将fileinput与fcntl一起使用。 - Easyrider
1
希望能够看到这个解决方案的一些副作用。 - user1767754
11
如果在“for”循环中出现错误,你会得到一个部分被覆盖的文件,其中包含重复的行或一半被切断的行。我不会这样做,你可能想在f.seek(0)之后立即使用f.truncate(),这样如果出现错误,你只会得到一个不完整的文件。但是,真正的解决方案(如果你有足够的硬盘空间)是输出到一个临时文件,然后在一切都成功后使用os.replace()pathlib.Path(temp_filename).replace(original_filename)将其与原始文件交换。 - user3064538
1
如果你按照被接受的答案所提到的那样添加 i.strip('\n') != "line you want to remove...",那就可以完美地解决我的问题。因为仅仅使用 i 对我来说没有任何作用。 - Mangohero1

50

在我看来,最好而且最快的选项是将内容重写到其他地方,而不是将所有内容存储到列表中并重新打开文件进行写入。

with open("yourfile.txt", "r") as file_input:
    with open("newfile.txt", "w") as output: 
        for line in file_input:
            if line.strip("\n") != "nickname_to_delete":
                output.write(line)

就是这样!只需要一个循环,您就可以完成同样的事情。速度会更快。


我们可以使用生成器表达式来代替普通的for循环。这样程序就不会将文件中的所有行加载到内存中,对于大文件来说这并不是一个好主意。它每次只会在内存中保留一行。使用生成器表达式的for循环看起来像这样:(output.write(line) for line in input if line!="nickname_to_delete"+"\n") - shrishinde
6
当您循环遍历文件对象时,您没有将文件读入内存中,因此此解决方案与您的建议完全相同。 - Steinar Lima
你可能想要删除原始文件并将第二个文件重命名为原始文件的名称,在Linux操作系统上使用Python,代码如下: subprocess.call(['mv', 'newfile.txt', 'yourfile.txt']) - Max
7
os.replace(Python 3.3中新增)比调用系统命令mv更加跨平台。 - 7yl4r
我认为这是一个更好的解决方案,因为它不会在进行更改之前将整个文件存储在内存中,这可能会成为处理非常大的文件时的一个问题。 - ecv

39
这是从@Lother的答案中“fork”出来的(应该被认为是正确的答案)。
对于像这样的文件:
$ cat file.txt 
1: october rust
2: november rain
3: december snow

这段代码:

#!/usr/bin/python3.4

with open("file.txt","r+") as f:
    new_f = f.readlines()
    f.seek(0)
    for line in new_f:
        if "snow" not in line:
            f.write(line)
    f.truncate()

改进:
  • with open,这可以省略 f.close()
  • 更清晰的 if/else 条件语句来判断当前行是否存在字符串

1
如果需要 f.seek(0) 吗? - yifan
1
@yifan 是的。否则,你会将文件追加到自身(不包括你要排除的行),而不是覆盖原文件。 - user3064538

9

在第一遍阅读并在第二遍进行更改(删除特定行)的问题在于,如果文件大小巨大,则会耗尽RAM。相反,更好的方法是逐行阅读,并将它们写入一个单独的文件中,消除您不需要的内容。我已经使用此方法运行过12-50 GB大小的文件,并且RAM使用量几乎保持不变。只有CPU周期显示正在处理。


5
一个简单的解决方案还没有被提出:
with open( file_of_nicknames, "r+" ) as f:
    lines = f.readlines()           # Get a list of all lines
    f.seek(0)                       # Reset the file to the beginning

    idx = lines.index("Nickname\n") # Don't forget the '\n'
    lines.pop( idx )                # Remove the corresponding index

    f.truncate()                    # Stop processing now
                                    # because len(file_lines) > len( lines ) 
    f.writelines( lines )           # write back

受到先前答案的启发


4
如果您使用Linux操作系统,可以尝试以下方法。
假设您有一个名为animal.txt的文本文件:
$ cat animal.txt  
dog
pig
cat 
monkey         
elephant  

删除第一行:

>>> import subprocess
>>> subprocess.call(['sed','-i','/.*dog.*/d','animal.txt']) 

那么。
$ cat animal.txt
pig
cat
monkey
elephant

11
这个解决方案不是操作系统无关的,而且由于 OP 没有指定操作系统,我认为没有理由发布一个针对 Linux 的特定答案。 - Steinar Lima
6
任何建议使用子进程来完成可以用纯Python解决的任务的人都会被点踩!我同意@SteinarLima的看法,给他点赞。 - Jamie Lindsey
“-i” 选项是非标准的,在 *BSD 平台(包括 macOS)上与在 Linux 上的工作方式不同。Python 的“fileinput”模块可以以透明、可移植和本地化的方式完成相同的操作。 - tripleee

3

我喜欢这个答案中所解释的fileinput方法:删除文本文件中的一行 (python)

例如说我有一个带有空行的文件,我想要去掉空行,这里是我的解决方案:

import fileinput
import sys
for line_number, line in enumerate(fileinput.input('file1.txt', inplace=1)):
    if len(line) > 1:
            sys.stdout.write(line)

注意:在我的情况下,空行的长度为1。

2

也许你已经得到了正确的答案,但这是我的答案。 我不使用列表来收集未经过滤的数据(readlines()方法所做的),而是使用两个文件。一个用于保存主要数据,另一个用于在删除特定字符串时过滤数据。以下是代码:

main_file = open('data_base.txt').read()    # your main dataBase file
filter_file = open('filter_base.txt', 'w')
filter_file.write(main_file)
filter_file.close()
main_file = open('data_base.txt', 'w')
for line in open('filter_base'):
    if 'your data to delete' not in line:    # remove a specific string
        main_file.write(line)                # put all strings back to your db except deleted
    else: pass
main_file.close()

希望您会发现这很有用!:)

2

我认为如果你将文件读入列表中,然后迭代列表以查找要删除的昵称,则可以更加高效地完成操作。这样做可以避免创建额外的文件,但你需要将结果写回源文件。

以下是我的实现方式:

import, os, csv # and other imports you need
nicknames_to_delete = ['Nick', 'Stephen', 'Mark']

我假设nicknames.csv包含以下数据:

Nick
Maria
James
Chris
Mario
Stephen
Isabella
Ahmed
Julia
Mark
...

然后将文件加载到列表中:
 nicknames = None
 with open("nicknames.csv") as sourceFile:
     nicknames = sourceFile.read().splitlines()

下一步,迭代列表以匹配您需要删除的输入内容:
for nick in nicknames_to_delete:
     try:
         if nick in nicknames:
             nicknames.pop(nicknames.index(nick))
         else:
             print(nick + " is not found in the file")
     except ValueError:
         pass

最后,将结果写回文件中:
with open("nicknames.csv", "a") as nicknamesFile:
    nicknamesFile.seek(0)
    nicknamesFile.truncate()
    nicknamesWriter = csv.writer(nicknamesFile)
    for name in nicknames:
        nicknamesWriter.writeRow([str(name)])
nicknamesFile.close()

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