对 Python 代码使用的词语标记化器 tokenize,你懂了吗?【Python|标准库|tokenize】

tokenize

token: n. 象征;标志; adj. 作为标志的;
-ize: suff. 使成…状态;使…化;
tokenize:标识化;标记化;

tokenize 提供了“对 Python 代码使用的”词汇扫描器,是用 Python 实现的。扫描器可以给 Python 代码打上标记后返回,你可以看到每一个词或者字符是什么类型的。扫描器甚至将注释也单独标记,这样某些需要对代码进行特定风格展示的地方就很方便了。

为了简化标记流(token stream)的处理,所有的运算符(Operators)、分隔符(Delimiters) 和 Ellipsis(不是英文,就是 Python 中的一个变量,和省略号一样)都会被标记为 OP(一个表示标识类型的常量)类型。具体的类型可以通过tokenize.tokenize() 返回的具名元祖对象的 .exact_type 属性查看。

exact_type 是一个 @property 修饰的方法,所以只有调用时才精确的查看到底是什么类型的文本,这样就简化了标记流的处理

标记的输入

主要的入口是一个生成器:

tokenize.tokenize(readline)

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

生成器会生成有5个元素的具名元组,内容是:

  • type:标记类型
  • string:被标记的字符串
  • start:一个整数组成的 2-元组:(srow, scol),这个标记的开始位置的行和列。s:start;
  • end:一个整数组成的 2-元组:(erow, ecol),这个标记的结束为止的行和列。e:end;
  • line:被标记的字符串所在的那一行,就是输入的那一行的内容

返回的具名元组还有一个额外的属性 exact_type,标识了类型为 OP 词的确切操作类型。对于所有 OP 以外的标记,exact_type 的值等于 type 的值。

tokenize() 通过查找 UTF-8 BOM 或者编码 cookie 来确认文件的源编码。

tokenize.generate_tokens(readline)

将对 unicode 类型的字符串进行标记,而不是字节类型。

像 tokenize() 一样,readline 参数需要可调用,并且返回输入的一行,但是需要返回 str 对象,而不是 bytes。

返回的结果是一个迭代器,返回的具名元祖和 tokenize() 的完全一样。只不过没有 ENCODING(一种表示标识类型的常量)类型的标记。(tokenize() 第一个返回的就是 ENCODING 标记的内容)

ENCODING 和 OP 一样是常量,还有很多,都是用来标记类型的,在 tokenize 库里直接用即可,是从 token 包里直接导过来的。

还有一个函数提供反转标记过程的功能。有些工具要标记化一个脚本、修改标记流、回写修改后的脚本,这个函数就能派上用场了。

tokenize.untokenize(iterable)

把标记转装成 Python 源代码(指用 Python 写成的代码)。可迭代对象 iterable 返回的序列中每一个对象至少要有两个元素构成:标记类型和标记的字符串。其他的元素都会被忽略。

反转生成的脚本会作为一个单独的字符串返回。

返回的是字节类型的,使用 ENCODING 标记的内容进行编码,如果输入中没有这个标记的,那就返回 str 类型的。

tokenize() 需要查出源文件的编码,它用于执行此操作的函数也是可用的:

tokenize.detect_encoding(readline)

detect_encoding() 函数用来检测应该用于解码 Pyhton 源文件的编码。它需要一个参数 readline,和生成器 tokenize() 所需的相同

它最多会调用 readline 两次,然后返回要使用的编码(一个字符串)和它已读入的每一行(不是从字节解码的)组成的列表

它根据 PEP 263 中规定的方式从 UTF-8 BOM 或者编码 cookie 中检测编码方式。如果 BOM 和 cookie 都存在但不一致,会抛出 SyntaxError。如果找到 BOM,'utf-8-sig' 将作为编码返回。

如果没有指定编码,就返回默认的 'utf-8'

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

tokenize.open(filename)

使用 detect_encoding() 检测到的编码通过只读方式打开一个文件

异常:tokenize.TokenError

当一个文档字符串或表达式可能被分割成多行,但在文件中的任何地方都没能完成时抛出。

例如:

"""文档字符串
开头

或者

[
  1,
  2,
  3

注意:未关闭的单引号字符串不会引发错误。它们会被标记为 ERRORTOKEN(一种标记类型常量),然后是其内容的标记化。

命令行用法

tokenize 包可以从命令行以脚本的形式执行。

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

有以下可选参数

-h, --help

展示帮助信息

-e, --exact

使用确切的类型展示标识类型

如果 filename.py 指定,它里面的内容就用作标记化,否则就在 stdin 获取输入。

示例

1、将浮点文字转换为 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'))"

    在不同的平台,下面这句的结果可能不同。第一个是在 macOS,第二个是在 Win10。

    >>> exec(s)
    -3.21716034272e-07
    -3.217160342717258e-07

    在所有平台上,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:  # 把数字类型的转换后保存
            result.extend([
                (NAME, 'Decimal'),
                (OP, '('),
                (STRING, repr(tokval)),
                (OP, ')')
            ])
        else:
            result.append((toknum, tokval))
    return untokenize(result).decode('utf-8')

2、使用命令行的例子

脚本:

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      ''

3、以编程方式标记文件的例子

1、用 generate_tokens() 读取 unicode 字符串而不是字节类型的。

import tokenize

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

结果如下,可见用 generate_tokens() 是得不到 ENCODING 的

TokenInfo(type=1 (NAME), string='def', start=(1, 0), end=(1, 3), line='def say_hello():\n')
TokenInfo(type=1 (NAME), string='say_hello', start=(1, 4), end=(1, 13), line='def say_hello():\n')
TokenInfo(type=54 (OP), string='(', start=(1, 13), end=(1, 14), line='def say_hello():\n')
TokenInfo(type=54 (OP), string=')', start=(1, 14), end=(1, 15), line='def say_hello():\n')
TokenInfo(type=54 (OP), string=':', start=(1, 15), end=(1, 16), line='def say_hello():\n')
TokenInfo(type=4 (NEWLINE), string='\n', start=(1, 16), end=(1, 17), line='def say_hello():\n')
TokenInfo(type=5 (INDENT), string='    ', start=(2, 0), end=(2, 4), line='    print("Hello, World!")\n')
TokenInfo(type=1 (NAME), string='print', start=(2, 4), end=(2, 9), line='    print("Hello, World!")\n')
TokenInfo(type=54 (OP), string='(', start=(2, 9), end=(2, 10), line='    print("Hello, World!")\n')
TokenInfo(type=3 (STRING), string='"Hello, World!"', start=(2, 10), end=(2, 25), line='    print("Hello, World!")\n')
TokenInfo(type=54 (OP), string=')', start=(2, 25), end=(2, 26), line='    print("Hello, World!")\n')
TokenInfo(type=4 (NEWLINE), string='\n', start=(2, 26), end=(2, 27), line='    print("Hello, World!")\n')
TokenInfo(type=61 (NL), string='\n', start=(3, 0), end=(3, 1), line='\n')
TokenInfo(type=6 (DEDENT), string='', start=(4, 0), end=(4, 0), line='say_hello()')
TokenInfo(type=1 (NAME), string='say_hello', start=(4, 0), end=(4, 9), line='say_hello()')
TokenInfo(type=54 (OP), string='(', start=(4, 9), end=(4, 10), line='say_hello()')
TokenInfo(type=54 (OP), string=')', start=(4, 10), end=(4, 11), line='say_hello()')
TokenInfo(type=4 (NEWLINE), string='', start=(4, 11), end=(4, 12), line='')
TokenInfo(type=0 (ENDMARKER), string='', start=(5, 0), end=(5, 0), line='')

2、或者直接使用 tokenize() 读取字节类型的:

import tokenize

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

标记化的结果与 例2 中一致,只是多了一些信息。

附表

所有的标记类型

Operators

以下形符属于运算符:

+       -       *       **      /       //      %       @
<<      >>      &       |       ^       ~       :=
<       >       <=      >=      ==      !=

Delimiters

以下形符在语法中归类为分隔符:

(       )       [       ]       {
            }
,       :       .       ;       @       =       ->
+=      -=      *=      /=      //=     %=      @=
&=      |=      ^=      >>=     <<=     **=

句点也可出现于浮点数和虚数字面值中。连续三个句点有表示一个省略符的特殊含义。以上列表的后半部分为增强赋值操作符,在词法中作为分隔符,但也起到运算作用。

以下可打印 ASCII 字符作为其他形符的组成部分时具有特殊含义,或是对词法分析器有重要意义:

'       "       #       \

你可能感兴趣的:(Python)