diff --git a/App.config b/App.config
new file mode 100644
index 0000000..56efbc7
--- /dev/null
+++ b/App.config
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/App.xaml b/App.xaml
new file mode 100644
index 0000000..b79b2e7
--- /dev/null
+++ b/App.xaml
@@ -0,0 +1,9 @@
+
+
+
+
+
diff --git a/App.xaml.cs b/App.xaml.cs
new file mode 100644
index 0000000..352b433
--- /dev/null
+++ b/App.xaml.cs
@@ -0,0 +1,17 @@
+using System;
+using System.Collections.Generic;
+using System.Configuration;
+using System.Data;
+using System.Linq;
+using System.Threading.Tasks;
+using System.Windows;
+
+namespace LogAnalyzer
+{
+ ///
+ /// App.xaml 的交互逻辑
+ ///
+ public partial class App : Application
+ {
+ }
+}
diff --git a/LogAnalyzer.csproj b/LogAnalyzer.csproj
new file mode 100644
index 0000000..5fdeba4
--- /dev/null
+++ b/LogAnalyzer.csproj
@@ -0,0 +1,99 @@
+
+
+
+
+ Debug
+ AnyCPU
+ {0B886B0C-D80A-4AF0-A670-4FFD5FAE70FD}
+ WinExe
+ LogAnalyzer
+ LogAnalyzer
+ v4.7.2
+ 512
+ {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
+ 4
+ true
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+
+
+
+
+ 4.0
+
+
+
+
+
+
+
+ MSBuild:Compile
+ Designer
+
+
+ MSBuild:Compile
+ Designer
+
+
+ App.xaml
+ Code
+
+
+
+ MainWindow.xaml
+ Code
+
+
+
+
+ Code
+
+
+ True
+ True
+ Resources.resx
+
+
+ True
+ Settings.settings
+ True
+
+
+ ResXFileCodeGenerator
+ Resources.Designer.cs
+
+
+ SettingsSingleFileGenerator
+ Settings.Designer.cs
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/LogAnalyzer.slnx b/LogAnalyzer.slnx
new file mode 100644
index 0000000..737f5fe
--- /dev/null
+++ b/LogAnalyzer.slnx
@@ -0,0 +1,3 @@
+
+
+
diff --git a/LogParserService.cs b/LogParserService.cs
new file mode 100644
index 0000000..5a13428
--- /dev/null
+++ b/LogParserService.cs
@@ -0,0 +1,840 @@
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace LogAnalyzer
+{
+ public class LogParserService
+ {
+ // 匹配行首的时间戳格式: 2026/7/17 2:46:38
+ // 使用 ^ 确保匹配行首
+ private readonly Regex _timestampRegex = new Regex(
+ @"^(?\d{4}/\d{1,2}/\d{1,2}\s+\d{1,2}:\d{1,2}:\d{1,2})\s*(?.*)$",
+ RegexOptions.Compiled
+ );
+
+ ///
+ /// 解析日志文件,以行首时间戳作为分隔符合并多行日志
+ ///
+ public DataTable ParseLogFile(string filePath)
+ {
+ return ParseLogFile(filePath, Encoding.Default);
+ }
+
+ ///
+ /// 解析日志文件(支持指定编码)
+ ///
+ public DataTable ParseLogFile(string filePath, Encoding encoding)
+ {
+ if (!File.Exists(filePath))
+ throw new FileNotFoundException($"日志文件未找到: {filePath}", filePath);
+
+ var logTable = CreateLogTable();
+
+ try
+ {
+ using (var reader = new StreamReader(filePath, encoding))
+ {
+ string line;
+ string currentTimestamp = null;
+ StringBuilder currentMessage = new StringBuilder();
+ bool hasContent = false;
+
+ while ((line = reader.ReadLine()) != null)
+ {
+ // 检查行首是否匹配时间戳格式
+ var match = _timestampRegex.Match(line);
+
+ if (match.Success)
+ {
+ // 遇到新的时间戳,保存之前的条目
+ if (hasContent && currentTimestamp != null)
+ {
+ AddLogEntry(logTable, currentTimestamp, currentMessage.ToString());
+ }
+
+ // 开始新的条目
+ currentTimestamp = match.Groups["datetime"].Value;
+ string messagePart = match.Groups["message"].Value.Trim();
+ currentMessage.Clear();
+
+ if (!string.IsNullOrEmpty(messagePart))
+ {
+ currentMessage.Append(messagePart);
+ }
+
+ hasContent = true;
+ }
+ else
+ {
+ // 不是时间戳行,追加到当前消息
+ if (hasContent)
+ {
+ if (currentMessage.Length > 0)
+ {
+ currentMessage.Append(Environment.NewLine);
+ }
+ currentMessage.Append(line.TrimEnd());
+ }
+ else
+ {
+ // 如果文件开头没有时间戳,将整行作为特殊处理
+ // 这里可以忽略或作为独立条目
+ if (!string.IsNullOrWhiteSpace(line))
+ {
+ logTable.Rows.Add("未知时间", line.Trim());
+ }
+ }
+ }
+ }
+
+ // 保存最后一个条目
+ if (hasContent && currentTimestamp != null)
+ {
+ AddLogEntry(logTable, currentTimestamp, currentMessage.ToString());
+ }
+ }
+ }
+ catch (IOException ex)
+ {
+ throw new InvalidOperationException($"读取日志文件失败: {ex.Message}", ex);
+ }
+
+ return logTable;
+ }
+
+ ///
+ /// 添加日志条目到 DataTable
+ ///
+ private void AddLogEntry(DataTable table, string timestamp, string message)
+ {
+ // 去除多余的空行
+ string cleanMessage = message.Trim();
+ if (!string.IsNullOrEmpty(cleanMessage))
+ {
+ table.Rows.Add(timestamp, cleanMessage);
+ }
+ }
+
+ ///
+ /// 创建日志 DataTable 结构
+ ///
+ private DataTable CreateLogTable()
+ {
+ var table = new DataTable("LogData");
+ table.Columns.Add("时间戳", typeof(string));
+ table.Columns.Add("日志内容", typeof(string));
+ return table;
+ }
+
+ ///
+ /// 解析日志并显示详细信息(用于调试)
+ ///
+ public void ParseAndDisplay(string filePath)
+ {
+ var logData = ParseLogFile(filePath);
+
+ Console.WriteLine($"总共解析了 {logData.Rows.Count} 条日志条目");
+ Console.WriteLine(new string('=', 80));
+
+ int index = 1;
+ foreach (DataRow row in logData.Rows)
+ {
+ string timestamp = row["时间戳"]?.ToString() ?? "";
+ string message = row["日志内容"]?.ToString() ?? "";
+
+ Console.WriteLine($"条目 {index}:");
+ Console.WriteLine($"时间: {timestamp}");
+ Console.WriteLine($"内容:");
+ Console.WriteLine(message);
+ Console.WriteLine(new string('-', 80));
+ Console.WriteLine();
+
+ index++;
+ }
+ }
+
+ ///
+ /// 检查特定日志文件,显示每行的解析情况(调试用)
+ ///
+ public void DebugParse(string filePath)
+ {
+ if (!File.Exists(filePath))
+ {
+ Console.WriteLine($"文件不存在: {filePath}");
+ return;
+ }
+
+ Console.WriteLine("逐行解析调试:");
+ Console.WriteLine(new string('=', 80));
+
+ using (var reader = new StreamReader(filePath, Encoding.Default))
+ {
+ string line;
+ int lineNumber = 0;
+
+ while ((line = reader.ReadLine()) != null)
+ {
+ lineNumber++;
+ var match = _timestampRegex.Match(line);
+
+ Console.WriteLine($"行 {lineNumber}:");
+ Console.WriteLine($" 原始: {line}");
+
+ if (match.Success)
+ {
+ Console.WriteLine($" 识别为时间戳: {match.Groups["datetime"].Value}");
+ Console.WriteLine($" 消息部分: {match.Groups["message"].Value}");
+ }
+ else
+ {
+ Console.WriteLine($" 不是时间戳行(作为多行内容的一部分)");
+ }
+ Console.WriteLine();
+ }
+ }
+ }
+
+ ///
+ /// 从日志中提取包含数据库指令关键词的条目及其前后N条日志
+ ///
+ /// 原始日志DataTable
+ /// 前后文行数,默认为3
+ /// 包含数据库指令及其上下文的DataTable
+ public DataTable ExtractDatabaseCommandsWithContext(DataTable logTable, int contextLines = 3)
+ {
+ if (logTable == null || logTable.Rows.Count == 0)
+ throw new ArgumentException("日志数据为空", nameof(logTable));
+
+ // 创建结果表
+ var resultTable = new DataTable("DbCommandContext");
+ resultTable.Columns.Add("时间戳", typeof(string));
+ resultTable.Columns.Add("日志内容", typeof(string));
+ resultTable.Columns.Add("是否数据库指令", typeof(bool));
+ resultTable.Columns.Add("条目索引", typeof(int));
+ resultTable.Columns.Add("上下文类型", typeof(string));
+ resultTable.Columns.Add("匹配关键词", typeof(string)); // 记录匹配到的关键词
+
+ // 1. 查找所有包含数据库指令关键词的条目索引
+ var dbCommandIndices = new List();
+ var keywordMatches = new Dictionary(); // 记录每个条目匹配的关键词
+
+ for (int i = 0; i < logTable.Rows.Count; i++)
+ {
+ string message = logTable.Rows[i]["日志内容"]?.ToString() ?? "";
+ string matchedKeyword = ContainsDbKeyword(message);
+
+ if (!string.IsNullOrEmpty(matchedKeyword))
+ {
+ dbCommandIndices.Add(i);
+ keywordMatches[i] = matchedKeyword;
+ }
+ }
+
+ if (dbCommandIndices.Count == 0)
+ {
+ // 如果没有找到数据库指令,返回空表
+ return resultTable;
+ }
+
+ // 2. 收集需要提取的索引范围
+ var indicesToExtract = new HashSet();
+ foreach (int dbIndex in dbCommandIndices)
+ {
+ // 添加数据库指令本身
+ indicesToExtract.Add(dbIndex);
+
+ // 添加前N条
+ for (int i = 1; i <= contextLines; i++)
+ {
+ int prevIndex = dbIndex - i;
+ if (prevIndex >= 0)
+ indicesToExtract.Add(prevIndex);
+ }
+
+ // 添加后N条
+ for (int i = 1; i <= contextLines; i++)
+ {
+ int nextIndex = dbIndex + i;
+ if (nextIndex < logTable.Rows.Count)
+ indicesToExtract.Add(nextIndex);
+ }
+ }
+
+ // 3. 按索引排序并添加到结果表
+ var sortedIndices = indicesToExtract.OrderBy(idx => idx).ToList();
+
+ foreach (int idx in sortedIndices)
+ {
+ DataRow originalRow = logTable.Rows[idx];
+ string message = originalRow["日志内容"]?.ToString() ?? "";
+ bool isDbCommand = dbCommandIndices.Contains(idx);
+ string matchedKeyword = isDbCommand ? keywordMatches[idx] : "";
+
+ // 确定上下文类型
+ string contextType;
+ if (isDbCommand)
+ contextType = "数据库指令";
+ else
+ {
+ // 判断是前文还是后文
+ bool isBeforeDb = dbCommandIndices.Any(dbIdx => dbIdx > idx);
+ bool isAfterDb = dbCommandIndices.Any(dbIdx => dbIdx < idx);
+
+ if (isBeforeDb && !isAfterDb)
+ contextType = "指令前文";
+ else if (isAfterDb && !isBeforeDb)
+ contextType = "指令后文";
+ else if (isBeforeDb && isAfterDb)
+ contextType = "上下文(指令之间)";
+ else
+ contextType = "上下文";
+ }
+
+ resultTable.Rows.Add(
+ originalRow["时间戳"]?.ToString() ?? "",
+ message,
+ isDbCommand,
+ idx,
+ contextType,
+ matchedKeyword
+ );
+ }
+
+ return resultTable;
+ }
+ public DataTable ExtractTank1CommandsWithContext(DataTable logTable)
+ {
+ // 1. 参数校验
+ if (logTable == null || logTable.Rows.Count == 0)
+ throw new ArgumentException("日志数据为空", nameof(logTable));
+
+ // 2. 创建结果表,保持与输入表相同的结构(或按需自定义)
+ // var resultTable = logTable.Clone(); // Clone() 会复制表结构,但不复制数据
+
+ var matchedRows = new List();
+
+ // 3. 遍历原始表,匹配关键字
+ foreach (DataRow row in logTable.Rows)
+ {
+ string message = row["日志内容"]?.ToString() ?? "";
+
+ // 同时包含 "M1T1" 和 "T1执行" 才匹配(如需匹配其一,请将 && 改为 ||)
+ if (message.Contains("M1T1") || message.Contains("T1执行") ||
+ message.Contains(" T1") || message.Contains(",T1") || message.Contains(",401)"))
+ {
+ matchedRows.Add(row);
+ }
+ }
+
+ var finalRows = new List();
+ if (matchedRows.Count > 0)
+ {
+ int startIdx = 0;
+ string currentContent = matchedRows[0]["日志内容"]?.ToString() ?? "";
+
+ for (int i = 1; i < matchedRows.Count; i++)
+ {
+ string nextContent = matchedRows[i]["日志内容"]?.ToString() ?? "";
+
+ // 如果下一行的内容与当前块的内容不同
+ if (nextContent != currentContent)
+ {
+ // 将当前块的【开始行】加入结果
+ finalRows.Add(matchedRows[startIdx]);
+
+ // 如果当前块有连续重复(即 i - startIdx > 1),将【结束行】加入结果
+ if (i - startIdx > 1)
+ {
+ finalRows.Add(matchedRows[i - 1]);
+ }
+
+ // 重置状态,开始新的块
+ startIdx = i;
+ currentContent = nextContent;
+ }
+ }
+
+ // 处理最后一个块(循环结束后别忘了处理尾部)
+ finalRows.Add(matchedRows[startIdx]);
+ if (matchedRows.Count - startIdx > 1)
+ {
+ finalRows.Add(matchedRows[matchedRows.Count - 1]);
+ }
+ }
+
+ // 3. 构建并返回结果表
+ var resultTable = logTable.Clone();
+ foreach (var row in finalRows)
+ {
+ resultTable.ImportRow(row);
+ }
+
+ return resultTable;
+ }
+ public DataTable ExtractTank2CommandsWithContext(DataTable logTable)
+ {
+ // 1. 参数校验
+ if (logTable == null || logTable.Rows.Count == 0)
+ throw new ArgumentException("日志数据为空", nameof(logTable));
+
+ var matchedRows = new List();
+
+ // 3. 遍历原始表,匹配关键字
+ foreach (DataRow row in logTable.Rows)
+ {
+ string message = row["日志内容"]?.ToString() ?? "";
+
+ // 同时包含 "M1T1" 和 "T1执行" 才匹配(如需匹配其一,请将 && 改为 ||)
+ if (message.Contains("M1T2") || message.Contains("T2执行") ||
+ message.Contains(" T2") || message.Contains(",T2") || message.Contains(",402)"))
+ {
+ // 将匹配到的行导入结果表
+ matchedRows.Add(row);
+ }
+ }
+
+ var finalRows = new List();
+ if (matchedRows.Count > 0)
+ {
+ int startIdx = 0;
+ string currentContent = matchedRows[0]["日志内容"]?.ToString() ?? "";
+
+ for (int i = 1; i < matchedRows.Count; i++)
+ {
+ string nextContent = matchedRows[i]["日志内容"]?.ToString() ?? "";
+
+ // 如果下一行的内容与当前块的内容不同
+ if (nextContent != currentContent)
+ {
+ // 将当前块的【开始行】加入结果
+ finalRows.Add(matchedRows[startIdx]);
+
+ // 如果当前块有连续重复(即 i - startIdx > 1),将【结束行】加入结果
+ if (i - startIdx > 1)
+ {
+ finalRows.Add(matchedRows[i - 1]);
+ }
+
+ // 重置状态,开始新的块
+ startIdx = i;
+ currentContent = nextContent;
+ }
+ }
+
+ // 处理最后一个块(循环结束后别忘了处理尾部)
+ finalRows.Add(matchedRows[startIdx]);
+ if (matchedRows.Count - startIdx > 1)
+ {
+ finalRows.Add(matchedRows[matchedRows.Count - 1]);
+ }
+ }
+
+ // 3. 构建并返回结果表
+ var resultTable = logTable.Clone();
+ foreach (var row in finalRows)
+ {
+ resultTable.ImportRow(row);
+ }
+
+ return resultTable;
+ }
+ public DataTable ExtractTank3CommandsWithContext(DataTable logTable)
+ {
+ // 1. 参数校验
+ if (logTable == null || logTable.Rows.Count == 0)
+ throw new ArgumentException("日志数据为空", nameof(logTable));
+
+ var matchedRows = new List();
+
+ // 3. 遍历原始表,匹配关键字
+ foreach (DataRow row in logTable.Rows)
+ {
+ string message = row["日志内容"]?.ToString() ?? "";
+
+ // 同时包含 "M1T1" 和 "T1执行" 才匹配(如需匹配其一,请将 && 改为 ||)
+ if (message.Contains("M2T1") || message.Contains("T3执行") ||
+ message.Contains(" T3") || message.Contains(",T3") || message.Contains(",403)"))
+ {
+ // 将匹配到的行导入结果表
+ matchedRows.Add(row);
+ }
+ }
+
+ var finalRows = new List();
+ if (matchedRows.Count > 0)
+ {
+ int startIdx = 0;
+ string currentContent = matchedRows[0]["日志内容"]?.ToString() ?? "";
+
+ for (int i = 1; i < matchedRows.Count; i++)
+ {
+ string nextContent = matchedRows[i]["日志内容"]?.ToString() ?? "";
+
+ // 如果下一行的内容与当前块的内容不同
+ if (nextContent != currentContent)
+ {
+ // 将当前块的【开始行】加入结果
+ finalRows.Add(matchedRows[startIdx]);
+
+ // 如果当前块有连续重复(即 i - startIdx > 1),将【结束行】加入结果
+ if (i - startIdx > 1)
+ {
+ finalRows.Add(matchedRows[i - 1]);
+ }
+
+ // 重置状态,开始新的块
+ startIdx = i;
+ currentContent = nextContent;
+ }
+ }
+
+ // 处理最后一个块(循环结束后别忘了处理尾部)
+ finalRows.Add(matchedRows[startIdx]);
+ if (matchedRows.Count - startIdx > 1)
+ {
+ finalRows.Add(matchedRows[matchedRows.Count - 1]);
+ }
+ }
+
+ // 3. 构建并返回结果表
+ var resultTable = logTable.Clone();
+ foreach (var row in finalRows)
+ {
+ resultTable.ImportRow(row);
+ }
+
+ return resultTable;
+ }
+ public DataTable ExtractTank4CommandsWithContext(DataTable logTable)
+ {
+ // 1. 参数校验
+ if (logTable == null || logTable.Rows.Count == 0)
+ throw new ArgumentException("日志数据为空", nameof(logTable));
+
+ var matchedRows = new List();
+
+ // 3. 遍历原始表,匹配关键字
+ foreach (DataRow row in logTable.Rows)
+ {
+ string message = row["日志内容"]?.ToString() ?? "";
+
+ // 同时包含 "M1T1" 和 "T1执行" 才匹配(如需匹配其一,请将 && 改为 ||)
+ if (message.Contains("M2T2") || message.Contains("T4执行") ||
+ message.Contains(" T4") || message.Contains(",T4") || message.Contains(",404)"))
+ {
+ // 将匹配到的行导入结果表
+ matchedRows.Add(row);
+ }
+ }
+
+ var finalRows = new List();
+ if (matchedRows.Count > 0)
+ {
+ int startIdx = 0;
+ string currentContent = matchedRows[0]["日志内容"]?.ToString() ?? "";
+
+ for (int i = 1; i < matchedRows.Count; i++)
+ {
+ string nextContent = matchedRows[i]["日志内容"]?.ToString() ?? "";
+
+ // 如果下一行的内容与当前块的内容不同
+ if (nextContent != currentContent)
+ {
+ // 将当前块的【开始行】加入结果
+ finalRows.Add(matchedRows[startIdx]);
+
+ // 如果当前块有连续重复(即 i - startIdx > 1),将【结束行】加入结果
+ if (i - startIdx > 1)
+ {
+ finalRows.Add(matchedRows[i - 1]);
+ }
+
+ // 重置状态,开始新的块
+ startIdx = i;
+ currentContent = nextContent;
+ }
+ }
+
+ // 处理最后一个块(循环结束后别忘了处理尾部)
+ finalRows.Add(matchedRows[startIdx]);
+ if (matchedRows.Count - startIdx > 1)
+ {
+ finalRows.Add(matchedRows[matchedRows.Count - 1]);
+ }
+ }
+
+ // 3. 构建并返回结果表
+ var resultTable = logTable.Clone();
+ foreach (var row in finalRows)
+ {
+ resultTable.ImportRow(row);
+ }
+
+ return resultTable;
+ }
+ ///
+ /// 检查日志内容是否包含数据库指令关键词,返回匹配的关键词
+ ///
+ private string ContainsDbKeyword(string message)
+ {
+ if (string.IsNullOrWhiteSpace(message))
+ return null;
+
+ // 数据库指令关键词列表(不区分大小写)
+ string[] dbKeywords = {
+ "SELECT", "INSERT", "UPDATE", "DELETE", "CREATE", "ALTER",
+ "DROP", "TRUNCATE", "MERGE", "EXEC", "EXECUTE",
+ "FROM", "WHERE", "JOIN", "INNER", "LEFT", "RIGHT", "FULL", "OUTER",
+ "UNION", "INTERSECT", "EXCEPT", "VALUES", "SET", "WITH",
+ "AS", "ON", "AND", "OR", "NOT", "IN", "LIKE", "BETWEEN",
+ "BEGIN", "COMMIT", "ROLLBACK", "GRANT", "REVOKE",
+ "PROCEDURE", "FUNCTION", "TRIGGER", "INDEX", "VIEW"
+ };
+
+ string upperMessage = message.ToUpper();
+
+ // 检查是否包含任何关键词
+ foreach (string keyword in dbKeywords)
+ {
+ // 使用正则表达式确保关键词是独立的单词
+ string pattern = $@"\b{keyword}\b";
+ if (Regex.IsMatch(upperMessage, pattern, RegexOptions.IgnoreCase))
+ {
+ return keyword;
+ }
+ }
+
+ return null;
+ }
+
+ ///
+ /// 提取数据库指令及其上下文(分组版本)
+ ///
+ public DataTable ExtractDbCommandsGrouped(DataTable logTable, int contextLines = 3)
+ {
+ if (logTable == null || logTable.Rows.Count == 0)
+ throw new ArgumentException("日志数据为空", nameof(logTable));
+
+ var resultTable = new DataTable("DbCommandGrouped");
+ resultTable.Columns.Add("组ID", typeof(int));
+ resultTable.Columns.Add("时间戳", typeof(string));
+ resultTable.Columns.Add("日志内容", typeof(string));
+ resultTable.Columns.Add("类型", typeof(string));
+ resultTable.Columns.Add("行号", typeof(int));
+ resultTable.Columns.Add("匹配关键词", typeof(string));
+
+ // 查找所有包含数据库指令关键词的条目索引
+ var dbIndices = new List();
+ var keywordMap = new Dictionary();
+
+ for (int i = 0; i < logTable.Rows.Count; i++)
+ {
+ string message = logTable.Rows[i]["日志内容"]?.ToString() ?? "";
+ string matchedKeyword = ContainsDbKeyword(message);
+
+ if (!string.IsNullOrEmpty(matchedKeyword))
+ {
+ dbIndices.Add(i);
+ keywordMap[i] = matchedKeyword;
+ }
+ }
+
+ if (dbIndices.Count == 0)
+ return resultTable;
+
+ int groupId = 0;
+ var processedIndices = new HashSet();
+
+ foreach (int dbIndex in dbIndices)
+ {
+ if (processedIndices.Contains(dbIndex))
+ continue;
+
+ groupId++;
+ var indicesInGroup = new List();
+
+ // 添加数据库指令本身
+ indicesInGroup.Add(dbIndex);
+ processedIndices.Add(dbIndex);
+
+ // 添加前N条
+ for (int i = 1; i <= contextLines; i++)
+ {
+ int prevIndex = dbIndex - i;
+ if (prevIndex >= 0 && !processedIndices.Contains(prevIndex))
+ {
+ indicesInGroup.Add(prevIndex);
+ processedIndices.Add(prevIndex);
+ }
+ }
+
+ // 添加后N条
+ for (int i = 1; i <= contextLines; i++)
+ {
+ int nextIndex = dbIndex + i;
+ if (nextIndex < logTable.Rows.Count && !processedIndices.Contains(nextIndex))
+ {
+ indicesInGroup.Add(nextIndex);
+ processedIndices.Add(nextIndex);
+ }
+ }
+
+ // 按索引排序
+ indicesInGroup.Sort();
+
+ // 添加到结果表
+ foreach (int idx in indicesInGroup)
+ {
+ DataRow originalRow = logTable.Rows[idx];
+ string message = originalRow["日志内容"]?.ToString() ?? "";
+ bool isDb = idx == dbIndex || dbIndices.Contains(idx);
+ string matchedKeyword = isDb ? keywordMap[idx] : "";
+
+ string type;
+ if (isDb)
+ type = $"数据库指令 [{matchedKeyword}]";
+ else if (idx < dbIndex)
+ type = "前文";
+ else
+ type = "后文";
+
+ resultTable.Rows.Add(
+ groupId,
+ originalRow["时间戳"]?.ToString() ?? "",
+ message,
+ type,
+ idx + 1,
+ matchedKeyword
+ );
+ }
+ }
+
+ return resultTable;
+ }
+
+ ///
+ /// 获取数据库指令统计信息
+ ///
+ public Dictionary GetDbCommandStatistics(DataTable logTable)
+ {
+ var stats = new Dictionary();
+
+ for (int i = 0; i < logTable.Rows.Count; i++)
+ {
+ string message = logTable.Rows[i]["日志内容"]?.ToString() ?? "";
+ string matchedKeyword = ContainsDbKeyword(message);
+
+ if (!string.IsNullOrEmpty(matchedKeyword))
+ {
+ if (!stats.ContainsKey(matchedKeyword))
+ stats[matchedKeyword] = 0;
+ stats[matchedKeyword]++;
+ }
+ }
+
+ return stats;
+ }
+
+ ///
+ /// 导出数据库指令及其上下文为格式化字符串
+ ///
+ public string ExportDbCommandsToString(DataTable logTable, int contextLines = 3)
+ {
+ var dbContext = ExtractDbCommandsGrouped(logTable, contextLines);
+
+ if (dbContext.Rows.Count == 0)
+ return "未找到数据库指令";
+
+ StringBuilder sb = new StringBuilder();
+ sb.AppendLine("=".PadRight(100, '='));
+ sb.AppendLine($"数据库指令及其上下文(前后各{contextLines}条)");
+ sb.AppendLine("=".PadRight(100, '='));
+ sb.AppendLine();
+
+ // 统计信息
+ var stats = GetDbCommandStatistics(logTable);
+ sb.AppendLine("【统计信息】");
+ foreach (var kvp in stats.OrderByDescending(k => k.Value))
+ {
+ sb.AppendLine($" {kvp.Key}: {kvp.Value} 次");
+ }
+ sb.AppendLine();
+ sb.AppendLine(new string('-', 100));
+ sb.AppendLine();
+
+ int currentGroupId = -1;
+
+ foreach (DataRow row in dbContext.Rows)
+ {
+ int groupId = Convert.ToInt32(row["组ID"]);
+ string timestamp = row["时间戳"]?.ToString() ?? "";
+ string message = row["日志内容"]?.ToString() ?? "";
+ string type = row["类型"]?.ToString() ?? "";
+ int lineNumber = Convert.ToInt32(row["行号"]);
+
+ if (groupId != currentGroupId)
+ {
+ if (currentGroupId != -1)
+ {
+ sb.AppendLine(new string('-', 100));
+ sb.AppendLine();
+ }
+ currentGroupId = groupId;
+ sb.AppendLine($"--- 数据库指令组 {groupId} ---");
+ }
+
+ string prefix = type.StartsWith("数据库指令") ? "▶▶▶ " : " ";
+ sb.AppendLine($"{prefix}[{timestamp}] 行{lineNumber} [{type}]");
+
+ // 显示消息内容
+ string[] lines = message.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
+ foreach (string line in lines)
+ {
+ if (type.StartsWith("数据库指令"))
+ sb.AppendLine($" {line}");
+ else
+ sb.AppendLine($" {line}");
+ }
+ sb.AppendLine();
+ }
+
+ return sb.ToString();
+ }
+
+ ///
+ /// 只提取包含数据库指令的条目(不包含上下文)
+ ///
+ public DataTable ExtractDbCommandsOnly(DataTable logTable)
+ {
+ var resultTable = new DataTable("DbCommandsOnly");
+ resultTable.Columns.Add("时间戳", typeof(string));
+ resultTable.Columns.Add("日志内容", typeof(string));
+ resultTable.Columns.Add("匹配关键词", typeof(string));
+ resultTable.Columns.Add("条目索引", typeof(int));
+
+ for (int i = 0; i < logTable.Rows.Count; i++)
+ {
+ string message = logTable.Rows[i]["日志内容"]?.ToString() ?? "";
+ string matchedKeyword = ContainsDbKeyword(message);
+
+ if (!string.IsNullOrEmpty(matchedKeyword))
+ {
+ resultTable.Rows.Add(
+ logTable.Rows[i]["时间戳"]?.ToString() ?? "",
+ message,
+ matchedKeyword,
+ i
+ );
+ }
+ }
+
+ return resultTable;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/MainWindow.xaml b/MainWindow.xaml
new file mode 100644
index 0000000..751c2e0
--- /dev/null
+++ b/MainWindow.xaml
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
new file mode 100644
index 0000000..e4c293d
--- /dev/null
+++ b/MainWindow.xaml.cs
@@ -0,0 +1,96 @@
+using Microsoft.Win32;
+using System;
+using System.Collections.Generic;
+using System.Data;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Controls;
+using System.Windows.Data;
+using System.Windows.Documents;
+using System.Windows.Input;
+using System.Windows.Media;
+using System.Windows.Media.Imaging;
+using System.Windows.Navigation;
+using System.Windows.Shapes;
+
+namespace LogAnalyzer
+{
+ ///
+ /// MainWindow.xaml 的交互逻辑
+ ///
+ public partial class MainWindow : Window
+ {
+ private readonly LogParserService _parser;
+ private DataTable resultTable;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ _parser = new LogParserService();
+ }
+
+ private void BtnLoadLog_Click(object sender, RoutedEventArgs e)
+ {
+ // 1. 打开文件选择对话框
+ OpenFileDialog openFileDialog = new OpenFileDialog();
+ openFileDialog.Filter = "文本文件 (*.txt)|*.txt|所有文件 (*.*)|*.*";
+ openFileDialog.Title = "请选择染整设备日志文件";
+
+ if (openFileDialog.ShowDialog() == true)
+ {
+ try
+ {
+ TxtStatus.Text = "正在解析...";
+ // 强制UI刷新状态文本
+ LogDataGrid.UpdateLayout();
+
+ // 2. 调用服务解析文件
+ // 注意:如果文件非常大,建议使用异步 Task.Run 避免界面卡顿
+ resultTable = _parser.ParseLogFile(openFileDialog.FileName);
+ //DataTable resultTable1 = _parser.ExtractDatabaseCommandsWithContext(resultTable,1);
+
+ // 3. 绑定数据到 DataGrid
+ // DataTable 的 DefaultView 实现了 IBindingList,适合直接绑定
+ LogDataGrid.ItemsSource = resultTable.DefaultView;
+ //LogDataGrid1.ItemsSource = resultTable1.DefaultView;
+
+ TxtStatus.Text = $"解析完成,共加载 {resultTable.Rows.Count} 条记录。";
+ }
+ catch (Exception ex)
+ {
+ MessageBox.Show($"发生错误: {ex.Message}", "错误", MessageBoxButton.OK, MessageBoxImage.Error);
+ TxtStatus.Text = "加载失败";
+ }
+ }
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ DataTable resultTable1 = _parser.ExtractDatabaseCommandsWithContext(resultTable, 1);
+ LogDataGrid1.ItemsSource = resultTable1.DefaultView;
+ }
+ private void Button_Click1(object sender, RoutedEventArgs e)
+ {
+ DataTable resultTable1 = _parser.ExtractTank1CommandsWithContext(resultTable);
+ LogDataGrid1.ItemsSource = resultTable1.DefaultView;
+ }
+ private void Button_Click2(object sender, RoutedEventArgs e)
+ {
+ DataTable resultTable1 = _parser.ExtractTank2CommandsWithContext(resultTable);
+ LogDataGrid1.ItemsSource = resultTable1.DefaultView;
+ }
+ private void Button_Click3(object sender, RoutedEventArgs e)
+ {
+ DataTable resultTable1 = _parser.ExtractTank3CommandsWithContext(resultTable);
+ LogDataGrid1.ItemsSource = resultTable1.DefaultView;
+ }
+ private void Button_Click4(object sender, RoutedEventArgs e)
+ {
+ DataTable resultTable1 = _parser.ExtractTank4CommandsWithContext(resultTable);
+ LogDataGrid1.ItemsSource = resultTable1.DefaultView;
+ }
+ }
+}
diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..e9d5206
--- /dev/null
+++ b/Properties/AssemblyInfo.cs
@@ -0,0 +1,52 @@
+using System.Reflection;
+using System.Resources;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Windows;
+
+// 有关程序集的一般信息由以下
+// 控制。更改这些特性值可修改
+// 与程序集关联的信息。
+[assembly: AssemblyTitle("LogAnalyzer")]
+[assembly: AssemblyDescription("")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("")]
+[assembly: AssemblyProduct("LogAnalyzer")]
+[assembly: AssemblyCopyright("Copyright © 2026")]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+// 将 ComVisible 设置为 false 会使此程序集中的类型
+//对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型
+//请将此类型的 ComVisible 特性设置为 true。
+[assembly: ComVisible(false)]
+
+//若要开始生成可本地化的应用程序,请设置
+//.csproj 文件中的 CultureYouAreCodingWith
+//在 中。例如,如果你使用的是美国英语。
+//使用的是美国英语,请将 设置为 en-US。 然后取消
+//对以下 NeutralResourceLanguage 特性的注释。 更新
+//以下行中的“en-US”以匹配项目文件中的 UICulture 设置。
+
+//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
+
+
+[assembly: ThemeInfo(
+ ResourceDictionaryLocation.None, //主题特定资源词典所处位置
+ //(未在页面中找到资源时使用,
+ //或应用程序资源字典中找到时使用)
+ ResourceDictionaryLocation.SourceAssembly //常规资源词典所处位置
+ //(未在页面中找到资源时使用,
+ //、应用程序或任何主题专用资源字典中找到时使用)
+)]
+
+
+// 程序集的版本信息由下列四个值组成:
+//
+// 主版本
+// 次版本
+// 生成号
+// 修订号
+//
+[assembly: AssemblyVersion("1.0.0.0")]
+[assembly: AssemblyFileVersion("1.0.0.0")]
diff --git a/Properties/Resources.Designer.cs b/Properties/Resources.Designer.cs
new file mode 100644
index 0000000..8ad48d0
--- /dev/null
+++ b/Properties/Resources.Designer.cs
@@ -0,0 +1,71 @@
+//------------------------------------------------------------------------------
+//
+// 此代码由工具生成。
+// 运行时版本: 4.0.30319.42000
+//
+// 对此文件的更改可能导致不正确的行为,如果
+// 重新生成代码,则所做更改将丢失。
+//
+//------------------------------------------------------------------------------
+
+namespace LogAnalyzer.Properties
+{
+
+
+ ///
+ /// 强类型资源类,用于查找本地化字符串等。
+ ///
+ // 此类是由 StronglyTypedResourceBuilder
+ // 类通过类似于 ResGen 或 Visual Studio 的工具自动生成的。
+ // 若要添加或移除成员,请编辑 .ResX 文件,然后重新运行 ResGen
+ // (以 /str 作为命令选项),或重新生成 VS 项目。
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ internal class Resources
+ {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
+ internal Resources()
+ {
+ }
+
+ ///
+ /// 返回此类使用的缓存 ResourceManager 实例。
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Resources.ResourceManager ResourceManager
+ {
+ get
+ {
+ if ((resourceMan == null))
+ {
+ global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("LogAnalyzer.Properties.Resources", typeof(Resources).Assembly);
+ resourceMan = temp;
+ }
+ return resourceMan;
+ }
+ }
+
+ ///
+ /// 重写当前线程的 CurrentUICulture 属性,对
+ /// 使用此强类型资源类的所有资源查找执行重写。
+ ///
+ [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
+ internal static global::System.Globalization.CultureInfo Culture
+ {
+ get
+ {
+ return resourceCulture;
+ }
+ set
+ {
+ resourceCulture = value;
+ }
+ }
+ }
+}
diff --git a/Properties/Resources.resx b/Properties/Resources.resx
new file mode 100644
index 0000000..af7dbeb
--- /dev/null
+++ b/Properties/Resources.resx
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
\ No newline at end of file
diff --git a/Properties/Settings.Designer.cs b/Properties/Settings.Designer.cs
new file mode 100644
index 0000000..b95240a
--- /dev/null
+++ b/Properties/Settings.Designer.cs
@@ -0,0 +1,30 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+// Runtime Version:4.0.30319.42000
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+namespace LogAnalyzer.Properties
+{
+
+
+ [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
+ [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")]
+ internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase
+ {
+
+ private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings())));
+
+ public static Settings Default
+ {
+ get
+ {
+ return defaultInstance;
+ }
+ }
+ }
+}
diff --git a/Properties/Settings.settings b/Properties/Settings.settings
new file mode 100644
index 0000000..033d7a5
--- /dev/null
+++ b/Properties/Settings.settings
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file