inuinu blog(開発用)

BOT @wagagun の開発ノウハウや、IT向け?の雑記ブログです。

【JRuby】JRubyで任意の文字コードに変換し変換不能文字列を抽出する

この記事では、JRubyの利点である、Ruby関数とJava関数の組み合わせで、AS400(CCSID5026、5035)の文字列変換を行います。

JRubyでは、Ruby関数とJava関数を組み合わせることで、AS400(CCSID5026、5035)の文字列変換が容易に行なえます。

さて、今回は、JRubyを使用した文字コード変換です。

弊社には主業務の基幹システムがあるのですが、文字コードが第二水準+α程度のサポートで、半角英数字もサポートしていません。
このシステムと、別途開発しているWebシステムを接続したいのですが、文字コード変換をWeb側に実装する手段がわかりません。
何かございますでしょうか?

はい。あると思います。
早速、実験してみましょう。

この記事では、下記記事の続編になります。

inuinu-tech.hatenablog.jp

なぜJRubyなのか?

なぜJRubyなのか?

まず、私がネットでの調査した内容を、箇条書きにしていきます。

  • Javaは、文字列から1文字抜粋といった処理がサロゲートペア・絵文字に対応しているとは言えなさそう
  • Rubyではサロゲートペアでも絵文字でも1文字は1文字とカウントされ、1文字単位の抜粋も容易
  • Rubyでの文字コード変換は、encodeメソッドだが、取り扱える(マイナー)文字コードに限界がありそう
  • 文字コード変換にはKconvもあるが、いまいち情報に乏しい(気がする)
  • 調べると、Javaではマイナーな文字コードの変換をサポートしている模様
  • JRubyでは、Rubyの全機能に加え、Java関数・メソッドが利用できる

ここで言う「マイナーな文字コード」とは、CCSIDの5026や5035といったもの。

RubyRubyJavaの「いいところ取り」ができるので、こういった処理での出番がありそうですね。

JRubyも(Ruby同様)サロゲートペアは正しく扱われるか?

プログラミング例

Rubyではサロゲートペアでも絵文字でも1文字は1文字とカウントされるが、JRubyでも同じ挙動なのか、念のために調べてみましょう。
(これが大前提になります。)

●len.rb

#require 'bundler/setup'

# UTF-8文字列
strUtf8 = "abcabc①你好𩸽🍣🍺"
puts "UTF-8文字数=>#{strUtf8.size}" 

# Shift_JIS化
# 半角英小文字は大文字に変換
strSjis = strUtf8.encode("Shift_JIS", invalid: :replace, undef: :replace).upcase(:ascii)
puts "Shift_JIS文字数=>#{strUtf8.size}" 
# UTF-8に戻す
strSjis2Utf8 = strSjis.encode("utf-8", invalid: :replace, undef: :replace)
puts "Shift_JIS文字数(utf8戻したもの)=>#{strSjis2Utf8.size}" 

# 配列化
arrUtf8 = strUtf8.split('')
arrSjis = strSjis2Utf8.split('')

# ファイルに結果を出力
file = File.open("sample.txt", "w")

arrUtf8.each_with_index do |st, idx|
  file.puts "UTF-8=>#{st}、Shift_JIS=>#{strSjis2Utf8[idx]}"
end

file.close

exit

ソースコードで「半角英小文字は大文字に変換」としているのは、最初の質問の通り、オフィス用のコンピュータの中には、半角の英小文字を扱えない機種があるからです。
使用するコンピュータが該当しない場合は、大文字化する必要はありません。

upcaseで半角英字は大文字になりますが、全角も大文字ならないようにするには、Ruby2.4.0以降ではupcase(:ascii)とする必要があるようです。

実行結果

実行結果は下記の通り。

> jruby len.rb

UTF-8文字数=>12
Shift_JIS文字数=>12
Shift_JIS文字数(utf8戻したもの)=>12

実行結果のsample.txtは…

UTF-8=>a、Shift_JIS=>A
UTF-8=>b、Shift_JIS=>B
UTF-8=>c、Shift_JIS=>C
UTF-8=>a、Shift_JIS=>a
UTF-8=>b、Shift_JIS=>b
UTF-8=>c、Shift_JIS=>c
UTF-8=>①、Shift_JIS=>?
UTF-8=>你、Shift_JIS=>?
UTF-8=>好、Shift_JIS=>好
UTF-8=>𩸽、Shift_JIS=>?
UTF-8=>🍣、Shift_JIS=>?
UTF-8=>🍺、Shift_JIS=>?

JavaベースのJRubyでも問題なく、サロゲートペアでも絵文字でも、1文字ずつ抜粋できますね

サロゲートペアについてまとめると、こうなります。

サロゲートペア操作 Java/JavaベースのWeb言語 JRuby
文字数カウント 2文字としてカウント
裏技でJava関数を組み合わせることにより可能
(codePointCount)
1文字でカウント
文字を抽出 不可能
(Javaのバージョンが古い?)
可能

JRuby、使えそうですね!

ただし、マイナーな文字コードの対応を行うため、次章ではRubyecode()ではなく、Javaの関数を使用します。

JRubyJavaのメソッドを使用し、文字コードを変換する

この章では、1970年代から存在する由緒あるオフィスコンピュータを想定した文字変換の例を扱います。
Uncodeはおろか、第二水準といった世界です。丸数字も使用できません。

Rubyのencodeメソッドでは扱えない文字コードがある?

オフィス用のコンピュータでは、半角英字の小文字が使えなかったり、丸数字がダメで、Unicodeなんてもってのほか…といった、かなりシビアな文字コードを要求されるものがあります。

有名どころではAS400のCCSID=5026あるいは5035でしょうか。
この2つの違いは半角英小文字が使用できない/使用できるの違いくらいで、S_JISに毛が生えたような文字コードなのですが、S_JISとは多少異なります。

この2つの文字コードで特に厄介なのは、機種依存文字の中で使用できる/使用できない文字が混在すること。
具体的には、「①」が使用できませんが、「㈱」は使用可能。
Windows-31Jでは両方とも扱え、Shift_JISでは両方とも扱えないので、この2つでは完璧な変換はできません。セーフティなのはShift_JISですが、「5026/5035で扱える文字は可能な限り変換したい」というニーズがあると思いますので、そのニーズに可能な限り応えられるプログラムを作成していきます。

JRubyならJavaのgetBytes()で変換できそう

Ruby文字コード変換encode()では5026/5035相当の文字コードは扱っていないようです。
しかしJavajava.lang.String.newgetBytes()ではどうやら扱っている模様。

Olacleにある、「サポートされているエンコーディング 」の表中の「Cp930」がCCSID=5026相当で、「Cp939」がCCSID=5035相当のようですね。

JRubyでのコーディング例

CCSID=5026の場合のコーディング例。

●test_5026.rb

#require 'bundler/setup'
require 'java'

# UTF-8文字列
javaStrUtf8 = java.lang.String.new("abcabc①㈱你好𩸽🍣🍺".upcase(:ascii))

# 5026に変換
javaStr5026 = java.lang.String.new(javaStrUtf8.getBytes("Cp930"), "Cp930")
javaStrNewUtf8 = java.lang.String.new(javaStr5026.getBytes("UTF-8"), "UTF-8")

# 配列化
arr_Utf8 = javaStrUtf8.to_s.split('')
arr_5026 = javaStrNewUtf8.to_s.split('')

# ファイルに結果を出力
file = File.open("sample5026.txt", "w")

file.puts "UTF-8=>#{javaStrUtf8}、Cp930(5026)=>#{javaStrNewUtf8}"

file.puts "-"*20
file.puts "●CCSID=5026との比較"
arr_Utf8.each_with_index do |st, idx|
  file.puts "UTF-8=>#{st}、Cp930(5026)=>#{arr_5026[idx]}"
end

file.close

CCSID=5035の場合はこちら。

●test_5035.rb

#require 'bundler/setup'
require 'java'

# UTF-8文字列
javaStrUtf8 = java.lang.String.new("abcabc①㈱你好𩸽🍣🍺")

# 5035に変換
javaStr5035 = java.lang.String.new(javaStrUtf8.getBytes("Cp939"), "Cp939")
javaStrNewUtf8 = java.lang.String.new(javaStr5035.getBytes("UTF-8"), "UTF-8")

# 配列化
arr_Utf8 = javaStrUtf8.to_s.split('')
arr_5035 = javaStrNewUtf8.to_s.split('')

# ファイルに結果を出力
file = File.open("sample5035.txt", "w")

file.puts "UTF-8=>#{javaStrUtf8}、Cp939(5035)=>#{javaStrNewUtf8}"

file.puts "-"*20
file.puts "●CCSID=5035との比較"
arr_Utf8.each_with_index do |st, idx|
  file.puts "UTF-8=>#{st}、Cp939(5035)=>#{arr_5035[idx]}"
end

file.close

Java変数やメソッドを使用しますので、require 'java'を忘れずに。

