1. 概要

テキストの操作は、多くの開発者の日常業務の一部です。 このチュートリアルでは、AWKプログラミング言語を使用して大きなテキストデータセットを処理する方法を学習します。

2. オリジンストーリー

AWKの起源を知らない限り、その意味を誤解してしまう可能性があります。 それでは、過去にタイムトラベルして、AWKのクリエイターの心を読んでみましょう。

1970年代、 Bell Labs は、驚異的なコンピューティングの発明の範囲を提供することに重点を置いていました。それはすべて、過去10年間のUnixオペレーティングシステムの誕生から始まりました。 Unixユーザーにさらに力を与えるために、彼らは各ツールが1つの小さなタスクをうまく実行するツールボックスを描くことによってソフトウェアを作成しました。

とりわけ、根本的なコンピューティングのニーズの1つは、大きなテキストファイルを効率的に処理することでした。 通常のエディターも既存のプログラミング言語も、この要件に効率的に対処できませんでした。 一方では、通常のエディターが大きなテキストファイルを開くことさえほぼ不可能でしたが、他方では、小さなタスクであっても、コードが多すぎて記述できませんでした。

必要性は発明の母であるとよく言われるように、3人のコンピューター科学者、すなわちアルフレッド・エイホ ピーターJ。 ワインバーガー 、 とブライアン・カーニハン 、すぐに新しいプログラミング言語を共同執筆することで解決策を思いつきました。 さらに、彼らはそれぞれの最後の名前から最初の文字をとることによって、それをAWKと名付けることになりました。

何十年も経った今でも、この発明の美しさは、この急速に変化するテクノロジーの世界に今日でも関連しているという事実によって評価することができます。

3. シーケンシャル読み取り/書き込み

AWKを使用すると、サイズを気にせずに大きなテキストファイルを処理できます。 これが可能なのは、テキスト全体をメモリにロードするのではなく、チャンクで順番に読み取っているからです。 これを行うAWKの方法を見てみましょう。

3.1. 入力レコード

まず、AWKに入力されたテキスト全体を一連のレコードとして解釈する必要があることを伝える必要があります。 レコードを分離するために、変数RSを介して値にアクセスできる正規表現を使用します。 RSの値を指定しない場合、AWKは改行文字としてデフォルト値を選択します。 RS =” \ n”の場合のこの読み取りフローのシミュレーションを見てみましょう。

ほとんどの実用的な目的では、ファイル内の1行のテキストが使用可能なメモリに収まる可能性が高いため、このアプローチが適しています。

ただし、AWKは一部のシナリオで使用するのに理想的なツールではない場合があります。 たとえば、ファイルの最初の行と最後の行の内容が同じであるかどうかを確認する必要があるという架空の状況を考えてみましょう。 当然、この場合、AWKはファイル全体を行ごとにスキャンする必要があるため、効果がありません。

代わりに、headtailなどのUnixユーティリティを使用して、入力ファイルを最初と最後から同時に読み取ることで、この問題を効率的に解決できます。

[[ "$(head -1 input.txt)" == "$(tail -1 input.txt)" ]]; echo $?

3.2. 入力フィールド

AWKは一度に1つのレコードを読み取るため、このテキスト単位を効果的に処理するには、優れた戦略が必要です。 この目的のために、フィールドセパレーター(任意の正規表現)を使用して、レコードをフィールドのシーケンスにさらに分割できます。

AWKは、フィールドセパレータの値をFSという内部変数に格納します。 空白をフィールド区切り文字として扱うことはかなり一般的であるため、存在を示す正規表現 [\ t \ n] +、である FS、のデフォルト値に依存できます。 1つ以上のタブ、スペース、または改行文字。 もちろん、デフォルト値がユースケースに適合しない場合は、明示的に定義することを選択できます。

さらに、AWKを使用すると、フィールド変数を使用してフィールドテキストを参照するのに便利です。 一般的な経験則として、フィールド変数は、フィールドインデックスの前に$プレフィックスを使用して形成されます。 これを想像して、より高度な概念を理解する準備をしてみましょう。

ほとんどのプログラミング言語の配列インデックスとは異なり、AWKのフィールドインデックスは値1で始まり、最後のフィールドインデックスはNFという内部変数に格納されることに注意する必要があります。 さらに、で完全なレコードを参照する場合は、$0変数を使用できます。

