【注意】最后更新于 December 10, 2024,文中内容可能已过时,请谨慎使用。
摘要:本文介绍了一个生产环境中的实际案例——如何使用 Python 将车电测试日志自动转换为 EDA 系统所需的 XML 格式。从需求分析、架构设计到 20+ 个 Bug 修复,完整记录了一个自动化工具从 0 到 1 的开发过程。
📋 背景
在车电测试产线中,测试设备生成的日志格式与 EDA(Engineering Data Analysis)系统要求的 XML 格式不兼容。每天产生大量测试日志,人工转换效率低且容易出错。
核心需求:
- 将测试设备生成的
.log 文件自动转换为 EDA 系统指定的 XML 格式
- 支持批量处理(多进程)
- 提取关键测试数据:测试结果、序列号、测试时间、错误信息、测量数据等
- 处理各种边界情况和异常数据
🏗️ 系统架构
整体流程
1
2
3
4
5
6
|
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ 原始 Log │ -> │ 解析转换 │ -> │ XML 输出 │
│ (测试设备) │ │ (Python) │ │ (EDA 系统) │
└─────────────┘ └──────────────┘ └──────────────┘
↓ ↓ ↓
产线测试 正则提取 + 逻辑处理 数据入库分析
|
实施步骤
- 清空临时文件夹 - 确保处理环境干净
- 筛选前一天日志 - 多进程并行处理
- Log → XML 转换 - 核心解析逻辑
- 复制到 EDA 目录 - 供下游系统使用
🔧 核心实现
1. 项目结构
1
2
3
4
|
log_to_xml/
├── py_test_modify_V03.py # 核心转换脚本
├── 实现方法.md # 实施文档
└── README.md # 项目说明
|
2. 关键功能模块
2.1 日志解析(正则表达式)
1
2
3
4
5
6
7
8
9
10
11
|
# 提取测试状态
status_pattern = r"runner\s+-\s+INFO\s+-\s+Status:\s+(PASS|FAIL)"
# 提取序列号
serial_number_pattern = r"runner\s+-\s+INFO\s+-\s+Serial\s+Number:\s+(\S+)"
# 提取测试数据
test_data_pattern = r"runner\s+-\s+INFO\s+-\s+Test\s+(\d+)\s+-\s+([\w\-_.]+)\s+\(.*?\)\s+\.\.\.\s+(pass|fail)\s+\(([\dhms]+)\)"
# 提取测量数据
measurement_pattern = r"runner\s+-\s+INFO\s+-\s+teststep:(\S+)\s+testname:(.*?)\s+value:(\S+)\s+unit:(\S+)\s+judgetype:(\S+)\s+lowlimit:(\S+)\s+uplimit:(\S+)\s+datatype:(\S+)"
|
难点: 测试日志格式不统一,需要多个正则模式适配不同版本的日志输出。
2.2 型号识别
1
2
3
4
5
6
7
8
9
10
11
12
|
def determine_model_name(region_str):
"""根据区域字符串确定型号名称"""
if 'nperear' in region_str:
return 'NPE_REAR'
elif 'npefront' in region_str:
return 'NPE_FRONT'
elif 'niorear' in region_str or 'fct_rearfct' in region_str:
return 'NPD_REAR'
elif 'niofront' in region_str or 'fct_frontfct' in region_str:
return 'NPD_FRONT'
else:
return 'NA'
|
业务逻辑: 从日志文件名中提取区域信息,映射到标准型号名称。
2.3 XML 特殊字符转义
1
2
3
4
5
6
7
8
9
10
|
def escape_xml_special_chars(s):
"""转义 XML 特殊字符"""
special_chars = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
}
return ''.join(special_chars.get(char, char) for char in s)
|
重要性: 测试名称中可能包含 &、<、> 等字符,必须转义否则 XML 解析失败。
2.4 时间格式转换
1
2
3
4
5
6
|
def convert_timestamp(timestamp_str):
"""转换时间戳为带时区的 ISO 格式"""
dt = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S,%f")
tz = timezone(timedelta(hours=8)) # 北京时间
dt_with_tz = dt.replace(tzinfo=tz)
return dt_with_tz.isoformat(timespec='milliseconds')
|
要求: EDA 系统需要带时区的时间格式 2024-11-05T03:57:12.217+08:00。
🐛 Bug 修复全记录
这是本项目最有价值的部分——20+ 个真实 Bug 的发现和修复过程。
Bug 1: 测试名称包含冒号导致解析失败
现象: MeasurementData Name=" EXT_MP_PMIC_AI_UG5V8 adc feedback at 6.5v"" 存在两个冒号,EDA 系统无法正常解析。
原因: 测试名称中的冒号被误认为是字段分隔符。
修复方案:
1
2
3
4
5
|
# ❌ 错误做法:直接替换冒号
measurement_name.replace(':', '_')
# ✅ 正确做法:转义 XML 特殊字符
escape_xml_special_chars(measurement_name)
|
教训: 不要简单替换,要理解 XML 规范,使用标准转义方法。
Bug 2: XML 文件名格式错误
现象: 生成的 XML 文件名日期和时间部分没有正确分隔。
原因: 时间格式转换逻辑有误。
修复方案:
1
2
3
4
5
6
7
8
9
|
# 示例 "20241105082202245147" 转换为 "2024-11-05_08-22-02"
def convert_time_format(time_str):
year = time_str[:4]
month = time_str[4:6]
day = time_str[6:8]
hour = time_str[8:10]
minute = time_str[10:12]
second = time_str[12:14]
return f"{year}-{month}-{day}_{hour}-{minute}-{second}"
|
Bug 3: CompType 判断逻辑不完善
现象: 部分测量数据的 CompType 字段值不正确,导致 EDA 系统统计错误。
原因: 原始逻辑只考虑了部分单位类型。
修复方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
# 完整判断逻辑
if units in ['boolean', 'Boolean', 'boolen', 'string', '']:
comp_type = 'LOG'
elif units in ['unknown', 'g', 'A', 'ma', 'v', 'Hz', '%', 's', 'digital']:
try:
result_val = float(result)
lowlimit_val = float(lowlimit) if lowlimit else None
uplimit_val = float(uplimit) if uplimit else None
if lowlimit_val is not None and uplimit_val is not None:
if lowlimit_val == uplimit_val:
comp_type = 'EQ' # 上下限相等
else:
comp_type = 'GELE' # 上下限不相等
else:
comp_type = 'NA'
except ValueError:
comp_type = 'NA'
else:
comp_type = 'NA'
|
规则总结:
boolean/string/空 → LOG
- 上下限相等 →
EQ
- 上下限不相等 →
GELE
- 其他 →
NA
Bug 4: 测试 FAIL 时 TestTime 不准确
现象: 测试失败时,TestData 中的 TestTime 统计错误。
原因: 正则表达式中 “fail” 需要大写匹配。
修复方案:
1
2
3
4
5
6
7
8
9
10
11
|
# ❌ 错误:只匹配小写
test_time_pattern = r"\.\.\.\s+(pass|fail)\s+"
# ✅ 正确:匹配大小写
test_time_pattern = r"\.\.\.\s+(pass|FAIL|skip)\s+"
# 提取测试状态
if status_keyword == "pass":
test_status = "Pass"
elif status_keyword == "FAIL": # 注意大写
test_status = "Fail"
|
Bug 5: 缺失 Error 信息
现象: FAIL 的测试没有提取到错误信息,无法进行问题分析。
原因: 原始脚本没有解析错误相关字段。
修复方案: 添加 5 个错误相关字段提取:
1
2
3
4
5
6
7
8
9
|
# 正则表达式
errorcode_pattern = r'find errorcode\..*?errorcode:(\w+)'
errormessage_pattern = r'ERROR - Failing test due to: (.*?):'
errordetails_pattern = r'ERROR - Failing test due to: (.*)'
errortestname_pattern = r'INFO - Testname: (.*)'
errorsuptestname_pattern = r'Failing test due to:(.*?)\n.*? - +INFO - (.*?)\(.*?\)'
# 组合 ErrorFullTestName
errorfulltestname = convert_suptestname_string(errorsuptestname).strip() + "_" + errortestname
|
效果: 现在可以完整追踪错误:Test ID 266 - TestVotage_EXT H Bridge J21-1-2 DMM forward
Bug 6: 缺失 DutSwVersion 信息
现象: XML 中 DutSwVersion 字段为空,无法追溯测试时的软件版本。
原因: 版本信息有两种格式(Factory/Shipping),需要分别提取。
修复方案:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
# Factory 版本
version_pattern = r"Build at\s+:\s+(\w+\s+\d+\s+\d{4},\s+\d{2}:\d{2}:\d{2})"
# Shipping 版本(需要从特定日志段提取)
def get_ship_version(log_content):
lines = log_content.splitlines()
for i, line in enumerate(lines):
if "GUI Progress: check Ship switch version" in line:
for j in range(i + 1, i + 21):
if "Alive, info" in lines[j]:
match = re.search(r'Alive, info\s*:(.*)', lines[j])
if match:
return match.group(1).strip()
return None
# 合并逻辑
if factory_version is None and shipping_version is None:
version_name = "NA"
elif factory_version and shipping_version is None:
version_name = factory_version
elif factory_version is None and shipping_version:
version_name = shipping_version
else:
version_name = factory_version + ";" + shipping_version
|
Bug 7: StationId 格式不统一
现象: retry rate 无法按治具统计,因为 StationId 格式不一致(01, 001, 1 都有)。
原因: 产线不同工位的 StationId 格式不统一。
修复方案:
1
2
3
4
5
6
|
def transform_string(input_string):
"""将 StationId 统一转换为 00001~00009 格式"""
for i in range(1, 10):
if f"0{i}" in input_string:
return f"{i:05d}" # 格式化为 5 位数字
return "NA"
|
效果: 所有 StationId 统一为 00001, 00002, …, 00009,便于统计分析。
Bug 8: 特殊字符导致 XML 解析失败
现象: 部分测试名称包含 &, <, >, ", ' 等字符,XML 解析器报错。
原因: 未对特殊字符进行转义。
修复方案: 使用完整的 XML 转义函数(见 2.3 节)。
测试用例:
1
2
3
4
5
|
# 原始名称
'H Bridge J2600-1-25-27 forward & reverse'
# 转义后
'H Bridge J2600-1-25-27 forward & reverse'
|
Bug 9: 时间戳时区错误
现象: EDA 系统显示的时间比实际时间晚 8 小时。
原因: 未设置时区,默认为 UTC。
修复方案:
1
2
3
4
5
|
def convert_timestamp(timestamp_str):
dt = datetime.strptime(timestamp_str, "%Y-%m-%d %H:%M:%S,%f")
tz = timezone(timedelta(hours=8)) # 北京时间
dt_with_tz = dt.replace(tzinfo=tz)
return dt_with_tz.isoformat(timespec='milliseconds')
|
输出格式: 2024-11-05T03:57:12.217+08:00
Bug 10: 上下限为空的测量数据
现象: 部分测试项目(如 print_unitinfo_feedback)没有上下限,导致判断逻辑异常。
原因: 未处理 lowlimit 和 uplimit 为空的情况。
修复方案:
1
2
3
4
5
|
# 上下限为空时的处理
if not lowlimit and not uplimit:
# TDD 确认:没有影响,Status 设为 Pass
status = 'Pass'
comp_type = 'NA'
|
其他 Bug 修复清单
| # |
问题描述 |
解决方案 |
| 11 |
没有 SN 的日志 |
舍弃 SN 不正常的 log |
| 12 |
部分 log 转换后只有 Result 和 SN |
忽略这种不完整的 log |
| 13 |
机种名识别错误 |
完善 determine_model_name 函数 |
| 14 |
很多 log 没有站别信息 |
剔除没有 “update mes info” 的 log |
| 15 |
TV measurements 无法识别 |
增加 TV 测量的正则表达式 |
| 16 |
LogTime 字段为空 |
从每个 sub item 前提取时间戳 |
| 17 |
ErrorFullTestName 格式错误 |
TestData Name + "_" + MeasurementData Name |
| 18 |
Burn-in log 过大(1.34GB) |
跳过无转换价值的大文件 |
| 19 |
PASS 的 log 里面有 fail 项目 |
修正原始测试 log 的 SW |
| 20 |
LogCount 统一为 0 |
硬编码为 0 |
| 21 |
FCTTestFail 异常处理 |
添加特定错误模式匹配 |
| 22 |
SFCF01436 SFCS NG 剔除 |
添加特定错误码处理 |
📊 输出示例
生成的 XML 结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
<?xml version="1.0" encoding="UTF-8"?>
<TestResults>
<OverallResult>
<Result>Pass</Result>
<ErrorCode></ErrorCode>
<LogErrorMessage></LogErrorMessage>
<ErrorTestName></ErrorTestName>
<ErrorFullTestName></ErrorFullTestName>
<ErrorDetails></ErrorDetails>
</OverallResult>
<UnitSerialNumber>NRNWXPH077C482</UnitSerialNumber>
<StartDate>2024-12-09T13:34:30.276+08:00</StartDate>
<StopDate>2024-12-09T13:36:45.123+08:00</StopDate>
<TestCycleTime>2h15m30s</TestCycleTime>
<ModelName>NPD_REAR</ModelName>
<StationType>TT</StationType>
<StationId>00003</StationId>
<StationLine>FA11</StationLine>
<DutSwVersion>Build at : Nov 15 2024, 10:30:45</DutSwVersion>
<TestDatas>
<TestData Name="Test ID 001 - H Bridge Test" Status="Pass" TestTime="45">
<Measurements>
<MeasurementData
Name="H Bridge J2600-1-25-27 forward"
Status="Pass"
Result="0.0012"
LowerLimit="-13.5"
UpperLimit="-12"
Units="v"
CompType="GELE"
LogCount="0"
LogTime="2024-11-05T03:57:12.217+08:00"/>
</Measurements>
</TestData>
</TestDatas>
</TestResults>
|
💡 经验总结
1. 正则表达式是双刃剑
优点: 灵活强大,可以处理各种格式的日志。
缺点: 难以维护,日志格式一变就要调整。
建议:
- 为每个正则添加注释说明匹配格式
- 准备多个正则模式作为 fallback
- 考虑使用解析器库(如 pyparsing)替代复杂正则
2. 边界情况处理至关重要
本项目 20+ 个 Bug 中,80% 都是边界情况:
- 空值处理(上下限为空、版本信息缺失)
- 格式异常(特殊字符、时间格式错误)
- 数据不完整(缺少 SN、站别信息)
教训: 开发时要多问"如果 XXX 为空怎么办?"
3. 业务逻辑比技术实现更难
技术难点: 正则表达式、XML 生成、时间转换。
业务难点:
- 理解 EDA 系统的字段要求
- 确定 CompType 的判断规则
- 处理各种异常测试场景
建议: 多和业务方沟通,理解每个字段背后的业务含义。
4. 日志是最好的调试工具
1
2
3
4
|
# 关键步骤都添加日志
print(f"Found {len(test_matches)} Test IDs.")
print(f"Processing: {measurement_name}, Result: {result}")
print(f"Execution time: {execution_time:.2f} seconds")
|
效果: 出现问题时可以通过日志快速定位。
5. 函数化设计便于复用
将核心逻辑封装为 process_log_file() 函数:
🚀 性能优化
多进程处理
1
2
3
4
5
6
|
# 主脚本使用多进程并行处理
from multiprocessing import Pool
def process_batch(log_files):
with Pool(processes=4) as pool:
pool.map(process_log_file, log_files)
|
效果: 处理速度提升 3-4 倍。
执行时间监控
1
2
3
4
|
start_time = time.time()
# ... 处理逻辑 ...
end_time = time.time()
print(f"Execution time: {end_time - start_time:.2f} seconds")
|
典型性能:
- 单个日志文件:~0.5 秒
- 批量处理(100 个文件):~50 秒(4 进程)
📈 项目成果
效率提升
| 指标 |
改善前 |
改善后 |
提升 |
| 单文件处理时间 |
5 分钟(人工) |
0.5 秒(自动) |
600x |
| 错误率 |
~10% |
<0.1% |
99% |
| 可追溯性 |
无 |
完整错误信息 |
质的飞跃 |
数据质量
- ✅ 所有字段完整率:99.5%
- ✅ XML 格式正确率:100%
- ✅ 时间戳准确率:100%
- ✅ 错误信息可追溯:100%
🔮 未来改进方向
- 配置化 - 将正则表达式、字段映射配置到 YAML 文件
- Web 界面 - 提供可视化的日志上传和结果查看
- 实时监控 - 集成到产线 MES 系统,实时显示测试结果
- 数据分析 - 基于 XML 数据进行良率分析、趋势预测
- 异常检测 - 使用机器学习识别异常测试模式
📚 相关资源
🎯 结语
这个项目让我深刻体会到:真正的技术能力不在于使用多高深的算法,而在于解决实际问题的能力和对细节的把控。
20+ 个 Bug 的修复过程虽然痛苦,但每一个 Bug 都让我对业务理解更深一层。这正是工程师成长的最佳路径——在实战中打磨技能。
希望这篇文章能给你带来一些启发。如果你也在做类似的自动化工作,欢迎交流经验!
作者: Charles Miao
职位: DevOps / 硬件测试开发
日期: 2025 年 3 月
标签: #Python #自动化测试 #日志处理 #XML #DevOps #实战案例