编写自己的词法分析器

如果 Pygments 包中缺少你最喜欢的语言的词法分析器,你可以轻松地编写自己的词法分析器并扩展 Pygments。

你所需的一切都可以找到在 pygments.lexer 模块中。正如你可以在 API 文档 中阅读到的,词法分析器是一个类,它用一些关键字参数(词法分析器选项)初始化,并提供一个 get_tokens_unprocessed() 方法,该方法接受一个字符串或 unicode 对象,其中包含要进行词法分析的数据。

The get_tokens_unprocessed() 方法必须返回一个迭代器或可迭代对象,其中包含形式为 (index, token, value) 的元组。通常你不需要这样做,因为有一些基本词法分析器可以完成大部分工作,你可以对它们进行子类化。

如何添加词法分析器

要添加一个词法分析器,你必须执行以下步骤

  • pygments/lexers 下选择一个匹配的模块,或者为你的词法分析器类创建一个新模块。

    注意

    我们鼓励你将你的词法分析器类放在它自己的模块中,除非它是一个非常小的现有词法分析器的衍生品。

  • 接下来,确保词法分析器从模块外部已知。pygments.lexers 包中的所有模块都指定 __all__。例如,automation.py 设置

    __all__ = ['AutohotkeyLexer', 'AutoItLexer']
    

    将你的词法分析器类的名称添加到此列表中(或者如果你的词法分析器是模块中唯一的类,则创建该列表)。

  • 最后,可以通过重建词法分析器映射来使词法分析器公开已知。

    $ tox -e mapfiles
    

如何测试你的词法分析器

要添加一个新的词法分析器测试,请在 tests/snippets/<lexer_alias>/ 下创建一个仅包含你的代码片段的文件。然后运行 tox -- --update-goldens <filename.txt> 来自动填充当前预期的标记。检查它们看起来是否正常,然后检入该文件。

词法分析器测试使用 tox 运行,就像所有其他测试一样。在编写词法分析器时,你也可以只运行该词法分析器的测试,方法是使用 tox -- tests/snippets/language-name/ 和/或 tox -- tests/examplefiles/language-name/

使用 tox 运行测试套件将在测试输入上运行词法分析器,并检查输出是否与预期标记匹配。如果你正在改进词法分析器,标记输出发生变化是正常的。要更新测试的预期标记输出,请再次使用 tox -- --update-goldens <filename.txt>。查看更改并检查它们是否按预期进行,然后将它们与你提议的代码更改一起提交。

大型测试文件应放在 tests/examplefiles 中。这与 snippets 类似,但标记输出存储在单独的文件中。输出也可以使用 --update-goldens 重新生成。

注意

在贡献一个新的词法分析器时,你_必须_提供一个示例文件或测试片段。无法测试的词法分析器将不会被接受。

RegexLexer

Pygments 大多数词法分析器使用的词法分析器基类是 RegexLexer。这个类允许你用不同_状态_的_正则表达式_来定义词法分析规则。

状态是正则表达式的集合,这些正则表达式与_当前位置_的输入字符串匹配。如果这些表达式中的一个匹配,则执行相应的操作(例如,生成具有特定类型的标记,或更改状态),当前位置设置为上次匹配结束的位置,匹配过程从当前状态的_第一个_正则表达式继续。

注意

这意味着你总是跳回到第一个条目,即你不能按特定顺序匹配状态。例如,具有以下规则的状态将无法按预期工作

'state': [
    (r'\w+', Name,),
    (r'\s+', Whitespace,),
    (r'\w+', Keyword,)
]

在上面的示例中,Keyword 将永远不会匹配。要按顺序匹配某些标记类型,请参阅下面的 bygroups 助手。

词法分析器状态保存在堆栈中:每次进入新状态时,都会将新状态压入堆栈。最基本的词法分析器(如 DiffLexer)只需要一个状态。

每个状态都被定义为一个元组列表,其形式为 (regexactionnew_state),其中最后一项是可选的。在最基本的形式中,action 是一个标记类型(如 Name.Builtin)。这意味着:当 regex 匹配时,用匹配文本和类型 tokentype 发出一个标记,并将 new_state 压入状态堆栈。如果新状态为 '#pop',则改为从堆栈中弹出最顶部的状态。要弹出多个状态,请使用 '#pop:2' 等等。'#push' 是在堆栈顶端第二次压入当前状态的同义词。

以下示例显示了来自内置词法分析器的 DiffLexer。请注意,它包含一些额外的属性 namealiasesfilenames,这些属性对于词法分析器来说不是必需的。它们被内置词法分析器查找函数使用。

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),
        ]
    }

