tokenize --- Python 源代码的分词器

源码: Lib/tokenize.py


tokenize 模块为 Python 源代码提供了一个用 Python 实现的词法扫描器。 该模块中的扫描器还会将注释作为标记返回,这使得它可以用于实现"美化打印器",包括用于屏幕显示的代码着色器。

为简化令牌流处理,所有 operator 和 delimiter 令牌以及 Ellipsis 均通过通用的 OP 令牌类型返回;TokenInfo.exact_type 属性则提供确切的令牌类型。

警告

请注意本模块中的函数被设计为仅能解析符合语法的 Python 代码(当使用 ast.parse() 解析代码时不会引发异常)。 在提供无效的 Python 代码时本模块中函数的行为是 未定义 的并可能在任何时候发生改变。

对输入进行分词

主要的入口是一个 generator:

tokenize.tokenize(readline)

生成器 tokenize() 需要一个 readline 参数,这个参数必须是一个可调用对象,且能提供与文件对象的 io.IOBase.readline() 方法相同的接口。每次调用这个函数都要 返回字节类型输入的一行数据。

生成器生成 TokenInfo 个对象。

在 3.1 版本发生变更: 已更改为生成 TokenInfo 对象,而非普通元组。

根据 PEP 263 ,tokenize() 通过寻找 UTF-8 BOM 或编码 cookie 来确定文件的源编码。

tokenize.generate_tokens(readline)

对读取 unicode 字符串而不是字节的源进行标记。

和 tokenize() 一样, readline 参数是一个返回单行输入的可调用参数。然而, generate_tokens() 希望 readline 返回一个 str 对象而不是字节。

其结果是一个可生成 TokenInfo 对象的迭代器,与 tokenize() 完全相同;它不会生成 ENCODING 令牌。

class tokenize.TokenInfo(type, string, start, end, line)

A named tuple 用于描述一个令牌。它包含五个字段和一个额外的 exact_type 属性。

在 3.1 版本加入.

type

一个用于标识令牌类型的整数,其类型由 token 中的常量定义,例如 NAME 和 STRING。运算符和分隔符的类型为 OP;对于其特定的令牌类型,请使用 exact_type。

string

将令牌文本视为 str。对于 STRING 令牌,这包括引号、任何前缀以及转义序列,但不会对其进行评估。

对于 ENCODING 令牌,这就是其编码名称。未插入相应源文本的令牌可以包含空字符串。对于 FSTRING_MIDDLE 和 TSTRING_MIDDLE 令牌,双花括号会被替换为单花括号。

start

一个由整型组成的 2 元组 (srow, scol),用于指定该标记在源数据中的包含性起始位置。行从 1 开始编号,列从 0 开始编号。列按 Unicode 码点计数;一个制表符计作一列,不会将其扩展为制表位。

对于 ENCODING 代币,这是 (0, 0)。

end

一个由整型组成的 2-tuple (erow, ecol),用于指定 token 的独占结束位置。行号从 1 开始,列号从 0 开始。对于跨越多行的 token,erow 表示最后一行,ecol 表示该行中的列号。

例如,第一行开头的名称 spam 中,start 等于 (1, 0),而 end 等于 (1, 4)。

对于 ENCODING 令牌,此处为 (0, 0)。若将 NEWLINE 或 NL 令牌置于输入末尾且未添加行尾换行符,则其结束列可能比源文本多出 1 个字符。

line

str 包含发现该标记的完整源行内容,包括周围文本以及存在的行终止符。对于跨越多行的标记,该字段可包含多行源行内容;对于 ENCODING 和 ENDMARKER 标记,该字段为空。

exact_type

一个用于标识 OP 令牌具体操作符类型的整数,其值由 token 中的常量定义。对于所有其他令牌类型,该值等于 type。例如,对于 + 令牌,type 等于 OP,而 exact_type 等于 PLUS。

在 3.3 版本加入.

token 模块中的所有常量也会从 tokenize 中导出。

提供了另一个函数来逆转标记化过程。这对于创建对脚本进行标记、修改标记流并写回修改后脚本的工具很有用。

tokenize.untokenize(iterable)

将令牌转换为 Python 源代码。 iterable 必须返回至少有两个元素的序列,即令牌类型和令牌字符串。任何额外的序列元素都会被忽略。

结果将保证会被词元化为与输入相匹配因此转换是无损的并且确保可以来回反复。 此项保证只适用于词元类型和词元字符串因为词元之间的空位(列位置)可能发生改变。

它返回字节,使用 ENCODING 标记进行编码,这是由 tokenize() 输出的第一个标记序列。如果输入中没有编码令牌,它将返回一个字符串。

tokenize() 需要检测它所标记源文件的编码。它用来做这件事的函数是可用的:

tokenize.detect_encoding(readline)

detect_encoding() 函数用于检测解码 Python 源文件时应使用的编码。它需要一个参数, readline ,与 tokenize() 生成器的使用方式相同。

它最多调用 readline 两次,并返回所使用的编码(作为一个字符串)和它所读入的任何行(不是从字节解码的)的 list 。

它从 UTF-8 BOM 或编码 cookie 的存在中检测编码格式,如 PEP 263 所指明的。 如果 BOM 和 cookie 都存在,但不一致,将会引发 SyntaxError。 请注意,如果找到 BOM ,将返回 'utf-8-sig' 作为编码格式。

