Python大型文件数据读取及并行高效写入MongoDB代码分享

记录最近一个大文件数据处理入库任务,需求大致是这样:

数据处理需求:

需要定期拉取一个千万行级的大型文件,文件内容是按照指定分隔符进行分割的数据表内容.

  • 每行结束按照\n换行,类似下面这种格式:
1;female;Micky;19746;
2;male;Tom;749573;
3;male;Bob;465926;
...
  • 字段名分别为:idgendernamecode

分析思路

在进行代码编写之前,先整理思路,需要考虑的问题。
整体思路倒是简单:读取文件-->将每行转换key:value字典格式-->插入MongoDB
但是这里不得不考虑两个问题,内存问题、效率问题。

  • 大文件一般不能直接读取到内存,也许你的开发机器足够好能刚好承受,但你的代码如果直接放到生产上,可能直接就读取失败或者让生产机器的内存爆掉了。。
  • 写入效率第一点考虑,在insert_one和insert_many中必然是优先选用insert_many,即一次将一个列表的dict数据插入到MongoDB,这样的效率远高于逐条遍历的insert_one。
  • 写入效率第二点考虑,需考虑将数据并发写入MongoDB,具体究竟该使用协程、多线程还是多进程的方式呢?我们后面再说。

我的整体代码在最后部分,不想看中间啰嗦部分的可直接跳到最后~

本地数据库测试效率:1千万行数据,68秒写入完成

1. 生成器方式读取大文件

自定义一个生成器函数如下,使用next()方法每次可获取一行文件数据。
每行数据需要转换为字典格式返回,以便于可直接插如MongoDB。

