【问题标题】:How to find recurring word groups in text with C#? [closed]如何使用 C# 在文本中查找重复出现的词组? [关闭]
【发布时间】:2016-02-26 10:45:33
【问题描述】:

我在 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<string, int> wordList = new Dictionary<string, int>();
        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++;
                }
            }
        }

这是我的示例文本:

绿藻(单数:green alga)是一种大型的非正式藻类群,由叶绿藻和轮藻藻组成,它们现在被划分为不同的部门。 陆生植物或胚胎植物(高等植物)被认为是从轮生植物中出现的。 [1]由于胚芽植物不是藻类,因此被排除在外,因此绿藻是一个并系群。然而,包括绿藻和胚芽植物的进化枝是单系的,被称为绿色植物进化枝和植物界。绿藻包括单细胞和群落鞭毛虫,大多数每个细胞有两个鞭毛,以及各种群落、球状和丝状形式,以及宏观的多细胞海藻。在高等植物的近亲 Charales 中,发生了组织的完全细胞分化。大约有 8,000 种绿藻。 [2]许多物种大部分时间都以单细胞形式生活,而其他物种则形成coenobia(菌落)、长丝或高度分化的宏观海藻。 其他一些生物依靠绿藻为它们进行光合作用。 euglenids 和 chlorarachniophytes 中的叶绿体是从摄入的绿藻中获得的,[1] 并且在后者中保留了一个核形体(残留核)。绿藻也与纤毛虫草履虫、Hydra viridissima 和扁虫共生。某些绿藻种类,特别是 Trebouxiophyceae 和 Trentepohlia(Ulvophyceae 类)属的 Trebouxia 属,可以与真菌共生并形成地衣。一般来说,与地衣合作的真菌物种不能独立生活,而藻类物种通常在没有真菌的情况下生活在自然界中。 Trentepohlia 是一种丝状绿藻,可以在潮湿的土壤、岩石或树皮上独立生活,也可以在 Graphidaceae 的地衣中形成光共生体。

对于我的示例文本,我在第一行中得到了 greenalgae 字样,如预期的那样.

问题是,我不仅需要单个单词,还需要单词组。对于这个示例文本,我也想要 green algae 词,以及 green藻类 字。

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

提前致谢。

更新 如果你明白我的意思,你不需要阅读这些行。
由于我看到太多关于我的“组”定义的 cmets 不清楚,我想我需要更详细地说明我的观点,我希望在 cmets 部分写下这些行,但对于这次更新来说,这有点狭窄。首先,我知道 StackOverflow 不是编码服务。我试图在一篇文章中找到最常用的词组,并试图确定文章是关于什么的,我们也可以称之为标签生成器。为此,我试图找到最常用的单词,一开始还可以。然后我意识到这不是决定主题的好方法,因为我不能假设这篇文章只是关于第一个词或第二个词。在我的例子中,我不能说这篇文章只是关于 greenalgae,因为它们在这里有共同的意义,而不是单独的。如果我用一篇关于像“海伦娜·伯翰·卡特”这样的三位名人的文章来尝试这个(如果我假设它在文章中写了全名,而不仅仅是姓氏),我想把这些词放在一起,而不是一一列举。我正在尝试实现更聪明的算法,该算法以最准确的方式一次猜测主题。我不想限制字数,因为文章可能是关于“联合国工业发展组织”的(我再次假设它现在在文章中写成“UNIDO”)。我可以通过尝试让每个单词组从任何索引开始到任何长度的文本结尾来实现这一点。好吧,这确实不是一个好方法,尤其是对于长文本,但这并非不可能,对吧?但我一直在寻找一种更好的方法来做到这一点,我只是询问了一个更好的算法想法和最好的工具,我可以自己编写代码。我希望我最终明确了我的目标。

【问题讨论】:

  • 正则表达式帮不了你。我相信你需要一些术语提取算法。
  • 不知道什么可以帮助你,因为我在工作中使用付费 MT 软件。
  • 你的问题不清楚。请再次阅读我的第一条评论以及@galakt 的评论。我们都要求您定义词组的含义。仅仅说它不止一个词并不是一个定义。或者,如果您尝试在词组中编写一些代码,那么我们也许能够理解您想要什么。
  • @ErtürkÖztürk - 出于这个原因,组中的单词数量很重要。我想得到所有的词组,所以我抓住了文本中的第一个说 10 个词。那是一个群体。与文本中的前 11 个单词一样。它与第一组不同,因为它有一个额外的单词,但它确实包含前一组中的 10 个单词。您必须将词组定义为一个死板的概念,否则您不知道如何知道一个组从哪里开始和结束。
  • @ErtürkÖztürk - 组中的单词数量会产生很大的不同。如果您有 1,000,000 个单词并且您允许任何长度的单词组,那么您就有 500,000,000,500,000,000 个单词组。它可以很快变得很长。如果事实每次字数增加 10 倍,那么字组的数量就会增加近 100 倍。

