Skip to content

Instantly share code, notes, and snippets.

@takahashim
Last active October 9, 2023 03:56
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save takahashim/774ac716f31ebbd6f8df35c94a2337cc to your computer and use it in GitHub Desktop.
Save takahashim/774ac716f31ebbd6f8df35c94a2337cc to your computer and use it in GitHub Desktop.

青空文庫テキストファイル用Shift_JISとUnicodeとの変換定義(私案)

本文書は、青空文庫で配布されているテキストファイルをUnicodeに変換する規則、またUnicodeで入力されたテキストファイルを青空文庫テキストファイル用のShift_JISのファイルに変換する規則を提案する。

動機

青空文庫テキストファイルはJIS X 0208のShift_JISで記述されているが、Unicodeに相互変換したいというニーズがある。

  • UTF-8で入力・出力したい
  • UTF-8に対応したツールを使いたい

具体的な例としては、青空文庫テキストファイルの加工・修正にJavaScriptを使う例が挙げられる。JavaScriptでは内部で扱う文字列はUnicode文字列になる。そのためJavaScriptを使おうとすると、文字列をUnicodeとして扱わざるをえない。青空文庫テキストをJavaScript文字列に変換するための変換表を一意に定めなければ、ライブラリ間の互換性が維持できなくなる可能性がある。

なお、青空文庫内のデータベース(図書カードで表示される情報など)はUTF-8になっているが、テキストファイルはDB内には格納されておらず、Shift_JISのテキストファイルとして保管されている。

変更しないこと

  • 青空文庫で配布するテキストファイルの文字コードには適用しない(Shift_JIS / JIS X 0208のままとする)。
    • とはいえXHTML版として配布されているファイルはUTF-8に変更してもよいかも。
  • 現在の包摂規準は変更しない。外字注記辞書の包摂やデザイン差はそのまま採用する。
  • 外字注記記法について、JIS X 0213の面区点番号やUnicodeのコードポイントが指定された注記についても、まずは変換しないで注記のままとする。
    • そのうち変換オプションとして、注記をUnicode文字に変換するルールを別途定めてもよいと思われる。
    • また、後述する通りUnicodeからShift_JISへの変換では複数のコードポイントを同一の文字に変換しているが、これを禁止してエラーとするか、外字注記記法に変換するオプションもあってもよいと思われる。

前提としての予備知識

  • 青空文庫では外字注記記法があり、直接UCSのコードポイントを指定して文字を記述することができる。
  • その上で、現状の青空文庫は包摂規準としてJIS X 0208の包摂規準を採用しており、外字注記記法でUCSを指定する際にもこの包摂規準により包摂される文字は記述しないことになっている。

課題と方針

Shift_JISとUTF-8との変換表は様々なバリエーションがあるので、どの文字をどう変換するかを決めなければならない。

JIS X 0213でもShift_JISとUnicodeとの変換表が規定されているが、この変換表は0x5Cを円記号(U+00A5)に、0x815FをU+005Cに対応させている。これは、JIS X 0213でのShift_JISがJIS X 0201とJIS X 0208からなるもので、円記号はJIS X 0201のものが使われ、バックスラッシュ(REVERSE SOLIDUS)はJIS X 0208にしかないためである。しかし、例えば青空文庫でのくの字点「/\」をUTF-8で入力する際、1文字目が/(U+002F)ではなく(U+FF0F)を使うのであれば、2文字目も\(U+005C)ではなく(U+FF3C)を使いたくなる。これはJISの変換表とは異なるため、JISの変換表をそのまま利用することはできず、独自の変換を考える必要がある。

が、円記号とバックスラッシュ以外は、JISの変換表を使って問題ないのではないかと思われる。

まとめると、方針は以下のようになる。

  • Shift_JISの1バイト文字はUTF-8でもShift_JISでも違うデータに変換されず、同じバイト列になるようにする。
  • Shift_JISの2バイト文字(JIS X 0208の文字)は、原則としてJIS X 0213で規定されているUnicodeとの対応表の通りの変換を行う。ただし、円記号とバックスラッシュは例外扱いとし、互換領域(Halfwidth and Fullwidth Forms)への対応とする。

なお、将来的に青空文庫のテキストファイルがJIS X 0213の包摂規準に対応する場合は、Unicodeへの変換表についても再検討する。

