📚

awk組み込み変数の活用でさまざまな形式のテキストファイルを扱う

2023/01/20に公開

DXプラットフォーム部エンジニアの吉成です。

皆さんの日々の業務では、awk というプログラミング言語(あるいはコマンド)を活用されているでしょうか。
awk はテキスト処理が得意で、正規表現による文字列マッチング機能も強力です。

特にレコード・フィールドといった一定単位でのテキストの取り扱いに長けています。
レコードは awk プログラムにおけるテキストの処理単位で、典型的にはテキストデータの「行」などを指します。
フィールドはレコードをさらに細かく区切った単位で、典型的には csv ファイルの「,」で区切られた部分のテキストを指します。
典型例を紹介しましたが、レコードもフィールドも、分割単位をユーザが変えることもできます。

awk の最大のポイントは、「『パターン』にマッチしたレコードに対して『アクション』を実行する」ということです。
図1 は、awk における主要な概念であるレコード、フィールド、パターン、アクションの関係を表したものです。郵便番号とそれに対応する地名が記載されたタブ区切りのファイルから、郵便番号の部分を抽出するプログラムを例にしています。

図1: awk における主な概念の関係

awk の文法はC言語とよく似ています。C言語から影響を受けたプログラミング言語は数多くありますし、何らかの言語でのプログラミングを経験した方なら awk を使えるようになるのに時間はかからないでしょう。
今回は awk そのものの入門記事ではないため省略しますが、awk を使ったことがないという方もぜひ文法を調べてみてください。

タブ区切り以外のファイルも扱いたい

awk の文法を調べたら、タブ区切りでのテキストファイルはすぐに扱えるようになるはずです。

少し慣れてくると、awk でより様々な形式のファイルを扱いたいと思うようになるでしょう。
そうすると、

  • 入力と出力でフィールドの区切り文字を別にしたい!
  • csv ファイルを扱いたいのに、フィールド値の中の「,」でフィールドが区切られてしまう……
  • awk に小数の計算をさせてみたが、有効桁数が思っていたものと違う……

など、迷うことが増えてくるかもしれません。

これらの処理をスムーズに記述するためには、awk の基礎文法だけではなく組み込み変数のことも知っておく必要があります。

awk はテキスト処理に強いというだけあって、様々な形式のテキストファイルの読み書きに使える組み込み変数が用意されています。
例えば、FS には入力ファイルのフィールド区切り文字、OFS には出力ファイルのフィールド区切り文字を設定できます。

本記事では、そんな awk の組み込み変数、特にさまざまな形式のテキストファイルを入出力する際に使える組み込み変数とその使用例を紹介します。

なお、記事中のプログラムはすべて gawk 5.0.1 にて動作を確認しています。
また、特に断りなく「公式ドキュメント」と記載した場合には gawk の公式ドキュメントを指すこととします。

入力形式に関わる組み込み変数

入力におけるフィールドの区切り文字を指定: FS

フィールドの区切り文字を指定するための組み込み変数です。
つまり、CLI としての awk の -F オプションに対応しています。
初期値は [ \t]+、つまりスペースかタブの繰り返しです。

初期値を見ても分かるとおり、正規表現を指定できます。
正規表現に自信がないという方は、2022年12月23日の記事「正規表現専門家がエンジニアになって正規表現がわからなくなった話」もご覧ください。
当該記事は PostgreSQL を題材にしていますが、基礎的な部分は多くのツール・プログラミング言語に共通しています。

FS を使ってカンマ区切りのテキストを読み込む例を以下に示します。

$ echo "pencil,eraser,scissors" | awk 'BEGIN { FS = "," } { print $2 }'
eraser

しかし、単に FS = "," とするだけでは csv ファイルのフィールドを正しく区切ることができません。
csv ファイルはフィールド内に「,」を含むことがあり、その場合はフィールドの値を「"」で囲むことになっているためです。

下記の例は、先ほどと同じ awk プログラムでフィールド内に「,」を含む CSV ファイルを処理しようとした場合の結果です。
「Hello, world!」をひとまとまりのフィールド値として扱ってほしいところですが、「Hello」の後ろの「,」でフィールドが区切れてしまっています。

$ echo '123,"Hello, world!"' > test.csv
$ cat test.csv | awk 'BEGIN { FS = "," } { print $1; print $2 }'
123
"Hello

このような場合は、次に紹介する FPAT を使う必要があります。

フィールド値の形式を指定: FPAT