如果没有指定编码,那么将返回默认的 'utf-8' 编码。

使用 open() 来打开 Python 源文件:它使用 detect_encoding() 来检测文件编码。

tokenize.open(filename)

使用由 detect_encoding() 检测到的编码,以只读模式打开一个文件。

在 3.2 版本加入.

exception tokenize.TokenError

当文件中任何地方没有完成 docstring 或可能被分割成几行的表达式时触发,例如:

"""Beginning of
docstring

或者:

[1,
 2,
 3

命令行用法

在 3.3 版本加入.

tokenize 模块可以作为脚本从命令行执行。 只需简单地:

python -m tokenize [-e] [filename.py]

可以接受以下选项:

-h, --help

显示此帮助信息并退出

-e, --exact

使用确切的类型显示令牌名称

如果 filename.py 被指定,其内容会被标记到 stdout 。否则,标记化将在 stdin 上执行。

在 3.15 版本加入: 输出默认为彩色并且可以 使用环境变量来控制。

例子

脚本改写器的例子,它将 float 字面值转换为 Decimal 对象:

from tokenize import tokenize, untokenize, NUMBER, STRING, NAME, OP
from io import BytesIO

def decistmt(s):
    """在一个语句字符串中用 Decimal 替代浮点数。

    >>> from decimal import Decimal
    >>> s = 'print(+21.3e-5*-.1234/81.7)'
    >>> decistmt(s)
    "print (+Decimal ('21.3e-5')*-Decimal ('.1234')/Decimal ('81.7'))"

    指数的格式继承自平台的 C 库。
    已知用例有 "e-007" (Windows) 和 "e-07" (非 Windows)。
    由于我们只显示 12 个数位,且第 13 位的值不到 5,因此
    输出的其余部分应当是不依赖于具体平台的。

    >>> exec(s)  #doctest: +ELLIPSIS
    -3.21716034272e-0...7

    使用 Decimal 进行计算的输出应当在所有平台上保持一致。

    >>> exec(decistmt(s))
    -3.217160342717258261933904529E-7
    """
    result = []
    g = tokenize(BytesIO(s.encode('utf-8')).readline)  # 对字符串分词
    for toknum, tokval, _, _, _ in g:
        if toknum == NUMBER and '.' in tokval:  # 替换 NUMBER 词元
            result.extend([
                (NAME, 'Decimal'),
                (OP, '('),
                (STRING, repr(tokval)),
                (OP, ')')
            ])
        else:
            result.append((toknum, tokval))
    return untokenize(result).decode('utf-8')

从命令行进行标记化的例子。 脚本:

def say_hello():
    print("Hello, World!")

say_hello()

将进行分词产生以下形式的输出其中第一列是找到的词元所在行/列坐标范围,第二列是词元名称,而最后一列是词元的值(如果有的话):

$ python -m tokenize hello.py
0,0-0,0:            ENCODING       'utf-8'
1,0-1,3:            NAME           'def'
1,4-1,13:           NAME           'say_hello'
1,13-1,14:          OP             '('
1,14-1,15:          OP             ')'
1,15-1,16:          OP             ':'
1,16-1,17:          NEWLINE        '\n'
2,0-2,4:            INDENT         '    '
2,4-2,9:            NAME           'print'
2,9-2,10:           OP             '('
2,10-2,25:          STRING         '"Hello, World!"'
2,25-2,26:          OP             ')'
2,26-2,27:          NEWLINE        '\n'
3,0-3,1:            NL             '\n'
4,0-4,0:            DEDENT         ''
4,0-4,9:            NAME           'say_hello'
4,9-4,10:           OP             '('
4,10-4,11:          OP             ')'
4,11-4,12:          NEWLINE        '\n'
5,0-5,0:            ENDMARKER      ''

可以使用 -e 选项来显示确切的标记类型名称。

$ python -m tokenize -e hello.py
0,0-0,0:            ENCODING       'utf-8'
1,0-1,3:            NAME           'def'
1,4-1,13:           NAME           'say_hello'
1,13-1,14:          LPAR           '('
1,14-1,15:          RPAR           ')'
1,15-1,16:          COLON          ':'
1,16-1,17:          NEWLINE        '\n'
2,0-2,4:            INDENT         '    '
2,4-2,9:            NAME           'print'
2,9-2,10:           LPAR           '('
2,10-2,25:          STRING         '"Hello, World!"'
2,25-2,26:          RPAR           ')'
2,26-2,27:          NEWLINE        '\n'
3,0-3,1:            NL             '\n'
4,0-4,0:            DEDENT         ''
4,0-4,9:            NAME           'say_hello'
4,9-4,10:           LPAR           '('
4,10-4,11:          RPAR           ')'
4,11-4,12:          NEWLINE        '\n'
5,0-5,0:            ENDMARKER      ''

以编程方式对文件进行标记的例子,用 generate_tokens() 读取 unicode 字符串而不是字节:

import tokenize

with tokenize.open('hello.py') as f:
    tokens = tokenize.generate_tokens(f.readline)
    for token in tokens:
        print(token)

或者通过 tokenize() 直接读取字节数据:

import tokenize

with open('hello.py', 'rb') as f:
    tokens = tokenize.tokenize(f.readline)
    for token in tokens:
        print(token)