如你所见,这个词法分析器只使用一个状态。当词法分析器开始扫描文本时,它首先检查当前字符是否为空格。如果为真,它会扫描所有内容,直到换行符,并将数据作为 Text 标记返回(这是“无特殊高亮”标记)。

如果此规则不匹配,它会检查当前字符是否为加号。依此类推。

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

使用词法分析器

使用新词法分析器的最简单方法是使用 Pygments 支持从当前目录的相对路径加载词法分析器。

首先,将你的词法分析器类的名称更改为 CustomLexer

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

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

然后你可以使用附加标志 -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)

在加载自定义词法分析器和格式化器时,务必小心谨慎,只使用受信任的文件;Pygments 将对它们执行与 eval 等效的操作。

如果你只想将你的词法分析器与 Pygments API 一起使用,你可以自己导入和实例化词法分析器,然后将其传递给 pygments.highlight()

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

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

将鼠标悬停在每个标记上,以查看显示为工具提示的标记类型。

如果你的词法分析器对其他人有用,我们希望你将它贡献给 Pygments。有关建议,请参阅 贡献给 Pygments

正则表达式标记

你可以在正则表达式中本地定义正则表达式标记 (r'(?x)foo bar'),也可以通过向词法分析器类添加 flags 属性来全局定义。如果没有定义属性,则默认值为 re.MULTILINE。有关正则表达式标记的更多信息,请参阅 Python 文档中关于 正则表达式 的页面。

一次扫描多个标记

到目前为止,正则表达式、动作和状态的规则元组中的 action 元素一直是单个标记类型。现在我们来看看几个其他可能值的第一个。

这里是一个更复杂的词法分析器,它突出显示 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+', Whitespace),
            (r';.*', Comment),
            (r'\[.*?\]$', Keyword),
            (r'(.*?)(\s*)(=)(\s*)(.*)',
             bygroups(Name.Attribute, Whitespace, Operator, Whitespace, String))
        ]
    }

词法分析器首先查找空格、注释和节名称。之后,它会查找看起来像键值对的行,用 '=' 符号分隔,并带有可选的空格。

The bygroups 助手使用不同的标记类型生成正则表达式中的每个捕获组。首先是 Name.Attribute 标记,然后是 Text 标记,用于可选的空格,之后是 Operator 标记,用于等号。然后是 Text 标记,用于再次使用空格。该行的其余部分将作为 String 返回。

请注意,为了使此方法起作用,匹配的每个部分都必须位于捕获组((...))中,并且不能有嵌套的捕获组。如果您仍然需要使用组,请使用此语法定义非捕获组:(?:some|words|here)(注意开始括号后的 ?:)。

如果您发现需要在正则表达式中使用一个捕获组,而该组不应作为输出的一部分,但用于正则表达式中的反向引用(例如:r'(<(foo|bar)>)(.*?)(</\2>)'),您可以将 None 传递给 bygroups 函数,该组将被跳过输出。

更改状态

许多词法分析器需要多个状态才能按预期工作。例如,某些语言允许多行注释嵌套。由于这是一个递归模式,因此无法仅使用正则表达式进行词法分析。

这是一个识别 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)
        ]
    }

此词法分析器从 'root' 状态开始词法分析。它尝试尽可能多地匹配,直到找到斜杠('/')。如果斜杠后的下一个字符是星号('*'),RegexLexer 将这两个字符发送到输出流,标记为 Comment.Multiline,并继续使用在 'comment' 状态中定义的规则进行词法分析。

如果没有星号出现在斜杠之后,RegexLexer 将检查它是否为单行注释(即,后面跟着第二个斜杠)。如果这种情况也不存在,它必须是一个单斜杠,它不是注释的开始(必须提供单斜杠的单独正则表达式,否则斜杠将被标记为错误标记)。

'comment' 状态中,我们再次执行相同的操作。扫描,直到词法分析器找到星号或斜杠。如果它是多行注释的开始,则将 'comment' 状态压入堆栈,并继续扫描,再次处于 'comment' 状态。否则,检查它是否为多行注释的结束。如果是,则从堆栈中弹出状态。

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

