摘要:本文介绍了一个生产环境中的实际案例——如何使用 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 系统)   │
└─────────────┘    └──────────────┘    └──────────────┘
       ↓                   ↓                   ↓
  产线测试           正则提取 + 逻辑处理      数据入库分析

实施步骤

  1. 清空临时文件夹 - 确保处理环境干净
  2. 筛选前一天日志 - 多进程并行处理
  3. Log → XML 转换 - 核心解析逻辑
  4. 复制到 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 = {
        '&': '&',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&apos;'
    }
    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 &amp; 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)没有上下限,导致判断逻辑异常。

原因: 未处理 lowlimituplimit 为空的情况。

修复方案:

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%

🔮 未来改进方向

  1. 配置化 - 将正则表达式、字段映射配置到 YAML 文件
  2. Web 界面 - 提供可视化的日志上传和结果查看
  3. 实时监控 - 集成到产线 MES 系统,实时显示测试结果
  4. 数据分析 - 基于 XML 数据进行良率分析、趋势预测
  5. 异常检测 - 使用机器学习识别异常测试模式

📚 相关资源


🎯 结语

这个项目让我深刻体会到:真正的技术能力不在于使用多高深的算法,而在于解决实际问题的能力和对细节的把控。

20+ 个 Bug 的修复过程虽然痛苦,但每一个 Bug 都让我对业务理解更深一层。这正是工程师成长的最佳路径——在实战中打磨技能。

希望这篇文章能给你带来一些启发。如果你也在做类似的自动化工作,欢迎交流经验!


作者: Charles Miao
职位: DevOps / 硬件测试开发
日期: 2025 年 3 月
标签: #Python #自动化测试 #日志处理 #XML #DevOps #实战案例