初歩のシェルスクリプトで遊ぶ[シフトJISをutf8に変換-1]

調べものには使えません。
Ubuntu18.04 (Virtualbox) dash,bash

Windowsのテキストファイルを読む

$ cat "Windowsのcp932テキストファイル.txt" | od -tx1 | tail
0036160 fb c6 fb c7 fb c8 fb c9 fb ca fb cb fb cc fb cd
0036200 fb ce fb cf fb d0 fb d1 fb d2 fb d3 fb d4 fb d5
0036220 fb d6 fb d7 fb d8 fb d9 fb da fb db fb dc fb dd
0036240 fb de fb df fb e0 fb e1 fb e2 fb e3 fb e4 fb e5
0036260 fb e6 fb e7 fb e8 fb e9 fb ea fb eb fb ec fb ed
0036300 fb ee fb ef fb f0 fb f1 fb f2 fb f3 fb f4 fb f5
0036320 fb f6 fb f7 fb f8 fb f9 fb fa fb fb fb fc fc 40
0036340 fc 41 fc 42 fc 43 fc 44 fc 45 fc 46 fc 47 fc 48
0036360 fc 49 fc 4a fc 4b

シェルスクリプトの製作途中ではiconvを使ってもいいが、実際のテキストファイルの変換作業はodコマンドの数字からやってみます。
あくまで不真面目に。

いきなり作ったもの

図書カード:ガリバー旅行記
動作速度のテストに使ったのは、青空文庫の『ガリバー旅行記』です。
スクリプト本体と、スクリプトが読む「文字の辞書」のテキストファイル、を作るスクリプトで出来てます。
WindowsのシフトJISの文字リストを作るシェルスクリプト - HatenaDiary id:Narr
辞書ファイルを作るのに、このスクリプトも使ってます。

ちょっと長いんで、このエントリの分は、この1セットを載っけるだけにしときます。
いや、別のバージョンとか、けっこうたくさんあってですね。そっちのほうが面白いんすわ。バカっぽくて。

スクリプト本体(とりあえず終わりにしといた版) sjisTextToUtf8Text02.sh

#!/bin/dash
#set -x
: << '#--------------------------------------------------------------comment'
■cp932テキストファイルを、utf8テキストに変換する。(odコマンドから変換する)
・処理の中心はodコマンド,printfコマンド,変換データの辞書。
・cp932テキストを入力すると、utf8テキストが直接に出力される。
・改行文字[0d0a][0a]、タブ[09]に対応。出力の改行は「0a」となる。
・「0d」改行には非対応
・辞書はテキストファイルで一つのみ

▼ バイナリを読むコマンドのオプション
【10進数】で出力する。
od --endian=big --address-radix=n --format u1 --output-duplicates --width=64 |
hexdump -v -e '16/1 " %03u"' -e '"\n"' |

▼辞書ファイル
別スクリプトで作成
\347\212\276,\347\214\244,\357\250\226,\347\215\267,\347\216\275,
u129_64="\343\200\200"; u129_65="\343\200\201"; u129_66="\343\200\202";
「\8進数」と「\8進数の変数代入式」の複合辞書


▼処理の流れ
(1) odコマンドで、cp932ファイルを『10進数』の数列に変換する。
(2) 数列を判定して、数列を1文字ごとに分離する。
(3) 数列に対応したutf8の数列を、辞書ファイルから読み出し、シェル変数に代入する。
	この変数の内容はバックスラッシュ付きの8進数。
(4) シェル変数をprintfで出力すると、8進数に対応したバイナリデータ、つまり
	文字そのものが出力される。

▼処理の特徴
 ○辞書をシェル変数に代入するが、使う文字のみ読みに行く

・変数の内容に8進数を使い、printfで文字バイナリを出力する
「printf "\345\240\205"」
等、バックスラッシュ付き8進数をprintfに渡すと、バイナリデータを出力できる。
辞書テキストには、この8進数を書いておく。
printfの出力には、余計な改行等が付かない。

辞書テキストファイルの中に文字そのものを書き込み、文字をシェル変数に入れる方式だと、
可読文字以外の改行やバックスラッシュ、辞書の区切り文字そのものの扱いに、注意が必要になってしまう。

 ○シェル変数名
1バイト文字:u256_${dec}
2バイト文字:u${doubleDec}_${dec}
10進数で変数名を作っている。1バイト文字上位は「0x100」の10進数、256とする。
odコマンドの出力そのままを使う。ゼロ埋めされていない10進数であることに注意。
(変数代入式の辞書も、ゼロ埋めされない変数名にする必要がある)

 ○10進数の理由
辞書を読む、sed,cutコマンドに、odコマンドの出力をそのまま渡すため。
カンマ区切りのcsvファイルの、上1バイトを行、下1バイトを列とした。
行はsedで指定し、列はcutで指定する。

 ○2種類の辞書を文字の使用頻度で切り替える

辞書ファイルは、2種類の形式を、行ごとに使い分けている。

・カンマ区切りの8進数「sed | cut」用
,,,,,,,\346\274\276,\346\274\223,\346\273\267,\346\276\206,\346\275\272,………
・変数代入式そのもの「eval sed」用
u256_9="\11";u25632="\40";u25633="\41";u25634="\42";u25635="\43";u25636="\44";………


「sed | cut」方式は、文字を1文字ずつカンマ区切りの辞書から読み込む。
1文字ずつコマンドを2つ起動してパイプするので、とても遅いが、無駄な読み込みは無い。

辞書に変数代入式を全て書き込む方式は、単純で高速。しかし、
7884文字分も一気に読み込むのは、やりすぎだ。

cp932は、上位1バイトが決定すると、非常に大まかではあるが、文字の使用頻度がわかる。
平仮名、カタカナは非常に高頻度であり、第一水準漢字は必要。
逆に、第二水準漢字以降は、使われない字も多い。
よく使う文字の近くには、よく使う文字が収録されている、と言い換えることができる。

よって、第一水準漢字までは、1文字必要になるごとに、
sedコマンドで辞書から1行分、上位1バイト分のみ読み込む。
この部分はカンマ区切りでない、変数代入式そのものになっている。
これをevalで評価すると、変数代入が実行される。
辞書は1行が上位1バイトごと、2行以上に分かれた行はない。よって、
1行読めば、必要な文字は必ず入っている。
直後に変数「${dicLine'${dec}'}」に、読み込み完了フラグを代入する。


▼使用例、出力データ形式
$ cat _iroha-sjis.txt |./sjisTextToUtf8Text02.sh 
0123
		tab
          sp
色は匂へど 散りぬるを
我 が 世 誰 ぞ 常 な ら む
有為の奥山  今日越えて
浅き夢見じ  酔ひもせず

▼速度 (kshだと非常に遅い)
$ time cat gulliver_ryokoki.txt | dash ./sjisTextToUtf8Text02.sh 1>/dev/null
real	0m5.015s
user	0m4.124s
sys	0m0.919s

$ time cat gulliver_ryokoki.txt | bash ./sjisTextToUtf8Text02.sh 1>/dev/null
real	0m21.997s
user	0m20.496s
sys	0m1.439s

$ time cat gulliver_ryokoki.txt | ksh ./sjisTextToUtf8Text02.sh 1>/dev/null
real	2m33.729s
user	1m47.851s
sys	0m36.238s

#--------------------------------------------------------------comment

sjisTextToUtf8Text02(){

# 8進数バックスラッシュの、printf用辞書(変数代入複合版)
dict="./dic/nukamisoCP932decToUTF8octBS_02.txt"


# 予め辞書から変数に代入する文字列:1バイト文字 ASCII、半角カタカナ
eval `0< "${dict}" sed -n 256p `

od --endian=big --address-radix=n --format u1 --output-duplicates --width=64 |
#hexdump -v -e '16/1 " %03u"' -e '"\n"' |

while read line ;do
for dec in ${line} ;do
if test -n "${doubleDec}" ;then # ------------------------2バイト文字を出力

	# 辞書を読み込んだら変数に代入しておく
	eval 'if test -z "${u'${doubleDec}_${dec}'}" ;then \
	u'${doubleDec}_${dec}'="`< ${dict} sed -n ${doubleDec}p | cut -d , -f ${dec}`" ;fi'
	eval 'printf "${u'${doubleDec}_${dec}'}"'

	doubleDec=""

elif test \(  32 -le ${dec} -a ${dec} -le 126 \) -o \
           \( 161 -le ${dec} -a ${dec} -le 223 \) -o \
            ${dec} -eq 9 -o ${dec} -eq 10 ; then # --------1バイト文字を出力
# 事前に代入した変数で出力
	eval 'printf "${u256_'${dec}'}"'


# 0x8140「 」〜0x98fc「傲」
elif test  129 -le ${dec} -a ${dec} -le 152 ;then 
doubleDec="${dec}" # ------頻出2バイト文字の上位バイトを変数に格納して次バイトと繋ぐ

# 読み込み済みの辞書行を点検
eval 'dicLineCheck=${dicLine'${dec}'}' # evalでcase文を括らないために
case "${dicLineCheck}"0 in
( @ ) true ;;
( 0 )
	eval `0< "${dict}" sed -n ${dec}p`
	eval 'dicLine'${dec}'=@'
;;
esac

# 「elif test 〜」しなくても、elseで動いてしまう
#elif test \( 153 -le ${dec} -a ${dec} -le 159 \) -o \
#           \( 224 -le ${dec} -a ${dec} -le 252 \) -o \
#            ${dec} -eq 13 ;then 
else
doubleDec="${dec}" # ----その他2バイト文字の上位バイトを変数に格納して次バイトと繋ぐ

fi ; done ; done
}

sjisTextToUtf8Text02

exit 0

上記スクリプト用、辞書ファイル作成スクリプト(追加で2つ)

文字コード作成スクリプトを動かして中間辞書を作るスクリプトcp932dicMaker1.sh
#!/bin/sh
#set -x
: << '#--------------------------------------------------------------comment'
■ 辞書作成中間ファイル作成スクリプト

▼ 必要なコマンド

XXD(1)                       General Commands Manual  XXD(1)---引用
名前
       xxd - 16 進ダンプを作成したり、元に戻したり。

ICONV(1)                        Linux User Manual   ICONV(1)---引用
NAME
       iconv - convert text from one character encoding to another

▼つかいかた
1)作業ディレクトリを作って、下記3個のスクリプトファイルを置く。
cp932CharMaker3.sh 
cp932CharMaker4.sh 
cp932dicMaker1.sh (このスクリプト)

2)実行
cp932dicMaker1.sh (このスクリプト)を実行する。

▼生成物 cp932hex-utf8hex.txt

 100 7c , 7c 
 100 7d , 7d 
 100 7e , 7e 
 100 a1 , ef bd a1 
 100 a2 , ef bd a2 
 100 a3 , ef bd a3 

cp932の16進数,utf8の16進数
(1バイト文字は0x100を追加)
7885行
149,021 bytes

#--------------------------------------------------------------comment


echo "cp932の1バイト文字部分を作る(158行)" 1>&2
./cp932CharMaker3.sh | tr A-F a-f | tr " " "\n" | sed "s/../100 &/;s/.*/ & /" 1> 1byteFile.temp

echo "cp932の2バイト文字部分を作る(7724行)" 1>&2
./cp932CharMaker4.sh | tr A-F a-f | fold -w 5 | tr -d " " | sed "s/../& /;s/.*/ & /" 1> 2byteFile.temp

echo "1バイト文字と2バイト文字のテキストファイルを繋ぐ(7882行)" 1>&2
cat 1byteFile.temp 2byteFile.temp 1> cp932Code.txt
echo "掃除" 1>&2
rm 1byteFile.temp 2byteFile.temp


echo "あらためて最初から材料を用意する。今度は一つのファイルに追記する。" 1>&2
./cp932CharMaker3.sh 1> cp932.temp
./cp932CharMaker4.sh 1>>cp932.temp

echo "シフトJISの読めるテキストファイルに変換する" 1>&2
0< cp932.temp xxd -r -p 1> cp932text.temp
echo "掃除" 1>&2
rm cp932.temp

echo "シフトJIS(cp932)をutf8に変換する" 1>&2
0< cp932text.temp iconv -f cp932 -t utf8 1> utf8text.temp
echo "掃除" 1>&2
rm cp932text.temp

echo "1行1文字に改行を入れて(grep) | バイナリデータを読み取り(od) | 見た目の改行を削除して(tr) |" 1>&2
echo "改行データ「0a」を「@」に入れ替え(sed) | 「@」で改行して(tr) 1> ファイルに書き込む " 1>&2
0< utf8text.temp  grep -o .| od -An -tx1 | tr -d "\n" | sed "s/0a/@/g" | tr "@" "\n" 1> utf8Code.txt
echo "掃除" 1>&2
rm utf8text.temp

echo "不足分の改行「0d0a」「0a」水平タブ「09」を最初に入れてから" 1>&2
echo " 0d 0a , 0a "  1>  cp932hex-utf8hex.txt
echo " 100 0a , 0a " 1>> cp932hex-utf8hex.txt
echo " 100 09 , 09 " 1>> cp932hex-utf8hex.txt
echo "cp932とutf8をカンマ区切りで横に繋げた(paste)ものを、追記する" 1>&2
paste -d , cp932Code.txt utf8Code.txt 1>> cp932hex-utf8hex.txt
echo "掃除" 1>&2
rm cp932Code.txt utf8Code.txt 

echo "こんなん作った" 1>&2
0< cp932hex-utf8hex.txt head 1>&2


exit 0
中間辞書から完成辞書をつくるスクリプトsjisTextToUtf8Text01_dic31.sh
#!/bin/dash
#set -x
: << '#--------------------------------------------------------------comment'
■10進数アドレス辞書作成 v3 r3
〜printf出力用8進数バックスラッシュ版,同変数代入式版,複合版

3つの辞書ファイルが作成される。

▼用意するデータファイル
sjis , utf8(1バイトの両側に空白を入れる)
1バイト文字の上位バイトは、0xの「100」とする。
別途スクリプトで作成する。
手書きで編集しても構わないが、適切にカンマと空白を入れる。

・cp932hex-utf8hex.txt
 100 de , ef be 9e 
 100 df , ef be 9f 
 81 40 , e3 80 80 
 81 41 , e3 80 81 

▼作成されるファイル形式
・nukamisoCP932decToUTF8octBS_01.txt
\351\265\244,\351\265\221,\351\265\220,\351\265\231,

・sjisTextToUtf8Text01_dic31_2
u256_125="\175"; u256_126="\176"; u256_161="\357\275\241"; u256_162="\357\275\242"; 

行は10進数で文字コードの上位バイト
列も同様に、文字コードの下位バイト

・nukamisoCP932decToUTF8octBS_02_.txt
上記2ファイルの組み合わせ

▼複合辞書の内訳[nukamisoCP932decToUTF8octBS_02_.txt]

001 カンマ区切りcsv

128 カンマ区切りcsv
129 変数代入式 セミコロン区切り

152 変数代入式 セミコロン区切り
153 カンマ区切りcsv

255 カンマ区切りcsv
256 変数代入式 セミコロン区切り


▼使用方法
「辞書作成中間ファイル作成スクリプトcp932dicMaker1.sh」で、
辞書の元になるテキストファイルを作成し、このスクリプトと同じディレクトリに置く。
このスクリプトを実行すると、「codeLineDir」フォルダと3つのファイルを作成する。

(置いとくだけ) cp932CharMaker3.sh
(置いとくだけ) cp932CharMaker4.sh
(1番目に実行) cp932dicMaker1.sh
(2番目に実行) sjisTextToUtf8Text01_dic31.sh (あなたが見ているこのファイル)

〜Maker3.sh,〜Maker4.shは、直接には実行しない。

(注意)
「xxd」コマンドが必要 (cp932dicMaker1.sh)



#--------------------------------------------------------------comment

sjisTextToUtf8Text01_dic31_com(){ # 実行前チェック,辞書変換
# 辞書の元になるテキストファイルのパス
dic="./cp932hex-utf8hex.txt"
if ls "${dic}" ;then true ;else exit 1 ; fi
# 作業ディレクトリ作成
mkdir codeLineDir 2>/dev/null

# 辞書変換
echo "中間辞書[dic${$}.temp]を作成" 1>&2
0< "${dic}" sed -e 'h;s/,.*/,/;x;s/.*,//;s/[^ ][^ ]/0x&/g;H;x;s/\n//;' > dic${$}.temp
if test ${?} -eq 0; then head ./dic${$}.temp ;else exit 1 ; fi
}

sjisTextToUtf8Text01_dic31_1(){
: << '#--------------------------------------------------------------comment'
▼ printf 8進数版

8進数はprintfで作成するが、同時に「@」を添えておく。
後でバックスラッシュに入れ替える。

cp932のみ、重複を点検する。もし重複があったらexitする。
複数のcp932コードに、同じutf8コードが指定されていても、停止しない。

▼ readコマンドの変数分離
readコマンドは、空白で文字列を分離する。

(1) cp932上位バイト
(2) cp932下位バイト
(3) カンマ
(4) utf8 0xつき16進数 空白を含む、残りの文字列すべて

カンマの両側にも、空白が必要。
utf8側16進数には、頭に「 0x」が必要になる。


#--------------------------------------------------------------comment

( # サブシェルで起動
while read sjisCodeHexU sjisCodeHexL x transCodeHex0x ; do # -----変数作成と代入
#echo $sjisCodeHexU $sjisCodeHexL $x $transCodeHex0x 

U932=$((0x${sjisCodeHexU})) #cp932 10進数 上位バイト ゼロ埋めなし
L932=$((0x${sjisCodeHexL})) #cp932 10進数 下位バイト ゼロ埋めなし

# 「@」バックスラッシュ「\」の代わり
transCodeOctAt=`printf '@%o' ${transCodeHex0x}`
#echo "u${U932}_${L932} ${transCodeOctAt}"

# cp932側の重複のみ点検
eval 'if test -n "${U'${U932}'L'${L932}'}" ;then \
echo U'${U932}'L'${L932}':重複 ; exit 1 ; fi'

eval 'U'${U932}'L'${L932}'="'${transCodeOctAt}'"'

done 0< "dic${$}.temp"

for upper in `seq 1 256` ;do # ------------------------------上位バイトループ
	for lower in `seq 1 255` ;do # --------------------------下位バイトループ
	eval 'echo -n "${U'${upper}'L'${lower}'},"' 
	done
echo "" # 上位バイト1つぶん、1行分の改行
# 「@」→「\」,行末の連続した「,」を削除
done | sed -e 's/@/\\/g;s/,*$//' 1> ./codeLineDir/nukamisoCP932decToUTF8octBS_01.txt

echo 'sjisTextToUtf8Text01_dic32_1 終了' 1>&2
return 0
)
}

sjisTextToUtf8Text01_dic31_2(){
: << '#--------------------------------------------------------------comment'
▼ printf 8進数 変数代入式版

変数名を、10進数に変換したcp932コードを組み合わせて作る。
U${U932}L${L932}=${transCodeOctAt}

この「変数代入式の文字列」を、変数代入式の左辺と同じ変数名に代入する。

#--------------------------------------------------------------comment
(
while read sjisCodeHexU sjisCodeHexL x transCodeHex0x ; do

U932=$((0x${sjisCodeHexU}))
L932=$((0x${sjisCodeHexL}))

transCodeOctAt=`printf '@%o' ${transCodeHex0x}`

#echo "u${U932}_${L932}E%${transCodeOctAt}%;"

eval 'if test -n "${U'${U932}'L'${L932}'}" ;then \
echo U'${U932}'L'${L932}':重複 ; exit 1 ; fi'

# 「E」イコール「=」の代わり
# 「%」二重引用符「"」の代わり
# 「@」バックスラッシュ「\」の代わり
eval 'U'${U932}'L'${L932}'="u'${U932}'_'${L932}'E%'${transCodeOctAt}'%;"'

done 0< "dic${$}.temp"


for upper in `seq 1 256` ;do
	for lower in `seq 1 255` ;do
	eval 'echo -n "${U'${upper}'L'${lower}'}"' 
	done
echo ""
# 「E」→「=」,「%」→「"」,「@」→「\」,「;」→「; 」
done | sed -e 's/E/=/g;s/%/"/g;s/@/\\/g;s/;/; /g' 1> ./codeLineDir/composite_dic31_2

echo 'sjisTextToUtf8Text01_dic31_2 終了' 1>&2
return 0
)
}

sjisTextToUtf8Text01_dic31_3(){
: << '#--------------------------------------------------------------comment'
▼複合辞書作成
#--------------------------------------------------------------comment

cd ./codeLineDir/ || exit 1
if test "${?}" -ne 0 ;then exit 1 ;fi
if test _"`ls composite_dic31_2`" = _"composite_dic31_2" ;then true ;else exit 1 ;fi
if test _"`ls nukamisoCP932decToUTF8octBS_01.txt`" = _"nukamisoCP932decToUTF8octBS_01.txt" ;
then true ;else exit 1 ;fi

echo "1〜128" 1>&2
0< nukamisoCP932decToUTF8octBS_01.txt head --lines="128" 1> ./nukamisoCP932decToUTF8octBS_02.txt
echo "129〜152" 1>&2
0< composite_dic31_2 head --lines="152" | tail --lines="24" 1>> ./nukamisoCP932decToUTF8octBS_02.txt
echo "153〜255" 1>&2
0< nukamisoCP932decToUTF8octBS_01.txt head --lines="255" | tail --lines="103" 1>> ./nukamisoCP932decToUTF8octBS_02.txt
echo "256" 1>&2
0< composite_dic31_2 tail --lines="1" 1>> ./nukamisoCP932decToUTF8octBS_02.txt

echo "おわり" 1>&2
return 0
}

########################################################################
# 最初から最後まで通して実行するのに必要なファイルのリスト
# これら4ファイルを、同じディレクトリに置く
#
# cp932CharMaker3.sh (cp932の1バイト文字を作成)
# cp932CharMaker4.sh (cp932の2バイト文字を作成)
# cp932dicMaker1.sh (辞書作成中間ファイル作成スクリプト)
#
# sjisTextToUtf8Text01_dic31.sh (あなたが見ているこのファイル)
#
# まとめて自動実行するなら、↓をコメントアウトする
########################################################################

#: << '#--------------------------------------------------------------comment'
# ------最初の最初から実行するなら-----------
# cp932のテキストファイル作成から実行するならコメントアウト
./cp932dicMaker1.sh
#--------------------------------------------------------------comment
sjisTextToUtf8Text01_dic31_com
sjisTextToUtf8Text01_dic31_1
sjisTextToUtf8Text01_dic31_2
wait
sjisTextToUtf8Text01_dic31_3



exit 0