Nacházíte se zde: Domů Ponořme se do Pythonu 3

Úroveň obtížnosti: ♦♦♦♦♦

Případová studie: Přepis chardet pro Python 3

Words, words. They’re all we have to go on.
(Slova, slova. Jsou vším, čeho se musíme držet.)
Rosencrantz a Guildenstern jsou mrtvi

 

Ponořme se

Otázka: Co je příčnou č. 1 vedoucí ke zmatenému textu na webu, ve vaší poštovní schránce a ve všech dokumentech, které kdy byly napsány, napříč všemi počítačovými systémy? Je to kódování znaků. V kapitole Řetězce jsme se bavili o historii kódování znaků a o vytvoření Unicode — „jedno kódování vládne všem“. Moc bych si přál, kdybych se na webových stránkách nikdy víc nesetkával se zmatenými znaky, protože by všechny systémy pro vytváření textu ukládaly přesnou informaci o kódování a protože by byly všechny přenosové protokoly připravené na používání Unicode a každý systém pro zpracování textu by při konverzi mezi kódováními zachovával perfektní věrnost.

Rád bych taky poníka.

Unicode poníka.

Kdyby to tak byl Uniponík.

Budu si muset osedlat autodetekci znakového kódování.

Co se rozumí autodetekcí znakového kódování?

Rozumí se tím to, že vezmeme posloupnost bajtů v neznámém znakovém kódování a pokoušíme se kódování zjistit, abychom si text mohli přečíst. Podobá se to lámání kódu v situaci, kdy nemáme dešifrovací klíč.

Není to náhodou nemožné?

Z obecného pohledu to opravdu je nemožné. Ale některá kódování jsou optimalizována pro určité jazyky a jazyky nejsou náhodné. Některé posloupnosti znaků se objevují neustále, zatímco jiné posloupnosti nedávají žádný smysl. Když osoba plynně ovládající angličtinu otevře noviny a najde „txzqJv 2!dasd0a QqdKjvz“, okamžitě pozná, že nejde o angličtinu (i když se text skládá pouze z písmen, která se v angličtině používají). Na základě studia velkého množství „typického“ textu může počítačový algoritmus simulovat zmíněný druh plynné znalosti a může provést kvalifikovaný odhad týkající se jazyka textu.

Jinými slovy, detekce kódování je ve skutečnosti detekcí jazyka, která se kombinuje se znalostí tendence jazyka používat určité znakové kódování.

Existuje vůbec takový algoritmus?

Jak se ukazuje, tak ano. Všechny nejpoužívanější prohlížeče mají autodetekci kódování zabudovanou, protože web je plný stránek, které neobsahují vůbec žádnou informaci o kódování. Mozilla Firefox obsahuje knihovnu pro autodetekci kódování, která je open source. Knihovnu jsem přenesl do Pythonu 2 a modul jsem nazval chardet. V této kapitole vás krok za krokem provedu procesem přepisování modulu chardet z Pythonu 2 pro Python 3.

Úvod do modulu chardet

Než se do přepisu kódu pustíme, bylo by dobré, kdybyste rozuměli, jak funguje! Toto je stručná příručka pro usnadnění orientace ve vlastním kódu. Knihovna chardet je příliš velká na to, abych její kód vložil do textu této knihy. Ale můžete si ji stáhnout z chardet.feedparser.org.

Hlavním vstupním bodem detekčního algoritmu je universaldetector.py. Obsahuje jednu třídu, UniversalDetector. (Možná jste mysleli, že hlavním vstupním bodem je funkce detect z chardet/__init__.py. To je ale jen funkce pro zvýšení pohodlí, která vytvoří objekt třídy UniversalDetector, zavolá jej a vrátí jeho výsledek.)

UniversalDetector zvládá pět kategorií kódování:

  1. UTF-n s Byte Order Mark (BOM; znak pro určení pořadí bajtů). Zahrnuje UTF-8, obě varianty UTF-16 (Big-Endian a Little-Endian) a všechny 4 varianty pořadí bajtů UTF-32.
  2. Kódování s únikovými znaky (escape encodings), která jsou zcela kompatibilní se 7bitovým ASCII. Znaky spadající mimo ASCII začínají únikovými sekvencemi (escape sequence). Příklady: ISO-2022-JP (japonština) a HZ-GB-2312 (čínština).
  3. Vícebajtová kódování, ve kterých je každý znak reprezentován proměnným počtem bajtů. Příklady: Big5 (čínština), SHIFT_JIS (japonština), EUC-KR (korejština) a UTF-8 bez BOM.
  4. Jednobajtová kódování, ve kterých je každý znak reprezentován jedním bajtem. Příklady: KOI8-R (ruština), windows-1255 (hebrejština) a TIS-620 (thajština).
  5. windows-1252, která používají (zejména v Microsoft Windows) střední manažeři, kteří nerozpoznají znakové kódování od díry v zemi.