FILEDS = ["id", "gender", "name", "code"]
def gen_file(filepath):
    """将大型文件转化为生成器对象,每次返回一行对应的字典格式数据"""
   with open(filepath, "r", encoding="utf-8") as f:
        while True:
            line = f.readline()
            if not line:
                break
            filed_list = line.split(";")[:-1]
            doc_dict = {k: v for k, v in zip(FILEDS, filed_list)
            yield doc_dict

可使用如下方式获取指定一行数据:

# example
gen = gen_file("./example_file.dat")
# 获取一行字典数据
doc = next(gen)

2.将数据批量写入MongoDB

每次读取1000行数据,使用insert_many批量插入MongoDB,你也可以自定义一次插入多少数据量。最好不要使用insert_one一次插入一条数据,数据量大时效率非常低下。
代码示例:

def insert_mongo():
    # 连接到指定MongoDB集合
    client = MongoClient(host=MONGO_HOST, 
    port=MONGO_PORT)
    coll = client[MONGO_DB][MONGO_COLLECTION]
     # 获取大文件生成器对象
    gen =gen_file("example_file.dat")
    # 循环读取生成器,一次写入1000条文档到MongoDB,直到生成器读取完全
    while True:
        docs = []
        try:
            for i in range(1000):
                doc = next(sp_gen)
                docs.append(doc)
        except StopIteration:
            break
        finally:
            coll.insert_many(docs)

以上就做到了读取任意大小文件并相对高效的将每行数据写入到MongoDB中。不过这只是一个单线程的任务,若还想要更高效的提高写入效率,则需要设计并行读取写入的程序逻辑。

3. 如何并行将数据批量写入MongoDB?

在这一步我借助生成器做了多线程和协程写入的测试,也不再详述测试代码了,得出的结果是,使用多线程或协程在批量写入MongoDB时,并未有太多效率的提升,我猜想原因有二:一是Python的多线程本来由于GIL锁的存在是无法做到真正的并行的,二是MongoDB是NoSQL数据库本身写入的效率就很高了,在非并发的情况下并不能真正明显提升效率。

因此,看来想要在此基础上成倍提升写入效率只能采用并行,即多进程的方式才行。但是使用多进程有一点必须要注意的是,多进程通常是无法共享资源的,一个生成器对象无法互相读取。
解决思路

  • 按行拆分原始大文件,将原本的大文件拆分为多个小文件
  • 再将每个小文件转换为生成器对象
  • 使用进程池读取小文件生成器对象,并行批量写入MonoDB

4. 拆分小文件

以下我借助我的部分代码提供一个大致逻辑,但不再每个函数进行详细代码说明:

# 将大文件按指定行数切分为多个小文件,默认按照100w行进行拆分
filepath = "./test_dat.DAT"
splitpath = "./split_directory"
def split_file(self, sp_nm=1000000):
    # 以原文件名为基础名,拆分小文件以为_1、_2模式命名,
    base_path = filepath.replace(".", "_{}.")
    # 读取大文件为逐行返回的生成器对象
    gen = self.__gen_file()
    # 创建拆分小文件存放的目录
    self.__mk_sp_dir()
    flag = 1
    # 循环内每次读取生成器对象最高100w行写入小文件,直到完全读取触发StopIteration异常退出循环
    while True:
        split_name = base_path.format(str(flag))
        try:
            with open("%s/%s" % (splitpath, split_name), "w", encoding="utf8") as f:
                for i in range(sp_nm):
                    line = next(gen)
                    f.write(line)
            flag += 1
        except StopIteration:
            break
    print("Finished! Split into %s files in total." % flag)


# 获取拆分文件路径列表
def sp_file_lists(self):
    """获取生成器列表"""
    abspath = os.path.abspath(splitpath) + "/"
    sp_filepath_list = list(map(lambda x: abspath+x, os.listdir(splitpath)))
    return sp_filepath_list

5. 多进程读取拆分文件写入MongoDB

代码示例:

from multiprocessing import Pool

sp_filepath_list = handler.sp_file_lists()
p = Pool()
for file in sp_filepath_list:
    p.apply_async(insert_mongo, (file,))
print("----start----")
p.close()
p.join()
print("----finished----")

最后:完整代码分享

主逻辑部分完成代码分享在下面,希望对需要的人有帮助,代码包含生成器函数、文件拆分、批量插入MongoDB、文件清理等方法。使用多进程方式,写入效率按进程多少几乎成倍增加。

import os
import time
from multiprocessing import Pool
# 可配置的进程池大小、数据字段列表、小文件拆分行数标准
from config import PROCESS_POOL, INSERT_MANY_COUNT, COLUMNS, SPLIT_LINES
# 自定义的MongoDB连接类
from mongo_client import DBManager


class BigFileToMongoDB(object):
    # 初始化指定大文件路径及拆分结果目录,默认拆分至当前路径下的split_directory文件夹下
    def __init__(self, filepath, splitpath="./split_directory"):
        self.filepath = filepath
        self.splitpath = splitpath

    # 将大型文件转换为生成器,每次返回一行数据
    def __gen_file(self):
        """将大型文件转化为生成器对象,每次返回一行"""
        with open(self.filepath, "r", encoding="utf-8") as f:
            while True:
                line = f.readline()
                if not line:
                    break
                yield line

    def __mk_sp_dir(self):
        """创建拆分文件存放目录,split_directory"""
        if not os.path.exists(self.splitpath):
            os.mkdir(self.splitpath)

    # 将大文件按指定行数切分为多个小文件
    def split_file(self, sp_nm=1000000):
        """读取大文件生成器,默认每100w行拆分一个新的文件写入"""
        if SPLIT_LINES is not None:
            sp_nm = SPLIT_LINES
        base_path = self.filepath.replace(".", "_{}.")
        gen = self.__gen_file()
        self.__mk_sp_dir()
        flag = 1
        while True:
            split_name = base_path.format(str(flag))
            try:
                with open("%s/%s" % (self.splitpath, split_name), "w", encoding="utf8") as f:
                    for i in range(sp_nm):
                        line = next(gen)
                        f.write(line)
                flag += 1
            except StopIteration:
                break
        print("Finished! Split into %s files in total." % flag)

    @staticmethod
    def spfile_generator(sp_filepath):
        """将拆分后的文件转换为字典生成器
        针对不同格式文件需要不同的处理函数
        """
        with open(sp_filepath, "r", encoding="utf-8") as f:
            while True:
                line = f.readline()
                line_list = line.split(";")[:-1]
                dic = {i: j for i, j in zip(COLUMNS, line_list)}
                if not line:
                    break
                yield dic

    @property
    def sp_file_lists(self):
        """获取所有拆分文件绝对路径列表"""
        abspath = os.path.abspath(self.splitpath) + "/"
        sp_filepath_list = list(map(lambda x: abspath+x, os.listdir(self.splitpath)))
        return sp_filepath_list

    @staticmethod
    def insert_mongo(filepath):
        sp_gen = BigFileToMongoDB.spfile_generator(filepath)
        coll = DBManager()
        coll.connect()
        while True:
            docs = []
            try:
                for i in range(INSERT_MANY_COUNT):
                    doc = next(sp_gen)
                    docs.append(doc)
                coll.insert_many(docs)
            except StopIteration:
                break
        print("生成器元素已写入MongoDB")

    def clean_split_dir(self):
        for split_file in self.sp_file_lists:
            os.remove(split_file)
        print("清理完成")


def run_insert_pool(file_path):
    start = time.time()
    handler = BigFileToMongoDB(file_path)
    print("开始切分源文件")
    handler.split_file()
    sp_filepath_list = handler.sp_file_lists
    p = Pool(PROCESS_POOL)
    for file in sp_filepath_list:
        p.apply_async(handler.insert_mongo, (file,))

    print("----start----")
    p.close()
    p.join()
    end = time.time()
    print("Finish to MongoDB spend:{}s".format(end - start))
    handler.clean_split_dir()


if __name__ == '__main__':
    file_path = "test_dat.DAT"
    run_insert_pool(file_path)

以上。
所有文件的读取都使用生成器的方式,可以避免内存不足的问题。
本地数据库测试效率:1千万行数据,第二次76秒写入完成
当前代码只是demo,有很多不规范中英文混合打印请见谅,。

屏幕快照 2020-02-03

你可能感兴趣的:(Python大型文件数据读取及并行高效写入MongoDB代码分享)