3.3. 出力フィールドとレコード

したがって、AWKプログラムは、入力テキストをレコードとフィールドで構成されているものとして解釈します。 同様に、出力テキストにもこのアナロジーを使用します。 しかし同時に、出力レコードセパレーター(ORS)と出力フィールドセパレーター(OFS)異なる区切り文字を柔軟に選択できます。

レコードとフィールドとして編成された出力テキストを印刷するには、AWKの組み込みのprintコマンドを装備する必要があります。

print [expression ...]

ここで、この知識を使用して、入力レコードに存在する各数値 n について、数値の2乗を読み取り可能な形式 n * n =n2で印刷してみましょう。

{print $1 "*" $1 ,"=", $1*$1}

printステートメントは、コンマで区切られた3つの式を操作していることに注意する必要があります。

  • 最初の式は文字列式で、 $ 1“ *” $ 1、は方程式の左辺を表します
  • 2番目の式は、等号「=」に対応する文字列リテラルです。
  • 3つ目は、数式の右辺を形成する数式 $ 1 * $ 1、です。

そのため、個々の式を区切るコンマは、出力フィールドを識別するためのAWKのキューとして機能します。 これにより、印刷時に、各コンマは OFS に格納されている文字列に置き換えられます。この文字列のデフォルト値は、単一のスペース文字です。 さらに、印刷ステートメントの終わりは、AWKがレコードの終わりを識別するためのもう1つの手がかりです。 したがって、各printステートメントは、 ORS に格納されている文字列を出力します。この文字列のデフォルト値は、改行文字です。

最後に、プログラムが最初の4つの自然数をそれぞれ別々の行に含むサンプル入力で動作する場合の出力結果を見てみましょう。

1*1 = 1
2*2 = 4
3*3 = 9
4*4 = 16

4. AWKプログラム

4.1. パターン-アクションの基本

AWKプログラムを作成する前に、読み取る入力テキストの各行に使用する処理戦略を用意する必要があります。

  • この行で実行する必要のあるアクションのリスト
  • これらのアクションがピックアップされるために当てはまらなければならない条件またはパターン

その後、入力テキストの各行が処理される一連の[pattern][action]ステートメントとしてプログラムを作成できます。

1行ごとにコメントを含むテキストファイルからユーザーのフィードバックコメントを表示する単純なアプリケーションがあると想像してみてください。 ただし、読みやすさの観点から、100文字未満のコメントのみを表示したいと思います。 これを行うためのAWKプログラムを作成する方法を見てみましょう。

length($0) < 100 { print }

はい、それは1行のプログラムです。 ご覧のとおり、アクションステートメントは中括弧内にあり、組み込みの length()関数と $0で識別される現在のレコード変数を使用する条件付きパターンの直後にあります。

4.2. デフォルトの動作

AWKは、多くの一般的なシナリオでデフォルトの動作でベイク処理されるため、テキストデータの操作に関しては開発者にとって非常に使いやすいです。 その結果、非常に少ないコードで多くのことを達成できます。

一般的に、パターンおよびアクションステートメントは、の記述に必ずしも必須ではありません。 AWKは、欠落しているパターンとアクションをデフォルトの動作で埋めることにより、この柔軟性を提供できます。

  • デフォルトのパターンは、入力からのすべてのレコードに一致します
  • デフォルトのアクションでは、すべてのレコードが印刷されます

この事実を知っていると、ユーザーフィードバックフィルタリングAWKプログラムのアクション部分をスキップし、パターンをそのままにして同じ結果を得ることができます。

length($0) < 100

4.3. 実行フロー

AWKプログラムの実行フローは、テキスト処理タスクのニーズに大きく影響されています。 これを理解するために、最初にホワイトボードに大まかな要約レポートをスケッチする必要があると想像してみましょう。 このタスクを効果的に完了するには、次のプロセスが必要です。

  • マーカーや消しゴムなどの関連アイテムを取得します
  • レポートのタイトルを特定する
  • 元のソースからのすべてのユーザーフィードバックを一度に1つずつ読み取り、その長さにフィルターを適用します
  • 最後に、正のフィードバックの割合などの重要な数値を与えることで要約できます

