最近在做一个文件的按行去重任务,要求是对超大的json文件按行去重。
由于文件的体积过大(GB级别),因此不可能全部放进内存中进行去重,只能先分成许多小文件然后对多个小文件进行排序去重,最后多个小文件合并成一个大的文件。
最终,以较小的内存完成任务,较高的速度完成了任务。
具体实现思路如下:
- 按行遍历需要去重的大文件(GB级别),计算每一行的hash值,根据 i = (hash值)%n 将该行写入
part_i.json
,这样做的目的是使得各个文件中没有重复的行,i = (hash值)%n不一样,hash值肯定不一样。 - 将小文件读入内存中,使用以下代码进行排序去重,我这里使用多线程来加速排序去重.
- 最后,将去重后的小文件合并合并到一个文件中,因为切分大文件时按行计算
i = (hash值)%n
来将该行写入了对应的文件,因此,各个文件之间是没有重复的行的。
python
代码实现
按行遍历需要去重的大文件(GB级别),计算每一行的hash值,根据 i = (hash值)%n 将该行写入 part_i.json
,这样做的目的是使得各个文件中没有重复的行,i = (hash值)%n不一样,hash值肯定不一样。
import json
import hashlib
import os
import heapq
import threading
import time
def read_json_lines(filename):
"""
使用迭代器返回一个文件的所有行
:param filename:
"""
with open(filename, "r") as f:
for line in f:
yield line
def split_json_file(file_path, output_dir, n_file):
"""
将文件拆分为多个小文件
遍历大文件的每行,计算hash值, 将这行按照hash_code%n存到第n个文件(保证每个文件之间没有重复行)
:param file_path: 输入文件的路径
:param output_dir: 输出的文件夹
:param n_file: 切分的文件数量,需要自己根据文件的大小和内存来确定
:return:
"""
files = [open(f"{output_dir}part_{i}.json", "w") for i in range(n_file)]
for line in read_json_lines(file_path):
data = json.loads(line)
key = hashlib.md5(line.encode()).hexdigest()
index = int(key, 16) % n_file
files[index].write(line)
output_file = []
for f in files:
output_file.append(os.path.abspath(f.name))
# output_file.append(f.name)
f.close()
return output_file
将小文件读入内存中,使用以下代码进行排序去重,我这里使用多线程来加速排序去重
def hash_line(line):
"""
返回字符串的sha_256的hash值
:param line:
:return: hash值
"""
return int(hashlib.sha256(line.encode()).hexdigest(), 16)
# 小文件内部排序
def sort_json_file(file_path, output_path):
"""
对小文件进行排序并去重
:param file_path:
:param output_path:
:return:
"""
print(f"正在排序去重文件{file_path}")
output = ""
with open(file_path, "r") as input_file:
lines = input_file.readlines()
lines.sort(key=hash_line)
lines = list(set(lines))
with open(output_path, "w") as output_file:
output_file.writelines(lines)
output = os.path.abspath(output_file.name)
print(f"{file_path}排序去重完成!")
return output
def split_array(arr, n):
"""
将数组拆分为多个数组
:param arr:
:param n:
:return:
"""
length = len(arr)
return [arr[i:i + n] for i in range(0, length, n)]
# 使用多线程对多个文件进行排序
def sorted_files(files):
"""
对多个文件进行内部排序
:param files:
"""
for input_file in files:
sort_json_file(input_file, input_file)
def mutil_thread_sort_file(files, n_thread=10):
"""
使用多线程对多个文件进行内部排序
:param files: 需要排序的文件列表
:param n_thread: 使用的线程数
"""
if len(files) < n_thread:
# 文件数小于线程数,将线程数改为文件数
n_thread = len(files)
file_lists = split_array(files, n_thread)
threads = []
for file_list in file_lists:
t = threading.Thread(target=sorted_files, args=(file_list,))
t.start()
threads.append(t)
for t in threads:
t.join()
最后,将去重后的小文件合并合并到一个文件中,因为切分大文件时按行计算i = (hash值)%n
来讲该行写入了对应的文件,因此,各个文件之间是没有重复的行的。
def merge_files(file_list, output_file):
"""
:param file_list: 合并的文件列表
:param output_file: 输出的文件
"""
with open(output_file, "w") as output:
for file in file_list:
with open(file, 'r') as f:
for line in f:
output.write(line)
将以上模块进行汇总:
def deduplicate_json_file(file_path, output_path, n_file):
"""
大文件按行hash值去重
#拆分大文件
#遍历大文件的每行,计算hash值, 将这行按照hash_code%n存到第n个文件(保证每个文件之间没有重复行)
#对切分的小文件进行排序然后去重
#归并排序合并所有小文件
#删除临时文件
:param file_path: 大文件路径
:param output_path: 输出路径
:param n_file: 拆分文件数量,拆分后的文件需要能放进内存
"""
temp_dir = "temp_dir/"
os.makedirs(temp_dir, exist_ok=True)
# 对文件进行切分
file_paths = split_json_file(file_path, temp_dir, n_file)
# 对切分的小文件进行排序然后去重
# files = []
# for file in file_paths:
# files.append(sorted_files(file,file))
mutil_thread_sort_file(file_paths)
# 归并排序合并所有小文件
merge_files(file_paths, output_path)
#merge_sorted_files(file_paths, output_path)
# 删除临时文件
for file in file_paths:
if os.path.exists(file):
os.remove(file)
如果想对大文件进行按行排序的话可以对多个小文件进行归并排序
,
将 deduplicate_json_file()
函数中的merge_files(file_paths, output_path)
替换成merge_sorted_files(file_paths, output_path)
即,下是参考代码
将多个文件的当前行保存到堆中,通过堆排序来获取最小值行并加入到大文件中,获取后更新该文件的当前行。一直迭代直到所有文件到达结尾。
# 文件归并排序
def hash_line(line):
return int(hashlib.sha256(line.encode()).hexdigest(), 16)
def merge_sorted_files(file_list, output_file):
# 打开每个文件
readers = [open(file, 'r') for file in file_list]
# 使用堆来维护当前打开的文件的当前行
heap = [(hash_line(reader.readline()), reader, reader.readline()) for i, reader in enumerate(readers) if reader]
heapq.heapify(heap)
with open(output_file, "w") as output:
while heap:
val, reader, line = heapq.heappop(heap)
output.write(line)
# 试图获取下一行
try:
next_line = reader.readline()
if next_line:
heapq.heappush(heap, (hash_line(next_line), reader, next_line))
except Exception as e:
# 如果到达了文件末尾,则忽略该文件
print(f"出现错误{str(e)}")
pass
for f in readers:
f.close()
如果想使用其他的值进行排序,修改hash_line(line)
函数即可。
测试结果:
对150MB
左右,100w
行的数据进行去重,可以在20s
内完成。
对20GB
左右,1亿
行的数据,可以在60min
内完成。
总的来说,去重性能还是不错的。
完整代码:
import json
import hashlib
import os
import heapq
import threading
import time
def read_json_lines(filename):
"""
使用迭代器返回一个文件的所有行
:param filename:
"""
with open(filename, "r") as f:
for line in f:
yield line
def split_json_file(file_path, output_dir, n_file):
"""
将文件拆分为多个小文件
遍历大文件的每行,计算hash值, 将这行按照hash_code%n存到第n个文件(保证每个文件之间没有重复行)
:param file_path: 输入文件的路径
:param output_dir: 输出的文件夹
:param n_file: 切分的文件数量,需要自己根据文件的大小和内存来确定
:return:
"""
files = [open(f"{output_dir}part_{i}.json", "w") for i in range(n_file)]
for line in read_json_lines(file_path):
data = json.loads(line)
key = hashlib.md5(line.encode()).hexdigest()
index = int(key, 16) % n_file
files[index].write(line)
output_file = []
for f in files:
output_file.append(os.path.abspath(f.name))
# output_file.append(f.name)
f.close()
return output_file
def hash_line(line):
"""
返回字符串的sha_256的hash值
:param line:
:return: hash值
"""
return int(hashlib.sha256(line.encode()).hexdigest(), 16)
# 小文件内部排序
def sort_json_file(file_path, output_path):
"""
对小文件进行排序并去重
:param file_path:
:param output_path:
:return:
"""
print(f"正在排序去重文件{file_path}")
output = ""
with open(file_path, "r") as input_file:
lines = input_file.readlines()
lines.sort(key=hash_line)
lines = list(set(lines))
with open(output_path, "w") as output_file:
output_file.writelines(lines)
output = os.path.abspath(output_file.name)
print(f"{file_path}排序去重完成!")
return output
def split_array(arr, n):
"""
将数组拆分为多个数组
:param arr:
:param n:
:return:
"""
length = len(arr)
return [arr[i:i + n] for i in range(0, length, n)]
# 使用多线程对多个文件进行排序
def sorted_files(files):
"""
对多个文件进行内部排序
:param files:
"""
for input_file in files:
sort_json_file(input_file, input_file)
def mutil_thread_sort_file(files, n_thread=10):
"""
使用多线程对多个文件进行内部排序
:param files: 需要排序的文件列表
:param n_thread: 使用的线程数
"""
if len(files) < n_thread:
# 文件数小于线程数,将线程数改为文件数
n_thread = len(files)
file_lists = split_array(files, n_thread)
threads = []
for file_list in file_lists:
t = threading.Thread(target=sorted_files, args=(file_list,))
t.start()
threads.append(t)
for t in threads:
t.join()
# 文件归并排序
def hash_line(line):
return int(hashlib.sha256(line.encode()).hexdigest(), 16)
def merge_sorted_files(file_list, output_file):
# 打开每个文件
readers = [open(file, 'r') for file in file_list]
# 使用堆来维护当前打开的文件的当前行
heap = [(hash_line(reader.readline()), reader, reader.readline()) for i, reader in enumerate(readers) if reader]
heapq.heapify(heap)
with open(output_file, "w") as output:
while heap:
val, reader, line = heapq.heappop(heap)
output.write(line)
# 试图获取下一行
try:
next_line = reader.readline()
if next_line:
heapq.heappush(heap, (hash_line(next_line), reader, next_line))
except Exception as e:
# 如果到达了文件末尾,则忽略该文件
print(f"出现错误{str(e)}")
pass
for f in readers:
f.close()
# 文件归并
def merge_files(file_list, output_file):
"""
:param file_list: 合并的文件列表
:param output_file: 输出的文件
"""
with open(output_file, "w") as output:
for file in file_list:
with open(file, 'r') as f:
for line in f:
output.write(line)
def deduplicate_json_file(file_path, output_path, n_file):
"""
大文件按行hash值去重
#拆分大文件
#遍历大文件的每行,计算hash值, 将这行按照hash_code%n存到第n个文件(保证每个文件之间没有重复行)
#对切分的小文件进行排序然后去重
#归并排序合并所有小文件
#删除临时文件
:param file_path: 大文件路径
:param output_path: 输出路径
:param n_file: 拆分文件数量,拆分后的文件需要能放进内存
"""
temp_dir = "temp_dir/"
os.makedirs(temp_dir, exist_ok=True)
# 对文件进行切分
file_paths = split_json_file(file_path, temp_dir, n_file)
# 对切分的小文件进行排序然后去重
# files = []
# for file in file_paths:
# files.append(sorted_files(file,file))
mutil_thread_sort_file(file_paths)
# 归并排序合并所有小文件
merge_files(file_paths, output_path)
# 删除临时文件
for file in file_paths:
if os.path.exists(file):
os.remove(file)
if __name__ == '__main__':
deduplicate_json_file('E:\desktop\wiki_data\entity\\rel.json', 'temp_dir/rel.json', 1000)