Shift_JIS→UCSへの変換

1バイト文字

Shift_JIS中、1バイトで表現される文字(ASCII内の文字)については、Shift_JIS<->UTF-8間でバイト単位で保存される。

変換に揺れがある文字については以下に従う。

文字 SJIS 変換結果 参考:JIS X 0213 参考:CP932
\ 0x5C U+005C (U+00A5) U+005C
~ 0x7E U+007E (U+203E) U+007E

2バイト文字

Shift_JIS中、1バイトで表現される文字(JIS X 0208の文字)については、 JIS X 0213で規定されているマッピングに従う。

ただし、(0x818F)についてはU+00A5ではなくU+FFE5に、(0x815F)はU+005CではなくU+FF3Cに、それぞれ変換する。

変換に揺れがある文字については以下に従う。

文字 SJIS 変換結果 参考:JIS X 0213 参考:CP932
0x815C U+2014 U+2014 (U+2015)
0x815F U+FF3C (U+005C) U+FF3C
0x8160 U+301C U+301C (U+FF5E)
0x8161 U+2016 U+2016 (U+2225)
0x817C U+2212 U+2212 (U+FF0D)
0x818F U+FFE5 (U+FFE5 / U+00A5) U+FFE5
0x8191 U+00A2 U+00A2 (U+FFE0)
0x8192 U+00A3 U+00A3 (U+FFE1)
0x81CA U+00AC U+00AC (U+FFE2)

UCS→Shift_JISへの変換

1バイト文字

前述の通り、バイト単位で保存される。

(Shift_JISでの)2バイト文字

UCS→Shift_JISへの変換の場合、前述のShift_JIS→UCSの変換では使われない文字についても、Shift_JISに変換する場合には採用する。 つまり、UCSでは異なるコードポイントの文字が、Shift_JISでは同一の文字になる場合がある。

変換に揺れがある文字については以下に従う。

文字 Unicode 変換結果
U+2014 0x815C
U+2015 0x815C
U+FF3C 0x815F
U+301C 0x8160
U+FF5E 0x8160
U+2016 0x8161
U+2225 0x8161
U+2212 0x817C
U+FF0D 0x817C
U+FFE5 0x818F
¥ U+00A5 0x818F
¢ U+00A2 0x8191
U+FFE0 0x8191
£ U+00A3 0x8192
U+FFE1 0x8192
¬ U+00AC 0x81CA
U+FFE2 0x81CA

上記の表から明らかな通り、複数の文字が同一の文字に変換されることがある。言い換えれば、Unicode→Shift_JIS→Unicodeのround trip conversionは保証されない。これは意図的なものである。

本案はあくまでShift_JISがマスターファイルの記述に使う基本エンコーディングとして考えている。そのため、Shift_JIS→Unicode→Shift_JISのround tripが成功することを期待しており、この変換ではこのround tripは実現されている。Unicodeはあくまで補助的・一時的なフォーマット用のエンコーディングであり、マスターとしての同一性が保持されていればよい。それよりも、Unicodeでの入力・加工時の利便性(例えばU+2014とU+2015のどちらでも入力できるようにする)を優先し、基本エンコーディングであるShift_JIS上にした際に揺れを吸収できていることが重要である。

アルゴリズム

表を使ってもよいが、以下のようにして一般的なCP932マッピングを部分的に使用することもできる。

Shift_JIS→UCS

  1. 範囲表を使って、Shift_JIS(!= Shift_JIS-2004)に含まれない文字を排除する
  2. CP932マッピングで変換する
  3. 以下の変換を行う
    • U+2015→U+2014
    • U+FF5E→U+301C
    • U+2225→U+2016
    • U+FF0D→U+2212
    • U+FFE0→U+00A2
    • U+FFE1→U+00A3
    • U+FFE2→U+00AC

※Shift_JIS -> EUC-JIS-2004 -> UTF-8で変換すれば簡単そう?(要検証→余計面倒くさそうなので止めておく)

UCS→Shift_JIS

  1. 範囲表を使って、期待されるShift_JISに対応するUCSになっていない文字を排除する
  2. 以下の変換を行う
    • U+2014→U+2015
    • U+301C→U+FF5E
    • U+2016→U+2225
    • U+203E→U+007E
    • U+2212→U+FF0D
    • U+00A2→U+FFE0
    • U+00A3→U+FFE1
    • U+00A5→U+FFE5
    • U+00AC→U+FFE2
  3. CP932マッピングで変換する

