「ASCIIをUTF-8にして」それが『できない』ことを理解してもらえなかった話


物語の始まり

事の発端は納品後。
先方からメッセージが届きました。

クライアント様「このファイルの文字コードがShift_JISになっておりますので、UTF-8で再納品をお願いいたします。」

拙者(あれ…UTF-8にしてたと思うんだけどな)

拙者「確認いたします。」

文字コードを確認する

本案件はいわゆる更新案件で、今回の納品時に言われていたのは、「文字コードがUTF-8ではないものは変換して納品してくれ」ということ。

そして、ご指摘いただいたのは、今回の更新案件で中身はいじらなかったJavaScriptファイル。
本来ならば納品するファイルではないのですが、文字コード変換という要件があったため、納品ファイルとして加えられたものでした。

一括で文字コードを変えたので作業漏れかなぁと思っていました。

ファイルの中身は記事用にかなり適当につくったものですが、まあだいたいこんな感じです。

sample.js
const outputMessage = function () {
  console.log('sample');
};

エディタで確認する

私はVSCodeでコーディングを行っておりましたので、まず見るところはウィンドウの右下です。

確かにUTF-8にはなっています。

ターミナルで確認する

Macで作業しておりましたので、ターミナルで文字コードを確認します。

まず、fileコマンドを使用してみます。
--mimeオプションをつけて、Web屋さんにはおなじみであろうMIMEタイプで出力してもらいます。

fileコマンド
$ file --mime sample.js
sample.js: text/plain; charset=us-ascii

あれ?

nkfコマンドでも試します。(homebrewから導入しました)

nkfコマンド
$ nkf -g sample.js
ASCII

文字コードはASCII(アスキー)だった

改めてsample.jsを見返してみると、ASCIIと互換性のある1バイト文字しか使っていませんでした。

互換性のある文字コード部分しか使っていないために、コンピュータにはASCIIもUTF-8もShift_JISも同じに見え、閲覧環境のデフォルトエンコーディング設定によってはUTF-8と表示されたり、Shift_JISと表示されるのだろう、クライアントの閲覧環境はデフォがShift_JISなんだろうと予測しました。

つまり、納品したファイルは、互換性がある文字のみを含んでいるため、文字コードをShift_JISからUTF-8に変換しようとしても変換できないファイルだったということです。

検証

このままお返しするにはエビデンスが足りない気がしたので、本当にUTF-8にしてもShift_JISにしても同じデータなのか確認します。

文字コード表を確認

まずは、UTF-8とShift_JISの文字コードを確認します。

便宜上16進数で説明を進めます。
ビット・バイト・16進数などの説明は省きます。

ASCII

ASCIIは7ビットコードです。

