【Awk & Jq活用講座】 文字列の分割を分割を極める ~ split関数の使い方


2021/04/01

CSV形式のデータとは、その名の通りコンマ文字(,)を区切り位置として利用してデータの境界を区切るのテキストのことです。

ですが世の中でデータを表現するのはCSVファイルばかりではなく、CSVではない区切り位置のルールを持ったデータ形式のテキストファイルも多く存在しています。

今回は任意の区切り文字(セパレーター)を設定し、任意の文字をより柔軟・簡単に分割(split)する方法を解説していきます。


はじめに

当サイトではオフィス業務のComputer-Aidedなハイブリッドな方法を模索し、より効率的なExcel業務を実現したい多忙なオフィスワーカー向けの主にAwkとSedを使うシェル講座です。

シェルスクリプトはどこでもどんなOSでも基本的に使えて、しかも一度使い方を覚えると、Excelと組み合わせて最高に効率の良いオフィスワークツールが作れることでしょう。

合同会社タコスキングダム|蛸壺の技術ブログ


データファイルのsplit(分割)の基礎

今回のお題として、CSV以外のコンマ切り文字を持った形式のテキストを入力として扱ってみます。

なお最終的な出力はCSV形式で統一します。

区切り文字(セパレーター)としてはどのような文字でも基本的に利用できますが、今回の例では空白文字(スペース文字)とアンパサンド文字(&)の2つの場合を取り扱ってみます。

以下のようなコマンドでテストデータを予め生成しておきます。

            
            $ cat << EOF > testdata.ssv
山下モゲ雄 営業部 本社 3年
島田フガ子 経理部 名古屋支部 15年
岡田ピポ太 製造部 山口工場 8年
沢口モフ代 人事部 本社 4年
銭形ガメ吉 海外部 メキシコ支部 11年
EOF

$ cat << EOF > testdata.asv
山下モゲ雄&営業部&本社&3年
島田フガ子&経理部&名古屋支部&15年
岡田ピポ太&製造部&山口工場&8年
沢口モフ代&人事部&本社&4年
銭形ガメ吉&海外部&メキシコ支部&11年
EOF
        

Awkでsplitする場合

まずはAwkのパターンからやってみます。

Awkではオプション
-Fで区切り文字を渡すことにより、柔軟に入力ファイルのセパレーターを変えることができます。ちなみにデフォルトのセパレーターはスペース文字(空白)ですので、Csvファイルを取り込む時は必ず-F","としておかないといけません。

            
            #👇区切り文字がスペース文字の場合には-F" "は省略可
$ awk -F" " 'BEGIN{ OFS="," } {
    print $1,$2,$3,$4;
}' testdata.ssv
#👇Csvで出力
山下モゲ雄,営業部,本社,3年
島田フガ子,経理部,名古屋支部,15年
岡田ピポ太,製造部,山口工場,8年
沢口モフ代,人事部,本社,4年
銭形ガメ吉,海外部,メキシコ支部,11年

$ awk -F"&" 'BEGIN{ OFS="," } {
    print $1,$2,$3,$4;
}' testdata.asv
#👇Csvで出力
山下モゲ雄,営業部,本社,3年
島田フガ子,経理部,名古屋支部,15年
岡田ピポ太,製造部,山口工場,8年
沢口モフ代,人事部,本社,4年
銭形ガメ吉,海外部,メキシコ支部,11年
        
ということで区切り文字の柔軟な変更はAwkのもっとも得意とする処理の一つと言えます。

ちなみにオプション引数を使いたくない場合には、Awk内のBEGINのアクションブロックなどで
FS変数を制御する方法も考えられます。

            
            $ awk '
BEGIN {
    FS=" "; OFS=",";
} {
    print $1,$2,$3,$4;
}' testdata.ssv
#👇Csvで出力
山下モゲ雄,営業部,本社,3年
島田フガ子,経理部,名古屋支部,15年
岡田ピポ太,製造部,山口工場,8年
沢口モフ代,人事部,本社,4年
銭形ガメ吉,海外部,メキシコ支部,11年

$ awk '
BEGIN {
    FS="&"; OFS=",";
} {
    print $1,$2,$3,$4;
}' testdata.asv
#👇Csvで出力
山下モゲ雄,営業部,本社,3年
島田フガ子,経理部,名古屋支部,15年
岡田ピポ太,製造部,山口工場,8年
沢口モフ代,人事部,本社,4年
銭形ガメ吉,海外部,メキシコ支部,11年
        

複雑なセパレーターの設定

オプション-FFSに指定できる文字に関しては正規表現も利用できるためかなり高度な区切り「表現」が利用できます。

例えば以下のようにカスタマイズ可能です。

            
            $ awk '
BEGIN {
    FS="[ ,+&$-/]"; OFS=",";
} {
    print $1,$2,$3,$4;
}' << EOF
山下モゲ雄+営業部&本社/3年
島田フガ子,経理部-名古屋支部,15年
岡田ピポ太,製造部-山口工場,8年
沢口モフ代$人事部 本社&4年
銭形ガメ吉/海外部$メキシコ支部+11年
EOF
#👇Csvで出力
山下モゲ雄,営業部,本社,3年
島田フガ子,経理部,名古屋支部,15年
岡田ピポ太,製造部,山口工場,8年
沢口モフ代,人事部,本社,4年
銭形ガメ吉,海外部,メキシコ支部,11年
        
同時に複数のセパレーターを指定する場合、文字の順序によってはAwkに受付られないものが有り、エラーが発生する場合があります。エラーが発生した場合は、その都度順序を調整してみてください。

            
            $ awk '
