open mode

  • 'w'
  • 'r'
  • 'a'

    • append 模式
    • 注意: 文件不存在,不会报错 : 1. 创建文件 2. 写入文件

行分割符 linesep 的处理

read write 的默认转换

原则:在以 text ( "r" ) 模式打开文件时,python 会自动把 os.linesep 替换成 ~"\n"~。

即不论文件中存储的是 "\r"(macos)、 "\r\n"(windows) 或者 "\n" 在当前系统上, python 的 read 都会把它替换成"\n"; python write 会做逆向转换

关于 file.seek() 操作

注意 os.linesep 处理陷阱:

  1. os.read 读取到的是一个字符 "\n" , 但是文件指针(offset)跳转的却是 len(os.linesep)
  2. 因此,如果读取 "\n" 后,想要跳转回原来的位置需要跳转 len(os.linesep) 个偏移量

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    
    import os
    with open(file, "r+") as f:
        ...
        pos = f.tell()
        ch = f.read(1)              # 假设读取到的是“\n”
        # 现在的位置是 pos + len(os.linesep)
    
    
        # 跳转回“\n” 之前的正确方法
        f.seek(f.tell()-len(os.linesep), os.SEEK_SET)
        # 或者下面的方法
        f.seek(pos, os.SEEK_SET)
    
        # 错误方法
        f.seek(f.tell() - 1, os.SEEK_SET)     # windows 上偏移量计算错误
        f.seek(-len(os.linesep), os.SEEK_CUR) # 验证报错

write read 和 seek 偏移量的差异

  1. f.seek 总是以 1 byte 为偏移量单位
  2. f.read 和 f.write

    • 在 text mode 读写是,以 1 个字符为偏移量单位

      • 注意可能是多个 byte, 对于 os.linesep 或者 多字节字符
    • 在 byte mode 读写时,以 1 个 byte 为偏移量单位

删除文件末尾换行符 os.linesep 的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def file_peek(f, direction=-1):
    print(f'before peek, {f.tell()=}, {direction=}')
    before = f.tell()
    if direction > 0:
        result = f.read(1)
    elif direction < 0:
        f.seek(f.tell() - 1, os.SEEK_SET)
        result = f.read(1)
    else:
        result = None
    f.seek(before, os.SEEK_SET)
    print(f'after peek, {f.tell()=}, {direction=}, {str(result)}, {len(result)}')
    return result

def delete_last_linesep(file, content):
    with open(file, 'r+', encoding='utf-8') as f:
        # move pointer to end
        f.seek(0, os.SEEK_END)
 
        pos = f.tell()
        # move pos to last - 1
        if pos > len(os.linesep) and file_peek(f, -1) == '\n': # 最后一行是'\n'结尾的情况
            f.seek(f.tell() - len(os.linesep), os.SEEK_SET)
            f.truncate()

修改最好一行的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import os

def file_peek(f, direction=-1):
    print(f'before peek, {f.tell()=}, {direction=}')
    before = f.tell()
    if direction > 0:
        result = f.read(1)
    elif direction < 0:
        f.seek(f.tell() - 1, os.SEEK_SET)
        result = f.read(1)
    else:
        result = None
    f.seek(before, os.SEEK_SET)
    print(f'after peek, {f.tell()=}, {direction=}, {str(result)}, {len(result)}')
    return result

def file_walk(f, count):
    """ move cursor count char"""
    return f.seek(f.tell() + count, os.SEEK_SET)
    
    
def modify_last_line(file, content):
    with open(file, 'r+', encoding='utf-8') as f:
        # move pointer to end
        f.seek(0, os.SEEK_END)
 
        pos = f.tell()
        # move pos to last - 1
        if pos > len(os.linesep) and file_peek(f, -1) == '\n': # 最后一行是'\n'结尾的情况
            print(repr(file_peek(f, -len(os.linesep))))
            # file_walk(f, -len(os.linesep))
            f.seek(f.tell() - len(os.linesep), os.SEEK_SET)
            f.truncate()
            pos = f.tell()
            
        i = 0
        while pos > 0:
            if file_peek(f, -1) == '\n':
                pos = f.tell()
                break
            
            file_walk(f, -1)
            pos = f.tell()
            print(f'{pos = }')
                
            i += 1
            if i > 1000:
                break
            

        print(f'last: {pos = }')
        # find '\n'
        if pos > 0:
            # switch to pos + 1, after '\n', ie. the last line with content
            f.seek(pos, os.SEEK_SET)
            f.truncate()
    
        f.write(content)

文本编辑器最好一个换行 os.linesep 表示

  1. vscode

    • 末尾没有 os.linesep

      1 this is line
      
      • windows 上: "this is line"
    • 末尾有 os.linesep

      1 this is line
      2
      
      • windows 上: "this is line\r\n"
    • 注意:

      • 通过 有行号的空行 标志 os.linesep
      • 光标可移动位置不能说明问题
  2. emacs

    • 末尾没有 os.linesep

      1 this is line
      
    • 末尾有 os.linesep

      1 this is line
      
      • windows 上: "this is line\r\n"

        • 一个 os.linesep
      1 this is line
      2
      
      • windows 上: "this is line\r\n\r\n"

        • 两个 os.linesep
    • 注意:

      • 通过 光标可移动的空行数 标志有几个 os.linesep

TODO 多字节字符的读写 f.read() f.write()