UTF-n s BOM

Pokud text začíná značkou BOM, můžeme rozumně předpokládat, že je zakódován v UTF-8, UTF-16 nebo UTF-32. (Značka BOM nám přesně řekne, o které kódování jde. Byla pro tento účel navržena.) To se děje přímo v UniversalDetectoru, který vrátí výsledek okamžitě, bez dalšího zpracovávání textu.

Kódování escape sekvencemi

Pokud text obsahuje rozpoznatelné posloupnosti s únikovými znaky (escape sequence), může to být příznakem použití kódování, kterému se v angličtině říká escaped encoding. UniversalDetector vytvoří EscCharSetProber (je definován v escprober.py) a přivede do něj text.

EscCharSetProber vytvoří sadu konečných automatů, které vycházejí z modelů pro HZ-GB-2312, ISO-2022-CN, ISO-2022-JP a ISO-2022-KR (jsou definovány v escsm.py). EscCharSetProber přivádí text do každého z těchto konečných automatů — bajt po bajtu. Pokud některý z konečných automatů skončí s jednoznačnou identifikací kódování, vrátí EscCharSetProber okamžitě pozitivní výsledek objektu třídy UniversalDetector, který jej vrátí volajícímu. Pokud kterýkoliv z konečných automatů narazí na nepřípustnou posloupnost, je vyřazen a další zpracování pokračuje jen s ostatními konečnými automaty.

Vícebajtová kódování

Za předpokladu, že není použita značka BOM, UniversalDetector zkontroluje, zda text obsahuje nějaké znaky s nastaveným osmým bitem. Pokud tomu tak je, vytvoří sérii „detekčních zařízení“ (prober) pro rozpoznání vícebajtových kódování, jednobajtových kódování a nakonec, jako poslední možnost, pro windows-1252.

Detekční objekt pro vícebajtová kódování, MBCSGroupProber (třída je definována v mbcsgroupprober.py), je ve skutečnosti jen obálkou. Ovládá ostatní detekční objekty, po jednom pro každé vícebajtové kódování: Big5, GB2312, EUC-TW, EUC-KR, EUC-JP, SHIFT_JIS a UTF-8. MBCSGroupProber směřuje text do každého z těchto specializovaných detekčních objektů a kontroluje výsledky. Pokud nějaký detekční objekt hlásí, že nalezl nepřípustnou posloupnost bajtů, je vyřazen z dalšího zpracování (takže například libovolné následné volání metody UniversalDetector.feed() vyřazený detekční objekt přeskočí). Pokud detekční objekt hlásí, že si je poměrně jistý rozpoznáním kódování, oznámí MBCSGroupProber tento pozitivní výsledek objektu UniversalDetector, který oznámí výsledek volajícímu.

Většina z detekčních objektů pro vícebajtová kódování je odvozena z MultiByteCharSetProber (definována v mbcharsetprober.py) a jednoduše se navěsí na příslušný konečný automat a analyzátor rozložení. Zbytek práce nechá na MultiByteCharSetProber. MultiByteCharSetProber prohání text přes konečné automaty specializované na jednotlivá kódování — bajt po bajtu. Vyhledává posloupnosti bajtů, které by indikovaly průkazné pozitivní nebo negativní výsledky. MultiByteCharSetProber současně posílá text do analyzátoru rozložení, který je specifický pro každé kódování.

Analyzátory rozložení (jsou definovány v chardistribution.py) používají jazykově specifické modely nejčastěji se vyskytujících znaků. Jakmile MultiByteCharSetProber předá analyzátorům rozložení dostatečný objem textu, vypočítá ohodnocení spolehlivosti, které je založeno na počtu často používaných znaků, na celkovém počtu znaků a na jazykově závislém rozložení. Pokud je spolehlivost dostatečně velká, vrátí MultiByteCharSetProber výsledek do MBCSGroupProber, který jej vrátí do UniversalDetectoru, který jej vrátí volajícímu.

Případ japonštiny je obtížnější. Analýza rozložení podle jednotlivých znaků nevede vždy k rozlišení EUC-JP a SHIFT_JIS, takže SJISProber (definován v sjisprober.py) používá také dvojznakovou analýzu rozložení. SJISContextAnalysis a EUCJPContextAnalysis (definice se v obou případech nacházejí v jpcntx.py a obě třídy dědí ze společné třídy JapaneseContextAnalysis) v textu kontrolují frekvenci výskytů slabičných znaků hiragana. Jakmile bylo zpracováno dostatečné množství textu, vracejí úroveň spolehlivosti do SJISProber, který zkontroluje oba analyzátory a vrátí výsledek s vyšší úrovní spolehlivosti do MBCSGroupProber.

Jednobajtová kódování

