<think>我们需要注意以下几点:
1. 创建项目目录和result目录。
2. 检查ref和dut目录下是否有文件,没有则报错。
3. 创建Excel工作簿,以项目名+日期命名。
4. 处理ref和dut的bugreport文件,根据平台关键字提取数据,并写入Excel的对应工作表。
5. 根据平台是否相同,使用不同的关键字列表(相同平台使用平台关键字,不同平台使用common_keyword)创建比较工作表。
6. 特殊处理三个关键字:Booting Linux on physical、init first stage started!、init second stage started!。其中Booting Linux on physical强制记录为0,另外两个记录与前一个关键字的差值(时间戳差值)。
7. 在比较工作表中,第六列(差值)大于300的正值标红。
由于需求中提到了时间戳的差值计算,我们需要从日志行中提取时间戳字符串,然后转换为时间对象,再计算差值(毫秒)。
步骤:
1. 解析命令行参数(项目名,ref平台,dut平台)。
2. 创建目录结构。
3. 检查ref和dut目录。
4. 创建Excel工作簿。
5. 处理ref和dut的bugreport文件(查找、解压、提取文件)。
6. 根据平台加载关键字(mtk_keyword.txt等)。
7. 从bugreport文件中提取数据(注意特殊关键字的处理)。
对于特殊关键字,我们需要提取日志行的时间戳,然后计算时间差(毫秒)。
8. 将提取的数据写入Excel的对应工作表(ref_bootimg和dut_bootimg)。
9. 根据平台是否相同,决定使用哪个关键字列表(相同平台用平台关键字,不同用common_keyword)来创建比较工作表。
10. 在比较工作表中,按照要求计算阶段时间和差值,并对第六列中大于300的正值标红。
注意:时间戳的格式为 "MM-DD HH:mm:ss.fff",例如 "03-04 03:33:58.101"
我们将编写以下函数:
- create_project_directories(project_name)
- check_directories()
- create_excel_workbook(result_dir, project_name)
- find_and_extract_bugreport(directory)
- load_keywords(platform, script_dir) # 加载平台关键字
- load_common_keywords(script_dir) # 加载common_keyword
- extract_timings_from_file(file_path, keywords) # 提取时间数据,特殊关键字需要计算时间戳差值
- write_to_excel_sheet(excel_file, sheet_name, data)
- create_comparison_sheet(excel_file, dut_data, ref_data, keywords, same_platform) # 创建比较表,计算阶段时间、差值,并标红
在提取时间数据时,对于普通关键字,我们仍然用之前的方式(正则表达式匹配数字)。但对于三个特殊关键字,我们需要:
- 对于"Booting Linux on physical": 记录0。
- 对于"init first stage started!": 找到该行和"Booting Linux on physical"行的时间戳,计算差值(毫秒)。
- 对于"init second stage started!": 找到该行和"init first stage started!"行的时间戳,计算差值(毫秒)。
因此,在提取数据时,我们需要同时记录关键字对应的日志行的时间戳(如果存在的话)。
由于需求中要求即使没有找到"Booting Linux on physical"也要记录0,所以我们在处理特殊关键字时:
- 如果关键字是"Booting Linux on physical",我们记录0,同时记录它的时间戳(如果找到的话,用于后续计算)。
- 如果找不到,则后续的"init first stage started!"和"init second stage started!"将无法计算,记录为None。
在提取数据函数中,我们将遍历关键字列表,对于每个关键字,我们搜索文件中所有匹配的行(注意可能有多个匹配,我们取最后一个?根据需求,我们取最后一个出现的匹配行,因为日志是顺序的,最后一个可能是最接近的?但通常每个事件只出现一次,不过为了安全,我们取最后一个)。
对于特殊关键字,我们不仅需要提取时间戳字符串,还要将其转换为datetime对象(注意:年份需要补充,我们可以用当前年份,因为bugreport通常都是当年的)。
时间戳转换函数:
def parse_timestamp(timestamp_str, base_year=2023):
try:
# 格式: "03-04 03:33:58.101"
dt = datetime.strptime(f"{base_year}-{timestamp_str}", "%Y-%m-%d %H:%M:%S.%f")
return dt
except:
return None
但是,注意:bugreport中的时间戳可能跨年?所以我们可以不关心年份,因为两次事件在同一个bugreport中,年份相同,所以用同一个基准年即可(用当前年)。
步骤:
1. 读取整个文件内容。
2. 对于每个关键字,如果是特殊关键字,则使用正则表达式匹配包含该关键字的整行,并提取该行的时间戳部分(格式为"MM-DD HH:mm:ss.fff")。
3. 对于普通关键字,则使用原来的方式(匹配关键字后面的数字)。
我们将设计一个函数来处理特殊关键字的提取。
由于需求中要求三个特殊关键字的处理方式,我们在提取数据时,需要先提取所有特殊关键字的时间戳,然后计算差值。
具体步骤(在extract_timings_from_file函数中):
- 初始化一个字典来存放每个关键字的值(包括普通关键字和特殊关键字)。
- 另外,我们需要一个字典来存放每个关键字对应的时间戳(datetime对象),用于特殊关键字的计算。
- 遍历关键字列表:
if keyword == "Booting Linux on physical":
在文件中搜索该关键字,记录最后一个匹配行的时间戳,并转换为datetime对象。如果找到,则记录时间戳,并记录该事件的时间戳对象(用于后续计算),同时该关键字的值设为0。
如果没有找到,则记录时间戳为None,值设为0(但后续的依赖它的关键字将无法计算)。
elif keyword in ["init first stage started!", "init second stage started!"]:
在文件中搜索该关键字,记录最后一个匹配行的时间戳,并转换为datetime对象。
然后,根据它依赖的前一个关键字的时间戳来计算差值(毫秒)。
注意:前一个关键字的时间戳必须存在,且当前关键字的时间戳也存在,否则记录为None。
else:
普通关键字,用正则表达式匹配关键字后面的数字,取最后一个匹配。
注意:由于三个特殊关键字是固定的,我们可以写死处理逻辑。
但是,需求中要求关键字列表来自文件,所以我们需要确保在关键字列表中,这三个特殊关键字是按照顺序出现的(因为计算阶段时间需要前一个事件)。所以,关键字文件中的顺序应该是:
Booting Linux on physical
init first stage started!
init second stage started!
这样,我们在提取数据时,按照关键字列表顺序处理,但计算"init first stage started!"时,需要用到"Booting Linux on physical"的时间戳,因此我们可以在处理过程中存储这些时间戳。
因此,我们在函数内部维护一个字典(timestamps)来存储每个关键字的时间戳(如果找到的话)。
具体步骤:
timestamps = {} # 存储关键字对应的时间戳(datetime对象)
results = {} # 存储关键字对应的值(数字)
for keyword in keywords:
if keyword 是特殊关键字之一:
搜索该关键字的日志行,提取时间戳字符串,转换为datetime对象,存入timestamps[keyword]
else:
用原来的方式提取数字
然后,根据特殊关键字的依赖关系计算值:
if keyword == "Booting Linux on physical":
results[keyword] = 0
elif keyword == "init first stage started!":
if timestamps.get("Booting Linux on physical") and timestamps.get(keyword):
delta = timestamps[keyword] - timestamps["Booting Linux on physical"]
results[keyword] = delta.total_seconds() * 1000 # 毫秒
else:
results[keyword] = None
elif keyword == "init second stage started!":
if timestamps.get("init first stage started!") and timestamps.get(keyword):
delta = timestamps[keyword] - timestamps["init first stage started!"]
results[keyword] = delta.total_seconds() * 1000
else:
results[keyword] = None
else:
results[keyword] = 用正则匹配到的数字(最后一个)
但是注意:我们可能无法保证关键字列表的顺序,所以我们在处理每个关键字时,需要独立搜索。而且,在搜索"init first stage started!"时,需要先有"Booting Linux on physical"的时间戳。因此,我们可以在遍历关键字列表之前,先搜索所有特殊关键字的时间戳(如果它们存在于关键字列表中的话)。或者,我们可以先处理"Booting Linux on physical",再处理"init first stage started!",最后处理"init second stage started!",但关键字列表可能不按这个顺序。因此,我们最好先收集所有特殊关键字的时间戳(不管关键字列表顺序),然后再计算。
调整方案:
- 先一次性收集所有三个特殊关键字的时间戳(如果它们在关键字列表中的话)。
- 然后,再遍历关键字列表,对普通关键字进行提取,对特殊关键字则使用已经收集到的时间戳进行计算。
这样可以避免顺序问题。但是,如果关键字列表中没有包含依赖的关键字,那么就无法计算。例如,如果关键字列表中有"init first stage started!"但没有"Booting Linux on physical",那么我们就需要按照需求将"Booting Linux on physical"视为0,但是找不到它的时间戳,所以"init first stage started!"就无法计算,只能为None。
因此,我们可以这样做:
1. 从关键字列表中识别出三个特殊关键字,并分别搜索它们的时间戳(记录最后一个出现的时间戳)。
2. 然后,按照关键字列表的顺序,给每个关键字赋值:
"Booting Linux on physical": 0
"init first stage started!": 用它的时间戳减去"Booting Linux on physical"的时间戳(如果两者都存在)
"init second stage started!": 用它的时间戳减去"init first stage started!"的时间戳(如果两者都存在)
3. 普通关键字:用正则匹配数字。
所以,我们分两步:
step1: 遍历整个文件,收集三个特殊关键字的时间戳(不管关键字列表中是否有,因为如果关键字列表中有,我们就需要计算;如果没有,我们就不管)。
step2: 遍历关键字列表,对每个关键字,如果是特殊关键字,则从收集好的时间戳字典中取出并计算(如果依赖的时间戳存在);如果是普通关键字,则用正则匹配。
这样,我们可以避免重复搜索文件。
具体实现:
special_keywords = ["Booting Linux on physical CPU", "init first stage started!", "init second stage started!"]
# 注意:需求中的关键字是“Booting Linux on physical”,但日志中可能是“Booting Linux on physical CPU 0x...”,所以我们的关键字文件中的关键字应该能够匹配到日志行。因此,我们可能需要使用更灵活的正则,或者关键字文件中的关键字要写完整?或者只写部分?这里我们按照用户提供的关键字文件中的字符串进行匹配(整行包含该字符串即可)。
在收集特殊关键字的时间戳时,我们遍历整个文件,对每一行,如果包含特殊关键字之一,则提取时间戳,并更新该关键字的时间戳(我们取最后一个,所以后面覆盖前面)。
然后,再处理关键字列表中的每个关键字。
因此,extract_timings_from_file函数的步骤:
1. 读取文件内容,按行处理。
2. 初始化一个字典:special_timestamps = {keyword: None for keyword in special_keywords}
3. 遍历每一行:
for line in lines:
for keyword in special_keywords:
if keyword in line:
# 提取时间戳:行首的"MM-DD HH:mm:ss.fff"
# 注意:时间戳在行首,格式为"03-04 03:33:58.101"
timestamp_str = line.split()[0] + ' ' + line.split()[1] # 前两个部分:日期和时间
try:
dt = parse_timestamp(timestamp_str)
special_timestamps[keyword] = dt
except:
pass # 保持None
4. 然后,初始化结果字典results = {}
5. 遍历关键字列表(参数keywords):
for keyword in keywords:
if keyword in special_keywords:
if keyword == "Booting Linux on physical CPU": # 注意:这里关键字可能和文件中的不完全匹配,所以关键字文件中的应该和日志中的一致。需求中写的是“Booting Linux on physical”,但日志中可能是“Booting Linux on physical CPU ...”,所以关键字文件中应该写完整的字符串,比如“Booting Linux on physical CPU”
results[keyword] = 0
elif keyword == "init first stage started!":
if special_timestamps.get("Booting Linux on physical CPU") and special_timestamps.get(keyword):
delta = special_timestamps[keyword] - special_timestamps["Booting Linux on physical CPU"]
results[keyword] = delta.total_seconds() * 1000
else:
results[keyword] = None
elif keyword == "init second stage started!":
if special_timestamps.get("init first stage started!") and special_timestamps.get(keyword):
delta = special_timestamps[keyword] - special_timestamps["init first stage started!"]
results[keyword] = delta.total_seconds() * 1000
else:
results[keyword] = None
else:
# 普通关键字:在整行中搜索关键字,然后找后面的数字
# 注意:我们可能已经读过文件了,现在再次搜索整个文件?为了避免再次读取,我们可以在第一步读取文件时把内容保存下来。
# 所以,我们第一步读取文件时,将内容保存为字符串,然后这里用正则搜索。
pass
6. 但是,普通关键字的提取也需要读取整个文件内容(或者我们之前已经读取了,可以复用)。所以我们在第一步读取文件内容时,将整个内容保存下来,然后对于普通关键字,我们使用正则表达式在内容中搜索。
因此,我们可以:
content = 整个文件内容(字符串)
然后对于普通关键字,我们使用正则表达式:pattern = re.compile(rf"{re.escape(keyword)}.*?(\d+)", re.IGNORECASE | re.DOTALL)
匹配最后一个出现的数字。
7. 注意:特殊关键字我们已经在第一步按行处理时提取了时间戳,所以普通关键字我们单独处理。
为了避免两次读取文件,我们在第一步按行处理特殊关键字时,同时将整个内容保存下来。
因此,步骤调整:
a. 读取整个文件内容(字符串)和行列表。
b. 遍历行列表,提取三个特殊关键字的时间戳(记录最后一个匹配行的时间戳)。
c. 遍历关键字列表,对每个关键字:
- 如果是特殊关键字,则从special_timestamps中取时间戳并计算(如上述)。
- 如果是普通关键字,则在content中搜索,用正则表达式匹配关键字后面的数字(取最后一个匹配)。
注意:三个特殊关键字的字符串是固定的,所以我们在代码中写死。但是,关键字文件中的这三个关键字必须和我们代码中指定的字符串完全一致(包括大小写)。因此,我们要求关键字文件中的这三个关键字必须和日志中的字符串片段完全匹配(包括空格和标点)。
考虑到日志行中的关键字可能有变化,我们最好使用更灵活的方式(比如允许部分匹配),但需求中已经给出了具体的字符串,我们就按照需求中的字符串来定义。
但是,注意需求中的字符串:
"Booting Linux on physical" -> 日志中可能是 "Booting Linux on physical CPU 0x...",所以关键字文件中应该写能够匹配到日志行的字符串,比如"Booting Linux on physical CPU"
因此,我们在关键字文件中,应该写完整的字符串(如"Booting Linux on physical CPU")才能匹配到。
所以,我们定义:
BOOTING_KEY = "Booting Linux on physical CPU"
FIRST_STAGE_KEY = "init first stage started!"
SECOND_STAGE_KEY = "init second stage started!"
然后,在代码中,我们使用这三个字符串。
现在,我们开始编写代码。
由于代码较长,我们将分块编写。
注意:在提取时间戳时,时间戳格式为"03-04 03:33:58.101",但有时可能只有两位毫秒?我们按照三位处理,如果不足三位,可以补零。
时间戳转换函数:
def parse_timestamp(timestamp_str, base_year=datetime.now().year):
try:
# 格式: "03-04 03:33:58.101"
# 将时间戳字符串分割为日期和时间部分
parts = timestamp_str.split()
if len(parts) < 2:
return None
date_part = parts[0]
time_part = parts[1]
# 时间部分可能有毫秒,也可能没有
if '.' in time_part:
time_format = "%H:%M:%S.%f"
else:
time_format = "%H:%M:%S"
# 组合成完整字符串
full_str = f"{base_year}-{date_part} {time_part}"
dt = datetime.strptime(full_str, "%Y-%m-%d %H:%M:%S.%f" if '.' in time_part else "%Y-%m-%d %H:%M:%S")
return dt
except Exception as e:
logger.error(f"解析时间戳错误: {timestamp_str}, 错误: {e}")
return None
但是,注意:时间戳字符串在行首,例如:"03-04 03:33:58.101",它由两个空格分隔的部分组成:日期(03-04)和时间(03:33:58.101)。所以,我们直接按空格分割一次,取前两个部分。
我们假设时间戳字符串总是位于行首,且格式固定。
现在,我们开始编写完整的脚本。
由于代码很长,我们将逐步实现函数。
注意:我们假设bugreport文件可能很大,所以按行读取而不是一次性读取整个文件。但是,为了普通关键字的正则匹配,我们需要整个文件内容,所以我们可以先按行读取并处理特殊关键字,同时将行拼接成字符串(用于普通关键字的搜索)。或者,先读取整个内容,然后按行分割。如果文件不大,可以一次性读取。
我们选择一次性读取整个文件内容,然后按行分割。
步骤:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
content = f.read()
lines = content.splitlines()
然后,用lines去处理特殊关键字的时间戳,而content用于普通关键字的正则搜索。
最后,我们编写完整的代码。
注意:在比较工作表中,第四列和第五列是阶段时间,算法是当前行的值减去前一行的值(第一行为0)。但是,特殊关键字我们已经处理成了阶段时间(差值),所以在比较工作表中,对于特殊关键字,我们直接使用提取的值(因为已经是阶段时间了)?不,在提取数据时,特殊关键字的值已经是阶段时间(差值),而普通关键字是绝对时间戳(毫秒)。所以,在比较工作表中,我们需要区分吗?
实际上,在提取数据时,普通关键字的值是事件发生的时间戳(毫秒),而特殊关键字的值已经是阶段时间(毫秒)。所以,在比较工作表中,我们计算阶段时间时:
- 对于普通关键字,我们计算当前事件的时间戳减去上一个事件的时间戳(即阶段时间)。
- 对于特殊关键字,我们直接使用提取的值(已经是阶段时间)?但是,在比较工作表中,我们要求的是每个阶段的耗时,而特殊关键字的值正是阶段耗时,所以我们可以直接使用。
但是,在比较工作表中,我们要求:
第四列:DUT各阶段数据(算法为第一行为0,第二行数据为第二列第二行减去第一行的数据,以此类推到最后一行)
这里,第二列是提取的DUT数据(对于普通关键字是绝对时间戳,对于特殊关键字是阶段耗时)。这样就不一致了。
所以,我们需要统一:在提取数据时,普通关键字也转换为阶段耗时?但是,普通关键字的事件可能不是连续的,所以无法直接转换为阶段耗时。
因此,我们调整:在提取数据时,我们只提取事件发生的时间戳(毫秒数,从0开始?),然后统一在比较工作表中计算阶段耗时(后一个事件减去前一个事件)。
但是,特殊关键字我们已经转换为了阶段耗时,所以不能这样处理。
为了解决这个问题,我们可以在提取数据时,将所有关键字的值都转换为从0开始的绝对时间(毫秒):
- "Booting Linux on physical CPU":0
- "init first stage started!":从Booting到first stage的时间差(记为T1),那么它的绝对时间就是T1。
- "init second stage started!":从Booting到second stage的时间差(记为T2),那么它的绝对时间就是T1 + (second stage - first stage)?不对,应该是T2 = T1 + (second stage - first stage) 吗?不对,T2是相对于Booting的时间,而second stage - first stage是T2-T1,所以T2 = T1 + (second stage - first stage) 是成立的。
因此,我们可以这样:
- 将"Booting Linux on physical CPU"的绝对时间设为0。
- 将"init first stage started!"的绝对时间设为(从Booting到first stage的时间差)T1。
- 将"init second stage started!"的绝对时间设为(从Booting到second stage的时间差)T2,也就是 T1 + (second stage - first stage) = T2。
但是,我们如何得到T2?我们在提取"init second stage started!"时,计算的是相对于first stage的差值,而不是相对于Booting的。所以,我们无法直接得到T2。
因此,我们改变策略:在提取数据时,特殊关键字也提取为绝对时间(相对于Booting的时间)。
- Booting: 0
- first stage: 从Booting到first stage的时间差(毫秒) -> 绝对时间戳
- second stage: 从Booting到second stage的时间差(毫秒) -> 绝对时间戳
这样,我们就可以将特殊关键字和普通关键字统一为绝对时间戳(从Booting事件为0开始的时间戳)。
如何得到second stage的绝对时间戳?
second stage的绝对时间戳 = first stage的绝对时间戳 + (second stage - first stage的时间差)
所以,在提取数据时,对于second stage,我们可以这样计算:
if booting_time and first_stage_time and second_stage_time:
second_stage_abs = first_stage_abs + (second_stage_time - first_stage_time) # 但是,second_stage_time是一个时间对象,而first_stage_abs是毫秒数,这样不行。
实际上,我们在第一步已经得到了每个事件的时间戳(datetime对象),所以:
booting_time = special_timestamps[BOOTING_KEY]
first_stage_time = special_timestamps[FIRST_STAGE_KEY]
second_stage_time = special_timestamps[SECOND_STAGE_KEY]
first_stage_abs = (first_stage_time - booting_time).total_seconds() * 1000
second_stage_abs = (second_stage_time - booting_time).total_seconds() * 1000
所以,我们无需在提取阶段耗时,而是直接计算绝对时间戳(相对于booting事件)。
因此,在提取数据时,对于特殊关键字,我们记录的是绝对时间戳(相对于booting事件,booting事件为0)。
这样,我们就可以将特殊关键字和普通关键字一视同仁:都是绝对时间戳(从0开始)。
然后,在比较工作表中,阶段时间的计算统一为:当前事件的时间戳减去上一个事件的时间戳。
所以,我们调整 extract_timings_from_file 函数:
- 对于特殊关键字,我们计算其相对于booting事件的绝对时间戳(毫秒)。
- 对于普通关键字,我们仍然提取事件发生的绝对时间戳(毫秒),但是这个时间戳是相对于booting事件吗?不是,普通关键字的时间戳是系统日志的时间,和booting事件的时间戳是同一个时钟,所以我们可以用普通关键字的时间戳减去booting事件的时间戳,得到相对于booting事件的绝对时间戳。
因此,我们需要先提取booting事件的时间戳(datetime对象),然后将普通关键字的时间戳也转换为datetime对象,再减去booting事件的时间戳,得到毫秒数。
所以,步骤调整为:
1. 提取所有特殊关键字的时间戳(datetime对象),包括booting事件。
2. 记录 booting_time = special_timestamps[BOOTING_KEY]
3. 对于普通关键字,我们也要在日志行中提取时间戳(datetime对象),然后计算 (time - booting_time) 的毫秒数,作为该事件的绝对时间戳(相对于booting事件)。
注意:如果booting_time不存在,那么我们就无法计算任何绝对时间戳,所以所有事件的值都为None。
因此, extract_timings_from_file 函数的步骤:
a. 读取文件内容,分割为行。
b. 初始化 special_timestamps = { BOOTING_KEY: None, FIRST_STAGE_KEY: None, SECOND_STAGE_KEY: None }
c. 遍历每一行,如果行中包含BOOTING_KEY、FIRST_STAGE_KEY、SECOND_STAGE_KEY,则提取时间戳并更新special_timestamps。
d. 取 booting_time = special_timestamps[BOOTING_KEY]
e. 初始化 results = {}
f. 遍历关键字列表:
if keyword == BOOTING_KEY:
results[keyword] = 0
elif keyword == FIRST_STAGE_KEY:
if booting_time and special_timestamps[FIRST_STAGE_KEY]:
dt = special_timestamps[FIRST_STAGE_KEY]
results[keyword] = (dt - booting_time).total_seconds() * 1000
else:
results[keyword] = None
elif keyword == SECOND_STAGE_KEY:
if booting_time and special_timestamps[SECOND_STAGE_KEY]:
dt = special_timestamps[SECOND_STAGE_KEY]
results[keyword] = (dt - booting_time).total_seconds() * 1000
else:
results[keyword] = None
else:
# 普通关键字: 在日志行中搜索该关键字,提取时间戳
# 注意:普通关键字可能有多个匹配,我们取最后一个匹配行
matched_time = None
for line in lines:
if keyword in line:
timestamp_str = ' '.join(line.split()[:2]) # 前两个部分
try:
dt = parse_timestamp(timestamp_str)
matched_time = dt
except:
continue
if matched_time and booting_time:
# 计算相对于booting_time的时间戳(毫秒)
results[keyword] = (matched_time - booting_time).total_seconds() * 1000
else:
results[keyword] = None
g. 返回 results
但是,这样遍历每一行来搜索普通关键字,效率较低(特别是关键字很多时)。我们可以优化:在第一步遍历行的时候,我们可以为每个普通关键字也保存最后一个匹配行的时间戳。我们可以建立一个字典,键是普通关键字,值是对应的时间戳(datetime对象)。
因此,我们可以在第一步遍历行时,同时检查所有关键字(特殊+普通)的时间戳。
所以,我们这样做:
- 将关键字列表中的所有关键字都加入到一个集合中。
- 然后,遍历每一行:
for line in lines:
for keyword in all_keywords:
if keyword in line:
timestamp_str = ' '.join(line.split()[:2])
dt = parse_timestamp(timestamp_str)
if dt:
# 记录该关键字的时间戳(取最后一个,所以不断更新)
keyword_timestamps[keyword] = dt
- 然后,我们有了每个关键字的时间戳(datetime对象)。
- 接着,我们计算每个关键字相对于booting事件的时间差(毫秒):
results = {}
booting_time = keyword_timestamps.get(BOOTING_KEY) # 注意:BOOTING_KEY必须在关键字列表中,否则为None
for keyword in keywords:
if keyword == BOOTING_KEY:
results[keyword] = 0
else:
if booting_time is not None and keyword in keyword_timestamps and keyword_timestamps[keyword] is not None:
results[keyword] = (keyword_timestamps[keyword] - booting_time).total_seconds() * 1000
else:
results[keyword] = None
注意:这里的前提是booting_time存在,且当前关键字的时间戳也存在。
但是,特殊关键字中的first stage和second stage,我们也是用同样的方式计算,所以没有问题。
因此,我们修改 extract_timings_from_file 函数如下:
- 读取文件的所有行。
- 初始化 keyword_timestamps = {keyword: None for keyword in keywords} # 为关键字列表中的每个关键字初始化
- 遍历每一行,检查该行是否包含任何关键字,如果包含,则更新时间戳(取最后一个)。
- 然后,提取 BOOTING_KEY 的时间戳(如果BOOTING_KEY在关键字列表中)。
- 遍历关键字列表,计算相对于 BOOTING_KEY 的时间戳的毫秒数。
注意:BOOTING_KEY可能不在关键字列表中,那么我们就无法计算。但是,需求中要求即使没有找到BOOTING_KEY,也要记录为0,所以我们在 results[keyword]=0 时,并不依赖于booting_time。但是,其他关键字依赖于booting_time,如果booting_time不存在,则其他关键字为None。
所以,我们这样写:
booting_time = keyword_timestamps.get(BOOTING_KEY) # 注意:BOOTING_KEY must be in keywords? 不一定,如果关键字列表中没有BOOTING_KEY,那么keyword_timestamps中就没有它。所以,只有当BOOTING_KEY在关键字列表中时,我们才有booting_time。
因此,我们要求:如果用户想要计算其他关键字的时间戳,关键字列表中必须包含BOOTING_KEY。否则,其他关键字的时间戳无法计算(为None)。
所以,我们建议在关键字列表中添加BOOTING_KEY。
现在,我们编写 extract_timings_from_file 函数。
但是,效率问题:如果关键字很多,每行都要遍历所有关键字,可能会慢。我们可以优化:对每个关键字生成一个正则表达式,然后并行匹配?或者,我们可以将所有关键字合并为一个大的正则表达式,然后匹配行中的任意关键字。
但是,为了 simplicity,我们暂时使用遍历关键字的方法,因为关键字不会太多。
如果文件很大,我们可以先 compile 关键字,然后 use any().
例如:
for line in lines:
for keyword in keywords:
if keyword in line:
# 处理
break # 一行可能包含多个关键字,但我们只记录每个关键字最后一次出现,所以即使一行有多个关键字,我们也会记录每个关键字
但是,一行可能包含多个关键字,所以我们不能break,而是要继续检查该行是否包含其他关键字。
所以,我们不break。
现在,我们开始编写代码。
由于代码量较大,我们只写关键函数,并省略一些细节。
注意:在比较工作表中,我们计算阶段时间时,第一行为0,第二行(事件2)的阶段时间 = 事件2的绝对时间 - 事件1的绝对时间,事件3的阶段时间 = 事件3的绝对时间 - 事件2的绝对时间,等等。
因此,在 create_comparison_sheet 函数中,我们遍历关键字列表(顺序很重要),然后:
prev_dut = 0
prev_ref = 0
for i, keyword in enumerate(keywords):
dut_abs = dut_data.get(keyword) # 这是DUT的绝对时间戳(相对于booting事件)
ref_abs = ref_data.get(keyword) # REF的绝对时间戳
# 计算阶段时间
if i == 0:
# 第一行
dut_stage = 0
ref_stage = 0
else:
if dut_abs is not None and prev_dut is not None:
dut_stage = dut_abs - prev_dut
else:
dut_stage = None
if ref_abs is not None and prev_ref is not None:
ref_stage = ref_abs - prev_ref
else:
ref_stage = None
# 写入Excel
...
# 更新prev_dut和prev_ref
if dut_abs is not None:
prev_dut = dut_abs
if ref_abs is not None:
prev_ref = ref_abs
注意:阶段时间可能为负吗?正常情况下,事件是顺序发生的,所以不会为负。但如果日志顺序错乱,有可能。但我们 assume 不会。
现在,我们编写完整的代码。
由于时间关系,我们省略一些日志和错误处理,但尽量保留主要逻辑。
注意:我们假设三个特殊关键字的字符串为常量:
BOOTING_KEY = "Booting Linux on physical CPU"
FIRST_STAGE_KEY = "init first stage started!"
SECOND_STAGE_KEY = "init second stage started!'
但是,用户的关键字文件中可能使用不同的字符串,所以我们还是以用户的关键字文件为准。也就是说,我们并不在代码中 hardcode 这三个关键字,而是完全按照关键字文件中的关键字来匹配。所以,在提取时间戳时,我们 treat all keywords equally.
因此,我们不需要 special treatment for these keywords in the extraction function, because we are going to extract timestamps for every keyword in the list.
所以, extract_timings_from_file 函数可以统一处理所有关键字。
综上所述,我们实现的 extract_timings_from_file 函数如下:
def extract_timings_from_file(file_path, keywords):
# 读取文件
try:
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
lines = f.readlines()
except:
return {}
# 初始化每个关键字的时间戳为None
keyword_timestamps = {keyword: None for keyword in keywords}
# 用于解析时间戳的函数
base_year = datetime.now().year
for line in lines:
for keyword in keywords:
if keyword in line:
# 提取时间戳:行首的两个部分
parts = line.strip().split()
if len(parts) < 2:
continue
timestamp_str = parts[0] + ' ' + parts[1]
dt = parse_timestamp(timestamp_str, base_year)
if dt:
keyword_timestamps[keyword] = dt
# 注意:不break,因为一行可能包含多个关键字
# 提取 booting_keyword
# 注意:我们不知道哪个关键字是booting,所以我们需要用户确保关键字列表中有一个关键字是booting,并且我们统一认为booting event的 timestamp 作为参考点
# 但是,我们如何确定哪个关键字是booting?我们无法确定。
# 所以,我们只能选择一个参考事件,比如我们指定必须是 "Booting Linux on physical CPU" 作为参考事件,即参考事件的关键字 must be in the keywords list.
# 因此,我们要求:如果用户希望计算绝对时间,那么关键字列表必须包含参考事件(booting event)的关键字。
# 查找参考事件的时间戳
booting_time = None
# 参考事件的关键字
# 注意:我们 hardcode 参考事件的关键字为 "Booting Linux on physical CPU"
REFERENCE_KEY = "Booting Linux on physical CPU"
if REFERENCE_KEY in keyword_timestamps:
booting_time = keyword_timestamps[REFERENCE_KEY]
else:
# 如果没有参考事件,那么我们就不能用,所以所有事件的时间戳都是None
booting_time = None
results = {}
for keyword in keywords:
if keyword == REFERENCE_KEY:
# 参考事件的时间差为0
results[keyword] = 0
else:
if booting_time is not None and keyword_timestamps[keyword] is not None:
results[keyword] = (keyword_timestamps[key] - booting_time).total_seconds() * 1000
else:
results[keyword] = None
return results
但是,这样做,我们就 hardcode 了参考事件的关键为 "Booting Linux on physical CPU"。
而且,如果用户的关键字列表中不包含这个关键字,那么我们就无法计算其他关键字的时间戳。
所以,我们必须在加载关键字文件时,确保如果 platform 是 MTK/QCOM/SPRD,那么关键字文件中必须包含 "Booting Linux on physical CPU" 吗?
需求中要求,如果没有找到 "Booting Linux on physical CPU",则记录为0,但其他 event 就无法计算。
因此,我们只能要求用户的关键字文件包含这个参考事件。
现在,我们开始写代码。
由于代码量很大,我们只写出关键部分,并尽量写完整。
注意:我们 hardcode 参考事件的关键字为 "Booting Linux on physical CPU"。
我们写 parse_timestamp 函数。
由于时间关系,我们省略了详细注释,代码中会包含必要注释。
注意:在 create_comparison_sheet 函数中,我们需要 stage time 的计算(后一个减前一个),并且要处理可能为 None 的情况。
开始写代码。
</think>
下面我将创建一个完整的 Python 脚本,满足您所有的需求,特别是处理时间戳差值的计算。这个脚本会处理 bugreport 文件,根据平台关键字提取数据,计算时间戳差值,并在 Excel 中进行全面的数据比较和标记。
```python
#!/usr/bin/env python3
import os
import sys
import argparse
import zipfile
import re
import shutil
from datetime import datetime, timedelta
import openpyxl
from openpyxl.styles import PatternFill
from openpyxl.utils import get_column_letter
import logging
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout)
]
)
logger = logging.getLogger(__name__)
# 特殊关键字常量
BOOTING_KEY = "Booting Linux on physical"
FIRST_STAGE_KEY = "init first stage started!"
SECOND_STAGE_KEY = "init second stage started!"
def create_project_directories(project_name):
"""创建项目目录结构"""
project_dir = os.path.join(os.getcwd(), project_name)
result_dir = os.path.join(project_dir, 'result')
# 创建主目录和result子目录
os.makedirs(result_dir, exist_ok=True)
logger.info(f"✅ 创建项目目录: {project_dir}")
logger.info(f"└── result")
return project_dir, result_dir
def check_directories():
"""检查ref和dut目录是否有文件"""
ref_dir = os.path.join(os.getcwd(), 'ref')
dut_dir = os.path.join(os.getcwd(), 'dut')
errors = []
# 检查ref目录
if not os.path.exists(ref_dir) or not os.path.isdir(ref_dir):
errors.append(f"❌ ref目录不存在: {ref_dir}")
elif not os.listdir(ref_dir):
errors.append(f"❌ ref目录为空: {ref_dir}")
# 检查dut目录
if not os.path.exists(dut_dir) or not os.path.isdir(dut_dir):
errors.append(f"❌ dut目录不存在: {dut_dir}")
elif not os.listdir(dut_dir):
errors.append(f"❌ dut目录为空: {dut_dir}")
if errors:
for error in errors:
logger.error(error)
sys.exit(1)
logger.info("✅ ref和dut目录检查通过")
return ref_dir, dut_dir
def create_excel_workbook(result_dir, project_name):
"""创建Excel工作簿"""
today = datetime.now().strftime("%Y%m%d")
excel_file = os.path.join(result_dir, f"{project_name}_{today}.xlsx")
wb = openpyxl.Workbook()
# 创建工作表
wb.create_sheet("comparison", 0) # 第一张表
wb.create_sheet("dut_bootimg", 1) # 第二张表
wb.create_sheet("ref_bootimg", 2) # 第三张表
# 删除默认创建的工作表
if "Sheet" in wb.sheetnames:
del wb["Sheet"]
wb.save(excel_file)
logger.info(f"📊 创建Excel工作簿: {excel_file}")
return excel_file
def find_and_extract_bugreport(directory):
"""查找并解压bugreport文件"""
# 查找bugreport开头的文件
bugreport_files = []
for f in os.listdir(directory):
if f.lower().startswith('bugreport'):
bugreport_files.append(f)
if not bugreport_files:
logger.error(f"❌ 在 {directory} 中未找到bugreport文件")
return None
# 使用第一个找到的bugreport文件
bugreport_file = os.path.join(directory, bugreport_files[0])
logger.info(f"🔍 找到bugreport文件: {bugreport_file}")
# 创建解压目录
extract_dir = os.path.join(directory, "extracted")
os.makedirs(extract_dir, exist_ok=True)
# 如果是ZIP文件则解压
if zipfile.is_zipfile(bugreport_file):
try:
with zipfile.ZipFile(bugreport_file, 'r') as zip_ref:
zip_ref.extractall(extract_dir)
logger.info(f"📦 解压文件到: {extract_dir}")
except zipfile.BadZipFile:
logger.error(f"❌ 文件不是有效的ZIP格式: {bugreport_file}")
return None
else:
# 如果不是ZIP文件,直接复制到解压目录
shutil.copy(bugreport_file, os.path.join(extract_dir, os.path.basename(bugreport_file)))
logger.info(f"📄 复制非压缩文件到: {extract_dir}")
# 在解压目录中查找bugreport文件
extracted_files = []
for root, _, files in os.walk(extract_dir):
for file in files:
if file.lower().startswith('bugreport'):
extracted_files.append(os.path.join(root, file))
if not extracted_files:
logger.error(f"❌ 在 {extract_dir} 中未找到解压后的bugreport文件")
return None
# 返回第一个匹配的文件
return extracted_files[0]
def load_keywords(platform, script_dir):
"""加载平台特定的关键字"""
platform_map = {
"MTK": "mtk_keyword.txt",
"QCOM": "qcom_keyword.txt",
"SPRD": "sprd_keyword.txt"
}
filename = platform_map.get(platform)
if not filename:
logger.error(f"❌ 不支持的平台: {platform}")
return None
keyword_file = os.path.join(script_dir, filename)
if not os.path.exists(keyword_file):
logger.error(f"❌ 关键字文件不存在: {keyword_file}")
return None
try:
with open(keyword_file, 'r') as f:
keywords = [line.strip() for line in f.readlines() if line.strip()]
except Exception as e:
logger.error(f"❌ 读取关键字文件错误: {e}")
return None
logger.info(f"📝 从 {keyword_file} 加载 {len(keywords)} 个关键字")
return keywords
def load_common_keywords(script_dir):
"""加载公共关键字"""
keyword_file = os.path.join(script_dir, "common_keyword.txt")
if not os.path.exists(keyword_file):
logger.error(f"❌ 公共关键字文件不存在: {keyword_file}")
return None
try:
with open(keyword_file, 'r') as f:
keywords = [line.strip() for line in f.readlines() if line.strip()]
except Exception as e:
logger.error(f"❌ 读取公共关键字文件错误: {e}")
return None
logger.info(f"📝 从 {keyword_file} 加载 {len(keywords)} 个公共关键字")
return keywords
def parse_timestamp(timestamp_str, base_year=datetime.now().year):
"""解析日志时间戳字符串为datetime对象"""
try:
# 格式: "03-04 03:33:58.101"
# 将时间戳字符串分割为日期和时间部分
parts = timestamp_str.split()
if len(parts) < 2:
return None
date_part = parts[0]
time_part = parts[1]
# 时间部分可能有毫秒,也可能没有
if '.' in time_part:
time_format = "%H:%M:%S.%f"
else:
time_format = "%H:%M:%S"
# 组合成完整字符串
full_str = f"{base_year}-{date_part} {time_part}"
dt = datetime.strptime(full_str, "%Y-%m-%d %H:%M:%S.%f" if '.' in time_part else "%Y-%m-%d %H:%M:%S")
return dt
except Exception as e:
logger.error(f"解析时间戳错误: {timestamp_str}, 错误: {e}")
return None
def extract_timings_from_file(file_path, keywords):
"""从文件中提取关键字对应的时间值,处理特殊关键字"""
results = {}
timestamps = {}
if not os.path.exists(file_path):
logger.error(f"❌ 文件不存在: {file_path}")
return results
logger.info(f"🔎 在 {os.path.basename(file_path)} 中搜索关键字...")
try:
# 尝试多种编码打开文件
for encoding in ['utf-8', 'latin-1', 'cp1252']:
try:
with open(file_path, 'r', encoding=encoding) as f:
lines = f.readlines()
break
except UnicodeDecodeError:
continue
else:
# 所有编码都失败,使用二进制模式
with open(file_path, 'rb') as f:
lines = f.read().decode('utf-8', errors='ignore').splitlines()
except Exception as e:
logger.error(f"❌ 读取文件错误: {e}")
return results
# 收集所有关键字的时间戳
for line in lines:
for keyword in keywords:
if keyword in line:
# 提取时间戳部分(行首的前两个部分)
parts = line.strip().split()
if len(parts) < 2:
continue
timestamp_str = f"{parts[0]} {parts[1]}"
timestamp = parse_timestamp(timestamp_str)
if timestamp:
timestamps[keyword] = timestamp
logger.debug(f" 找到 {keyword} 时间戳: {timestamp_str}")
# 处理特殊关键字
booting_timestamp = timestamps.get(BOOTING_KEY)
first_stage_timestamp = timestamps.get(FIRST_STAGE_KEY)
second_stage_timestamp = timestamps.get(SECOND_STAGE_KEY)
# 处理每个关键字
for keyword in keywords:
if keyword == BOOTING_KEY:
# Booting Linux 总是记录为0
results[keyword] = 0
logger.info(f" ✓ {BOOTING_KEY}: 设置为0 (特殊处理)")
elif keyword == FIRST_STAGE_KEY:
if booting_timestamp and first_stage_timestamp:
# 计算与Booting的时间差(毫秒)
delta = (first_stage_timestamp - booting_timestamp).total_seconds() * 1000
results[keyword] = delta
logger.info(f" ✓ {FIRST_STAGE_KEY}: 差值计算 = {delta:.2f} ms (相对于Booting Linux)")
else:
results[keyword] = None
logger.warning(f" ⚠ {FIRST_STAGE_KEY}: 未找到或无法计算差值")
elif keyword == SECOND_STAGE_KEY:
if first_stage_timestamp and second_stage_timestamp:
# 计算与First Stage的时间差(毫秒)
delta = (second_stage_timestamp - first_stage_timestamp).total_seconds() * 1000
results[keyword] = delta
logger.info(f" ✓ {SECOND_STAGE_KEY}: 差值计算 = {delta:.2f} ms (相对于init first stage)")
else:
results[keyword] = None
logger.warning(f" ⚠ {SECOND_STAGE_KEY}: 未找到或无法计算差值")
else:
# 普通关键字:尝试提取时间值
pattern = re.compile(rf"{re.escape(keyword)}.*?(\d+)", re.IGNORECASE)
match = None
for line in lines:
if keyword in line:
match = pattern.search(line)
if match:
break
if match:
try:
value = int(match.group(1))
results[keyword] = value
logger.info(f" ✓ {keyword}: {value} ms")
except (ValueError, IndexError):
results[keyword] = None
logger.warning(f" ⚠ {keyword}: 找到但无法解析数值")
else:
results[keyword] = None
logger.warning(f" ✗ {keyword}: 未找到")
return results
def write_to_excel_sheet(excel_file, sheet_name, data):
"""将数据写入Excel工作表"""
if not data:
logger.warning(f"⚠ 无数据可写入 {sheet_name} 工作表")
return
try:
wb = openpyxl.load_workbook(excel_file)
except Exception as e:
logger.error(f"❌ 加载Excel文件错误: {e}")
return
# 创建或选择工作表
if sheet_name in wb.sheetnames:
sheet = wb[sheet_name]
sheet.delete_rows(1, sheet.max_row) # 清空现有数据
else:
sheet = wb.create_sheet(sheet_name)
# 写入表头
sheet['A1'] = "关键字"
sheet['B1'] = "时间 (ms)"
# 写入数据
row = 2
for keyword, value in data.items():
sheet.cell(row=row, column=1, value=keyword)
sheet.cell(row=row, column=2, value=value if value is not None else "N/A")
row += 1
# 调整列宽
sheet.column_dimensions['A'].width = 40
sheet.column_dimensions['B'].width = 15
try:
wb.save(excel_file)
logger.info(f"📝 数据已写入 {sheet_name} 工作表")
except Exception as e:
logger.error(f"❌ 保存Excel文件错误: {e}")
def create_comparison_sheet(excel_file, dut_data, ref_data, keywords, same_platform=False):
"""创建比较工作表并计算差值,处理特殊关键字"""
try:
wb = openpyxl.load_workbook(excel_file)
except Exception as e:
logger.error(f"❌ 加载Excel文件错误: {e}")
return
# 创建或选择工作表
if "comparison" in wb.sheetnames:
sheet = wb["comparison"]
sheet.delete_rows(1, sheet.max_row) # 清空现有数据
else:
sheet = wb.create_sheet("comparison", 0)
# 设置表头
headers = [
"关键字",
"DUT值 (ms)",
"REF值 (ms)",
"DUT阶段时间 (ms)",
"REF阶段时间 (ms)",
"差值 (ms)"
]
for col, header in enumerate(headers, 1):
sheet.cell(row=1, column=col, value=header)
# 准备标红样式
red_fill = PatternFill(start_color="FFFF0000", end_color="FFFF0000", fill_type="solid")
# 初始化前值
prev_dut = 0
prev_ref = 0
# 处理每个关键字
for row_idx, keyword in enumerate(keywords, 2):
dut_value = dut_data.get(keyword)
ref_value = ref_data.get(keyword)
# 写入关键字和原始值
sheet.cell(row=row_idx, column=1, value=keyword)
sheet.cell(row=row_idx, column=2, value=dut_value if dut_value is not None else "N/A")
sheet.cell(row=row_idx, column=3, value=ref_value if ref_value is not None else "N/A")
# 计算阶段时间
dut_stage = None
ref_stage = None
diff = None
# 特殊处理:Booting Linux 总是0
if keyword == BOOTING_KEY:
sheet.cell(row=row_idx, column=4, value=0)
sheet.cell(row=row_idx, column=5, value=0)
sheet.cell(row=row_idx, column=6, value=0)
# 更新前值
prev_dut = dut_value if dut_value is not None else 0
prev_ref = ref_value if ref_value is not None else 0
continue
# 处理init first stage
if keyword == FIRST_STAGE_KEY:
if dut_value is not None:
dut_stage = dut_value # 已经是与Booting的差值
sheet.cell(row=row_idx, column=4, value=dut_stage)
prev_dut = dut_value # 更新为绝对时间
if ref_value is not None:
ref_stage = ref_value # 已经是与Booting的差值
sheet.cell(row=row_idx, column=5, value=ref_stage)
prev_ref = ref_value # 更新为绝对时间
# 处理init second stage
elif keyword == SECOND_STAGE_KEY:
if dut_value is not None:
dut_stage = dut_value # 已经是与first stage的差值
sheet.cell(row=row_idx, column=4, value=dut_stage)
prev_dut = dut_value # 更新为绝对时间
if ref_value is not None:
ref_stage = ref_value # 已经是与first stage的差值
sheet.cell(row=row_idx, column=5, value=ref_stage)
prev_ref = ref_value # 更新为绝对时间
# 处理普通关键字
else:
if dut_value is not None:
dut_stage = dut_value - prev_dut
sheet.cell(row=row_idx, column=4, value=dut_stage)
prev_dut = dut_value
if ref_value is not None:
ref_stage = ref_value - prev_ref
sheet.cell(row=row_idx, column=5, value=ref_stage)
prev_ref = ref_value
# 计算差值
if dut_stage is not None and ref_stage is not None:
diff = dut_stage - ref_stage
cell = sheet.cell(row=row_idx, column=6