内容概述
Python文件操作
针对大文件如何操作
为什么不能修改文件?
你需要知道的基本知识
1. Python文件操作
这一部分内容不是重点,因为很简单网上很多,主要看看文件操作的步骤就可以了。
#!/usr/bin/env python # -*- coding: UTF-8 -*- # Author: rex.cheny # E-mail: rex.cheny@outlook.com import os import time import sys FILE_PATH = "/Volumes/Work/ProgrammingStudy/AutoOperationStudy/Day03/file.txt" if os.path.exists(FILE_PATH): # 把文件对象给一个变量,这样后续才能操作这个文件对象,这里默认是以只读方式打开 FILE_HANDLER = open(FILE_PATH, encoding="utf-8") # 读取 """ read(size) 不加SIZE可以是整个文件读取,加上SIZE可以一部分一部分的读取,对于大文件要使用SIZE分段读取,对于小文件可以整体读取 readline() 一次读取一行 readlines() 一次读取全部文件到一个列表,适合读取配置文件,因为配置文件通常都是一行一行的,对于配置文件这种非常适合因为它比较小而且 配置文件都是一行一行的 """ data = FILE_HANDLER.read() print(FILE_HANDLER.fileno()) time.sleep(600) print(data) # 关闭 FILE_HANDLER.close() else: print("文件:", FILE_PATH, "不存在。") # 一行一行读取 if os.path.exists(FILE_PATH): FILE_HANDLER = open(FILE_PATH, encoding="utf-8") dataList = FILE_HANDLER.readlines() for line in dataList: print(line) FILE_HANDLER.close() # 写入一行文件 if os.path.exists(FILE_PATH): """ mode r 是只读模式 w 是只写模式打开文件,其实是创建,所以如果原来的文件有内容它就给它清空了,所以在原有的内容写就不能用这种模式 a 追加模式,只能追加 r+ 读写模式,可以读取也能追加,只能追加 rb 读取二进制文件 wb 写入二进制文件 """ FILE_HANDLER = open(FILE_PATH, encoding="utf-8", mode="a") FILE_HANDLER.write("你好吗 ") # 这个语句是立即落盘, 因为延迟写入,上面的write只是写入缓存并没有真正写入磁盘,如果对于写入内容比较多,你不去调用flush的话可能如果执行完write语句后刚好计算机 # 断电,那么就容易出现数据丢失的情况。 FILE_HANDLER.flush() # 关闭已打开的文件流 FILE_HANDLER.close()
更加简单的创建文件对象的方式,上面的方式需要手动关闭文件流,下面的方式它会自动关闭文件流
#!/usr/bin/env python # -*- coding: UTF-8 -*- # Author: rex.cheny # E-mail: rex.cheny@outlook.com import os import sys FILE_PATH = "/Volumes/Work/ProgrammingStudy/AutoOperationStudy/Day03/file.txt" if os.path.exists(FILE_PATH): with open(FILE_PATH, encoding="utf-8", mode="a") as FILE_HANDLER: FILE_HANDLER.write("你好吗 ")
通过with open还可以打开多个文件
with open (FILE1, "r") as f1, open(FILE2, "w") as f2: pass
2. 针对大文件如何操作
如何操作大文件比如10G一个文件,显然不能使用read()和readlines(),就算你的内存够大,这样一个文件都读取到内存里显然不是一个好办法,太浪费了内存了,另外从磁盘加载到内存页需要时间。是不是可以使用readline()?可以一行行读取,不过读的时候虽然是一行,可是不断的读取到内存最终还是会占用很大内存,所以一个好的方法是,读取一行到内存,处理完后把内存的那一行内容删除,然后再读取一行,也就是内存只保留一行。
#!/usr/bin/env python # -*- coding: UTF-8 -*- # Author: rex.cheny # E-mail: rex.cheny@outlook.com import os import sys FILE_PATH = "/Volumes/Work/ProgrammingStudy/AutoOperationStudy/Day03/file.txt" if os.path.exists(FILE_PATH): with open(FILE_PATH, encoding="utf-8", mode="r") as FILE_HANDLER:
# 这里并没有调用文件对象的read方法,此时这里这个文件对象是一个迭代器 for line in FILE_HANDLER: print(line)
说明:由于上面使用的是迭代器而不是一个列表,所以你想控制输出N行或者第N行你就需要自己来做一个计数器,也就自己记录当前是第几行。
3. 为什么不能修改文件
你通过r+打开文件发现即便你使用当前读取到第N行(假设有M行)你调用write()还是无法插入,这个新增的内容只会显示在最后一行
你通过w打开文件发现源文件被清空了,感觉好危险的样子。
感觉vi编辑器、word这些都能直接修改啊?
这都是为什么呢?
数据存储简要原理
磁盘分区的最小单位是柱面,但是数据存储的最小单位是block也就是块,这个是文件系统相关内容,不是磁盘的物理特性。一个文件通常会占用1个或多个块。下面简要画图来看:
为了性能通常都会寻找连续的块来存放数据(读写效率最高,减少寻址时间),但是随着时间的推移(删除、新建、更新文件等操作)导致很多文件存放的块并不是连续的,就想文档2。假设我们现在修改文档1
而且需要修改的数据正好在文档1的块2中。如果这时候允许你直接修改,那就意味着原来的数据存放位置要想后挪动,可是你看如果挪动,block03的内容就容易覆盖了block04也就是文档2的内容,显然这是不允许的,所以肯定不允许你从数据物理存放的角度来插入数据,所以就算你通过r+模式打开也无法插入。如果你以w模式打开系统会当做新文件来处理所以清空原来的数据。
那么为什么r+可以追加呢?追加是在文件末尾,假设block03还有空间那么就继续使用这个块,如果发现这个块空间不够,那么除了使用这个块剩余的空间它在最后保存的时候还会去申请新的块来保存内容,总之原有数据顺序不能变。
VI或者word为什么可以修改?
我们用VI打开文件尤其是一个大文件你会感觉慢,其实是因为它要把内容加载内存,然后你在内存中修改,也就是可以在文件任意位置修改,当修改完成保存磁盘的时候它会覆盖原有内容,同时如果本次修改增加了更多内容则会申请新的磁盘块用于存放。对于word来说也是一样的,你发现打开一个10M以上的word文件也并不快它也是加载到内存里,有人说如果文件很大,我的内存不是很大,能不能打开呢?可以,你是否还记得一个叫做虚拟内存的东西么,当然如果物理内存+虚拟内存都不够了,你也就打不开了。我记得之前用Windows电脑的时候出现过打开文件提示说内存不够。
4. 如何实现修改文件呢
在python中其实也一样,因为它底层调用的就是库函数。如果你想实现修改的效果怎么办?你要么读取都读取到内存中然后修改,最后再保存其实也就是覆盖源文件
我这里有一个之前替换线上配置文件某些信息的例子,main()方法不是重点,可以忽略,后面的replacepath才是文件修改部分。
#!/usr/bin/python # -*- coding: UTF-8 -*- import sys import os import re def main(argv=None): if argv is None: argv = sys.argv # 规范化路径 path = os.path.normpath(r"/Users/rex.chen/Downloads/apps/") config_path = os.path.normpath(r"product/conf") # 获取目录列表 appnamelist = os.listdir(path) modifyappname = [] # 遍历目录 for appname in appnamelist: print appname # 拼接路径 app_config_full_path = os.path.join(path, appname, config_path) filename = r"log4j.properties" filefullpath = os.path.join(app_config_full_path, filename) # 判断路径是否存在 if os.path.exists(filefullpath): print filefullpath key_word = r"${CATALINA_OUT}" new_word = r"/work/logs/{{appname}}-{{port}}" # 调用自定义函数并获取返回值 result = replacepath(appname, filefullpath, key_word, new_word) if result: modifyappname.append(appname) else: pass else: print "文件路径:%s 不存在" % filefullpath print "需要修改日志文件的程序个数为:%s 名称如下:" % len(modifyappname) for tempname in modifyappname: print tempname def replacepath(temp_appname, temp_filepath, temp_keyword, temp_newword, ismodify=False): # 打开文件 file1 = open(temp_filepath, 'r') # 这里不能直接使用传递过来的keyword进行搜索是因为这里需要一个正则表达式。字符串虽然是一个特殊的正则表达式,但是因为有特殊符号需要转义 # 这里的含义是读取文件整体内容,并对内容进行搜索查找符合表达式的内容,把找到的放到变量中,这是一个列表 allchars = re.findall("${CATALINA_OUT}", file1.read()) # 统计列表长度,这样就获取了匹配的个数 count = len(allchars) print "关键字 %s 在程序 %s 出现次数: %s" % (temp_keyword, temp_appname, count) # 如果count大于0,表示这个文件里面有我们要替换的字符,如果不大于0则退出该函数 if count > 0: ismodify = True else: file1.close() return ismodify # 移动指针到文件首部 file1.seek(0) # 读取所有内容 alllines = file1.readlines() # 关闭文件流 file1.close() # 设置正则 p = re.compile("${CATALINA_OUT}") # 以写模式打开文件,因为这里是同一个文件,所以w参数会清空该文件,这样就实现了在同一个文件中查找和替换 file2 = open(temp_filepath, 'w') # 遍历之前的所有内容 for line in alllines: try: # 使用sub进行替换,p对象包含正则也就是需要匹配的内容,temp_newword是新内容,line是在哪里进行查找匹配 c = p.sub(temp_newword, line) # c 对象为替换后的新内容,然后写入文件 file2.write(c) except IOError as err: print "文件写入错误:"+str(err) file2.close() ismodify = True return ismodify if __name__ == "__main__": sys.exit(main())
我上面采取的方式就是全部读取到内存,然后进行处理最后写入。如果文件很大怎么办呢?
如果文件过大你可以一次读取一部分然后进行修改或者用上面提到的文件迭代器,修改后保存到一个新文件,然后在读取一部分修改后追加到那个新文件,所有修改和保存完成之后,删除原文件把新文件重命名为原来的文件名。
5. 额外的基础知识
延迟写入:磁盘是低速设备,内存是高速设备,如果高速设备向低速设备写入数据那根据木桶原理其速度不会高于短板,所以程序就会等待,那么系统中通常有很多服务如果是一个对外提供服务的服务器那么写入数据的操作一定少不了,如果都实时写入磁盘那性能将会非常地下。所以所有写入都先写入buffer也就是缓存,然后由系统来觉得什么时候把缓存中的数据同步到磁盘上。通常来说缓存写满了就会进行一次同步或者在到了一个同步周期也会进行同步。这就是为什么会有flush()的原因。你可以尝试一下,当你调用完write(),然后让程序睡眠一下,你去查看原来的文件一定没有你写入的数据。