You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Python生成器未降低内存消耗,求实现排查及优化方案

问题描述

我有三个json.gz文件,分别存储按不同ID分组的餐厅、菜单和匹配数据。需读取所有文件并按ID生成包含对应三类数据的新JSON文件,但运行代码的虚拟机存在内存限制。最初我将三个文件全部加载为JSON对象后用普通for循环处理,导致内存消耗过高。意识到仅需单个ID对应的数据来生成最终文件,不应同时加载所有ID的数据,因此采用Python生成器方案并编写了如下代码:

from json import loads

def load_data_set(string_restaurants_data: str,
                  string_menus_data: str,
                  string_matchings_data: str,): 


  menus_data = loads(string_menus_data)   
  matchings_data = loads(string_matchings_data)   
  restaurants_data = loads(string_restaurants_data)
  
  for id, menu_data in menus_data.items():
    yield id, restaurants_data[id], menu_data, matchings_data[id] if id in matchings_data else "{}"



def main():
    '''
    some code to read the json.gz files. The data is stored in these three string variables: string_restaurants_data, string_menus_data, string_matchings_data
    '''

    restaurants_data_set = load_data_set(string_restaurants_data,
                                             string_menus_data,
                                             string_matchings_data)
    
    size_generator = sys.getsizeof(restaurants_data_set)

    del string_menus_data
    del string_restaurants_data
    del string_matchings_data
    gc.collect()

    list_result = {}
    for restaurant in restaurants_data_set:
      result_data = aggregate_menu_data(restaurant[0],
                                        dumps(restaurant[2]),
                                        dumps(restaurant[1]),
                                        dumps(restaurant[3]),
                                        string_parameters,
                                        eval(debug))
    

      list_result.update(result_data)
    
    data['result'] = dumps(list_result)

但对比无生成器的版本,虚拟机任务管理器显示内存消耗未明显降低。现咨询:该Python生成器实现是否正确?有无更优方案可按需加载所需数据且不影响处理速度?Python版本为3.11。


解答

一、当前生成器实现的问题

你的生成器实现不正确,核心问题在于:

  • 在load_data_set函数内部,你已经把三个JSON字符串全部加载成了完整的字典对象(menus_data、matchings_data、restaurants_data),这三个大字典已经完全占用了内存,生成器只是逐个返回其中的元素,并没有减少内存占用——生成器本身占用内存极小,但背后的三个完整数据集已经把内存占满了,所以和无生成器版本内存消耗差异不大。
  • 你在生成器创建后才删除原始字符串并触发GC,但三个大字典已经存在于内存中,这部分内存并没有被释放。

二、优化方案

要真正实现按需加载、降低内存占用,需要从「读取文件」和「解析JSON」两个环节入手,避免一次性加载整个文件内容或解析整个JSON结构:

1. 流式读取并解析JSON(推荐)

因为你的JSON是按ID分组的键值对结构,可以使用ijson库(需先安装:pip install ijson)来流式解析JSON,逐个读取每个ID对应的条目,而不是一次性加载整个JSON到内存:

import ijson
import gzip
from json import dumps

def stream_id_entries(file_path):
    """流式读取gzip压缩的JSON文件,逐个返回(ID, 对应数据)"""
    with gzip.open(file_path, 'rb') as f:
        # 解析JSON对象的每个键值对
        parser = ijson.kvitems(f, '')
        for id, data in parser:
            yield id, data

