上一篇文章《python3实现归并排序算法图文详解》中,我们了解了归并排序算法的基本使用逻辑。这一篇文章我们对这个逻辑进行一个延伸,从内存延伸到对硬盘上的文件进行排序,也就是所谓的外部排序。
所谓的大文件就是无法一次性全部读到内存中的文件。
为了操作不真的把我机器的内存都榨干,这里假设机器的内存是300MB,刨除一些系统占用,规定每次读到内存的文件大小不能超过200MB。
我又用下面的程序创建了一个1GB大小的文件
with open('bigfile.txt','a') as f:
for i in range(100000000):
f.write(str(random.randint(100000000,999999999))+'\r\n')
这个文件一共有1亿行,每一行以字符串格式存储着一位9位数,并在末尾加上了换行符,所以每行占据10B,算下来一共有10亿B也就是1GB。
下面要实现的就是在内存占用每次不超过200MB的情况下将这个1GB的文件进行排序。硬盘空间很大,随意使用。
大家应该还记得《python3实现归并排序算法图文详解》中的两张图
针对一个长列表,我们先分割为两个小列表,并将小列表排好序。利用两个游标从左到右移动,我们对两个已经各自排好序的列表合并成了一个新的有序列表。
这里的小技巧就是利用了递归的思路,对每一个小的列表都进行了归并排序。
但是我们这个问题不需要这么麻烦。首先将1GB的大文件分解为5个200MB的小文件,这里的小文件就是可以直接读进内存一次性排序。
然后直接对这5个小文件读进内存进行排序。
最后再利用归并的思路,通过5个游标分别在5个文件中逐行从左往右移动,将每次最小的一行读到新文件中,达到排序的效果,如下图
文件分割
首先来对文件进行分割,每次从原文件读200MB的整行数,然后保存到一个新文件。
这里要补充下python中读取文件的几个方法。
f.readlines(n)
可以接一个参数n
,注意这里的n
并不是行数,而是以字节为单位的大小,表示当读到某行时总大小超过了n就不再读下一行。同理,f.readline(n)
也可以传递n,表示读取下一行的前n个字节,如果下一行总大小少过n就读取整行。而在某些没有换行符的文件中就不能用这两种读取的方法了,因为无论如何都是直接读取所有内容,只能采用f.read(n)
的方式,这里的n也同样是字节数
def splitFile(path, chunk):
baseDir, baseFile = os.path.split(path)
fileIndex = 1
files = []
with open(path, 'r') as f:
while True:
lines = f.readlines(chunk)
lines.sort()
if lines:
newFileName = os.path.join(baseDir, str(fileIndex))
with open(newFileName, 'a') as sf:
sf.write(''.join(lines))
files.append(newFileName)
fileIndex += 1
else:
break
return files
if __name__ == '__main__':
path = '/home/fuhx/python_projects/datanalgo/linkedlist/bigfile.txt'
chunk = 1024 * 1024 * 200
files = splitFile(path, chunk)
print(files)
这里会将新文件保存到和原文件相同的目录,所以首先获取baseDir
用来和后面的文件名拼接。新的文件名也很简单,从数字1开始一直往上加。
需要注意的就是这里的trunk
,是字节数,f.readlines(trunk)
表示逐行读取一直到超过trunk
大小就不再读下一行。后面对trunk
赋值1024*1024*200
就是200MB
,所以每个新文件的大小大约就是200MB
。
lines.sort()
是对读进来的200MB
内容进行排序,sort()
方法并不会生成新的列表所以不会占用更多内存。
之后如果读取到的内容不为空,就用空字符串将内容列表拼接起来写入新文件,否则跳出循环不再读。
空字符串在python中也是False,同样的还有空字典,空列表等
运行完返回如下
['/home/fuhx/python_projects/datanalgo/linkedlist/1',
'/home/fuhx/python_projects/datanalgo/linkedlist/2',
'/home/fuhx/python_projects/datanalgo/linkedlist/3',
'/home/fuhx/python_projects/datanalgo/linkedlist/4',
'/home/fuhx/python_projects/datanalgo/linkedlist/5']
查看一下每个文件的大小,除了最后一个文件,其余的都是大约200MB
-rw-rw-r-- 1 fuhx fuhx 201M Jul 18 23:50 1
-rw-rw-r-- 1 fuhx fuhx 201M Jul 18 23:52 2
-rw-rw-r-- 1 fuhx fuhx 201M Jul 18 23:53 3
-rw-rw-r-- 1 fuhx fuhx 201M Jul 18 23:53 4
-rw-rw-r-- 1 fuhx fuhx 154M Jul 18 23:54 5
并且这些文件都已经各自排好序了。
归并文件
然后需要将这些文件进行归并法组合成新的文件了。
通过创建一个字典,key为5个文件的指针,value为当前各自文件中游标对应的一行数据。将value最小的值写入新文件,同时对对应的key读取下一行,来达到循环比较的目的。
如果有文件读到的下一行为空,则删除该key,并关闭文件指针。这里因为没有用with open
的语法,注意自己主动关闭文件。
def mergeFiles(fileList: list) -> str:
"""
:param fileList: a list of file absolute path
:return: a string of merged file absolute path
"""
fs = [open(file_, 'r') for file_ in fileList]
tempDict = {}
mergedFile = open('merged.txt', 'a')
for f in fs:
initLine = f.readline()
if initLine:
tempDict[f] = initLine
# print(tempDict)
while tempDict:
min_item = min(tempDict.items(), key=lambda x: x[1])
mergedFile.write(min_item[1])
nextLine = min_item[0].readline()
if nextLine:
tempDict[min_item[0]] = nextLine
else:
del tempDict[min_item[0]]
min_item[0].close()
mergedFile.close()
return os.path.join(os.getcwd(), 'merged.txt')
首先从文件名列表通过列表生成器返回文件指针的列表,然后循环读取初始行。之后如果用来比较的字典不为空,则执行上面的逻辑,最后返回文件完整路径。
这里用来比较的语法min(tempDict.items(), key=lambda x: x[1])
使用了key
参数,不明白的朋友可以参考另一篇博客《python中min和max方法的key参数使用方法详解》。
最后成功返回新文件,通过对比发现比原文件占用硬盘要少,不过总行数是一样的。
[fuhx@testmachine linkedlist]$ cat merged.txt | wc -l
100000000
[fuhx@testmachine linkedlist]$ cat bigfile.txt | wc -l
100000000
[fuhx@testmachine linkedlist]$ ll -h merged.txt bigfile.txt
-rw-rw-r-- 1 fuhx fuhx 1.1G Jul 17 17:37 bigfile.txt
-rw-rw-r-- 1 fuhx fuhx 954M Jul 19 11:51 merged.txt
通过top
可以查看,在进行合并的过程中,内存占用了0.2%
,我电脑4GB的内存大约就是8MB,但是CPU使用已经接近100%
不过,在进行文件分割的时候,内存飙到了60%
。但是这并不代表在任何机器上都会占用这么高的内存,因为Linux有一套自己的内存使用规则。
说到Linux中的内存管理,这又是一个大话题了,下次再单独写一篇博客,聊一聊free
命令查看内存的各个字段,以及如何控制/proc/sys
中的内核参数来优化内存使用。
我是T型人小付,一位坚持终身学习的互联网从业者。喜欢我的博客欢迎在csdn上关注我,如果有问题欢迎在底下的评论区交流,谢谢。
本文地址:https://blog.csdn.net/Victor2code/article/details/107443547
如对本文有疑问, 点击进行留言回复!!
Python笔记-UiSelector中resourceId定位方式
【3Dtiles】3Dmax模型处理为gltf和3dtiles,包含LOD效果
荐 用Django全栈开发——08. 使用AdminLTE开发前端登录页面
网友评论