Detekční objekt pro jednobajtové kódování, SBCSGroupProber (třída je definována v sbcsgroupprober.py) je rovněž obálkou, která ovládá skupinu jiných detekčních objektů — jeden pro každou kombinaci jednobajtového kódování a jazyka: windows-1251, KOI8-R, ISO-8859-5, MacCyrillic, IBM855 a IBM866 (ruština); ISO-8859-7 a windows-1253 (řečtina); ISO-8859-5 a windows-1251 (bulharština); ISO-8859-2 a windows-1250 (čeština, maďarština, slovenština a další); TIS-620 (thajština); windows-1255 a ISO-8859-8 (hebrejština).

SBCSGroupProber předává text do každého z těchto detekčních objektů a kontroluje výsledky. Všechny tyto detekční objekty jsou implementovány v jedné třídě, SingleByteCharSetProber (definována v sbcharsetprober.py), která prostřednictvím argumentu přebírá jazykový model. Jazykový model definuje, jak často se v typickém textu vyskytují dvojznakové posloupnosti. SingleByteCharSetProber zpracovává text a zjišťuje nejčastěji se vyskytující dvojznakové posloupnosti. Jakmile byl zpracován dostatečný objem textu, vypočítá úroveň spolehlivosti, která je založena na počtu často se vyskytujících posloupností, na celkovém počtu znaků a na jazykově závislém rozložení.

Hebrejština se řeší jako zvláštní případ. Pokud se text na základě analýzy rozložení dvojznakových posloupností jeví jako hebrejština, snaží se HebrewProber (třída definována v hebrewprober.py) rozlišit mezi vizuální hebrejštinou (kdy je text uložen ve skutečnosti „pozpátku“ řádek po řádku a poté je zobrazen „normálně“, takže může být čten zprava doleva) a logickou hebrejštinou (kdy je zdrojový text uložen v pořadí čtení a klientský program ho vykresluje zprava doleva). Protože se některé znaky kódují jinak podle toho, zda se nacházejí uprostřed slova nebo na jeho konci, můžeme rozumně odhadnout směr zdrojového textu a vrátit příslušné kódování (windows-1255 pro logickou hebrejštinu nebo ISO-8859-8 pro vizuální hebrejštinu).

windows-1252

Pokud UniversalDetector v textu detekuje znaky s nastaveným osmým bitem a žádný z vícebajtových nebo jednobajtových detekčních objektů nevrátil spolehlivý výsledek, vytvoří Latin1Prober (třída je definována v latin1prober.py) a snaží se detekovat anglický text v kódování windows-1252. Tato detekce je ze své podstaty nespolehlivá, protože anglické znaky se kódují stejným způsobem v mnoha různých kódováních. Jediný způsob, jak lze kódování windows-1252 rozpoznat, je založen na běžně používaných symbolech, jako jsou střídavé uvozovky (smart quotes; knižní, jiný znak na začátku a jiný na konci), kulaté apostrofy, symbol copyright a podobně. Latin1Prober automaticky redukuje ohodnocení své spolehlivosti, aby umožnil přesnějším detektorům vyhrát, pokud je to vůbec možné.

Spouštíme 2to3

Jsme připraveni k přenesení modulu chardet z Pythonu 2 do Pythonu 3. Python 3 se dodává s pomocným skriptem nazvaným 2to3, který jako vstup přebírá zdrojový kód napsaný pro Python 2 a automaticky převádí vše, co dovede, do podoby pro Python 3. V některých případech je to snadné — funkce se přejmenovala nebo se přesunula do jiného modulu —, ale v ostatních případech to může být docela složité. Abyste získali představu, co vše umí převést, podívejte se na přílohu Přepis kódu do Python 3 s využitím 2to3. V této kapitole začneme spuštěním 2to3 pro balík chardet. Ale jak brzy uvidíte, po provedení kouzel automatickými nástroji nám zbude ještě spousta práce.

Hlavní balík chardet je rozdělen do několika různých souborů. Všechny se nacházejí ve stejném adresáři. Skript 2to3 převod více souborů najednou usnadňuje. Jako argument na příkazovém řádku stačí předat jméno adresáře a 2to3 převede každý ze souborů, které se v něm nacházejí.

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
.
.
. (takto to chvíli pokračuje)
.
.
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

Teď spustíme skript 2to3 na testovací skript 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

No vida. Nebylo to tak hrozné. Konvertovalo se jen pár importů a příkazů print. Když už o tom mluvíme, jaký byl problém se všemi těmi příkazy import? Abychom na to mohli odpovědět, musíme rozumět tomu, jak se modul chardet dělí na více souborů.

Krátká odbočka k vícesouborovým modulům

