第8章 テキストストリームのフィルタ処理
LPIC-1 103.2 / 103.7 相当
ログやCSVのようなテキストデータを、コマンドを組み合わせて自在に加工する方法を学びます。パイプと組み合わせることで真価を発揮します。
よく使うフィルタコマンド
| コマンド | 役割 |
|---|---|
cut | 指定した区切り文字やカラム位置でテキストを切り出す |
sort | 行を並び替える |
uniq | 連続する重複行をまとめる(事前にsortが必要なことが多い) |
tr | 文字を別の文字に変換・削除する |
wc | 行数・単語数・文字数を数える |
$ cut -d ':' -f 1 /etc/passwd | head -3 # ':'区切りの1列目(ユーザー名)だけ抜き出す
root
daemon
bin
$ sort names.txt | uniq # 並び替えてから重複行をまとめる
alice
bob
carol
$ sort names.txt | uniq -c # 出現回数付きでまとめる
2 alice
1 bob
3 carol
この行のこの値に注目: uniq -cの各行先頭の数字が、その行が元のファイルに何回登場したかを表す出現回数です。この例ではcarolが3回で最多だったことが分かります。
sed — ストリームエディタ
sed は、ファイルを開かずにテキストの置換や削除をその場(ストリーム上)で行います。もっとも基本的な使い方は置換コマンド s/対象/置換後/ です。
$ cat file.txt
foo foo baz
$ sed 's/foo/bar/' file.txt # 各行の最初のfooをbarに置換(結果を表示するだけ)
bar foo baz
$ sed 's/foo/bar/g' file.txt # g(global)で行内のすべてを置換
bar bar baz
$ sed -i 's/foo/bar/g' file.txt # -i でファイル自体を書き換える
$ cat file.txt
bar bar baz
この行のこの値に注目: gを付けない場合は各行の最初の一致だけ(1つ目のfoo)が置換され、2つ目のfooはそのまま残っています。-iを付けた最後の実行だけがfile.txt自体を書き換えるため、直後にcatで確認すると変更が永続化されていることが分かります。
-iはファイルを直接書き換えるため、実行前の内容には戻せません。重要な設定ファイルなどを編集する前は、cp file.txt file.txt.bakのようにバックアップを取っておくと安全です。
awk — 列指向のテキスト処理
awk は行を自動的に列(フィールド)へ分割し、$1、$2...としてアクセスできる、より高機能なテキスト処理ツールです。
$ awk '{print $1}' access.log # 各行の1列目だけを表示
203.0.113.10
203.0.113.11
198.51.100.7
$ awk -F ':' '{print $1}' /etc/passwd # -F で区切り文字を指定
root
daemon
bin
この行のこの値に注目: awkは既定では空白(スペース・タブ)を区切りにフィールドを分割するため、access.logのようなスペース区切りのログには-Fを付けなくても1列目を取り出せます。一方/etc/passwdはコロン区切りのため、-F ':'で区切り文字を明示しないと、1行全体が$1として扱われてしまいます。
cat memo.txt | grep コーヒー のように、複数のフィルタをパイプでつないで結果がどう変わるか試してみましょう(簡易シミュレーターのため sed / awk 自体は未対応です)。
正規表現の基礎
grep・sed・awkはいずれも「正規表現」でパターンを指定できます。基本正規表現(BRE)と拡張正規表現(ERE)があり、grepでは-E(またはegrep)を付けるとEREが使えます。
| 記号 | 意味 |
|---|---|
. | 任意の1文字 |
* | 直前の文字の0回以上の繰り返し |
^ | 行頭 |
$ | 行末 |
[0-9] | 0から9のいずれか1文字 |
+(ERE) | 直前の文字の1回以上の繰り返し |
(a|b)(ERE) | aまたはb |
$ grep '^root' /etc/passwd # 行頭がrootの行
root:x:0:0:root:/root:/bin/bash
$ grep -E '[0-9]+' access.log # 数字が1回以上連続する箇所を含む行(ERE)
203.0.113.10 - - [21/Aug/2026:10:02:11 +0900] "GET /index.html HTTP/1.1" 200 1024
...(IPアドレスやステータスコードなど数字を含む行がほぼすべて一致する)
$ grep -v '^#' /etc/ssh/sshd_config # -v: マッチしない行だけを表示(コメント行を除外)
Port 22
PermitRootLogin no
PasswordAuthentication no
$ grep -c error app.log # -c: マッチした行数だけを表示
1
この行のこの値に注目: grep -v '^#'の結果には、#で始まるコメント行や説明文が一切含まれておらず、実際に有効な設定行だけが残ります。設定ファイルの「実質的な設定内容」だけを素早く確認したいときによく使われるテクニックです。
BRE(基本正規表現)とERE(拡張正規表現)の違い
Linuxの正規表現には、大きく分けてBRE(Basic Regular Expression、基本正規表現)とERE(Extended Regular Expression、拡張正規表現)の2つの方言があります。両者の記号自体はほとんど共通していますが、一部の特殊文字を「そのままの記号として使うか」「バックスラッシュを付けて特別な意味にするか」が逆転しているため、混同しやすいポイントです。
| 意味 | BRE(既定のgrep・sedなど) | ERE(grep -E・egrep) |
|---|---|---|
| グループ化 | \( \) | ( ) |
| 繰り返し回数の指定 | \{ \} | { } |
| 1回以上の繰り返し | \+(拡張として使えることが多い) | + |
| 0回または1回 | \?(同上) | ? |
| 選択(OR) | \|(同上) | | |
つまりBREでは、(や{のような文字は「ただの文字」として扱われ、特別な意味を持たせたい場合は\(や\{のようにバックスラッシュでエスケープする必要があります。EREではこの関係が逆転し、素の(や{がそのまま特別な意味を持ちます。
grep 'a(b|c)d' fileのように書いても、(や|はただの文字として扱われるため、意図した「aの後にbまたはc、その後にd」という検索にはなりません。ERE(grep -E)を使うか、BREのまま\(・\|・\)とエスケープする必要があります。
POSIX文字クラス
[a-z]のような範囲指定の代わりに、環境(ロケール)に依存しにくい書き方としてPOSIX文字クラスが用意されています。[[ ]]のように二重の角括弧で囲んで使います。
| POSIX文字クラス | 意味 |
|---|---|
[:alpha:] | アルファベット |
[:digit:] | 数字 |
[:alnum:] | 英数字 |
[:space:] | 空白文字(スペース・タブ・改行など) |
[:upper:] | 大文字 |
[:lower:] | 小文字 |
[:punct:] | 記号(句読点など) |
$ grep -E '^[[:digit:]]+$' file.txt # 行全体が数字だけの行を検索
アンカー(位置を指定する記号)
| アンカー | 意味 |
|---|---|
^ | 行頭 |
$ | 行末 |
\< / \> | 単語の始まり/終わり(GNU拡張) |
\b | 単語の境界(GNU拡張。\<・\>をまとめて表現できる) |
$ grep '\bcat\b' file.txt # "cat"という単語単体にマッチ("category"などには一致しない)
貪欲マッチと最短マッチ
正規表現の*や+は、既定では可能な限り長く一致しようとします。これを貪欲マッチ(greedy match)と呼びます。
$ echo '<a><b>' | grep -oE '<.*>'
<a><b>
「<a>だけにマッチしてほしかった」と思っても、.*は最初の<から最後の>までを一気に一致させてしまいます。POSIX準拠のgrep・sed・awkの正規表現エンジンには、Perlのような「最短マッチ」を明示的に指定するモディファイア(*?など)は基本的にありません。最短マッチが必要な場合は、貪欲にならないパターン(<[^>]*>のように、対象外の文字を明示的に除外する書き方)を使うか、後述のgrep -Pで対応することになります。
grep -E / -F / -P の違い
| オプション | 意味 |
|---|---|
-E | 拡張正規表現(ERE)として解釈する(egrepと同等) |
-F | 正規表現ではなく、単純な文字列として検索する(fgrepと同等。特殊文字が多い文字列を検索する際に高速かつ安全) |
-P | Perl互換正規表現(PCRE)として解釈する。最短マッチや先読みなど、より高度な機能が使えるが、環境によっては利用できないこともある |
.や*のような正規表現の特殊文字がたくさん含まれる場合(バージョン番号やURLなど)、うっかりそれらを正規表現として解釈されてしまうと意図しないマッチが発生します。「正規表現として解釈する必要が無い」と分かっている検索では、-Fを使うと安全で、処理も高速になります。
sedとawkでの正規表現の使い方の違い
sedは既定でBREを使います(sed -Eやsed -rでEREに切り替え可能)。一方awkは、多くの実装でERE相当の正規表現をそのまま使えます。
$ sed -E 's/(foo|bar)/baz/' file.txt # sedでEREのグループ・選択を使う場合は -E が必要
$ awk '/^[0-9]+$/ {print}' file.txt # awkでは素の( ) や + がそのまま拡張正規表現として使える
「同じ()や+という記号でも、コマンドや付けるオプションによって解釈が変わる」という点は、LPIC試験でも実務でも混乱しやすいポイントです。迷ったときは、まず対象コマンドのmanページで既定がBRE/EREのどちらかを確認する習慣をつけると安全です。
regex(7)マニュアルページの読み方
正規表現そのものの詳しい仕様は、特定のコマンドのマニュアルではなく、man 7 regexで参照できます。セクション番号7は「その他(マクロパッケージや規約など)」を意味し、regex(7)という表記は「セクション7の"regex"という項目」を指します。BRE・EREそれぞれの正確な文法や、POSIX文字クラスの一覧など、コマンド個別のマニュアルには載っていない正規表現自体の仕様を確認したいときに参照します。
$ man 7 regex
実務でよく使うパターン集とその限界
| 用途 | パターン例(ERE) | 限界・注意点 |
|---|---|---|
| IPv4アドレス(簡易版) | ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ | 各オクテットが0〜255の範囲であることまでは検証できず、"999.999.999.999"のような無効な値にも一致してしまう |
| メールアドレス(簡易版) | ^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ | RFC準拠の厳密なメールアドレス検証には遠く及ばない、あくまで簡易的なチェック用 |
| 日付(YYYY-MM-DD) | ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ | 桁数の形だけを見ており、"2026-13-99"のような実在しない日付も通してしまう |
| ログのタイムスタンプ抽出 | ^[A-Z][a-z]{2} [0-9]{1,2} [0-9]{2}:[0-9]{2}:[0-9]{2} | syslog形式(例:Aug 21 07:02:11)を想定した例で、フォーマットが異なるログには当然一致しない |
sortの主なオプション
| オプション | 意味 |
|---|---|
-n | 文字列ではなく数値として比較して並び替え |
-r | 逆順(降順)に並び替え |
-k N | N番目のフィールドを基準に並び替え |
-u | 並び替えた上で重複行を除去(uniqと組み合わせなくてよい) |
$ sort -t: -k3 -n /etc/passwd | head -3 # ':'区切りの3列目(UID)を数値として昇順ソート
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
この行のこの値に注目: UID(3列目)が0・1・2という小さい番号順に並んでおり、これらはシステム用にあらかじめ予約されたアカウントです。一般ユーザーのUIDは通常1000番以降から割り当てられるため、このソート結果を見れば「システムアカウントが先頭に集まっている」ことを確認できます。
diff — 2つのファイルの差分を見る
設定ファイルを変更する前にバックアップと比較したい場合など、2つのファイルの違いを調べるにはdiffを使います。
$ diff old.conf new.conf
3c3
< timeout = 30
---
> timeout = 60
$ diff -u old.conf new.conf # -u: unified形式。パッチファイルとしてもよく使われる読みやすい形式
--- old.conf 2026-08-15 09:00:00
+++ new.conf 2026-08-20 14:30:00
@@ -1,4 +1,4 @@
host = example.com
port = 8080
-timeout = 30
+timeout = 60
この行のこの値に注目: unified形式では、変更のない行は先頭が半角スペース、削除された行は-、追加された行は+で示されます。@@ -1,4 +1,4 @@は「old.confの1行目から4行、new.confの1行目から4行」を表示している、という差分の位置情報です。
<で始まる行が1つ目のファイル(old.conf)側の内容、>で始まる行が2つ目のファイル(new.conf)側の内容を表します。
join — 共通の列を持つ2つのファイルを結合する
joinは、あらかじめソートされた2つのファイルを、共通のキー列を基準にデータベースのJOINのように結合します。
$ cat users.txt
1 alice
2 bob
$ cat depts.txt
1 sales
2 dev
$ join users.txt depts.txt
1 alice sales
2 bob dev
sedのその他の使い方
置換以外にも、行の削除や範囲指定など、sedにはさまざまな使い方があります。
$ cat file.txt
1行目
2行目
3行目
# コメント行
4行目
$ sed '2d' file.txt # 2行目を削除して表示
1行目
3行目
# コメント行
4行目
$ sed '2,4d' file.txt # 2〜4行目を削除して表示
1行目
4行目
$ sed -n '2,4p' file.txt # -n(自動出力を抑制)と p(表示)を組み合わせ、2〜4行目だけを表示
2行目
3行目
# コメント行
$ sed '/^#/d' file.txt # 正規表現にマッチする行(先頭が#の行)を削除
1行目
2行目
3行目
4行目
この行のこの値に注目: いずれの実行でも元のfile.txt自体は変更されておらず(-iを付けていないため)、画面に表示される結果だけが変わります。sed '2,4d'とsed -n '2,4p'は同じ範囲指定(2,4)を使いながら、削除(d)と表示(p)という逆の動作をしている点を見比べると理解しやすくなります。
awkのフィールド変数とパターン
awkには、現在の行番号を表すNRや、フィールド(列)の総数を表すNFなど、あらかじめ用意された特殊変数があります。また、条件(パターン)を指定してマッチした行だけを処理することもできます。
$ awk '{print NR, $0}' file.txt # 各行の先頭に行番号を付けて表示
1 1行目
2 2行目
3 3行目
$ awk -F: '{print $1, NF}' /etc/passwd # ユーザー名と、その行の列数を表示
root 7
daemon 7
$ awk -F: '$3 >= 1000 {print $1}' /etc/passwd # UID(3列目)が1000以上の行だけユーザー名を表示
alice
bob
この行のこの値に注目: NFの値7は、/etc/passwdの各行がコロン区切りで7列(ユーザー名・パスワード・UID・GID・コメント・ホーム・シェル)から成ることを示しています。最後の例では、UIDが1000未満のシステムアカウント(root・daemonなど)は除外され、一般ユーザーのalice・bobだけが表示されます。
trの活用例
trは1文字ずつの変換や削除、圧縮(連続する文字をまとめる)ができます。
$ echo "Hello World" | tr 'a-z' 'A-Z' # 小文字を大文字に変換
HELLO WORLD
$ echo "a b c" | tr -s ' ' # -s: 連続する空白を1文字に圧縮
a b c
$ cat file.txt | tr -d '\r' # -d: 指定文字を削除(Windows改行コードの除去などに使われる)
(file.txtにCRLF改行が含まれていた場合、\r(CR)だけが取り除かれ、通常のLF改行のテキストに変換される。見た目上の変化はないが、od -c などでバイト単位に見ると\rが消えていることが確認できる)
\r\n(CRLF)が入っていることがあり、Linux上のスクリプトで予期しないエラーの原因になります。tr -d '\r'や、専用ツールのdos2unixで改行コードを統一するのは定番のトラブル対処法です。
複数のフィルタを組み合わせた集計の例
本章で紹介したコマンドは、単体でも便利ですが、組み合わせることで真価を発揮します。たとえばアクセスログから「どのIPアドレスが最もアクセス数が多いか」を調べたい場合、次のような1行で集計できます。
$ awk '{print $1}' access.log | sort | uniq -c | sort -nr | head -n 5
# 1列目(IPアドレス)を抜き出し、並べ替えて重複をカウントし、多い順に上位5件を表示
482 203.0.113.10
317 198.51.100.7
203 203.0.113.11
98 198.51.100.22
54 203.0.113.15
この行のこの値に注目: 各行の先頭の数字がアクセス回数で、sort -nrにより多い順に並んでいます。この例では203.0.113.10からのアクセスが482回と突出しており、特定のIPアドレスからの異常な集中アクセス(DoS的な挙動やクローラーなど)を見つける調査の第一歩としてよく使われる集計です。
このように「抜き出す(awk/cut)→整列する(sort)→集計する(uniq -c)→並べ替える(sort -nr)→絞り込む(head)」という流れは、ログ解析の現場で非常によく使われる定石です。一度にすべてを書こうとせず、パイプの前半だけを実行して結果を確認しながら、1段ずつ追加して組み立てていくとミスに気づきやすくなります。
sedとawkの使い分けの目安
sedとawkはどちらもテキスト処理に使われますが、得意分野が異なります。sedは「行単位の単純な置換・削除」に強く、1行で完結する軽い加工に向いています。一方awkは「列(フィールド)を意識した集計・条件分岐」を伴う、やや複雑な処理に向いています。目安として、「文字列を置き換えたいだけ」ならsed、「特定の列の値をもとに計算・絞り込みをしたい」ならawk、と使い分けると迷いにくくなります。両者を無理に1つにまとめようとせず、パイプでつなげて使うのが実践的なアプローチです。