如果 RegexLexer 遇到一个标记为错误标记的换行符,堆栈将被清空,词法分析器将继续在 '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' 压入堆栈,然后词法分析器继续处于 directive 状态,直到找到关闭的 >,然后它继续处于 comment 状态,直到找到关闭的 */。然后,两个状态再次从堆栈中弹出,词法分析器继续处于根状态。

    在版本 0.9 中添加: 元组可以包含特殊的 '#push''#pop'(但不能 '#pop:n')指令。

  • 您可以将一个状态的规则包含在另一个状态的定义中。这是通过使用 pygments.lexer 中的 include 来完成的。

    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'),
            ]
        }
    

    这是一个针对由函数和注释组成的语言的假设词法分析器。由于注释可能出现在顶层和函数中,因此我们需要在两种状态中都使用注释规则。如您所见,include 助手避免了重复多次出现的规则(在此示例中,状态 'comment' 永远不会被词法分析器进入,因为它只存在于 'root''function' 中)。

  • 有时,您可能想要“组合”来自现有状态的状态。这可以通过使用 pygments.lexer 中的 combined 助手来实现。

    如果您没有创建新状态,而是将 combined('state1', 'state2') 作为规则元组的第三个元素,则将从 state1 和 state2 形成一个新的匿名状态,如果规则匹配,词法分析器将进入此状态。

    这并不经常使用,但在某些情况下可能会有所帮助,例如 PythonLexer 的字符串文字处理。

  • 如果您希望词法分析器从不同的状态开始词法分析,则可以通过覆盖 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 预处理器注释可选。请注意,通过将不存在于标记映射中的值放入堆栈,您很容易使词法分析器崩溃。同样,从堆栈中删除 'root' 可能会导致奇怪的错误!

  • 在某些词法分析器中,如果遇到任何与状态中规则不匹配的内容,则应弹出状态。您可以在状态列表的末尾使用空正则表达式,但 Pygments 提供了一种更明显的方法来表达这一点:default('#pop') 等效于 ('', Text, '#pop')

    在版本 2.0 中添加。

从 RegexLexer 派生的词法分析器的子类化

在版本 1.6 中添加。

有时,多种语言非常相似,但仍应由不同的词法分析器类进行词法分析。

当对从 RegexLexer 派生的词法分析器进行子类化时,父类和子类中定义的 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+', Whitespace),
        ],
        'string': [
            ('[^"]+', String),
            ('"', String, '#pop'),
        ],
        'comment': [
            ...
        ],
    }

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

BaseLexer 定义了两个状态,词法分析名称和字符串。DerivedLexer 定义了自己的 token 字典,该字典扩展了基本词法分析器的定义。

  • “root”状态具有一个额外的规则,然后是特殊对象 inherit,它告诉 Pygments 在该位置插入父类的 token 定义。

  • 由于没有 inherit 规则,“string”状态完全被替换。

  • “comment”状态完全被继承。

使用多个词法分析器

对同一个输入使用多个词法分析器可能很棘手。这里显示了一种最简单的组合技术:您可以用词法分析器类替换规则元组中的操作条目。然后,匹配的文本将使用该词法分析器进行词法分析,并且生成的标记将被生成。

例如,请查看此简化的 HTML 词法分析器。

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 助手完成的,该助手以另一个词法分析器类作为参数。

注意 bygroupsusing 的组合。这确保了直到 </script> 结束标签的内容由 JavascriptLexer 处理,而结束标签则作为具有 Name.Tag 类型的普通标记生成。

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

由于您无法引用当前正在定义的类,请使用 this(从 pygments.lexer 导入)来引用当前词法分析器类,即 using(this)。这种结构可能看起来没有必要,但这通常是在固定分隔符之间词法分析任意语法而不引入深度嵌套状态的最明显方法。

using() 助手有一个特殊的关键字参数 state,它的工作原理如下:如果给出,则要使用的词法分析器最初不处于 "root" 状态,而是处于此参数给出的状态。这对于高级 RegexLexer 子类(例如 ExtendedRegexLexer)不起作用(见下文)。

传递给 using() 的任何其他关键字参数都将添加到用于创建词法分析器的关键字参数中。

委托词法分析器

另一种用于嵌套词法分析器的方案是 DelegatingLexer,例如用于模板引擎词法分析器。它在初始化时接受两个词法分析器作为参数:root_lexerlanguage_lexer

输入的处理方式如下:首先,整个文本使用 language_lexer 进行词法分析。所有使用 Other 的特殊类型生成的标记都会被连接起来,然后传递给 root_lexerlanguage_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 更改为其他内容,可以将另一个标记类型作为第三个参数传递给词法分析器。

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

回调

有时,语言的语法非常复杂,词法分析器无法仅通过使用正则表达式和堆栈来处理它。

为此,RegexLexer 允许在规则元组中提供回调,而不是标记类型(bygroupsusing 只是预实现的回调)。回调必须是一个接受两个参数的函数

  • 词法分析器本身

  • 最后一个匹配规则的匹配对象

然后,回调必须返回一个包含(或简单地生成)(index, tokentype, value) 元组的可迭代对象,这些元组将被 get_tokens_unprocessed() 直接传递。这里的 index 是标记在输入字符串中的位置,tokentype 是正常的标记类型(如 Name.Builtin),而 value 是关联的输入字符串部分。