chardet je vícesouborový modul. Mohl jsem se rozhodnout, že veškerý kód uložím do jednoho souboru (pojmenovaného chardet.py), ale neudělal jsem to. Místo toho jsem vytvořil adresář (pojmenovaný chardet) a v něm jsem vytvořil soubor __init__.py. Pokud Python najde v adresáři soubor __init__.py, předpokládá, že všechny ostatní soubory ve stejném adresáři jsou součástí stejného modulu. Jméno adresáře je jménem modulu. Soubory v adresáři se mohou odkazovat na ostatní soubory ve stejném adresáři nebo dokonce v jeho podadresářích. (Více si o tom řekneme za minutku.) Ale celá kolekce souborů se okolnímu pythonovskému kódu jeví jako jediný modul — jako kdyby všechny funkce a třídy byly definovány v jediném souboru s příponou .py.

A co je vlastně v souboru__init__.py? Nic. Všechno. Něco mezi tím. Soubor __init__.py nemusí definovat vůbec nic. Může to být doslova prázdný soubor. Nebo jej můžeme použít k definici funkcí, které jsou našimi hlavními vstupními body. Nebo do něj můžeme umístit všechny naše funkce. Podstatná je jediná věc.

Adresář se souborem __init__.py se vždy považuje za vícesouborový modul. Pokud v adresáři není umístěn soubor __init__.py, považuje se prostě za adresář, který nemá k souborům s příponou .py žádný vztah.

Podívejme se, jak to funguje v praxi.

>>> import chardet
>>> dir(chardet)             
['__builtins__', '__doc__', '__file__', '__name__',
 '__package__', '__path__', '__version__', 'detect']
>>> chardet                  
<module 'chardet' from 'C:\Python31\lib\site-packages\chardet\__init__.py'>
  1. Pokud neuvažujeme obvyklé atributy tříd, najdeme v modulu chardet jedinou věc a tou je funkce detect().
  2. Tady máme první stopu, která říká, že modul chardet je víc než jen obyčejným souborem: u slova „module“ se ve výpisu objevuje soubor __init__.py umístěný v adresáři chardet/.

Nahlédněme do souboru __init__.py.

def detect(aBuf):                              
    from . import universaldetector            
    u = universaldetector.UniversalDetector()
    u.reset()
    u.feed(aBuf)
    u.close()
    return u.result
  1. V souboru __init__.py je definována funkce detect(), která je hlavním bodem knihovny chardet.
  2. Ale funkce detect() neobsahuje skoro žádný kód! Ve skutečnosti pouze importuje modul universaldetector a začíná jej používat. Ale kde je definován universaldetector?

Odpověď je skryta v tomto divně vypadajícím příkazu import:

from . import universaldetector

V překladu do češtiny to znamená „importuj modul universaldetector, který je umístěn ve stejném adresáři, jako já“. Tím „já“ se myslí soubor chardet/__init__.py. Říká se tomu relativní import. Představuje způsob, jakým se mohou soubory ve vícesouborovém modulu na sebe vzájemně odkazovat, aniž by se musely starat o konflikty jmen s jinými moduly, které můžeme mít nainstalované v naší vyhledávací cestě pro import. Uvedený příkaz import bude modul universaldetector hledat pouze uvnitř adresáře chardet/.

Zmíněné dva koncepty — __init__.py a relativní importy — znamenají, že náš modul můžeme rozbít na tolik kousků, kolik si přejeme. Modul chardet se skládá z 36 souborů s příponou .py — z 36! A přitom vše, co musíme udělat, když jej chceme začít používat, je import chardet. Pak můžeme zavolat hlavní funkci chardet.detect(). Aniž o tom náš kód ví, funkce detect() je ve skutečnosti definována v souboru chardet/__init__.py. A aniž o tom musíme vědět my, funkce detect() používá k odkazu na třídu definovanou uvnitř chardet/universaldetector.py mechanismus relativního importu, který zase používá relativní import pěti dalších souborů, které se rovněž nacházejí v adresáři chardet/.

Kdykoliv se přistihnete, že v Pythonu píšete rozsáhlou knihovnu (nebo, což je pravděpodobnější, když zjistíte, že se vaše malá knihovna rozrostla ve velkou), udělejte si čas na refaktorizaci a změňte ji na vícesouborový modul. Je to jedna z mnoha věcí, ve kterých je Python dobrý. Takže té výhody využijte.

Opravme, co 2to3 neumí

False je syntaktická chyba

Teď zkusíme skutečný test. Spustíme testovací sadu (test suite) na zkušební skript (test harness). Protože je testovací sada navržena tak, aby pokryla všechny možné cesty, kudy se běh programu může kódem ubírat, jde o dobrý způsob, jak ověřit, že v našem přeneseném kódu někde nejsou skryté chyby.

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

Hmm, to je jen drobnost. V Pythonu 3 je False vyhrazeným slovem, takže je nemůžeme použít jako jméno proměnné. Podíváme se do constants.py na to, kde je proměnná definována. Tady máme původní verzi z constants.py předtím, než ji skript 2to3 změnil:

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

