Python的file.truncate()意外未截断

3 投票
2 回答
77 浏览
提问于 2025-04-14 15:23

我有一个非常简单的Python程序:

def print_file(filename):    
    with open(filename,'r') as read_file:
        print(read_file.read())

def create_random_file(filename,count):
    with open(filename,'w+', encoding='utf-8') as writefile:
        for row_num in range(count):
            writefile.write(f'{row_num}: fo bar baz\n')

def truncate_file_after_first_line(file,read_a_line):
    file.seek(0,0)  # go to start of file
    print(f"After seeking to 0, at position {file.tell()}");
    if (read_a_line):
        header = file.readline()
        print(f"After reading a line, at position {file.tell()}");
        print(f"Found header '{header.rstrip()}'\n")
    file.write('TRUNCATE AFTER THIS\n')
    print(f"After writing marker, at position {file.tell()}");
    file.truncate()

def mangle_file(filename,read_a_line):
    with open(filename,'r+') as file:
        truncate_file_after_first_line(file,read_a_line)

# ----

filename = 'testpy.txt'
read_a_line = True

create_random_file(filename,5)
print("Original file:")
print_file(filename)
mangle_file(filename,read_a_line)
print("Truncated file:")
print_file(filename)

所以,我:

  • 创建了一个包含5行的文件(并且也打印到标准输出)。
  • 然后,在 mangle_file() 函数里:
    • 我用 r+ 选项打开文件,也就是说,可以读取和写入。文件指针在文件的开头。
    • 根据布尔值 read_a_line 的不同,我会:
      • a) 移动到位置0,读取一行,写入标记 TRUNCATE AFTER THIS\n,然后截断文件。
      • b) 移动到位置0,写入标记 TRUNCATE AFTER THIS\n,然后截断文件。
    • 最后,我关闭文件。
  • 然后再读取文件并打印出来。

听起来很简单,但在a)的情况下,当文件的第一行(即 0: fo bar baz)在截断之前被读取时,结果文件是:

0: fo bar baz
1: fo bar baz
2: fo bar baz
3: fo bar baz
4: fo bar baz
TRUNCATE AFTER THIS

也就是说, truncate() 没有起作用,标记被添加到了未截断的文件中。而我本来期待在读取第一行后就截断:

0: fo bar baz
TRUNCATE AFTER THIS

对于b),结果文件如预期那样是:

TRUNCATE AFTER THIS

我对 truncate() 的理解有什么错误吗?

更新:添加了一些 tells

read_a_line = True 时:

Original file:
0: fo bar baz
1: fo bar baz
2: fo bar baz
3: fo bar baz
4: fo bar baz

After seeking to 0, at position 0
After reading a line, at position 14
Found header '0: fo bar baz'

After writing marker, at position 90

Truncated file:
0: fo bar baz
1: fo bar baz
2: fo bar baz
3: fo bar baz
4: fo bar baz
TRUNCATE AFTER THIS

read_a_line = False 时:

Original file:
0: fo bar baz
1: fo bar baz
2: fo bar baz
3: fo bar baz
4: fo bar baz

After seeking to 0, at position 0
After writing marker, at position 20
Truncated file:
TRUNCATE AFTER THIS

2 个回答

3

truncate()这个函数确实可以截断文件,但它截断的位置不对。正如@defaultUsernameN提到的那个讨论串中所说,readline()truncate()之间的互动似乎有一些没有记录的行为,导致它们对文件中“当前”所在位置的理解不一致。相关的问题仍然没有解决。

在你的代码中,你可以通过在截断之前手动调整位置来修复这个问题:

def truncate_file_after_first_line(file,read_a_line):
    file.seek(0,0)  # go to start of file
    if (read_a_line):
        header = file.readline()
        print(f"Found header '{header.rstrip()}'\n")
        file.seek(0,1) # go to second line
    file.write('TRUNCATE AFTER THIS\n')
    file.truncate()

输出:

Original file:
0: fo bar baz
1: fo bar baz
2: fo bar baz
3: fo bar baz
4: fo bar baz

Found header '0: fo bar baz'

Truncated file:
0: fo bar baz
TRUNCATE AFTER THIS
3

根据fopen的说明

在读写流中,读和写可以随意交替进行。需要注意的是,ANSI C要求在输出和输入之间必须有一个文件定位的操作,除非输入操作遇到了文件末尾。(如果没有满足这个条件,那么读取的结果可能会包含除了最近一次写入之外的其他写入结果。)因此,在Linux下,最好在这样的流的写入和读取操作之间加上fseek(3)或fsetpos(3)操作。这种操作可能看起来没有任何作用(比如调用fseek(..., 0L, SEEK_CUR)只是为了它的同步效果)。

如果你把

    file.write('TRUNCATE AFTER THIS\n')

改成

    file.seek(file.tell()) 
    file.write('TRUNCATE AFTER THIS\n')

这个函数应该会按照你想要的方式运行(在我的MacOS上是这样)。否则,“r+”模式可能会表现得像“ra”模式(尽管这并不是一个有效的模式)——也就是说,只能在文件的后面或最后追加内容(这取决于已经缓冲了多少文本)。

撰写回答