【问题标题】:Very basic English grammar parser非常基础的英语语法解析器
【发布时间】:2009-06-23 16:17:01
【问题描述】:

我正在编写一个非常基本的解析器(主要是为了更好地理解它们的工作原理),它接受用户输入的几个单词,检测句子结构是否正常,并输出结果。语法是:

句子: 名词动词

文章句

连词句

连词: “和” “或者” “但是”

名词: “鸟类” “鱼” “C++”

动词: “规则” “飞” “游泳”

文章: “那个”

编写语法很简单。它正在实现给我带来麻烦的代码。我的伪代码是:

main()
get user input (string words;)
while loop (cin >> words)
call sentence()
end main()

sentence()
call noun()
if noun() call verb() (if verb is true return "OK" ???)(else "not ok"???)
else if not noun() call article()
                if article() call sentence() (if sentence is true "OK"???)(else "not"?)
else if not noun() call conjunction()
                   if sentence() conjunction() sentence() - no idea how to implement
                                                             return "OK"
else "not ok"

所以这是我非常草率的伪代码。我有几个关于实施的问题。

  1. 对于单词函数(名词、动词等),我应该如何检查它们是否为真? (如检查用户的输入是否有鸟、鱼、苍蝇、游泳等)

  2. 应该如何处理连词调用和输出?

  3. 我应该处理来自主函数还是调用函数的输出?

  4. 如果我的伪代码完全错误,上述问题都不重要。基础有什么问题吗?

作为补充说明,我正在进行第 6 章的编程练习:使用 C++ 的实践和原则,所以我更喜欢使用我已经学过的语言语法,所以任何属于高级编程类别的东西可能不是很有帮助。 (练习中明确指出不要使用令牌,所以把它们算出来。)

提前致谢

最后编辑:在本书的公共小组中,我问了同样的问题,Bjarne Stroustrup 回复说他将练习解决方案放到了网上。他基本上将输入读入到语句函数中,并使用 if 语句来返回真或假。但是,他没有使用文章,所以我的文章要复杂得多。我想如果我从这个练习中学到了什么,那就是在处理大量用户输入时,标记化是关键(据我目前所知)。这是我现在的代码。稍后我可能会回到它,因为它仍然有很多错误,并且基本上只有在句子正常并且无法处理(名词,连词,句子)之类的事情时才会返回,但现在我要继续前进。

#include "std_lib_facilities.h"

bool article(string words)
{
               if (words == "the")
               return true;
               else return false;        
}

bool verb(string words)
{
               if (words == "rules" || words == "fly" || words == "swim")
               return true;
               else return false;                   
}

bool noun(string words)
{
               if (words == "birds" || words == "fish" || words == "c++")
               return true;
               else return false;                   
}

bool conjunction(string words)
{
              if (words == "and" || words == "but" || words == "or")
              return true;
              else return false;                  
}

bool sentence()
{
string w1;
string w2;
string w3;
string w4;

cin >> w1;
if (!noun(w1) && !article(w1)) return false; // grammar of IFS!

cin >> w2;
if (noun(w1) && !verb(w2)) return false;
if (article(w1) && !noun(w2)) return false;

cin >> w3;
if (noun(w1) && verb(w2) && (w3 == ".")) return true;
if (verb(w2) && !conjunction(w3)) return false;
if (noun(w2) && !verb(w3)) return false;
if (conjunction(w3)) return sentence();

cin >> w4;
if (article(w1) && noun(w2) && verb(w3) && (w4 == ".")) return true;
if (!conjunction(w4)) return false;
if (conjunction(w4)) return sentence();
}


int main()
{                                   
cout << "Enter sentence. Use space then period to end.\n";
            bool test = sentence();
            if (test)
               cout << "OK\n";
            else
               cout << "not OK\n";

keep_window_open(); }

【问题讨论】:

  • 我编辑了消息以显示一些我拼凑在一起的丑陋代码来说明我的意思。错误消息和其他功能将在我掌握基础知识后处理。
  • 您想从头开始编写解析器吗?或者您想知道如何使用解析器生成器工具来构建您可以自定义的解析器?
  • 我想从头开始写。
  • 我现在遇到的问题是它似乎是单独阅读每个单词。如何在不使用标记的情况下获得句子功能来阅读每个单词?

