using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using DCIT.Core.Models; namespace DCIT.Core.Services { public class AutoExtractService { private static readonly Regex TokenRegex = new Regex( @"(?[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,})|(?[A-Za-z][A-Za-z0-9_\-']*)|(?[\u4E00-\u9FFF]+)", RegexOptions.Compiled); private readonly WordTextExtractionService _wordTextExtractionService; public AutoExtractService(WordTextExtractionService wordTextExtractionService) { _wordTextExtractionService = wordTextExtractionService; } public Task ExtractAsync( IList selectedFiles, AutoExtractSettings settings, IList existingRules, IProgress progress, CancellationToken cancellationToken) { return Task.Run( () => Execute(selectedFiles, settings, existingRules, progress, cancellationToken), cancellationToken); } private AutoExtractResult Execute( IList selectedFiles, AutoExtractSettings settings, IList existingRules, IProgress progress, CancellationToken cancellationToken) { var result = new AutoExtractResult(); var extractedDocuments = new List(); if (selectedFiles == null || selectedFiles.Count == 0) { return result; } settings = settings ?? new AutoExtractSettings(); existingRules = existingRules ?? Array.Empty(); for (var index = 0; index < selectedFiles.Count; index++) { cancellationToken.ThrowIfCancellationRequested(); var filePath = selectedFiles[index]; progress?.Report(new ExtractionProgress { Current = index + 1, Total = selectedFiles.Count, Message = "正在分析 " + filePath, }); try { var extracted = _wordTextExtractionService.ExtractVisibleText(filePath); if (!string.IsNullOrWhiteSpace(extracted.VisibleText)) { extractedDocuments.Add(extracted); result.DocumentsProcessed++; } else { result.DocumentsSkipped++; result.Warnings.Add(filePath + " 未提取到可见文本。"); } } catch (Exception ex) { result.DocumentsSkipped++; result.Warnings.Add(filePath + " 提取失败: " + ex.Message); } } if (extractedDocuments.Count == 0) { return result; } var candidateMap = new Dictionary(StringComparer.Ordinal); var existingSearchTexts = new HashSet( existingRules .Where(rule => !string.IsNullOrWhiteSpace(rule.SearchText)) .Select(rule => rule.SearchText), StringComparer.Ordinal); if (settings.ExtractKeywords) { foreach (var keyword in BuildKeywordCandidates(extractedDocuments, settings)) { AddCandidate(candidateMap, existingSearchTexts, keyword); } } if (settings.ExtractHighFrequencyWords) { foreach (var word in BuildHighFrequencyWordCandidates(extractedDocuments, settings)) { AddCandidate(candidateMap, existingSearchTexts, word); } } if (settings.ExtractHighFrequencySentences) { foreach (var sentence in BuildSentenceCandidates(extractedDocuments, settings)) { AddCandidate(candidateMap, existingSearchTexts, sentence); } } foreach (var candidate in candidateMap.Values.OrderBy(item => item.FirstOccurrenceOrder)) { result.Candidates.Add(candidate); } return result; } private IEnumerable BuildKeywordCandidates(IList documents, AutoExtractSettings settings) { var tokenObservations = BuildTokenObservations(documents, settings).ToList(); if (tokenObservations.Count == 0) { return Enumerable.Empty(); } var candidates = new List(); if (settings.UseKeywordFrequency) { candidates.AddRange(CreateCandidatesFromRankedEntries( RankByFrequency(tokenObservations).Take(settings.MaxKeywordCount), AutoExtractCategory.Keyword, "关键字(词频)")); } if (settings.UseKeywordTfIdf) { candidates.AddRange(CreateCandidatesFromRankedEntries( RankByTfIdf(tokenObservations).Take(settings.MaxKeywordCount), AutoExtractCategory.Keyword, "关键字(TF-IDF)")); } if (settings.UseKeywordTextRank) { candidates.AddRange(CreateCandidatesFromRankedEntries( RankByTextRank(tokenObservations).Take(settings.MaxKeywordCount), AutoExtractCategory.Keyword, "关键字(TextRank)")); } return candidates; } private IEnumerable BuildHighFrequencyWordCandidates(IList documents, AutoExtractSettings settings) { var tokenObservations = BuildTokenObservations(documents, settings).ToList(); return CreateCandidatesFromRankedEntries( RankByFrequency(tokenObservations).Take(settings.MaxWordCount), AutoExtractCategory.HighFrequencyWord, "高频词"); } private IEnumerable BuildSentenceCandidates(IList documents, AutoExtractSettings settings) { var sentenceMap = new Dictionary(StringComparer.Ordinal); var order = 0; var delimiters = new HashSet((settings.SentenceDelimiters ?? string.Empty).ToCharArray()); foreach (var document in documents) { foreach (var sentence in SplitSentences(document.VisibleText, delimiters)) { var trimmed = sentence.Trim(); if (!IsAllowedSentence(trimmed, settings)) { continue; } order++; RankedEntry entry; if (!sentenceMap.TryGetValue(trimmed, out entry)) { entry = new RankedEntry { Text = trimmed, Count = 0, Score = 0, FirstDocumentPath = document.FilePath, FirstOccurrenceOrder = order, }; sentenceMap[trimmed] = entry; } entry.Count++; entry.Score = entry.Count; } } return CreateCandidatesFromRankedEntries( sentenceMap.Values .OrderByDescending(item => item.Count) .ThenBy(item => item.FirstOccurrenceOrder) .Take(settings.MaxSentenceCount), AutoExtractCategory.HighFrequencySentence, "高频句"); } private IEnumerable BuildTokenObservations(IList documents, AutoExtractSettings settings) { var observations = new List(); var order = 0; for (var documentIndex = 0; documentIndex < documents.Count; documentIndex++) { var document = documents[documentIndex]; foreach (Match match in TokenRegex.Matches(document.VisibleText ?? string.Empty)) { if (match.Groups["Email"].Success) { if (!settings.ExcludeEmailAddresses) { order++; observations.Add(CreateTokenObservation(match.Value, document.FilePath, documentIndex, order)); } continue; } if (match.Groups["English"].Success) { if (settings.CountEnglishWords && match.Value.Length >= settings.MinEnglishWordLength) { order++; observations.Add(CreateTokenObservation(match.Value, document.FilePath, documentIndex, order)); } continue; } if (match.Groups["Chinese"].Success && settings.CountChineseWords) { foreach (var token in SegmentChinese(match.Value, settings.MinChineseWordLength)) { if (!ContainsChinese(token)) { continue; } order++; observations.Add(CreateTokenObservation(token, document.FilePath, documentIndex, order)); } } } } return observations; } private static IEnumerable SegmentChinese(string text, int minLength) { if (string.IsNullOrWhiteSpace(text)) { yield break; } var normalized = new string(text.Where(character => character >= 0x4E00 && character <= 0x9FFF).ToArray()); if (string.IsNullOrWhiteSpace(normalized)) { yield break; } var effectiveMinLength = Math.Max(minLength, 1); var maxLength = Math.Min(4, normalized.Length); var emitted = false; for (var length = maxLength; length >= effectiveMinLength; length--) { for (var index = 0; index + length <= normalized.Length; index++) { emitted = true; yield return normalized.Substring(index, length); } } if (!emitted && normalized.Length >= effectiveMinLength) { yield return normalized; } } private IEnumerable RankByFrequency(IList observations) { return observations .GroupBy(item => item.Token, StringComparer.Ordinal) .Select(group => new RankedEntry { Text = group.Key, Count = group.Count(), Score = group.Count(), FirstDocumentPath = group.OrderBy(item => item.GlobalOrder).First().FilePath, FirstOccurrenceOrder = group.Min(item => item.GlobalOrder), }) .OrderByDescending(item => item.Score) .ThenBy(item => item.FirstOccurrenceOrder) .ToList(); } private IEnumerable RankByTfIdf(IList observations) { var documentCount = observations.Select(item => item.DocumentIndex).Distinct().Count(); var byToken = observations.GroupBy(item => item.Token, StringComparer.Ordinal); var ranked = new List(); foreach (var tokenGroup in byToken) { var docFrequency = tokenGroup.Select(item => item.DocumentIndex).Distinct().Count(); var termFrequency = tokenGroup.Count(); var idf = Math.Log((documentCount + 1.0) / (docFrequency + 1.0)) + 1.0; ranked.Add(new RankedEntry { Text = tokenGroup.Key, Count = termFrequency, Score = termFrequency * idf, FirstDocumentPath = tokenGroup.OrderBy(item => item.GlobalOrder).First().FilePath, FirstOccurrenceOrder = tokenGroup.Min(item => item.GlobalOrder), }); } return ranked .OrderByDescending(item => item.Score) .ThenBy(item => item.FirstOccurrenceOrder) .ToList(); } private IEnumerable RankByTextRank(IList observations) { var graph = new Dictionary>(StringComparer.Ordinal); var orderedTokens = observations.OrderBy(item => item.GlobalOrder).ToList(); const int windowSize = 4; for (var index = 0; index < orderedTokens.Count; index++) { var current = orderedTokens[index].Token; if (!graph.ContainsKey(current)) { graph[current] = new HashSet(StringComparer.Ordinal); } for (var offset = 1; offset < windowSize && index + offset < orderedTokens.Count; offset++) { var neighbor = orderedTokens[index + offset].Token; if (string.Equals(current, neighbor, StringComparison.Ordinal)) { continue; } graph[current].Add(neighbor); if (!graph.ContainsKey(neighbor)) { graph[neighbor] = new HashSet(StringComparer.Ordinal); } graph[neighbor].Add(current); } } var scores = graph.Keys.ToDictionary(key => key, key => 1.0, StringComparer.Ordinal); for (var iteration = 0; iteration < 20; iteration++) { var next = new Dictionary(StringComparer.Ordinal); foreach (var node in graph.Keys) { var score = 0.15; foreach (var neighbor in graph[node]) { var degree = Math.Max(graph[neighbor].Count, 1); score += 0.85 * (scores[neighbor] / degree); } next[node] = score; } scores = next; } return observations .GroupBy(item => item.Token, StringComparer.Ordinal) .Select(group => new RankedEntry { Text = group.Key, Count = group.Count(), Score = scores.ContainsKey(group.Key) ? scores[group.Key] : 0, FirstDocumentPath = group.OrderBy(item => item.GlobalOrder).First().FilePath, FirstOccurrenceOrder = group.Min(item => item.GlobalOrder), }) .OrderByDescending(item => item.Score) .ThenBy(item => item.FirstOccurrenceOrder) .ToList(); } private static IEnumerable CreateCandidatesFromRankedEntries( IEnumerable entries, AutoExtractCategory category, string categoryDisplay) { return entries.Select(entry => new ExtractionCandidate { PrimaryCategory = category, CategoryDisplay = categoryDisplay, OriginalText = entry.Text, StatisticDisplay = string.Format("次数={0}; 分值={1:F3}", entry.Count, entry.Score), FirstDocumentPath = entry.FirstDocumentPath, FirstOccurrenceOrder = entry.FirstOccurrenceOrder, }); } private static IEnumerable SplitSentences(string text, HashSet delimiters) { if (string.IsNullOrWhiteSpace(text)) { yield break; } var buffer = string.Empty; foreach (var character in text) { if (delimiters.Contains(character)) { if (!string.IsNullOrWhiteSpace(buffer)) { yield return buffer; buffer = string.Empty; } continue; } buffer += character; } if (!string.IsNullOrWhiteSpace(buffer)) { yield return buffer; } } private static bool IsAllowedSentence(string sentence, AutoExtractSettings settings) { if (string.IsNullOrWhiteSpace(sentence)) { return false; } var hasChinese = ContainsChinese(sentence); var hasEnglish = sentence.Any(character => (character >= 'A' && character <= 'Z') || (character >= 'a' && character <= 'z')); if (hasChinese && settings.CountChineseSentences) { return true; } if (hasEnglish && settings.CountEnglishSentences) { return true; } return !hasChinese && !hasEnglish; } private static bool ContainsChinese(string value) { return value.Any(character => character >= 0x4E00 && character <= 0x9FFF); } private static TokenObservation CreateTokenObservation(string token, string filePath, int documentIndex, int order) { return new TokenObservation { Token = token, FilePath = filePath, DocumentIndex = documentIndex, GlobalOrder = order, }; } private static void AddCandidate( IDictionary candidateMap, ISet existingSearchTexts, ExtractionCandidate candidate) { if (string.IsNullOrWhiteSpace(candidate.OriginalText)) { return; } if (existingSearchTexts.Contains(candidate.OriginalText)) { return; } ExtractionCandidate existing; if (!candidateMap.TryGetValue(candidate.OriginalText, out existing)) { candidateMap[candidate.OriginalText] = candidate; return; } if (existing.CategoryDisplay.IndexOf(candidate.CategoryDisplay, StringComparison.OrdinalIgnoreCase) < 0) { existing.CategoryDisplay += ", " + candidate.CategoryDisplay; } } private class TokenObservation { public string Token { get; set; } public string FilePath { get; set; } public int DocumentIndex { get; set; } public int GlobalOrder { get; set; } } private class RankedEntry { public string Text { get; set; } public int Count { get; set; } public double Score { get; set; } public string FirstDocumentPath { get; set; } public int FirstOccurrenceOrder { get; set; } } } }