using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using DCIT.Core.Models; using Newtonsoft.Json; namespace DCIT.Core.Services { public class RuleService { private static readonly TimeSpan RegexValidationTimeout = TimeSpan.FromMilliseconds(250); public RuleFile Load(string ruleFilePath) { if (string.IsNullOrWhiteSpace(ruleFilePath) || !System.IO.File.Exists(ruleFilePath)) { return new RuleFile(); } var content = System.IO.File.ReadAllText(ruleFilePath, Encoding.UTF8); return JsonConvert.DeserializeObject(content) ?? new RuleFile(); } public void Save(string ruleFilePath, RuleFile ruleFile) { var report = Validate(ruleFile); if (report.HasErrors) { var detail = string.Join(Environment.NewLine, report.Issues.Select(FormatIssue)); throw new InvalidOperationException(detail); } var content = JsonConvert.SerializeObject(ruleFile, Formatting.Indented); System.IO.File.WriteAllText(ruleFilePath, content, new UTF8Encoding(false)); } public RuleValidationReport Validate(RuleFile ruleFile) { ruleFile = ruleFile ?? new RuleFile(); var report = new RuleValidationReport(); var idSet = new HashSet(StringComparer.OrdinalIgnoreCase); var validatedRules = new List(); for (var index = 0; index < ruleFile.Rules.Count; index++) { var rule = ruleFile.Rules[index] ?? new RuleDefinition(); if (string.IsNullOrWhiteSpace(rule.Id)) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "规则 ID 不能为空。")); } else if (!idSet.Add(rule.Id.Trim())) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "规则 ID 重复。")); } if (string.IsNullOrWhiteSpace(rule.Name)) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "规则名称为空。")); } if (string.IsNullOrWhiteSpace(rule.SearchText)) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "原文本或表达式不能为空。")); continue; } Regex compiledRegex = null; if (rule.MatchMode == RuleMatchMode.RegularExpression) { try { compiledRegex = new Regex(rule.SearchText, GetRegexOptions(rule), RegexValidationTimeout); ValidateRegexRule(report, index, rule, compiledRegex); } catch (Exception ex) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "正则表达式非法: " + ex.Message)); } } if (rule.MatchMode == RuleMatchMode.PlainText && !string.IsNullOrEmpty(rule.ReplacementText) && rule.ReplacementText.IndexOf(rule.SearchText, rule.IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase) >= 0) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "替换文本会再次命中原文本,请确认是否符合预期。")); } if (rule.MatchMode == RuleMatchMode.RegularExpression && compiledRegex != null && !string.IsNullOrEmpty(rule.ReplacementText) && IsRegexReplacementSelfMatching(compiledRegex, rule.ReplacementText)) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "替换模板会再次命中当前规则,请确认是否符合预期。")); } var current = new ValidatedRuleContext(index, rule, compiledRegex); foreach (var previous in validatedRules) { AddCompatibilityIssues(report, previous, current); } validatedRules.Add(current); } return report; } public RuleDefinition CreateAutoExtractRule(ExtractionCandidate candidate, IList existingRules) { var prefix = GetPrefix(candidate.PrimaryCategory); var namePrefix = GetNamePrefix(candidate.PrimaryCategory); var nextIndex = 1; var existingIds = new HashSet(existingRules.Select(rule => rule.Id ?? string.Empty), StringComparer.OrdinalIgnoreCase); var existingNames = new HashSet(existingRules.Select(rule => rule.Name ?? string.Empty), StringComparer.OrdinalIgnoreCase); string id; do { id = string.Format("{0}{1:0000}", prefix, nextIndex++); } while (existingIds.Contains(id)); nextIndex = 1; string name; do { name = string.Format("{0}-{1:0000}", namePrefix, nextIndex++); } while (existingNames.Contains(name)); return new RuleDefinition { Id = id, Name = name, IsEnabled = true, MatchMode = RuleMatchMode.PlainText, IsCaseSensitive = false, IsWholeWord = false, SearchText = candidate.OriginalText, ReplacementText = string.Empty, Note = string.Format( "自动提取; 类型={0}; 统计={1}; 首次文档={2}; 时间={3:yyyy-MM-dd HH:mm:ss}", candidate.CategoryDisplay, candidate.StatisticDisplay, candidate.FirstDocumentPath, DateTime.Now), }; } public string FormatIssue(RuleValidationIssue issue) { return string.Format( "[{0}] 规则#{1} ({2}/{3}) {4}", issue.Severity, issue.Index + 1, issue.RuleId ?? "-", issue.RuleName ?? "-", issue.Message); } private static RuleValidationIssue CreateIssue(RuleValidationSeverity severity, int index, RuleDefinition rule, string message) { return new RuleValidationIssue { Severity = severity, Index = index, RuleId = rule.Id, RuleName = rule.Name, Message = message, }; } private static RegexOptions GetRegexOptions(RuleDefinition rule) { var options = RegexOptions.CultureInvariant; if (!rule.IsCaseSensitive) { options |= RegexOptions.IgnoreCase; } return options; } private static void ValidateRegexRule(RuleValidationReport report, int index, RuleDefinition rule, Regex regex) { if (MatchesEmptyText(regex)) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "正则表达式可匹配零长度文本,执行时可能导致死循环。")); } try { regex.Replace("validation", rule.ReplacementText ?? string.Empty, 1); } catch (Exception ex) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Error, index, rule, "替换模板非法: " + ex.Message)); } if (HasNestedQuantifierRisk(rule.SearchText)) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "正则表达式存在嵌套量词风险,可能导致性能急剧下降。")); } if (MayTimeoutDuringValidation(regex)) { report.Issues.Add(CreateIssue(RuleValidationSeverity.Warning, index, rule, "正则表达式验证探测时出现超时风险,建议简化表达式。")); } } private static bool MatchesEmptyText(Regex regex) { var emptyMatch = regex.Match(string.Empty); if (emptyMatch.Success && emptyMatch.Length == 0) { return true; } var sampleMatch = regex.Match("A"); return sampleMatch.Success && sampleMatch.Length == 0; } private static bool IsRegexReplacementSelfMatching(Regex regex, string replacementText) { try { return regex.Match(replacementText ?? string.Empty).Success; } catch { return false; } } private static bool MayTimeoutDuringValidation(Regex regex) { try { _ = regex.IsMatch(new string('a', 4096) + "!"); return false; } catch (RegexMatchTimeoutException) { return true; } } private static bool HasNestedQuantifierRisk(string pattern) { if (string.IsNullOrWhiteSpace(pattern)) { return false; } return Regex.IsMatch( pattern, @"\((?:[^()\\]|\\.)+[+*](?:[^()\\]|\\.)*\)[+*{]", RegexOptions.CultureInvariant); } private static void AddCompatibilityIssues(RuleValidationReport report, ValidatedRuleContext previous, ValidatedRuleContext current) { if (previous.Rule.Target != current.Rule.Target) { return; } if (IsEquivalentSearchText(previous.Rule.SearchText, current.Rule.SearchText)) { report.Issues.Add(CreateIssue( RuleValidationSeverity.Warning, current.Index, current.Rule, string.Format("当前规则与 #{0} 使用相同原文本或表达式,请确认顺序影响。", previous.Index + 1))); if (HaveDifferentMatchingOptions(previous.Rule, current.Rule)) { report.Issues.Add(CreateIssue( RuleValidationSeverity.Warning, current.Index, current.Rule, string.Format("当前规则与 #{0} 使用相同原文本但匹配选项或替换文本不同,继续执行结果可能不稳定。", previous.Index + 1))); } } if (previous.Rule.MatchMode == RuleMatchMode.PlainText && current.Rule.MatchMode == RuleMatchMode.PlainText && HasPlainTextOverlap(previous.Rule, current.Rule)) { report.Issues.Add(CreateIssue( RuleValidationSeverity.Warning, current.Index, current.Rule, string.Format("当前规则与 #{0} 存在重叠命中风险,请确认执行顺序。", previous.Index + 1))); } } private static bool IsEquivalentSearchText(string left, string right) { return string.Equals(left ?? string.Empty, right ?? string.Empty, StringComparison.OrdinalIgnoreCase); } private static bool HaveDifferentMatchingOptions(RuleDefinition left, RuleDefinition right) { return left.MatchMode != right.MatchMode || left.IsCaseSensitive != right.IsCaseSensitive || left.IsWholeWord != right.IsWholeWord || !string.Equals(left.ReplacementText ?? string.Empty, right.ReplacementText ?? string.Empty, StringComparison.Ordinal); } private static bool HasPlainTextOverlap(RuleDefinition left, RuleDefinition right) { if (string.IsNullOrEmpty(left.SearchText) || string.IsNullOrEmpty(right.SearchText)) { return false; } if (left.IsWholeWord && right.IsWholeWord) { return false; } var comparison = left.IsCaseSensitive && right.IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; return left.SearchText.IndexOf(right.SearchText, comparison) >= 0 || right.SearchText.IndexOf(left.SearchText, comparison) >= 0; } private static string GetPrefix(AutoExtractCategory category) { switch (category) { case AutoExtractCategory.Keyword: return "AK"; case AutoExtractCategory.HighFrequencyWord: return "AW"; default: return "AS"; } } private static string GetNamePrefix(AutoExtractCategory category) { switch (category) { case AutoExtractCategory.Keyword: return "AUTO-KW"; case AutoExtractCategory.HighFrequencyWord: return "AUTO-WORD"; default: return "AUTO-SENT"; } } private sealed class ValidatedRuleContext { public ValidatedRuleContext(int index, RuleDefinition rule, Regex compiledRegex) { Index = index; Rule = rule; CompiledRegex = compiledRegex; } public int Index { get; private set; } public RuleDefinition Rule { get; private set; } public Regex CompiledRegex { get; private set; } } } }