Python中mmap模块(处理大文本)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

如果现在有一个需求我们需要处理一个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 的可选关键字形参。 同时指定 flagsprot 和 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_EXCLos.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() 抛出异常 ValueErrorwrite_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对象还有一组类似文件操作的接口readreadlineflush等等。即mmap对象兼具bytearray和file对象的功能。不过还是要注意对于超大文件的读先不考虑写的问题吧从磁盘到内核依然会占用内存因此绝对不能一口气全部读出来。read(N)是必须的mmap的使用只是可能会提高效率。如果频繁的创建和关闭mmap映射这种创建是为了指向超大文件的不同位置反而效率更低。一般情况下的read(N)实现不需要使用mmap。

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

总结使用mmap的时机

  1. 将一个普通文件映射到内存中通常在需要对文件进行频繁读写时使用这样用内存映射读写取代I/O缓存读写以获得较高的性能

  1. 将特殊文件进行匿名内存映射可以为关联进程提供共享内存空间

  1. 为无关联的进程提供共享内存空间一般也是将一个普通文件映射到内存中。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: python