106
@heeroo_ymsw

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

物語の始まり

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

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

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

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

文字コードを確認する

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

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

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

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

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

エディタで確認する

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

スクリーンショット 2021-04-01 11.45.42.png

確かに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_jp.js(utf-8)
const outputMessage = function () {
  console.log('サンプル');
};
$ file --mime sample_jp.js
sample_jp.js: text/plain; charset=utf-8    # 日本語を含んだのでutf-8と認識されている

$ xxd sample_jp.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_jp.js > sample_jp_b

$ nkf -s sample_jp.js > sample_jp_sjis.js

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

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

$ xxd sample_jp_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_jp_sjis.js > sample_jp_sjis_b

$ diff sample_jp_b sample_jp_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」

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

拙者「はい。:pensive:

まとめ

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

  • 文字コードについて、私がうまく表現できなかった
  • クライアント(の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になりました。

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

106
ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
heeroo_ymsw
un-t
インターネットビジネスを中心とした企画、設計、デザイン、システム、運用、マーケティング、リサーチ等の総合的なクリエイティブファームです。

コメント

私はそういうのを分かっていない相手に説明するのも面倒(というか時間の無駄)なので、適当にマルチバイト文字を含んだコメントを追記して保存し直して渡してます。
既にファイル中に既存のコメントがあるなら、どこか適当なコメントの行末に全角スペースでも追加しておけば見た目は変えず文字コードは固定できます。
要は、ファイル中のコード実行内容に影響しない部分にマルチバイト文字を含めてしまえばいいのです。

24

@math-oh

閲覧&コメントありがとうございます。

今回は案件の特性を考えて、コメントは諦めてしまいました。。。

ご指摘の手法、邪道だったりするのかな…とも思ったのですが、やっぱりやられている方いらっしゃるんだなぁととても参考になりました!
記事にも追記しておこうと思います。
ありがとうございます!

1
5

@saitofjp
実はBOM付き求められていたオチ…!

その可能性も拭えませんね。。。

1

BOM付きを避けた理由が知りたい

1

円記号問題を引き合いに出すなら。
本来 ASCII には円記号 (¥)は収録されておらず。UTF-8Latin1 がベースなので。
SJIS0x5C (¥)UTF-8 なら U+5C (\) ではなく U+A5 (¥) に変更されていないといけない、っていうのはありますね。。。。
(ASCIIUTF-80x5C (U+5C)BACKSLASH (\) なので )

2

あなたがお使いのエディタのバグです。
エディタのメーカーにバグ報告してください。ではダメですかね?
正直コメントを入れるなんて本質とは関係のない対策はしたくないと思ってしまいます。

0

バイナリ比較までやったのにそれを提示しなかったのは悪手かと思います。
客からすれば「何日も待ったのに何も変わってないものだけを突き返された」と見える訳で、落胆さえされるでしょう。

文字エンコーディングは「どの文字集合でエンコードを行ったか」ではなく「どの文字集合として正しくデコード可能なのか」が重要になります。
この記事の話であれば「ご指摘のファイルはASCII文字集合、いわゆる半角英数字のみが含まれるファイルですのでUTF-8としてもShiftJISとしても正当です。お使いのソフトによっては自動検出でShiftJISと見なす可能性があります。」と返すべきでした。

余談ですが、似た事例として「ファイル毎に文字集合を記憶しているエディタ(SakuraEditor等)を使っているが、一部のファイルで急に文字化けする様になった原因が分からない」というものがあります。

6

文字エンコーディングは「どの文字集合でエンコードを行ったか」ではなく「どの文字集合として正しくデコード可能なのか」が重要になります。

そうでしょうか?私は前者の方が重要であり、そこにアイデンティティがあると考えております。そのため文字化けのなどの問題は『作成時のエンコーディングが悪い』のではなく。『作成されたエンコーディングを理解して、正しくデコードしていないことが悪い』と思っております。

ZIPファイルや実行ファイルしかりそうですが。作成時の文字コードで実行(展開する)ことが、最低限の鉄則です。なので大事なのは、デコードではなくエンコードと考えます。

※ ただし、OSのシステム文字コードに依存する仕組みは国際対応が難しいため、近年 (と言っても随分と経つが) では世界の文字を収録することを目的としたUNICODEがニュートラルな文字集合として扱われてます。

なお手前味噌ではありますが、類似ケースを記事にしたものがございます。

デコードに焦点をあてる危うさの参考になるかと思います。

実際、ZIPコンテナやID3Tagなどでは、エンコード情報を宣言する仕様が追加されております。

1

@kishibashi3

あなたがお使いのエディタのバグです。
エディタのメーカーにバグ報告してください。ではダメですかね?

マルチバイト文字を含まないファイルをどのエンコードと認識するかはエディタにもよりますし設定でデフォルトを変更できるものも多いので、エディタのバグとしてしまうあなたの対応のほうが問題ではないでしょうか。

正直コメントを入れるなんて本質とは関係のない対策はしたくないと思ってしまいます。

そもそもクライアントの要求が「ファイルの文字コードをUTF-8にすること」ですし。

2

そうでしょうか。私はasciiはasciiであってsjisでもutf8でもないと思いますけど。

私なら「このファイルはasciiで、asciiはutf8のサブセットです。asciiをsjisと判断したエディタは誤検知です。お疑いなら御社のエンジニアにご確認下さい。」とでも返しますね。

0

@kishibashi3 さん
Latain-1、UTF-8などは、そもそもASCIIと互換性を明示的に持った文字コード (エンコード) です。ことASCII領域に於いては排他ではなく、識別は出来ません。それをソフトウェアのバグと言うのは無理があります。

(そもそも宣言子なしにソフトウェアに文字コード (エンコード) を認識させると言う考え方自体が破綻してますが。)

0

バグという言葉にそんなにこだわるなら、asciiコードの自動検知は環境によって換わってしまうのです。とでも添えるといいのではないでしょうか。
いや、私が言いたいこと、すでに書いてあったようです。失礼しました。

0

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

自分ならばクライアントに、
「文字コードがShift_JISであると判定された方法をお教えください」
「文字コードがUTF-8であるという定義をお教えください」とヒアリングします。

@heeroo_ymswさんのコメントのように、
もしかすると、クライアントは、BOM付きのUTF-8テキストを欲していたのかもしれません。

2

みなさん BOM BOM 言ってますけど、BOM (ファイルの先頭)だと bash 等々は受け付けてくれないんですよね。
BOM と同じバイト列になるはずですけど Zero Width No-Break Space である U+FEFF (UTF-8 では 0xEF 0xBB 0xBF)を末尾に付けるのがいいのではないかと思いました。

Unicode の仕様通りに対応しているプログラムなら、これがどこかにあってもほぼ何もないのと同じ扱いにしてくれるはず。
(といいつつも、ファイルを連結したファイルで途中にこれが入っている事が理由で発覚しづらい問題に遭遇した事はありますし、最初に言った通りメジャーな物ですら「実際はそうは扱ってくれない」のですけど)

0

@a_k_ さん

みなさん BOM BOM 言ってますけど、BOM (ファイルの先頭)だと bash 等々は受け付けてくれないんですよね。
BOM と同じバイト列になるはずですけど Zero Width No-Break Space である U+FEFF (UTF-8 では 0xEF 0xBB 0xBF)を末尾に付けるのがいいのではないかと思いました。

Unicode の仕様通りに対応しているプログラムなら、これがどこかにあってもほぼ何もないのと同じ扱いにしてくれるはず。

BOM は宣言子であり、その後の逐次処理に大きく関わる内容なので先頭でなければならないと思うのですが。決して 「テキスト ファイルの文字コードを総合的に判断するための目印」 ではありません。

また、UTF-8のBOM (0xEF 0xBB 0xBF)ZERO WIDTH NO-BREAK SPACE (U+FEFF) はバイト列が同じだけで、全くの別物です。

概要
プログラムがテキストデータを読み込む時、その先頭の数バイトからそのデータがUnicodeで表現されていること、また符号化形式(エンコーディング)としてどれを使用しているかを判別できるようにしたものである

Q: What is a BOM?

A: A byte order mark (BOM) consists of the character code U+FEFF at the beginning of a data stream, where it can be used as a signature defining the byte order and encoding form, primarily of unmarked plaintext files. Under some higher level protocols, use of a BOM may be mandatory (or prohibited) in the Unicode data stream defined in that protocol. [AF]

( data stream と表現されているので、基本的には逐次処理のためのものだと思います。)

Q: What should I do with U+FEFF in the middle of a file?

A: In the absence of a protocol supporting its use as a BOM and when not at the beginning of a text stream, U+FEFF should normally not occur. For backwards compatibility it should be treated as ZERO WIDTH NON-BREAKING SPACE (ZWNBSP), and is then part of the content of the file or string. The use of U+2060 WORD JOINER is strongly preferred over ZWNBSP for expressing word joining semantics since it cannot be confused with a BOM. When designing a markup language or data protocol, the use of U+FEFF can be restricted to that of Byte Order Mark. In that case, any U+FEFF occurring in the middle of a file can be treated as an unsupported character. [AF]

( BOMZWNBSP は別物であり、 データ ストリームの途中の U+FEFF は テキスト コンテンツ である。 BOMではなく、ZWNBSPとして解釈されなければならない と明記されております。)

そもそも、シェル スクリプトのシェバンなどの仕組みに於いて、ASCIIは共通言語の様なものです。その共通言語を求めるシーンで、UNICODE固有のBOMの解釈を求めるのは、本来の前方互換性が矛盾しています。BOMは、あくまでもUNICODEの仕様であり、UNICODE内のエンコードの目印です。他の文字コードの相互運用のためのものではないため、他の文字コードを意識し始めた段階で UTF-8のASCII互換性への期待 が破綻します。(もっとも、シェバンを使わずインタプリタで直接するのであれば処理系依存の話なので、テキスト エディタなどと同様にソフトウェア単位で会話は出来ますが。XMLもソフトウェアによってBOMの解釈の有無は異なりますし。)

ZWSP と記述ミスの指摘を頂きましたので、一応修正しました。

1

@libraplanet さん
お返事ありがとうございます。

BOM は宣言子であり、その後の逐次処理に大きく関わる内容なので先頭でなければならないと思うのですが。決して 「テキスト ファイルの文字コードを総合的に判断するための目印」 ではありません。

ここ私の認識とは少しだけ違いました。BOM は "バイト順序の目印 (byte order mark)" です。
先頭でなければならないのは、最初の数十バイトないし数行でエンコーディングを決めるプログラムが多いからですね。

そして用途は 「テキスト ファイルの文字コードを総合的に判断するための目印」だと思っています。

当時, UTF-16 では little endian と big endian の判別の問題に苦慮していました。
(更に当時まだ現役だった国毎の多様なテキストエンコーディングとの間の判別にも)

そこで「じゃあこの仕様通りにしたら、ほぼいるんだかいないんだかわからない稀有な存在になる文字をバイト順が逆になるパターンは予約して存在させない事にして、それ先頭に置いたらすぐに little endian なのか big endian なのかわかるんじゃね。これヘッダみたいな扱いとかにすれば中の方では本当に気にしなくてよくなるし先頭がいいよね。俺ら天才じゃんワロw 」

みたいな経緯だったと想像(妄想)します。

というわけで, バイト単位で処理される UTF-8 にはそもそも BOM は不要であるので付けるべきではないというのが, UTF-8 で BOM を認めない人達の主な主張だったりします。
でもその場合はそれはただの ZWNBSP として、いるんだかいないんだかよくわからない扱いで居てもいいと私は思うんですよねぇ。

あとシェバンの例はわからん事はないのですが、実際には Java のソースコードとかも各種 IDE も純正のコンパイラも BOM は食ってくれなかったような気がします (昔試してダメで以降は素直に BOM なしでやってるので状況は変わってる可能性も)。

それに「そちらを立てるとこちらが立たなくなる」みたいな機能では敢えてどちらも入れないというのもありだと思いますが, ZWNBSP への対応で両立しない機能って特にないと思うんですけどねぇ。
特に読み込むだけのプログラムは。

エディタは「この文字にはカーソル合わせちゃダメなのかー」とか面倒だとは思うんですが、Unicode は割と初期から「この文字とこの文字はくっついて一文字になるから」みたいなのが沢山あったし、そのあたりは実際に問題なく使えるレベルの実装が増えてきてますから、やればできるんでしょう。

(あとですが、ZWNBSP と ZWSP はコードポイントとしても役割としても違うと思います。
もし libraplanet さんが ZWNBSP の事を ZWSP だと思ってたら、すれ違いの度合いが大きくなると思ったので念のため)

0

大変な状況でしたね、お疲れ様です。

「報告」で記載の内容はメールでのやりとりのみですか?そうならば
自分なら相手側が理解していないと判断したあたりで、電話で以下を話します。
・技術に詳しい人間が周りにいないか確認し、いればサポートに入ってもらう
・ASCII部分はどの文字コードでも共通で、ASCIIしか含んでいないファイルの文字コードは気にする必要ない

それで解決に向かわないようなら、マルチバイト文字を含むコメントの追加かなあ、、必要ない修正は加えたくないけど(今回の場合、技術に疎い人のために、ある意味必要な修正)

0
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
記事投稿イベント開催中
新人プログラマ応援 - みんなで新人を育てよう!
~
ユーザーは見つかりませんでした