如何使用C#在文本中查找重复的单词组?

我在StringBuilder(sb)中反复使用这个代码,我在互联网上找到了这个代码,根据作者的说法,它与Word的单词计数器非常一致。

StringBuilder wordBuffer = new StringBuilder(); int wordCount = 0; // 1. Build the list of words used. Consider ''' (apostrophe) and '-' (hyphen) a word continuation character. Dictionary wordList = new Dictionary(); foreach (char c in sb.ToString()) { if (char.IsLetter(c) || c == '\'' || c == '-') { wordBuffer.Append(char.ToLower(c)); } else { if (wordBuffer.Length > 3) { int count = 0; string word = wordBuffer.ToString(); wordList.TryGetValue(word, out count); wordList[word] = ++count; wordBuffer.Clear(); wordCount++; } } } 

这是我的示例文本:

绿藻(单数:绿藻)是一种大型的,非正式的藻类组合,由绿藻和藻类藻类组成,现在被放置在不同的区域。 土地植物或胚胎植物(高等植物)被认为是从Charophytes中出现的。[1] 由于胚胎植物不是藻类,因此被排除在外,绿藻是一种副系。 然而,包括绿藻和胚性植物的进化枝是单系的并且被称为进化枝(Viridiplantae)和植物界(Plantae)。 绿藻包括单细胞和殖民鞭毛虫,大多数每个细胞有两个鞭毛,以及各种殖民,球形和丝状forms,以及宏观,多细胞海藻。 在Charales,高等植物的最亲近的亲属,发生组织的完全细胞分化。 大约有8000种绿藻。[2] 许多物种的大部分时间都是单细胞,而其他物种则形成瓢虫(菌落),长丝或高度分化的宏观海藻。 一些其他生物依靠绿藻进行光合作用。 苏格兰和氯霉素中的叶绿体是从摄入的绿藻中获得的,[1]后者保留了核形态(退化核)。 绿色藻类也在纤毛虫草履虫(Hydra viridissima)和扁虫(flatworms)中共生发现。 一些种类的绿藻,特别是Trebouxiophyceae和Trentepohlia(类Ulvophyceae)的Trebouxia属,可以与真菌共生组合形成地衣。 一般来说,与地衣配合的真菌物种不能独立生存,而藻类物种通常在没有真菌的情况下生活在自然界中。 Trentepohlia是一种丝状绿藻,可以在潮湿的土壤,岩石或树皮上独立生活,或在Graphidaceae家族的地衣中形成光照植物。

使用我的示例文本,我会按照预期在第一行中获得绿色藻类的单词。

问题是 ,我不需要单个单词,我也需要单词组。 通过这个示例文本,我也想要绿藻词,以及绿色藻类词。

我的选择性问题是:我需要以高性能来完成它,因为文本可能很长。 正如我研究的那样,在这种情况下使用RegEx的性能并不高,但我不确定是否有第二种方法可以实现。

提前致谢。

更新 如果你得到了我所询问的内容,你不需要阅读这些内容。
由于我看到太多关于我的“小组”定义的评论并不清楚,我想我需要更详细地说明我的观点,我希望在评论部分写下这些内容,但这个更新的范围有点狭窄。 首先,我知道StackOverflow不是编码服务。 我试图在一篇文章中找到最常用的单词组,并试图决定文章的内容,我们也称之为标签生成器。 为此我试图找到最常用的单词,一开始就没问题。 然后我意识到这不是决定主题的好方法,因为我不能认为这篇文章只是关于第一个或第二个词。 在我的例子中,我不能说这篇文章只是关于绿色藻类,因为它们在这里意味着什么,而不是一个人。 如果我试着写一篇关于三个名人的文章,比如“Helena Bonham Carter”(如果我认为它的文章是全文写的,不仅仅是姓氏),我想把这些文字一个接一个地放在一起。 我正在尝试实现更聪明的算法,它以最精确的方式一次性地猜测主题。 我不想限制字数,因为文章可能是关于“联合国工业发展组织”(我再次假设它现在写成文章中的“工发组织”)。 我可以通过尝试将每个单词组从任何索引开始到任意长度的文本结尾来实现这一点。 好吧,这不是一个好方法,尤其是长文本,但这不是不可能的吗? 但我正在寻找一种更好的方法来做到这一点,我只是问了一个更好的算法思想和最好的工具,我可以自己编写代码。 我希望我最终明确表达我的目标。

我认为这很有效。

 var text = @"The green algae (singular: green alga) are ..."; // include all your text var remove = "().,:[]0123456789".Select(x => x.ToString()).ToArray(); var words = Regex .Matches(text, @"(\S+)") .Cast() .SelectMany(x => x.Captures.Cast()) .Select(x => remove.Aggregate(x.Value, (t, r) => t.Replace(r, ""))) .Select(x => x.Trim().ToLowerInvariant()) .Where(x => !String.IsNullOrWhiteSpace(x)) .ToArray(); var groups = from n1 in Enumerable.Range(0, words.Length) from n2 in Enumerable.Range(1, words.Length - n1) select String.Join(" ", words.Skip(n1).Take(n2)); var frequencies = groups .GroupBy(x => x) .Select(x => new { wordgroup = x.Key, count = x.Count() }) .OrderByDescending(x => x.count) .ThenBy(x => x.wordgroup.Count(y => y == ' ')) .ThenBy(x => x.wordgroup) .ToArray(); 

这给了我连续单词序列的每个单词分组的频率,包括所有单词的单个单词组。

单词的数量是288.单词组的总数288 x (288 + 1) / 2 = 41,616 。 单词组的最终数量(在对重复的单词组进行分组并删除空/空白字符串之后)为41,449。

以下是这些41,449中的前100个:

20 x“the”,13 x“和”,12 x“藻类”,12 x“in”,11 x“green”,10 x“of”,9 x“green algae”,8 x“are”,6 x“as”,6 x“种”,5 x“a”,4 x“is”,4 x“或”,4 x“至”,3 x“胚胎植物”,3 x“forms”,3 x“发现“,3 x”地衣“,3 x”活“,3 x”on“,3 x”植物“,3 x”那个“,3 x”藻类和“,3 x”和“,”3 x“作为“,3 x”中的“,3 x”的“,2 x”alga“,2 x”can“,2 x”clade“,2 x”class“,2 x”colonial“,2 x “丝状”,2 x“从”,2 x“更高”,2 x“宏观”,2 x“最”,2 x“其他”,2 x“海藻”,2 x“他们的”,2 x“trentepohlia “,2 x”而“,2 x”与“,2 x”藻类是“,2 x”是“,2 x”绿藻“,2 x”高等植物“,2 x”在地衣中“,2 x“绿色”,2 x“种类”,2 x“绿色”,2 x“绿色”,2 x“绿藻”,2 x“绿藻”,2 x“绿藻” ,2 x“绿色物种”,2 x“绿藻”,2 x“绿藻种”,1 x“约”,1 x“获得”,1 x“藻类”,1 x“也”, 1 x“关联”,1 x“树皮”,1 x“be”,1 x“both”,1 x“不能”,1 x“细胞”,1 x“细胞”,1 x“细胞”,1 x“charales”,1 x“charophyte”,1 x“charophytes”,1 x“chlorarachniophytes”,1 x“chlorophyte”,1 x“叶绿体“,1 x”纤毛“,1 x”最接近“,1 x”球形“,1 x”coenobia“,1 x”菌落“,1 x”导管“,1 x”组成“,1 x”分化“ ,1 x“分化”,1 x“分裂”,1 x“出现”,1 x“euglenids”,1 x“排除”,1 x“家族”,1 x“少数”,1 x“细丝”,1 x“鞭毛”,1 x“鞭毛虫”,1 x“扁虫”,1 x“for”,1 x“forms”,1 x“完整”,1 x“真菌”,1 x“真菌”

实现此目的的方法是获取初始文本,并使用string.split(' ');空格分割为字符串数组string.split(' ');

接下来,您需要迭代数组中的每个字符串。 这对于单个单词来说很容易,但对于组来说则更复杂。 因此,您需要定义组大小。 您必须控制每次迭代时指针前进的数组中的位数。

一旦你能够迭代数组,你需要获取一组单词(无论你选择它多长时间),并将其存储在某个地方。 示例中的字典是一种很好的方法。

如果字典包含单词group,则将其值增加1。 如果它不包含该组,只需添加默认值1即可。

  if (wordList.ContainsKey(theKey)) { wordList[theKey]++; } else { wordList.Add(theKey, 1); } 

你正确地提到你的研究表明正则表达式不是高性能。 对于这个任务,正则表达式完全是错误的工具 – 你不是在寻找模式,而是在检查组。 为此,您必须从头到尾查看文本,检查值。

任何涉及迭代文本并在其上运行重复函数的任务都不应该使用正则表达式。

编辑:我最初对Regex性能的假设是不正确的 – 在C#中,它似乎比Java更快,但我仍然认为纯正则表达式方法不如使用正则表达式来标记文本那么快然后使用循环或linq表达式来查找组。

