2011年10月15日

怪しい文字エンコード手動選択方法の作り方

ようやく暖めて腐らせておいたネタを公開する準備ができました、多分。
もう3週間くらい前に書いた文章ですが、とりあえずすべてに目をつぶって書き込みます。(笑)

どうしてこう姑息な手段しか思いつかないのか、と言われそうですが、紆余曲折の末まあだいたいこれで行けそうな気がしてきたので、書いてみます。(笑)
あくまでも自分の都合上無理矢理作ったものなので、誰も真似しないと思いますが、良い子(※)は特に真似しないように、ね!

(※)このブログのこれまでの経緯やプロフィールを読まず、僕が単なる「プラグラマー」(プラモデル+プログラマーの造語。プラモデル感覚でしかコードを組み立てられない人の意)だと知らずに参考にしようとする方

「プラグラマー」より「コピー&ペースター」の方がいいですかね?でも、物理的にはほぼ手打ちをしていますし。(笑)
それはさておき、先日から正規表現に悩んでいると書いていたのは今回のこの記事のことだったのですが、結局L----NoteのV1.5に組み込むテキストファイル読み込み機能における文字エンコードの判別は、当初の方針通りユーザーの手動選択とすることにしました。
ついては「先頭数行をプレビューしただけでは冒頭に英文が続いていた場合判別不可能」という欠点をなんとか埋めることができないかと考えた挙げ句、正規表現で日本語を拾い出してその文字が化けているか否かをユーザーに確認してもらおうと思いました。

(どうしてこんな回りくどい方法に固執するのかは以前のエントリーをご参照ください)

原理として考えたものは
・一群のテキスト中文字化けを起こすのはASCIIコード(アルファベットや基本的な記号、制御コード)以外である
・正しいエンコードを行えば、文字化けしない日本語を見つけることができる。つまりそれが正しいエンコードタイプである。
・すべてが英文であるならば、どの文字コードを使用しても結果は同じである(少なくともUnicode/EUC-JP/Shift-JIS間では)
というものです。

最後のはかなり無理がありますね。
読み込んだテキスト及び端末上で見える結果は同じようでも、そのファイルを元のPCなどに戻した時、オリジナルの文字コードとは変わってしまう可能性があります。
というのも、L----Note V1.5には読み込むだけでなく別途元ファイルを直接編集する機能もつけてしまったので、オリジナルがShift-JISであるのを、見え方が変わらないからということでUTF-8(最初にプレビューされる)を選択してしまうと、書き込み時にエンコード種別が変わってしまうことになります。
この辺りをどうするかまだ煮詰めていないのですが、煩雑になってもユーザーに書き込み時エンコードを指定してもらうしかないかなあ、と思います。
他にこの方法では、たまたま文字化けしない単語を正しくないエンコードで引っ張ってきてしまうということもあり得そうな気がしますし、Unicodeでは日本の漢字も中国の漢字も一緒のブロックになっているとのことで、その辺りで問題が起きそうな気もします。(どのような問題かは想定できないですけど)
僕にとっては未知の領域(ほとんどがそうですが)なので、悩ましいところです。

さて、前置きばかり長くて、どのようなものかイメージできないと思いますので、基本的な動作の流れを書きますと、大まかには文字コードを指定して選び出した日本語文字列をダイアログに表示し、文字化けしていたら次のエンコードタイプで再度ダイアログを呼び出すという動作を繰り返し、文字化けしないエンコードを探すというものなのですが、もう少し細かく書くと下記となります。

1.読み込むファイルを選択し、バイト配列を取得(下記コードでは省略)
2.バイト配列をencCheck()というメソッドに渡し、ここで特定のエンコードタイプ決め打ちでダイアログ表示メソッドを呼ぶ
3.ダイアログ表示メソッドshowConfirmDialog()では指定されたエンコードタイプでバイト配列を文字列に変換
4.各エンコードタイプに合わせた正規表現のパターン2種(A日本語を見つけるパターン/B英文字記号スペースを見つけるパターン)を設定
5.3で変換した文字列からプレビュー用文字列を切り出し(先頭100文字)
6.5とは別に(長い文章に正規表現を使うと遅そうなので)行で分割した文字列を1行ずつ4で用意したパターンAの正規表現で検索
7.マッチした行を文字列変数に格納し、forループをbreak
8.7の文字列からパターンBにマッチする文字列、つまり英文字記号スペースを区切り文字(,)に置換
9.8の配列から意味のある日本語(として受け止められることを期待する)2文字以上の連続した文字列を選び出す
10.プレビュー用文字列と9で用意した確認文字列をダイアログに表示し、ユーザーに確認してもらう
11.文字化けしていたら「再読込」ボタンをユーザーが押してencCheck()を呼び、そこで次のエンコードタイプを指定して再度shoConfirmDialog()を呼ぶ
12.以上を繰り返し(UTF-8→Shift-JIS→EUCとトグル)、文字化けがないところでOKボタンによりファイル読み込みメソッドへと進む