どのような文字列がフィールド ではない かを定義する FS に対して、 FPAT はどのような文字列がフィールド である かを指定します。
ただし、FPAT は gawk 固有の組み込み変数であり、gawk 以外の処理系では使用できません。
初期値は [^[:space:]]+、つまり、スペース、タブ、改行、行頭復帰、ページ分割、垂直タブ以外の文字の繰り返しです。
FPAT を定義すると、 FS や後述の FIELDWIDTHS での設定は上書きされます。

FPAT を使えば、下記のようにフィールド値に「,」を含む csv を扱うことができます。
正規表現は公式ドキュメントの記述を参照しました。
| の前の ([^,]*) で「,」を含まないフィールド値に、| の後の (\"([^\"]|\"\")+\") で「,」を含み「"」で囲まれているフィールド値にそれぞれ対応しています。

$ echo '123,"Hello, world!"' > test.csv
$ cat test.csv | awk 'BEGIN { FPAT = "([^,]*)|(\"([^\"]|\"\")+\")" } { print $1; print $2 }'
123
"Hello, world!"

固定長フィールドのファイルを扱うなら: FIELDWIDTHS

スペースで区切られた数値の文字列によって、各フィールドの文字数を指定できます。
実務では稀に出てくる、各フィールドの長さが固定のファイルを取り扱うときに使用します。
FPAT 同様 gawk 固有の組み込み変数であり、gawk 以外の処理系では使用できません。
初期値は空文字列です。
FIELDWIDTHS を定義すると、 FSFPAT での設定は上書きされます。

FSFPATFIELDWIDTHS のうち FPATFIELDWIDTH は値を設定すると他の2つの設定を上書きしますが、
どちらも設定した場合には下記の例のように後勝ちで設定されるようです。

$ echo '123,"Hello, world!"' > test.csv
$ cat test.csv | awk 'BEGIN { FIELDWIDTHS = "5 5 *"; FPAT = "([^,]*)|(\"([^\"]|\"\")+\")" } { print $2 }'
"Hello, world!"
$ cat test.csv | awk 'BEGIN { FPAT = "([^,]*)|(\"([^\"]|\"\")+\")"; FIELDWIDTHS="5 5 *" } { print $2 }'
Hello

ちなみに、公式ドキュメントには

A space-separated list of columns that tells gawk how to split input with fixed columnar boundaries.

と記載されていますが、少なくとも gawk 5.0.1 ではタブ区切りでもいけるようです。

$ echo '123,"Hello, world!"' > test.csv
$ cat test.csv | awk 'BEGIN { FIELDWIDTHS = "5\t5\t*" } { print $2 }'
Hello

入力におけるレコード区切り文字を指定: RS

これまではフィールドの値や区切り文字を指定する組み込み変数を見てきましたが、
RSレコードの区切り文字を指定する組み込み変数です。
FSFPAT ほど使用頻度は高くないものの、FS のことを知っていれば分かりやすい組み込み変数ですね。
初期値は \n になっています。

FS は gawk 以外の処理系でも正規表現を指定できましたが、RS の場合は gawk のみ正規表現を指定できます。
gawk 以外の多くの処理系では、 RS に指定された値の最初の文字だけがレコードの区切り文字として使用されます。
(ここでは gawk の公式ドキュメントに倣って
「gawk 以外の多くの処理系では」と記載していますが、実際は gawk 以外の処理系でも新しめのバージョンでは
RS の正規表現での指定に対応していることがあります。
例えば mawk 1.3.4 でも RS に指定した正規表現が機能することを確認しています)

出力形式に関わる組み込み変数

出力におけるフィールドの区切り文字を指定: OFS

FS の出力版です。 print 文で複数の値を出力する際に使用されます。
ただし、FS とは異なり対応する CLI オプションはありません。
初期値は (スペース 1 つ)です。

下記に使用例を示します。

$ echo -e "160\t0005\n162\t0803" > test.tsv
$ cat test.tsv
160     0005
162     0803
$ cat test.tsv | awk 'BEGIN { OFS = "-"; } { print $1, $2 }'
160-0005
162-0803

出力におけるレコード区切り文字を指定: ORS

RS の出力版です。
初期値は \n になっています。
RS 同様使用頻度は高くありませんが、RSOFS と一緒に覚えておくと良さそうです。

出力における数値→文字列変換の形式を指定する: OFMT

print 文で数値を文字列として出力する際の出力形式を指定できます。
あくまで出力する際の形式を指定するための組み込み変数なので、内部的な値の持ち方には影響を与えません。

初期値は %.6g になっています。
これは、

  • g: 通常の浮動小数点数と指数部付きの浮動小数点数のどちらか文字数の少ないほうを採用するという意味の書式指定文字
  • .6: 小数点以下6桁(一部の書式指定文字に対しては「最大で6文字」を意味する)
    です。
    なお、ここでいう通常の浮動小数点数とは 1.234 のような日常生活でもよく見るような小数の記法、指数部付きの浮動小数点数とは 1.234e+03 (= 1234) などのような指数部を持つ記法です。

C 言語や Python といった他の言語でフォーマット指定子の記述をしたことがある方には馴染みのある記法かもしれません。
他の言語ではあまり見られないフォーマット指定子もあるものの、ほとんどは他の言語と共通しているので、
詳しい記法は公式ドキュメントを参照してください。

OFMT と混同しやすい CONVFMT

OFMT と混同しやすい組み込み変数として CONVFMT があります。

CONVFMT も数値→文字列の変換形式を指定するという点では OFMT と同じです。
初期値も %.6gOFMT の初期値と変わりません。
しかしながら、いつ変換するときに使われるかOFMT とは異なります。
具体的には、OFMT が print 文での出力の数値→文字列変換に使われるのに対して、
CONVFMT はプログラム内での計算中に数値→文字列変換をする必要が出てきたときに使われます。

言葉ではなかなか分かりづらいところかと思いますので、下記の例を見ていただければと思います。
OFMT はあくまで出力形式の指定で内部での値を変えないので、(4a) では 1/3 * 3 = 1 が出力されています。
一方 CONVFMTa = a"" での数値→文字列変換で適用されて 変数 a に格納された値を変えますが、出力形式には関与しません。

BEGIN {
    print "OFMT";
    a = 1/3;
    print a;        # (1a) デフォルトの OFMT が適用される
    OFMT = "%.1f";  # 小数点以下 1 桁のみを出力するように変更
    print a;        # (2a) 変更した OFMT が適用される
    a = a"";        # 計算時の数値→文字列変換には OFMT は適用されない
    print a;        # (3a) a はこの時点では文字列なので、OFMT の指定は適用されない
    print a * 3;    # (4a) 内部的には 0.333333... のままなので、3 倍すると 1 になる
    b = 1/6;
    print b;        # (5a) 変更した OFMT が適用される

    OFMT = "%.6g";  # OFMT を初期値に戻す

    print "CONVFMT";
    a = 1/3;
    print a;            # (1b) デフォルトの OFMT が適用される
    CONVFMT = "%.1f";   # 文字列変換時は小数点以下 1 桁だけを残すよう変更
    print a;            # (2b) デフォルトの OFMT が適用される
    a = a"";            # 計算時の数値→文字列変換には変更した CONVFMT が適用される
    print a;            # (3b) CONVFMT 適用済みなので小数点以下 1 桁しか残っていない
    print a * 3;        # (4b) 内部的にも "0.3" になったので、3 倍すると 0.9 になる
    b = 1/6;
    print b;            # (5b) 文字列に変換せず数値のまま出力すれば CONVFMT は適用されない
}
$ awk -f ofmt_vs_convfmt.awk 
OFMT
0.333333  # (1a)
0.3       # (2a)
0.333333  # (3a)
1.0       # (4a)
0.2       # (5a)
CONVFMT
0.333333  # (1b)
0.333333  # (2b)
0.3       # (3b)
0.9       # (4b)
0.166667  # (5b)

おわりに

本記事では、入出力形式の指定に関わるものを中心に、 awk の組み込み変数について見ていきました。

awk には今回紹介したもの以外にも

  • 各フィールドの値を表す $1, $2, …, $NF
  • 現在処理している行の行番号を表す NR
  • 各種環境変数を参照できる ENVIRON
    など、便利な組み込み変数が多数用意されています。

図抜けた手軽さと軽快さゆえになんとなく使ってしまいがちな awk ですが、
時々は立ち止まってより便利な書き方がないか調べてみるのも楽しいと思います。

参考文献

The GNU Awk User’s Guide, https://www.gnu.org/software/gawk/manual/gawk.html, 2023年1月10日参照.

中島雅弘, 富永浩之, 國信真吾, 花川直己, AWK 実践入門 ライトウェイトなテキスト処理言語の超定番, 技術評論社, 2015.

FORCIA Tech Blog

Discussion