BEGIN {
    FS="[ ,+&-$/]"; OFS=",";
} {
    print $1,$2,$3,$4;
}' << EOF
山下モゲ雄+営業部&本社/3年
島田フガ子,経理部-名古屋支部,15年
岡田ピポ太,製造部-山口工場,8年
沢口モフ代$人事部 本社&4年
銭形ガメ吉/海外部$メキシコ支部+11年
EOF
#👇Csvで出力
awk: bad regex '[ ,+&-$/]': Invalid character range
        

Jqでsplitする場合

では先程Awkでやってみた分解をJqでも行ってみます。

JqでのSplit系のメソッドは何通りかありますが、使用に注意が必要なのは正規表現が引数として扱える扱えないの区別があることです。

Jqのマニュアルより以下にこれらの関数をまとめると、

            
            split(文字列):
    セパレーター(区切り位置)となる文字列で分割した文字列を配列として返す。
    引数の文字列は正規表現ではないので注意

split(パターン; フラグ):
    上のsplit(文字列)メソッドの正規表現拡張版。
    フラグを指定することでこちらの関数が識別される。
    返り値として分割された文字列の配列が返される

splits(パターン)もしくはsplits(パターン; フラグ):
    上のsplit(パターン; フラグ)と作用は同じだが、
    返り値として文字列の配列ではなく、ストリームが返される
        
となりますが、結果として配列が返ってくるほうが扱いやすいので、ほぼ利用するのはsplit(文字列)split(パターン; フラグ)になると思います。

また、基本的に正規表現ではない分、
split(文字列)関数の方がより高速に動作することが期待されるので、簡単な分割パターンならば極力split(文字列)で攻めたほうがベターです。

            
            $ jq -s -R '
    [ split("\n")[] | select(length > 0) | split(" ") ]
' testdata.ssv
#👇配列として表示
[
  [
    "山下モゲ雄",
    "営業部",
    "本社",
    "3年"
  ],
  [
    "島田フガ子",
    "経理部",
    "名古屋支部",
    "15年"
  ],
  [
    "岡田ピポ太",
    "製造部",
    "山口工場",
    "8年"
  ],
  [
    "沢口モフ代",
    "人事部",
    "本社",
    "4年"
  ],
  [
    "銭形ガメ吉",
    "海外部",
    "メキシコ支部",
    "11年"
  ]
]
        
ちなみに、出力をJSONストリームから配列表示にしてくれる-s/--slurpはmapなどの後段の処理を続ける場合などには重要になるオプションです。-sオプションを使う場合には、入力がストリームと扱われず改行も含めて単一の文字列に扱われることから、改行文字\nの位置で自前の配列化を行う操作を行う必要があります。(ストリーム表示と配列表示の違いはこのスクリプトから-sオプションを外してみたら良く理解できると思いますので、疑問に感じたら一度やってみてください。)

またセパレータが
&のファイルでは以下のようになります。

            
            $ jq -sR '
    [ split("\n")[] | select(length > 0) | split("&") ]
' testdata.asv
#...出力結果は先ほどと同じ
        
CsvファイルをJqコマンドで扱う場合には、特にSplit関数の取り扱いを理解しておくべきですので、このテクニックの重要度はかなり高いと言えます。

複雑なセパレーターの設定

余談として、Jqでも複雑なルールによる分割が行えるということも確認しておきましょう。

以下は単一の文字列を配列として分割してみる一例です。

ここでは前の節で述べていたように、正規表現で分割する
split(パターン; フラグ)関数を用います。

            
            $ echo 'a, b,c,d, e, +f$g/h i jkl mn+o p' | jq -R '
    split("\\s|,|\\+|\\$|/";"g") | map( . | select(length > 0) )
'
#👇出力
[
  "a",
  "b",
  "c",
  "d",
  "e",
  "f",
  "g",
  "h",
  "i",
  "jkl",
  "mn",
  "o",
  "p"
]
        
この例では|を用いて、ORの用法でセパレータを繋げていますが、より正規表現的に文字を扱うと先程のスクリプトは以下としても同様です。

            
            $ echo 'a, b,c,d, e, +f$g/h i jkl mn+o p' | jq -R '
    split("[\\s,+$/]";"g") | map( . | select(length > 0) )
'
#...出力は先程と同様
        
また、正規表現に頼らず、通常のsplit(文字列)関数を使って、よりJq的に関数プログラミングチックに書くと以下でもOKです。

            
            $ echo 'a, b,c,d, e, +f$g/h i jkl mn+o p' | jq -R '
    split(" ") |
    map( split(",") | .[] ) |
    map( split("+") | .[] ) |
    map( split("$") | .[] ) |
    map( split("/") | .[] )
'
#👇複数のセパレーターで配列化
[
  "a",
  "b",
  "c",
  "d",
  "e",
  "f",
  "g",
  "h",
  "i",
  "jkl",
  "mn",
  "o",
  "p"
]
        
このスクリプトのキモは、Jqでのチェーン処理を上手く利用して、split関数による配列化と、後段のmapによる配列の成分分解と再配列化(map( split(",") | .[] )の部分)をパイプラインで繋いでいることにあります。Jqのsplitには複数の文字を同時に区切り文字として取り扱う機能がないので、各パイプラインのsplit関数にセパレーターを追加していくようなコードスタイルになります。


まとめ

今回は、CsvファイルをAwkとJqで任意のセパレータを使って配列へと分割する方法を詳しく見て参りました。

どちらのコマンドを使っても、Csv形式で使われる以外のセパレート文字によっても最終的にはCsv形式に変換・統一して編集できることを示すことができました。

今後の記事でもsplit関数は作成するスクリプトなどにひょっこり姿を表すと思いますが、その時はこの記事のことを思い出していただけると幸いです。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。