写你自己的lexer

如果您最喜欢的语言的lexer在pygments包中丢失,您可以轻松地编写自己的并扩展pygments。

你所需要的一切都可以在里面找到 pygments.lexer 模块。你可以在里面读到 API documentation lexer是用一些关键字参数(lexer选项)初始化的类,它提供 get_tokens_unprocessed() 方法,该方法将数据赋给字符串或Unicode对象lex。

这个 get_tokens_unprocessed() 方法必须返回一个迭代器或iterable,其中包含形式中的元组 (index, token, value) . 通常您不需要这样做,因为有基本的lexer可以完成大部分工作,并且您可以子类化。

RegexLexer

几乎所有Pygments的词法分析器都使用的词法分析器基类是 RegexLexer 。此类允许您根据以下方面定义词法分析规则 regular expressions 对于不同的 states

状态是一组正则表达式,它们与 当前位置 . 如果其中一个表达式匹配,则执行相应的操作(例如生成具有特定类型的令牌或更改状态),将当前位置设置为最后一个匹配结束的位置,匹配过程将继续使用当前状态的第一个正则表达式。

lexer状态保存在堆栈上:每次输入新状态时,新状态都被推到堆栈上。最基本的lexer(如 DiffLexer )只需要一个状态。

每个州都定义为以下形式的元组列表 (regexactionnew_state ),其中最后一项是可选的。在最基本的形式中, action 是令牌类型(如 Name.Builtin )。这意味着:当 regex 匹配,发出具有匹配文本和类型的令牌 tokentype 和推送 new_state 在状态堆栈上。如果新状态是 '#pop' ,则从堆栈中弹出最顶端的状态。要弹出多个状态,请使用 '#pop:2' 诸若此类。 '#push' 是第二次将当前状态推送到堆栈顶部的同义词。

下面的示例显示 DiffLexer 从内置的雷克萨斯。请注意,它包含一些附加属性 namealiasesfilenames 这不是雷克斯所需要的。它们由内置的lexer查找函数使用。::

from pygments.lexer import RegexLexer
from pygments.token import *

class DiffLexer(RegexLexer):
    name = 'Diff'
    aliases = ['diff']
    filenames = ['*.diff']

    tokens = {
        'root': [
            (r' .*\n', Text),
            (r'\+.*\n', Generic.Inserted),
            (r'-.*\n', Generic.Deleted),
            (r'@.*\n', Generic.Subheading),
            (r'Index.*\n', Generic.Heading),
            (r'=.*\n', Generic.Heading),
            (r'.*\n', Text),
        ]
    }

正如你所看到的,这个lexer只使用一种状态。当lexer开始扫描文本时,它首先检查当前字符是否为空格。如果为真,它将扫描所有内容,直到换行,并将数据作为 Text 令牌(即“无特殊突出显示”令牌)。

如果此规则不匹配,则检查当前字符是否为加号。等等。

如果当前位置没有匹配的规则,则当前字符将作为 Error 表示词法错误且位置增加1的标记。

使用词法分析器

使用新的lexer最简单的方法是使用pygments的支持从相对于当前目录的文件中加载lexer。

首先,将lexer类的名称更改为customlex:

from pygments.lexer import RegexLexer
from pygments.token import *

class CustomLexer(RegexLexer):
    """All your lexer code goes here!"""

然后您可以从命令行加载并测试带有附加标志的lexer -x

$ python -m pygments -x -l your_lexer_file.py <inputfile>

要指定除CustomLexer之外的类名,请将其附加冒号:

$ python -m pygments -x -l your_lexer.py:SomeLexer <inputfile>

或者,使用python API:

# For a lexer named CustomLexer
your_lexer = load_lexer_from_file(filename, **options)

# For a lexer named MyNewLexer
your_named_lexer = load_lexer_from_file(filename, "MyNewLexer", **options)

在加载自定义的lexer和格式化程序时,请特别小心地只使用受信任的文件;pygment将执行与 eval 在他们身上。

如果只想将lexer与pygments API一起使用,可以自己导入和实例化lexer,然后将其传递给 pygments.highlight() .

使用 -f 用于选择与终端转义序列不同的输出格式的标志。这个 HtmlFormatter 帮助您调试您的词法分析器。您可以使用 debug_token_types 用于显示分配给输入文件每个部分的标记类型的选项:

