记一次append file数据写入不符合预期的排查过程

博主在使用Python导出数据到CSV文件时遇到一个问题,即写入的数据被后面的写入内容覆盖。经过排查,发现与Python的文件缓冲区大小(默认8192字节)有关。当写入字符串超过缓冲区大小时,文件指针未正确更新,导致数据覆盖。通过调用`f.tell()`来查看文件指针位置,意外解决了问题。最后,博主解释了8192字节与`open()`函数的缓冲参数关系,并建议在写入大量数据时计算好缓冲区大小。
摘要由CSDN通过智能技术生成

好久好久没写博客了,有两个原因,一个是有点懒散,另一个也是因为有点懒散。。。

背景

最近因为工作需要支援其他项目的工作,我被分配到的其中一个任务是导出db中一个表的部分数据写入到一个csv文件,由于公司规范问题(可能是因为表太大了),不能请sre和dba帮忙,因此需要自己写脚本来导出数据。
这个项目比较旧,是用python写的,导出数据到文件的逻辑也是使用python写的。

因为想一次多写点数据,写入的时候就直接写入了一段比较长的字符串;
同时为了下次执行程序能在上次停下来的地方继续跑,用的是文件追加写的模式。

问题

奇怪的问题出现了,后面写进去的数据居然覆盖到前面去了!
我想要的效果:

我得到的效果:

排查过程

先贴代码:
python版本是2.7,运行环境是linux centos 7.7

import csv

def test_append_file():
    lines = []
    for i in range(10000, 10700):
        lines.append([i, i+1])

    with open('test_append.txt', 'ab') as f:
        cw = csv.writer(f)
        cw.writerows([[i, j] for i, j in lines])


if __name__ == '__main__':
    test_append_file()

但是当我把lines内的元素缩小到只有100,就能正常写入文件。

刚开始我以为难道是lines容不下这么多元素??但是打印了一下长度是对的,开始出现问题的index=10630 前后10个元素也都是对的,所以lines没问题(其实也不可能这么点数量就有问题);
后来我想是不是csv有bug,换成直接写文件的形式,还是一样的问题:

def test_append_file():
    lines = []
    for i in range(10000, 10700):
        lines.append([i, i+1])

    with open('test_append.txt', 'ab') as f:
        # cw = csv.writer(f)
        # cw.writerows([[i, j] for i, j in lines])
        f.writelines(['%d,%d\n' % (i, j) for i, j in lines])
        

if __name__ == '__main__':
    test_append_file()
    

我又想是不是cw.writerows()或者f.writelines()有问题,换成f.write()cw.writerow()一行行地写,还是有问题。。。

网上查了一下,也没有找到一样的问题 凸(艹皿艹 )

但是有人报告过这个问题,是BufferedWriter的问题,但是我试验了一下没有出现issue中出现的现象,应该是我用的python版本已经修复了,所以这个issue也不是我遇到的问题。

虽然和我的问题不一样,但是又想会不会是文件指针没有置到文件末尾导致有问题(其实就是这个问题),
于是在每行写入后还打印了一下f.tell()的结果,但是f.tell()的输出看起来很正常,每行写入之后都有置到文件末尾。
(其实这时候如果我有去看一眼文件写入的效果,可能我就能更早解决了,因为调了f.tell()之后文件指针就对了!!)

一筹莫展,本来想着要不一次写入100个元素算了,但是如果不知道这里为什么会出问题,怎么保证写入100个一定不会有问题呢🤔

换了几次lines内元素的大小,观察了一下开始出现问题的地方,每个元素位数(十进制)变化时,覆盖写入的位置也会不一样,所以应该跟lines个数没关系,跟元素转字符串后总长度有关系;
数了一下,大概是从8193这个位置开始覆盖的,也就是说前面写入了8192个字符,8192,正好是2的13次方,感觉跟文件操作的底层代码应该有点关系。

现在反正知道了8192这个分界线,于是直接构造了一个8192长度的字符串写入文件,想看看能不能发现什么;

str_line = 'a'*8192
str_line += 'b'

另一方面,我再次想试试执行f.tell(),然后去看了一眼生成的文件,这次居然写对了??我把f.tell()注释掉之后,文件开始的地方又被覆盖了??这是什么操作??

这时候不知道为什么突然想到了strace,跟踪了一下进程,于是就看到下面的现象:
没有加f.tell():
在这里插入图片描述
加了f.tell():
在这里插入图片描述
不太懂,但是看起来大概意思是
打开的文件描述符是5,
文件指针指到从末尾的地方偏移0位,
…(看不懂跳过)
将字符串写入文件时,程序先将前面8192个字符’aaaaa…‘写入到文件,
然后再写入剩下的1个字符’b’,
关闭文件。

加了f.tell()后,多执行了:将文件指针重新指到从末尾的地方偏移0位!
也就是说,因为打开文件后,要写入的字符串太长,底层需要分多次写入,但是分次写入的时候,文件指针不知道为啥又指到了开头,于是就出现了后写入的内容覆盖了前面的内容;
而执行了f.tell之后,文件指针又回到了末尾,所以可以正确写入!

关于为什么是8192
open函数

把8192也加入到搜索关键字后,发现其实这个8192是打开文件时调用的open(name[, mode[, buffering]])buffering入参控制的,当没有指定这个入参时,会使用系统默认,可以这么看:
我的运行环境就是8192啦。

>>> import io
>>> print (io.DEFAULT_BUFFER_SIZE)
8192

试了一下把代码中调用open()的时候传入的buffering调小时,出现问题的位置就更偏前了。

而将buffering设置为0(不使用缓冲区)后,可以看到strace的结果少了调用mmap()函数,所以不指定buffering时调用的mmap()函数应该就是为了开辟缓冲区,这时数据也可以正确一次性写入到文件。
在这里插入图片描述

结论

当打开文件时,如果不指定open()的buffering入参,读写文件会使用buffer,buffer大小会使用系统默认值(一般是4096或8192,可以通过io.DEFAULT_BUFFER_SIZE查看)。

而关于我的程序,最好还是使用buffer,然后将buffer_size计算好后填入buffering入参,就可以一次多写一点了。

扩展阅读:
关于为什么要使用buffer

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值