Tento kus kódu byl navržen, aby knihovna běžela ve starších verzích Pythonu 2. Před Pythonem 2.3 neexistoval zabudovaný typ bool. Uvedený kód detekuje nepřítomnost zabudovaných konstant True a False a v případě potřeby je definuje.

Ale v Pythonu 3 je typ bool přítomen vždy, takže je celý úryvek kódu zbytečný. Nejjednodušší řešení spočívá v nahrazení všech výskytů constants.True a constants.False hodnotami True a False. Pak z constants.py odstraníme onen mrtvý kód.

Takže následující řádek v universaldetector.py

self.done = constants.False

se změní na

self.done = False

Ách, nebylo to uspokojující? Kód je teď kratší a čitelnější.

Nenalezen modul constants

Nastal čas spustit znovu test.py. Uvidíme, jak daleko se dostaneme.

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

Co to říká? Jaképak „No module named constants“ (doslova „žádný modul jménem constants“)? Modul constants tam samozřejmě je! Je přímo tady v chardet/constants.py.

Vzpomínáte si, jak skript 2to3 opravil všechny ty příkazy import? Tato knihovna používá množství relativních importů — moduly, které importují jiné moduly nacházející se uvnitř stejné knihovny —, ale v Pythonu 3 se změnila logika relativních importů. V Pythonu 2 jsme mohli jednoduše provést import constants a Python by nejdříve prohledával adresář chardet/. V Pythonu 3 jsou všechny příkazy import absolutní. Pokud chceme v Pythonu 3 provést relativní import, musíme to říct explicitně:

from . import constants

No moment. Neměl se o tohle postarat skript 2to3 za nás? No, on to udělal. Ale tento konkrétní příkaz import kombinoval dva typy importu na jednom řádku: relativní import modulu constants, který se nachází uvnitř knihovny, a absolutní import modulu sys, který je předinstalován jako součást pythonovské standardní knihovny. V Pythonu 2 jsme je mohli zkombinovat do jednoho řádku příkazu import. V Pythonu 3 to nejde a skript 2to3 není dost chytrý na to, aby příkaz import rozdělil na dva.

Řešení spočívá v ručním rozdělení příkazu import. Takže tento import „dva v jednom“…

import constants, sys

… musíme změnit na dva oddělené importy:

from . import constants
import sys

Variace tohoto problému jsou rozesety po celé knihovně chardet. Na některých místech je to „import constants, sys“, jinde je to „import constants, re“. Oprava je stejná. Ručně rozdělíme příkaz import na dva řádky. Na jednom uvedeme relativní import, na druhém absolutní import.

Kupředu!

Jméno 'file' není definováno

A zase jdeme na to. Spouštíme test.py, abychom provedli naše testovací případy…

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

Tak tohle mě překvapilo, protože tento obrat jsem používal, co mi paměť sahá. V Pythonu 2 byla globální funkce file() jiným jménem (alias) pro funkci open(), která představovala standardní způsob otvírání textových souborů pro čtení. V Pythonu 3 už globální funkce file() neexistuje, ale funkce open() je tu nadále.

Takže nejjednodušší řešení problému chybějící funkce file() spočívá v jejím nahrazení voláním funkce open():

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

A to je vše, co o tom můžu říct.

Řetězcový vzorek nelze použít pro bajtové objekty

Teď se začnou dít zajímavé věci. Slůvkem „zajímavé“ rozumím „pekelně matoucí“.

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

Abychom to odladili, podívejme se, co je self._highBitDetector. Je to definováno v metodě __init__ třídy UniversalDetector:

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

Jde o předkompilovaný regulární výraz, který má hledat znaky mimo ASCII, tj. v rozsahu 128–255 (0x80–0xFF). Počkat, tohle není úplně správně. Musíme použít přesnější terminologii. Tento vzorek je navržen pro hledání bajtů s hodnotou mimo ASCII, tedy v rozsahu 128–255.

A v tom je ten problém.

V Pythonu 2 byl řetězec polem bajtů. Jeho kódování znaků bylo zachyceno odděleně. Pokud jsme po Pythonu 2 chtěli, aby znakové kódování udržoval u řetězce, museli jsme použít Unicode řetězec (u''). Ale v Pythonu 3 je řetězec vždy tím, co Python 2 nazýval Unicode řetězec — to znamená polem Unicode znaků (které mohou být vyjádřeny různým počtem bajtů). A protože je tento regulární výraz definován řetězcovým vzorkem, může být použit jen pro prohledávání řetězců, což je pole znaků. Ale my nechceme prohledávat řetězec. Prohledáváme pole bajtů. Pohledem na trasovací výpis zjistíme, že k chybě došlo v universaldetector.py:

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

