tokenize --- 對 Python 代碼使用的標記解析器?

源碼: Lib/tokenize.py


tokenize 模塊為 Python 源代碼提供了一個(gè)詞法掃描器,用 Python 實(shí)現。該模塊中的掃描器也將注釋作為標記返回,這使得它對于實(shí)現“漂亮的輸出器”非常有用,包括用于屏幕顯示的著(zhù)色器。

為了簡(jiǎn)化標記流的處理,所有的 運算符定界符 以及 Ellipsis 返回時(shí)都會(huì )打上通用的 OP 標記。 可以通過(guò) tokenize.tokenize() 返回的 named tuple 對象的 exact_type 屬性來(lái)獲得確切的標記類(lèi)型。

對輸入進(jìn)行解析標記?

主要的入口是一個(gè) generator:

tokenize.tokenize(readline)?

生成器 tokenize() 需要一個(gè) readline 參數,這個(gè)參數必須是一個(gè)可調用對象,且能提供與文件對象的 io.IOBase.readline() 方法相同的接口。每次調用這個(gè)函數都要 返回字節類(lèi)型輸入的一行數據。

生成器產(chǎn)生 5 個(gè)具有這些成員的元組:令牌類(lèi)型;令牌字符串;指定令牌在源中開(kāi)始的行和列的 2 元組 (srow, scol) ;指定令牌在源中結束的行和列的 2 元組 (erow, ecol) ;以及發(fā)現令牌的行。所傳遞的行(最后一個(gè)元組項)是 實(shí)際的 行。 5 個(gè)元組以 named tuple 的形式返回,字段名是: type string start end line 。

返回的 named tuple 有一個(gè)額外的屬性,名為 exact_type ,包含了 OP 標記的確切操作符類(lèi)型。 對于所有其他標記類(lèi)型, exact_type 等于命名元組的 type 字段。

在 3.1 版更改: 增加了對 named tuple 的支持。

在 3.3 版更改: 添加了對 exact_type 的支持。

根據:pep:263 , tokenize() 通過(guò)尋找 UTF-8 BOM 或編碼 cookie 來(lái)確定文件的源編碼。

tokenize.generate_tokens(readline)?

對讀取 unicode 字符串而不是字節的源進(jìn)行標記。

tokenize() 一樣, readline 參數是一個(gè)返回單行輸入的可調用參數。然而, generate_tokens() 希望 readline 返回一個(gè) str 對象而不是字節。

其結果是一個(gè)產(chǎn)生具名元組的的迭代器,與 tokenize() 完全一樣。 它不會(huì )產(chǎn)生 ENCODING 標記。

所有來(lái)自 token 模塊的常量也可從 tokenize 導出。

提供了另一個(gè)函數來(lái)逆轉標記化過(guò)程。這對于創(chuàng )建對腳本進(jìn)行標記、修改標記流并寫(xiě)回修改后腳本的工具很有用。

tokenize.untokenize(iterable)?

將令牌轉換為 Python 源代碼。 iterable 必須返回至少有兩個(gè)元素的序列,即令牌類(lèi)型和令牌字符串。任何額外的序列元素都會(huì )被忽略。

重構的腳本以單個(gè)字符串的形式返回。 結果被保證為標記回與輸入相匹配,因此轉換是無(wú)損的,并保證來(lái)回操作。 該保證只適用于標記類(lèi)型和標記字符串,因為標記之間的間距(列位置)可能會(huì )改變。

它返回字節,使用 ENCODING 標記進(jìn)行編碼,這是由 tokenize() 輸出的第一個(gè)標記序列。如果輸入中沒(méi)有編碼令牌,它將返回一個(gè)字符串。

tokenize() 需要檢測它所標記源文件的編碼。它用來(lái)做這件事的函數是可用的:

tokenize.detect_encoding(readline)?

detect_encoding() 函數用于檢測解碼 Python 源文件時(shí)應使用的編碼。它需要一個(gè)參數, readline ,與 tokenize() 生成器的使用方式相同。

它最多調用 readline 兩次,并返回所使用的編碼(作為一個(gè)字符串)和它所讀入的任何行(不是從字節解碼的)的 list 。

它從 UTF-8 BOM 或編碼 cookie 的存在中檢測編碼格式,如 PEP 263 所指明的。 如果 BOM 和 cookie 都存在,但不一致,將會(huì )引發(fā) SyntaxError。 請注意,如果找到 BOM ,將返回 'utf-8-sig' 作為編碼格式。

如果沒(méi)有指定編碼,那么將返回默認的 'utf-8' 編碼.

使用 open() 來(lái)打開(kāi) Python 源文件:它使用 detect_encoding() 來(lái)檢測文件編碼。

tokenize.open(filename)?

使用由 detect_encoding() 檢測到的編碼,以只讀模式打開(kāi)一個(gè)文件。

3.2 新版功能.

exception tokenize.TokenError?

當文件中任何地方?jīng)]有完成 docstring 或可能被分割成幾行的表達式時(shí)觸發(fā),例如:

"""Beginning of
docstring

或者:

[1,
 2,
 3

注意,未封閉的單引號字符串不會(huì )導致錯誤發(fā)生。它們被標記為 ERRORTOKEN ,然后是其內容的標記化。

命令行用法?

3.3 新版功能.

tokenize 模塊可以作為一個(gè)腳本從命令行執行。這很簡(jiǎn)單:

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

可以接受以下選項:

-h, --help?

顯示此幫助信息并退出

-e, --exact?

使用確切的類(lèi)型顯示令牌名稱(chēng)

如果 filename.py 被指定,其內容會(huì )被標記到 stdout 。否則,標記化將在 stdin 上執行。

例子?

腳本改寫(xiě)器的例子,它將 float 文本轉換為 Decimal 對象:

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

def decistmt(s):
    """Substitute Decimals for floats in a string of statements.

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

    The format of the exponent is inherited from the platform C library.
    Known cases are "e-007" (Windows) and "e-07" (not Windows).  Since
    we're only showing 12 digits, and the 13th isn't close to 5, the
    rest of the output should be platform-independent.

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

    Output from calculations with Decimal should be identical across all
    platforms.

    >>> exec(decistmt(s))
    -3.217160342717258261933904529E-7
    """
    result = []
    g = tokenize(BytesIO(s.encode('utf-8')).readline)  # tokenize the string
    for toknum, tokval, _, _, _ in g:
        if toknum == NUMBER and '.' in tokval:  # replace NUMBER tokens
            result.extend([
                (NAME, 'Decimal'),
                (OP, '('),
                (STRING, repr(tokval)),
                (OP, ')')
            ])
        else:
            result.append((toknum, tokval))
    return untokenize(result).decode('utf-8')

從命令行進(jìn)行標記化的例子。 腳本:

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

say_hello()

將被標記為以下輸出,其中第一列是發(fā)現標記的行 / 列坐標范圍,第二列是標記的名稱(chēng),最后一列是標記的值(如果有)。

$ 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 選項來(lái)顯示確切的標記類(lèi)型名稱(chēng)。

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

以編程方式對文件進(jìn)行標記的例子,用 generate_tokens() 讀取 unicode 字符串而不是字節:

import tokenize

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

或者通過(guò) tokenize() 直接讀取字節數據:

import tokenize

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