きっかけ
PythonでWebスクレイピングで日本語のページから情報を取ってくるときや、ファイルを開くときに文字コードが原因で怒られることがプログラミング初心者のころはよくありました。
そのようなエラーが起きたときは基本的にググって「へー 文字コードってやつが原因なのか とりあえずこうすれば怒られないのね」とそのときそのときでエラーを直していたのですが、よくわからないままにしとくのもアレなので文字コードについて調べて、知識の整理も兼ねたアウトプットとしてこの記事を書きました。
符号化文字集合と文字符号化方式
文字コードについて理解するためには、符号化文字集合と文字符号化方式という概念およびその具体例を理解する必要があります。
この2つは混同している人が多い印象(私も混同していました)です。
文字集合
まず符号化文字集合の土台となる文字集合について説明します。
文字集合は言葉どおり文字の集合のことを指します。
例としては「平仮名」という文字集合なら「あ、い、う、え、お...、わ、を、ん」を要素として、「アルファベット」という文字集合なら「a、b、c、...、y、z」を要素としていると考えるとすぐにわかるでしょう。
「Unicode」、「JIS X 0208」、「ASCII」は文字集合です。
「Unicode」は全世界のすべての文字を含んだ(ことを目的とした)文字集合であり、「JIS X 0208」は日本語で使用される文字を含んだ文字集合であり、「ASCII」は英数字その他の記号を含んだ文字集合です。
符号化文字集合
文字集合の例の一部として「Unicode」、「JIS X 0208」、「ASCII」を例に挙げましたが、正確にはこれらは符号化文字集合と呼ばれるものです。
これらは集合の要素をコンピュータで表すために、集合の要素と数値が一対一に対応付けられています。
この記事では、この数値のことをコードポイントとよびます。
例としてPython3を使って文字を10進数、16進数、バイナリ表記のコードポイントで表現してみましょう。
Python3の組み込み関数であるordはString型の入力を受け取ると、Unicodeでそれに対応するコードポイントの10進数表記を返します。
char = "あ" #当然ですがUnicodeは平仮名もサポートしています。
# 10進数で表す
ord(char)
# >>> 12354
# 16進数で表す(0xは16進数表記であることを表す接頭辞です)
hex(ord(char))
# >>> '0x3042'
# バイナリで表す(0bはバイナリ表記であることを表す接頭辞です)
bin(ord(char))
# >>> '0b11000001000010'
文字符号化方式
文字符号化方式は、符号化文字集合によって出力されたコードポイントを、コンピュータが利用できるバイト列に変換する方式のことです。
よくみる「UTF-8」、「UTF-16」、「Shift_JIS」は文字符号化方式の1種です。
「UTF-8」、「UTF-16」はUnicode集合のコードポイントをバイト列に対応させる変換方式、「Shift_JIS」はJIS系(日本語系)の集合のコードポイントをバイト列に対応させる変換方式です。Windowsを使っているとたまに見るCP932は、「Shift_JIS」をマイクロソフトが独自に拡張した変換方式です。
実際にPythonで文字をバイト列に変換してみましょう。
文字列の前のbはバイト列であることを表しています。
sample_str = "あめんぼ"
# UTF-8方式でバイト列に変換
print("UTF-8: ", sample_str.encode("utf-8"))
# >>> UTF-8: b'\xe3\x81\x82\xe3\x82\x81\xe3\x82\x93\xe3\x81\xbc'
# UTF-16方式でバイト列に変換
print("UTF-16: ", sample_str.encode("utf-16"))
# >>> UTF-16: b'\xff\xfeB0\x810\x930|0'
# Shift_JIS方式でバイト列に変換
print("Shift_JIS: ", sample_str.encode("shift_jis"))
# >>> Shift_JIS: b'\x82\xa0\x82\xdf\x82\xf1\x82\xda'
同じ文字列でもこのように文字コードとして出力されるバイト列は全く変わったものになることがわかります。
結局文字コードって?
上で説明した2つの概念「符号化文字集合」と「文字符号化方式」はそれぞれ言い換えるならば、
「文字に割り当てた番号(コードポイント)」と「実際にコンピュータが扱うバイト列」です。
この2つをまとめた「文字とバイト列の対応関係」のことを文字コードというそうです。
文字コードの必要性
データ量
上述のように文字は文字コードによってバイト列として扱われます。
ここで同じ文字でも文字コードによって対応するバイト列が異なることは上でも説明しましたが、文字に対応するバイト列が長ければ当然その文字のデータ量も多くなります。
上の説明だけだと、わかりづらいのでPython3を使って説明します。
str = "あ"
# UTF-8で「あ」をバイト列で表す
str.encode("UTF-8")
# >>> b'\xe3\x81\x82'
# 0xE3 0x81 0x82で「あ」という文字1つで3バイト
# shift_jisで「あ」をバイト列で表す
str.encode("shift_jis")
# >>> b'\x82\xa0'
# 0x82 0xA0で 「あ」という文字1つで2バイト
このように同じ「あ」という文字1つ表すのに、UTF-8では3バイト、shift_jisでは2バイトと文字コードによってデータ量が変わってくることがわかります。
もちろんデータ量が少ないほうが好ましいので、使う文字の範囲によってデータ量が最適化される文字コードがあったほうが便利です。
後方互換性
文字コードにおける後方互換性とは、ある文字コードXが適用されたものを、他の文字コードYを適用しても文字コードXの通りに文字がデコードされるとき、文字コードYは文字コードXに対して後方互換性を持つといいます。
例えば、UTF-8はASCIIに対して後方互換性を持っているので、ASCIIで書かれたファイルなどに文字コードUTF-8を適用してもコンピュータはしっかり文字を出力することができます。
この後方互換性についてですが、ASCIIコードを前提に書かれたプログラムが多い(httpなどが当てはまるらしいです)ため、特にASCIIとの後方互換性を持っていることが重要です。
このように互換性の付与やデータ量を考慮して、複数の文字コードが開発されてきました。
Pythonで文字コードの変換
Python3では文字列型(str)はUTF-8をデフォルトの文字コードとして使うようになっています。
余談
僕はPython3しか触ったことがないのですが、Python2ではデフォルトの文字コードが英数字のみをサポートしているASCIIだったので、日本語を使う際には文字コードの変換が必要だったそうです。Python3しゅごい。
またPython3では、文字列を出力する際にはOSのデフォルトで使われる文字コードにエンコードしてから出力します。例えばWindowsではcp932というShift_JISを独自拡張した文字コードを標準として使っています。
守備範囲が広いUTF-8をデフォルトの文字コードとして使っているため、基本的にPythonで文字コードの変換を行うことはあまりないのですが、たまに以下のようなエラーが起きることがあります。
sample_character = "ō"
# Unicodeのコードポイントで表す
hex(ord(sample_character))
# >>> '0x14d'
# utf-8のバイト列で表す
sample_character.encode("utf-8")
# >>> b'\xc5\x8d'
# shift_jisのバイト列で表す
sample_character.encode("shift_jis")
# >>> UnicodeEncodeError: 'shift_jis' codec can't encode character '\u014d'
エラーを解釈すると、「shift_jisは \u014dで表されているものを文字として読み取ることができません。」という意味になります。
これはshift_jisという文字符号化方式がサポートしている文字集合(JIS X 0208)が、「ō」(Unicodeでコードポイント0x14d)を要素として含んでいないので、文字として表せないと言っています。
このようなエラーが起きてしまった場合は、「ō」を含んだUnicodeの文字符号化方式であるUTF-8などの文字コードを指定して出力してやることで「ō」の出力の際にエラーが起こらなくなります。
まとめ
ふとした気持ちで調べた文字コードですが奥が深すぎてビビりました。
記事にするにあたって間違いがないように調べたつもりですが間違ったところがあったらご指摘くださるとありがたいです。
参考にしたところ
符号化文字集合と文字符号化方式 - 「プログラマのための文字コード技術入門」を読んだ
UnicodeとかUTF-8とかShift_JISとか色々複雑なので自メモ