A co je to aBuf? Podívejme se ještě o kousek zpět, na místo, kde se volá UniversalDetector.feed(). Jedno z míst, kde se volá, se nachází v testovacím kódu (test harness) test.py.

u = UniversalDetector()
.
.
.
for line in open(f, 'rb'):
    u.feed(line)

A tady máme odpověď: aBuf je řádek načítaný v metodě UniversalDetector.feed() ze souboru na disku. Podívejte se pořádně na parametry, které se používají při otvírání souboru: 'rb'. 'r' znamená „read“ (čtení). No dobrá, to je toho. Čteme ze souboru. No jo! 'b' znamená „binárně“. Bez příznaku 'b' by cyklus for četl soubor po řádcích a každý řádek by převáděl na řetězec — tedy na pole Unicode znaků — s využitím systémového výchozího znakového kódování. Ale s příznakem 'b' čte cyklus for ze souboru po řádcích a každý řádek ukládá do pole bajtů přesně v takovém tvaru, v jakém se nachází v souboru. Výsledné pole bajtů se předává do UniversalDetector.feed() a nakonec se dostane až k předkompilovanému regulárnímu výrazu self._highBitDetector, aby se našly osmibitové… znaky. Ale my nemáme znaky. My máme bajty. A do prčic.

Potřebujeme, aby tento regulární výraz nehledal v poli znaků, ale v poli bajtů.

Když už jsme na to přišli, bude náprava jednoduchá. Regulární výrazy definované řetězci mohou hledat v řetězcích. Regulární výrazy definované poli bajtů mohou hledat v polích bajtů. Abychom definovali vzorek polem bajtů, jednoduše změníme typ argumentu, který používáme pro definici regulárního výrazu, na pole bajtů. (Hned na následujícím řádku je další případ téhož problému.)

  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()

Když necháme ve všech zdrojových textech vyhledat použití modulu re, objevíme další dva případy v charsetprober.py. Jde opět o případy, kdy jsou regulární výrazy definovány jako řetězce, ale používáme je pro aBuf, což je pole bajtů. Řešení je stejné: definujeme vzorky regulárních výrazů jako pole bajtů.

  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

Objekt typu 'bytes' nelze implicitně převést na str

Divoucnější a divoucnější…

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

Zde dochází k nešťastné kolizi mezi stylem zápisu zdrojového textu a interpretem Pythonu. Chyba TypeError se může vázat na kteroukoliv část řádku, ale trasovací výpis nám neříká, kde přesně je. Může to být v první nebo v druhé části podmínky, ale z trasovacího výpisu se to nepozná. Abychom prostor pro hledání zúžili, měli bychom řádek rozdělit:

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

A znovu spustíme test:

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

Aha! Problém se nevyskytoval v první části podmínky (self._mInputState == ePureAscii), ale v druhé. Takže co zde vlastně způsobuje chybu TypeError? Možná si myslíte, že metoda search() očekává hodnotu odlišného typu. To by ale nevygenerovalo takový trasovací výpis. Pythonovské funkce mohou přebírat libovolné hodnoty. Pokud předáme správný počet argumentů, funkce se provede. Pokud bychom předali hodnotu jiného typu, než funkce očekává, mohla by havarovat. Ale pokud by se tak stalo, trasovací výpis by ukazoval na místo někde uvnitř funkce. Jenže tento trasovací výpis říká, že se nikdy nedošlo tak daleko, aby se metoda search() zavolala. Takže problém musí být skryt v operaci +, protože ta se snaží o zkonstruování hodnoty, která bude nakonec předána metodě search().

Z předchozího ladění víme, že aBuf je polem bajtů. A co je tedy self._mLastChar? Jde o členskou proměnnou definovanou v metodě reset(), která je ve skutečnosti volána z metody __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 = ''

A tady máme odpověď. Vidíte to? self._mLastChar je řetězec, ale aBuf je pole bajtů. Konkatenaci (zřetězení, spojení) nelze provádět pro řetězec a pole bajtů — ani když jde o řetězec nulové délky.

No dobrá, ale k čemu je tedy self._mLastChar? V metodě feed(), jen pár řádků pod místem označeným v trasovacím výpisu, vidíme…

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]

Volající funkce volá metodu feed() pořád dokola s tím, že jí pokaždé předá pár bajtů. Metoda zpracuje zadané bajty (dostává je v aBuf) a potom uloží poslední bajt do self._mLastChar pro případ, že by jej potřebovala při dalším volání. (Při použití vícebajtového kódování by metoda feed() mohla být zavolána pro polovinu znaku a pak by mohla být volána pro jeho druhou polovinu.) Ale protože je teď aBuf místo řetězce polem bajtů, musíme udělat pole bajtů i z self._mLastChar. Takže:

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

