python对超大JSON大文件的按行去重,利用每行的hash值对文件去重。

最近在做一个文件的按行去重任务,要求是对超大的json文件按行去重。
由于文件的体积过大(GB级别),因此不可能全部放进内存中进行去重,只能先分成许多小文件然后对多个小文件进行排序去重,最后多个小文件合并成一个大的文件。
最终,以较小的内存完成任务,较高的速度完成了任务。

具体实现思路如下:

  1. 按行遍历需要去重的大文件(GB级别),计算每一行的hash值,根据 i = (hash值)%n 将该行写入 part_i.json,这样做的目的是使得各个文件中没有重复的行,i = (hash值)%n不一样,hash值肯定不一样。
  2. 将小文件读入内存中,使用以下代码进行排序去重,我这里使用多线程来加速排序去重.
  3. 最后,将去重后的小文件合并合并到一个文件中,因为切分大文件时按行计算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)


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值