【SE雑談】某サイトにおける「凛として時雨」の文字化け現象の追跡と、異なる文字コード間における文字化け現象について


身近にいい「文字化け」の題材を見つけたので、
これを元ネタに「文字化け」について改めて振り返る、という目的で、
少しこれを深堀してみる(深堀るほどのネタにもならないが…)。

まあどっちかというとこれを単純に好奇心と興味本位で追求したいという思いの方が強いのだが。
(仕事に飽きただけである)


 


 
きっかけはツイッターのTLで某フォロワーさんから「『凛として時雨』の文字がおかしい」というつぶやきがあったことによる。
どうも問題があったのはFMノースウェーブのサイトのようだ。
そのフォロワーさんによれば、「凛として時雨」という文字が「蜃帙→縺励※譎る岑」で表示されている、とのことである。
自身が担当しているシステムでこんなこと起きたらと思うと速攻で逃げ出したくなる案件であるが、
まあそれはいいとして、実際どういう流れでこんなことになったのか?を追跡してみたい。

このテの「文字化け」は、得てして、特定の文字コードAで書かれたテキストを異なる文字コードBで表示しようとしたときに起きる。
文字コードAでは「1番は"あ"、2番は"い"」という決め事だが(あくまで例だ)
文字コードBでは「1番は"憎"、2番は"悪"」という決め事のため(あくまで例だ)
Aで「あい」と表示されていたものが
Bで「憎悪」と表示されることになるのだ(あくまで例だが言い得て妙だ)
これはまあネタで書いたがw、要するに、
文字そのものを示すコード値を変換しないまま、異なる文字コード間で表示をしようとしているためにこうなる。
この例だと、文字コードBでも、"あ"や"い"に対応するコード値が存在する(はずな)わけなので、
表示に際してはそれを意識して「文字コード変換」をしてやらなければならない。
文字コードBでは「98番が"あ"、99番が"い"」だとすると

  • Aの1番⇒Bの98番(あ)
  • Aの2番⇒Bの99番(い)

という「変換」が必要になるわけである。

ここまではただの一般知識に過ぎず、わざわざここで語るまでもないことだが、
実際、今回の「凛として時雨」がどのような流れで「蜃帙→縺励※譎る岑」になったのか?
を探ってみることにする。




最初これを聞いた時、「UTF-8からSJISか、SJISからUTF-8だろーなー」と思って予想はしていた。
ただ昨今のWebサイトのトレンドがUTF-8っぽい気がするので、表示する側(つまりWebサイト)はUTF-8だと思っていた。
ここで、SJISで書いた「凛として時雨」を変換無でUTF-8で表示しなおしてみる。

SJISUTF-8

凛として時雨 �z�Ƃ��Ď��J


あれっ?全然違う。
どうやら表示側はUTF-8ではないようだな。
つまり、UTF-8⇒SJISのようだ。
で、逆にしてみる。

UTF-8SJIS

凛として時雨 蜃帙→縺励※譎る岑


おおっ!正解を発見した。

この時点で実物を見ていたわけではなかったが、事象そのものは再現確認ができた。
その後フォロワーさんに現象が発生していたサイトを教えてもらい、実際、FMノースウェーブのサイトを拝見したら、
確かにこのサイトは文字コードがSJISになっていることが確認できた。
つまり
UTF-8の「凛として時雨」を無理やりSJISのサイト上に表示させようとして起きた
というのが事のあらましだということがわかった。

より細かく実態を追いかけてみると

 
(A)UTF-8コード値 E5 87 9B E3 81 A8 E3 81 97 E3 81 A6 E6 99 82 E9 98 A8
(B)SJISにおける同一コード値に対応する文字
(C)SJISにおける正しい文字コード値 997A 82C6 82B5 82C4 8E9E 894A


となる。
UTF-8は基本的に(※)漢字や平仮名、片仮名1文字を3バイトで表現するが、SJISは2バイトである。
このため「凛として時雨」の6文字はUTF-8では6×3=18バイトになり、
18バイトはSJIS換算(1文字2バイト)だと18÷2=9文字になるため、
文字化け後の方が文字数が増える形になっている。
(※)当然、例外もある。例えばサロゲートペアは1文字4バイトになる。また、1文字2バイトの文字もある。これについては、自身の失敗体験談の一例を載せているので参考にしてほしい。どうでもいいが。

冒頭の例でいえば、例えば「凛」は、
UTF-8の[E5-87-9B]というコード値を、
SJISの[99-7A]というコード値に変換してあげなければ正しく表示ができない、
というのが上の表の(A)と(C)の対応付けの表現になっている。




ちなみに、最初の予想(SJIS⇒UTF-8)だった場合に同じ追跡を行うと

 
(D)SJISコード値 99 7A 82 C6 82 B5 82 C4 8E 9E 89 4A
(E)UTF-8における同一コード値に対応する文字 z Ƃ Ď J


となる。

