一、要解决的问题
测试汽车诊断系统的时候,很难通过实车覆盖所有的诊断场景,即使可以,时间代价也很高。
所以很多时候需要通过模拟的手段进行测试,那就要改造报文,长度较短的报文可以直接手动修改,对于一个字节数上千的报文,就需要借助工具了。
比如一个字节数为772的十六进制报文,我们要实现以下2个需求:
- 替换指定字节位的内容为相应的值(从第0个字节开始计数)
- 替换指定连续字节位的内容为相应的值(从第0个字节开始计数)
原始报文格式 和 期待输出的报文格式为:
FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
二、代码实现
整体思路
首先,我们可以用一个字典存储我们要做的操作
如 loc_text = {550:‘00’,‘601 to 605’:‘0102030405’},标识我们要把第550个字节替换为00,第601个字节到第605个字节替换为0102030405
# 原始报文
text = ''
# 要操作的报文字节位置和替换进去的内容
loc_text = {550: '00', '601 to 605': '0102030405'}
for index, value in loc_text.items():
if isinstance(index, str) and 'to' in index:
try:
start = int(index.split(' ')[0])
end = int(index.split(' ')[2]) + 1
# TODO:连续字节替换处理
except ValueError:
print(f'在 {test} 中to前后不是数字')
elif isinstance(index, int):
# 单个字节替换处理
pass
else:
print(f"无法处理的索引值:{index}")
# 输出结果
print(text)
接下来,需要对原始报文进行替换,有2种思路:
- 第一种思路,将需要操作的字节位置 * 2,然后通过对原始报文字符串进行切片和拼接实现;
- 第二种思路,把原始报文按照字节分开,转为列表或字典或数组等,然后替换指定位置的内容。
第一种思路实现
- 使用字符串的切片,注意其前闭后开的特性,以及一个字节占了2个字符
for index, value in loc_text.items():
if isinstance(index,str) and 'to' in index:
try:
start = int(index.split(' ')[0])
end = int(index.split(' ')[2])
# 连续字节替换处理
# 字节数 * 2 为在字符串中的起始位置
text = text[: start * 2] + value + text[(end * 2) + 2 :]
except ValueError:
print(f'在 {test} 中to前后不是数字')
else:
# 单个字节替换处理
text = text[:index * 2] + value + text[(index * 2) + 2 :]
第二种思路实现(3种方法)
1. 将原始报文转为字节数组
- bytearray 是 Python 的一种可变字节数组类型。它是 bytes 类型的可变版本,可以进行修改操作
- bytearray 对象可以通过索引和切片来访问和修改其中的值
- bytearray.fromhex() 是 bytearray 类的一个方法,用于将十六进制字符串转换为对应的字节数组
# 将原始报文转为字节数组
text_to_array = bytearray.fromhex(text)
for index, value in loc_text.items():
if isinstance(index,str) and 'to' in index:
try:
start = int(index.split(' ')[0])
end = int(index.split(' ')[2])
# 连续字节替换处理,需要替换进去的内容也要转为字节数组
text_to_array[start:(end + 1)] = bytearray.fromhex(value)
except ValueError:
print(f'在 {test} 中to前后不是数字')
elif isinstance(index, int):
# 单个字节替换处理,需要将字符串转为十六进制的数字
text_to_array[index] = int(value, 16)
else:
print(f"无法处理的索引值:{index}")
text = text_to_array.hex().upper()
2. 将原始报文转为列表
使用正则表达式的sub方法,将报文按照字节分开。
(1)正则表达式为 (.{2})(?=.{2})
表示匹配任意两个字符,并且这两个字符后面还要跟着两个字符
这里使用了正向预查(lookahead)。
具体解析如下:
- . 表示匹配任意字符。 {2} 表示前面的模式匹配两次。
- (.) 使用括号将模式包裹起来,表示将匹配结果捕获为一个分组。
- (?=.{2}) 是一个正向预查,它表示在当前位置往后查找时,后面必须跟着两个字符。
(2)转换具体代码为 re.sub(r’(.{2})(?=.{2})‘, r’\1 ', text)
其中, r’\1 ’ 是替换字符串
它表示将匹配到的内容 替换为 分组捕获的结果 \1(即两个字符),并在后面添加一个空格
import re
.....
# 首先把报文按照字节用空格分开,这里使用正则表达式
re_split = re.sub(r'(.{2})(?=.{2})', r'\1 ', text)
# 然后将报文按照空格分割为列表
split_to_list = re_split .split(' ')
for index, value in loc_text.items():
if isinstance(index,str) and 'to' in index:
try:
start = int(index.split(' ')[0])
end = int(index.split(' ')[2])
# 连续替换,连续替换的值也需要转为列表
value_split = re.sub(r'(.{2})(?=.{2})',r'\1 ',value).split(' ')
split_to_list[start:end+1]=value_split
except ValueError:
print(f'在 {test} 中to前后不是数字')
elif isinstance(index,int):
split_to_list[index] = value
else:
print(f"无法处理的索引值:{index}")
text = ''.join(split_to_list)
3. 将原始报文转为字典
- 在列表实现的基础上,将报文进一步改为字典
- enumerate 是 Python 中的一个内置函数,用于将一个可迭代对象(例如列表、元组、字符串等)组合为一个索引序列,返回一个迭代器对象
- zip 是 Python 中的一个内置函数,用于将多个可迭代对象(例如列表、元组等)的对应元素打包成元组,返回一个迭代器对象
import re
.....
# 首先把报文按照字节用空格分开,这里使用正则表达式
re_split = re.sub(r'(.{2})(?=.{2})', r'\1 ', text)
# 然后将报文按照空格分割为列表
split_to_list = re_split .split(' ')
# 使用lamda表达式,和enumerate方法,将索引和值组成字典
split_to_dic = {index:result for index,result in enumerate(split_to_list)}
for index, value in loc_text.items():
if isinstance(index,str) and 'to' in index:
try:
start = int(index.split(' ')[0])
end = int(index.split(' ')[2])
# 连续替换的值转为列表
value_split = re.sub(r'(.{2})(?=.{2})',r'\1 ',value).split(' ')
# 使用zip函数,将索引和值打包成元组,返回一个迭代器对象
value_iter = zip(range(start, end+1),value_split)
# 更新字典相应key的value
split_to_dic.update(value_iter)
except ValueError:
print(f'在 {test} 中to前后不是数字')
elif isinstance(index,int):
split_to_dic[index] = value
else:
print(f"无法处理的索引值:{index}")
text = ''.join(split_to_dic.values())
三、代码效率
我们使用 timeit 模块,统计一下每种实现方法的代码效率
首先将需要替换的内容稍微添加一些 loc_text = {0:‘AA’,‘1 to 2’:‘0000’,100:‘BB’,‘205 to 207’:‘AABBDD’,550:‘00’,‘601 to 609’:‘010203040506000000’}
然后使用下面的结构统计执行时间
import timeit
time_start = timeit.default_timer()
#这里放要执行的代码
.......
time_end = timeit.default_timer()
time_spend = time_end - time_start
# 记得将XX替换为具体的名称
print(f'XX耗时 {time_spend}')
统计结果为
切片耗时 6.520003080368042e-05
数组耗时 2.389994915574789e-05
列表耗时 0.0008663999615237117
字典耗时 0.004955200012773275
可以看到,数组效率最高,其次是切片,然后是列表,最后是字典