Ruby
 Computer >> コンピューター >  >> プログラミング >> Ruby

Ruby開発者のための文字エンコーディング、Unicode、UTF-8徹底入門

RubyでUndefinedConversionErrorIncompatibleCharacterEncodingsといった例外を目にしたことはありませんか?目にすることはあっても、その意味を正しく理解できた人は少ないのではないでしょうか。この記事を読めば、文字エンコーディングの仕組みと、それがRubyでどう実装されているのかが分かるようになります。読み終える頃には、これらのエラーをより簡単に理解し、修正できるようになっているはずです。

そもそも「文字エンコーディング」とは何なのでしょうか?

どんなプログラミング言語でも、私たちは文字列を扱います。入力として処理したり、出力として表示したりします。しかし、コンピュータは「文字列」を理解できません。コンピュータが理解できるのはビット、すなわち1と0だけです。この文字列をビットへ変換するプロセスこそが、文字エンコーディング(符号化)と呼ばれるものです。

ところで、文字エンコーディングはコンピュータ時代特有のものではありません。コンピュータが登場する前に存在した、もっとシンプルな仕組み——モールス信号から学ぶことができます。

モールス信号

モールス信号の定義は非常にシンプルです。2種類の記号、つまり短点と長点という2つの信号だけで、英字アルファベット全体を表現します。例えば以下の通りです。

  • A は .-(短点1つ+長点1つ)
  • E は .(短点1つのみ)
  • O は ---(長点3つ)

このシステムは1837年頃に考案され、わずか2種類の記号・信号だけでアルファベット全体を符号化することを可能にしました。

Ruby開発者のための文字エンコーディング、Unicode、UTF-8徹底入門

画像に写っているのは「エンコーダー」、つまりメッセージの符号化・復号を担当する人間です。しかしこれは、コンピュータの登場によってまもなく変わることになります。

手作業から自動符号化へ

メッセージを符号化するには、モールス信号のアルゴリズムに従って、人が手作業で文字を記号に変換する必要がありました。

モールス信号と同じように、コンピュータも「1」と「0」という2つの記号だけを使います。コンピュータにはこの並びしか保存できず、読み取った際にはユーザーにとって意味のある形に解釈しなければなりません。

どちらの場合もプロセスは次のように流れます。

Message -> Encoding -> Store/Send -> Decoding -> Message

モールス信号でSOSを送る場合はこうなります。

SOS -> Encode('SOS') -> ...---... -> Decode('...---...') -> SOS
-----------------------              --------------------------
       送信者                                 受信者

コンピュータなど新しい技術による大きな変化は、符号化と復号のプロセスが自動化され、情報の変換にもう人間が不要になったことです。

コンピュータが発明された当初、文字を自動的に1と0へ変換するために作られた初期の標準規格の一つ(最初ではありませんが)がASCIIでした。

ASCIIはAmerican Standard Code for Information Interchange(米国標準情報交換コード)の略です。「米国(American)」という部分が、当時のコンピュータの情報処理のあり方に大きな影響を与えました。その理由は次の章で見ていきます。

ASCII(1963年)

モールス信号のような電信符号やごく初期のコンピュータの知識をもとに、1963年頃、コンピュータ上で文字を符号化・復号するための標準が作られました。このシステムは比較的シンプルで、当初は127文字——英語アルファベットと追加記号——しかカバーしていませんでした。

ASCIIは各文字に10進数の番号を割り当て、それを2進数に変換して保存する仕組みです。例を見てみましょう。

「A」はASCIIでは65です。そこで65を2進数に変換する必要があります。

やり方をご存じない方のために簡単に説明すると:65を2で割り続け、0になるまで繰り返します。割り切れない場合は余りの1を加えます。

65 / 2 = 32 + 1
32 / 2 = 16 + 0
16 / 2 = 8 + 0
8 / 2  = 4 + 0
4 / 2  = 2 + 0
2 / 2  = 1 + 0
1 / 2  = 0 + 1

そして、余りを逆順に並べます。

1000001

つまり、オリジナルのASCII(現在のUS-ASCII)では「A」は「1000001」として保存されます。現在のように8ビットのコンピュータが一般的になった今では、01000001(8ビット=1バイト)となります。

すべての文字に対して同じ処理を行うため、7ビットで最大2^7=127文字を保存できるわけです。

以下が完全な対応表です。