同様に、AWKプログラムは、必要なユーザー定義関数をロードすることから始まります。 次に、 Begin ブロックに記述されたステートメントを実行することにより、1回限りのセットアップアクティビティを実行します。 その後、 Main ブロックのステートメントが実行され、処理する行がなくなるまで、一度に1行ずつ入力テキストが処理されます。 最後に、 End ブロックからステートメントを実行することにより、1回限りのクリーンアップまたは要約アクティビティを実行します。

これらの各アクティビティはオプションであることに注意する必要があります。 ただし、ほとんどの場合、少なくともメインブロックがあります。 さらに、BeginブロックとEndブロックを必要な数だけ持つことができますが、一般的なスタイルは、それぞれを1つだけ使用することです。

4.4. BEGINおよびENDパターン

これまでに作成した単純なユーザーフィードバックアプリケーションからは、元のファイルの合計コメントのうち、リストされている短いコメントの割合を知ることはできません。 さらに、文脈を設定する可能性のあるタイトルがないため、読みやすさが低下します。

これらのアドオンタスクはどちらも、一度だけ実行する必要があるため、一般的な[pattern][action]シーケンスでは効果的に処理できません。 そのため、タイトルは本体が生成される前に付ける必要があります。 そして、パーセンテージの要約は、すべてのコメントの後に下部に表示されます。

この目的のために、BEGINパターンとENDパターンを使用する必要があります。これらは、次の2つの理由で一意です。

  • これらのパターンが続くすべてのアクションは、1回だけ実行されます
  • これら2つのパターンのアクションをスキップすることはできません

それでは、新しい要件をサポートするために、元のAWKプログラムを拡張しましょう。

BEGIN { print "User Feedback (Only Short Comments)" }
length($0) < 100 { count++; print $0 }
END { print "Percentage of Short Comments:", (count/NR)*100, "%" }

AWKはすべての変数のデフォルトの数値0を解釈するため、変数countを初期化していないことに注意する必要があります。 さらに、組み込み変数NRを使用しました。これにより、これまでに読み取った行数がわかります。

さらに、要件が拡大するにつれて、パターンのアクションとして複数のステートメントを実行する必要があります。 したがって、AWKでは、メインブロックとエンドブロックで行ったように、各ステートメントを別々の行に配置するか、セミコロンで区切る必要があります。

4.5. awkコマンド

これまで、AWK言語で記述されたプログラムの構造とフローを理解することに完全に焦点を当ててきました。 基本をカバーしたので、プログラムの実行を開始し、AWKプログラムの期待される出力と実際の出力を比較することによって概念を固めます。

AWKプログラムを実行するには、awkコマンドを使用する必要があります。

awk [ -F fs ] [ -v var=value ] [ 'prog' | -f progfile ] [ file ...  ]

まず、awkコマンドを使用してユーザーフィードバックアプリケーションの最初のワンライナーバージョンを実行する方法を見てみましょう。

awk 'length($0) < 100' input.txt

インラインプログラムを引用符で囲む必要があることに注意する必要があります。

同じアプローチを使用して長いプログラムを実行することもできますが、それでは読みやすさが失われ、プログラムの編集でエラーが発生しやすくなります。 それでは、プログラムが複数の行にまたがるシナリオにスクリプトベースのアプローチを使用しましょう。

awk -f comments.awk input.txt

以前と同様に、入力ソースは同じテキストファイルinput.txtです。 ただし、今回は、awkコマンドがAWKプログラムを含むスクリプトcomments.awkを実行しています。

5. 検索パターン

高度なテキスト処理のほとんどは、特定のパターンのテキストを検索することを伴うことがよくあります。 これらのユースケースに対応するために、AWKは正規表現を使用して検索パターンを作成することをサポートしています。

5.1. 正規表現

ユーザーフィードバックアプリケーションで、基盤となるサービスに満足していると表明した顧客の割合を概算するための新しい要件を受け取ったとします。

AWKプログラムを使用した概念実証としてこれを行うには、まず、物事を単純に保つためにいくつかの仮定を立てましょう。

  • 短いコメントだけでなく、分析のすべてのコメントを考慮する必要があります
  • 「良い」または「幸せ」という言葉を含むフィードバックコメントは、顧客からのフィードバックに満足していることを示します

