文字コード

文字コード変換

文字コードは標準ライブラリの codecs モジュールで扱える。直接これを使わなくても、いろいろな関数が encoding=... オプションで文字コード指定を受け付ける。デフォルトはUTF-8である。例えばコマンドライン引数で与えたEUC-JPのファイルをUTF-8に変換して utf8/ サブディレクトリに保存するには、次のようにすればよい。

import sys

def convert(source, target):
    with open(source, 'r', encoding='euc_jis_2004', errors='replace') as f:
        content = f.read()
    with open(target, 'w', encoding='utf-8') as f:
        f.write(content)

if __name__ == "__main__":
    for i in range(1, len(sys.argv)):
        convert(sys.argv[i], "utf8/" + sys.argv[i])

ここで euc_jp では①などの「機種依存文字」がうまく扱えないので euc_jis_2004 を使う。それでも変換できない場合があるので、errors='replace' を付けてエラーで止まらないようにする。例えば ∑ (0xad 0xf4 → U+2211) は変換できなかった。iconv なら EUC-JP-MS とすれば変換できた。

'①'.encode('euc_jp')        # UnicodeEncodeError
'①'.encode('euc_jis_2004')  # b'\xad\xa1'
'∑'.encode('euc_jis_2004')  # UnicodeEncodeError
b'\xad\xa1'.decode('euc_jis_2004')  # '①'
b'\xad\xf4'.decode('euc_jis_2004')  # UnicodeDecodeError

同様に shift_jiscp932 にする。

'①'.encode('shift_jis')  # UnicodeEncodeError
'①'.encode('cp932')      # b'\x87@'
'∑'.encode('cp932')      # b'\x87\x94'

ZIPアーカイブのファイル名文字化け対策

ZIP アーカイブの操作は zipfile パッケージでできる。ただ,日本語環境の Windows で作成したアーカイブは,ファイル名が CP932(Shift JIS の Microsoft 拡張)になっている。これを Mac や Linux で展開するには,UTF-8 に変換しなければならない。

Python 3 の zipfile モジュール(zipfile.py)では

            if zinfo.flag_bits & 0x800:
                # UTF-8 filename
                fname_str = fname.decode("utf-8")
            else:
                fname_str = fname.decode("cp437")

のような処理がされている。コードページ 437(DOS Latin US)はオリジナルの IBM PC の文字セットである。したがって,これを UTF-8 に変換して展開するには,次のようにすればよさそうである(DoraTeX さんによる暗号化 ZIP 対応をマージした):

#! /usr/bin/env python3

import sys
from zipfile import ZipFile
from getpass import getpass

with ZipFile(sys.argv[1]) as z:
    for zinfo in z.infolist():
        if zinfo.flag_bits & 0x1:
            password = getpass('PASSWORD: ')
            z.setpassword(password.encode('utf-8'))
        if not zinfo.flag_bits & 0x800:
            try:
                name = zinfo.filename.encode('cp437').decode('cp932')
            except:
                name = zinfo.filename.encode('cp437').decode('utf-8')
            zinfo.filename = name
        print("Extracting", zinfo.filename)
        z.extract(zinfo)

この19行ほどのファイルを unzip.py という名前で作成して実行可能にしておけば,./unzip.py アーカイブ名.zip で展開できる。

Mac の展開ツール(ZIP アーカイブをダブルクリックすると現れる)はファイル名の文字コードの自動判断ができる(うまくいかない場合があるという話も聞いた)。Windows 8 以降の展開ツールも大丈夫らしい。Windows 7 は KB2704299 で対応したらしい。Mac の /usr/bin/unzip にはファイル名変換機能はない(CentOS 7 の /usr/bin/unzip には -O cp932 オプションがある)。

[追記] DoraTeX さんが改良版を作られた。

NFC,NFD

新しい macOS では APFS というファイルシステムが用いられるが,従来の Mac で用いられていた HFS+ というファイルシステムでは,ファイル名の Unicode 正規化がほぼ NFD という形式になっていた。例えばカレントディレクトリに「パンダ.txt」というファイルを作り,pathlib でファイル名を読み出す:

!touch パンダ.txt  # カレントディレクトリに「パンダ.txt」を作る

import pathlib

path = pathlib.Path(".")
s = str(list(path.glob("*ン*.txt"))[0])

ところが

In [ ]: s
Out[ ]: 'パンダ.txt'

In [ ]: list(s)
Out[ ]: ['ハ', '゚', 'ン', 'タ', '゙', '.', 't', 'x', 't']

In [ ]: [hex(ord(c)) for c in s]
Out[ ]:
['0x30cf',
 '0x309a',
 '0x30f3',
 '0x30bf',
 '0x3099',
 '0x2e',
 '0x74',
 '0x78',
 '0x74']

このように濁点・半濁点が分離される。これが NFD である。このように直前の文字に結合して半濁点,濁点を付ける文字はそれぞれ COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK(U+309A),COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK(U+3099)である。

通常の NFC に直すには unicodedata モジュールを使う:

import unicodedata

t = unicodedata.normalize('NFC', s)

すると,今度は

In [ ]: t
Out[ ]: 'パンダ.txt'

In [ ]: list(t)
Out[ ]: ['パ', 'ン', 'ダ', '.', 't', 'x', 't']

見かけは同じだが中身が違う。

別の例(Pokémon の é):

In [ ]: list('é')
Out[ ]: ['é']

In [ ]: unicodedata.normalize('NFD', 'é')
Out[ ]: 'é'

In [ ]: list(unicodedata.normalize('NFD', 'é'))
Out[ ]: ['e', '́']

たとえAPFSでも,過去のHFS+からコピーしたファイルはNFDになっていることがあるので,要注意。

カレントディレクトリ以下にあるすべてのファイルについて,ファイル名をNFCに直す。XR8L0bKA6Ofb は他と重ならないファイル名なら何でもいい。なぜか os.rename(s, t) ではうまくいかなかった。

for x in pathlib.Path(".").glob("**/*"):
    s = str(x)
    t = unicodedata.normalize("NFC", s)
    if s != t:
        print("Renaming", s)
        os.rename(s, "XR8L0bKA6Ofb")
        os.rename("XR8L0bKA6Ofb", t)

Homebrewのrsyncは --iconv=UTF8-MAC,UTF-8 というオプションでNFD→NFCにしてくれる。ただ,-E がmacOSのrsyncと違って "copy extended attributes, resource forks" の意味を持たないのが残念(man -M /usr/share/man rsyncman -M /usr/local/share/man rsync を比較)。

「埼玉」と「埼⽟」

総務省のマイナンバー制度とマイナンバーカードにある「マイナンバーカード交付状況」PDFで「埼玉」(玉: U+7389)と「埼⽟」(⽟: U+2F5F)が混在しているというを聞いた。後者はCJK部首/康熙部首である。こういうのは unicodedata.normalize('NFKC', '「埼玉」と「埼⽟」') で統一できる('NFKD' だと濁点などが分離する)。こういったものが混在する原因はAdobe DistillerまたはChromeのPDF出力であるとのこと

NFKC化で全部の部首が正規化されるわけではないようだ。次のようにすればよいと教えていただいた

str.maketrans("⺟⺠⻁⻄⻑⻘⻝⻤⻨⻩⻫⻭⻯⻲戶", "母民虎西長青食鬼麦黄斉歯竜亀戸")

詳しくはマイナンバーカード交付状況のPDFをスクレイピング・CSV変換参照。

もう一つ事例を教えていただいた愛知県内の感染者・検査件数からリンクされている「愛知県内発生事例一覧」PDF(例えば今日なら 342317.pdf)である。「長久手市」が「⻑久⼿市」に,「瀬戸市」が「瀬⼾市」に,「西尾市」が「⻄尾市」になってしまっている(ブラウザでは違いが見えにくいが⻑⼿⼾⻄が違う)。こちらはDistillerではなく << ... /Creator (DocuWorks PDF Driver 7.0.4) /Producer (DocuWorks PDF Build 9)>> となっている。プリンタドライバ経由でPDF化したもののようだ。

