對 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 標記的內容)

ENCODINGOP 一樣是常量,還有很多,都是用來標記類型的,在 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 字符作爲其他形符的組成部分時具有特殊含義,或是對詞法分析器有重要意義:

'       "       #       \
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章