$ python -m pygments -x -f html -Ofull,debug_token_types -l your_lexer.py:SomeLexer <inputfile>

将鼠标悬停在每个令牌上可查看显示为工具提示的令牌类型。

如果你的词法分析器对其他人有用,我们希望你能把它贡献给Pygments。看见 助长了卑鄙行为 寻求建议。

正则表达式

您可以在regex中本地定义regex标志 (r'(?x)foo bar' )或通过添加 flags attribute to your lexer class. If no attribute is defined, it defaults to re.MULTILINE. For more information about regular expression flags see the page about regular expressions 在python文档中。

一次扫描多个令牌

到目前为止, action regex、action和state的规则元组中的元素是单个标记类型。现在我们来看看其他几个可能值中的第一个。

这里有一个更复杂的lexer,它突出显示ini文件。ini文件由节、注释和 key = value 对::

from pygments.lexer import RegexLexer, bygroups
from pygments.token import *

class IniLexer(RegexLexer):
    name = 'INI'
    aliases = ['ini', 'cfg']
    filenames = ['*.ini', '*.cfg']

    tokens = {
        'root': [
            (r'\s+', Text),
            (r';.*?$', Comment),
            (r'\[.*?\]$', Keyword),
            (r'(.*?)(\s*)(=)(\s*)(.*?)$',
             bygroups(Name.Attribute, Text, Operator, Text, String))
        ]
    }

lexer首先查找空白、注释和节名。稍后,它将查找一个看起来像键、值对的行,由 '=' 符号和可选空白。

这个 bygroups helper生成regex中具有不同令牌类型的每个捕获组。首先 Name.Attribute 令牌,然后 Text 用于可选空白的标记,之后是 Operator 等号的标记。然后一个 Text 再次标记空白。行的其余部分返回为 String .

请注意,要使这一点起作用,匹配的每个部分都必须在一个捕获组(a (...) )并且不能有任何嵌套的捕获组。如果仍然需要组,请使用使用此语法定义的非捕获组: (?:some|words|here) (注意 ?: 在开始括号之后)。