次に、次の3つの条件を満たすコメントに一致するように正規表現を作成しましょう。

  • キーワードの左側にあるテキストは、行の先頭または空のスペースのいずれかであるため、(^ | [\\ t] +)と一致します。
  • いずれの場合も2つのキーワードのいずれかが表示される可能性があるため、中央部分は式(([gG] [oO] {2} [dD])|([hH] [aA] [pP] {2 } [yY]))
  • キーワードの右側にあるテキストは、行の終わりまたは空のスペースのいずれかであるため、($ | [\\ t] +)と一致します。

最後に、 pattern という変数に格納することで、すべてをまとめる準備が整いました。

pattern="(^|[\\t ]+)(([gG][oO]{2}[dD])|([hH][aA][pP]{2}[yY]))($|[\\t ]+)"

ふぅ! 表情全体を一度に見るのは大変です。 しかし、私たちを助けたのは、テキスト全体を小さなチャンクに分割する分割統治法を使用したことでした。これにより、単純な正規表現を簡単に作成できました。 その後、それらを1つの正規表現に結合しました。

5.2. チルダ演算子

関連するコメントと一致させるために便利な正規表現ができたので、チルダ演算子〜を使用して、レコードと一致させましょう

BEGIN {
    print "Positive Feedback Comments:"
    pattern="(^|[\\t ]+)(([gG][oO]{2}[dD])|([hH][aA][pP]{2}[yY]))($|[\\t ]+)"
}
$0 ~ pattern {
    count++
    print $0
}

END {
    print "Percentage:", (count/NR)*100, "%"
}

〜は二項演算子であることに注意する必要があるため、正規表現を $0と照合することを明示的に示しました。 または、正規表現を2つのスラッシュ /(^ | [\\ t] +)(([gG] [ oO] {2} [dD])|([hH] [aA] [pP] {2} [yY]))($ | [\\ t] +)/ $ 0

6. 高度なテキスト処理

ここまでは順調ですね。 ユーザーフィードバックアプリケーションを拡張して、ポジティブなユーザーフィードバックのストリークを報告するというもう少し難しい問題を解決する方法を見てみましょう。

6.1. 複数の行にわたるデータの追跡

ユーザーからの肯定的なフィードバックにより、「良い」または「幸せ」というキーワードを含むユーザーコメントを参照します。 一方、否定的なユーザーフィードバックは、「悪い」または「不幸」というキーワードを含むユーザーコメントです。 他のすべてのユーザーコメントは中立と見なされます。

現在、正のフィードバックストリークは、正のコメントで始まり、入力ソースに負のユーザーフィードバックが表示されるまで続く一連のコメントです。 逆に、ネガティブフィードバックストリークはネガティブコメントで始まり、ポジティブコメントで終わります。 いずれの場合も、この演習では、 ニュートラルなコメントはストリークを終わらせることはできません。 代わりに、それはそれを通るストリークの長さに寄与します。 この概念を明確にするために、サンプル入力を見てみましょう。

I'm happy with this service. Keep it up.
ok to use
Please expand your services to Canada. We'll benefit from your good work.
Terribly bad
When are you coming up in Dubai?
We need more of such good services in India
You guys know how to make your customers happy

次に、サンプルテキストのすべてのポジティブストリークとネガティブストリークを見つけましょう。

  • 4行目に否定的なコメントがあるため、正のストリークは1行目から始まり、3行目で終わります。
  • 6行目に肯定的なコメントがあるため、負のストリークは4行目で始まり、5行目で終わります。
  • 正のストリークは6行目から7行目までです

最初のポジティブストリークには、2行目のニュートラルコメントが含まれていることに注意する必要があります。 同様に、5行目のニュートラルなコメントは、ネガティブなストリークの一因となっています。

6.2. 配列

正のフィードバックストリークを印刷するソリューションの一部として、ストリークの開始点とその長さを表示する必要があります。 当然、これを行う1つの方法は、最初の肯定的なコメントが発生したレコードの開始位置と対応する長さの間のマッピングを維持できるメカニズムを使用することです。

