关键字过虑实现的思路及Aho–Corasick高效字符串匹配算法应用
在本人昨晚发的强大灵活的脏字过虑:1万字文章过虑1万关键词用时只要1毫秒(包括扩展的高亮功能) 文章中,只是介绍过虑的功能和性能,这个文章主要讲一下实现的思路,另外给大家看一下Aho–Corasick算法的C#实现。
既然是要过虑,那就要先查找,如果是直接的一个字符一个字符的匹配,那是很耗时的,因为时间花在不需要匹配的工作,有不少人会用正则去解决过虑,我09年的时候也这样,但后来发现大量关键词下性能确实极低下,所以才会另想它法。上一文中的过虑主要思想是这样的,开始会先用一个字典保存保存所有关键词,同一个字母开头的会另放在一个子字典里,这样一来,扫描的范围就大大的缩小了,然后再考虑到脏字一般是2个字的占了很大的比例,所以再在第二个字母做判断,如果不存在就不需要再扫描下去了,至于可跳字符,就是在直接需要扫描的时候一一判断的,没技巧可讲,另外一点值得注意的是,大小写敏感的情况下,在判断时需要转换大小写,大量关键词影响不小,所以就初始化时再保存了一分小写的,所以在扫描的时候就不需要转换了,所以是否大小写的两个情况性能上不会有什么变化,基本的思路就这样了。如果说,你想要很准确的过虑,那就要用到分词了(判断可以人性化),我的方法只能处理比较简单匹配与过虑。实现过程并没有使用Aho–Corasick算法。
在查找资料的时候还了解到Aho–Corasick算法,它可以帮助我们快速的找出多个子字符串。可以到这里了解算法:http://en.wikipedia.org/wiki/Aho%E2%80%93Corasick_string_matching_algorithm
5点有约会,先直接上实现代码了(从一老外的例子里小改的),本人测试过我上面的方法和使用Aho–Corasick过虑用的时候差不了多少:
/// <summary> /// 表示一个查找结果 /// </summary> public struct KeywordSearchResult { private int index; private string keyword; public static readonly KeywordSearchResult Empty = new KeywordSearchResult(-1, string .Empty); public KeywordSearchResult( int index, string keyword) { this .index = index; this .keyword = keyword; } /// <summary> /// 位置 /// </summary> public int Index { get { return index; } } /// <summary> /// 关键词 /// </summary> public string Keyword { get { return keyword; } } } /// <summary> /// Aho-Corasick算法实现 /// </summary> public class KeywordSearch { /// <summary> /// 构造节点 /// </summary> private class Node { private Dictionary< char , Node> transDict; public Node( char c, Node parent) { this .Char = c; this .Parent = parent; this .Transitions = new List<Node>(); this .Results = new List< string >(); this .transDict = new Dictionary< char , Node>(); } public char Char { get ; private set ; } public Node Parent { get ; private set ; } public Node Failure { get ; set ; } public List<Node> Transitions { get ; private set ; } public List< string > Results { get ; private set ; } public void AddResult( string result) { if (!Results.Contains(result)) { Results.Add(result); } } public void AddTransition(Node node) { this .transDict.Add(node.Char, node); this .Transitions = this .transDict.Values.ToList(); } public Node GetTransition( char c) { Node node; if ( this .transDict.TryGetValue(c, out node)) { return node; } return null ; } public bool ContainsTransition( char c) { return GetTransition(c) != null ; } } private Node root; // 根节点 private string [] keywords; // 所有关键词 public KeywordSearch(IEnumerable< string > keywords) { this .keywords = keywords.ToArray(); this .Initialize(); } /// <summary> /// 根据关键词来初始化所有节点 /// </summary> private void Initialize() { this .root = new Node( ' ' , null ); // 添加模式 foreach ( string k in this .keywords) { Node n = this .root; foreach ( char c in k) { Node temp = null ; foreach (Node tnode in n.Transitions) { if (tnode.Char == c) { temp = tnode; break ; } } if (temp == null ) { temp = new Node(c, n); n.AddTransition(temp); } n = temp; } n.AddResult(k); } // 第一层失败指向根节点 List<Node> nodes = new List<Node>(); foreach (Node node in this .root.Transitions) { // 失败指向root node.Failure = this .root; foreach (Node trans in node.Transitions) { nodes.Add(trans); } } // 其它节点 BFS while (nodes.Count != 0) { List<Node> newNodes = new List<Node>(); foreach (Node nd in nodes) { Node r = nd.Parent.Failure; char c = nd.Char; while (r != null && !r.ContainsTransition(c)) { r = r.Failure; } if (r == null ) { // 失败指向root nd.Failure = this .root; } else { nd.Failure = r.GetTransition(c); foreach ( string result in nd.Failure.Results) { nd.AddResult(result); } } foreach (Node child in nd.Transitions) { newNodes.Add(child); } } nodes = newNodes; } // 根节点的失败指向自己 this .root.Failure = this .root; } /// <summary> /// 找出所有出现过的关键词 /// </summary> /// <param name="text"></param> /// <returns></returns> public List<KeywordSearchResult> FindAllKeywords( string text) { List<KeywordSearchResult> list = new List<KeywordSearchResult>(); Node current = this .root; for ( int index = 0; index < text.Length; ++index) { Node trans; do { trans = current.GetTransition(text[index]); if (current == this .root) break ; if (trans == null ) { current = current.Failure; } } while (trans == null ); if (trans != null ) { current = trans; } foreach ( string s in current.Results) { list.Add( new KeywordSearchResult(index - s.Length + 1, s)); } } return list; } /// <summary> /// 简单地过虑关键词 /// </summary> /// <param name="text"></param> /// <returns></returns> public string FilterKeywords( string text) { StringBuilder sb = new StringBuilder(); Node current = this .root; for ( int index = 0; index < text.Length; index++) { Node trans; do { trans = current.GetTransition(text[index]); if (current == this .root) break ; if (trans == null ) { current = current.Failure; } } while (trans == null ); if (trans != null ) { current = trans; } // 处理字符 if (current.Results.Count > 0) { string first = current.Results[0]; sb.Remove(sb.Length - first.Length + 1, first.Length -1); // 把匹配到的替换为** sb.Append( new string ( '*' , current.Results[0].Length)); } else { sb.Append(text[index]); } } return sb.ToString(); } } |
测试结果:
下载源码:https://files.cnblogs.com/kudy/KeywordSearch.rar
出处:http://kudy.cnblogs.com
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库