【Awk & Jq活用講座】CSVデータ編集で使える最低限覚えておきたい正規表現の活用法


2021/03/26

AwkやSedといったテキストを編集する代表的なコマンドをより高度に操作するためには、
正規表現(Regular Expression)の理解が欠かせなくなります。

とはいえ各コマンドには正規表現の作法に違いが有りますし、同じコマンドでも違うOSで動いている場合にも動作に違いがあることもあります。

今回はあまりマニアックな正規表現のテクニックなどは避けながら、一般的なAwk(GNU Awk)とJqでcsvファイルを操作する際に最低限覚えておきたい正規表現をまとめてみます。


はじめに

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

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

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


AwkとJqの正規表現

基本的な操作に対する正規表現の対応表は以下のようになります。(※ただし正規表現内のEXP部分は任意の文字とみまします。)

操作

Awk

Jq

任意の1文字にマッチ

/./

"."

任意の文字列に最長マッチ

/.*/

".*"

任意の文字列に最短マッチ

/.*?/

".*?"

行の先頭にマッチ

/^EXP/

"^EXP"

行の末尾にマッチ

/EXP$/

"EXP$"

[...]内のどれか1文字にマッチ(右の例は0~9の数字とa~zの小文字)

/[0-9a-z]/

"[0-9a-z]"

[...]以外の1文字にマッチ(右の例は0~9の数字とa~zの小文字以外)

/[^0-9a-z]/

"[^0-9a-z]"

文字エスケープ

/\EXP/

"\\EXP"

任意の数字1文字にマッチ

/\d/

"\\d"

数字以外の1文字にマッチ

/\D/

"\\D"

アルファベットか数字かアンダーバー文字の1文字にマッチ

/\w/

"\\w"

アルファベットか数字かアンダーバー文字以外の1文字にマッチ

/\W/

"\\W"

空白文字(改行やタブも含む)の1文字にマッチ

/\s/

"\\s"

空白文字以外の1文字にマッチ

/\S/

"\\S"

パターンをグループ化(丸括弧で囲う)

/(EXP)/

"(EXP)"

どれかのパターンにマッチ(右の例はabcかefのパターンどちらか)

/abc|ef/

"abc|ef"

前の文字の0個または1個にマッチ

/EXP?/

"EXP?"

前の文字の0個以上にマッチ

/EXP*/

"EXP*"

前の文字の1個以上にマッチ

/EXP+/

"EXP+"

前の文字のm個にマッチ

/EXP{m}/

"EXP{m}"

(...)の中の文字列がm回繰り返したときにマッチ

/(EXP){m}/

"(EXP){m}"

前の文字のm個以上n個以下にマッチ

/EXP{m,n}/

"EXP{m,n}"

前の文字のm個以上にマッチ

/EXP{m,}/

"EXP{m,}"

前の文字のn個以下にマッチ

/EXP{,n}/

"EXP{,n}"

だいたいこの表にあるもので事足りるように思います。

AwkとJqの正規表現の大きな違いは、Awkの場合には正規表現をスラッシュで囲った中身が対象になるのに対し、Jqではダブルクオーテーション括弧の中身が検索されます。

よって、Awkでの
/文字が検索する文字列パターンにある場合には、文字エスケープ\(バックスラッシュ)で\/として上げないといけませんがJqではこれを考慮する必要がありません。

逆にJqではバックスラッシュ
\単体で使うと単なる文字として認識するので、文字エスケープとして呼び出したい場合にはバックスラッシュを二回重ねて\\としておく必要があります。

Awkの場合

おそらくcsvデータを集計・成形する際にAwkで正規表現を利用するパターンは主に下のユースケースが多いと思います。

            
            1. gensub関数で利用
2. Awk内のブロック条件内で利用
3. if条件の中で利用
        
それぞれのユースケースを以下でざっと見ていきましょう。

1. gensub関数

gensubはGnu awkに収録されている関数ですのでそれ以外のawkでは利用できませんが、パターンマッチなどが利用でき非常に強力なユーティリティ関数です。後方参照をさせるときに利用します。

            
            $ awk -F"," 'BEGIN {OFS=","} {
    date = gensub(/[0-9]{4}\/([0-9]{2})\/([0-9]{2})/, "\\1月\\2日", "g", $1);
    print date, $2, $3;
}' << EOF
2021/04/06,出張,山田
2021/04/08,会議,佐藤
2021/04/17,リモートワーク,鈴木
EOF
#👇出力
04月06日,出張,山田
04月08日,会議,佐藤
04月17日,リモートワーク,鈴木
        

2. Awk内のブロック条件内で利用

おそらくAwkの正規表現の利用法でもっとも多いのがアクションブロック前にパターン条件を指定して使う時の方法では無いかと思います。

            
            $ awk '