标签: c++ parsing nlp


【解决方案1】:

好的。如果你真的想手工做:-(

这个问题有两个部分:

  • 词法分析
  • 句法分析。
  • 我们可以忽略 Symantic 分析,因为这就是上面的原因。

首先,您将输入流标记为合理的标记。单词将是一个明显的选择,但这会给句法阶段留下很多工作。所以我会把你的话分成以下类型(连词、名词、动词、文章),然后写一个词法分析器来返回正确的词。

Lexer.cpp
enum Lexeme { END,Conjunction,Noun,Verb,Article };
Lexem getNextLexme(std::istream in)
{
    std::string word;
    in >> word;

    if (!in) {return END;}

         if (word == "and")   return Conjunction;
    else if (word == "birds") return Noun;
    else if (word == "fly")   return Verb;
    else if (word == "the")   return Article;

    ... etc
}

所以现在您可以根据简化的标记流来编写句法解析器。

bool ParseSentence(std::istream in)
{
    Lexeme token = getNextLexme(in);
    switch(token)
    {
        case Noun:    if (!parseVerb(in))
                      {    return false;
                      }
                      return parseConjunctionOrEnd(in);
        case Article: return ParseSentence();
        case END:     return true;
    }
}
bool parseVerb(std::istream in)
{
    Lexeme token = getNextLexeme(in);
    if (token != Verb) { /*ERROR*/ return false;}
    return true;
 }
 // etc

句法分析的另一个选择是建立一个状态表。但这涉及手工分析语法和确定状态。这应该只用最琐碎的语法来尝试,任何比你这里更大的东西都应该留给可以自动生成状态表的工具。

所以假设我在下面的原始帖子中定义的语法:
并希望我能做对,因为我不是充气工具 :-)

State 1:   Start <Nothing Happened>
               Article -> State 2
               Noun -> State 3
               Otherwise Error
State 2:   Seen Article.
               Noun -> State 3
               Otherwise Error
State 3:   Seen Noun  in Sentence.
               Verb -> State 4
               Otherwise Error
State 4:   Seen Noun Verb
               End -> State 5
               Conjunction -> State 1
State 5:   Finished:

State 0:   Error State.


int stateTable[][]    // CurrentState,CurrentObject
           = {/*State 0: Error State:*/{},
                                       // END,Conjunction,Noun,Verb,Article 
              /*State 1: Start*/       {  0,  0,          3,   0,   2},
              /*State 2: Article*/     {  0,  0,          3,   0,   0},
              /*State 3: Noun*/        {  0,  0,          0,   4,   0},
              /*State 4: Noun Verb*/   {  5,  1,          0,   0,   0},
              /*State 5: End*/         {}
             };

bool parseSentence(std::iostream& in)
{
    int currentState = 1;
    while((currentState != 0) && (currentState != 5))
    {
        int token = getNextLexme(in);
        currentState = stateTable[currentState][token];
    }
    return currentState == 5;
}

【讨论】:

  • 练习说不要打扰令牌,而只是读入一个字符串。但是,这无疑使我走上了正确的道路。如果我得到它,我会评论和编辑帖子。
  • @Alex... 这与您在不为您编写全部内容的情况下获得的答案差不多。将此答案与 Barry Brown 的答案结合起来,您就有了一个相当完整的解决方案。 Martin 的回答是给你代码,并为你指出正确的方向来自上而下地递归语法。像 Martin 所做的那样对输入流进行标记肯定会更好。但是,要像这样进行解析,您需要按照 Barry 的建议重新形成语法。确保它符合自顶向下解析的要求。
  • 该练习明确表示不要使用令牌,所以在我求助之前,我将尝试再使用一些技巧。此外,我肯定会改革语法,但似乎没有必要创建类(除非我使用令牌。)
  • @Alex... 你可以在没有令牌的情况下完成这个练习,但它不会使它变得复杂,而且它绝对是一条更清洁/更容易走的路。
  • 我知道除了一个主要缺陷外,我已经把代码写下来了。句子 function() 需要能够读取每个输入的单词,而不是一次只读取一个。标记化是实现这一目标的唯一方法吗?如果是这样,该死的 Stroustrup!
【解决方案2】:

我对这个问题很感兴趣。我将帮助 OP,Alex,做点什么,但由于我不太了解 C++,所以它将使用伪 C++。它也不会使用 lex/yacc,因为 Alex 想了解它们是如何制作的。如果您使用 lex 和 yacc 等工具,它们就会变成“黑匣子”。我现在没有时间把它全部放在一起,但我可以在几个小时内一点一点地完成它。我只是想现在就开始。

我们需要做的第一件事是清理语法。你有一个这样定义的句子:

sentence : noun verb
         | article sentence
         | sentence conjunction sentence

这个语法有缺陷。诸如“鱼游”之类的句子是有效的,因为句子是根据其自身定义的。递归是语法的正常部分,但需要正确处理。我会冒险猜测您不希望两篇或多篇文章连续出现。

更好的句子语法可能是:

sentence : clause conjunction clause
         | clause

clause : nounphrase verbphrase

nounphrase : noun
           | article noun

这消除了可能导致无限循环的无约束递归。

现在我们已经准备好处理解析器本身了。由于这是 C++,我们不妨让它面向对象。我现在得先走一步,但我会给你一个提示:每个生产规则都会有一个类。

【讨论】:

  • “既然这是 C++,我们不妨让它面向对象。[...] 每个生产规则都有一个类。”不知何故,这说明了当今世界的状况有什么问题。 ;-)
  • 感谢改进的语法。如果我对它进行标记,我将不得不使用类,但必须有一个不涉及标记的解决方案。我将尝试更多技巧(也许制作一个字符串向量?),如果我无法弄清楚,我将求助于标记化。
  • 是的,我要标记化。但是现在已经很晚了,所以我明天早上会处理它。但是,我不只需要一个令牌类来获取输入吗?我看不出有什么特别的理由为每条规则创建一个类。
【解决方案3】:

好的...我不会回答您的具体问题,但我想向您指出一些在处理此问题时需要考虑的一般想法。首先,解析很难。你有一个简单的语法,但它仍然很难。编译器前端负责解析......只是为了提供一些上下文。

有两种基本类型的解析...自顶向下解析和自底向上解析。它们是根据它们如何遍历语法树来命名的(想想将为可能的构造创建什么样的语法树)。自上而下的解析很容易,并且可能适用于您想要做的事情。最常见的自顶向下解析方法是递归下降解析:http://en.wikipedia.org/wiki/Recursive_descent_parser

但是,要使用递归下降解析,您的语法必须采用某种格式......对于某些语法,不可能递归下降解析它们。不过,您应该能够修改您的语法以适应这一点。

自上而下的解析器很容易编写...因为对于一种小型语言,您只需要几个函数。

第二种解析方式是自底向上解析。这通常在编译器中使用,因为它没有自上而下的限制。如果给定的字符串不适合语言,也更容易进行错误报告。

自下而上的解析器很难编写...大多数人使用解析器生成器来完成这项工作。我与 YACC 合作过很多次。您基本上输入语法(以及匹配该规则时要采取的操作)并解析语法。

自下而上的解析器使用一种叫做 shift-reduce 解析的东西。这是处理输入和匹配规则的方法。

再次查看您的代码,我想说您可以使用自上而下的解析器。抱歉,我不能给出具体的代码,但是谷歌自上而下的解析器示例(或递归下降解析器示例)可能会找到您需要的代码。

【讨论】:

  • 本章中的示例(一个计算器示例)使用自下而上的语法来解析运算顺序。但是,这似乎有所不同,因为它不是按严格的升序或降序检查名词和动词,而是检查“如果 x 为真,请确保 y 也为真”。它几乎可以自上而下地工作,但有一些曲折。稍后我将发布一些未完成的代码来说明我的意思。
【解决方案4】:

大多数解析程序文本解析是使用正式语法解析器完成的。英语和大多数口语都不是正式的语法,您将很难非常解析它们。这个问题困扰了 PHD 数十年,但没有取得很大成功。

【讨论】:

  • 我认为这不是这个问题的重点。他已经提出了一个简化版本的英语语法,它的形式足以解析。他的问题更多是关于如何创建一个解析程序。
  • 是的,问题是。我如何解析这个语法。
【解决方案5】:

词性