いずれにしても,次のようなフィルタで正規化できる:

#! /usr/bin/env python3

import fileinput
import unicodedata

tbl = str.maketrans("⺟⺠⻁⻄⻑⻘⻝⻤⻨⻩⻫⻭⻯⻲戶黑", "母民虎西長青食鬼麦黄斉歯竜亀戸黒")

for line in fileinput.input():
    line = unicodedata.normalize('NFKC', line)
    line = line.translate(tbl)
    print(line, end="")

NFKC化の副作用?として,()が()になるなど,全角の英数字・記号類が半角になる。

表引きだけの方法を教えていただいた:

tbl = str.maketrans(
    "⺃⺅⺉⺋⺎⺏⺐⺒⺓⺔⺖⺘⺙⺛⺟⺠⺡⺢⺣⺦⺨⺫⺬⺭⺱⺲⺹⺾⻁⻂⻃⻄⻍⻏⻑⻒⻖⻘⻟⻤⻨⻩⻫⻭⻯⻲⼀⼁⼂⼃⼄⼅⼆⼇⼈⼉⼊⼋⼌⼍⼎⼏⼐⼑⼒⼓⼔⼕⼖⼗⼘⼙⼚⼛⼜⼝⼞⼟⼠⼡⼢⼣⼤⼥⼦⼧⼨⼩⼪⼫⼬⼭⼮⼯⼰⼱⼲⼳⼴⼵⼶⼷⼸⼹⼺⼻⼼⼽⼾⼿⽀⽁⽂⽃⽄⽅⽆⽇⽈⽉⽊⽋⽌⽍⽎⽏⽐⽑⽒⽓⽔⽕⽖⽗⽘⽙⽚⽛⽜⽝⽞⽟⽠⽡⽢⽣⽤⽥⽦⽧⽨⽩⽪⽫⽬⽭⽮⽯⽰⽱⽲⽳⽴⽵⽶⽷⽸⽹⽺⽻⽼⽽⽾⽿⾀⾁⾂⾃⾄⾅⾆⾇⾈⾉⾊⾋⾌⾍⾎⾏⾐⾑⾒⾓⾔⾕⾖⾗⾘⾙⾚⾛⾜⾝⾞⾟⾠⾡⾢⾣⾤⾥⾦⾧⾨⾩⾪⾫⾬⾭⾮⾯⾰⾱⾲⾳⾴⾵⾶⾷⾸⾹⾺⾻⾼⾽⾾⾿⿀⿁⿂⿃⿄⿅⿆⿇⿈⿉⿊⿋⿌⿍⿎⿏⿐⿑⿒⿓⿔⿕戶黑",
    "乚亻刂㔾兀尣尢巳幺彑忄扌攵旡母民氵氺灬丬犭罒示礻罓罒耂艹虎衤覀西辶阝長镸阝青飠鬼麦黄斉歯竜亀一丨丶丿乙亅二亠人儿入八冂冖冫几凵刀力勹匕匚匸十卜卩厂厶又口囗土士夂夊夕大女子宀寸小尢尸屮山巛工己巾干幺广廴廾弋弓彐彡彳心戈戸手支攴文斗斤方无日曰月木欠止歹殳毋比毛氏气水火爪父爻爿片牙牛犬玄玉瓜瓦甘生用田疋疒癶白皮皿目矛矢石示禸禾穴立竹米糸缶网羊羽老而耒耳聿肉臣自至臼舌舛舟艮色艸虍虫血行衣襾見角言谷豆豕豸貝赤走足身車辛辰辵邑酉釆里金長門阜隶隹雨靑非面革韋韭音頁風飛食首香馬骨高髟鬥鬯鬲鬼魚鳥鹵鹿麥麻黃黍黒黹黽鼎鼓鼠鼻齊齒龍龜龠戸黒",
)

ものかのさんのやっかいな漢字 – CJK部首補助/康煕部首が参考になる。


Last modified: