【シェル芸人への道】grepを訪ねて

# **そのコマンド、grep**

Linuxのお勉強、CUIの使い方を勉強する際、かなり初期に出てくるであろう `grep` 。
私は `ls`, `cd` に次いで3番目のコマンドとして `grep` を学んだ記憶があります。

日頃からお世話になっている `grep` ですが、今日は改めてこのコマンドの奥深さを学んでみたいと思います。
環境はUbuntu 16.04にくっついてた `GNU grep 2.25` です。

```bash:バージョン
root@ubuntu16:~# cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=16.04
DISTRIB_CODENAME=xenial
DISTRIB_DESCRIPTION="Ubuntu 16.04.2 LTS"

root@ubuntu16:~# grep --version
grep (GNU grep) 2.25
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Written by Mike Haertel and others, see <http://git.sv.gnu.org/cgit/grep.git/tree/AUTHORS>.
```

# **grepとは**

さて、まずは `grep` とは一体何かを改めて調べてみます。
Wikipediaによれば、

> grep 【名詞】
> UNIXおよびUnixオペレーティングシステムにおけるコマンド。テキストファイル中から、正規表現に一致する行を検索して出力する。

と、まぁそうだよねというようなことが書いてありますが、その少し後に

> grep の名の由来は、ラインエディタedのコマンド g/re/p であり、その意味するところは「ファイル全体から/正規表現に一致する行を/表示する」である。

ということが書いてあります。
「ググる」と同じように「 `grep` する」なんて言葉が普通に通じてしまいますが、元々の由来はこういうことだったようです。
UNIX界隈では置換などの際に `g (global)` やら `p (print)` やら、また一緒に正規表現 `re(regular expression)` を使ったりしますが、その所作をまさしく表したコマンド名と言えます。
たいへん理にかなったコマンド名ですね。

# **grepの一族**

さて、人間生きていると `grep` そのものの他に `egrep` やら `fgrep` など、よく似た名前のコマンドを見たり、聞いたりすることがあります。
一体どんなものがあるのでしょうか。調べてみましょう。

```bash
root@ubuntu16:~# compgen -c | sort | uniq | grep 'grep'
bzegrep
bzfgrep
bzgrep
egrep
fgrep
grep
lzegrep
lzfgrep
lzgrep
pgrep
ptargrep
rgrep
xzegrep
xzfgrep
xzgrep
zegrep
zfgrep
zgrep

root@ubuntu16:~# compgen -c | sort | uniq | grep 'grep' | wc -l
18
```

なんということでしょう、18個も `grep` を名乗るコマンドがあるではありませんか。
しかし大変なのはそこではありません。

「 **`grep`** を調べるために **`grep`** を使ってしまっている」

そう、これです。なんて罪深いのでしょう。
シェル芸人たるもの別解を探さねばなりません。やってみましょう。

```bash:awkでgrep(awkrep)
root@ubuntu16:~# compgen -c | sort | uniq | awk '/grep/{print}' | wc -l
18
```

```bash:sedでgrep(sedrep)
root@ubuntu16:~# compgen -c | sort | uniq | sed -n '/grep/p' | wc -l
18
```

```bash:powerでgrep(powerep)
root@ubuntu16:~# compgen -c | sort | uniq | while read LINE ; do if [[ "${LINE}" =~ grep ]] ; then echo ${LINE} ; fi ; done | wc -l
18
```

とりあえず3つあれば大丈夫(根拠不明)でしょう...。

## **一族を並べてみる**

いっぱいあるので並べかえてみましょう。

```bash
root@ubuntu16:~# compgen -c | sort | uniq | grep 'grep'
bzegrep
bzfgrep
bzgrep
egrep
fgrep
grep
lzegrep
lzfgrep
lzgrep
pgrep
ptargrep
rgrep
xzegrep
xzfgrep
xzgrep
zegrep
zfgrep
zgrep
```

たしかに並んではいるのですが、文字数が色々なのでイマイチよくわかりません。
ですので、「文字数」を「辞書式」に優先して並べてみましょう。

```bash
root@ubuntu16:~# for CMD in $(compgen -c | sort | uniq | grep 'grep') ; do echo ${#CMD}: ${CMD} ; done | sort
4: grep
5: egrep
5: fgrep
5: pgrep
5: rgrep
5: zgrep
6: bzgrep
6: lzgrep
6: xzgrep
6: zegrep
6: zfgrep
7: bzegrep
7: bzfgrep
7: lzegrep
7: lzfgrep
7: xzegrep
7: xzfgrep
8: ptargrep
```

`${#変数名}` で変数に入っている **文字数のカウント** ができる。たいへんに便利ですね。

### **基本形**

さて、こいつらをよく見てみると基本的には「 **○○のgrep** 」みたいなネーミングになっています。
まずは基本となる `grep` に一文字を加えたものを見てみましょう。

|コマンド名|由来|意味|grepでの相当オプション|備考|
|--:|:--|:--|:--:|:--|
|grep|-|grep|-|基本形。|
|egrep|**e**xtended-regexp **grep**|拡張正規表現に対応したgrep|-E||
|fgrep|**f**ixed-strings **grep**|固定文字列のみに対応したgrep|-F|faster grepかと思っていた時期が私にもありました。|
|pgrep|**p**rocess **grep**|プロセス一覧に対するgrep|-| `grep -P` と同じだと思った？別物だよ。|
|rgrep|**r**ecursive **grep**|再帰的なgrep|-r| `grep -R` はまたちょっと違う。|

### **派生形**

上記のもの、特に `grep`, `egrep`, `fgrep` の派生形となっているのが以下です。
基本的には圧縮ファイルのままにgrepできるというものですね。

|接頭辞|由来|意味|対象|備考|
|--:|:--|:--|:--:|:--|
|z|g<wbr/>**z**<wbr/>ip|gzip圧縮ファイルに対するgrep|zgrep, zegrep, zfgrep| `grep -z` はまた別物。ごくまれに使う。|
|bz|**bz**ip2|bzip2圧縮ファイルに対するgrep|bzgrep, bzegrep, bzfgrep|使ったことがない。|
|lz|**lz**ma|lzma圧縮ファイルに対するgrep|lzgrep, lzegrep, lzfgrep|使ったことがない。|
|xz|**xz**|xz圧縮ファイルに対するgrep|xzgrep, xzegrep, xzfgrep|使ったことがない。|
|ptar|**p**erl-regexp **tar**|tar圧縮ファイルに対するperl正規表現でのgrep|ptargrep| `pgrep` は他人です。|

# **実践grep**

それでは実践です。
実践にあたっては適当なテキストファイル、そう、教材が必要です。
以下のサイトに敬礼しつつ、日本語と英語で好きな教材を探しましょう。

- **[青空文庫](http://www.aozora.gr.jp/)**
    - 1997年設立の電子図書館
    - 運営は **[ボランティア](http://www.aozora.gr.jp/aozora_bunkono_shikumi.html)** であり、 **[aozorahack](http://aozorahack.org/)** というサーバ運用改善活動もある
        - **[aozorahackのこれまでとサーバ移転について](https://www.slideshare.net/takahashim/aozorahack-hackathon-1)**
- **[Project Gutenberg](http://www.gutenberg.org/)**
    - 1971年設立の電子図書館

## **教材の準備**

今回は「 **[こころ(夏目漱石)](http://www.aozora.gr.jp/cards/000148/card773.html)** 」と「 **[不思議の国のアリス(ルイス・キャロル)](http://www.gutenberg.org/ebooks/35688)** 」を教材に選んでみます。

```bash
root@ubuntu16:~# curl -s -o kokoro.zip http://www.aozora.gr.jp/cards/000148/files/773_ruby_5968.zip
root@ubuntu16:~# curl -s -o alice.zip http://www.gutenberg.org/files/35688/35688.zip
root@ubuntu16:~# ls -lh
total 196K
-rw-r--r--  1 root root  39K Jul  9 16:00 alice.zip
-rw-r--r--  1 root root 151K Jul  9 15:59 kokoro.zip
```

それでは解凍しましょう。 `unzip` がなかったら `apt-get install unzip` でもしてください。

```bash
root@ubuntu16:~# unzip kokoro.zip
Archive:  kokoro.zip
Made with MacWinZipper?
  inflating: kokoro.txt

root@ubuntu16:~# unzip alice.zip
Archive:  alice.zip
  inflating: 35688.txt

# ファイル名を変えておく
root@ubuntu16:~# mv 35688.txt alice.txt
```

この時点でこころのファイルに一抹の不安が頭をよぎります。
おそるおそるファイルの中身を確認してみましょう。

```bash
root@ubuntu16:~# head kokoro.txt
ｱｱ・
        ??

-------------------------------------------------------
yeLXg?ｻ・
??｢ﾄz    L

stF
   r
i疔s?ｭｵtﾍ

bF
  r?tｭｶn?頏R?・
                L

root@ubuntu16:~# head alice.txt
The Project Gutenberg EBook of Alice in Wonderland, by Alice Gerstenberg

This eBook is for the use of anyone anywhere at no cost and with
almost no restrictions whatsoever.  You may copy it, give it away or
re-use it under the terms of the Project Gutenberg License included
with this eBook or online at www.gutenberg.net


Title: Alice in Wonderland
       A Dramatization of Lewis Carroll's "Alice's Adventures in
```

ふぅ、奴が出たようですね。念のため確認してみましょう。たすけて `nkf` えもん。

```bash
root@ubuntu16:~# apt-get install nkf

root@ubuntu16:~# nkf -g kokoro.txt
Shift_JIS
```

予想通りですね。彼には生まれ変わってもらいましょう。
`nkf -w` で **w**orld-wide なUTF-8への変換ができます。

```bash
root@ubuntu16:~# nkf -w --overwrite kokoro.txt

root@ubuntu16:~# head kokoro.txt
こころ
夏目漱石

-------------------------------------------------------
【テキスト中に現れる記号について】

《》：ルビ
（例）私《わたくし》は

｜：ルビの付く文字列の始まりを特定する記号

root@ubuntu16:~# nkf -g kokoro.txt
UTF-8
```

これで教材の準備が整いました。
あとは順にオプションを調べていってみましょう。

どれくらいくらいあるのかというと...

```bash
root@ubuntu16:~# grep --help | head
Usage: grep [OPTION]... PATTERN [FILE]...
Search for PATTERN in each FILE or standard input.
PATTERN is, by default, a basic regular expression (BRE).
Example: grep -i 'hello world' menu.h main.c

Regexp selection and interpretation:
  -E, --extended-regexp     PATTERN is an extended regular expression (ERE)
  -F, --fixed-strings       PATTERN is a set of newline-separated strings
  -G, --basic-regexp        PATTERN is a basic regular expression (BRE)
  -P, --perl-regexp         PATTERN is a Perl regular expression

root@ubuntu16:~# grep --help | grep -E "^ +-[a-Z][, ]" | wc -l
36
```

中々手強いですね。まずは不思議の国のアリスを題材に、各オプションで素直に遊んでみましょう。

## **普通の使い方**

まずはひねりもなく普通に使ってみましょう。 `play` に関連する部分をひたすら探してみます。

### **基本マッチ**

```bash:普通のgrep
root@ubuntu16:~# grep "play" alice.txt
  Rights to produce this play in all countries of the world
The illustrations of the characters of the play in this book were drawn by
W. H. Gilmore staged the play with the following cast:
(略)
```

### **マッチ行番号表示(-n, number)**

それぞれ何行目に出てきているのか気になりますね。 `-n` を使ってみましょう。

```bash:行数の表示(-n)
root@ubuntu16:~# grep -n "play" alice.txt
72:  Rights to produce this play in all countries of the world
99:The illustrations of the characters of the play in this book were drawn by
102:W. H. Gilmore staged the play with the following cast:
(略)
```

### **表示個数制限(-m, max)**

ちょっと数が多いので最初の5個だけを見たくなりました。 `-m` の出番です。

```bash:表示個数の制限(-m)
root@ubuntu16:~# grep -n -m 5 "play" alice.txt
72:  Rights to produce this play in all countries of the world
99:The illustrations of the characters of the play in this book were drawn by
102:W. H. Gilmore staged the play with the following cast:
173:_ALICE'S home. LEWIS CARROLL is discovered, playing chess. Golden-haired
256:Why do people always play with kings and queens? Mother has them in her
```

### **個数カウント(-c, count)**

結局何個ヒットしているのでしょうか。 `-c` の出番です。

```bash:個数の表示(-c)
root@ubuntu16:~# grep -c "play" alice.txt
29
```

先頭が大文字の "Play" はどうでしょう。

```bash
root@ubuntu16:~# grep -c "Play" alice.txt
1
```

### **大小文字の無視(-i, ignore)**

とにかく "play" 的なものをカウントしたいです。大文字小文字を無視しましょう。 `-i` の出番です。

```bash:大小文字のマッチ(-i)
# 87行目の "Players" が一緒にヒットするようになった
root@ubuntu16:~# grep -n -i "play" alice.txt
72:  Rights to produce this play in all countries of the world
87:Chicago, was produced by The Players Producing Company of Chicago (Aline
99:The illustrations of the characters of the play in this book were drawn by
102:W. H. Gilmore staged the play with the following cast:

root@ubuntu16:~# grep -c -i "play" alice.txt
30
```

### **単語マッチ(-w, word)**

よく見ると「 dis **play** 」なんてのもヒットしているようです。純粋な "play" をカウントしたいので `-w` の出番です。

```bash:単語単位でのマッチ(-w)
root@ubuntu16:~# grep -n -i -w "play" alice.txt
72:  Rights to produce this play in all countries of the world
99:The illustrations of the characters of the play in this book were drawn by
102:W. H. Gilmore staged the play with the following cast:

root@ubuntu16:~# grep -n -i -w -c "play" alice.txt
12
```

### **除外(-v, invert)**

結構減りました。ヒットしなくなった18行を探しましょう。 `-v` の出番です。

```bash:除外(-v)
root@ubuntu16:~# grep -i -w -v "play" alice.txt | grep -i "play"
Chicago, was produced by The Players Producing Company of Chicago (Aline
_ALICE'S home. LEWIS CARROLL is discovered, playing chess. Golden-haired
playing cards too. Look!
[_ALICE goes to the mantel and takes a pack of playing cards from the
You're playing against yourself, aren't you?
(略)

root@ubuntu16:~# grep -i -w -v "play" alice.txt | grep -c -i "play"
18
```

## **日本語でgrep**

さて、続いては日本語の「こころ」を使ってgrepの練習をしてみましょう。
ふしぎの国のアリスではそのままやっていましたが、本文のほかにヘッダーとフッターがついているのでファイルを少し加工してみます。

### **前準備**

```bash:ヘッダー
root@ubuntu16:~# cat -n kokoro.txt | head -n 30
     1  こころ
     2  夏目漱石
     3
     4  -------------------------------------------------------
     5  【テキスト中に現れる記号について】
(略)
    23  　私《わたくし》はその人を常に先生と呼んでいた。だからここでもただ先生と書くだけで本名は打ち明けない。これは世間を憚《はば》かる遠慮というよりも、その方が私にとって自然だからである。私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。筆を執《と》っても心持は同じ事である。よそよそしい頭文字《かしらもじ》などはとても使う気にならない。
(略)
```

このように、ヘッダがあるため本文は23行目から始まっているようです。
一方でフッターはどうでしょう。

```bash:フッター
root@ubuntu16:~# cat -n kokoro.txt | tail -n 20
(略)
  1579  　私は私の過去を善悪ともに他《ひと》の参考に供するつもりです。しかし妻だけはたった一人の例外だと承知して下さい。私は妻には何にも知らせたくないのです。妻が己《おの》れの過去に対してもつ記憶を、なるべく純白に保存しておいてやりたいのが私の唯一《ゆいいつ》の希望なのですから、私が死んだ後《あと》でも、妻が生きている以上は、あなた限りに打ち明けられた私の秘密として、すべてを腹の中にしまっておいて下さい。」
(略)
  1594  青空文庫作成ファイル：
  1595  このファイルは、インターネットの図書館、青空文庫（http://www.aozora.gr.jp/）で作られました。入力、校正、制作にあたったのは、ボランティアの皆さんです。
```
本文の終わりは1579行のようですね。それではこの範囲を抜き出してみましょう。

```bash
root@ubuntu16:~# sed -n 23,1579p kokoro.txt > kokoro_main.txt
```

```bash
root@ubuntu16:~# head -n 1 kokoro_main.txt ; echo "#####" ; tail -n 1 kokoro_main.txt
　私《わたくし》はその人を常に先生と呼んでいた。だからここでもただ先生と書くだけで本名は打ち明けない。これは世間を憚《はば》かる遠慮というよりも、その方が私にとって自然だからである。私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。筆を執《と》っても心持は同じ事である。よそよそしい頭文字《かしらもじ》などはとても使う気にならない。
#####
　私は私の過去を善悪ともに他《ひと》の参考に供するつもりです。しかし妻だけはたった一人の例外だと承知して下さい。私は妻には何にも知らせたくないのです。妻が己《おの》れの過去に対してもつ記憶を、なるべく純白に保存しておいてやりたいのが私の唯一《ゆいいつ》の希望なのですから、私が死んだ後《あと》でも、妻が生きている以上は、あなた限りに打ち明けられた私の秘密として、すべてを腹の中にしまっておいて下さい。」
```

よさそうですね。
あともうひとつ、読む文には大変助かるのですが《》で書かれているルビを除去しましょう。

```bash
root@ubuntu16:~# sed 's/《.*》//g' kokoro_main.txt | head
　私などはとても使う気にならない。
　私が先生と知り合いになったのは鎌倉より帰るべきはずであった。それで彼はとうとう帰る事になった。せっかく来た私は一人取り残された。
　学校の授業が始まるにはまだ大分な宿を探す面倒ももたなかったのである。
　宿は鎌倉でも辺鄙を一つ越さなければ手が届かなかった。車で行っても二十銭は取られた。けれども個人の別荘はそこここにいくつでも建てられていた。それに海へはごく近いので海水浴をやるには至極便利な地位を占めていた。
　私は毎日海へはいりに出掛けた。古い燻るのは愉快であった。
　私は実に先生をこの雑沓てる事にしていた。

［＃５字下げ］二［＃「二」は中見出し］

　私れていたからである。
```

んんん？どうもおかしいですね。
よく見てみると以下のような除去が起こっているようです。

```
　私《わたくし》はその人を常に先生と呼んでいた。だからここでもただ先生と書くだけで本名は打ち明けない。これは世間を憚《はば》かる遠慮というよりも、その方が私にとって自然だからである。私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。筆を執《と》っても心持は同じ事である。よそよそしい頭文字《かしらもじ》などはとても使う気にならない。

　↓

　私などはとても使う気にならない。
```

確かにsedでは「《～》までを除去する」と指定したものの、ちょっと意図とはずれてしまいました。
このようにマッチしすぎることがあるので、 **最長一致** には注意しておきましょう。

気を取り直して、以下のようにすれば最長一致に惑わされることなく、望みの除去を行うことができます。

```bash
root@ubuntu16:~# cat kokoro_main.txt | sed 's/《[^》]*》//g' | head
　私はその人を常に先生と呼んでいた。だからここでもただ先生と書くだけで本名は打ち明けない。これは世間を憚かる遠慮というよりも、その方が私にとって自然だからである。私はその人の記憶を呼び起すごとに、すぐ「先生」といいたくなる。筆を執っても心持は同じ事である。よそよそしい頭文字などはとても使う気にならない。
(略)
```

二重括弧の中について「 **任意の1文字** ( `.` )」でマッチングさせていたところを「 **》以外** ( `[^》]` )」でマッチングしているのがポイントです。

```bash
root@ubuntu16:~# sed -n 23,1579p kokoro.txt | sed 's/《[^》]*》//g' > kokoro_main.txt
```

### **基本マッチ**

それではあの有名なフレーズを探すべく「 **罪悪** 」でgrepしてみましょう。

```bash
root@ubuntu16:~# grep -n "罪悪" kokoro_main.txt
202:「聞こえました。恋の満足を味わっている人はもっと暖かい声を出すものです。しかし……しかし君、恋は罪悪ですよ。解っていますか」
(略)
```

出ました。こんなところにあったんですね。もっと後ろのイメージがありました。

### **前後行の表示(-B/A, Before/After)**

ちょっと前後の文脈が気になりますね。前後3行ずつ見てみましょう。 `-B` と `-A` の出番です。

```bash:マッチ前後を確認(-A/B)
root@ubuntu16:~# grep -n -B 3 -A 3 "しかし君、恋は罪悪ですよ" kokoro_main.txt
199-「ええ」
200-「君は今あの男と女を見て、冷評しましたね。あの冷評のうちには君が恋を求めながら相手を得られないという不快の声が交っていましょう」
201-「そんな風に聞こえましたか」
202:「聞こえました。恋の満足を味わっている人はもっと暖かい声を出すものです。しかし……しかし君、恋は罪悪です  。解っていますか」
203-　私は急に驚かされた。何とも返事をしなかった。
204-
205-［＃５字下げ］十三［＃「十三」は中見出し］
```

残念ながらシチュエーションはさっぱりわかりませんね。そういうこともあるでしょう。(楽観的)

### **"漢字クラス" のマッチ**

ついでなので「楽観的」を調べてみましょう。

```bash
root@ubuntu16:~# grep -n "楽観的" kokoro_main.txt
970:　兄と前後して着いた妹の夫の意見は、我々よりもよほど楽観的であった。父は彼に向かって妹の事をあれこれと尋ねていた。「身体が身体だからむやみに汽車になんぞ乗って揺れない方が好い。無理をして見舞に来られたりすると、かえってこっちが心配だから」といっていた。「なに今に治ったら赤ん坊の顔でも見に、久しぶりにこっちから出掛けるから差支えない」ともいっていた。
```

ありますね。他に **○○的** はあるんでしょうか。

```bash
root@ubuntu16:~# grep -c -E "..的" kokoro_main.txt
76
```

結構ありますね。具体的に見てみましょう。

```bash
root@ubuntu16:~# grep -n -m 3 -E "..的" kokoro_main.txt
13:　私は単に好奇心のために、並んで浜辺を下りて行く二人の後姿を見守っていた。すると彼らは真直に波の中に足を踏 み込んだ。そうして遠浅の磯近くにわいわい騒いでいる多人数の間を通り抜けて、比較的広々した所へ来ると、二人とも泳ぎ出した。彼らの頭が小さく見えるまで沖の方へ向いて行った。それから引き返してまた一直線に浜辺まで戻って来た。掛茶屋へ帰ると、井戸の水も浴びずに、すぐ身体を拭いて着物を着て、さっさとどこへか行ってしまった。
15:　その時の私は屈托がないというよりむしろ無聊に苦しんでいた。それで翌日もまた先生に会った時刻を見計らって、 わざわざ掛茶屋まで出かけてみた。すると西洋人は来ないで先生一人｜麦藁帽を被ってやって来た。先生は眼鏡をとって台の上に置いて、すぐ手拭で頭を包んで、すたすた浜を下りて行った。先生が昨日のように騒がしい浴客の中を通り抜けて、一人で泳ぎ出した時、私は急にその後が追い掛けたくなった。私は浅い水を頭の上まで跳かして相当の深さの所まで来て、そこから先生を目標に抜手を切った。すると先生は昨日と違って、一種の弧線を描いて、妙な方向から岸の方へ帰り始めた。それで私の目的はついに達せられなかった。私が陸へ上がって雫の垂れる手を振りながら掛茶屋に入ると、先生はもうちゃんと着物を着て入れ違いに外へ出て行った。
19:　私は次の日も同じ時刻に浜へ行って先生の顔を見た。その次の日にもまた同じ事を繰り返した。けれども物をいい掛 ける機会も、挨拶をする場合も、二人の間には起らなかった。その上先生の態度はむしろ非社交的であった。一定の時刻に超然として来て、また超然と帰って行った。周囲がいくら賑やかでも、それにはほとんど注意を払う様子が見えなかった。最初いっしょに来た西洋人はその後まるで姿を見せなかった。先生はいつでも一人であった。
```

これはいけません。15行のマッチを見てみると、 **の目的** というところでヒットしています。
確かに横着してピリオドでマッチさせたのがよくなかったようです。漢字でマッチさせましょう、漢字で。

...はて、漢字の文字クラスなんてあったでしょうか。

- [[InDesign][CS4][regex]正規表現の文字クラスまとめ](http://d.hatena.ne.jp/seuzo/20090309/1236525090)
- [Unicodeで「漢字」の正規表現](http://tama-san.com/kanji-regex/)

ありました。 **細かな点を抜きにすれば**  `[一-龠]` を漢字の文字クラスとして使えるようです。早速使ってみます。

```bash
root@ubuntu16:~# grep -n -E "[一-龠]{2}的" kokoro_main.txt | wc -l
56

root@ubuntu16:~# grep -n -m 3 -E "[一-龠]{2}的" kokoro_main.txt
13:　私は単に好奇心のために、並んで浜辺を下りて行く二人の後姿を見守っていた。すると彼らは真直に波の中に足を踏 み込んだ。そうして遠浅の磯近くにわいわい騒いでいる多人数の間を通り抜けて、比較的広々した所へ来ると、二人とも泳ぎ出した。彼らの頭が小さく見えるまで沖の方へ向いて行った。それから引き返してまた一直線に浜辺まで戻って来た。掛茶屋へ帰ると、井戸の水も浴びずに、すぐ身体を拭いて着物を着て、さっさとどこへか行ってしまった。
19:　私は次の日も同じ時刻に浜へ行って先生の顔を見た。その次の日にもまた同じ事を繰り返した。けれども物をいい掛 ける機会も、挨拶をする場合も、二人の間には起らなかった。その上先生の態度はむしろ非社交的であった。一定の時刻に超然として来て、また超然と帰って行った。周囲がいくら賑やかでも、それにはほとんど注意を払う様子が見えなかった。最初いっしょに来た西洋人はその後まるで姿を見せなかった。先生はいつでも一人であった。
22:　しばらくして海の中で起き上がるように姿勢を改めた先生は、「もう帰りませんか」といって私を促した。比較的強 い体質をもった私は、もっと海の中で遊んでいたかった。しかし先生から誘われた時、私はすぐ「ええ帰りましょう」と快く答えた。そうして二人でまた元の路を浜辺へ引き返した。
```

これで望みのマッチができました。多分。

### **日本語の単語マッチ(mecab & -w)**

さて、では最後に単語単位のマッチを日本語でもやってみたいと思います。「比較」と「比較的」を区別してみましょう。

```bash
root@ubuntu16:~# grep -n -w "比較" kokoro_main.txt
(出力なし)
```

あれ？何も出ませんね。「比較」ってないんでしょうか。

```bash
root@ubuntu16:~# grep -n "比較" kokoro_main.txt
(略)
433:　私は心のうちで、父と先生とを比較して見た。両方とも世間から見れば、生きているか死んでいるか分らないほど大人しい男であった。他に認められるという点からいえばどっちも零であった。それでいて、この将碁を差したがる父は、単なる娯楽の相手としても私には物足りなかった。かつて遊興のために往来をした覚えのない先生は、歓楽の交際から出る親しみ以上に、いつか私の頭に影響を与えていた。ただ頭というのはあまりに冷やか過ぎるから、私は胸といい直したい。肉のなかに先生の力が喰い込んでいるといっても、血のなかに先生の命が流れているといっても、その時の私には少しも誇張でないように思われた。私は父が私の本当の父であり、先生はまたいうまでもなく、あかの他人であるという明白な事実を、ことさらに眼の前に並べてみて、始めて大きな真理でも発見したかのごとくに驚いた。
(略)
```

ありました。ではなぜこれがマッチしなかったんでしょうか。
単語単位のマッチを行う `-w` オプションでは、単語の認識をスペースで行っています。アルファベット言語だと単語の区切りにスペースが入りますから、単語認識のルールとしてはぴったりです。
一方日本語ではどうでしょう。スペースは入りませんね。
そのため、句点や読点を挟んだとしても、改行やスペースが入らなければ **行全体が1つの単語** とみなされてしまいます。
grep自体は英語圏で生まれたものですから、このあたりは致し方ない部分であると言えます。

しかし、そうは言っても単語マッチがやりたいので頑張りましょう。
日本語解析の強い味方、 `mecab` を使います。

```bash
root@ubuntu16:~# apt-get install mecab
```

インストールはこれだけです。すばらしい。試してみましょう。

```bash
root@ubuntu16:~# echo "シェル芸人" | mecab
�	ﾆﾃｼ�,ｵｭｹ�,*,*,*,*,*
ｷ�	?ｻ�,ｿﾍﾌｾ,*,*,*,*,*
ｧ罩�
ｸ	ﾆﾃｼ�,ｵｭｹ�,*,*,*,*,*
莠	?ｻ�,ｿﾍﾌｾ,*,*,*,*,*
ｺ	ﾆﾃｼ�,ｵｭｹ�,*,*,*,*,*
EOS
```

洗礼がひどいですね。調べてみるとUTF-8用の辞書がないのがダメらしいです。

```bash
root@ubuntu16:~# apt-get install mecab-ipadic-utf8

root@ubuntu16:~# echo "シェル芸人" | mecab 
シェル	名詞,一般,*,*,*,*,シェル,シェル,シェル
芸人	名詞,一般,*,*,*,*,芸人,ゲイニン,ゲイニン
EOS
```

すばらしいですね。ではこいつをどうやって日本語の単語マッチに使うかというと `-O wakati` オプションを使います。
分かち書きといって、元の行を形態素に分割した状態で出力するものです。

```bash
root@ubuntu16:~# echo "本日はシェル芸人なり" | mecab -O wakati
本日 は シェル 芸人 なり
```

こんな感じ。あとはこいつを通したあとにgrepをかけてやると...。

```bash
root@ubuntu16:~# mecab -O wakati kokoro_main.txt | grep -n -m 3 -w "比較"
433:　 私 は 心 の うち で 、 父 と 先生 と を 比較 し て 見 た 。 両方 とも 世間 から 見れ ば 、 生き て いる か 死ん で いる か 分ら ない ほど 大人しい 男 で あっ た 。 他 に 認め られる という 点 から いえ ば どっち も 零 で あっ た 。 それでいて 、 この 将 碁 を 差し た がる 父 は 、 単なる 娯楽 の 相手 として も 私 に は 物足りなかっ た 。 かつて 遊興 の ため に 往来 を し た 覚え の ない 先生 は 、 歓楽 の 交際 から 出る 親しみ 以上 に 、 いつ か 私 の 頭 に 影響 を 与え て い た 。 ただ 頭 という の は あまりに 冷やか 過ぎる から 、 私 は 胸 と いい 直し たい 。 肉 の なか に 先生 の 力 が 喰い 込ん で いる と いっ て も 、 血 の なか に 先生 の 命 が 流れ て いる  と いっ て も 、 その 時 の 私 に は 少し も 誇張 で ない よう に 思わ れ た 。 私 は 父 が 私 の 本当 の 父 で あり 、 先生 は また いう まで も なく 、 あか の 他人 で ある という 明白 な 事実 を 、 ことさら に 眼 の 前 に 並   て み て 、 始めて 大きな 真理 で も 発見 し た か の ご とく に 驚い た 。
750:　 父 は この 言葉 を 何 遍 も 繰り返し た 。 私 は 心 の うち で この 父 の 喜び と 、 卒業 式 の あっ た 晩 先生 の 家 の 食卓 で 、 「 お 目 出 とう 」 と いわ れ た 時 の 先生 の 顔 付 と を 比較 し た 。 私 に は 口 で 祝っ て くれ ながら 、 腹 の 底 で けなし て いる 先生 の 方 が 、 それほど に も ない もの を 珍し そう に 嬉し がる 父 より も 、 かえって 高尚 に 見え た 。 私 は しまいに 父 の 無知 から 出る 田舎 臭い ところ に 不快 を 感   出し た 。
896:　 私 の 哀愁 は この 夏 帰省 し た 以後 次第に 情調 を 変え て 来 た 。 油蝉 の 声 が つくつく法師 の 声 に 変る ごとく に 、 私 を 取り巻く 人 の 運命 が 、 大きな 輪廻 の うち に 、 そろそろ 動い て いる よう に 思わ れ た 。 私 は 淋し そう な 父 の 態度 と 言葉 を 繰り返し ながら 、 手紙 を 出し て も 返事 を 寄こさ ない 先生 の 事 を また 憶 い 浮べ た 。 先生 と 父 と は 、 まるで 反対 の 印象 を 私 に 与える 点 において 、 比較 の 上 に も 、 連想 の 上 に も 、 いっしょ に 私 の 頭 に 上り やすかっ た 。
```

完成です。

ちなみにこんなのもあるようです。

- [日本語の文章から文字列を検索するコマンド jpgrep をつくってみた](http://blog.amedama.jp/entry/2015/12/10/211406)

ただオリジナルのgrepを拡張したものでないため、 `-v` 以外のオプションが使えません。
全体的な使い勝手に難があるため、オリジナルのgrepが使える形で進めました。

# **grepの速さを知る**

grepって速いらしいです。
冒頭に使ったなんちゃってgrepとオリジナルのgrepで速度比較をしてみましょう。

- grep
- sedrep
- awkrep
- powerep

出来る限り大きなテキストファイルが欲しいのですが、そうそうデカイテキストファイルなんか見つからないのでアリスをひたすら連結してみます。

```bash
root@ubuntu16:~# for i in {1..1000} ; do  cat alice.txt >> big_alice.txt ; done
root@ubuntu16:~# for i in {1..10000} ; do  cat alice.txt >> huge_alice.txt ; done

root@ubuntu16:~# ls -lh *alice.txt
-rw-r--r-- 1 root root 108K Mar 26  2011 alice.txt
-rw-r--r-- 1 root root 105M Oct  1 06:38 big_alice.txt
-rw-r--r-- 1 root root 1.1G Oct  1 06:40 huge_alice.txt
```

ひたすら "play" をカウントさせるだけの簡単なお仕事です。まずは100MB程度の `big_alice.txt` から。

```bash
# grep氏
root@ubuntu16:~# time cat big_alice.txt | grep "play" | wc -l
29000

real    0m0.106s
user    0m0.060s
sys     0m0.048s

# sedrep氏
root@ubuntu16:~# time cat big_alice.txt | sed -n '/play/p' | wc -l
29000

real    0m1.066s
user    0m0.996s
sys     0m0.068s

# awkrep氏
root@ubuntu16:~# time cat big_alice.txt | awk '/play/{print}' | wc -l
29000

real    0m1.267s
user    0m0.628s
sys     0m0.192s

# powerep氏
root@ubuntu16:~# time cat big_alice.txt | while read LINE ; do if [[ "${LINE}" =~ play ]] ; then echo ${LINE} ; fi ; done | wc -l
29000

real    0m55.960s
user    0m39.788s
sys     0m16.144s
```

結果はすべて正しく出ているようですが、すでに圧倒的な戦力差を見せつけるgrep氏。
予想通りではありますが、powerep氏はここでリタイアですね。

実力を見るために複数回まわしてみましょう。ワンライナーawkで平均、最大、最小を出します。

```bash
# grep氏
root@ubuntu16:~# for i in {1..10} ; do ( time cat big_alice.txt | grep "play" | wc -l) 2>&1 | grep real | awk '{print $2}' | sed -E 's/^([0-9]+)m([0-9\.]+)s$/\1 \2/g' | awk '{print $1*60+$2}' ; done | awk 'BEGIN{TOTAL=0;MAX=0;MIN=9999}{TOTAL+=$1;if(MAX<$1)MAX=$1;if(MIN>$1)MIN=$1}END{print "avg: "TOTAL/NR", max: "MAX", min: "MIN}'
avg: 0.0848, max: 0.103, min: 0.08

# sedrep氏
root@ubuntu16:~# for i in {1..10} ; do ( time cat big_alice.txt | sed -n '/play/p' | wc -l) 2>&1 | grep real | awk '{print $2}' | sed -E 's/^([0-9]+)m([0-9\.]+)s$/\1 \2/g' | awk '{print $1*60+$2}' ; done | awk 'BEGIN{TOTAL
=0;MAX=0;MIN=9999}{TOTAL+=$1;if(MAX<$1)MAX=$1;if(MIN>$1)MIN=$1}END{print "avg: "TOTAL/NR", max: "MAX", min: "MIN}'
avg: 1.0689, max: 1.086, min: 1.06

# awkrep氏
root@ubuntu16:~# for i in {1..10} ; do ( time cat big_alice.txt | awk '/play/{print}' | wc -l) 2>&1 | grep real | awk '{print $2}' | sed -E 's/^([0-9]+)m([0-9\.]+)s$/\1 \2/g' | awk '{print $1*60+$2}' ; done | awk 'BEGIN{TO
TAL=0;MAX=0;MIN=9999}{TOTAL+=$1;if(MAX<$1)MAX=$1;if(MIN>$1)MIN=$1}END{print "avg: "TOTAL/NR", max: "MAX", min: "MIN}'
avg: 0.8092, max: 0.817, min: 0.803
```

どうやら2番手はawkrep氏の模様。
では最後に1GBの `huge_alice.txt` を食わせてみましょう。やってることを同じなのでまとめて結果だけ。
あとついでに単純文字列のgrepならそのままより速いという **fgrep** も試してみます。

|rank|type|file|avg|max|min|avg ratio|
|---:|:---:|:---:|--:|--:|--:|---:|
|1|grep|big_alice.txt|**0.0848**|**0.103**|**0.08**|x1.00|
|2|fgrep|big_alice.txt|0.0948|0.104|0.01|x1.11|
|4|sed|big_alice.txt|1.0689|1.086|1.06|x12.60|
|3|awk|big_alice.txt|0.9092|0.817|0.803|x10.72|
|||||||
|1|grep|huge_alice.txt|**14.5974**|**16.098**|**13.735**|x1.00|
|2|fgrep|huge_alice.txt|15.5718|16.519|14.608|x1.06|
|4|sed|huge_alice.txt|16.414|17.42|15.559|x1.12|
|3|awk|huge_alice.txt|16.1243|17.372|15.33|x1.10|

結果は **grep > fgrep > awk > sed** の順となりました。
しかし `big_alice.txt` のときは他より10倍以上速かったgrepですが、 `huge_alice.txt` になると大差ないようになっています。
このあたりにはどんな理由があるんでしょうか。テストのやり方を含め、何か秘密がありそうです。
あと、fgrep君、君にはがっかりだよ。これも何か秘密が...。

# **おわりに**
grepの全オプションを網羅しようと思ったんですがある程度ストーリー仕立てにすると思った以上に大変なのでやめました。

**grepの奥は深い。**