要获得词性,您需要a dictionary list with parts of speech。除了将单词映射到词性列表的哈希表之外,另一种检查词性的可能方法是将每个词性的每组词加载到其自己的Bloom filter(一种来自字符串到布尔值)。

【讨论】:

    【解决方案6】:

    自然语言语法的一个方面是它们通常是模棱两可的。比如英文句子:

    我曾经穿着睡衣射杀了一头大象。他是怎么穿上我的睡衣的,我永远不会知道
    -- 格劳乔·马克思

    “in my pajamas”这个短语模棱两可地描述了主体“我”或客体“大象”。如果没有语义上下文,就不可能正确构建 AST。

    如果您希望避免这种情况,您可能需要一些有用的方法来处理歧义。一种策略是产生歧义短语的所有可能推导。使这成为可能的一个工具是Earley Parser。与其他类型的解析器(例如递归下降解析器)不同,Earley 解析器以解析器状态转换的形式生成所有派生,而不是简单的树。实际上,这并不难处理。

    【讨论】:

    • 很难破坏一个漂亮的海滩。
    • 自然语言中的另一个歧义是单词可以是两种或多种类型之一。例如,这些名词/动词:fly、call、stack、wreck。而这个名词/动词/形容词:光。我想你已经为你完成了工作。 :-)
    【解决方案7】:

    使用 Flex 和 Bison:

    Bison 中的语法规则:

    %%
    
    English            :   SentenceList
    
    SentenceList       :   Sentence
                       |   Article  Sentence
                       |   Sentence Conjunction Sentence
    
    Sentence           :   Noun Verb
    
    
    Conjunction        :   TOKEN_WordAnd
                       |   TOKEN_WordOr
                       |   TOKEN_WordBut
    
    
    Noun               :   TOKEN_WORD_BIRDS
                       |   TOKEN_WORD_FISH
                       |   TOKEN_WORD_CPP
    
    Verb:              :   TOKEN_WORD_RULES
                       |   TOKEN_WORD_FLY
                       |   TOKEN_WRD_SWIM
    
    Article            :   TOKEN_WORD_THE
    %%
    

    【讨论】:

    • 为什么投反对票。我能想到一个更好的方法来解析一个定义明确的语法!使用 corect 工具来完成这项工作。
    • 也许是因为 OP 不是在寻找解析器 - 他是在寻找帮助来编写解析器...
    • 也许吧。但这是写的。
    • 使用工具获取语法并生成解析器与编写解析器完全不同。
    • 我认为它是:唯一的区别是您不需要用纸和铅笔设置 10 个小时来生成状态表。困难的部分是定义一个有效的语法,这是由你而不是工具来完成的。
    【解决方案8】:

    你可以去 Ubiquity 抢,这是一个 Firefox 的插件,旨在使用自然语言来完成常见的网络任务(它是用 JavaScript 编写的,但也许你可以得到一个有用的通用算法)

    【讨论】:

      【解决方案9】:

      在编写解析器之前,我是否建议您学习 lex 和 yacc 或 flex 和 bison 对?这些工具专门用于创建解析器和词法分析器。

      它们会自动生成您的 c/c++(可能是其他)代码,因此您不必担心用户参数的各种边界情况。您可以在 30 分钟内完成上述语法。

      至于你的问题:

      对于词函数(名词、动词、 等)我应该如何检查 如果他们是真的? (如检查是否 用户的输入有鸟,鱼,飞, 游泳等)

      这里需要大量使用 strcasecmp(),并进行各种错误检查。

      我应该如何处理连词 调用和输出?

      我不太明白你在这里的意思。如果它有效与否,我只会返回某种哨兵值。

      我应该处理来自 main 函数还是调用函数?

      主要来自调用函数,因为它们具有您所关心的个别功能。 main() 只是把它粘在一起的胶水。

      如果以上问题都不重要 我的伪代码完全错误。是 基础有什么问题吗?

      按照你的方式看起来是可行的,但是通过切换到 lex/yacc 或 flex/bison 会让你免于头疼

      【讨论】:

      • 我猜是因为他问是因为他想了解它们是如何工作的。我同意使用工具学习解析器会教给你很多东西。 然后写你自己的。
      猜你喜欢
      • 2014-10-26
      • 1970-01-01
      • 2011-09-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多