実装

Ruby

aozora-sjis2utf8.rb

#!/usr/bin/env ruby

if ARGV.size != 1
  abort "usage: aozora-sjis2utf8.rb <filename>"
end

filename = ARGV[0]

s = File.read(filename, encoding: 'cp932')
u = s.encode("utf-8")
u.gsub!("\u2015", "\u2014")
u.gsub!("\uff5e", "\u301c")
u.gsub!("\u2225", "\u2016")
u.gsub!("\uff0d", "\u2212")
u.gsub!("\uffe0", "\u00a2")
u.gsub!("\uffe1", "\u00a3")
u.gsub!("\uffe2", "\u00ac")
print u

aozora-utf82sjis.rb

#!/usr/bin/env ruby

if ARGV.size != 1
  abort "usage: aozora-utf82sjis.rb <filename>"
end

filename = ARGV[0]

u = File.read(filename, encoding: 'utf-8')
u.gsub!("\u2014"){ "\u2015" }
u.gsub!("\u301c"){ "\uff5e" }
u.gsub!("\u2016"){ "\u2225" }
u.gsub!("\u203e"){ "\u007e" }
u.gsub!("\u2212"){ "\uff0d" }
u.gsub!("\u00a2"){ "\uffe0" }
u.gsub!("\u00a3"){ "\uffe1" }
u.gsub!("\u00a5"){ "\uffe5" }
u.gsub!("\u00ac"){ "\uffe2" }
s = u.encode("cp932")
print s

参考ページ

謝辞

矢野啓介さんにコメントをいただきました。