#👇一列目が数字で終わるもの
$1 ~ /[0-9]$/ {
    print FNR ": " $1
}
' << EOF
1234
abc
dfgh
567
ij
EOF
#👇出力
1: 1234
4: 567
        
正規表現を比較するには~演算子を利用して判定します。

3. If条件の中で利用

先ほどと同じですが、if構文の中でも正規表現で比較する~演算子を利用して条件分岐で利用することもできます。

            
            $ echo 'abc,efg123,hijk,lmn,456,ABC' | awk -F"," '{
    for (i=1;i<=NF;i++) {
        #👇3つの数字の連続の列があったら表示
        if ($i ~ /\d{3}/) {
            print $i;
        }
    }
}'
#👇出力
efg123
456

$ echo 'abc,efg123,hijk,lmn,456,ABC' | awk -F"," '{
    for (i=1;i<=NF;i++) {
        #👇3つの数字の連続以外の列があったら表示
        if ($i !~ /\d{3}/) {
            print $i;
        }
    }
}'
#👇出力
abc
hijk
lmn
ABC
        
なおAwkでは、セルの中の文字列を適切に処理する用途が多いので、さほど難解な正規表現は利用しないほうがベターです。もし、テキストファイル全体に及ぶような編集が必要であればSedでテキストの前処理を行いましょう。

Jqの場合

Jqの正規表現もAwkと考え方は一緒ですが、jsonオプジェクトに変換した後に、適切にフィールドの値を正規表現で操作するようなことが主流の使い方です。

つまりは、リファレンスでも述べられいるように文字列を後処理する関数フィルターを指定するようなユースパターンとして利用できます。

            
            用法:
    <文字列> | フィルター関数(<正規表現>)
    <文字列> | フィルター関数(<正規表現>; <フラグ>)
    <文字列> | フィルター関数([<正規表現>, <正規表現>, ...])
    <文字列> | フィルター関数([<正規表現>, ..., <正規表現>, <フラグ>])
        
指定出来るフラグに関しては

            
            g - グローバル検索:
    正規表現に一致した全てのパターンを探す
i - 大文字と小文字を区別:
    指定すると大文字と小文字を区別しない
m - マルチラインモード:
    改行文字にも'.'でマッチ出来るようになる。
    先頭位置を^、最後尾位置を$で指定できる。
s - シングルラインモード:
    デフォルト。
    改行文字までを検索する。
    ただし先頭位置は'^' -> '\A'、最後尾位置は'$' -> '\Z'として利用可能。
n - 空マッチ:
    指定すると空のマッチ結果の場合を無視する
p - 自動ラインモード:
    sとmを自動で切り替え
l - 最長マッチモード:
    デフォルトを最長マッチで検索するようになる。
x - 拡張正規モード:
    拡張正規が利用できる。
    空白文字の無視、コメントのスキップなど
        
ということで正規表現の利用できる関数を使うことになるのですが、

            
            + split
+ test
+ match
+ capture
+ gsub
        
を使えればcsvデータを扱うには十分事足りると思います。

ここでは良く利用するsplitとtestに着目し、これに絞って利用例をあげていきます。

split関数

split関数は、本題のようにcsvデータにとっては、コンマ切りしてjsonオブジェクトに切り分けるテクニックに欠かせない関数の一つです。

以下はこの一連の記事で毎回出てくる利用法です。

            
            jq -sR '
#👇mapを使うために配列化のために([...])でラップする
[
    #👇改行位置で配列に変換し、中身を取り出す
    split("\\r?\\n"; "g")[]
    #👇空文字を弾く
    | select(length > 0)
    #👇コンマ切りで配列に変換する
    | split(","; "g")
]' << EOF
2021/04/06,出張,山田
2021/04/08,会議,佐藤
2021/04/17,リモートワーク,鈴木
EOF
#👇出力
[
  [
    "2021/04/06",
    "出張",
    "山田"
  ],
  [
    "2021/04/08",
    "会議",
    "佐藤"
  ],
  [
    "2021/04/17",
    "リモートワーク",
    "鈴木"
  ]
]
        

test関数

Jqでのtest関数はよくselectと組み合わせることで、Json配列から条件を満たす要素を取り出す操作に利用できます。

            
            $ echo '["abc", "efg", "123"]' | jq -s '.[][] | select(. | test("[0-9]{3}"))'
#👇出力
"123"

#👇testは結果をtrue/falseで返すのでnotを追加すると否定のフィルターとなる
$ echo '["abc", "efg", "123"]' | jq -s '.[][] | select(. | test("[0-9]{3}") | not)'
#👇出力
"abc"
"efg"
        

まとめ

以上、AwkとJqでの正規表現の基本的な利用方法と主要なユースケースの紹介でした。

さらにcsvデータの有効活用に磨きをかけるためにはパターンマッチングや後方参照のテクニックを理解する必要がありますが、Awkでのgensub関数や、Jqでのcapture関数の応用方法はまた別の機会にまとめて紹介したいと思います。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

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