455 lines
17 KiB
Python
455 lines
17 KiB
Python
import os
|
||
import tarfile
|
||
import shutil
|
||
import stat
|
||
import re
|
||
import gzip
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
from PyQt5.QtWidgets import QMessageBox
|
||
|
||
def show_error_message(parent, file_path):
|
||
"""显示文件格式错误提示弹窗"""
|
||
QMessageBox.critical(
|
||
parent,
|
||
"文件格式错误",
|
||
f"不支持的文件格式:\n{file_path}\n\n请上传.tar.gz格式的压缩包。"
|
||
)
|
||
|
||
def show_critical_message(parent, title, message):
|
||
"""显示通用错误提示弹窗"""
|
||
QMessageBox.critical(
|
||
parent,
|
||
title,
|
||
message
|
||
)
|
||
|
||
def show_info_message(parent, title, message):
|
||
"""显示信息提示弹窗"""
|
||
QMessageBox.information(
|
||
parent,
|
||
title,
|
||
message
|
||
)
|
||
|
||
def show_question_message(parent, title, message):
|
||
"""显示询问提示弹窗,返回用户选择(Yes/No)"""
|
||
return QMessageBox.question(
|
||
parent,
|
||
title,
|
||
message,
|
||
QMessageBox.Yes | QMessageBox.No,
|
||
QMessageBox.No
|
||
)
|
||
|
||
def get_app_cache_root():
|
||
current_file = os.path.abspath(__file__)
|
||
app_cache_root = os.path.dirname(os.path.dirname(current_file))
|
||
return app_cache_root
|
||
|
||
def get_project_root():
|
||
app_cache_root = get_app_cache_root()
|
||
return app_cache_root
|
||
|
||
def unzip_log(tar_path, extract_path='.'):
|
||
"""
|
||
解压 tar.gz 文件到指定目录
|
||
|
||
参数:
|
||
tar_path (str): tar.gz 文件的路径
|
||
extract_path (str): 解压目标目录,默认为当前目录
|
||
"""
|
||
try:
|
||
# 检查文件是否存在
|
||
if not os.path.exists(tar_path):
|
||
raise FileNotFoundError(f"文件不存在: {tar_path}")
|
||
|
||
# 创建解压目录(如果不存在)
|
||
os.makedirs(extract_path, exist_ok=True)
|
||
|
||
# 打开 tar.gz 文件并解压
|
||
with tarfile.open(tar_path, "r:gz") as tar:
|
||
# 列出所有文件(可选)
|
||
print(f"解压文件列表:")
|
||
for member in tar.getmembers():
|
||
print(f"- {member.name}")
|
||
|
||
# 解压所有文件到目标目录
|
||
tar.extractall(path=extract_path)
|
||
print(f"\n成功解压到: {os.path.abspath(extract_path)}")
|
||
|
||
except tarfile.TarError as e:
|
||
print(f"tar 文件处理错误: {e}")
|
||
except Exception as e:
|
||
print(f"解压失败: {e}")
|
||
|
||
def remove_readonly(func, path, excinfo):
|
||
"""用于处理删除只读文件的错误回调函数"""
|
||
# 尝试修改文件权限
|
||
os.chmod(path, stat.S_IWRITE)
|
||
# 再次尝试删除
|
||
func(path)
|
||
|
||
def clean_log_data(path):
|
||
"""删除目录,处理权限问题"""
|
||
if not os.path.exists(path):
|
||
print(f"目录不存在: {path}")
|
||
return
|
||
|
||
try:
|
||
# 方法1: 使用onerror回调处理权限问题
|
||
shutil.rmtree(path, onerror=remove_readonly)
|
||
print(f"成功删除目录: {path}")
|
||
|
||
except Exception as e:
|
||
print(f"删除目录时出错: {e}")
|
||
# 方法2: 先递归修改权限再删除(备选方案)
|
||
try:
|
||
# 递归修改目录权限
|
||
for root, dirs, files in os.walk(path):
|
||
for dir in dirs:
|
||
dir_path = os.path.join(root, dir)
|
||
os.chmod(dir_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
||
for file in files:
|
||
file_path = os.path.join(root, file)
|
||
os.chmod(file_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
|
||
|
||
# 修改权限后再次尝试删除
|
||
shutil.rmtree(path)
|
||
print(f"通过修改权限成功删除目录: {path}")
|
||
except Exception as e2:
|
||
print(f"修改权限后仍无法删除目录: {e2}")
|
||
|
||
def read_specific_line(file_path, line_number):
|
||
"""
|
||
读取文件中指定行的内容(行号从1开始)
|
||
"""
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8') as file:
|
||
for current_line, content in enumerate(file, 1): # 从1开始计数
|
||
if current_line == line_number:
|
||
return content.strip() # strip() 去除换行符和空格
|
||
# 若行号超出文件总行数,返回None
|
||
return None
|
||
except FileNotFoundError:
|
||
print(f"错误:文件 '{file_path}' 不存在")
|
||
return None
|
||
|
||
def append_to_file(file_path, content):
|
||
"""
|
||
以追加模式将字符串写入文件
|
||
|
||
参数:
|
||
file_path (str): 文件路径
|
||
content (str): 要写入的字符串内容
|
||
"""
|
||
try:
|
||
# 打开文件,模式为 'a'(追加),编码指定为 utf-8 以支持中文
|
||
with open(file_path, 'a', encoding='utf-8') as file:
|
||
# 写入内容(可根据需要添加换行符 '\n')
|
||
file.write(content + '\n') # 加 '\n' 使每次写入占一行
|
||
print(f"内容已成功追加到文件:{file_path}")
|
||
except Exception as e:
|
||
print(f"写入文件失败:{str(e)}")
|
||
|
||
def read_file_to_string(file_path):
|
||
"""
|
||
打开文件并将全部内容读取到一个字符串中
|
||
|
||
参数:
|
||
file_path (str): 要读取的文件路径
|
||
返回:
|
||
str: 文件内容字符串;若读取失败则返回 None
|
||
"""
|
||
try:
|
||
# 使用 with 语句打开文件(自动处理关闭)
|
||
# 'r' 表示只读模式,encoding='utf-8' 确保中文正常读取
|
||
with open(file_path, 'r', encoding='utf-8') as file:
|
||
# read() 方法读取全部内容并返回字符串
|
||
content = file.read()
|
||
return content
|
||
except FileNotFoundError:
|
||
print(f"错误:文件 '{file_path}' 不存在")
|
||
except PermissionError:
|
||
print(f"错误:没有权限读取文件 '{file_path}'")
|
||
except UnicodeDecodeError:
|
||
print(f"错误:文件 '{file_path}' 不是 UTF-8 编码,无法读取")
|
||
except Exception as e:
|
||
print(f"读取文件失败:{str(e)}")
|
||
return None
|
||
|
||
def merge_logrotate_files(source_path, num_files, output_path):
|
||
"""
|
||
合并由logrotate分割的文件
|
||
|
||
参数:
|
||
source_path (str): 原始文件路径(不包含.1, .2等后缀)
|
||
num_files (int): 要合并的文件数量(包括原始文件)
|
||
output_path (str): 合并后文件的输出路径
|
||
"""
|
||
if num_files < 1:
|
||
raise ValueError("文件数量必须至少为1")
|
||
|
||
# 构建要合并的文件列表
|
||
# 日志轮转文件通常按 .1(最新备份), .2(次新), ... 原始文件(最新)的顺序排列
|
||
files_to_merge = []
|
||
|
||
# 添加备份文件(从.1到.num_files-1)
|
||
for i in range(1, num_files):
|
||
backup_file = f"{source_path}.{i}"
|
||
if os.path.exists(backup_file):
|
||
files_to_merge.append(backup_file)
|
||
else:
|
||
print(f"警告: 备份文件 {backup_file} 不存在,已跳过")
|
||
|
||
# 添加原始文件(最新的日志)
|
||
if os.path.exists(source_path):
|
||
files_to_merge.append(source_path)
|
||
else:
|
||
raise FileNotFoundError(f"原始文件 {source_path} 不存在")
|
||
|
||
# 如果找到的文件少于要求的数量,给出警告
|
||
if len(files_to_merge) < num_files:
|
||
print(f"警告: 只找到 {len(files_to_merge)} 个文件,而不是要求的 {num_files} 个")
|
||
|
||
# 合并文件
|
||
with open(output_path, 'w') as outfile:
|
||
for file_path in files_to_merge:
|
||
try:
|
||
with open(file_path, 'r') as infile:
|
||
# 读取并写入文件内容
|
||
outfile.write(infile.read())
|
||
# 在文件之间添加一个换行,避免内容粘连
|
||
outfile.write('\n')
|
||
print(f"已合并: {file_path}")
|
||
except Exception as e:
|
||
print(f"合并文件 {file_path} 时出错: {str(e)}")
|
||
|
||
print(f"所有文件已合并至: {output_path}")
|
||
|
||
def get_nth_integer_after_line(file_path, target_string, n=1):
|
||
"""
|
||
打开文件,找到包含目标字符串的行,读取下一行并提取第n个整数
|
||
|
||
参数:
|
||
file_path: 文件路径
|
||
target_string: 要查找的目标字符串
|
||
n: 要返回的第几个整数(从1开始计数)
|
||
|
||
返回:
|
||
找到的第n个整数,如果未找到则返回None
|
||
"""
|
||
if n < 1:
|
||
print("错误:n必须是大于等于1的整数")
|
||
return None
|
||
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8') as file:
|
||
# 逐行读取文件
|
||
for line in file:
|
||
# 检查当前行是否包含目标字符串
|
||
if target_string in line:
|
||
# 读取下一行
|
||
next_line = next(file, None)
|
||
if next_line is None:
|
||
print("目标字符串所在行为文件最后一行,没有下一行")
|
||
return None
|
||
|
||
# 处理下一行,提取所有整数
|
||
integers = []
|
||
# 分割成单词,尝试转换为整数
|
||
words = next_line.strip().split()
|
||
for word in words:
|
||
# 清理单词,保留数字和负号
|
||
cleaned_word = ''.join(filter(lambda c: c.isdigit() or c == '-', word))
|
||
if cleaned_word: # 确保清理后不为空
|
||
try:
|
||
num = int(cleaned_word)
|
||
integers.append(num)
|
||
except ValueError:
|
||
continue
|
||
|
||
# 检查是否有足够的整数
|
||
if len(integers) >= n:
|
||
return integers[n-1] # 因为列表是0索引,所以n-1
|
||
else:
|
||
print(f"下一行中只找到 {len(integers)} 个整数,无法返回第 {n} 个")
|
||
return None
|
||
|
||
# 如果遍历完文件都没找到目标字符串
|
||
print(f"文件中未找到包含 '{target_string}' 的行")
|
||
return None
|
||
|
||
except FileNotFoundError:
|
||
print(f"错误:文件 '{file_path}' 不存在")
|
||
return None
|
||
except Exception as e:
|
||
print(f"处理文件时发生错误:{str(e)}")
|
||
return None
|
||
|
||
def extract_first_second_level_timestamp(text):
|
||
"""
|
||
提取字符串中第一个不带时区的秒级时间戳(格式:YYYY-MM-DDTHH:MM:SS)
|
||
并返回标准ISO格式字符串
|
||
|
||
参数:
|
||
text (str): 包含时间戳的原始字符串
|
||
|
||
返回:
|
||
str: 提取到的ISO格式时间戳,未找到则返回None
|
||
"""
|
||
# 正则表达式匹配不带时区的秒级时间戳(YYYY-MM-DDTHH:MM:SS)
|
||
# 严格匹配日期和时间的数字范围(如月份1-12,日期1-31等)
|
||
timestamp_pattern = (
|
||
r'\b\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])' # 日期部分 YYYY-MM-DD
|
||
r'T([01]\d|2[0-3]):([0-5]\d):([0-5]\d)\b' # 时间部分 THH:MM:SS
|
||
)
|
||
|
||
# 查找第一个匹配的时间戳
|
||
match = re.search(timestamp_pattern, text)
|
||
|
||
if match:
|
||
timestamp_str = match.group()
|
||
|
||
try:
|
||
# 解析为datetime对象(不带时区)
|
||
dt = datetime.strptime(timestamp_str, "%Y-%m-%dT%H:%M:%S")
|
||
# 返回ISO格式(秒级)
|
||
return dt.isoformat()
|
||
except ValueError:
|
||
# 理论上正则已过滤无效格式,此处作为兜底
|
||
return timestamp_str
|
||
|
||
# 未找到匹配的时间戳
|
||
return None
|
||
|
||
def is_full_word_present(text, word):
|
||
"""
|
||
判断text中是否包含完整的word(全词匹配,大小写敏感)
|
||
|
||
参数:
|
||
text (str): 待检查的字符串
|
||
word (str): 要匹配的完整单词
|
||
|
||
返回:
|
||
bool: 存在全词匹配返回True,否则返回False
|
||
"""
|
||
# 使用正则表达式元字符定义单词边界,确保全词匹配
|
||
# re.escape()用于转义word中的特殊字符
|
||
pattern = r'\b' + re.escape(word) + r'\b'
|
||
|
||
# 搜索匹配(大小写敏感)
|
||
match = re.search(pattern, text)
|
||
|
||
return bool(match)
|
||
|
||
def parse_idllog_line(log_line):
|
||
"""
|
||
解析特定格式的日志行,提取关键信息
|
||
|
||
参数:
|
||
log_line (str): 要解析的日志行字符串
|
||
|
||
返回:
|
||
dict: 包含提取的信息的字典,若解析失败则返回None
|
||
"""
|
||
try:
|
||
# 处理开头的 <162> 部分,先移除这部分内容
|
||
# 找到第一个空格,跳过 <162> 部分
|
||
first_space_index = log_line.find(' ')
|
||
if first_space_index == -1:
|
||
return None
|
||
content_after_prefix = log_line[first_space_index:].strip()
|
||
# 从剩余内容中提取第一个时间戳(到下一个空格)
|
||
timestamp_end = content_after_prefix.find(' ')
|
||
if timestamp_end == -1:
|
||
return None
|
||
timestamp = content_after_prefix[:timestamp_end].strip()
|
||
|
||
# 查找包含|分隔符的部分
|
||
pipe_start = log_line.find('|')
|
||
if pipe_start == -1:
|
||
return None
|
||
pipe_content = log_line[pipe_start:]
|
||
|
||
# 按|分割内容
|
||
parts = [part.strip() for part in pipe_content.split('|') if part.strip()]
|
||
|
||
# 检查是否有足够的部分
|
||
if len(parts) < 5:
|
||
return None
|
||
|
||
# 提取各部分信息
|
||
component_type = parts[1] # 部件类型
|
||
event_type = parts[2] # 事件类型
|
||
event_level = parts[3] # 事件等级
|
||
event_code = parts[4] # 事件代码
|
||
|
||
# 事件描述是剩余部分的组合
|
||
event_description = '|'.join(parts[5:]) if len(parts) > 5 else ""
|
||
|
||
# 提取第一个单词(以空格为分隔符)
|
||
sensor = event_description.split()[0]
|
||
|
||
return {
|
||
'timestamp': timestamp,
|
||
'component_type': component_type,
|
||
'event_type': event_type,
|
||
'event_level': event_level,
|
||
'event_code': event_code,
|
||
'sensor' : sensor,
|
||
'description': event_description
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f"解析日志行时出错: {str(e)}")
|
||
return None
|
||
|
||
def extract_maintenancelog_gz_files(input_dir, output_dir, max_files=10):
|
||
"""
|
||
解压指定目录下的maintenancelog.1.gz到log.max_files.gz文件到目标目录
|
||
|
||
参数:
|
||
input_dir: 压缩文件所在的目录路径
|
||
output_dir: 解压后文件的保存目录路径
|
||
max_files: 最大文件编号,默认为10
|
||
"""
|
||
# 确保输入输出目录存在
|
||
input_path = Path(input_dir)
|
||
output_path = Path(output_dir)
|
||
|
||
# 创建输出目录(如果不存在)
|
||
output_path.mkdir(parents=True, exist_ok=True)
|
||
|
||
# 检查输入目录是否存在
|
||
if not input_path.exists() or not input_path.is_dir():
|
||
print(f"错误:输入目录 '{input_path}' 不存在或不是一个目录")
|
||
return
|
||
|
||
for i in range(1, max_files + 1):
|
||
# 压缩文件路径
|
||
gz_filename = input_path / f"maintenance.log.{i}.gz"
|
||
|
||
# 检查文件是否存在
|
||
if not gz_filename.exists() or not gz_filename.is_file():
|
||
print(f"文件 {gz_filename} 不存在,跳过")
|
||
continue
|
||
|
||
# 解压后的文件路径
|
||
output_filename = output_path / f"maintenance.log.{i}"
|
||
|
||
try:
|
||
# 打开压缩文件并解压
|
||
with gzip.open(gz_filename, 'rb') as f_in:
|
||
with open(output_filename, 'wb') as f_out:
|
||
# 分块读取写入,处理大文件更高效
|
||
while True:
|
||
chunk = f_in.read(1024 * 1024) # 1MB块
|
||
if not chunk:
|
||
break
|
||
f_out.write(chunk)
|
||
|
||
print(f"成功解压: {gz_filename} -> {output_filename}")
|
||
|
||
except Exception as e:
|
||
print(f"解压 {gz_filename} 时出错: {str(e)}") |