Ruby開発者のための文字エンコーディング、Unicode、UTF-8徹底入門 (https://www.plcdev.com/ascii_chart より)

ASCIIの問題点

ここで、フランス語のçや日本語の「大」のような別の文字を追加したい場合はどうなるでしょう?

そう、問題が発生します。

ASCII以降、人々はこの問題を解決するために独自のエンコーディング方式を作りました。より多くのビットを使う方式ですが、今度は別の問題が生じました。

最大の問題は、ファイルを読み込むとき、それがどのエンコーディング方式なのか分からないことです。誤ったエンコーディングで解釈しようとすると、「」や「Ã,ÂÂÂÂ」のような意味不明な文字列になってしまいました。

こうしたエンコーディング方式の進化は大規模で多岐にわたりました。言語によって異なる方式が必要となり、中国語のように文字数が多い言語では、自国の文字体系を符号化するためにより複雑なシステムを開発せざるを得ませんでした。

長年の苦闘の末、新たな標準が誕生しました。それがUnicodeです。この標準が、現代のコンピュータにおける情報の符号化・復号のあり方を定義しています。

Unicode(1988年)

Unicodeの目標は非常にシンプルです。公式サイトによれば、「プラットフォーム、プログラム、言語に関係なく、すべての文字に一意の番号を提供すること」です。

つまり、各言語の各文字には一意のコードが割り当てられており、これは「コードポイント」とも呼ばれます。現在、13万7千以上の文字が登録されています。

Unicode標準には、これらのコードポイントを符号化する複数の方式がありますが、最も広く使われているのがUTF-8です。

実は、Go言語を作ったRob Pike氏とKen Thompson氏こそがUTF-8の作者です。UTF-8が成功を収めたのは、その数字の符号化方法が効率的かつ巧妙だからです。具体的に見ていきましょう。

UTF-8:Unicode Transformation Format(1993年)

UTF-8は現在、ウェブサイトにおける事実上の標準エンコーディングです(全ウェブサイトの94%以上が採用)。多くのプログラミング言語やファイル形式のデフォルトエンコーディングでもあります。なぜこれほど成功したのでしょうか?その仕組みを解説します。

UTF-8は他のエンコーディング方式と同様に、Unicodeで定義された番号を2進数に変換してコンピュータに保存します。

UTF-8には2つの重要な特徴があります。 - ビットの保存が効率的。1文字は1〜4バイトで表現できる - Unicodeと可変バイト長を採用しているため、先頭127文字が1バイトで済み、ASCIIとの互換性がある。つまりASCIIファイルをUTF-8として開ける

それでは、UTF-8の動作を詳しく分解してみましょう。

1バイトのUTF-8

Unicodeテーブル上の値に応じて、UTF-8は異なるバイト数を使用します。

先頭の127文字については、次のテンプレートを使います。 0_______

先頭の0は常に固定で、その後にUnicode上の値(ASCIIとも共通)を表す2進数が続きます。例:A = 65 = 1000001。

RubyのStringクラスのunpackメソッドで確認してみましょう。

'A'.unpack('B*').first

# 01000001

Bは最上位ビット(最も大きな値を持つビット)を先頭とする2進形式を求める指定です。アスタリスクは、ビットがなくなるまで処理を続けるようRubyに指示します。数値を指定した場合は、その桁数分のビットだけ取得します。

'A'.unpack('B4').first

# 01000

2バイトのUTF-8

Unicode上の値(コードポイント)が127を超え、2047までの範囲の文字には、次のテンプレートで2バイトを使います。

110_____ 10______

Unicodeの値のために空いているのは11ビットです。例を見てみましょう。

ÀはUnicodeでは192、2進数では11000000で8ビットです。1バイトのテンプレートには収まらないため、2バイトのテンプレートを使います。

110_____ 10______

右から左に向かって埋めていきます。

110___11 10000000

残った空きビットはどうするのか?単純に0で埋めます。最終結果は11000011 10000000です。

ここでパターンが見えてきますね。左から読むと、最初の8ビットグループの先頭に1が2つ並んでいます。これは「この文字は2バイトを使う」ことを示しています。

11000011 10000000
--       

これもRubyで確認できます。

'À'.unpack('B*').first

# 1100001110000000

ちょっとしたコツですが、次のように書くと出力を見やすく整形できます。

'À'.unpack('B8 B8').join(' ')

# 11000011 10000000

'À'.unpack('B8 B8')は配列を返すので、joinでスペース区切りの文字列に連結しています。unpackの引数にある「8」は、8ビットずつ2つのグループとして取得せよという意味です。

3バイトのUTF-8

文字のUnicode値が前述のテンプレートの11ビットに収まらない場合、さらに1バイト余分に必要になります。

1110____  10______  10______

ここでもテンプレート冒頭の1が3つ並んでいることで、「3バイトの文字を読もうとしている」ことが分かります。

処理の流れは同じです。Unicode値を2進数に変換し、右から左へスロットを埋めていき、余った空きは0で埋めます。

4バイトのUTF-8

中には、前のテンプレートの11ビットでは足りない値もあります。絵文字🙂の例で見てみましょう。Unicodeにおいて絵文字も「a」や「大」と同じように一つの文字として扱われます。

「🙂」のUnicode上の値(コードポイント)は128578です。これを2進数にすると11111011001000010で17ビット。3バイトテンプレートの空きスロットは16個しかないため収まりません。そこで、メモリ上で4バイトを使う新しいテンプレートが必要になります。

11110___  10______ 10______  10______

また同様に、2進数の値で埋めていきます。

11110___  10_11111 10011001  10000010

そして、残りを0で埋めると次のようになります。

11110000  10011111 10011001  10000010

Rubyで実際に確認してみましょう。

4バイト使うことは既に分かっているので、出力の可読性を高めておきましょう。

'🙂'.unpack('B8 B8 B8 B8').join(' ')

# 11110000 10011111 10011001 10000010

もちろん、シンプルに次のようにも書けます。

'🙂'.unpack('B*')

また、bytesメソッドを使えばバイト列を配列として取り出せます。

"🙂".bytes

# [240, 159, 153, 130]

さらに、mapで各要素を2進数に変換できます。

"🙂".bytes.map {|e| e.to_s 2}

# ["11110000", "10011111", "10011001", "10000010"]

文字列として欲しい場合はjoinを使えばOKです。

"🙂".bytes.map {|e| e.to_s 2}.join(' ')

# 11110000 10011111 10011001 10000010

UTF-8はUnicodeに十分すぎるほどの容量を持つ

UTF-8のもう一つの重要なポイントは、現存するすべてのUnicode値(コードポイント)をカバーできるだけでなく、将来登場する文字までも格納できることです。

なぜなら、UTF-8の4バイトテンプレートには21個の空きスロットがあるからです。つまり最大2^21(=2,097,152)個の値を保存できます。これは、Unicode標準で今後扱うことになる最大約110万の値を大きく上回っています。

このため、将来的に新しい文字や言語のために別のエンコーディング方式へ移行する必要はない、という確信をもってUTF-8を使い続けられるのです。

Rubyでさまざまなエンコーディングを扱う

Rubyでは、文字列のエンコーディングを次のようにすぐに確認できます。

'Hello'.encoding.name

# "UTF-8"

また、文字列を別のエンコーディング方式に変換することもできます。例えば:

encoded_string = 'hello, how are you?'.encode("ISO-8859-1", "UTF-8")

encoded_string.encoding.name

# ISO-8859-1

変換に互換性がない場合、デフォルトではエラーが発生します。例えば「hello 🙂」をUTF-8からASCIIに変換したいとしましょう。絵文字「🙂」はASCIIに含まれないため変換できません。この場合、Rubyはエラーを発生させます。

"hello 🙂".encode("ASCII", "UTF-8")

# Encoding::UndefinedConversionError (U+1F642 from UTF-8 to US-ASCII)

ただしRubyには例外オプションがあり、エンコードできない文字を「?」に置き換えることもできます。

"hello 🙂".encode("ASCII", "UTF-8", undef: :replace)

# hello ?

さらに、特定の文字を新しいエンコーディングで有効な文字に置き換えるオプションもあります。

"hello 🙂".encode("ASCII", "UTF-8", fallback: {"🙂" => ":)"})

# hello :)

Rubyスクリプトのエンコーディングを確認する

自分が編集中のスクリプトファイル(.rbファイル)のエンコーディングを知りたいときは、次のようにします。

__ENCODING__

# 私の環境では "#<Encoding:UTF-8>" と表示されます。

Ruby 2.0以降、RubyスクリプトのデフォルトエンコーディングはUTF-8ですが、先頭行にコメントを入れることで変更可能です。

# encoding: ASCII

__ENCODING__
# #<Encoding:US-ASCII>

ただし、特段の理由がない限りUTF-8標準のままにしておくのがベストです。

Rubyでエンコーディングを扱うためのヒント

Rubyがサポートしているエンコーディングの一覧は、Encoding.name_listで確認できます。大きな配列が返ってきます。

["ASCII-8BIT", "UTF-8", "US-ASCII", "UTF-16BE", "UTF-16LE", "UTF-32BE", "UTF-32LE", "UTF-16", "UTF-32", "UTF8-MAC"...

英語圏以外の文字を扱う際にもう一つ重要なのが、Ruby 2.4以前はupcasereverseなどの一部のメソッドが期待どおりに動作しなかった点です。例えばRuby 2.3では、upcaseは次のような結果になりました。

# Ruby 2.3
'öıüëâñà'.upcase

# 'öıüëâñà'

当時の回避策は、RailsのActiveSupportや外部gemを使うことでしたが、Ruby 2.4からは完全なUnicodeケースマッピングがサポートされるようになりました。

# Ruby 2.4以降
'öıüëâñà'.upcase

# 'ÖIÜËÂÑÀ'

絵文字で遊んでみる

最後に、絵文字がUnicodeとRubyでどう扱われるか見てみましょう。

'🖖'.chars

# ["🖖"]

これは「Raised Hand with Part Between Middle and Ring Fingers」、通称「Vulcan Salute(バルカンの挨拶)」絵文字です。同じ絵文字でも、デフォルト以外の肌の色を付けると興味深いことが起こります。

'🖖🏾'.chars

# ["🖖", "🏾"]

一文字のはずなのに、2つの要素に分割されました。

何が起きたのでしょう?

実は、Unicodeには複数の文字の組み合わせとして定義された文字が存在します。この場合、コンピュータは2つの文字が並んでいるのを見ると、肌の色を適用した1つの絵文字として表示します。

国旗絵文字にも面白い例があります。

'🇦🇺'.chars

# ["🇦", "🇺"]

Unicodeでは、国旗絵文字は内部的に🇦や🇿といった「Regional Indicator Symbols(地域指示記号)」と呼ばれる抽象的な文字で表現されています。これらは通常、国旗以外では使われず、コンピュータは2つの記号が並んでいるのを見ると、その組み合わせに対応する国旗があればそれを表示します。

ぜひ試してみてください。次のテキストをコピーして、テキストエディタなどでカンマを削除してみましょう。

🇦,🇺

まとめ

UnicodeとUTF-8の仕組み、そしてそれらがRubyやエンコーディング関連のエラーとどう関わるのかについての解説が、皆さんの参考になれば幸いです。

最も大切な教訓は、どんなテキストを扱うときでも必ず何らかのエンコーディング方式が関連づけられており、保存や変換の際にはそれを意識することが重要だということです。可能であればUTF-8のようなモダンなエンコーディングを使えば、将来の変換の手間から解放されるでしょう。

Rubyのバージョンについて

この記事のすべての例はRuby 2.6.5で動作確認しています。オンラインREPLや、Rubyがインストールされていればターミナルでirbを実行してローカル環境でも試せます。

最近のリリースでUnicodeサポートが強化されているため、記事の内容が長く役立ち続けるよう最新バージョンを採用しました。いずれにしても、Ruby 2.4以降であれば、ここで示したすべての例がそのまま動作するはずです。

  1. Outlookで文字化けを解消!文字エンコードの変更方法をわかりやすく解説

    Outlookでメールを受信したとき、本文が意味不明な記号や文字化けして表示された経験はありませんか?送信者から届いたメッセージが正しく読めない場合、多くは文字エンコードの不一致が原因です。この記事では、その原因と解決策をわかりやすく解説します。キーボードで入力した文字は、「エンコード」と呼ばれる複雑な処理を通じてコンピューターに認識され、画面上に対応する文字として表示されます。つまり、どの数値がどの文字に対応するのかを決めているのが、この文字エンコードという仕組みです。また、文字の視覚的な表現は「グリフ(Glyph)」と呼ばれ、さまざまなグリフの集合が「フォント」を構成します。文章のフォント

  2. RuboCopで実現するRubyコードのリントと自動フォーマット徹底ガイド

    リンティング(Lint)とは、ソースコードに含まれるプログラム上の誤りやスタイル違反を自動的に検出する仕組みです。このチェックは「リンター」と呼ばれる静的コード解析ツールによって行われます。一方、コードフォーマッターは、あらかじめ設定されたルールセットにソースコードを厳密に準拠させることに特化したツールです。一般的にリンターは違反を報告するだけで、実際の修正はプログラマーの手に委ねられます。それに対してコードフォーマッターは、ルールを直接ソースコードに適用し、フォーマットのミスを自動的に修正してくれます。 プロジェクトで一貫性のあるコードスタイルを実現するには、通常リンティング用とフォーマッ