ものすごく回りくどいですね。(汗)
でも、どちらにせよ一時しのぎの予定なので、想定外の問題がぞろぞろ出てきそうですが、一応自分でテストした範囲では期待した通りに動いている感じなので、これで行けそうです。
というか、もうこれで行くんですけどね。(笑)

例によって誰の参考にもならないと思いますが、以下にコードを記載してみます。
酔狂な方がいたら、実際に試せるはずです。(実際のコードから余計な部分は除いてあるので、余計な部分を削除していたらご容赦ください)

最初の呼び出しはencCheck()の第4引数を-1とかで呼び出します。

public void encCheck(byte[] buf,String fPath,int enc){	// 引数:バイト配列/ファイルパス/エンコード種別
/* Prgrssからbyte配列ファイルを受け取り、文字化けの確認後問題なければDBに登録するメソッド群 */
/* showConfirmDialogにデータを渡し、各エンコードタイプで文字化けならshowConfirmDialog()で再 */
/* 読込され、再びこのメソッドが文字化け時のエンコードタイプを第4引数に入れて呼び出される。 */
/* 最初にこのメソッドを呼び出す時は第4引数に-1を設定し、switchのdefaultに合致させる */
switch(enc){
case UTF8:
showConfirmDialog(buf,fPath,SJIS);
break;
case SJIS:
showConfirmDialog(buf,fPath,EUC);
break;
case EUC:
showConfirmDialog(buf,fPath,UTF8);
break;
default:
showConfirmDialog(buf,fPath,UTF8);
break;
}
}

public void showConfirmDialog(byte[] buf,final String fPath,int enc){
/* encCheck()と連携するメソッド。ダイアログで文字化けの目視確認をしてもらい、文字化けがあれば */
/* encCheck()を再度呼び出し、エンコードタイプをトグルして再確認する */
// boolean confirmOK = false;
String mesPlus = ""; // ダイアログのメッセージに付加する現在のエンコードタイプをユーザーに知らせるための文字列
String str = ""; // 本文格納用文字列
Pattern p = null; // 日本語にヒットさせるためのパターン
Pattern p2 = null; // 英文字記号スペースにヒットさせるためのパターン
try {
switch(enc){
case UTF8:
str = new String(buf,S_UTF8); // エンコードタイプを指定してStringに変換
p = Pattern.compile("[\\p{InHiragana}\\p{InKatakana}\\p{InCJKUnifiedIdeographs}]+");
p2 = Pattern.compile("[\\p{InBasicLatin}\\p{Punct}\\p{Space}\\*]");
mesPlus = "(UTF-8)";
break;
case SJIS:
str = new String(buf,S_SJIS);
p = Pattern.compile("[ぁ-んァ-ヶ亜-滌漾-熙\-K]+");
p2 = Pattern.compile("[ -~0-Z\\s\\t\\r\\*]+");
mesPlus = "(Shift-JIS)";
break;
case EUC:
str = new String(buf,S_EUC);
p = Pattern.compile("[ぁ-んァ-ヶ亜-熙\-K]+");
p2 = Pattern.compile("[ -~0-Z\\s\\t\\r\\*]+");
mesPlus = "(EUC_JP)";
break;
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}

final String finalStr = str; // ここより3行ダイアログ用final変数
final byte[] finalBuf = buf;
final int finalEnc = enc;
String prevStr = str; // プレビュー用文字列
if(prevStr.length() > 100){
prevStr = prevStr.substring(0, 99);
}

String chkStr = str;
// 文字化け確認用文字列生成
String[] tmpChkStr = str.trim().replaceAll("\r\n","\n").split("\n"); // Shift-JIS用に念のため変換。不要?
for(int ck = 0;ck < tmpChkStr.length;ck++){
Matcher matcher = p.matcher(tmpChkStr[ck]);
if(matcher.find()){ // マッチする文字列が見つかったらchkStrに保管してbreak
chkStr = tmpChkStr[ck];
break;
}else{
chkStr = "";
}
}
Matcher matcher2 = p2.matcher(chkStr);
chkStr = matcher2.replaceAll(","); // 意味のある言葉で区切るため、英文字記号を分割区切りとして置換
String chkStr3 = "";
String[] chkStr2 = chkStr.split(","); // 1文字以上の日本語を選び出すため分割
for(int ck2 = 0;ck2 < chkStr2.length;ck2++){
if(chkStr2[ck2].length() > 1){
chkStr3 = chkStr2[ck2];
break;
}
}
if(chkStr3.length() > 10){
chkStr3 = chkStr3.substring(0, 10);
}
if(chkStr3 == null || chkStr3.equals("")) chkStr3 = "×確認可能文字列なし"; // 日本語が見つからなかった時表示

LayoutInflater factory = LayoutInflater.from(this);
final View confirmView = factory.inflate(R.layout.confirm_copy, null); // プレビュー用カスタムダイアログ

TextView prev = (TextView)confirmView.findViewById(R.id.preview); // プレビュー用先頭文字列表示用
prev.setText(prevStr);

AlertDialog.Builder adb = new AlertDialog.Builder(this);
adb
.setTitle("読込確認" + mesPlus)
.setMessage("[ ]内抜粋文字に文字化けがあれば再読込ボタンを押してください" + "\n" + "[" + chkStr3 + "]")
.setView(confirmView)
.setPositiveButton("OK",new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// 文字化けがなければOKが押されるのでファイル読み込みメソッドを呼び出す
fileRead(finalBuf,fPath,finalEnc);
dialog.dismiss();
}
})
.setNeutralButton("再読込",new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// 文字化けの場合encCheckを再度呼び出す
encCheck(finalBuf,fPath,finalEnc);
dialog.dismiss();
}
})
.setNegativeButton("Cancel", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.show();
}




