Python中mmap模块处理大文本的操作方法

如果现在有一个需求,我们需要处理一个20G的大文件,我们会怎么处理呢?思考下,我们需要怎么实现这个功能。

我们可能会这么实现:

def get_datas():
    source_text_path = "路径"
    with open(source_text_path, 'rb') as f:
        data = f.readlines()
    yield data
if __name__ == '__main__':
    for e in get_datas():
        deal_data(e)  # 处理数据

这样虽然能实现,但是我们处理的时候需要消耗的资源和性能不是很友好,所以我们要优化,也就是使用mmap模块。

mmap是一种虚拟内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一映射关系。它省掉了内核态和用户态页copy这个动作(两态间copy),直接将用户态的虚拟地址与内核态空间进行映射,进程直接读取内核空间,速度提高了,内存占用也少了。

简单点来说,mmap函数实现的是内存共享。内存共享是两个不同的进程共享内存的意思:同一块物理内存被映射到两个进程的各自的进程地址空间。这个物理内存已经被规定了大小(大小一定要比实际写入的东东大)以及名称。当需要写入时,找到内存名称,然后写入内存,等需要读取时候, 首先要知道你要读取多大(因为物理内存比你要读取的东西大,全部读取的话会读到一些“空”的东西),然后寻找对应名称的物理块,然后读取。

mmap 介绍

Windows

mmap.mmap(fileno, length, tagname=None, access=ACCESS_DEFAULT[, offset])

参数说明:

  • fileno:文件描述符,可以是file对象的fileno()方法,或者来自os.open(),在调用mmap()之前打开文件,不再需要文件时要关闭。

  • length:要映射文件部分的大小(以字节为单位),这个值为0,则映射整个文件,如果大小大于文件当前大小,则扩展这个文件。

  • tagname:为映射提供标签名称的字符串,Windows 允许你对同一文件拥有许多不同的映射。如果指定现有标签的名称,则会打开该标签,否则将创建该名称的新标签。如果省略此参数或设置为None ,则创建的映射不带名称。避免使用tag参数将有助于使代码在Unix和Windows之间可移植。

  • access:文件权限

  • ACCESS_READ:读访问;

  • ACCESS_WRITE:写访问,默认;

  • ACCESS_COPY:拷贝访问,不会把更改写入到文件,使用flush把更改写到文件。

offset:非负整数偏移量,默认从0开始。

Unix

mmap.mmap(fileno, length, flags=MAP_SHARED, prot=PROT_WRITE|PROT_READ, access=ACCESS_DEFAULT[, offset])

参数说明:

  • flags:映射的性质,默认MAP_SHARED。
  • MAP_PRIVATE 会创建私有的写入时拷贝映射,因此对mmap对象内容的修改将为该进程所私有;
  • MAP_SHARED 会创建与其他映射同一文件区域的进程所共享的映射。
  • prot:它将给出所需的内存保护方式;最有用的两个值是 PROT_READ和 PROT_WRITE,分别指明页面为可读或可写。 prot 默认为PROT_READ | PROT_WRITE。
  • access:注意的是可以指定 access 作为替代 flags 和 prot 的可选关键字形参。 同时指定 flags,prot 和 access 将导致错误。

fileno文件描述符有如下

  • os.O_RDONLY:以只读的方式打开Readonly
  • os.O_WRONLY:以只写的方式打开Write only
  • os.O_RDWR:以读写的方式打开 Read and write
  • os.O_APPEND:以追加的方式打开
  • os.O_CREAT:创建并打开一个新文件
  • os.O_EXCL:os.O_CREAT| os.O_EXCL 如果指定的文件存在,返回错误
  • os.O_TRUNC:打开一个文件并截断它的长度为零(必须有写权限)
  • os.O_BINARY:以二进制模式打开文件(不转换)
  • os.O_NOINHERIT:阻止创建一个共享的文件描述符
  • os.O_SHORT_LIVED
  • os.O_TEMPORARY:与O_CREAT一起创建临时文件
  • os.O_RANDOM:缓存优化,但不限制从磁盘中随机存取
  • os.O_SEQUENTIAL :缓存优化,但不限制从磁盘中序列存取
  • os.O_TEXT:以文本的模式打开文件(转换)

支持的方法

  • close(): 关闭 mmap。 后续调用该对象的其他方法将导致引发 ValueError 异常。 此方法将不会关闭打开的文件。
  • closed: 如果文件已关闭则返回 True。
  • find(str, start, end): 从 start 下标开始,在 m中从左往右寻找子串 str最早出现的下标;没有找到则返回-1。
  • flush([offset, n]):将对文件的内存副本的修改刷新至磁盘。 如果不使用此调用则无法保证在对象被销毁前将修改写回存储。 如果指定了 offset和 size,则只将对指定范围内字节的修改刷新至磁盘;在其他情况下,映射的全部范围都会被刷新。
  • windows: 返回的非零值表示成功;否则返回0。 零表示失败。
  • unix: 返回零值以表示成功。 当调用失败时将引发异常。
  • move(dest, src, count): 将从偏移量 src开始的 count个字节拷贝到目标索引号 dest。 如果 mmap 创建时设置了 ACCESS_READ,则调用 move将引发异常。
  • read([n]): 返回一个字节,其中包含从当前文件位置开始的至多 n 个字节。 如果参数省略,为 None 或负数,则返回从当前文件位置开始直至映射结尾的所有字节。 文件位置会被更新为返回字节数据之后的位置
  • read_byte():返回一个1字节长的字符串,从 m 对应的文件中读1个字节,要是已经到了EOF还调用 read_byte(),则抛出异常 ValueError。
  • readline():返回一个字符串,从 m 对应文件的当前位置到下一个’\n’,当调用 readline() 时文件位于 EOF,则返回空字符串。
  • resize(newsize):如果存在的话, 改变映射以及下层文件的大小。 如果 mmap 创建时设置了 ACCESS_READ或 ACCESS_COPY,则改变映射大小将引发异常。
  • rfind(sub[, start[, end]]):返回子序列 sub在对象内被找到的最大索引号,使得 sub 被包含在 [start, end] 范围中。 可选参数 start和 end 会被解读为切片表示法。 如果未找到则返回 -1。
  • seek(pos[, whence]):设置文件的当前位置。 whence 参数为可选项并且默认为 os.SEEK_SET 或 0 (绝对文件定位);其他值还有 os.SEEK_CUR 或 1 (相对当前位置查找) 和 os.SEEK_END 或 2 (相对文件末尾查找)。
  • size():返回文件的长度,该数值可以大于内存映射区域的大小。
  • tell():返回文件指针的当前位置。
  • write(str):将str写入文件指针当前位置的内存并返回写入的字节总数 (一定不小于 len(str),因为如果写入失败,将会引发错误)。 在字节数据被写入后文件位置将会更新。 如果 mmap 创建时设置了 ACCESS_READ,则向其写入将引发 异常
  • write_byte(byte):将整数值 byte 写入文件指针当前位置的内存;文件位置前进 1。 如果 mmap 创建时设置了 ACCESS_READ,则向其写入将引发异常。