この目的のために、AWKは連想配列の機能を提供して(キー、値)ペアを格納します。ここで、キーと値は文字列または数値データ型のいずれかです。 私たちの仕事が解決策にたどり着くのを容易にするいくつかの連想配列を定義しましょう。

まず、処理中の現在の正のフィードバックストリークの開始インデックスと終了インデックスを追跡する必要があります。 したがって、これらの値をそれぞれ cur_streak [“ start”]およびcur_streak [“ end”]で参照してみましょう。

次に、 len_streak という名前の配列を使用して、ストリークの開始レコード番号とその長さの間のマッピングを追跡しましょう。 したがって、ストリークがi th レコードで始まる場合、 len_streak[i]はその長さを示します。

6.3. ユーザー定義関数

他の多くのプログラミング言語と同様に、AWKは関数の使用を通じてコードの再利用性とモジュール性をサポートします。 それでは、 reset_streak()というヘルパー関数を記述して、現在のストリークに関連付けられているパラメーターを初期化またはリセットすることから始めましょう。

function reset_streak(streak) {
    streak["start"] = -1
    streak["len_so_far"] = 0
}

最終的には、各ストリークの長さと開始行番号を印刷する必要があります。 それでは、配列のキーと値のペア arr を反復処理する、 print_streaks_info()という別の関数を作成しましょう。

function print_streaks_info(arr, index) {
    for(index in arr) {
        print index, arr[index]
    }
}

すべての変数はAWKのグローバルスコープを持っていることに注意する必要があります。 そのため、実際の関数の引数arrを2番目のパラメーターindexから分離するために、パラメーターリストにスペースを追加するという一般的な規則に従いました。 さらに、変数 index はローカル変数として効果的に機能し、関数の呼び出し時に引数として渡す必要はありません。

6.4. 範囲パターン

ポジティブなユーザーフィードバックストリークをよく見ると、テキストがポジティブなトーンでキーワードと一致したときに始まります。 さらに、テキストが否定的なトーンでキーワードと一致すると終了します。

レポートのタイトルを印刷することから始めましょう。次に、パターンマッチングとパラメーターの正規表現を初期化して、BEGINブロックの現在のストリークの進行状況を追跡します。

BEGIN {
    print "Positive Feedback Streaks:"
    positive_pattern="(^|[\\t ]+)(([gG][oO]{2}[dD])|([hH][aA][pP]{2}[yY]))($|[\\t ]+)"
    negative_pattern="(^|[\\t ]+)(([bB][aA][dD])|([uU][nN][hH][aA][pP]{2}[yY]))($|[\\t ]+)"
    reset_streak(cur_streak)
}

最初は、そのようなシナリオを処理するのは少し難しいように見えますが、AWKには救助計画があります。 これに効率的に取り組むために、AWKの範囲パターンを使用して、最初の行が最初のパターンbegin_patternと一致し、最後の行が2番目のパターンend_pattern:と一致するように、連続する行の範囲を追跡できます。

$0 ~ begin_pattern, $0 ~ end_pattern

これらの線を考えると、正のストリークは、範囲パターンで識別可能な閉開区間であることがわかります。

$0 ~ positive_pattern, $0 ~ negative_pattern

同様に、これらのパターンの位置を逆にすると、ネガティブストリークをクローズドオープンインターバルとして識別できます。

$0 ~ negative_pattern, $0 ~ positive_pattern

最も重要なことは、どちらの場合も、ストリークの終わりは、範囲パターンによって選択された範囲の最後から2番目の行と同じであることに注意する必要があります。 これは、各範囲パターンの2番目の部分の役割が最初の行の決定に限定されているためです。その後、そのストリークは継続できなくなります。 その結果、入力テキストを一連の正と負のフィードバックストリークとして視覚化できます。

6.5. メインブロック

これまでのところ、パターンの準備ができています。 それでは、2つの重要な洞察を使用して、コアアクションステートメントを考え出すのに役立てましょう。

  • 2つの交互の範囲は、正と負のストリーク間の境界を共有します
  • 境界を表す線は、両方の範囲パターンに一致します

まず、各レコードの cur_line_visited_count の値を0に初期化して、後で境界線を識別するために使用できるようにします。

{
    cur_line_visited_count = 0
}