ほとんど見えない(環境によって見え方が異なると思う。俺は菱形マーク内に「?」が入った文字で見える)と思うが、
対応するコード値が示すのがUTF-8(Unicode)上では制御文字だったりするので、形を持った「文字」にならないようだ。
というより、対応する「文字」が見つかると(この「見つけ方」も環境依存がありそうだが)、優先してそっちをまず「文字」にあてはめようとするため、
取り残されたほうのバイトが孤立して結果制御文字に当てはめられる、というケースもある。

例えば最初の[99]は恐らくSGCIという制御文字になっている。
(ただUnicodeのU+0099は、UTF-8では[C2][99]という2バイト表現が必要なように思うが、
 先頭に[C2]がないのでそもそもSGCIという制御文字になってるかどうかも怪しい。詳細は後述する)
2バイト目の[7A]はUTF-8ではASCII文字の半角zに対応する為ちゃんと「文字」で表現された。
一方、3バイト目の[82]は[99]と同じ考え方で制御文字になっているのに、
4~5バイト目の[C6][82]は2バイト合わせて1文字にされている。
これは、[C6]という、UTF-8における2バイト文字範囲のコードが現れたため、
「[C6]の次に控えるバイトを含めて2バイト文字だ」と表示する側(ブラウザだったり、テキストエディタだったり)が判断したためだ。
実際、[C6][82]は2バイト文字の、なんか「b」のてっぺんに右に向かう髭が付いたような文字になっている。(ドイツ語?かな)

これが、本項の冒頭で書いた”対応する「文字」が見つかると優先してそっちをまず「文字」にあてはめようとする”という考え方だ。
3~5バイト目の、…[82][C6][82]…という並びの中で、「後半2バイトをくっつける」という判断は、”後半2バイトはちゃんと「文字」になる”という理由から。
結果、先頭1バイトの[82]だけは孤立してしまい、無理やり制御コードっぽく振る舞う形になっている。
1バイト目の[99]もそうだが、本来、UnicodeのU+0080以降のコード値は、
UTF-8では先頭に[C2]~[DF]をつけて2バイト文字として扱うのが基本ルールだが、
[99]も[82]も、前方にいるはずの1バイト目がいない状態で孤立させられてしまっている。
([99]のところで”恐らく”SGCIという制御文字になっているといったのはそのため)

まあ”対応する「文字」が見つかると優先してそっちをまず「文字」にあてはめようとする”というのはUTF-8⇒SJISにおいても同じで、
今回たまたまSJIS側にちゃんと「文字」が控えていたので、
SJIS⇒UTF-8のケースに比べて制御文字が現れる(孤立させられる)ことがなかったということだろう。
というか、UTF-8の全角文字が1文字3バイトで表現するのに対し、SJISの全角文字が1文字2バイト表現なわけだから、
多い方を少ない方にあてはめるUTF-8⇒SJISはまだ救いようがあるが、
少ない方を多い方にあてはめるSJIS⇒UTF-8は救える範囲が少なくなる、
というのは、感覚的にもまあそうだろうなって感じはする。




このサイトはラジオ局の公式ページであり、トップページに現在オンエア中の曲情報を表示している。
きっかけになったフォロワーさんはこれを見て「文字化けが起きている」ことを確認したという。
「現在オンエア中の」というのがポイントなので、
つまり後から確認しにいってもその曲が終わればトップページの表示は次の曲に切り替わってしまうため、
リアルタイムに事象をこの目で確かめることはできないわけだ(「羊の群れは丘を登る」のようだ)
ただ過去にオンエアした曲の一覧というのがあって、それを確認することはできた。
しかし、それを見ると、例えば「秦基博」「キュウソネコカミ」「桑田佳祐」などのアーティスト名は文字化けていない。

半角アルファベット等のASCII文字は異なる文字コード間で互換があるので(ってどっかで書いたな…まあいいか)
基本、UTF-8⇔SJISなどの異なる文字コード間で、特に意識しなくても(意図して「変換」を実施しなくても)文字化けが起こることはない。
なので、「ANDROP」「LOST IN TIME」あたりはこの問題の影響範囲外になる。
ちなみにその意味では「ストレイテナー」も同様の問題を孕んでいることになるが、
↑のオンエア記録を見る限りでは「STRAIGHTENER」という英字表記だったため、
この形でのオンエアならやはり同様に影響範囲外だったであろう。
一方で漢字・平仮名・片仮名などの全角文字は、文字コードごとにコード値が異なる為、意図して変換しないと今回のような文字化けを起こす。
よって、「凛として時雨」がだめなら「秦基博」「キュウソネコカミ」「桑田佳祐」あたりも全滅していておかしくないはずなのだ。
例えば「秦基博」なら↓のようになっていたはずだ。

 
(A) E7 A7 A6 E5 9F BA E5 8D 9A
(B)


今までの考察に則れば「秦基博」「キュウソネコカミ」「桑田佳祐」あたりは、
ちゃんとUTF-8⇒SJISへの文字コード変換しているということになるが、
そうなるとやはり「凛として時雨」だけが漏れる対象になるのはどうにも解せないので、
「オンエア情報はSJISで保持する」というのが基本ルールとしてあるが、
「凛として時雨」の今回のケースだけをたまたまUTF-8で保持してしまった(ないし、SJIS保持ルールの対応漏れがあった)ために発生した