Když ve všech zdrojových souborech vyhledáme „mLastChar“, najdeme podobný problém v mbcharsetprober.py. Ale místo uchovávání posledního znaku se uchovávají poslední dva znaky. Třída MultiByteCharSetProber používá k uchovávání posledních dvou znaků seznam jednoznakových řetězců. V Pythonu 3 musíme použít seznam celých čísel, protože ve skutečnosti neuchováváme znaky, ale bajty. (Bajty jsou prostě celá čísla v intervalu 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]

Nepodporované typy operandů pro +: 'int' a 'bytes'

Mám jednu dobrou a jednu špatnou zprávu. Ta dobrá je, že děláme pokroky…

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'

… Ta špatná je, že to někdy tak nevypadá.

Ale on to je pokrok! Opravdu! I když trasovací výpis označuje stejný řádek kódu, je to jiná chyba, než se hlásila dříve. Pokrok! Takže kdepak máme problém teď? Když jsme to kontrolovali minule, nesnažil se tento řádek řetězit int s polem bajtů (bytes). Ve skutečnosti jsme strávili dost času tím, abychom zajistili, že self._mLastChar bude pole bajtů. Jak se mohlo změnit na int?

Odpověď není skrytá v předchozích řádcích kódu, ale v následujících.

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]

Tato chyba se nevyskytne při prvním volání metody feed(). Vyskytne se při druhém volání poté, co byl proměnné self._mLastChar přiřazen poslední bajt aBuf. No a v čem je tedy problém? Když z bajtového pole získáme jeden prvek, dostaneme celé číslo a ne bajtové pole. Abychom ten rozdíl viděli, ukážeme si to v interaktivním shellu:

>>> 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'
  1. Definujeme pole bajtů o délce 3.
  2. Poslední prvek pole bajtů má hodnotu 191.
  3. Je to celé číslo (integer).
  4. Zřetězení pole bajtů s celým číslem nefunguje. Právě jsme navodili chybu, kterou jsme pozorovali v universaldetector.py.
  5. A tady máme nápravu. Místo získávání posledního prvku z pole bajtů použijeme operaci pro získání výřezu (slicing). Vytvoříme jí nové pole bajtů, které obsahuje jen poslední prvek. To znamená, že začneme posledním prvkem a pokračujeme v tvorbě výřezu (slice), dokud nedosáhneme konce pole bajtů. Teď je mLastChar polem bajtů o délce 1.
  6. Zřetězením pole bajtů o délce 1 s polem bajtů o délce 3 dostaneme nové pole bajtů o délce 4.

Takže abychom zajistili, že bude metoda feed() v universaldetector.py pokračovat v činnosti nezávisle na tom, jak často je volána, musíme inicializovat self._mLastChar polem bajtů o nulové délce a potom musíme zajistit, aby tato proměnná zůstala polem bajtů.

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

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

funkce ord() očekávala řetězec o délce 1, ale byl nalezen int

Jste už unaveni? Už to máme skoro hotové…

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

OK, takže c je typu int, ale funkce ord() očekávala jednoznakový řetězec. No dobrá. Kde je definována proměnná 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)]

To nám nepomůže. Tady se jen předává funkci. Podívejme se hlouběji do zásobníku.

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

Vidíte to? V Pythonu 2 byla proměnná aBuf řetězcem, takže proměnná c byla jednoznakovým řetězcem. (Ten dostáváme, když iterujeme přes řetězec — všechny znaky, jeden po druhém.) Ale teď je aBuf polem bajtů, takže c je typu int a ne jednoznakový řetězec. Jinými slovy, už nepotřebujeme volat funkci ord(), protože c už je typu int!

Takže:

  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]

Vyhledáním „ord(c)“ ve všech zdrojových textech odhalíme podobné problémy v 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)]

… a v latin1prober.py

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

Proměnná c iteruje přes aBuf, což znamená, že v ní bude celé číslo a ne jednoznakový řetězec. Řešení je stejné: ord(c) změníme na prosté 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]

Neuspořádatelné datové typy: int() >= str()

A spusťme to znovu.

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()

A co se děje zase tady? „Unorderable types“ čili neuspořádatelné typy? (Neuspořádatelné ve smyslu, že mezi těmito hodnotami nelze určit pořadí.) A rozdíl mezi bajty a řetězci znovu vystrkuje svou ošklivou hlavu. Ale podívejte se na kód:

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

A odkud se vzala proměnná aStr? Podívejme se hlouběji do zásobníku:

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

Hele, podívejme. To je náš starý přítel aBuf. Jak už jste mohli odhadnout ze všech předchozích problémů, se kterými jsme se v této kapitole setkali, aBuf je pole bajtů. V tomto místě je metoda feed() nepředává jako celek. Vytváří z něj výřez. Ale jak jsme viděli v této kapitole o něco dříve, výřezem z pole bajtů vznikne pole bajtů. Takže parametr aStr, který přebírá metoda get_order(), je pořád pole bajtů.