00〜1Fは制御文字、20は空白となっており、その次が印字可能文字、つまり記号やABC...が始まります。
表を全部書くのは大変なので一部抜粋します。(フルバージョンはWikipedia

-0 -1 -2 -3 -4 -5 -6 -7 -8 -9 -A -B -C -D -E -F
4- @ A B C D E F G H I J K L M N O
5- P Q R S T U V W X Y Z [ \ ] ^ _

つまり、16進数で表した時、0x41→A、0x5A→Zとなります。(0xは16進数であることを表す)

UTF-8

UTF-8は、ASCIIと互換性をもたせるため、ASCIIと同じ部分は1バイトで表現し、その他は2〜6バイトで表現します。
つまり、ASCIIで定義されている記号や英数字部分は全く同じです。

Shift_JIS

Shift_JISは、JIS X 0201部分を1バイトで表現し、JIS X 0208部分を2バイトで表現します。
平たく言えば、JIS X 0201は半角英数とカタカナを表現し、JIS X 0208がその他日本語を表現する部分です。

この、JIS X 0201の英数字部分はASCIIと全く同じ、というわけではなく、実は印字した際に変わるものが2つあります。

それは、0x5Cと、0x7Eになります。

ASCIIでは、それぞれ「\(バックスラッシュ)」「~(チルダ)」と表示されますが、Shift_JISでは「¥(円マーク)」「‾(オーバーライン)」と表示されます。
このバックスラッシュが厄介で、文字化けの原因になったりしますが、それはまた別のお話。

ただし、表示されるものが変わるというだけで、バイナリとしてはASCIIと同じなので、Shift_JISかそれ以外かという判断はできないということになります。

バイナリを確認する

これらを言葉で説明するのはなかなか面倒なので、実際にバイナリを確認して差分をとれば立派なエビデンスになるのでは!?と考えた私はバイナリを覗きました。

今回はxxdコマンドを使います。(odとかhexdumpでもヨシ、癖がない気がするのでxxdがすこ)
xxdは、ファイルや標準入力から受け取ったものを2進数、もしくは16進数でダンプできるコマンドです。
厳密に言えばバイナリ(2進数)ではないですが。。。

ASCII

まずはASCIIのまま。

xxd
$ xxd sample.js
00000000: 636f 6e73 7420 6f75 7470 7574 4d65 7373  const outputMess
00000010: 6167 6520 3d20 6675 6e63 7469 6f6e 2028  age = function (
00000020: 2920 7b0a 2020 636f 6e73 6f6c 652e 6c6f  ) {.  console.lo
00000030: 6728 2773 616d 706c 6527 293b 0a7d 3b    g('sample');.};

UTF-8

これをnkf -wで文字コードをUTF-8に変換して、もう一度xxdで見てみます。

UTF-8でxxd
$ nkf -w sample.js > sample_utf8.js

$ xxd sample_utf8.js
00000000: 636f 6e73 7420 6f75 7470 7574 4d65 7373  const outputMess
00000010: 6167 6520 3d20 6675 6e63 7469 6f6e 2028  age = function (
00000020: 2920 7b0a 2020 636f 6e73 6f6c 652e 6c6f  ) {.  console.lo
00000030: 6728 2773 616d 706c 6527 293b 0a7d 3b    g('sample');.};

パッと見同じです。ダンプして差分を取ってみます。

diff
$ xxd sample.js > sample_b

$ xxd sample_utf8.js > sample_utf8_b

$ diff sample_b sample_utf8_b  # 出力なし

ですよねー。
ちなみに、文字コードを確認しても、

ASCIIなんだってば
$ file --mime sample_utf8.js
sample_utf8.js: text/plain; charset=us-ascii

Shift_JIS

続いて、Shift_JIS。

nkf -sで文字コードをShift_JISに変換します。

sjisでxxd
$ nkf -s sample.js > sample_sjis.js

$ xxd sample_sjis.js 
00000000: 636f 6e73 7420 6f75 7470 7574 4d65 7373  const outputMess
00000010: 6167 6520 3d20 6675 6e63 7469 6f6e 2028  age = function (
00000020: 2920 7b0a 2020 636f 6e73 6f6c 652e 6c6f  ) {.  console.lo
00000030: 6728 2773 616d 706c 6527 293b 0a7d 3b    g('sample');.};

$ xxd sample_sjis.js > sample_sjis_b

$ diff sample_b sample_sjis_b  # 出力なし

$ file --mime sample_sjis.js
sample_sjis.js: text/plain; charset=us-ascii

こちらも差分なし、ということでバイナリ的にも全く一緒のデータであることが分かりました。

おまけ:日本語だとちゃんと差分が出ることを確認する

sample_ja.js(utf-8)
const outputMessage = function () {
  console.log('サンプル');
};
$ file --mime sample_ja.js
sample_ja.js: text/plain; charset=utf-8    # 日本語を含んだのでutf-8と認識されている

$ xxd sample_ja.js
00000000: 636f 6e73 7420 6f75 7470 7574 4d65 7373  const outputMess
00000010: 6167 6520 3d20 6675 6e63 7469 6f6e 2028  age = function (
00000020: 2920 7b0a 2020 636f 6e73 6f6c 652e 6c6f  ) {.  console.lo
00000030: 6728 27e3 82b5 e383 b3e3 8397 e383 ab27  g('............'
00000040: 293b 0a7d 3b                             );.};

$ xxd sample_ja.js > sample_ja_b

$ nkf -s sample_ja.js > sample_ja_sjis.js

$ file --mime sample_ja_sjis.js
sample_ja_sjis.js: text/plain; charset=unknown-8bit    # unknown-8bit = SJISの意

$ nkf -g sample_ja_sjis.js    # 念の為nkfコマンドで確認 (-g もしくは --guess)
Shift_JIS

$ xxd sample_ja_sjis.js 
00000000: 636f 6e73 7420 6f75 7470 7574 4d65 7373  const outputMess
00000010: 6167 6520 3d20 6675 6e63 7469 6f6e 2028  age = function (
00000020: 2920 7b0a 2020 636f 6e73 6f6c 652e 6c6f  ) {.  console.lo
00000030: 6728 2783 5483 9383 7683 8b27 293b 0a7d  g('.T...v..');.}
00000040: 3b                                       ;

$ xxd sample_ja_sjis.js > sample_ja_sjis_b

$ diff sample_ja_b sample_ja_sjis_b 
4,5c4,5
< 00000030: 6728 27e3 82b5 e383 b3e3 8397 e383 ab27  g('............'
< 00000040: 293b 0a7d 3b                             );.};
---
> 00000030: 6728 2783 5483 9383 7683 8b27 293b 0a7d  g('.T...v..');.}
> 00000040: 3b                                       ;

報告

拙者「確認いたしましたが、英数字(一部記号)のみ含まれているファイルでして、UTF-8とShift_JISでバイト表現は共通ですので、納品させていただいたファイルで問題ありません。」

クライアント様「文字コードを変換して納品してください。」

拙者「…あ、変換してもバイト表現が一緒なのd」

クライアント様「文字コードを変換して納品してください。

拙者「……承知いたしました。」

〜変換(全く同じファイルを納品)〜

拙者「データとしては同じものですので、文字コードを確認する環境のエンコード設定がShift_JISになっている可能s」

クライアント様「文字コードの変更が確認できませんでした。今回は、本ファイルに関して本番アップロードをしない運びといたします。」

拙者「はい。

まとめ

私が完全敗北した理由として、

  • 文字コードについて、私がうまく表現できなかった
  • クライアント(のIT担当部署)様に話が届くまで、数社(数人)を介さないと言葉が届かない伝言ゲーム案件だった
    • バイナリの話をしても伝わらない気がしたので、エビデンスのためにあれこれ出力したファイルは提出しなかった
    • 出力したファイルも伝言ゲームしてもらう必要があるためちょっと躊躇した

当たり前だろと思われる内容だったかもしれませんが、また同じ状況になったとき、同様の検証や伝達がスムーズに行えるようにまとめた記事でした。

追記:半ば無理やりUTF-8にしてしまう方法

2021/04/01: コメントを受け追記しました。コメントありがとうございます!

今回は案件の特性上突っ込まれそうだったのでやりませんでしたが、ファイルにマルチバイト文字を含ませることで文字コードを指定することができます。

sample.js
// コメント
const outputMessage = function () {
  console.log('sample');
};

このように適当に日本語でコメントを追加してしまえば、

文字コード確認
$ file --mime sample_comment.js 
sample_comment.js: text/plain; charset=utf-8

というように、UTF-8になりました。

(今思えば無理やりコメント追記すればよかったなぁ。。)