#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import re
import sys
import argparse
import xlwt
from collections import defaultdict
# 分区名称映射表(前缀 → 友好名称)
PARTITION_NAME_MAP = {
'02_': 'system',
'03_': 'vendor',
'04_': 'product',
'05_': 'odm',
'06_': 'my_product',
'07_': 'my_engineering',
'08_': 'my_stock',
'09_': 'my_heytap',
'10_': 'my_company',
'11_': 'my_carrier',
'12_': 'my_region',
'13_': 'my_preload',
'14_': 'data',
'15_': 'my_bigball',
'16_': 'my_manifest',
'17_system_dlkm': 'system_dlkm',
'17_vendor_dlkm': 'vendor_dlkm',
'17_cache': 'cache',
'18_': 'system_ext'
}
def parse_du_file(file_path):
"""解析du命令输出文件并转换为MB"""
data = {}
try:
with open(file_path, 'r') as f:
for line in f:
if 'Permission denied' in line or 'No such file' in line or not line.strip():
continue
match = re.match(r'(\d+\.?\d*)\s*([KMG]?)[Bb]?\s+(.*)', line.strip())
if match:
size, unit, path = match.groups()
size = float(size)
# 单位转换到MB
if unit == 'K': size = size / 1024.0
elif unit == '': size = size / (1024 * 1024.0)
elif unit == 'M': pass
elif unit == 'G': size = size * 1024.0
data[path] = round(size, 4)
except IOError as e:
print("警告: 无法读取文件 {}: {}".format(file_path, str(e)))
return data
def extract_file_prefix(filename):
"""提取文件前缀"""
if filename.startswith('17_'):
return filename.replace('.txt', '')
match = re.match(r'^(\d+_)', filename)
return match.group(1) if match else "other_"
def is_main_partition_file(filename, prefix):
"""检查是否为主分区文件"""
if prefix.startswith('17_'):
return True
expected_name = prefix + PARTITION_NAME_MAP[prefix] + ".txt"
return filename == expected_name
def parse_lpdump_file(file_path):
"""解析 lpdump 输出文件,获取各分区大小和super物理分区大小"""
sector_size = 512 # 每个扇区大小为 512 字节
partition_sizes = {}
try:
with open(file_path, 'r') as f:
lines = f.readlines()
in_super_layout = False
super_size = 0.0 # 初始化super物理分区大小
for line in lines:
if "Super partition layout:" in line:
in_super_layout = True
continue
if not in_super_layout or not line.strip():
continue
# 使用正则匹配 Size 行
match = re.search(r'Size:\s*(\d+)\s+bytes', line, re.IGNORECASE)
if match:
try:
size_bytes = int(match.group(1))
super_size = size_bytes / float(1024 * 1024) # 转换为 MB
print ("super 分区大小:%.2f MB" % super_size)
except Exception as e:
print ("无法解析 super 分区大小:%s" % str(e))
continue
match = re.search(r'super:\s*(\d+)\s*.. (\d+):\s*([a-zA-Z0-9_]+)_a\s*\((\d+)\s+sectors\)', line)
if match:
name = match.group(3) + '_a'
size_sectors = int(match.group(4)) # 直接使用括号内的数值
size_mb = round(size_sectors * sector_size / (1024 * 1024), 4)
partition_sizes[name] = size_mb
# 特别提取 super 分区的大小
if name == 'super_a':
super_size = size_mb
except Exception as e:
print("解析 lpdump 文件出错: %s" % e)
return partition_sizes, super_size
def generate_dual_report(folder1, folder2, output_xlsx, is_os_project, upgrade_generations, maintenance_generations, reserve_per_generation, commercial_max_usage):
"""生成双机对比报告"""
folder1_name = os.path.basename(os.path.normpath(folder1))
folder2_name = os.path.basename(os.path.normpath(folder2))
for folder in [folder1, folder2]:
if not os.path.exists(folder):
print("错误: 目录不存在 - %s" % folder)
return "目录 %s 不存在,请检查路径" % folder
if not os.path.isdir(folder):
print("错误: 路径不是目录 - %s" % folder)
return "%s 不是有效目录" % folder
# 初始化数据结构
machine1_main_data = {}
machine2_main_data = {}
machine1_all_files = defaultdict(dict)
machine2_all_files = defaultdict(dict)
# 新增:解析 lpdump 文件
machine1_lpdump, machine1_super_size = parse_lpdump_file(os.path.join(folder1, '18_lpdump.txt'))
machine2_lpdump, machine2_super_size = parse_lpdump_file(os.path.join(folder2, '18_lpdump.txt'))
# 收集数据
for folder_path, main_dict, all_dict in [
(folder1, machine1_main_data, machine1_all_files),
(folder2, machine2_main_data, machine2_all_files)
]:
print("处理目录: %s" % folder_path)
try:
for filename in os.listdir(folder_path):
if not filename.endswith('.txt'):
continue
prefix = extract_file_prefix(filename)
if prefix == '01_' or prefix not in PARTITION_NAME_MAP:
continue
file_path = os.path.join(folder_path, filename)
partition_name = PARTITION_NAME_MAP[prefix]
file_data = parse_du_file(file_path)
all_dict[filename] = file_data
if is_main_partition_file(filename, prefix):
print("解析主分区文件: %s" % file_path)
main_dict[prefix] = file_data
except OSError as e:
print("目录访问错误: %s" % str(e))
return "无法访问目录 %s: %s" % (folder_path, str(e))
# 创建Excel工作簿
try:
wb = xlwt.Workbook(encoding='utf-8')
# ====== 定义样式变量 ======
header_style = xlwt.easyxf('font: bold on')
title_style = xlwt.easyxf('font: bold on, height 280; align: wrap on, vert centre')
normal_style = xlwt.easyxf()
added_style = xlwt.easyxf('pattern: pattern solid, fore_colour light_green;')
removed_style = xlwt.easyxf('pattern: pattern solid, fore_colour rose;')
summary_style = xlwt.easyxf('font: bold on, color blue;')
wrap_style = xlwt.easyxf('align: wrap on, vert centre') # 备注列样式
# ====== 创建总览Sheet页 ======
ws_overview = wb.add_sheet('总览')
print("创建总览Sheet页(仅主分区文件)")
current_row = 0
# 写入总览标题
ws_overview.write_merge(
current_row, current_row, 0, 5,
"存储使用总览(仅主分区文件)",
title_style
)
current_row += 1
# 写入文件夹名称
ws_overview.write(current_row, 1, folder1_name, header_style)
ws_overview.write(current_row, 2, folder2_name, header_style)
current_row += 1
# 写入表头(增加备注列)
headers = ['分区', '总大小(MB)', '总大小(MB)', '差值(MB)', '标记', '备注(增大TOP3)']
for col, header in enumerate(headers):
ws_overview.write(current_row, col, header, header_style)
current_row += 1
# 存储各分区汇总数据
overview_data = []
total_machine1 = 0.0
total_machine2 = 0.0
# 按分区顺序处理数据
for prefix in sorted(PARTITION_NAME_MAP.keys()):
partition_name = PARTITION_NAME_MAP[prefix]
# 跳过data分区
if partition_name == 'data':
continue
# 使用 lpdump 提供的大小替代原来的 du 总和
key = partition_name + '_a'
size1 = machine1_lpdump.get(key, 0.0)
size2 = machine2_lpdump.get(key, 0.0)
diff = round(size1 - size2, 4)
# 更新总计
total_machine1 += size1
total_machine2 += size2
# 确定标记样式
if diff > 0:
mark = "增加"
style = added_style
elif diff < 0:
mark = "减少"
style = removed_style
else:
mark = "无变化"
style = normal_style
# 计算分区中增大的大于5MB的路径
top_notes = []
if diff > 0: # 只在分区增大时计算TOP路径
path_diffs = []
all_paths = set(machine1_main_data.get(prefix, {}).keys()) | set(machine2_main_data.get(prefix, {}).keys())
for path in all_paths:
size1_path = machine1_main_data.get(prefix, {}).get(path, 0.0)
size2_path = machine2_main_data.get(prefix, {}).get(path, 0.0)
path_diff = size1_path - size2_path
if path_diff > 5: # 只记录增大小于5MB的路径
path_diffs.append((path, path_diff))
# 按增大值降序排序
path_diffs.sort(key=lambda x: x[1], reverse=True)
for i, (path, diff_val) in enumerate(path_diffs):
# 截断过长的路径名
if len(path) > 50:
path = "..." + path[-47:]
top_notes.append("%d. %s: +%.2fMB" % (i+1, path, diff_val))
# 保存分区数据
overview_data.append({
'name': partition_name,
'machine1': size1,
'machine2': size2,
'diff': diff,
'style': style,
'mark': mark,
'notes': "\n".join(top_notes) if top_notes else "无显著增大路径"
})
# 写入行数据到总览页(增加备注列)
print("写入总览页: %s, 测试机大小: %.2f MB, 对比机大小: %.2f MB" % (partition_name, size1, size2)) # 调试输出
ws_overview.write(current_row, 0, partition_name, style)
ws_overview.write(current_row, 1, size1, style)
ws_overview.write(current_row, 2, size2, style)
ws_overview.write(current_row, 3, diff, style)
ws_overview.write(current_row, 4, mark, style)
ws_overview.write(current_row, 5, overview_data[-1]['notes'], wrap_style) # 使用已定义的wrap_style
current_row += 1
# 添加空行
current_row += 1
# 写入总计行
total_diff = total_machine1 - total_machine2
if total_diff > 0:
total_mark = "总增加"
total_style = added_style
elif total_diff < 0:
total_mark = "总减少"
total_style = removed_style
else:
total_mark = "无变化"
total_style = normal_style
ws_overview.write(current_row, 0, "总计", header_style)
ws_overview.write(current_row, 1, total_machine1, header_style)
ws_overview.write(current_row, 2, total_machine2, header_style)
ws_overview.write(current_row, 3, total_diff, header_style)
ws_overview.write(current_row, 4, total_mark, header_style)
ws_overview.write(current_row, 5, "", header_style) # 备注列留空
# 判断是否满足预留条件
current_row += 1
if is_os_project:
reserved_space = total_machine1 + upgrade_generations * reserve_per_generation + maintenance_generations * 200 - commercial_max_usage*1024
else:
reserved_space = total_machine1 + upgrade_generations * reserve_per_generation + maintenance_generations * 200
space_difference = machine1_super_size - reserved_space
if reserved_space > machine1_super_size:
status = "不满足"
status_style = removed_style
print("预留空间不足!当前占用: %.2fMB | 预留空间: %.2fMB | Super大小: %.2fMB | 还差 %.2fMB" %
(total_machine1, reserved_space, machine1_super_size, abs(space_difference)))
else:
status = "满足"
status_style = added_style
print("预留空间充足!当前占用: %.2fMB | 预留空间: %.2fMB | Super大小: %.2fMB | 剩余 %.2fMB" %
(total_machine1, reserved_space, machine1_super_size, space_difference))
# 写入判断结果
ws_overview.write(current_row, 0, "预留判断", header_style)
ws_overview.write(current_row, 1, "", normal_style)
ws_overview.write(current_row, 2, "", normal_style)
ws_overview.write(current_row, 3, "", normal_style)
ws_overview.write(current_row, 4, status, status_style)
ws_overview.write(current_row, 5, "当前占用: %.2fMB | 预留空间: %.2fMB | Super大小: %.2fMB" % (total_machine1, reserved_space, machine1_super_size), wrap_style)
# 设置备注列宽度(100字符)
ws_overview.col(5).width = 256 * 100
# ====== 为每个文件创建单独的Sheet页(保持不变) ======
# 获取所有唯一的文件名(两个文件夹的并集)
all_filenames = sorted(set(machine1_all_files.keys()) | set(machine2_all_files.keys()))
for filename in all_filenames:
# 提取文件前缀
prefix = extract_file_prefix(filename)
# 跳过无效前缀
if prefix not in PARTITION_NAME_MAP:
continue
# 获取分区名称
partition_name = PARTITION_NAME_MAP[prefix]
# 创建Sheet页名称(文件名不带扩展名)
sheet_name = filename.replace('.txt', '')
if len(sheet_name) > 31: # Excel sheet名称长度限制
sheet_name = sheet_name[:31]
# 创建Sheet页
ws = wb.add_sheet(sheet_name)
print("创建文件Sheet页: %s" % sheet_name)
# 当前行指针
current_row = 0
# 写入分区标题
title = "分区: %s - 文件: %s" % (partition_name, filename)
ws.write_merge(
current_row, current_row, 0, 5,
title,
title_style
)
current_row += 1
# 写入文件夹名称
ws.write_merge(current_row, current_row, 0, 1, folder1_name, header_style)
ws.write_merge(current_row, current_row, 2, 3, folder2_name, header_style)
ws.write(current_row, 4, "差异(M)", header_style)
ws.write(current_row, 5, "标记", header_style)
current_row += 1
# 写入表头
headers = ['路径', '大小(M)', '路径', '大小(M)', '差异(M)', '标记']
for col, header in enumerate(headers):
ws.write(current_row, col, header, header_style)
current_row += 1
# 获取文件数据
data1 = machine1_all_files.get(filename, {})
data2 = machine2_all_files.get(filename, {})
# 获取所有路径(合并两个文件夹的路径)
all_paths = sorted(set(data1.keys()) | set(data2.keys()))
# 初始化变化统计数据
total_increase = 0.0 # 增大总和
total_decrease = 0.0 # 减小总和
total_added = 0.0 # 新增文件总和
total_removed = 0.0 # 去除文件总和
# 写入数据行
for path in all_paths:
size1 = data1.get(path, 0.0)
size2 = data2.get(path, 0.0)
# 计算差值
diff = size1 - size2
# 确定标记和样式
if size1 == 0 and size2 > 0:
mark = "除去"
cell_style = removed_style
total_removed += size2
elif size1 > 0 and size2 == 0:
mark = "新增"
cell_style = added_style
total_added += size1
else:
if diff > 0:
mark = "增大"
cell_style = added_style
total_increase += diff
elif diff < 0:
mark = "减小"
cell_style = removed_style
total_decrease += abs(diff)
else:
mark = "相同"
cell_style = normal_style
# 写入行数据
# folder1列
if size1 > 0:
ws.write(current_row, 0, path, cell_style)
ws.write(current_row, 1, size1, cell_style)
else:
ws.write(current_row, 0, "", cell_style)
ws.write(current_row, 1, "", cell_style)
# folder2列
if size2 > 0:
ws.write(current_row, 2, path, cell_style)
ws.write(current_row, 3, size2, cell_style)
else:
ws.write(current_row, 2, "", cell_style)
ws.write(current_row, 3, "", cell_style)
# 差异和标记列
ws.write(current_row, 4, diff, cell_style)
ws.write(current_row, 5, mark, cell_style)
current_row += 1
# 添加文件汇总行
file_total1 = sum(data1.values())
file_total2 = sum(data2.values())
file_diff = file_total1 - file_total2
# 写入汇总行
ws.write(current_row, 0, "文件汇总", header_style)
ws.write(current_row, 1, file_total1, header_style)
ws.write(current_row, 2, "", header_style)
ws.write(current_row, 3, file_total2, header_style)
ws.write(current_row, 4, file_diff, header_style)
ws.write(current_row, 5, "", header_style)
current_row += 1
# 添加变化分类统计行
message = (
u"%s路径下: "
u"减小%.2fM "
u"增大%.2fM "
u"新增文件%.2fM "
u"减少文件%.2fM"
) % (
partition_name,
total_decrease,
total_increase,
total_added,
total_removed
)
ws.write_merge(
current_row, current_row, 0, 5,
message,
summary_style
)
# 保存文件
wb.save(output_xlsx)
return "对比报告已成功生成: %s" % output_xlsx
except Exception as e:
import traceback
traceback.print_exc()
return "生成Excel文件时出错: %s" % str(e)
# 单机拆解模式保持不变
def generate_single_report(folder, output_xlsx):
"""单机拆解模式(保持不变)"""
# ...(原有单机拆解模式实现)...
if __name__ == "__main__":
# 创建参数解析器
parser = argparse.ArgumentParser(description='存储空间分析工具')
subparsers = parser.add_subparsers(dest='mode', help='运行模式')
# 双机对比模式
dual_parser = subparsers.add_parser('dual', help='双机对比模式')
dual_parser.add_argument('folder1', help='第一个文件夹路径')
dual_parser.add_argument('folder2', help='第二个文件夹路径')
dual_parser.add_argument('output', help='输出Excel文件路径')
dual_parser.add_argument('--is-os-project', action='store_true', default=False, help='是否是OS项目')
dual_parser.add_argument('--upgrade-generations', type=int, default=0, help='升级代数')
dual_parser.add_argument('--maintenance-generations', type=int, default=0, help='维护代数')
dual_parser.add_argument('--reserve-per-generation', type=float, default=0, help='每代预留大小 (单位MB)')
dual_parser.add_argument('--commercial-max-usage', type=float, default=0, help='商业化历史最大占用 (单位MB)')
# 单机拆解模式
single_parser = subparsers.add_parser('single', help='单机拆解模式')
single_parser.add_argument('folder', help='待分析文件夹路径')
single_parser.add_argument('output', help='输出Excel文件路径')
# 解析参数
args = parser.parse_args()
if args.mode == 'dual':
print("运行双机对比模式...")
result = generate_dual_report(
args.folder1,
args.folder2,
args.output,
is_os_project=args.is_os_project,
upgrade_generations=args.upgrade_generations,
maintenance_generations=args.maintenance_generations,
reserve_per_generation=args.reserve_per_generation,
commercial_max_usage=args.commercial_max_usage
)
elif args.mode == 'single':
print("运行单机拆解模式...")
result = generate_single_report(args.folder, args.output)
else:
result = "错误:请选择 'dual' 或 'single' 模式"
print(result)这个是一个存储解析脚本,双机拆解模式的总览页是通过解析18_lpdump.txt文件中数据,现在将单机拆解模式同步,并且判断是否满足预留的功能也同步到单机拆解模式
最新发布