と考える方が、まあ自然な流れな気はする。
⇒要するに「オンエア情報」と「それを表示する側」は同じ文字コードを取り扱っていて、
 両者間で文字コード変換の必要がなく、そもそも「変換する」という考え方やロジック自体が存在しない

逆に言えば、
「表示する側」と「オンエア情報」は、どうも物理的に別の場所・管理下にあり、
「表示する側」は、どこかにある「オンエア情報」を、何らかのタイミングで読み込んで表示している
(オンエア予定時刻に従ってリアルタイム読込するとか?)
という、このWebサイトの大まかな作りはなんとなく想像ができる。
この問題を知ったあと、この「大まかな作り」に気付いたときには、
「そもそも「表示する側」(SJIS)と「オンエア情報」(UTF-8)で違う文字コードにしてる時点で危うい」と思っていたが、
実際はそうではなく、
「両者間は基本原則「同じ文字コード」で動いていて、データ管理が物理的に分かれているだけ」
という事情なら、
今回のようにたまたま1件だけ漏れたというのはなんとなく説明が付くし、わからんでもない。
(なんかの理由で1件だけUTF-8にしちまったんだろう、みたいな)

まあどこまでいっても予想の域は出ないし、
人のシステムの粗を付けるほど自分の担当したシステムが優秀なわけではもちろんないので
課題の深堀はこの程度にしておくか。

ちなみに今回、「凛として時雨」でオンエアされた曲のタイトルは「DIE meets HARD」である。
これには全角文字が含まれておらず、英字だけで構成されているため、「文字化け」の影響範囲外である。
例えば今回オンエアされたのが「鮮やかな殺人」「CRAZY感情STYLE」などの、
全角文字だけで構成されている・ないし全角文字を含む曲だった場合はどうなっていたかというのは気になるところではある。
過去にそういった事例があったのかも見ておきたいところだ。
まあ、時間があったら探してみよう。。




なお、余談になるが、これをJavaで再現する簡易実装例を下記に挙げる。
(クラス名は彼らの楽曲「Telecastic Fake Show」に則っているが全く完全に関連はなくただの思い付きである)

import java.io.*;  

public class TelecasticFakeShowTest {

public static void main(String[] args) throws Throwable {  
  
	String strOrg = "凛として時雨";  
	String strUtf8 = new String(strOrg.getBytes("UTF-8"),"UTF-8");  
	String strSjis = new String(strOrg.getBytes("MS932") , "MS932");  
	String strBug = new String(strOrg.getBytes("UTF-8"),"MS932");  
	String strBug2 = new String(strOrg.getBytes("MS932"),"UTF-8");  
	  
	System.out.println("1.UTF-8でバイト化し、UTF-8でエンコード→" + strUtf8);  
	System.out.println("2.SJISでバイト化し、SJISでエンコード→" + strSjis);  
	System.out.println("3.UTF-8でバイト化し、SJISでエンコード→" + strBug);  
	System.out.println("4.SJISでバイト化し、UTF-8でエンコード→" + strBug2);  
	  
}  

}


Javaでは一部の文字に対する考慮を除けば

String str = new String("凛として時雨".getBytes("UTF-8"),"UTF-8");  


でUTF-8に文字コード変換できるし

String str = new String("凛として時雨".getBytes("MS932"),"MS932");  


でSJISに文字コード変換できる。
これを基にした実装例になる。
1.2.はこれに則っているが、
3.4.はgetBytesの引数と、Stringコンストラクタの引数がそれぞれ違う文字コードになっており、
特定の文字コードにおけるバイト値を表すbyteの配列を、違う文字コードにあてはめて変換しようと(意図的に)している。

実行すると以下のようになる(Windowsマシン上での実行結果である)。


結局、最後に表示(標準出力)するのはWindowsマシン上なので、
1.~4.とも全て、表示する段階でSJISにされちまっているが、
まあそういう意味では今回の事例に近い実験結果が見られるだろう。
今回のケースは3.にあたる。
UTF-8におけるコード値を、そのままSJISに当てはめた結果だ。
4.は、文字になった一部を除いて全部「?」になっているが、
これはSJIS上に対応する文字がない(文字として表現できない)ことを指している。
このあたりは↑で書いたことに則る。
1.2.はどちらも同じ結果だが、
1.は裏で持ってるコードがUTF-8なので、表示する段階で再度SJISに変換しなおしている
(これは意図して実装してるんじゃなく、JavaがSystem.out.printlnするときに勝手にやってる)
UTF-8のほうが文字数が多い関係で、「UTF-8にしかない文字」があった場合、4.のように「?」になるが、
「凛として時雨」は全ての文字がUTF-8にもSJISにもあるので結果が変わらない。
結果として1.2.は同じになる。




ところで「凛として時雨」はずいぶん前のシングルとアルバムを買って以来、だいぶ疎遠になってしまった。
一時期「Telecastic Fake Show」を狂ったように聴きこんでいたのを思い出す。
(どうでもいいけどこれもう9年も前なんだな、、、)
最近のやつ聴いたことないが(今回オンエアの「DIE meets HARD」も知らない)、聴いてみようかな~