好久好久没写博客了,有两个原因,一个是有点懒散,另一个也是因为有点懒散。。。
背景
最近因为工作需要支援其他项目的工作,我被分配到的其中一个任务是导出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