您可以在此处看到一个示例。

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` 的正则表达式匹配成功,则会调用该函数,并将匹配对象作为参数传入。注意,在回调函数执行完毕后,处理过程会正常继续,即从上一次匹配结束的位置继续。回调函数无法影响匹配的位置。

关于词法分析器回调函数,并没有简单易懂的示例,但你可以在例如 ml.py 中的 `SMLLexer` 类中看到它们是如何工作的。

ExtendedRegexLexer 类

即使使用回调函数,`RegexLexer` 仍然不足以处理诸如 Ruby 语言等具有复杂语法规则的语言。

但别担心,即使在这种情况下,你也不必放弃正则表达式方法:Pygments 提供了 `RegexLexer` 的一个子类,名为 `ExtendedRegexLexer`。这里保留了 `RegexLexers` 的所有已知功能,并且令牌的指定方式也完全相同,除了一个细节。

该 `get_tokens_unprocessed()` 方法不将内部状态数据存储为局部变量,而是存储在 `pygments.lexer.LexerContext` 类的实例中,并且该实例作为第三个参数传递给回调函数。这意味着你可以在回调函数中修改词法分析器状态。

该 `LexerContext` 类包含以下成员

  • text – 输入文本

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

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

  • end – 对正则表达式进行匹配的最大位置,默认为 `text` 的长度

此外,该 `get_tokens_unprocessed()` 方法可以接受一个 `LexerContext` 而不是一个字符串,然后它将处理该上下文,而不是为字符串参数创建一个新的上下文。

注意,由于你可以在回调函数中将当前位置设置为任何值,因此在回调函数执行完毕后,调用者不会自动设置该位置。例如,以下是使用 `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 中的 Ruby 词法分析器。

处理关键字列表

对于相对较短的列表(数百个),你可以使用 `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),
        ],
    }

正如你所见,你可以添加 `prefix` 和 `suffix` 部分到构造的正则表达式中。

修改令牌流

一些语言提供了大量的内置函数(例如 PHP)。这些函数的总数因系统而异,因为并非所有人都安装了所有扩展。在 PHP 的情况下,有超过 3000 个内置函数。这是一个非常庞大的数量,远远超过了你想要放在正则表达式中的数量。

但由于只有 `Name` 令牌可以是函数名,因此可以通过覆盖 `get_tokens_unprocessed()` 方法来解决这个问题。以下词法分析器是 `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

该 `PhpLexer` 和 `LuaLexer` 使用此方法来解析内置函数。

常见的陷阱和最佳实践

正则表达式在 Pygments 词法分析器中无处不在。我们编写了本节来提醒你使用正则表达式时可能遇到的几个常见错误。还有一些关于如何使你的词法分析器更易于阅读和审查的提示。如果你想贡献一个新的词法分析器,你需要阅读本节,但在任何情况下阅读本节都很有帮助。

  • 编写规则时,尝试合并简单的规则。例如,将

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

    合并为

    (r"[\(\)\[\]{}]", token.Punctuation)
    
  • 小心使用 `.*`。它会贪婪地匹配尽可能多的字符。例如,像 `@.*@` 这样的规则将匹配整个字符串 `@first@ second @third@`,而不是匹配 `@first@` 和 `@third@`。在这种情况下,你可以使用 `@.*?@` 来提前停止匹配。`?` 试图匹配尽可能少的次数。

  • 注意所谓的“灾难性回溯”。作为第一个示例,考虑正则表达式 `(A+)*B`。就匹配内容而言,它等效于 `A*B`,但不匹配将花费很长时间。这是由于正则表达式引擎的工作方式。假设你给它 50 个 ‘A’ 以及结尾处的 ‘C’。它首先贪婪地匹配 `A+` 中的 ‘A’s,但发现由于 ‘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'"(\\?.)*?"'
    

    如果缺少结尾引号,正则表达式引擎将发现它无法匹配结尾,并尝试使用 `*?` 中的更少匹配项进行回溯。当它找到一个反斜杠时,因为它已经尝试了 `\\.` 的可能性,所以它会尝试 `.`(将其识别为一个简单的字符,没有特殊含义),如果在(无效)输入字符串中有很多反斜杠,这会导致同样的指数级回溯问题。一个很好的写法是 `r'"([^\\]|\\.)*?"'`,其中内部组只能以一种方式匹配。更好的方法是使用专门的状态,这不仅可以避免这个问题,而且还可以让你突出显示字符串转义。

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

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

    这会为注释中的每个字符生成一个令牌,这会减慢词法分析过程的速度,并且也会使原始令牌输出(特别是测试输出)难以阅读。改用以下方法

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