フューチャー技術ブログ

シェルスクリプトで固定長ファイルに区切り文字を入れてCSVに変換する

春の入門連載の12本目です。

はじめに

こんにちは。新卒2年目の山下です。

最近ちょっとした作業でシェルスクリプトを触る機会が増え、「固定長ファイルをCSVに変換する」といったややマニアックな文字列操作をすることがありました。

せっかくの機会ですので、利用コマンドやオプションをまとめました。

本編

環境

  • Red Hat Enterprise Linux release 8.5 (Ootpa)
    ※利用する環境によってはマルチバイト文字を扱う際の挙動が異なる可能性があります。

利用するファイル

例として、固定長Shift_JIS(SJIS)のファイルを可変調UTF-8に変換します。

入力の固定長ファイルは次の条件のものを想定しています。

  • 下記エスケープが必要な文字が含まれない
    • 改行コード(\nや\rなど)
    • カンマ(,)
    • クォート(”)

変換前

  • 文字コード:SJIS
  • レコード長:10Byte
  • カラム長:2Byte(全カラム固定)
  • ※上記エスケープ文字や特殊文字の一部には対応出来ません
hoge.txt
$ cat hoge.txt
アイウエオカキクケ012345678サシスセソタチツテabcdefghア

変換後

  • 文字コード:UTF-8
  • CSV
result.csv
$ cat result.csv
アイ,ウエ,オカ,キク,ケ
01,23,45,67,8
サシ,スセ,ソタ,チツ,テ
ab,cd,ef,gh,ア

①文字コードの変更

はじめに、Bashで適切に文字を読み込めるよう、文字コードの変換を行います。

iconvコマンドを利用し-fで指定した文字コードを-tで指定した文字コードに変換し標準出力することが可能です。

  • 変換前のファイル

    $ cat hoge.txt #-- SJISをUTF-8と解釈して出力しているため文字化けしている
    ���������P01234567�W�������eabcdefgh�A
  • iconvで文字コードを変換後のファイル

    $ iconv -f SJIS -t UTF8 hoge.txt >> hogehoge.txt #--リダイレクトすることでhogehoge.txtに出力
    アイウエオカキクケ012345678サシスセソタチツテabcdefghア

②改行付与

fold

最も基本的な改行付与のコマンドはfoldコマンドです。

指定した値で改行を増やし標準出力します。

fold -option filepath
オプション 概要
-b バイト数で数える
-w 幅で数える
バイト数区切りで改行付与する場合

-bで指定したバイト数単位で改行が付与されます。

今回の例の場合、前段で文字コード変換(SJIS→UTF-8)をしたため、
半角カナSJIS:1Byteや全角文字SJIS:2ByteのほとんどはUTF-8:3ByteにByte数が変更されるため、カラムずれがおきています。

(現環境では文字の途中で改行コードが挿入されることはなく、10Byteを超える場合は手前の文字までが1行となっています。)

$ fold -b10 hogehoge.txt
アイウ
エオカ
キクケ0
12345678
サシス
セソタ
チツテa
bcdefghア
文字列幅区切りで改行付与する場合

文字列幅は全角:2半角:1でカウントされます。

SJISの半角文字は1文字:1Byte全角:2Byteため、文字列幅基準で改行付与することでUTF-8に変換した後でも想定するレコード長で区切ることが出来ました。

$ fold -w10 hogehoge.txt
アイウエオカキクケ
012345678
サシスセソタチツテ
abcdefghア
文字数区切りで改行付与する場合

参考程度ですが、文字数区切りで改行を付与する方法も記載します。

grep -oを利用して.に当てはまる文字を1文字ずつ出力し、指定の文字数で改行を付与しています。

$ for char in $(grep -o . hogehoge.txt) ; do echo -n $char; count=$(( count + 1 )); if [ $count -eq 9 ]; then echo ""; count=0; fi; done
アイウエオカキクケ
012345678
サシスセソタチツテ
abcdefghア

③カラム区切り文字の挿入

次に区切り文字を挿入してカラム分割を行います。

カラム単位で個別区切り

sedコマンドの-eオプションを利用して指定した文字数で個別に,(カンマ)を挿入する方法です。

$ while read line ;do echo $line | sed -e 's/./&,/8' -e 's/./&,/6' -e 's/./&,/4' -e 's/./&,/2' ; done < hogehoge.txt
アイ,ウエ,オカ,キク,ケ
01,23,45,67,8
サシ,スセ,ソタ,チツ,テ
ab,cd,ef,gh,ア

正規表現を利用した文字数区切り

文字列を1文字ずつループしながら下記処理を実行しています。

  1. 全半角を識別
  2. 半角文字の場合は2文字、全角文字の場合は1文字間隔で区切り文字,(カンマ)を挿入
  3. 行の末尾で改行
while read line
do
count=0
for ((i=0; i<${#line}; i++)); do
char="${line:$i:1}"
if [[ ${#line} = $(( i + 1 )) ]]; then
echo $char
continue
fi
if [[ -n $(echo $char | grep -P "[ヲ-ン]") ]] || [[ -n $(echo $char | grep -E "[0-9A-Za-z]") ]]; then #ASCII or 半角カナは2文字カウントしたらカンマを挿入
count=$((count + 1))
if [[ $count = 2 ]]; then
echo -n "$char,"
count=0
else
echo -n $char
fi
else
echo -n "$char,"
fi
done
done < hogehoge.txt > result.csv

------ 結果 ------
アイ,ウエ,オカ,キク,ケ
01,23,45,67,8
サシ,スセ,ソタ,チツ,テ
ab,cd,ef,gh,ア

ようやくCSVに変換することが出来ました。

まとめ

Bashの標準機能のみで全半角混在の文字列をキレイに分割することは少し難しいですが、簡単なレイアウトであれば上記を流用することでカラム分割が可能になりました。

また、今回調べる中で利用するバージョンやディストリビューションによって挙動が異なる事を知りました。

例えばfoldコマンドを使った改行をubuntuで試すと、下記のように文字化けが起こります。(マルチバイト文字の扱いが異なることが原因のようです。)

  • Description: Ubuntu 22.04.2 LTS
$ fold -w10 hogehoge.txt
アイウ�
��オカ�
�クケコ
0123456789
サシス�
��ソタ�
�ツテト
abcdefghij

今回の手法は汎用的に利用できるものでは無いかも知れませんが、マルチバイト文字のByte数が変わると分割や文字カウント方法が少し複雑になり単純計算できない事も多くまとまった内容は少ないです。参考になることがあれば嬉しいです。

参考

アイキャッチ画像は磯の香り - No: 29069589|写真AC を利用させていただきました。