#!/usr/bin/env ruby
def sjis?(x)
(0x01 <= x && x <= 0x1f) ||
(0x20 <= x && x <= 0x7E) ||
(0x8140 <= x && x <= 0x817E) ||
(0x8180 <= x && x <= 0x81AC) ||
(0x81B8 <= x && x <= 0x81BF) ||
(0x81C8 <= x && x <= 0x81CE) ||
(0x81DA <= x && x <= 0x81E8) ||
(0x81F0 <= x && x <= 0x81F7) ||
(0x81FC <= x && x <= 0x81FC) ||
(0x824F <= x && x <= 0x8258) ||
(0x8260 <= x && x <= 0x8279) ||
(0x8281 <= x && x <= 0x829A) ||
(0x829F <= x && x <= 0x82F1) ||
(0x8340 <= x && x <= 0x837E) ||
(0x8380 <= x && x <= 0x8396) ||
(0x839F <= x && x <= 0x83B6) ||
(0x83BF <= x && x <= 0x83D6) ||
(0x8440 <= x && x <= 0x8460) ||
(0x8470 <= x && x <= 0x847E) ||
(0x8480 <= x && x <= 0x8491) ||
(0x849F <= x && x <= 0x84BE) ||
(0x889F <= x && x <= 0x88FC) ||
(0x8940 <= x && x <= 0x897E) ||
(0x8980 <= x && x <= 0x89FC) ||
(0x8A40 <= x && x <= 0x8A7E) ||
(0x8A80 <= x && x <= 0x8AFC) ||
(0x8B40 <= x && x <= 0x8B7E) ||
(0x8B80 <= x && x <= 0x8BFC) ||
(0x8C40 <= x && x <= 0x8C7E) ||
(0x8C80 <= x && x <= 0x8CFC) ||
(0x8D40 <= x && x <= 0x8D7E) ||
(0x8D80 <= x && x <= 0x8DFC) ||
(0x8E40 <= x && x <= 0x8E7E) ||
(0x8E80 <= x && x <= 0x8EFC) ||
(0x8F40 <= x && x <= 0x8F7E) ||
(0x8F80 <= x && x <= 0x8FFC) ||
(0x9040 <= x && x <= 0x907E) ||
(0x9080 <= x && x <= 0x90FC) ||
(0x9140 <= x && x <= 0x917E) ||
(0x9180 <= x && x <= 0x91FC) ||
(0x9240 <= x && x <= 0x927E) ||
(0x9280 <= x && x <= 0x92FC) ||
(0x9340 <= x && x <= 0x937E) ||
(0x9380 <= x && x <= 0x93FC) ||
(0x9440 <= x && x <= 0x947E) ||
(0x9480 <= x && x <= 0x94FC) ||
(0x9540 <= x && x <= 0x957E) ||
(0x9580 <= x && x <= 0x95FC) ||
(0x9640 <= x && x <= 0x967E) ||
(0x9680 <= x && x <= 0x96FC) ||
(0x9740 <= x && x <= 0x977E) ||
(0x9780 <= x && x <= 0x97FC) ||
(0x9840 <= x && x <= 0x9872) ||
(0x989F <= x && x <= 0x98FC) ||
(0x9940 <= x && x <= 0x997E) ||
(0x9980 <= x && x <= 0x99FC) ||
(0x9A40 <= x && x <= 0x9A7E) ||
(0x9A80 <= x && x <= 0x9AFC) ||
(0x9B40 <= x && x <= 0x9B7E) ||
(0x9B80 <= x && x <= 0x9BFC) ||
(0x9C40 <= x && x <= 0x9C7E) ||
(0x9C80 <= x && x <= 0x9CFC) ||
(0x9D40 <= x && x <= 0x9D7E) ||
(0x9D80 <= x && x <= 0x9DFC) ||
(0x9E40 <= x && x <= 0x9E7E) ||
(0x9E80 <= x && x <= 0x9EFC) ||
(0x9F40 <= x && x <= 0x9F7E) ||
(0x9F80 <= x && x <= 0x9FFC) ||
(0xE040 <= x && x <= 0xE07E) ||
(0xE080 <= x && x <= 0xE0FC) ||
(0xE140 <= x && x <= 0xE17E) ||
(0xE180 <= x && x <= 0xE1FC) ||
(0xE240 <= x && x <= 0xE27E) ||
(0xE280 <= x && x <= 0xE2FC) ||
(0xE340 <= x && x <= 0xE37E) ||
(0xE380 <= x && x <= 0xE3FC) ||
(0xE440 <= x && x <= 0xE47E) ||
(0xE480 <= x && x <= 0xE4FC) ||
(0xE540 <= x && x <= 0xE57E) ||
(0xE580 <= x && x <= 0xE5FC) ||
(0xE640 <= x && x <= 0xE67E) ||
(0xE680 <= x && x <= 0xE6FC) ||
(0xE740 <= x && x <= 0xE77E) ||
(0xE780 <= x && x <= 0xE7FC) ||
(0xE840 <= x && x <= 0xE87E) ||
(0xE880 <= x && x <= 0xE8FC) ||
(0xE940 <= x && x <= 0xE97E) ||
(0xE980 <= x && x <= 0xE9FC) ||
(0xEA40 <= x && x <= 0xEA7E) ||
(0xEA80 <= x && x <= 0xEAA4)
end
if ARGV.size != 1
abort "usage: aozora-sjis2utf8.rb <filename>"
end
filename = ARGV[0]
s = File.read(filename, encoding: 'cp932')
s.each_char do |ch|
if !sjis?(ch.ord)
raise "invalid char '#{ch}(0x#{ch.ord.to_s(16)})'"
end
end
u = s.encode("utf-8")
u.gsub!("\u2015", "\u2014")
u.gsub!("\uff5e", "\u301c")
u.gsub!("\u2225", "\u2016")
u.gsub!("\uff0d", "\u2212")
u.gsub!("\uffe0", "\u00a2")
u.gsub!("\uffe1", "\u00a3")
u.gsub!("\uffe2", "\u00ac")
print u
#!/usr/bin/env ruby
if ARGV.size != 1
abort "usage: aozora-utf82sjis.rb <filename>"
end
filename = ARGV[0]
u = File.read(filename, encoding: 'utf-8')
u.gsub!("\u2014"){ "\u2015" }
u.gsub!("\u301c"){ "\uff5e" }
u.gsub!("\u2016"){ "\u2225" }
u.gsub!("\u203e"){ "\u007e" }
u.gsub!("\u2212"){ "\uff0d" }
u.gsub!("\u00a2"){ "\uffe0" }
u.gsub!("\u00a3"){ "\uffe1" }
u.gsub!("\u00a5"){ "\uffe5" }
u.gsub!("\u00ac"){ "\uffe2" }
s = u.encode("cp932")
print s
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment