Пориньте у Python 3/Приклад: Перенесення chardet на Python 3

Матеріал з Вікіпідручника

Слова, слова. Це все що у нас є щоб продовжувати.
Розенкранц та Гільденстерн мертві

Питання: що є причиною №1 кракозябрів в інтернеті, в вашій поштовій скринці, і в кожній комп’ютерній системі яку ви писали? Це кодування символів. В розділі Текст, я розповідав про історію кодувань та створення Юнікоду, "одного кодування щоб керувати ними всіма". Я б був щасливий ніколи більше не бачити кракозябрів на екрані, тому що всі системи створення тексту зберігали б точну інформацію про кодування, всі протоколи передачі мали уявлення про Юнікод, і кожна система що працює з текстом зберігала б ідеальну якість при перетвореннях між різними кодуваннями.

Мені також сподобалось би поні.

Юнікодове поні.

Юніпоні, так би мовити.

Але я задовольнюсь автовизначенням кодування символів.


* * *


Що таке автовизначення кодування символів?[ред.]

Це означає взяти послідовність байт в невідомому кодуванні, та спробувати визначити кодування, аби мати змогу прочитати текст. Це як зламування шифру, коли у вас немає ключа.

Хіба це не неможливо?[ред.]

Взагалі, так. Щоправда, деякі кодування оптимізовані для деяких мов, а мови не випадкові. Деякі послідовності символів з’являються постійно, в той час як інші не мають жодного сенсу. Людина яка вільно читає англійською, відкривши газету і побачивши там "txzqJv 2!dasd0a QqdKjvz" одразу зрозуміє що це не англійська (хоча рядок і складений цілком з латинських літер). Вивчаючи багато "типового" тексту, комп’ютерний алгоритм може імітувати подібний рівень "вільного володіння" і вгадувати мову тексту.

Іншими словами, визначення кодування, це насправді визначення мови, комбіноване з знанням того які мови використовують які кодування.

А такий алгоритм існує?[ред.]

Як виявилось, так. Всі основні браузери мають автовизначення кодування символів, тому що в інтернеті повно сторінок які взагалі не містять інформації про кодування. Mozilla Firefox містить бібліотеку автовизначення кодування яка поширюється з відкритим кодом. Я портував ту бібліотеку на Python 2, як модуль chardet. В цьому розділі ми крок за кроком пройдемось кріз процес портування модуля chardet з Python 2 на Python 3.


* * *


Представляєю модуль chardet[ред.]

Перед тим як ми почнемо переносити код, було б добре розібратись у тому як він працює. Це корокта довідка щодо того як читати код. Бібліотека chardet занадто велика щоб вставити її код прямо сюди, але ви можете прочитати її код на GitHub.

Головною вхідною точкою алгоритму визначення є universaldetector.py, який має один клас, UniversalDetector. (Ви можете думати що головною вхідною точкою є функція detect в файлі chardet/__init__.py, але насправді це просто функція для зручності, яка створює об’єкт UniversalDetector, викликає його, та повертає результат виклику.)

Визначення кодування це насправді переодягнене визначення мови.

UniversalDetector працює з 5 видами кодувань:

  1. UTF-N з маркером порядку байтів (BOM). Це включає UTF-8, як Big-Endian так і Little-Endian варіанти UTF-16, та всі чотирибайтові варіанти UTF-32.
  2. Екрановані кодування, які повністю сумісні з семибітним ASCII, де не-ASCII символи починаються з символу екранування. Приклади: ISO-2022-JP (Японська) та HZ-GB-2312 (Китайська).
  3. Багатобайтові кодування, в яких кожен символ представлено різною кількістю байтів. Приклади: BIG5 (Китайська), SHIFT_JIS (Японська), EUC-KR (Корейська), та UTF-8 без BOM.
  4. Однобайтові кодування, в яких кожен символ представлений одним байтом. Приклади: KOI8-R (Російська), WINDOWS-1255 (Єврейська), та TIS-620 (Тайська).
  5. WINDOWS-1251, яка використовується переважно на Microsoft Windows менеджерами середньої ланки, які не відрізнять кодування символів від власної дупи.

UTF-8 з BOM[ред.]

Якщо текст починається з BOM, ми маємо причину припускати що текст закодований в UTF-8, UTF-16, чи UTF-32. (BOM вкаже нам точно яка з них, вона якраз для цього призначена.) Цей випадок опрацьовується прямо в UniversalDetector, який негайно поверне результат, без подальшої обробки.

Екрановані кодування[ред.]

Якщо текст містить впізнавану ескейп-послідовність, це може вказувати що використано екрановане кодування. UniversalDetector створить EscCharSetProber (описаний в escprober.py) та передасть йому текст.

EscCharSetProber створює послідовність скінченних автоматів, які базуються на основі моделей кодувань HZ-GB-2312, ISO-2022-CN, ISO-2022-JP, та ISO-2022-KR (описаних в escsm.py). EscCharSetProber передає текст в кожен з цих скінченних автоматів, по одному байту за раз. Якщо будь-який з цих скінченних автоматів зупиниться, унікально ідентифікуючи кодування, EscCharSetProber негайно поверне позитивний результат в UniversalDetector, який поверне результат тому хто викликав цей об’єкт. Якщо будь-який з скінченних автоматів зустрічає недозволену послідовність, вона викидається і обробка продовжується з іншими скінченними автоматами.


Багатобайтові кодування[ред.]

Припускаючи відсутність BOM, UniversalDetector перевіряє чи містить текст будь-які символи, з найбільш значущим бітом в байті. Якщо так, він створює послідовність "проберів" для визначення багатобайтових кодувань, однобайтових кодувань, і як крайній вихід - windows-1252.

Пробер багатобайтових кодувань, MBCSGroupProber (описаний в mbcsgroupprober.py), є просто оболонкою яка керує групою інших проберів, по одному на кожне багатобайтове кодування: Big5, GB2312, EUC-TW, EUC-KR, EUC-JP, SHIFT_JIS, та UTF-8. MBCSGroupProber передає текст кожному з цих проберів та перевіряє результати. Якщо пробер звітує про те що він знайшов недозволену послідовність байтів, він видаляється з подальшої обробки (так, що наприклад, будь-які подальші виклики UniversalDetector.feed() пропустять той пробер. Якщо пробер звітує що він достатньо впевнений в тому що визначив кодування, MBCSGroupProber звітує про цей позитивний результат в UniversalDetector, а той передає його далі.

Більшість проберів багатобайтових кодувань наслідуються від MultiByteCharSetProber (описаний в mbcharsetprober.py), і просто підтягують відповідний скінченний автомат, та аналізатор розподілу і дозволяють MultiByteCharSetProber зробити решту роботи. MultiByteCharSetProber пропускає текст через відповідні кодуванням скінченні автомати, по байту за раз, щоб дізнатись про послідовності байтів, які вкажуть на заключний позитивний чи негативний результат. В той же час, MultiByteCharSetProber передасть текст відповідному до кодування аналізатору розподілу.

Аналізатори розподілу (визначені в chardistribution.py) використовують мовно-специфічні моделі того які символи використовуються найчастіше. Як тільки MultiByteCharSetProber передасть достатньо тексту аналізатору розпоідлу, він обчислює коефіцієнт впевненості, який базується на кількостях часто використовуваних символів, загальному числу символів, та залежній від мови ймовірності розподілу символів. Якщо рівень впевненості достатньо високий, MultiByteCharSetProber поверне результат в MBCSGroupProber, який поверне його в UniversalDetector а той вже передасть його далі.

Випадок Японської більш складний. Односимвольний аналіз розподілу не завжди є достатнім для того аби відрізнити EUC-JP та SHIFT_JIS, тому SJISProber (описаний в sjisprober.py) також використовує аналіз розподілу пар символів. SJISContextAnalysis та EUCJPContextAnalysis (обоє описані в jpcntx.py та обоє наслідуються від спільного класу JapaneseContextAnalysis), перевіряють частоту складових символів Хірагани в тексті. Як тільки текст буде опрацьовано, вони повертають рівень впевненості в SJISProber, який перевіряє обидва аналізатори і повертає результат того, чий рівень впевненості буде вищим в MBCSGroupProber.

Однобайтові кодування[ред.]

Серйозно, де моє Юнікодове поні?

Пробер однобайтових кодувань, SBCSGroupProber (описаний в sbcsgroupprober.py), також є лише оболонкою яка керує групою інших проберів, по одному для кожної комбінації однобайтових кодувань та мов: windows-1251, KOI8-R, ISO-8859-5, MacCyrillic, IBM855, та IBM866 (Російська); ISO-8859-7 та windows-1253 (Грецька); ISO-8859-5 та windows-1251 (Болгарська); ISO-8859-2 та windows-1250 (Угорська); TIS-620 (Тайська); windows-1255 та ISO-8859-8 (Іврит).

SBCSGroupProber передає текст кожному з цих специфічних для мови та кодування проберів і перевіряє результати. Всі ці пробери реалізовані в єдиному класі, SingleByteCharSetProber (описаному в sbcharsetprober.py), який приймає модель мови як аргумент. Модель мови описує як часто різні двосимвольні послідовності з’являються в типовому тексті. SingleByteCharSetProber опрацьовує текст, і відмічає найчастіше вживані двосимвольні послідовності. Як тільки текст було опрацьовано, він обчислює рівень впевненості, базуючись на кількостях частовживаних послідовностей, загальній кількості символів та унікальному для мови розподілу частоти.

Іврит опрацьовується окремим випадком. Якщо текст схожий на іврит, за даними аналізу розподілу двосимвольних послідовностей, HebrewProber (описаний в hebrewprober.py) спробує відрізнити між візуальним івритом (де текст справді зберігається "задом наперед" рядок за рядком, а потім так і відображається аби його можна було читати справа наліво) та логічним івритом (де текст зберігається в порядку читання, а потім відображається з права наліво програмою). Тому що деякі символи кодуються по різному залежно від того чи знаходяться вони в середині чи наприкінці слова, ми можемо обґрунтовано вгадати напрямок тексту, і повернути відповідне кодування (windows-1255 для логічного івриту, та ISO-8859-8 для візуального івриту).

windows-1252[ред.]

Якщо UniversalDetector виявляє в тексті символ з останнім значущим бітом, але жодне з інших багатобайтових кодувань не підійшло, він створює Latin1Prober (описаний в latin1prober.py) щоб спробувати виявити англомовний текст в кодуванні windows-1252. Ця перевірка страшенно ненадійна, тому що латинські букви кодуються однаково в багатьох кодуваннях. Єдиний спосіб відрізнити windows-1252 - за допомогою часто використовуваних символів, таких як лапки, апострофи, символ копірайту, і т.п. Latin1Prober автоматично зменшує коефіцієнт впевненості, аби дати можливість більш точним проберам виграти, якщо це взагалі можливо.


* * *


Запуск 2to3[ред.]

Ми збираємось портувати модуль chardet з Python 2 на Python 3. Python 3 поставляється з корисним скриптом, який називається 2to3, який бере на вхід код на Python 2 та автоматично перетворює його на Python 3. В багатьох випадках це легко - коли функція була перейменована чи переміщена в інший модуль, але в інших випадках все може ставати досить складним. Щоб отримати відчуття того всього що він може зробити, зверніться до додатку Poring code to Python 3 with 2to3. В цьому розділі ми почнемо з запуску 2to3 над пакетом chardet, але як ви побачите, після того як автоматичні інструменти здійснять свою магію, залишиться все ще досить роботи.

Головний пакунок chardet поділений між кількома різними файлами, що знаходяться в одній директорії. Скрипт 2to3 досить просто конвертує багато файлів за раз: просто передайте в якості аргумента командного рядка директорію, і 2to3 опрацює кожен файл всередині.

C:\home\chardet> python c:\Python30\Tools\Scripts\2to3.py -w chardet\
RefactoringTool: Skipping implicit fixer: buffer
RefactoringTool: Skipping implicit fixer: idioms
RefactoringTool: Skipping implicit fixer: set_literal
RefactoringTool: Skipping implicit fixer: ws_comma
--- chardet\__init__.py (original)
+++ chardet\__init__.py (refactored)
@@ -18,7 +18,7 @@
 __version__ = "1.0.1"

 def detect(aBuf):
-    import universaldetector
+    from . import universaldetector
     u = universaldetector.UniversalDetector()
     u.reset()
     u.feed(aBuf)
--- chardet\big5prober.py (original)
+++ chardet\big5prober.py (refactored)
@@ -25,10 +25,10 @@
 # 02110-1301  USA
 ######################### END LICENSE BLOCK #########################

-from mbcharsetprober import MultiByteCharSetProber
-from codingstatemachine import CodingStateMachine
-from chardistribution import Big5DistributionAnalysis
-from mbcssm import Big5SMModel
+from .mbcharsetprober import MultiByteCharSetProber
+from .codingstatemachine import CodingStateMachine
+from .chardistribution import Big5DistributionAnalysis
+from .mbcssm import Big5SMModel

 class Big5Prober(MultiByteCharSetProber):
     def __init__(self):
--- chardet\chardistribution.py (original)
+++ chardet\chardistribution.py (refactored)
@@ -25,12 +25,12 @@
 # 02110-1301  USA
 ######################### END LICENSE BLOCK #########################

-import constants
-from euctwfreq import EUCTWCharToFreqOrder, EUCTW_TABLE_SIZE, EUCTW_TYPICAL_DISTRIBUTION_RATIO
-from euckrfreq import EUCKRCharToFreqOrder, EUCKR_TABLE_SIZE, EUCKR_TYPICAL_DISTRIBUTION_RATIO
-from gb2312freq import GB2312CharToFreqOrder, GB2312_TABLE_SIZE, GB2312_TYPICAL_DISTRIBUTION_RATIO
-from big5freq import Big5CharToFreqOrder, BIG5_TABLE_SIZE, BIG5_TYPICAL_DISTRIBUTION_RATIO
-from jisfreq import JISCharToFreqOrder, JIS_TABLE_SIZE, JIS_TYPICAL_DISTRIBUTION_RATIO
+from . import constants
+from .euctwfreq import EUCTWCharToFreqOrder, EUCTW_TABLE_SIZE, EUCTW_TYPICAL_DISTRIBUTION_RATIO
+from .euckrfreq import EUCKRCharToFreqOrder, EUCKR_TABLE_SIZE, EUCKR_TYPICAL_DISTRIBUTION_RATIO
+from .gb2312freq import GB2312CharToFreqOrder, GB2312_TABLE_SIZE, GB2312_TYPICAL_DISTRIBUTION_RATIO
+from .big5freq import Big5CharToFreqOrder, BIG5_TABLE_SIZE, BIG5_TYPICAL_DISTRIBUTION_RATIO
+from .jisfreq import JISCharToFreqOrder, JIS_TABLE_SIZE, JIS_TYPICAL_DISTRIBUTION_RATIO

 ENOUGH_DATA_THRESHOLD = 1024
 SURE_YES = 0.99
.
.
. (воно деякий час продовжується подібним чином)
.
.
RefactoringTool: Files that were modified:
RefactoringTool: chardet\__init__.py
RefactoringTool: chardet\big5prober.py
RefactoringTool: chardet\chardistribution.py
RefactoringTool: chardet\charsetgroupprober.py
RefactoringTool: chardet\codingstatemachine.py
RefactoringTool: chardet\constants.py
RefactoringTool: chardet\escprober.py
RefactoringTool: chardet\escsm.py
RefactoringTool: chardet\eucjpprober.py
RefactoringTool: chardet\euckrprober.py
RefactoringTool: chardet\euctwprober.py
RefactoringTool: chardet\gb2312prober.py
RefactoringTool: chardet\hebrewprober.py
RefactoringTool: chardet\jpcntx.py
RefactoringTool: chardet\langbulgarianmodel.py
RefactoringTool: chardet\langcyrillicmodel.py
RefactoringTool: chardet\langgreekmodel.py
RefactoringTool: chardet\langhebrewmodel.py
RefactoringTool: chardet\langhungarianmodel.py
RefactoringTool: chardet\langthaimodel.py
RefactoringTool: chardet\latin1prober.py
RefactoringTool: chardet\mbcharsetprober.py
RefactoringTool: chardet\mbcsgroupprober.py
RefactoringTool: chardet\mbcssm.py
RefactoringTool: chardet\sbcharsetprober.py
RefactoringTool: chardet\sbcsgroupprober.py
RefactoringTool: chardet\sjisprober.py
RefactoringTool: chardet\universaldetector.py
RefactoringTool: chardet\utf8prober.py

Тепер, давайте запустимо скрипт 2to3 на наших страховочних тестах, test.py.

C:\home\chardet> python c:\Python30\Tools\Scripts\2to3.py -w test.py
RefactoringTool: Skipping implicit fixer: buffer
RefactoringTool: Skipping implicit fixer: idioms
RefactoringTool: Skipping implicit fixer: set_literal
RefactoringTool: Skipping implicit fixer: ws_comma
--- test.py (original)
+++ test.py (refactored)
@@ -4,7 +4,7 @@
 count = 0
 u = UniversalDetector()
 for f in glob.glob(sys.argv[1]):
-    print f.ljust(60),
+    print(f.ljust(60), end=' ')
     u.reset()
     for line in file(f, 'rb'):
         u.feed(line)
@@ -12,8 +12,8 @@
     u.close()
     result = u.result
     if result['encoding']:
-        print result['encoding'], 'with confidence', result['confidence']
+        print(result['encoding'], 'with confidence', result['confidence'])
     else:
-        print '******** no result'
+        print('******** no result')
     count += 1
-print count, 'tests'
+print(count, 'tests')
RefactoringTool: Files that were modified:
RefactoringTool: test.py

Ну, це не було аж так складно. Просто переписати кілька імпортів та команд print. І раз ми вже про це заговорили, а що то була за проблема з import</code? Щоб дати відповідь на це запитання потрібно розуміти як модуль chardet розбитий на файли.


* * *


Короткий відступ про багатофайлові модулі[ред.]

chardet це багатофайловий модуль. Я міг би вирішити покласти ввесь код у один файл (названий chardet.py), але не зробив цього. Натомість, я створив каталог (названий chardet), а тоді створив в ньому файл __init__.py. Якщо Python бачить в директорії файл __init__.py, він припускає що всі файли в тій директорії є частиною одного модуля. Назвою модуля є назва директорії. Файли всередині директорії можуть посилатись на інші файли в директорії чи навіть в піддиректоріях. (За хвилину про це більше). Але ввесь набір файлів виглядає для решти коду як єдиний модуль - так ніби всі функції та класи є в єдиному .py файлі.

Що відбувається в файлі __init__.py? Нічого. Все. Щось середнє. Файл __init__.py не повинен нічого описувати, він може бути буквально порожнім. Або ви можете використати його аби описати там функції що є точками входу. Чи описати там всі функції. Чи всі крім однієї.

Каталог з файлом __init__.py завжди розлядається як багатофайловий модуль. Без файлу __init__.py каталог є просто каталогом з непов’язаними .py файлами.

Давайте подивимось як це працює на практиці:

>>> import chardet
>>> dir(chardet)
['__builtins__', '__doc__', '__file__', '__name__',
 '__package__', '__path__', '__version__', 'detect']

Окрім звичайних атрибутів, єдине що міститься в модулі chardet - це функція detect().

 >>> chardet 
<module 'chardet' from 'C:\Python31\lib\site-packages\chardet\__init__.py'>

А ось наша перша підказка про те що модуль chardet це не просто файл: цей модуль описується як файл __init__.py всередині директорії chardet/.

Давайте заглянемо в той файл __init__.py:

def detect(aBuf):
    from . import universaldetector
    u = universaldetector.UniversalDetector()
    u.reset()
    u.feed(aBuf)
    u.close()
    return u.result

__init__.py описує функцію detect(), яка є головною точкою входу в бібліотеку chardet. Але ця функція майже не містить коду, Все що вона насправді робить - це імпортує модуль universaldetector та починає його використовувати. Але де описується universaldetector?

Відповідь дає ця дивна команда імпорту:

from . import universaldetector

Що в перекладі на українську означає "імпортуй модуль universaldetector, він знаходиться в тій самій директорії що і я", де "я" - це chardet/__init__.py. Це називається відносним імпортом. Це спосіб для файлів всередині багатофайлового модуля посилатись один на одного, не хвилюючись про конфлікти імен з іншими модулями, які ви могли встановити в одну з директорій в яких Python шукає модулі для імпорту. Дана команда import буде шукати модуль universaldetector лише в директорії chardet/.

Ці два поняття - __init__.py, та відносні імпорти, означають що ви можете розбити ваш модуль на так багато шматочків як захочете. Модуль chardet містить 36 файлів .py. 36! І все одно, все що вам потрібно зробити аби його використати - це import chardet, а потім викликати основну функцію chardet.detect(). Без відома вашого коду, функція detect() описується в файлі chardet/__init__.py. Також, без вашого відома, функція detect() використовує відносний імпорт щоб звернутись до класу описаного в файлі chardet/universaldetector.py, який далі використовує відносні імпорти на п’яти інших файлах, кожен з яких міститься в каталозі chardet/.

Якщо ви будете писати велику бібліотеку на Python (чи, що більш ймовірно, коли побачите що ваша маленька бібліотека виросла і стала великою), знайдіть час для рефакторингу її в багатофайловий модуль. Це одна з багатьох речей з якими Python справляється добре, тому варто використати її як перевагу.


* * *


Виправлення того що не зміг 2to3[ред.]

False is invalid syntax[ред.]

У вас ж є тести, правда?

Тепер справжнє випробування: запуст набору тестів. Так як тести створені таким чином щоб покривати всі можливі шляхи в коді, це гарний спосіб перевірити наш портований код, щоб переконатись що ніде не заповзли ніякі баги.

C:\home\chardet> python test.py tests\*\*
Traceback (most recent call last):
  File "test.py", line 1, in <module>
    from chardet.universaldetector import UniversalDetector
  File "C:\home\chardet\chardet\universaldetector.py", line 51
    self.done = constants.False
                              ^
SyntaxError: invalid syntax

Хмм, невеликий сучок. В Python 3, False - зарезервоване слово, тому його не можна використовувати як ім’я змінної. Давайте подивимось на constants.py, і знайдемо де воно описане. Ось оригінальна версія коду з constants.py, до того як скрипт 2to3 його змінив:

import __builtin__
if not hasattr(__builtin__, 'False'):
    False = 0
    True = 1
else:
    False = __builtin__.False
    True = __builtin__.True

Цей шматок коду створений щоб дозволити бібліотеці працювати на старіших версіях Python 2. До Python 2.3, в Python не було вбудованого типу bool. Цей код виявляє відсутність вбудованих констант True та False і описує їх за потреби.

Проте, Python 3 завжди матиме тип bool, тому ввесь той шматок коду не є обов’язковим. Найпростішим рішенням є замінити всі входження constants.True та constants.False на True та False відповідно, а потім видалити цей мертвий код з constants.py.

Тоді цей рядок в universaldetector.py:

self.done = constants.False

Стає цим:

self.done = False

Ох, хіба це не задоволення? Код став коротший і більш читабельний вже зараз.

No module named constants[ред.]

Час запустити test.py знову, і подивитись як далеко він зможе дійти.

C:\home\chardet> python test.py tests\*\*
Traceback (most recent call last):
  File "test.py", line 1, in <module>
    from chardet.universaldetector import UniversalDetector
  File "C:\home\chardet\chardet\universaldetector.py", line 29, in <module>
    import constants, sys
ImportError: No module named constants

Що ти сказав? Нема модуля названого constants? Звичайно є модуль з назвою constants. Він ось тут, в chardet/constants.py.

Пам’ятаєте, коли скрипт 2to3 виправив всі ті команди імпорту? Ця бібліотека має багато відносних імпортів, тобто модулів що імпортують інші модулі з своєї бібліотеки, але логіка відносних імпортів змінилась в Python 3. В Python 2, ви могли просто написати import constants і він шукав би спершу в директорії chardet/. У Python 3, всі імпорти за замовчуванням абсолютні. Якщо ви хочете здійснити відносний імпорт в Python 3, треба про це написати явно:


from . import constants

Але зачекайте. Хіба скрипт 2to3 не повинен був виправити це за нас? Ну, він так і зробив, але ця конкретна команда імпорту поєднує два типи імпорту на одному рядку: відносний імпорт модуля constants та абсолютний імпорт модуля sys, який входить в стандартну бібліотеку. В Python 2, ви можете комбінувати це в одній команді. В Python 3 - не можете, а скрипт 2to3 не настільки розумний щоб розбити команду імпорту на дві.

Єдиним виходом буде зробити це вручну. Тому цей однорядковий імпорт:

import constants, sys

Повинен стати двома окремими імпортами:

from . import constants
import sys

Існують варіації цієї проблеми розкидані по всій бібліотеці chardet. В деяких місцях це "import constants, sys", в деяких "import constants, re". Рішення всюди однакове, розбити імпорт на два, один відносний, один абсолютний.

Вперед!

Name 'file' is not defined[ред.]

open() - це новий file().

І ось ми знову запускаємо test.py, щоб пройтись по тестах:

C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml
Traceback (most recent call last):
  File "test.py", line 9, in <module>
    for line in file(f, 'rb'):
NameError: name 'file' is not defined

Це мене здивувало, тому що я звик використовувати цю ідіому відколи себе пам`ятаю. В Python 2, глобальна функція file() була псевдонімом для функції open(), яка була стандартним способом відкривання текстових файлів для читання. В Python 3 глобальної функції file() вже не існує, але функція open() все ще залишається.

Тому, найпростішим вирішенням проблеми відсутньї функції file(), буде викликати функцію open() натомість:

for line in open(f, 'rb'):

І це все що я про це мав сказати.

Can’t use a string pattern on a bytes-like object[ред.]

А тепер речі стають цікавими. І під "цікавими" я маю на увазі "до чорта заплутаними".

C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:\home\chardet\chardet\universaldetector.py", line 98, in feed
    if self._highBitDetector.search(aBuf):
TypeError: can't use a string pattern on a bytes-like object

Щоб це зневадити, давайте подивимось що таке self._highBitDetector. Воно описане в методі __init__ класу UniversalDetector:

class UniversalDetector:
    def __init__(self):
        self._highBitDetector = re.compile(r'[\x80-\xFF]')

Це попередньо відкомпільований регулярний вираз створений для знаходження не ASCII символів в діапазоні 128-255 (Ox80-0xFF). Але зачекайте, це не цілком правильно, я повинен бути точнішим з моєю термінологією. Цей паттерн створений для пошуку не ASCII байтів в діапазоні 128-255.

І ось тут лежить наша проблема.

В Python 2, рядок був масивом байтів, кодування символів яких відстежувалось окремо. Якщо ви хотіли щоб Python 2 слідкував за кодуванням символів, потрібно було використовувати натомість рядок Юнікод (u). Але в Python 3, рядки - завжди те що в Python 2 називалось Юнікодовими рядками, тобто масив символів Юнікоду (можливо з різними кількостями байтів на символ). Так як даний регулярний вираз описаний рядком, він може використовуватись лише для пошуку рядків - тобто повторюсь, масивів символів. Але те що ми шукаємо - це не рядок, це масив байтів. Якщо подивитись на трейсбек, ця помилка виникла в universaldetector.py:

def feed(self, aBuf):
    .
    .
    .
    if self._mInputState == ePureAscii:
        if self._highBitDetector.search(aBuf):

А що таке aBuf? Давайте перейдемо до місця з якого викликається UniversalDetector.feed(). Одне з тих місць де цей метод викликається - тестова страховка в test.py.

u = UniversalDetector()
.
.
.
for line in open(f, 'rb'):
    u.feed(line)
Не масив символів, а масив байтів

І тут ми знаходимо нашу відповідь: в методі UniversalDetector.feed(), aBuf - це рядок що читається з файлу на диску. Подивіться уважно на параметри використані для відкривання файлу: 'rb'. 'r' означає "read". Ну гаразд, подумаєш, ми читаємо файл. Ах, але 'b' означає "binary". Без прапора 'b', цей цикл for прочитав би файл, рядок за рядком, і перетворив би кожен рядок в масив символів Юнікоду - згідно з системним кодуванням за замовчуванням. Але з прапором 'b', цей цикл for читає файл, рядок за рядком, і і зберігає кожен рядок точно так як він записаний в файлі, як масив байтів. Цей масив байтів передається до UniversalDetector.feed(), та в кінцевому результаті передається в попередньо відкомпільований регулярний вираз, self._highBitDetector, для пошуку високобітових... символів. Але в нас немає символів, в нас є байти. Ой...

Що нам потрібно в цьому регулярному виразі - це шукати не масив символів, а масив байтів.

Як тільки ми це усвідомимо, рішення не буде складним. Регулярні вирази описані рядками можуть шукати рядки. Регулярні вирази описані масивами байтів можуть шукати масиви байтів. Щоб описати шаблон над байтовим масивом, ми просто змінюємо тип аргументу, який ми використовуємо для опису регулярних виразів, на масив байт. (Також наступний приклад покаже виправлення іншої аналогічної проблеми):

  class UniversalDetector:
      def __init__(self):
-         self._highBitDetector = re.compile(r'[\x80-\xFF]')
-         self._escDetector = re.compile(r'(\033|~{)')
+         self._highBitDetector = re.compile(b'[\x80-\xFF]')
+         self._escDetector = re.compile(b'(\033|~{)')
          self._mEscCharSetProber = None
          self._mCharSetProbers = []
          self.reset()

Пошук по всьому коду інших використань модуля re дає ще два випадки, в charsetprober.py. І знову, код що описує регулярні вирази - це рядки, але виконуються вони над aBuf, який є масивом байтів. Розв’язок такий самий: описувати шаблони регулярних виразів як масиви байтів.

 class CharSetProber:
      .
      .
      .
      def filter_high_bit_only(self, aBuf):
-         aBuf = re.sub(r'([\x00-\x7F])+', ' ', aBuf)
+         aBuf = re.sub(b'([\x00-\x7F])+', b' ', aBuf)
          return aBuf
    
      def filter_without_english_letters(self, aBuf):
-         aBuf = re.sub(r'([A-Za-z])+', ' ', aBuf)
+         aBuf = re.sub(b'([A-Za-z])+', b' ', aBuf)
          return aBuf

Can't convert 'bytes' object to str implicitly[ред.]

Цікавіше і цікавіше...

C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:\home\chardet\chardet\universaldetector.py", line 100, in feed
    elif (self._mInputState == ePureAscii) and self._escDetector.search(self._mLastChar + aBuf):
TypeError: Can't convert 'bytes' object to str implicitly

Тут ми бачимо невдале зіткнення стилю кодування з інтерпретатором Python. TypeError може виникати будь-де в тому рядку, але інтерпретатор не вкаже точно де саме. Виняток може виникати в першій умові, або в другій, і трейсбек виглядатиме так само. Щоб його звузити, поле пошуку, варто розділити рядок надвоє, якось так:

elif (self._mInputState == ePureAscii) and \
    self._escDetector.search(self._mLastChar + aBuf):

І перезапустити тест:

C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:\home\chardet\chardet\universaldetector.py", line 101, in feed
    self._escDetector.search(self._mLastChar + aBuf):
TypeError: Can't convert 'bytes' object to str implicitly

Ага! Проблема була не в першій умові (self._mInputState == ePureAscii), а в другій. То що в ній могло спричинити TypeError? Можливо ви думаєте що метод search() очікує значення іншого типу, але це не згенерувало б такого трейсбеку. Функції в Python можуть приймати будь-яке значення, якщо ви передасте потрібну кількість аргументів, функція почне виконуватись. Вона може аварійно зупинитись, якщо ви передасте значення типу на який вона не очікувала, але якщо це трапиться, трейсбек буде показувати кудись всередину функції. Але цей трейсбек каже що виконання не перейшло всередину метода search(). Отож, проблема повинна бути в операторі +, який намагається сконструювати значення що буде передано в метод search().

Ми з попереднього зневадження знаємо що aBuf - це масив байтів. Тоді що таке self._mLastChar? Це екземпляра, описана в методі reset(), який викликається в методі __init__().

class UniversalDetector:
    def __init__(self):
        self._highBitDetector = re.compile(b'[\x80-\xFF]')
        self._escDetector = re.compile(b'(\033|~{)')
        self._mEscCharSetProber = None
        self._mCharSetProbers = []
        self.reset()

    def reset(self):
        self.result = {'encoding': None, 'confidence': 0.0}
        self.done = False
        self._mStart = True
        self._mGotData = False
        self._mInputState = ePureAscii
        self._mLastChar = ''

І тепер ми маємо нашу відповідь. Ви її бачите? self._mLastChar - це рядок, але aBuf - масив байтів. І ви не можете конкатенувати рядок з масивом байтів - навіть порожній рядок.

А що self._mLastChar взагалі таке? В методі feed(), лише на кілька рядків нижче від того місця де трапився трейсбек.

if self._mInputState == ePureAscii:
    if self._highBitDetector.search(aBuf):
        self._mInputState = eHighbyte
    elif (self._mInputState == ePureAscii) and \
            self._escDetector.search(self._mLastChar + aBuf):
        self._mInputState = eEscAscii

self._mLastChar = aBuf[-1]

Функція викликає цей метод feed() знову і знову з кількома байтами за раз. Метод обробляє байти які йому були дані (в параметрі aBuf), потім зберігає останній байт в self._mLastChar на випадок якщо він буде потрібним при наступному виклику. (В багатобайтових кодуваннях метод feed() може бути викликаним з половиною символа, а потім викликаним знову з іншою половиною. Але через те що aBuf тепер байтовий масив а не рядок, self._mLastChar теж повинен бути байтовим масивом. Тому:

  def reset(self):
      .
      .
      .
-     self._mLastChar = ''
+     self._mLastChar = b''

Пошук "mLastChar" по всьому коду знаходить подібну проблему в mbcharsetprober.py, але замість останнього символа, він відслідковує останні два символи. Клас MultiByteCharSetProber використовує список з односимвольних рядків щоб слідкувати за останніми двома символами. В Python 3, він повинен використати список цілих, тому що він насправді запам'ятовує не символи, він запам'ятовує байти. (Байти - це просто символи від 0 до 255).

  class MultiByteCharSetProber(CharSetProber):
      def __init__(self):
          CharSetProber.__init__(self)
          self._mDistributionAnalyzer = None
          self._mCodingSM = None
-         self._mLastChar = ['\x00', '\x00']
+         self._mLastChar = [0, 0]

      def reset(self):
          CharSetProber.reset(self)
          if self._mCodingSM:
              self._mCodingSM.reset()
          if self._mDistributionAnalyzer:
              self._mDistributionAnalyzer.reset()
-         self._mLastChar = ['\x00', '\x00']
+         self._mLastChar = [0, 0]

Unsupported operand type(s) for +: 'int' and 'bytes'[ред.]

В мене є гарні новини і погані новини. Гарні новини полягають в тому що ми здійснюємо прогрес...

C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:\home\chardet\chardet\universaldetector.py", line 101, in feed
    self._escDetector.search(self._mLastChar + aBuf):
TypeError: unsupported operand type(s) for +: 'int' and 'bytes'

... А погані новини в тому, що цей прогрес не завжди відчутно.

Але це прогрес! Справді! Навіть незважаючи на те що трейсбек показує на той самий рядок коду, це вже інша помилка. Прогрес! То яка прооблема в нас тепер? Останнього разу коли я перевіряв, цей рядок коду не намагався конкатенувати ціле число (int</code) з байтовим масивом (bytes). Насправді, ви щойно витратили багато часу переконуючись що self._mLastChar буде байтовим масивом. Як він перетворився на int?

Відповідь лежить не в попередніх рядках коду, а в оцих:

if self._mInputState == ePureAscii:
    if self._highBitDetector.search(aBuf):
        self._mInputState = eHighbyte
    elif (self._mInputState == ePureAscii) and \
            self._escDetector.search(self._mLastChar + aBuf):
        self._mInputState = eEscAscii

self._mLastChar = aBuf[-1]
Кожен елемент рядка - це рядок. Кожен елемент байтового масиву - це ціле число.

Ця помилка не виникає при першому виклику методу feed(), вона виникає при другому, після того як self._mLastChar отримає значення останнього байта aBuf. Ну добре, а з цим яка проблема? А така, що якщо взяти елемент байтового масиву, то це буде ціле число, а не байтовий масив. Щоб побачити різницю, давайте разом відкриємо інтерактивну оболонку:

>>> aBuf = b'\xEF\xBB\xBF' 
>>> len(aBuf)
3 
>>> mLastChar = aBuf[-1] 
>>> mLastChar  
191 
>>> type(mLastChar)  
<class 'int'> 
>>> mLastChar + aBuf  
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'bytes' 
>>> mLastChar = aBuf[-1:]  
>>> mLastChar
b'\xbf' 
>>> mLastChar + aBuf  
b'\xbf\xef\xbb\xbf'
① Опишемо байтовий масив довжиною 3.
② Останнім елементом цього байтового масиву є 191.
③ Це ціле число.
④ Конкатенація цілого числа з байтовим масивом не працює. Ми щойно відтворили помилку на яку натрапили в universaldetector.py.
⑤ Ах, ось і вирішення. Замість того аби отримувати останній елемент байтового масиву, використайте зрізання списків, аби створити новий байтовий масив що містить лише останній елемент. Тобто, почніть з останнього елементу і продовжуйте зріз до кінця байтового масиву. Тепер mLastChar - це байтовий масив довжини 1.
⑥ Конкатенація байтового масиву довжини 1 з байтовим масивом довжини 3 дає новий байтовий масив довжини 4.

Отож, щоб переконатись що метод feed() в universaldetector.py продовжує працювати не зважаючи на те як часто його викликають, ви повинні ініціалізувати self._mLastChar як байтовий масив нульової довжини а тоді пересвідчитись що він залишатиметься байтовим масивом.

              self._escDetector.search(self._mLastChar + aBuf):
          self._mInputState = eEscAscii

- self._mLastChar = aBuf[-1]
+ self._mLastChar = aBuf[-1:]

ord() expected string of length 1, but int found[ред.]

Ще не втомились? Ми вже майже там...

C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml                       ascii with confidence 1.0
tests\Big5\0804.blogspot.com.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:\home\chardet\chardet\universaldetector.py", line 116, in feed
    if prober.feed(aBuf) == constants.eFoundIt:
  File "C:\home\chardet\chardet\charsetgroupprober.py", line 60, in feed
    st = prober.feed(aBuf)
  File "C:\home\chardet\chardet\utf8prober.py", line 53, in feed
    codingState = self._mCodingSM.next_state(c)
  File "C:\home\chardet\chardet\codingstatemachine.py", line 43, in next_state
    byteCls = self._mModel['classTable'][ord(c)]
TypeError: ord() expected string of length 1, but int found

Гаразд, c - int, але функція ord() очікувала односимвольного рядка. Це справедливо. Де описаний c?

# codingstatemachine.py
def next_state(self, c):
    # for each byte we get its class
    # if it is first byte, we also get byte length
    byteCls = self._mModel['classTable'][ord(c)]

Явно не тут, сюди він просто передається. Давайте подивимось вище в стеку.

# utf8prober.py
def feed(self, aBuf):
    for c in aBuf:
        codingState = self._mCodingSM.next_state(c)

Ви це бачите? В Python 2, aBuf був рядком, тому c був односимвольним рядком. (Це те що ви отримаєте при ітерації по рядку - всі символи, один за одним.) Але зараз, aBuf - байтовий масив, тому c - це int, а не односимвольний рядок. Іншими словами, немає потреби викликати функцію ord(), тому що c - це вже int!

Тому:

  def next_state(self, c):
      # for each byte we get its class
      # if it is first byte, we also get byte length
-     byteCls = self._mModel['classTable'][ord(c)]
+     byteCls = self._mModel['classTable'][c]

Шукаючи в коді входження "ord(c)" можна виявити подібні проблеми в sbcharsetprober.py...

# sbcharsetprober.py
def feed(self, aBuf):
    if not self._mModel['keepEnglishLetter']:
        aBuf = self.filter_without_english_letters(aBuf)
    aLen = len(aBuf)
    if not aLen:
        return self.get_state()
    for c in aBuf:
        order = self._mModel['charToOrderMap'][ord(c)]

... та в latin1prober.py ...

# latin1prober.py
def feed(self, aBuf):
    aBuf = self.filter_with_english_letters(aBuf)
    for c in aBuf:
        charClass = Latin1_CharToClass[ord(c)]

c ітерується крізь aBuf, що означає що це ціле число, а не односимвольний рядок. Рішення є таким самим - замінити ord(c) на просто c.

  # sbcharsetprober.py
  def feed(self, aBuf):
      if not self._mModel['keepEnglishLetter']:
          aBuf = self.filter_without_english_letters(aBuf)
      aLen = len(aBuf)
      if not aLen:
          return self.get_state()
      for c in aBuf:
-         order = self._mModel['charToOrderMap'][ord(c)]
+         order = self._mModel['charToOrderMap'][c]

  # latin1prober.py
  def feed(self, aBuf):
      aBuf = self.filter_with_english_letters(aBuf)
      for c in aBuf:
-         charClass = Latin1_CharToClass[ord(c)]
+         charClass = Latin1_CharToClass[c]

Unorderable types: int() >= str()[ред.]

Давайте ще раз.

C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml                       ascii with confidence 1.0
tests\Big5\0804.blogspot.com.xml
Traceback (most recent call last):
  File "test.py", line 10, in <module>
    u.feed(line)
  File "C:\home\chardet\chardet\universaldetector.py", line 116, in feed
    if prober.feed(aBuf) == constants.eFoundIt:
  File "C:\home\chardet\chardet\charsetgroupprober.py", line 60, in feed
    st = prober.feed(aBuf)
  File "C:\home\chardet\chardet\sjisprober.py", line 68, in feed
    self._mContextAnalyzer.feed(self._mLastChar[2 - charLen :], charLen)
  File "C:\home\chardet\chardet\jpcntx.py", line 145, in feed
    order, charLen = self.get_order(aBuf[i:i+2])
  File "C:\home\chardet\chardet\jpcntx.py", line 176, in get_order
    if ((aStr[0] >= '\x81') and (aStr[0] <= '\x9F')) or \
TypeError: unorderable types: int() >= str()

Через що це? "Unorderable types"? Знову, різниця між байтовими масивами і рядками показує свою огидну голову. Подивіться на код:

class SJISContextAnalysis(JapaneseContextAnalysis):
    def get_order(self, aStr):
        if not aStr: return -1, 1
        # find out current char's byte length
        if ((aStr[0] >= '\x81') and (aStr[0] <= '\x9F')) or \
           ((aStr[0] >= '\xE0') and (aStr[0] <= '\xFC')):
            charLen = 2
        else:
            charLen = 1

А звідки з'явився aStr? Давайте піднімемось стеком:

def feed(self, aBuf, aLen):
    .
    .
    .
    i = self._mNeedToSkipCharNum
    while i < aLen:
        order, charLen = self.get_order(aBuf[i:i+2])

О, дивіться, це наш старий друг, aBuf. Як ви вже могли дізнатись при вирішенні інших проблем в цьому розділі, aBuf це масив байтів. Тут, метод feed() не просто передає його оптом, він його зрізає. Але, як ви бачили раніше в цьому розділі, зрізання байтового масиву повертає нам байтовий масив, тому параметр aStr, який передається до метода get_order() все ще є байтовим масивом.

І що цей код намагається зробити з aStr? Він бере перший елемент байтового масиву та порівнює його з рядком довжини 1. В Python 2 таке працювало, тому що aStr та aBuf були рядками, і aStr[0] був рядком, і ви могли порівняти рядки на нерівність. Але в Python 3, aStr та aBuf є байтовими рядками, aStr[0] є цілим, і ви не можете порівнювати рядки та цілі числа не привівши тип одного з них до іншого явно.

В цьому випадку немає потреби робити код більш складним додаючи явне перетворення тпиів. aStr[0] дає ціле число, а всі змінні з якими ми це число порівнюємо є константами. Давайте змінимо їх значення з односимвольних рядків на цілі числа. І поки ми тут, давайте перейменуємо aStr на aBuf, тому що вона насправді містить не рядок.

  class SJISContextAnalysis(JapaneseContextAnalysis):
-     def get_order(self, aStr):
-      if not aStr: return -1, 1
+     def get_order(self, aBuf):
+      if not aBuf: return -1, 1
          # find out current char's byte length
-         if ((aStr[0] >= '\x81') and (aStr[0] <= '\x9F')) or \
-            ((aBuf[0] >= '\xE0') and (aBuf[0] <= '\xFC')):
+         if ((aBuf[0] >= 0x81) and (aBuf[0] <= 0x9F)) or \
+            ((aBuf[0] >= 0xE0) and (aBuf[0] <= 0xFC)):
              charLen = 2
          else:
              charLen = 1

          # return its order if it is hiragana
-      if len(aStr) > 1:
-             if (aStr[0] == '\202') and \
-                (aStr[1] >= '\x9F') and \
-                (aStr[1] <= '\xF1'):
-                return ord(aStr[1]) - 0x9F, charLen
+      if len(aBuf) > 1:
+             if (aBuf[0] == 202) and \
+                (aBuf[1] >= 0x9F) and \
+                (aBuf[1] <= 0xF1):
+                return aBuf[1] - 0x9F, charLen

          return -1, charLen

  class EUCJPContextAnalysis(JapaneseContextAnalysis):
-     def get_order(self, aStr):
-      if not aStr: return -1, 1
+     def get_order(self, aBuf):
+      if not aBuf: return -1, 1
          # find out current char's byte length
-         if (aStr[0] == '\x8E') or \
-           ((aStr[0] >= '\xA1') and (aStr[0] <= '\xFE')):
+         if (aBuf[0] == 0x8E) or \
+           ((aBuf[0] >= 0xA1) and (aBuf[0] <= 0xFE)):
              charLen = 2
-         elif aStr[0] == '\x8F':
+         elif aBuf[0] == 0x8F:
              charLen = 3
          else:
              charLen = 1

        # return its order if it is hiragana
-    if len(aStr) > 1:
-           if (aStr[0] == '\xA4') and \
-              (aStr[1] >= '\xA1') and \
-              (aStr[1] <= '\xF3'):
-                 return ord(aStr[1]) - 0xA1, charLen
+    if len(aBuf) > 1:
+           if (aBuf[0] == 0xA4) and \
+              (aBuf[1] >= 0xA1) and \
+              (aBuf[1] <= 0xF3):
+               return aBuf[1] - 0xA1, charLen

        return -1, charLen

Пошук викликів функції ord() по всьому коду виявляє цю проблему в chardistribution.py (якщо точніше, то в класах EUCTWDistributionAnalysis, EUCKRDistributionAnalysis, GB2312DistributionAnalysis, Big5DistributionAnalysis, SJISDistributionAnalysis, та EUCJPDistributionAnalysis. В кожному випадку, виправлення аналогічне до зміни яку ми зробили для класів EUCJPContextAnalysis та SJISContextAnalysi в jpcntx.py.

Global name 'reduce' is not defined[ред.]

Ще раз в пролом...

C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml                       ascii with confidence 1.0
tests\Big5\0804.blogspot.com.xml
Traceback (most recent call last):
  File "test.py", line 12, in <module>
    u.close()
  File "C:\home\chardet\chardet\universaldetector.py", line 141, in close
    proberConfidence = prober.get_confidence()
  File "C:\home\chardet\chardet\latin1prober.py", line 126, in get_confidence
    total = reduce(operator.add, self._mFreqCounter)
NameError: global name 'reduce' is not defined

Згідно з офіційним повідомленням Що нового в Python 3.0, функція reduce() була переміщена з головного простору імен в модуль functools. Цитуючи повідомлення: "Використовуйте functools.reduce() якщо вам справді це потрібно; щоправда в 99 відсотках випадків явний цикл for є більш читабельним". Ви можете прочитати більше про це рішення на блозі Ґвідо ван Россума: The fate of reduce() in Python 3000.

def get_confidence(self):
    if self.get_state() == constants.eNotMe:
        return 0.01
  
    total = reduce(operator.add, self._mFreqCounter)

Функція reduce() приймає два аргументи - функцію та список (якщо говорити точно, будь-який ітеровний об'єкт) - та застосовує функцію кумулятивно до кожного елемента списку. Іншими словами, це екстравагантний обхідний шлях додавання (чи іншої операції) всіх елементів списку та повернення результату.

Виділений вище рядок коду був настільки типовим, що в Python ввели глобальну функцію sum().


  def get_confidence(self):
      if self.get_state() == constants.eNotMe:
          return 0.01
  
-     total = reduce(operator.add, self._mFreqCounter)
+     total = sum(self._mFreqCounter)

Так як ми більше не використовуємо модуль operator, можна також видалити відповідний import на початку файлу.

  from .charsetprober import CharSetProber
  from . import constants
- import operator

А МОНА МНУ ТЕСТИ?

C:\home\chardet> python test.py tests\*\*
tests\ascii\howto.diveintomark.org.xml                       ascii with confidence 1.0
tests\Big5\0804.blogspot.com.xml                             Big5 with confidence 0.99
tests\Big5\blog.worren.net.xml                               Big5 with confidence 0.99
tests\Big5\carbonxiv.blogspot.com.xml                        Big5 with confidence 0.99
tests\Big5\catshadow.blogspot.com.xml                        Big5 with confidence 0.99
tests\Big5\coolloud.org.tw.xml                               Big5 with confidence 0.99
tests\Big5\digitalwall.com.xml                               Big5 with confidence 0.99
tests\Big5\ebao.us.xml                                       Big5 with confidence 0.99
tests\Big5\fudesign.blogspot.com.xml                         Big5 with confidence 0.99
tests\Big5\kafkatseng.blogspot.com.xml                       Big5 with confidence 0.99
tests\Big5\ke207.blogspot.com.xml                            Big5 with confidence 0.99
tests\Big5\leavesth.blogspot.com.xml                         Big5 with confidence 0.99
tests\Big5\letterlego.blogspot.com.xml                       Big5 with confidence 0.99
tests\Big5\linyijen.blogspot.com.xml                         Big5 with confidence 0.99
tests\Big5\marilynwu.blogspot.com.xml                        Big5 with confidence 0.99
tests\Big5\myblog.pchome.com.tw.xml                          Big5 with confidence 0.99
tests\Big5\oui-design.com.xml                                Big5 with confidence 0.99
tests\Big5\sanwenji.blogspot.com.xml                         Big5 with confidence 0.99
tests\Big5\sinica.edu.tw.xml                                 Big5 with confidence 0.99
tests\Big5\sylvia1976.blogspot.com.xml                       Big5 with confidence 0.99
tests\Big5\tlkkuo.blogspot.com.xml                           Big5 with confidence 0.99
tests\Big5\tw.blog.xubg.com.xml                              Big5 with confidence 0.99
tests\Big5\unoriginalblog.com.xml                            Big5 with confidence 0.99
tests\Big5\upsaid.com.xml                                    Big5 with confidence 0.99
tests\Big5\willythecop.blogspot.com.xml                      Big5 with confidence 0.99
tests\Big5\ytc.blogspot.com.xml                              Big5 with confidence 0.99
tests\EUC-JP\aivy.co.jp.xml                                  EUC-JP with confidence 0.99
tests\EUC-JP\akaname.main.jp.xml                             EUC-JP with confidence 0.99
tests\EUC-JP\arclamp.jp.xml                                  EUC-JP with confidence 0.99
.
.
.
316 tests

Чорт забирай, справді працює! Я танцюю.


* * *


Підсумок[ред.]

Чого ми навчились?

  1. Портувати будь-який нетривіальний код з Python 2 на Python 3 буде боляче. І цього ніяк не уникнути. Це важко.
  2. Автоматизований інструмент 2to3 допомагає чим може, але він опрацьовує лише легкі частини - перейменування функцій, перейменування модулів, зміни синтаксису. Це вражаюче інженерне творіння, хоча з іншого боку - всього лише розумний інструмент пошуку та заміни.
  3. Проблемою №1 в цій бібліотеці була різниця між рядками та байтами. В даному випадку це виглядає очевидним, так як ввесь сенс бібліотеки chardet - в перетворенні потоку байтів у рядок. Але "рядок байтів" зустрічається набагато частіше ніж ви можете подумати. Читаєте файл в "двійковому" режимі? Отримаєте потік байтів. Завантажуєте веб-сторінку? Викликаєте веб API? Вони теж повертають потік байтів.
  4. Ви повинні розуміти свою програму. В деталях. Бажано - тому що ви її написали, але щонайменше - просто комфортно почуватись з всіма її примхами та затхлими кутами. Помилки повсюди.
  5. Тести суттєві. Не портуйте нічого без них. Єдине через що я впевнений що chardet працює в Python 3 є те що я почав з набору тестів які виконали всі основні частини коду. Якщо у вас немає тестів, напишіть хоч якісь перед тим як почати портувати на Python3. Якщо у вас є кілька тестів, напишіть ще. Якщо у вас є багато тестів, тоді можуть початись справжні веселощі.


Веб-сервіси HTTP · Пакування бібліотек