文字コードについては

http://charset.7jp.net/

様を参考とさせていただきました。
正規表現の検索パターンの範囲の選択で判断に迷う部分もあったのですが、問題になりそうなのはあまり使われない漢字だと思うので、大まかには大丈夫なのではないかな?と思います。


なお、今回WordPressとかでないとできないと思っていたのですが、何気なく検索してみたらさくらのブログでもSyntaxHighlighterが使えることが分かったので導入してみました。
自サイトに取り込む方法がどうもうまくいかなかったのでホスティングによる導入ですが、これで閲覧される方に負担をかけなくて済みそうです。良かった。

posted by 白虹 at 00:18| Comment(4) | TrackBack(0) | Android開発
この記事へのコメント
gucci リング
Posted by ルブタン 通販 at 2013年07月19日 17:42
初めまして。
面白そうなコードがありましたので、自分ならどう書くかなという感じでやってみました。
もうプログラミングされていないかも知れませんが、足跡ということで。


/**
* Prgrssからbyte配列ファイルを受け取り、文字化けの確認後問題なければDBに登録するメソッド群
* showConfirmDialogにデータを渡し、各エンコードタイプで文字化けならshowConfirmDialog()で再
* 読込され、再びこのメソッドが文字化け時のエンコードタイプを第4引数に入れて呼び出される。
* 最初にこのメソッドを呼び出す時は第4引数に-1を設定し、switchのdefaultに合致させる
* @param buf バイト配列
* @param fPath ファイルパス
* @param enc エンコード種別
*/
@Deprecated //非推奨または不要. → showConfirmDialog()
public void encCheck(byte[] buf,String fPath,int enc){
showConfirmDialog(buf,fPath/*,enc*/);
}

/**
* 文字コード関連
*/
enum MOJI_CODE {
UTF8("(UTF-8)", Charset.forName("utf8"),
Pattern.compile("[\\p{InHiragana}\\p{InKatakana}\\p{InCJKUnifiedIdeographs}]+"),
Pattern.compile("[\\p{InBasicLatin}\\p{Punct}\\p{Space}\\*]")),

SJIS("(Shift-JIS)", Charset.forName("shift-jis"),
Pattern.compile("[ぁ-んァ-ヶ亜-滌漾-熙\-K]+"),
Pattern.compile("[ -~0-Z\\s\\t\\r\\*]+")),

EUC("(EUC_JP)", Charset.forName("euc-jp"),
Pattern.compile("[ぁ-んァ-ヶ亜-熙\-K]+"),
Pattern.compile("[ -~0-Z\\s\\t\\r\\*]+")),
;

String mesPlus; // ダイアログのメッセージに付加する現在のエンコードタイプをユーザーに知らせるための文字列
Charset charset; // エンコードタイプ
Pattern p; // 日本語にヒットさせるためのパターン
Pattern p2; // 英文字記号スペースにヒットさせるためのパターン
MOJI_CODE(String mesPlus, Charset charset, Pattern p, Pattern p2) {
this.mesPlus = mesPlus;
this.charset = charset;
this.p = p;
this.p2 = p2;
}

//文字コードを巡回するためのメソッド.
static MOJI_CODE getFirst() { return values()[0]; }
/** 自分の次のencを返す. 最期の場合は先頭を返す. */
MOJI_CODE getNext() { int next = ordinal()+1; if(next >= values().length) next = 0; return values()[next]; }
}
Posted by 通りすがり at 2019年01月17日 17:29
/**
* <del>encCheck()と連携するメソッド。</del>ダイアログで文字化けの目視確認をしてもらい、
* 文字化けがあれば<del>encCheck()を再度呼び出し</del>、エンコードタイプをトグルして再確認する
* boolean confirmOK = false;
*/
public void showConfirmDialog(byte[] buf,String fPath/*,int enc*/){
new ConfirmEncodeDialog(this, buf, fPath).show();
}