说明

@galakt正如我上面提到的,让我们说3.这有关系吗?

单词组的想法完全是抽象的。 是的,它是一组单词,但整个文本块是一组单词。 必须应用规则来管理您对该组单词的行为。

下面是一个示例方法,它将根据通过方法调用传递的大小返回所有单词组的字典。 它不会从文本中删除任何非字母数字字符,但即使组大小较大,它也很快。

要调用它,请使用Dictionary SingleWordGroups = GetWordGroupInstances(1);

  private Dictionary GetWordGroupInstances(int GroupSize) { Dictionary WordGroupInstances = new Dictionary(); //Grab the string to work from... String[] sourceText = GetSourceText().Split(' '); int pointer = 0; StringBuilder groupBuilder = new StringBuilder(); while (pointer < sourceText.Length - GroupSize) { groupBuilder.Clear(); int offset = pointer + GroupSize; for (int i = pointer; i < offset; i++) { //prepend a space to allow separation between words in groups. //We can make a substring from this later starting from char 1 //to lose the initial whitespace from the string. groupBuilder.Append(" ").Append(sourceText[i]); } String key = groupBuilder.ToString().Substring(1); if (!WordGroupInstances.ContainsKey(key)) { WordGroupInstances.Add(key, 1); } else { WordGroupInstances[key]++; } /** * Setting the pointer to increase by group size grabs a group, moves on * to the end of the group, and grabs the next. * */ pointer += GroupSize; /** * Setting the point to increment by 1 grabs a group, advances by 1 word, then * grabs the next, so from the phrase - "Hello world, I'm some text", the groups of size 2 would be * "Hello world,", "world, I'm", "I'm some" etc... */ //pointer++; } return WordGroupInstances; } 

下面的方法被修改为依次产生所有组输出,所以The Green Green Algae The Green Algae等......

值得注意的是,整个文本必须转换为小写或大写,以便单词不依赖于大小写。 我已经对此进行了一些改进以提高性能(并且不需要中断指令)。

  private Dictionary GetAllGroups() { Dictionary WordGroupInstances = new Dictionary(); StringBuilder groupBuilder = new StringBuilder(); String[] sourceText = GetSourceText().Split(' '); for (int i = 0; i < sourceText.Length; i++) { groupBuilder.Clear(); for (int j = i; j < sourceText.Length; j++) { groupBuilder.Append(" ").Append(sourceText[j]); String key = groupBuilder.ToString().Substring(1); if (!WordGroupInstances.ContainsKey(key)) { WordGroupInstances.Add(key, 1); } else { WordGroupInstances[key]++; } } } return WordGroupInstances; } 

在使用文本语料库(288个单词)进行性能测试后,它将在0.171886秒内创建41773个单词组。

这是一种流式方法,它从可枚举的单词中递归地构建大小为N的组(在本例中为3)。 将输入标记为单词并不重要(我在本例中使用了一个简单的正则表达式)。

 //tokenize input (enumerable of string) var words = Regex.Matches(input, @"\w+").Cast().Select(m => m.Value); //get word groups (enumerable of string[]) var groups = GetWordGroups(words, 3); //do what you want with your groups; suppose you want to count them var counts = new Dictionary(StringComparer.CurrentCultureIgnoreCase); foreach (var group in groups.Select(g => string.Join(" ", g))) { int count; counts.TryGetValue(group, out count); counts[group] = ++count; } IEnumerable GetWordGroups(IEnumerable words, int size) { if (size <= 0) throw new ArgumentOutOfRangeException(); if (size == 1) { foreach (var word in words) { yield return new string[] { word }; } yield break; } var prev = new string[0]; foreach (var next in GetWordGroups(words, size - 1)) { yield return next; //stream of groups includes all groups up to size - 1, but we only combine groups of size - 1 if (next.Length == size - 1) { if (prev.Length == size - 1) { var group = new string[size]; Array.Copy(prev, 0, group, 0, prev.Length); group[group.Length - 1] = next[next.Length - 1]; yield return group; } prev = next; } } } 

这种流式传输方法的一个优点是可以最大限度地减少内存中必须保留的字符串数量(这会减少大型文本的内存使用量)。 根据您接收输入的方式,另一种优化可能是在读取输入时使用TextReader生成标记枚举。

下面是一个中间分组输出的示例(每个项目实际上是令牌数组,在此输入白色空间以进行输出):

 The green The green algae green algae The green algae singular algae singular green algae singular green singular green algae singular green alga green alga singular green alga