次に、現在のストリークの開始が設定されていない場合は、現在のレコード番号NRに設定できます。 さらに、正のストリークに対応する範囲パターンに一致するため、cur_line_visited_countおよびcur_streak[“ len_so_far”]をインクリメントする必要があります。

$0 ~ positive_pattern, $0 ~ negative_pattern {
    cur_line_visited_count++
    if(cur_streak["start"] == -1) {
        cur_streak["start"] = NR
    }
    cur_streak["len_so_far"]++
}

最後に、ネガティブストリークを参照する範囲パターンに対応するアクションを記述しましょう。 そのため、現在の行が2回一致していることがわかると、それが共有境界であることがわかります。 したがって、進行中のストリークがあるかどうかに応じて、現在のストリークを終了するか、現在のラインNRで新しいポジティブストリークを開始できます。

$0 ~ negative_pattern, $0 ~ positive_pattern {
    cur_line_visited_count++
    if(cur_line_visited_count == 2) {
        if(cur_streak["start"] != -1 && cur_streak["start"] < NR) {
            len_streak[cur_streak["start"]] = cur_streak["len_so_far"] - 1
            reset_streak(cur_streak)
        } else {
            cur_streak["start"] = NR
        }
    }
}

len_streak 配列に現在のストリークの長さを保存する一方で、最後の行が含まれないように、その値を1つ減らしたことに注意する必要があります。

6.6. エンドブロック

最後の正のストリークの例外として、対応する負のフィードバックストリークが存在しない場合があります。 したがって、すべての縞を印刷する前に、Endブロックでこのエッジケースを処理する必要があります。

END {
    if(cur_streak["end"] == -1 ){
        len_streak[cur_streak["start"]] = NR - cur_streak["start"] + 1
    }
    print_streaks_info(len_streak)
}

終了ブロックで変数NRを使用すると、読み取ったファイルの最後のレコードのインデックスが常に提供されることに注意する必要があります。

6.7. AWKスクリプト

プログラムのすべての部分が完了したので、AWKスクリプト全体を見てみましょう。

function print_streaks_info(arr, index) {
    for(index in arr) {
        print "starting index: " index, ", length of streak: " arr[index]
    }
}

function reset_streak(streak) {
    streak["start"] = -1
    streak["len_so_far"] = 0
}

BEGIN { 
    print "Positive Feedback Streaks:"
    positive_pattern="(^|[\\t ]+)(([gG][oO]{2}[dD])|([hH][aA][pP]{2}[yY]))($|[\\t ]+)"
    negative_pattern="(^|[\\t ]+)(([bB][aA][dD])|([uU][nN][hH][aA][pP]{2}[yY]))($|[\\t ]+)"
    reset_streak(cur_streak)
}
{
    cur_line_visited_count = 0 
}

$0 ~ positive_pattern, $0 ~ negative_pattern {
    if(cur_streak["start"] == -1) {
        cur_streak["start"] = NR
    }
    cur_line_visited_count++
    cur_streak["len_so_far"]++
}

$0 ~ negative_pattern, $0 ~ positive_pattern {
    cur_line_visited_count++
    if(cur_line_visited_count == 2) {
        if(cur_streak["start"] != -1 && cur_streak["start"] < NR) {
            len_streak[cur_streak["start"]] = cur_streak["len_so_far"] - 1
            reset_streak(cur_streak)
        } else {
            cur_streak["start"] = NR 
        }
    }
}

END { 
    if(cur_streak["start"] != -1) {
        len_streak[cur_streak["start"]] = NR - cur_streak["start"] + 1
    }
    print_streaks_info(len_streak)
}

最後に、このAWKスクリプトを使用してサンプル入力を処理しましょう。

$ awk -f streak_script.awk input_comments.txt

もちろん、結果が以前の分析と一致することを確認できます。

Positive Feedback Streaks:
starting index: 6 , length of streak: 2
starting index: 1 , length of streak: 3

7. 結論

このチュートリアルでは、AWKプログラミング言語の基本的な構成要素の理解を深めることにより、やる気を起こさせる滑走路を設定しました。 これで、作業中に発生するテキスト処理の問題を解決するAWKプログラムを作成する準備が整いました。