private class ConfirmEncodeDialog extends AlertDialog {
private MOJI_CODE enc = MOJI_CODE.getFirst();

/**
* @param context コンテキスト
* @param buf バイト配列
* @param fPath ファイルパス
*/
ConfirmEncodeDialog(Context context, byte[] buf, String fPath) {
super(context);

LayoutInflater factory = LayoutInflater.from(context);
View confirmView = factory.inflate(R.layout.confirm_copy, null); // プレビュー用カスタムダイアログ
setView(confirmView);

TextView prev = (TextView)confirmView.findViewById(R.id.preview); // プレビュー用先頭文字列表示用

Button positiveButton = getButton(DialogInterface.BUTTON_POSITIVE);
positiveButton.setText("OK");
positiveButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 文字化けがなければOKが押されるのでファイル読み込みメソッドを呼び出す
fileRead(buf, fPath, enc);
dismiss();
}
});

Button neutralButton = getButton(DialogInterface.BUTTON_NEUTRAL);
neutralButton.setText("再読込");
neutralButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
enc = enc.getNext();
refresh(buf, prev);
}
});

Button negativeButton = getButton(DialogInterface.BUTTON_NEGATIVE);
negativeButton.setText("Cancel");
negativeButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
dismiss();
}
});

refresh(buf, prev);
}

private void refresh(byte[] buf, TextView prev) {
String str = new String(buf, enc.charset); //encでbufを変換

String prevStr = str; // プレビュー用文字列
if(prevStr.length() > 100){
prevStr = prevStr.substring(0, 99);
}
prev.setText(prevStr);

String chkStr3 = getSample(str);

setTitle("読込確認" + enc.mesPlus);
setMessage("[ ]内抜粋文字に文字化けがあれば再読込ボタンを押してください" + "\n" + "[" + chkStr3 + "]");
}

//この中身は元からコピーほぼそのまま('p'→'enc.p', 'p2'→'enc.p2'のみ)
/**
* 文字化け確認用文字列生成
*/
private String getSample(String str) {

String chkStr = str;
// 文字化け確認用文字列生成
String[] tmpChkStr = str.trim().replaceAll("\r\n","\n").split("\n"); // Shift-JIS用に念のため変換。不要?
for(int ck = 0;ck < tmpChkStr.length;ck++){
Matcher matcher = enc.p.matcher(tmpChkStr[ck]);
if(matcher.find()){ // マッチする文字列が見つかったらchkStrに保管してbreak
chkStr = tmpChkStr[ck];
break;
}else{
chkStr = "";
}
}
Matcher matcher2 = enc.p2.matcher(chkStr);
chkStr = matcher2.replaceAll(","); // 意味のある言葉で区切るため、英文字記号を分割区切りとして置換
String chkStr3 = "";
String[] chkStr2 = chkStr.split(","); // 1文字以上の日本語を選び出すため分割
for(int ck2 = 0;ck2 < chkStr2.length;ck2++){
if(chkStr2[ck2].length() > 1){
chkStr3 = chkStr2[ck2];
break;
}
}
if(chkStr3.length() > 10){
chkStr3 = chkStr3.substring(0, 10);
}
if(chkStr3 == null || chkStr3.equals("")) chkStr3 = "×確認可能文字列なし"; // 日本語が見つからなかった時表示

return chkStr3;
}
}

private void fileRead(byte[] buf , String path, MOJI_CODE enc) {
//省略
}
Posted by 通りすがり at 2019年01月17日 17:30
コンパイルエラーは(R.id.*以外は)ありませんが、実行していませんので、動くかは分かりません (_ _;
Posted by 通りすがり at 2019年01月17日 17:33
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

この記事へのトラックバックURL
http://blog.sakura.ne.jp/tb/48788905

この記事へのトラックバック