如果您发现自己需要一个regex内的捕获组,该捕获组不应该是输出的一部分,而是在用于回引用的正则表达式中使用(例如: r'(<(foo|bar)>)(.*?)(</\2>)' 你可以通过 None 到bygroups函数,该组将在输出中跳过。

变化状态

许多lexer需要多个状态才能按预期工作。例如,某些语言允许嵌套多行注释。因为这是一个递归模式,所以不可能只使用正则表达式来lex。

这里是一个识别C++风格注释的词汇表(多行) /* */ 和单线 // 直到行尾)::

from pygments.lexer import RegexLexer
from pygments.token import *

class CppCommentLexer(RegexLexer):
    name = 'Example Lexer with states'

    tokens = {
        'root': [
            (r'[^/]+', Text),
            (r'/\*', Comment.Multiline, 'comment'),
            (r'//.*?$', Comment.Singleline),
            (r'/', Text)
        ],
        'comment': [
            (r'[^*/]+', Comment.Multiline),
            (r'/\*', Comment.Multiline, '#push'),
            (r'\*/', Comment.Multiline, '#pop'),
            (r'[*/]', Comment.Multiline)
        ]
    }

这个lexer开始在 'root' 国家。它尽量匹配,直到找到一个斜线 ('/' ). 如果斜杠后面的下一个字符是星号 ('*'RegexLexer 将这两个字符发送到标记为的输出流 Comment.Multiline 并继续遵循 'comment' 状态。

如果斜线后面没有星号, RegexLexer 检查是否是单行注释(即后跟第二个斜杠)。如果不是这样的话,它必须是一个单斜杠,而不是注释起始符(也必须给出单个斜杠的单独正则表达式,否则斜杠将被标记为错误标记)。

里面 'comment' 国家,我们再做同样的事。扫描直到lexer找到一个星或斜线。如果是打开多行注释,请按 'comment' 堆栈上的状态并继续扫描,再次在 'comment' 状态。否则,检查它是否是多行注释的结尾。如果是,则从堆栈中弹出一个状态。

注意:如果从空堆栈中弹出,您将得到 IndexError . (有一个简单的方法可以防止这种情况发生:不要 '#pop' 在根状态)。

如果 RegexLexer 遇到标记为错误标记的换行符时,堆栈将被清空,lexer将继续在 'root' 状态。这有助于为错误输入生成容错突出显示,例如,当单行字符串未关闭时。

高级状态技巧

您还可以对状态执行以下操作:

  • 如果将一个元组而不是简单字符串作为规则元组中的第三项,则可以将多个状态推送到堆栈上。例如,如果要匹配包含指令的注释,请执行以下操作:

    /* <processing directive>    rest of comment */
    

    您可以使用以下规则:

    tokens = {
        'root': [
            (r'/\* <', Comment, ('comment', 'directive')),
            ...
        ],
        'directive': [
            (r'[^>]+', Comment.Directive),
            (r'>', Comment, '#pop'),
        ],
        'comment': [
            (r'[^*]+', Comment),
            (r'\*/', Comment, '#pop'),
            (r'\*', Comment),
        ]
    }
    

    当遇到上述样品时,首先 'comment''directive' are pushed onto the stack, then the lexer continues in the directive state until it finds the closing > ,然后继续处于注释状态,直到结束 */ . 然后,两个状态再次从堆栈中弹出,lexing继续处于根状态。

    在 0.9 版本加入: 元组可以包含 '#push''#pop' (但不是) '#pop:n' 指令。

  • 您可以在另一个州的定义中包含一个州的规则。这是通过使用 includepygments.lexer ::

    from pygments.lexer import RegexLexer, bygroups, include
    from pygments.token import *
    
    class ExampleLexer(RegexLexer):
        tokens = {
            'comments': [
                (r'(?s)/\*.*?\*/', Comment),
                (r'//.*?\n', Comment),
            ],
            'root': [
                include('comments'),
                (r'(function)( )(\w+)( )({)',
                 bygroups(Keyword, Whitespace, Name, Whitespace, Punctuation), 'function'),
                (r'.*\n', Text),
            ],
            'function': [
                (r'[^}/]+', Text),
                include('comments'),
                (r'/', Text),
                (r'\}', Punctuation, '#pop'),
            ]
        }
    

    对于由函数和注释组成的语言,这是一个假设的lexer。Because comments can occur at toplevel and in functions, we need rules for comments in both states. 如你所见, include 帮助程序保存多次发生的重复规则(在本例中,状态 'comment' 不会被莱克斯进入,因为它只会被包括在 'root''function'

  • 有时,您可能希望从现有状态“组合”一个状态。这是可能的 combined 帮助者 pygments.lexer .

    如果你,而不是一个新的状态,写 combined('state1', 'state2') 作为规则元组的第三项,将从state1和state2形成新的匿名状态,如果规则匹配,lexer将进入此状态。

    这不是经常使用的,但在某些情况下是有用的,例如 PythonLexer 的字符串文本处理。

  • 如果希望lexer以不同的状态开始词法分析,可以通过重写“get_tokens_unprocessed()”方法来修改堆栈:

    from pygments.lexer import RegexLexer
    
    class ExampleLexer(RegexLexer):
        tokens = {...}
    
        def get_tokens_unprocessed(self, text, stack=('root', 'otherstate')):
            for item in RegexLexer.get_tokens_unprocessed(self, text, stack):
                yield item
    

    一些雷克斯 PhpLexer 用这个做领导 <?php 预处理器注释可选。请注意,通过将令牌映射中不存在的值放入堆栈,您可以轻松地崩溃lexer。也去除 'root' 从堆栈中可能会导致奇怪的错误!

  • 在某些lexer中,如果遇到与状态中的规则不匹配的情况,则应弹出状态。您可以在状态列表的末尾使用空regex,但pygments提供了一种更明显的拼写方法: default('#pop') 等于 ('', Text, '#pop') .

    在 2.0 版本加入.

从regexlexer派生的子类lexer

在 1.6 版本加入.

有时,多种语言非常相似,但仍然应该由不同的lexer类进行lexed。

当从regexlexer派生的lexer子类化时, tokens 父类和子类中定义的字典将合并。例如::

from pygments.lexer import RegexLexer, inherit
from pygments.token import *

class BaseLexer(RegexLexer):
    tokens = {
        'root': [
            ('[a-z]+', Name),
            (r'/\*', Comment, 'comment'),
            ('"', String, 'string'),
            (r'\s+', Text),
        ],
        'string': [
            ('[^"]+', String),
            ('"', String, '#pop'),
        ],
        'comment': [
            ...
        ],
    }

class DerivedLexer(BaseLexer):
    tokens = {
        'root': [
            ('[0-9]+', Number),
            inherit,
        ],
        'string': [
            (r'[^"\\]+', String),
            (r'\\.', String.Escape),
            ('"', String, '#pop'),
        ],
    }

这个 BaseLexer 定义两种状态,命名和字符串。这个 DerivedLexer 定义它自己的tokens字典,它扩展了基本lexer的定义:

  • “根”状态有一个附加规则,然后是特殊对象 inherit ,它告诉Pygments在该点插入父类的标记定义。

  • “字符串”状态被完全替换,因为 inherit 规则。

  • “注释”状态是完全继承的。

使用多个lexer

对同一输入使用多个lexer可能很困难。这里展示了最简单的组合技术之一:您可以用lexer类替换规则元组中的操作条目。然后,匹配的文本将与该lexer进行lexed,并生成结果标记。

例如,看看这个精简的HTML lexer::

from pygments.lexer import RegexLexer, bygroups, using
from pygments.token import *
from pygments.lexers.javascript import JavascriptLexer

class HtmlLexer(RegexLexer):
    name = 'HTML'
    aliases = ['html']
    filenames = ['*.html', '*.htm']

    flags = re.IGNORECASE | re.DOTALL
    tokens = {
        'root': [
            ('[^<&]+', Text),
            ('&.*?;', Name.Entity),
            (r'<\s*script\s*', Name.Tag, ('script-content', 'tag')),
            (r'<\s*[a-zA-Z0-9:]+', Name.Tag, 'tag'),
            (r'<\s*/\s*[a-zA-Z0-9:]+\s*>', Name.Tag),
        ],
        'script-content': [
            (r'(.+?)(<\s*/\s*script\s*>)',
             bygroups(using(JavascriptLexer), Name.Tag),
             '#pop'),
        ]
    }

这里的内容是 <script> 将标记传递给新创建的 JavascriptLexer 而不是由 HtmlLexer . 这是通过使用 using 将其他lexer类作为其参数的帮助程序。

注意 bygroupsusing . 这样可以确保内容符合 </script> 结束标记由 JavascriptLexer ,而结束标记作为普通标记生成 Name.Tag 类型。

还注意到 (r'<\s*script\s*', Name.Tag, ('script-content', 'tag')) 规则。这里,两个状态被推到状态堆栈上, 'script-content''tag' . 这意味着首先 'tag' 处理,这将使属性和结束 > 然后 'tag' 弹出状态,堆栈顶部的下一个状态将为 'script-content' .

由于不能引用当前定义的类,请使用 this (进口自 pygments.lexer )引用当前的lexer类,即。 using(this) . 这种构造似乎是不必要的,但这通常是在固定定界符之间使用任意语法而不引入深度嵌套状态的最明显方法。

这个 using() 助手有一个特殊的关键字参数, state ,其工作原理如下:如果给定,则最初要使用的lexer不在 "root" 状态,但处于此参数给定的状态。This does not work with advanced RegexLexer 子类,如 ExtendedRegexLexer (见下文)。

传递给“using()”的任何其他关键字参数都添加到用于创建lexer的关键字参数中。

委托lexer

嵌套lexer的另一种方法是 DelegatingLexer 例如,用于模板引擎lexer。它以两个lexer作为初始化的参数:a root_lexer 和A language_lexer .

输入处理如下:首先,整个文本用 language_lexer . 所有使用特殊类型的 Other 然后连接并赋予 root_lexer . 语言符号 language_lexer 然后插入到 root_lexer 的令牌流。:

from pygments.lexer import DelegatingLexer
from pygments.lexers.web import HtmlLexer, PhpLexer

class HtmlPhpLexer(DelegatingLexer):
    def __init__(self, **options):
        super().__init__(HtmlLexer, PhpLexer, **options)

此过程确保,例如,即使将模板标记放入HTML标记或属性中,其中包含模板标记的HTML也会正确突出显示。

如果你想换针牌 Other 对于其他对象,您可以为lexer提供另一个令牌类型作为第三个参数::

DelegatingLexer.__init__(MyLexer, OtherLexer, Text, **options)

回调

有时一种语言的语法是如此复杂,以至于lexer不能仅仅通过使用正则表达式和堆栈来处理它。

为此, RegexLexer 允许在规则元组中提供回调,而不是令牌类型 (bygroupsusing 只是预先实现的回调)。回调必须是具有两个参数的函数:

  • 雷克斯本身

  • 上次匹配规则的匹配对象

然后回调必须返回ITerable of(或只是yield) (index, tokentype, value) 元组,然后通过 get_tokens_unprocessed() . 这个 index 这是令牌在输入字符串中的位置, tokentype 是普通的令牌类型(比如 Name.Builtinvalue 输入字符串的关联部分。

您可以在这里看到一个示例:

from pygments.lexer import RegexLexer
from pygments.token import Generic

class HypotheticLexer(RegexLexer):

    def headline_callback(lexer, match):
        equal_signs = match.group(1)
        text = match.group(2)
        yield match.start(), Generic.Headline, equal_signs + text + equal_signs

    tokens = {
        'root': [
            (r'(=+)(.*?)(\1)', headline_callback)
        ]
    }

如果正则表达式用于 headline_callback 匹配,用匹配对象调用函数。请注意,回调完成后,处理将正常继续,即在上一个匹配结束后继续。回调不可能影响位置。

对于lexer回调,实际上没有任何简单的示例,但是您可以看到它们的实际操作,例如 SMLLexer class in ml.py .

extendedRegexLexer类

这个 RegexLexer ,即使使用回调,不幸的是,对于Ruby这样的语言的语法规则来说,功能还不够强大。

但不要害怕;即使这样,也不必放弃正则表达式方法:Pygments有一个 RegexLexer , the ExtendedRegexLexer . 所有已知的RegexLexers特性也都可以在这里使用,并且令牌的指定方式完全相同, 除了 一个细节:

这个 get_tokens_unprocessed() 方法不将其内部状态数据保存为局部变量,而是保存在 pygments.lexer.LexerContext 类,该实例作为第三个参数传递给回调。这意味着您可以在回调中修改lexer状态。

这个 LexerContext 类具有以下成员:

  • text --输入文本

  • pos --用于匹配正则表达式的当前起始位置

  • stack --包含状态堆栈的列表

  • end -- the maximum position to which regexes are matched, this defaults to the length of text

另外, get_tokens_unprocessed() 方法可以给定 LexerContext 而不是字符串,然后将处理此上下文,而不是为字符串参数创建新上下文。

请注意,由于您可以将当前位置设置为回调中的任何内容,因此调用方不会在回调完成后自动设置该位置。例如,这就是上面假设的lexer如何用 ExtendedRegexLexer ::

from pygments.lexer import ExtendedRegexLexer
from pygments.token import Generic

class ExHypotheticLexer(ExtendedRegexLexer):

    def headline_callback(lexer, match, ctx):
        equal_signs = match.group(1)
        text = match.group(2)
        yield match.start(), Generic.Headline, equal_signs + text + equal_signs
        ctx.pos = match.end()

    tokens = {
        'root': [
            (r'(=+)(.*?)(\1)', headline_callback)
        ]
    }

这听起来可能令人困惑(实际上可能是)。但它是必需的,举个例子来看 ruby.py .

处理关键字列表

对于相对较短的列表(数百个),可以直接使用 words() (较长的列表,请参见下一节)。这个函数自动为您处理一些事情,包括转义元字符和Python的第一个匹配项,而不是交替的最长匹配项。把清单放进去吧 pygments/lexers/_$lang_builtins.py (请参阅此处的示例),如果可能,则由代码生成。

使用示例 words() 类似于:

from pygments.lexer import RegexLexer, words, Name

class MyLexer(RegexLexer):

    tokens = {
        'root': [
            (words(('else', 'elseif'), suffix=r'\b'), Name.Builtin),
            (r'\w+', Name),
        ],
    }

如你所见,你可以添加 prefixsuffix 构成Regex的部件。

修改令牌流

有些语言提供了许多内置函数(例如php)。这些功能的总量因系统而异,因为并非每个人都安装了每个扩展。对于PHP,有超过3000个内置函数。这是一个非常庞大的函数,远远超过了您想要放入正则表达式的数量。

但因为只有 Name 标记可以是函数名这可以通过重写 get_tokens_unprocessed() 方法。下面的lexer子类 PythonLexer 因此,它将一些附加名称突出显示为伪关键字:

from pygments.lexers.python import PythonLexer
from pygments.token import Name, Keyword

class MyPythonLexer(PythonLexer):
    EXTRA_KEYWORDS = set(('foo', 'bar', 'foobar', 'barfoo', 'spam', 'eggs'))

    def get_tokens_unprocessed(self, text):
        for index, token, value in PythonLexer.get_tokens_unprocessed(self, text):
            if token is Name and value in self.EXTRA_KEYWORDS:
                yield index, Keyword.Pseudo, value
            else:
                yield index, token, value

这个 PhpLexerLuaLexer 使用此方法解析内置函数。

常见陷阱和最佳实践

正则表达式在Pygments词法分析器中随处可见。我们写这一节是为了警告你在使用它们时可能会犯的几个常见错误。还有一些让你的词法分析器更容易阅读和复习的小贴士。如果你想贡献一个新的词法分析器,你需要阅读这一节,但你可能会发现它在任何情况下都很有用。

  • 在编写规则时,尽量合并简单的规则。例如,Combine::

    (r"\(", token.Punctuation),
    (r"\)", token.Punctuation),
    (r"\[", token.Punctuation),
    (r"\]", token.Punctuation),
    ("{", token.Punctuation),
    ("}", token.Punctuation),
    

    转到::

    (r"[\(\)\[\]{}]", token.Punctuation)
    
  • 小心地对待 .* 。这与它能达到的最大程度的匹配。例如,像这样的规则 @.*@ 将匹配整个字符串 @first@ second @third@ ,而不是匹配 @first@@second@ 。您可以使用 @.*?@ 在这种情况下,提前停止。这个 ? 试图匹配 as few times 尽可能的。

  • 谨防所谓的“灾难性倒退”。作为第一个示例,请考虑正则表达式 (A+)*B 。这相当于 A*B 关于它匹配的内容,但是 non -比赛将需要很长时间。这是因为正则表达式引擎的工作方式。假设你喂它50个‘A’,最后一个‘C’。它首先与A的贪婪相匹配 A+ ,但发现它不能匹配结尾,因为‘B’与‘C’不同。然后它返回,从第一个中删除一个‘A’ A+ 并试图将其余的作为另一个匹配 (A+)* 。这再次失败,因此它在输入字符串中进一步向左回溯,依此类推。实际上,它尝试了所有组合

    (AAAAAAAAAAAAAAAAA)
    (AAAAAAAAAAAAAAAA)(A)
    (AAAAAAAAAAAAAAA)(AA)
    (AAAAAAAAAAAAAAA)(A)(A)
    (AAAAAAAAAAAAAA)(AAA)
    (AAAAAAAAAAAAAA)(AA)(A)
    ...
    

    因此,匹配具有指数复杂度。在词法分析器中,其效果是在解析无效输入时,Pygments似乎会挂起。**

    >>> import re
    >>> re.match('(A+)*B', 'A'*50 + 'C') # hangs
    

    作为一个更微妙、更真实的示例,下面是一个写得很糟糕的正则表达式来匹配字符串:

    r'"(\\?.)*?"'
    

    If the ending quote is missing, the regular expression engine will find that it cannot match at the end, and try to backtrack with less matches in the *?. When it finds a backslash, as it has already tried the possibility \\., it tries . (recognizing it as a simple character without meaning), which leads to the same exponential backtracking problem if there are lots of backslashes in the (invalid) input string. A good way to write this would be r'"([^\\]|\\.)*?"', where the inner group can only match in one way. Better yet is to use a dedicated state, which not only sidesteps the issue without headaches, but allows you to highlight string escapes.

    'root': [
        ...,
        (r'"', String, 'string'),
        ...
    ],
    'string': [
        (r'\\.', String.Escape),
        (r'"', String, '#pop'),
        (r'[^\\"]+', String),
    ]
    
  • 在编写注释或字符串等模式的规则时,请在每个令牌中匹配尽可能多的字符。这是一个例子,说明了 not 要做的事:

    'comment': [
        (r'\*/', Comment.Multiline, '#pop'),
        (r'.', Comment.Multiline),
    ]
    

    这会在注释中为每个字符生成一个令牌,这会减慢词法分析过程,还会使原始令牌输出(尤其是测试输出)难以阅读。改为这样做::

    'comment': [
        (r'\*/', Comment.Multiline, '#pop'),
        (r'[^*]+', Comment.Multiline),
        (r'\*', Comment.Multiline),
    ]