使用Python从文本文件中删除一行

4
我有一个文件,每一行都以一个数字开头。用户可以通过输入要删除的行的数字来删除该行。
我遇到的问题是设置打开模式。当我使用`a+`时,原始内容仍然存在。但是,我想保留的行被附加到文件末尾。另一方面,当我使用`w+`时,整个文件都被删除了。我确定有比使用`w+`模式打开它,删除所有内容,然后重新打开并追加行的更好方法。
 def DeleteToDo(self):
    print "Which Item Do You Want To Delete?"
    DeleteItem = raw_input(">") #select a line number to delete
    print "Are You Sure You Want To Delete Number" + DeleteItem + "(y/n)"
    VerifyDelete = str.lower(raw_input(">"))
    if VerifyDelete == "y":
        FILE = open(ToDo.filename,"a+") #open the file (tried w+ as well, entire file is deleted)
        FileLines = FILE.readlines() #read and display the lines
        for line in FileLines:
            FILE.truncate()
            if line[0:1] != DeleteItem: #if the number (first character) of the current line doesn't equal the number to be deleted, re-write that line
                FILE.write(line)
    else:
        print "Nothing Deleted"

这是一个典型文件的样子。
1. info here
2. more stuff here
3. even more stuff here

4
“我相信有比使用w+模式打开文件、删除所有内容、再重新打开并追加行更好的方法。” “不行。” - millimoose
那么,在a+模式下没有办法删除文件的内容吗? - user1104854
1
你唯一能做的就是将文件截断到要删除行的起始位置之前,然后只写出之后的行。即使这样做,如果你没有快速确定哪个字节的方法,这可能也不能加速。(例如索引等) - millimoose
1
事实上,数据库系统设计的一个主要影响因素是它们必须克服文件系统无法执行您要求的操作的限制。(因此,它们能够通过更新更改/替换固定大小的记录来完成所有操作。) - millimoose
@user1104854 truncate()的文档指出:“如果提供了可选的大小参数,则文件将被截断为该大小。大小默认为当前位置。”由于readlines()读取文件中的所有内容,当前位置可能是文件末尾。正如我所说,您需要在删除行之前截断文件,然后写出所有行。 - millimoose
显示剩余10条评论
5个回答

2
当您打开一个文件进行写入时,您会覆盖该文件(删除其当前内容并启动新文件)。您可以通过阅读open()命令的文档来了解此信息。
当您打开一个文件进行追加时,您不会覆盖该文件。但是如何只删除一行?文件是存储在存储设备上的字节序列;您无法删除一行并使所有其他行自动“向下滑动”到存储设备上的新位置。
(如果您的数据存储在数据库中,则实际上可以从数据库中删除一行; 但文件不是数据库。)
因此,传统的解决方法是:您从原始文件中读取,并将其复制到新的输出文件中。在复制时,您执行任何所需的编辑;例如,您可以通过不复制该行来删除一行;或者您可以通过在新文件中编写该行来插入一行。
然后,在成功写入新文件且成功关闭它且没有错误的情况下,您可以重命名新文件为与旧文件相同的名称(这会覆盖旧文件)。
在Python中,您的代码应该是这样的:
import os

# "num_to_delete" was specified by the user earlier.

# I'm assuming that the number to delete is set off from
# the rest of the line with a space.

s_to_delete = str(num_to_delete) + ' '
def want_input_line(line):
    return not line.startswith(s_to_delete)

in_fname = "original_input_filename.txt"
out_fname = "temporary_filename.txt"

with open(in_fname) as in_f, open(out_fname, "w") as out_f:
    for line in in_f:
        if want_input_line(line):
            out_f.write(line)

os.rename(out_fname, in_fname)

请注意,如果你恰好有一个名为temporary_filename.txt的文件,该代码将覆盖它。实际上,我们并不在意文件名是什么,我们可以使用tempfile模块请求Python为我们创建一些唯一的文件名。
任何较新版本的Python都允许您在单个with语句中使用多个语句,但如果您正在使用Python 2.6或类似版本,则可以嵌套两个with语句以获得相同的效果:
with open(in_fname) as in_f:
    with open(out_fname, "w") as out_f:
        for line in in_f:
            ... # do the rest of the code