5026/5035の違いは主に変数や、cp930cp939ですが、5026のみupcase(:ascii)が5行目に挿入されています。
5026の場合は半角英小文字は認められないのですが、Java文字コード変換ではcp930/cp939関わらず通ってしまうため、Rubyupcase(:ascii)で、5026のみ半角英小数字を大文字化しています。

java.lang.String.new()は見た目通りJavaのメソッドです。
これらとJavaメソッドgetBytes()を組み合わせて、文字列を変換・再定義していきます。

ソース前半の文字列の定義や変換はJavaで行っていますので、Rubyではそのままでは文字列として使用できません。
下記のようにto_sメソッドにて、Rubyで扱える文字列に変換します。

# 配列化
arr_Utf8 = javaStrUtf8.to_s.split('')
arr_5035 = javaStrNewUtf8.to_s.split('')

こういった言語間同士の変数型変換は、RubyPythonのリソースを実行できるPyCall でも見られますね。

プログラム例ではJava変数(上記ではjavaStrUtf8javaStrNewUtf8)と、Ruby変数(同じくarr_Utf8arr_5035)の見分けが付くようにしています。

Javaの変数はキャメルケース推奨ですが、Rubyはスネークケースが推奨されています。

実行結果

実行してみましょう。 5026はこちら。

jruby test_5026.rb

5035はこちら。

jruby test_5035.rb

実行結果(5026)。

UTF-8=>ABCabc①㈱你好𩸽🍣🍺、Cp930(5026)=>ABCabc?㈱?好???
--------------------
●CCSID=5026との比較
UTF-8=>A、Cp930(5026)=>A
UTF-8=>B、Cp930(5026)=>B
UTF-8=>C、Cp930(5026)=>C
UTF-8=>a、Cp930(5026)=>a
UTF-8=>b、Cp930(5026)=>b
UTF-8=>c、Cp930(5026)=>c
UTF-8=>①、Cp930(5026)=>?
UTF-8=>㈱、Cp930(5026)=>㈱
UTF-8=>你、Cp930(5026)=>?
UTF-8=>好、Cp930(5026)=>好
UTF-8=>𩸽、Cp930(5026)=>?
UTF-8=>🍣、Cp930(5026)=>?
UTF-8=>🍺、Cp930(5026)=>?

キーポイントは、5026は機種依存文字の内「①」は対象外、「㈱」は対象なのですが、期待どおり「①」は「?」となり、「㈱」は生き残りました! これは素晴らしい!

さらに、5035では…

UTF-8=>abcabc①㈱你好𩸽🍣🍺、Cp939(5035)=>abcabc?㈱?好???
--------------------
●CCSID=5035との比較
UTF-8=>a、Cp939(5035)=>a
UTF-8=>b、Cp939(5035)=>b
UTF-8=>c、Cp939(5035)=>c
UTF-8=>a、Cp939(5035)=>a
UTF-8=>b、Cp939(5035)=>b
UTF-8=>c、Cp939(5035)=>c
UTF-8=>①、Cp939(5035)=>?
UTF-8=>㈱、Cp939(5035)=>㈱
UTF-8=>你、Cp939(5035)=>?
UTF-8=>好、Cp939(5035)=>好
UTF-8=>𩸽、Cp939(5035)=>?
UTF-8=>🍣、Cp939(5035)=>?
UTF-8=>🍺、Cp939(5035)=>?

5026との違いは、5035は英小文字を許していることです。

5026/5035とも、変換できない文字は「?」となります。
上記プログラム例では、「?」となった場合の変換前の文字も拾うことができますので、仮にこれを入力フォーム画面の住所欄だとすると、

住所欄の内「①你𩸽🍣🍺」は入力できない文字です。

といったように、変換できなかった文字はこれとこれ!といったメッセージを表示することが可能です。

文字化けはチェックできても、具体的な変換不要文字を抜粋できない言語はあるので、Ruby(JRuby)のアドバンテージになります。

最後に

実験結果の感想ですが、まさにRubyJavaのいいところ取りだと思います。

今回のプログラム例では、単に変換のみを扱いましたが、以下に応用できそうです。

  • CSVデータを介して、バッチプログラムにてオフィスコンピュータにデータを渡す際のチェック処理として
  • フィスコンピュータがJDBCをサポートしている場合は、APIプログラムを作成しリアルタイムにデータを更新する際のチェック処理として

Webシステムからオフィスコンピュータへの、データの受け渡し部分に実装できそうですね。

それでは、また。