C#的“穷人版”词法分析器

35

我正在尝试用C#编写一个非常简单的解析器。

我需要一个词法分析器——能让我将正则表达式与标记相关联,这样它可以读取正则表达式并将符号返回给我。

看起来我应该能够使用Regex来完成实际的重活,但我看不到容易的方法。首先,Regex似乎只能在字符串上工作,而不是流(为什么会这样?!?)。

基本上,我想要以下接口的实现:

interface ILexer : IDisposable
{
    /// <summary>
    /// Return true if there are more tokens to read
    /// </summary>
    bool HasMoreTokens { get; }
    /// <summary>
    /// The actual contents that matched the token
    /// </summary>
    string TokenContents { get; }
    /// <summary>
    /// The particular token in "tokenDefinitions" that was matched (e.g. "STRING", "NUMBER", "OPEN PARENS", "CLOSE PARENS"
    /// </summary>
    object Token { get; }
    /// <summary>
    /// Move to the next token
    /// </summary>
    void Next();
}

interface ILexerFactory
{
    /// <summary>
    /// Create a Lexer for converting a stream of characters into tokens
    /// </summary>
    /// <param name="reader">TextReader that supplies the underlying stream</param>
    /// <param name="tokenDefinitions">A dictionary from regular expressions to their "token identifers"</param>
    /// <returns>The lexer</returns>
    ILexer CreateLexer(TextReader reader, IDictionary<string, object> tokenDefinitions);
}

那好吧,请发送代码...
不过,说真的,我准备开始编写上述接口的实现,但我很难相信在.NET(2.0)中没有一种简单的方法可以做到这一点。

所以,有没有关于简单实现上述功能的建议?(此外,我不想使用任何“代码生成器”。对于这个东西来说,性能并不重要,我也不想引入任何构建过程中的复杂性。)


1
在我看来,有很多好的工具可以生成词法分析器/语法分析器代码(例如ANTLR或Irony),但如果你从未从头开始编写过一些基本的解析代码,那么利用这些生成器可能会很困难。最近我编写了一个简单的开源数学表达式解析器(https://github.com/gsscoder/exprengine)。它使用访问者模式遍历抽象语法树(AST)进行求值。词法分析器/语法分析器是从头开始编写的,没有使用生成器!希望它能够有所帮助。 - gsscoder
7个回答

29
我在这里发布的原版回答存在一个问题,即只有当当前表达式有多个“正则表达式”匹配时才能工作。也就是说,一旦只有一个正则表达式匹配,它将返回一个标记-而大多数人都希望正则表达式是“贪婪”的。特别是对于像“引用字符串”这样的东西。
唯一建立在Regex之上的解决方案是逐行读取输入(这意味着无法拥有跨越多行的标记)。我可以接受这一点-毕竟,这是一个穷人的词法分析器!此外,在任何情况下从词法分析器中获取行号信息通常很有用。
因此,这里有一个新版本,解决了这些问题。信用还归功于这个: this
public interface IMatcher
{
    /// <summary>
    /// Return the number of characters that this "regex" or equivalent
    /// matches.
    /// </summary>
    /// <param name="text">The text to be matched</param>
    /// <returns>The number of characters that matched</returns>
    int Match(string text);
}

sealed class RegexMatcher : IMatcher
{
    private readonly Regex regex;
    public RegexMatcher(string regex) => this.regex = new Regex(string.Format("^{0}", regex));

    public int Match(string text)
    {
        var m = regex.Match(text);
        return m.Success ? m.Length : 0;
    }
    public override string ToString() => regex.ToString();
}

public sealed class TokenDefinition
{
    public readonly IMatcher Matcher;
    public readonly object Token;

    public TokenDefinition(string regex, object token)
    {
        this.Matcher = new RegexMatcher(regex);
        this.Token = token;
    }
}

public sealed class Lexer : IDisposable
{
    private readonly TextReader reader;
    private readonly TokenDefinition[] tokenDefinitions;

    private string lineRemaining;

    public Lexer(TextReader reader, TokenDefinition[] tokenDefinitions)
    {
        this.reader = reader;
        this.tokenDefinitions = tokenDefinitions;
        nextLine();
    }

    private void nextLine()
    {
        do
        {
            lineRemaining = reader.ReadLine();
            ++LineNumber;
            Position = 0;
        } while (lineRemaining != null && lineRemaining.Length == 0);
    }

    public bool Next()
    {
        if (lineRemaining == null)
            return false;
        foreach (var def in tokenDefinitions)
        {
            var matched = def.Matcher.Match(lineRemaining);
            if (matched > 0)
            {
                Position += matched;
                Token = def.Token;
                TokenContents = lineRemaining.Substring(0, matched);
                lineRemaining = lineRemaining.Substring(matched);
                if (lineRemaining.Length == 0)
                    nextLine();

                return true;
            }
        }
        throw new Exception(string.Format("Unable to match against any tokens at line {0} position {1} \"{2}\"",
                                          LineNumber, Position, lineRemaining));
    }

    public string TokenContents { get; private set; }
    public object Token   { get; private set; }
    public int LineNumber { get; private set; }
    public int Position   { get; private set; }

    public void Dispose() => reader.Dispose();
}

示例程序:

string sample = @"( one (two 456 -43.2 "" \"" quoted"" ))";

var defs = new TokenDefinition[]
{
    // Thanks to [steven levithan][2] for this great quoted string
            // regex
    new TokenDefinition(@"([""'])(?:\\\1|.)*?\1", "QUOTED-STRING"),
    // Thanks to http://www.regular-expressions.info/floatingpoint.html
    new TokenDefinition(@"[-+]?\d*\.\d+([eE][-+]?\d+)?", "FLOAT"),
    new TokenDefinition(@"[-+]?\d+", "INT"),
    new TokenDefinition(@"#t", "TRUE"),
    new TokenDefinition(@"#f", "FALSE"),
    new TokenDefinition(@"[*<>\?\-+/A-Za-z->!]+", "SYMBOL"),
    new TokenDefinition(@"\.", "DOT"),
    new TokenDefinition(@"\(", "LEFT"),
    new TokenDefinition(@"\)", "RIGHT"),
    new TokenDefinition(@"\s", "SPACE")
};

TextReader r = new StringReader(sample);
Lexer l = new Lexer(r, defs);
while (l.Next())
    Console.WriteLine("Token: {0} Contents: {1}", l.Token, l.TokenContents);

输出:

Token: LEFT Contents: (
Token: SPACE Contents:
Token: SYMBOL Contents: one
Token: SPACE Contents:
Token: LEFT Contents: (
Token: SYMBOL Contents: two
Token: SPACE Contents:
Token: INT Contents: 456
Token: SPACE Contents:
Token: FLOAT Contents: -43.2
Token: SPACE Contents:
Token: QUOTED-STRING Contents: " \" quoted"
Token: SPACE Contents:
Token: RIGHT Contents: )
Token: RIGHT Contents: )

1
四年后,非常感谢您,先生!昨天我设法让一个正则表达式词法分析器输出我想要的内容,但即使经过优化,它仍然很慢(大约需要15秒来处理36k个标记)。老的词法分析器所做的工作是有效的(一次性运行所有正则表达式,然后将匹配的索引与文件中当前索引进行比较,假设:“正则表达式很慢,所以只运行一次,然后永远不再运行”),但是您的方法比它快了14.5秒!显然,正则表达式并不像大多数人认为的那样昂贵。再次感谢! - Lennard Fonteijn
在这一行中进行一个小调整:this.regex = new Regex(string.Format("^{0}", regex));,请确保将 ^{0} 用括号包起来,像这样 ^({0}),否则类似于 \r|\n 的匹配将无法正确匹配! - Lennard Fonteijn

6
这是一个关于IT技术的翻译内容:

这可能有些过头,但请看一下CodePlex上的Irony

Irony是.NET平台上实现语言的开发工具包。它利用c#语言和.NET Framework 3.5的灵活性和强大性来实现全新的、简化的编译器构建技术。 与大多数现有的yacc/lex风格的解决方案不同,Irony不使用任何从用专门的元语言编写的语法规范生成扫描器或解析器代码。在Irony中,目标语言语法直接以c#编码,使用运算符重载来表达语法结构。 Irony的扫描器和解析器模块使用编码为c#类的语法来控制解析过程。查看表达式语法示例以了解在c#类中定义语法并将其用于工作解析器的示例。


1
啊,我明白了——听起来像是 C++ 的 Boost Spirit 的 C# 版本。谢谢……不过正如你从我的回答中看到的,对于我所寻找的东西来说绝对是过度设计了。 - Paul Hollingsworth
1
确实是一个有趣的项目,至少你可以像普通的C#一样获得IDE支持(因为语法变成了普通的C#代码 :))。我猜这有点像LINQ帮助你停止编写真正的SQL。 - IgorK

5

除非您有非常不寻常的语法,我强烈建议不要自己编写词法分析器/解析器。

我通常发现C#的词法分析器/解析器真的很缺乏。但是,F#带有fslex和fsyacc,您可以学习如何使用本教程。我已经在F#中编写了几个词法分析器/解析器,并在C#中使用它们,这非常容易做到。

我想这并不是一个穷人的词法分析器/解析器,因为您必须学习一种全新的语言才能入门,但这是一个开始。


2

更改我的原始答案。

看一下SharpTemplate,它有不同语法类型的解析器,例如:

#foreach ($product in $Products)
   <tr><td>$product.Name</td>
   #if ($product.Stock > 0)
      <td>In stock</td>
   #else
     <td>Backordered</td>
   #end
  </tr>
#end

它为每种类型的标记使用正则表达式:

public class Velocity : SharpTemplateConfig
{
    public Velocity()
    {
        AddToken(TemplateTokenType.ForEach, @"#(foreach|{foreach})\s+\(\s*(?<iterator>[a-z_][a-z0-9_]*)\s+in\s+(?<expr>.*?)\s*\)", true);
        AddToken(TemplateTokenType.EndBlock, @"#(end|{end})", true);
        AddToken(TemplateTokenType.If, @"#(if|{if})\s+\((?<expr>.*?)\s*\)", true);
        AddToken(TemplateTokenType.ElseIf, @"#(elseif|{elseif})\s+\((?<expr>.*?)\s*\)", true);
        AddToken(TemplateTokenType.Else, @"#(else|{else})", true);
        AddToken(TemplateTokenType.Expression, @"\${(?<expr>.*?)}", false);
        AddToken(TemplateTokenType.Expression, @"\$(?<expr>[a-zA-Z_][a-zA-Z0-9_\.@]*?)(?![a-zA-Z0-9_\.@])", false);
    }
}

这是如何使用的

foreach (Match match in regex.Matches(inputString))
{
    ...

    switch (tokenMatch.TokenType)
    {
        case TemplateTokenType.Expression:
            {
                currentNode.Add(new ExpressionNode(tokenMatch));
            }
            break;

        case TemplateTokenType.ForEach:
            {
                nodeStack.Push(currentNode);

                currentNode = currentNode.Add(new ForEachNode(tokenMatch));
            }
            break;
        ....
    }

    ....
}

它通过使用栈的推入和弹出来保持状态。

2
Malcolm Crowe在这里提供了一个很棒的C# LEX/YACC实现,链接。它通过为LEX创建正则表达式来工作...直接下载

0

如果您查看我的WPF转换器库中的ExpressionConverter,它具有基本的C#表达式词法分析和语法分析。从记忆中没有使用正则表达式。


0

可以使用Flex和Bison来编写C#程序。

爱尔兰大学的一位研究员开发了一个部分实现,可以在以下链接中找到:Flex/Bison for C#

它可能被认为是“穷人的词法分析器”,因为他似乎仍然存在一些问题,例如没有预处理器,存在“悬挂else”情况等。


1
该页面自2004年以来未进行更新,而词法分析器本身是从C# 0.28规范派生而来的。我认为这个“穷人版词法分析器”不应该在现实世界中使用。 - Juliet
1
这是一个很好的观点,不过我认为由于他正在尝试做一些简单的事情,这个快速而不完善(显然未完成)的词法分析器将是一个不错的起点。 - erik

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接