另外,请注意我没有使用.readlines()方法来获取输入行,因为.readlines()会一次性将整个文件的内容全部读入内存,如果文件非常大,这样做会很慢甚至无法工作。你可以简单地使用从open()返回的“文件对象”编写一个for循环;这将使你每次得到一行,即使是真正大的文件,你的程序也能够处理。
编辑:请注意,我的答案假设你只想进行一次编辑步骤。如@jdi在另一个答案的评论中指出的那样,如果你想允许“交互式”编辑,用户可以删除多行、插入行或其他操作,则最简单的方法实际上是使用.readlines()将所有行都读入内存,对结果列表进行插入/删除/更新等操作,然后仅在编辑完成时将列表写入文件一次。

谢谢你的写作!我已经使用上面的当前方法使其工作,但我会尝试你的方法。 - user1104854

1
def DeleteToDo():
    print ("Which Item Do You Want To Delete?")
    DeleteItem = raw_input(">") #select a line number to delete
    print ("Are You Sure You Want To Delete Number" + DeleteItem + "(y/n)")
    DeleteItem=int(DeleteItem) 
    VerifyDelete = str.lower(raw_input(">"))
    if VerifyDelete == "y":
        FILE = open('data.txt',"r") #open the file (tried w+ as well, entire file is deleted)
        lines=[x.strip() for x in FILE if int(x[:x.index('.')])!=DeleteItem] #read all the lines first except the line which matches the line number to be deleted
        FILE.close()
        FILE = open('data.txt',"w")#open the file again
        for x in lines:FILE.write(x+'\n')    #write the data to the file

    else:
        print ("Nothing Deleted")
DeleteToDo()

太棒了,经过小小的调整(将DeleteItem更改为字符串),这个程序运行得非常好。你能解释一下“lines = [x.strip....]”这句话的意思吗?我查了一下strip函数,它可以去除前导字符,但是最后一部分具体是在做什么呢?(x[:x.index.....) - user1104854
strip()函数可用于删除字符串中的前导和尾随空格。 x[:x.index('.')] 函数返回当前行中从开头到第一个点号(.)字符之间的子字符串,该子字符串是数字。如果该子字符串的整数值等于 DeleteItem,则会跳过该行。 - Ashwini Chaudhary
只有当一行满足了“if”条件时,才会在结尾执行 strip() - Ashwini Chaudhary

1

不要逐行将所有行写入文件,而是从内存中删除该行(使用readlines()读取文件的行),然后一次性将内存写回磁盘。这样你就可以得到想要的结果,而且不会阻塞I/O。


“Clog the I/O”?(这听起来像是缓冲区会以某种方式处理的东西。) - millimoose
这取决于文本文件的大小。对于巨大的文件,逐行读写文件可能会更简单(从程序上)并且如果您按块读/写它们,则速度会更快。 - Dhara
@bos,您能否给我一个关于您所说的内容的例子?我是Python的新手,不太明白您的意思。 - user1104854
我不建议使用.readlines()方法函数。这会将整个文件读入内存,如果文件非常大,这将是一个问题。请参阅我的答案,了解如何编写一个for循环,每次只读取一行。 - steveha
@steveha:我认为,如果文件太大,一次性读取会导致问题,那么除了数据库以外的任何选择都是不好的。 - Amr
显示剩余4条评论

0

您不需要在文件中检查行号,可以像这样做:

def DeleteToDo(self):
    print "Which Item Do You Want To Delete?"
    DeleteItem = int(raw_input(">")) - 1
    print "Are You Sure You Want To Delete Number" + str(DeleteItem) + "(y/n)"
    VerifyDelete = str.lower(raw_input(">"))
    if VerifyDelete == "y":
        with open(ToDo.filename,"r") as f:
            lines = ''.join([a for i,a in enumerate(f) if i != DeleteItem])

        with open(ToDo.filename, "w") as f:
            f.write(lines)
    else:
        print "Nothing Deleted"

我尝试了这个,但没有任何反应。也许我误解了你的意思,但我认为有几个小错误。首先,为什么要从DeleteItem中减去1?另外,我认为它必须是一个字符串,这样它才能在下一条语句(打印)中连接起来。我非常感谢你的想法,因为我也想让它正常工作。只是想用Python变得更好。 - user1104854
@user1104854:我已经编辑过来修复字符串问题了,-1是因为通常你会指定从第1行开始删除,而不是0,但是在编程中索引通常从0开始。 - Amr

0
你可以在阅读了适当的文档之后,使用mmap映射文件...

...然后呢?内存映射文件并没有类似数据库的语义。您可以轻松地覆盖字节,但要删除字节并将所有其他字节向下移动仍然是不简单的。 - steveha

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