标签: c# regex text


【解决方案1】:

实现这一点的方法是获取初始文本,并使用string.split(' ');将其按空格拆分为字符串数组

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

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

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

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

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

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

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

陈述

@galakt 正如我上面提到的,假设是 3。这有关系吗?

词组的概念是完全抽象的。是的,它是一组单词,但整个文本块是一组单词。 必须应用规则来管理你对这组词的行为。

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

要调用它,请使用Dictionary&lt;String, int&gt; SingleWordGroups = GetWordGroupInstances(1);

    private Dictionary<String, int> GetWordGroupInstances(int GroupSize) {

        Dictionary<String, int> WordGroupInstances = new Dictionary<string, int>();

        //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;

    }

下面的方法被修改为依次产生所有的组输出,所以 这 绿色的 绿藻 绿藻 等等……

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

   private Dictionary<String, int> GetAllGroups() {
        Dictionary<string, int> WordGroupInstances = new Dictionary<string, int>();
        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个词组。

【讨论】:

  • “任何涉及遍历文本并在其上运行重复函数的任务都不应使用正则表达式。”这不是真的。正则表达式模式的一个常见用途是查找和替换,其中涉及遍历文本并重复应用函数(替换表达式)。
  • 对于查找和替换,您不需要自己迭代文本,除非您将文本保存在某种列表或字典中,您只需将正则表达式应用于整个语料库。使用查找和替换,使用我在上面发布的单词组方法迭代整个文本会更快,标记您正在搜索的每个组的索引和偏移量,然后使用索引进行替换。如果您要在整个文本中寻找相同的模式,那么正则表达式总是一个坏主意——它实在是太慢了。
  • 感谢您的回答亚历克斯。我现在会更新我的问题,在尝试所有其他方法后,我会决定最好的方法。
【解决方案2】:

我认为这很有效。

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<Match>()
        .SelectMany(x => x.Captures.Cast<Capture>())
        .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 "and", 12 x "algae", 12 x "in", 11 x "green", 10 x "of", 9 x "green algae", 8 x "are ", 6 x "as", 6 x "species", 5 x "a", 4 x "is", 4 x "or", 4 x "to", 3 x "embryophytes", 3 x "form", 3 x“发现”,3 x“地衣”,3 x“活”,3 x“开”,3 x“植物”,3 x“那个”,3 x“藻类和”,3 x“和”, 3 x“作为”,3 x“在”,3 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 "trentepohlia", 2 x "while", 2 x "with", 2 x "algae are", 2 x "are a", 2 x "green alga", 2 x "高等植物", 2 x "in lichens ", 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 “是”, 1 x “两者”, 1 x “不能” , 1 x “细胞”, 1 x “细胞”, 1 x “细胞”, 1 x “charales”, 1 x “轮藻”, 1 x “轮藻”, 1 x “绿藻”, 1 x “叶绿素”, 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“用于”,1 x“形式”,1 x“完整”,1 x“真菌”,1 x“真菌”

【讨论】:

  • 哇,这正是我要找的。让我也尝试不同的文本以查看性能,但这就是答案。
  • @ErtürkÖztürk - 我为remove 变量添加了一点速度上的改进。如果您喜欢我的回答,请不要忘记投票并接受。
  • 这种方法会得到很好的结果,但是为此使用正则表达式特别慢。我很想知道这个操作需要多长时间才能打开一组组。
  • @Alex - 正则表达式可能是这段代码中最快的部分之一。我会做一些基准测试看看。
  • @Alex - words 的计算似乎需要 1.1ms。 groups 需要 344 毫秒。未存储的frequencies 需要 23.6 毫秒,完全排序的frequencies 需要 425 毫秒。
【解决方案3】:

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

//tokenize input (enumerable of string)
var words = Regex.Matches(input, @"\w+").Cast<Match>().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<string, int>(StringComparer.CurrentCultureIgnoreCase);
foreach (var group in groups.Select(g => string.Join(" ", g)))
{
    int count;
    counts.TryGetValue(group, out count);
    counts[group] = ++count;
}


IEnumerable<string[]> GetWordGroups(IEnumerable<string> 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 

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2012-12-14
    • 2011-12-17
    • 2021-10-09
    • 1970-01-01
    • 2016-04-07
    • 2017-04-18
    • 1970-01-01
    • 2018-08-22
    相关资源
    最近更新 更多