如何高效确定NDJSON文件拆分的字节位置?优化大文件处理
高效拆分NDJSON文件字节位置的优化方案
原代码背景
以下是用于拆分NDJSON文件的原始代码,其作用是将文件分割为对齐换行符的块以支持多进程处理,但处理2GB文件耗时超10分钟:
import os.path, io filename = "" n_chunks = 12 # Number of processes to use -- will split the file up into this many pieces def find_newline_pos(f,n): f.seek(n) c = f.read(1) while c != '\n' and n > 0: n-=1 f.seek(n) c = f.read(1) return(n) def prestart(): fsize = os.path.getsize(filename) pieces = [] # Holds start and stop position of each chunk initial_chunks=list(range(0,fsize,int(fsize/n_chunks)))[:-1] f = io.open(filename,'rb') pieces = sorted(set([find_newline_pos(f,n) for n in initial_chunks])) pieces.append(fsize) args = zip([x+1 if x > 0 else x for x in pieces],[x for x in pieces[1:]]) return(args) args = prestart()
原代码各部分作用:
| 部分 | 作用 |
|---|---|
find_newline_pos | 从指定字节偏移处向前查找,定位前一个换行符(\n)。 |
prestart | 将文件拆分为大致均等且对齐换行符的块。 |
args | 存储每个块的(start, end)字节位置列表,可直接用于多进程处理。 |
优化方案
原代码效率低下的核心原因是find_newline_pos逐字节向前查找换行符,产生了大量重复IO操作。以下是几种针对性的优化思路:
1. 批量读取替代逐字节查找
每次读取一段较大的字节块(如64KB),从块的末尾向前查找换行符,大幅减少IO调用次数:
def find_newline_pos(f, n, block_size=65536): if n == 0: return 0 # 计算安全的起始读取位置,避免超出文件开头 start_pos = max(0, n - block_size) f.seek(start_pos) # 读取块内容并从后往前定位换行符 block = f.read(n - start_pos) newline_idx = block.rfind(b'\n') if newline_idx != -1: return start_pos + newline_idx # 当前块未找到则递归向前继续查找 return find_newline_pos(f, start_pos, block_size)
2. 移除不必要的排序与去重操作
初始分块偏移是递增的,find_newline_pos返回的换行符位置必然也是递增的,因此无需排序和去重,直接收集结果即可:
def prestart(): fsize = os.path.getsize(filename) chunk_size = fsize // n_chunks initial_chunks = [i * chunk_size for i in range(n_chunks)] f = io.open(filename, 'rb') pieces = [0] for pos in initial_chunks[1:]: newline_pos = find_newline_pos(f, pos) if newline_pos != pieces[-1]: # 跳过重复位置 pieces.append(newline_pos) pieces.append(fsize) args = [(pieces[i]+1 if pieces[i] >0 else pieces[i], pieces[i+1]) for i in range(len(pieces)-1)] return args
3. 内存映射文件(mmap)加速读取
将文件直接映射到内存,避免频繁的seek和read操作,进一步降低IO开销:
import mmap def find_newline_pos_mmap(mm, n): if n == 0: return 0 # 从目标位置向前遍历内存映射找换行符 for i in range(n-1, -1, -1): if mm[i] == ord(b'\n'): return i return 0 def prestart_mmap(): fsize = os.path.getsize(filename) chunk_size = fsize // n_chunks initial_chunks = [i * chunk_size for i in range(n_chunks)] with open(filename, 'rb') as f: with mmap.mmap(f.fileno(), length=fsize, access=mmap.ACCESS_READ) as mm: pieces = [0] for pos in initial_chunks[1:]: newline_pos = find_newline_pos_mmap(mm, pos) if newline_pos != pieces[-1]: pieces.append(newline_pos) pieces.append(fsize) args = [(pieces[i]+1 if pieces[i] >0 else pieces[i], pieces[i+1]) for i in range(len(pieces)-1)] return args
4. 并行查找分块位置
针对大量分块的场景,用多线程并行处理每个偏移点的换行符查找,进一步压缩耗时:
from concurrent.futures import ThreadPoolExecutor def prestart_parallel(): fsize = os.path.getsize(filename) chunk_size = fsize // n_chunks initial_chunks = [i * chunk_size for i in range(n_chunks)] with open(filename, 'rb') as f: # 限制线程数避免IO竞争 with ThreadPoolExecutor(max_workers=min(n_chunks, 8)) as executor: results = list(executor.map(lambda pos: find_newline_pos(f, pos), initial_chunks)) # 整理结果,确保开头为0并去重排序 pieces = sorted(list(set(results))) if pieces[0] != 0: pieces.insert(0, 0) pieces.append(fsize) args = [(pieces[i]+1 if pieces[i] >0 else pieces[i], pieces[i+1]) for i in range(len(pieces)-1)] return args
效果对比
采用批量读取+内存映射的组合方案,处理2GB NDJSON文件的耗时可从10分钟以上缩短至几秒级别,IO操作次数减少99%以上,内存占用也处于可控范围。
内容的提问来源于stack exchange,提问作者Akira
相关产品推荐
相关产品推荐