A co se tento kód s aStr pokouší dělat? Získává první prvek z pole bajtů a srovnává jej s jednoznakovým řetězcem. V Pythonu 2 to fungovalo, protože aStr a aBuf byly řetězce a aStr[0] by byl taky řetězec. U řetězců můžeme zjišťovat, zda jsou různé. Ale v Pythonu 3 jsou proměnné aStr a aBuf poli bajtů a aStr[0] je celé číslo. Číslo a řetězec nemůžeme porovnávat na neshodu, aniž jednu z hodnot explicitně nepřevedeme na stejný typ.

V tomto případě nemusíme kód komplikovat přidáváním explicitního převodu typu. aStr[0] je celé číslo. Vše, s čím ho srovnáváme, jsou konstanty. Můžeme je změnit z jednoznakových řetězců na čísla. A když už to děláme, změňme také identifikátor aStr na aBuf, protože to ve skutečnosti není řetězec (string).

  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

Hledáním výskytu funkce ord() ve zdrojových textech odkryjeme stejný problém v chardistribution.py (konkrétně ve třídách EUCTWDistributionAnalysis, EUCKRDistributionAnalysis, GB2312DistributionAnalysis, Big5DistributionAnalysis, SJISDistributionAnalysis a EUCJPDistributionAnalysis). Ve všech případech se oprava podobá změnám, které jsme provedli v třídách EUCJPContextAnalysis a SJISContextAnalysis v souboru jpcntx.py.

Globální jméno 'reduce' není definováno

Ještě jedna trhlina…

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

Podle oficiálního průvodce What’s New In Python 3.0 byla funkce reduce() vyňata z globálního prostoru jmen a přesunuta do modulu functools. Citujme z průvodce: „Pokud opravdu potřebujete functools.reduce(), použijte ji. Ale v 99 procentech případů je explicitní cyklus for čitelnější.“ O tomto rozhodnutí se dočtete více na weblogu Guida van Rossuma: The fate of reduce() in Python 3000 (Osud reduce v Pythonu 3000).

def get_confidence(self):
    if self.get_state() == constants.eNotMe:
        return 0.01

    total = reduce(operator.add, self._mFreqCounter)

Funkce reduce() přebírá dva argumenty — funkci a seznam (přesněji řečeno, může to být libovolný iterovatelný objekt) — a kumulativně aplikuje zadanou funkci na každý z prvků seznamu. Jinými slovy, jde o efektní a nepřímý způsob realizace součtu všech prvků seznamu.

Tato obludnost byla tak běžná, že byla do Pythonu přidána globální funkce sum().

  def get_confidence(self):
      if self.get_state() == constants.eNotMe:
          return 0.01

-     total = reduce(operator.add, self._mFreqCounter)
+     total = sum(self._mFreqCounter)

Protože jsme přestali používat modul operator, můžeme také ze začátku souboru odstranit příslušný příkaz import.

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

A tož, možeme to otestovať?

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

No to mě podrž, ono to funguje! /me si trošku zatancuje

Shrnutí

Co jsme se naučili?

  1. Přepisování jakéhokoliv netriviálního kódu z Pythonu 2 do Pythonu 3 bude bolestivé. Nedá se to obejít. Je to obtížné.
  2. Automatický nástroj 2to3 nám částečně pomůže, ale postará se jen o snadnější části — přejmenování funkcí, přejmenování modulů, úpravy syntaxe. Jde o impozantní kus inženýrské práce, ale koneckonců jde jen o inteligentního robota provádějícího vyhledávání a náhrady.
  3. Problémem č. 1 při přepisování této knihovny byl rozdíl mezi řetězci a bajty. V tomto případě se to zdá být zřejmé, protože hlavním účelem knihovny chardet je převod proudu bajtů na řetězec. Ale „s proudem bajtů“ se setkáváme častěji, než byste si mysleli. Čtete soubor v „binárním“ režimu? Dostáváte proud bajtů. Získáváte obsah webovské stránky? Voláte webové aplikační rozhraní? Také se vrací proud bajtů.
  4. Programu musíte rozumět vy. Skrz naskrz. Především protože jste ho napsali, ale musíte se vyrovnat se všemi jeho triky a zatuchlými kouty. Chyby jsou všude.
  5. Testovací případy jsou nepostradatelné. Pokud je nemáte, nic nepřepisujte. Jediný důvod, proč věřím tomu, že chardet funguje v Pythonu 3, spočívá v tom, že jsem začal s testovací sadou, která prověřovala všechny hlavní cesty, kudy se kód ubírá. Pokud žádné testy nemáte, napište je dříve, než začnete přenos do Pythonu 3 realizovat. Pokud máte jen pár testů, napište jich víc. Pokud máte hodně testů, pak teprve může začít opravdová legrace.

© 2001–11 Mark Pilgrim