对于EOF的处理,write() 和 read_byte() 抛出异常 ValueError,而 write_byte() 和 read() 什么都不做。

使用mmap读取大文件

from mmap import mmap
def read_data(file_path):
    with open(file_path, "r+") as f:
        m = mmap(f.fileno(), 0)
        g_index = 0
        for index, char in enumerate(m):
            if char == b"\n":
                yield m[g_index:index + 1].decode()
                g_index = index + 1
if __name__ == "__main__":
    file_path = ""
    for content in read_data(file_path):
        print(content)

什么时候用mmap?

用mmap来读取超大文件,不是mmap的主要应用场景,Python官方文件也没有提到这一点。如果仅仅是读取超大文件,使用文件对象的read(N),来得更快更好更简单。

关于标准库中的mmap模块。现有一个需求,要对超大文件(接近40G)进行读写,notepad++等工具直接拒绝打开此文件。用 r+ 模式打开文件,可以随意读写,但是要特别小心。readline()是否能够使用,要看这个文件每行都多长,如果没有换行,就不能用,就算知道每行的大小,也要带个参数N来控制最大读取数量。readlines()是肯定不能用的,就算带参数,也可能直接卡死!read(N)没问题,主要控制是N的大小。

总之,传统读写文件的方式可以用,但是不够方便。速度也是个问题,传统的缓存IO方式,涉及到OS内核态的内存和进程虚拟空间内存的内容交换,对于超大文件而言,这种交换会浪费大量的CPU时间和内存。mmap是另一个方式!它省掉了内核态和用户态页拷贝这个动作(两态间copy),直接将用户态的虚拟地址与内核态空间进行映射,进程直接读取内核空间,速度提高了,内存占用也少了。

总结来说,常规文件操作为了提高读写效率和保护磁盘,使用了页缓存机制。这样造成读文件时需要先将文件页从磁盘拷贝到页缓存中,由于页缓存处在内核空间,不能被用户进程直接寻址,所以还需要将页缓存中数据页再次拷贝到内存对应的用户空间中。这样,通过了两次数据拷贝过程,才能完成进程对文件内容的获取任务。写操作也是一样,待写入的buffer在内核空间不能直接访问,必须要先拷贝至内核空间对应的主存,再写回磁盘中(延迟写回),也是需要两次数据拷贝。

而使用mmap操作文件中,创建新的虚拟内存区域和建立文件磁盘地址和虚拟内存区域映射这两步,没有任何文件拷贝操作。而之后访问数据时发现内存中并无数据而发起的缺页异常过程,可以通过已经建立好的映射关系,只使用一次数据拷贝,就从磁盘中将数据传入内存的用户空间中,供进程使用。

总而言之,常规文件操作需要从磁盘到页缓存再到用户主存的两次数据拷贝。而mmap操控文件,只需要从磁盘到用户主存的一次数据拷贝过程。说白了,mmap的关键点是实现了用户空间和内核空间的数据直接交互而省去了空间不同数据不通的繁琐过程。因此在某些场景下,mmap效率更高。

从python官网上看mmap的介绍,生成的mmap对象,就像一个bytearray对象,可以直接用index的方式读写,可以切片。同时,mmap对象还有一组类似文件操作的接口,read,readline,flush等等。即mmap对象兼具bytearray和file对象的功能。不过还是要注意,对于超大文件的读(先不考虑写的问题吧),从磁盘到内核,依然会占用内存,因此绝对不能一口气全部读出来。read(N)是必须的,mmap的使用只是可能会提高效率。(如果频繁的创建和关闭mmap映射,这种创建是为了指向超大文件的不同位置,反而效率更低。一般情况下的read(N)实现,不需要使用mmap。)

mmap的另一个应用场景,是进程间的内存共享。多个进程将同一个文件map到同一段内核地址上,即实现了相互之间的共同访问。

总结:使用mmap的时机

  • 将一个普通文件映射到内存中,通常在需要对文件进行频繁读写时使用,这样用内存映射读写取代I/O缓存读写,以获得较高的性能;
  • 将特殊文件进行匿名内存映射,可以为关联进程提供共享内存空间;
  • 为无关联的进程提供共享内存空间,一般也是将一个普通文件映射到内存中。

原文地址:https://blog.csdn.net/weixin_41951954/article/details/128837722