def main():
    # 三个文件的流式生成器
    menus_stream = stream_id_entries('menus.json.gz')
    # 匹配数据量通常较小,可预加载到字典按需查找
    matchings_dict = dict(stream_id_entries('matchings.json.gz'))

    list_result = {}
    # 遍历菜单的每个ID,按需获取对应餐厅数据
    for menu_id, menu_data in menus_stream:
        # 重新打开餐厅流,查找当前ID对应的数据(生成器只能遍历一次)
        restaurant_data = {}
        with gzip.open('restaurants.json.gz', 'rb') as f:
            restaurant_parser = ijson.kvitems(f, '')
            for id, data in restaurant_parser:
                if id == menu_id:
                    restaurant_data = data
                    break

        matching_data = matchings_dict.get(menu_id, {})
        # 调用聚合函数(建议直接传Python对象,避免重复序列化)
        result_data = aggregate_menu_data(
            menu_id,
            menu_data,
            restaurant_data,
            matching_data,
            string_parameters,
            bool(eval(debug))  # 提前解析debug参数,避免重复eval
        )
        list_result.update(result_data)

    # 若最终结果过大,可改为逐个写入文件,避免占用过多内存
    data['result'] = dumps(list_result)

2. 进阶优化:同步遍历排序后的流

如果餐厅数据量极大,每次重新打开流查找效率太低,可以先提取所有ID并排序,再同步遍历三个流:

import ijson
import gzip
from json import dumps

def get_sorted_ids(file_path):
    """提取文件中所有ID并排序"""
    ids = []
    with gzip.open(file_path, 'rb') as f:
        parser = ijson.keys(f, '')
        for id in parser:
            ids.append(id)
    return sorted(ids)

def stream_sorted_entries(file_path, sorted_ids):
    """按指定ID顺序流式返回对应数据"""
    with gzip.open(file_path, 'rb') as f:
        parser = ijson.kvitems(f, '')
        id_iter = iter(sorted_ids)
        current_target_id = next(id_iter, None)
        for id, data in parser:
            while current_target_id is not None and id > current_target_id:
                # 若当前ID大于目标ID,说明目标ID不存在,返回空数据
                yield current_target_id, {}
                current_target_id = next(id_iter, None)
            if id == current_target_id:
                yield id, data
                current_target_id = next(id_iter, None)
        # 处理剩余未找到的ID
        while current_target_id is not None:
            yield current_target_id, {}
            current_target_id = next(id_iter, None)

def main():
    # 获取菜单的所有ID并排序(假设菜单ID是全集)
    sorted_ids = get_sorted_ids('menus.json.gz')

    # 按排序后的ID同步遍历三个流
    restaurants_stream = stream_sorted_entries('restaurants.json.gz', sorted_ids)
    menus_stream = stream_sorted_entries('menus.json.gz', sorted_ids)
    matchings_stream = stream_sorted_entries('matchings.json.gz', sorted_ids)

    list_result = {}
    for (rid, rdata), (mid, mdata), (maid, madata) in zip(restaurants_stream, menus_stream, matchings_stream):
        # 三个流的ID应该一致
        result_data = aggregate_menu_data(
            rid,
            mdata,
            rdata,
            madata,
            string_parameters,
            bool(eval(debug))
        )
        list_result.update(result_data)

    data['result'] = dumps(list_result)

3. 额外内存优化细节

  • 避免全量存储结果:如果最终生成的JSON文件过大,不要把所有结果存在list_result字典中,而是每次生成一条结果就追加写入输出文件(注意处理JSON格式的首尾括号)。
  • 减少序列化开销:你的代码中把数据dump成字符串传给aggregate_menu_data,如果函数内部需要反序列化,会浪费性能,建议直接传递Python对象。
  • 替换eval(debug):eval存在安全风险,且重复调用会降低性能,建议提前解析为布尔值,比如debug_flag = bool(eval(debug)),再传递给函数。

三、内存占用验证方法

可以使用tracemalloc模块精准跟踪内存使用,比任务管理器更可靠:

import tracemalloc

tracemalloc.start()

# 执行你的业务代码

snapshot = tracemalloc.take_snapshot()
top_stats = snapshot.statistics('lineno')

print("[Top 10内存占用条目]")
for stat in top_stats[:10]:
    print(stat)

内容的提问来源于stack exchange,提问作者sergioMoreno

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.17 19:47:05