grepで特定の文字（文字列）をm~n個含む行を抽出する方法

## はじめに
今回はLinuxの`grep`コマンドを使って**「特定の文字（文字列）を任意の個数含む行」**を抽出する方法をご紹介します。（というか思いついたので書く）

（↓↓※すべてコマンドの前に`cat 検索したいファイル |`をつけます）

・[特定の1文字(**A**)を、m~n個含む行の抽出](#特定の1文字をn個含む行の抽出)

```sh
egrep "^([^A]*A){m,n}[^A]*$"
```
・[特定の文字列（2文字以上）(**AB**)を、m~n個含む行の抽出](#特定の文字列2文字以上をn個含む行の抽出)

```sh
sed -r 's/(AB)/-\1/g' | egrep "^([^-]*-){m,n}[^-]*$" | sed 's/-//g'

## "-" は「ファイル内に存在しない文字」を使う。
```
・[検索範囲を指定したい場合　(L~R文字目の間に、特定の文字列（2文字以上）(**AB**)を、m~n個含む行の抽出)](#検索範囲を指定したい場合)　（2ステップで行う）

```sh
## step1 : 検索に該当した行の行番号を取得
sed -r 's/(^.{x})(.*)(.{y}$)/\2/g' | grep -n "" | sed -r 's/(AB)/-\1/g' | egrep "^([^-]*-){m,n}[^-]*$" | sed 's/-//g' | sed 's/:.*$/:-/g' > file.txt

## step2 : 元のファイルからstep1の行番号の行のみを抽出
sed 's/^/-/g' | grep -n "" | grep -f file.txt | sed 's/.*-//g'

## "-" はファイル内に存在しない文字を使用する。（"-"以外を使用したい場合は、両方の命令文で全ての"-"を変更する。）
## " ^.{x} " や " .{y}$ " は検索範囲外の文字列を示す（先頭からx文字 と 末尾からy文字 は検索範囲外; その間の文字列が検索範囲(L~R文字目)）
```

+ <font color=blue>（↓↓コメントの方もご参照ください。）[[最後に](#最後に)]</font>

---

## 特定の1文字を、n個含む行の抽出

```sh
$ cat test_data1.txt
0xxxxxxxxx      ## Aが0個
1xxxxxxAxx      ## Aが1個
2xxAAxxxxx      ## ...
3AxAxxxxAx
4AAAAxxxxx
5AxxxAxAAA
6xxAAAAAAx
7AxAAAAxAA
8AAAxAAAAA
9AAAAAAAAA      ## Aが9個
```

上のような**test_data1.txt**に対して、" **<font color=red>A</font>** " を任意の個数含む行を抽出したい場合は、例えば以下のように書きます。

```sh:test_data1.txt
## Aを5~7個含む
$ cat test_data1.txt | egrep "^([^A]*A){5,7}[^A]*$"
5AxxxAxAAA
6xxAAAAAAx
7AxAAAAxAA

## xを5~7個含む
$ cat test_data1.txt | egrep "^([^x]*x){5,7}[^x]*$"
2xxAAxxxxx
3AxAxxxxAx
4AAAAxxxxx

## "A"以外の文字を検索したい場合は、"A"の部分を全てその文字に変更する。
## egrep 内の" {5,7} " で繰り返し回数を指定する（今回は5~7回）
```

<details><summary>**やっていることの解説**</summary>
`[^A]`は、**「Aでない1文字」**を表します。
したがって、`[^A]*A`は**「Aでない文字が0~文字」**+**「A」**となり、これを先頭から任意の個数つなげて`^([^A]*A){n}`とすることで、`xxAx...xA...A`のように、**任意の位置に計n個のAを持つ文字列**が指定されます。<br>
あとは以降の文字列をすべてA以外(`[^A]*$`)で指定してやれば、Aをn個だけもつ文字列が指定されます。（つまりすべてつなげると、**`^([^A]*A){n}[^A]*$`**となる。）<br>
また、`egrep`や`grep -E`を使用することで、`(`...`)`や`{`...`}`の前に`\`を書かずに済むようになります。</details>

## 特定の文字列（2文字以上）を、n個含む行の抽出
```sh
$ cat test_data2.txt
0xxxxxxxxxxxxxxxxxx      ## ABが0個
1xxxxABxxxxxxxxxxxx      ## ABが1個
2xxxABxxxxxABxxxxxx      ## ...
3ABABABxxxxxxxxxxxx
4xxxxABxxABABABxxxx
5ABxxxABABxxxxxABAB
6ABABxABxxABxxxABAB
7ABABABABABABxxABxx
8ABxABABABABABABABx
9ABABABABABABABABAB      ## ABが9個
```
上のような**test_data2.txt**に対して、" **<font color=red>AB</font>** " を任意の個数含む行を抽出したい場合は、例えば以下のように書きます。

```sh:test_data2.txt
## ABを5~7個含む
$ cat test_data2.txt | sed -r 's/(AB)/-\1/g' | egrep "^([^-]*-){5,7}[^-]*$" | sed 's/-//g'
5ABxxxABABxxxxxABAB
6ABABxABxxABxxxABAB
7ABABABABABABxxABxx

## Bxxを1個含む
$ cat test_data2.txt | sed -r 's/(Bxx)/-\1/g' | egrep "^([^-]*-){1}[^-]*$" | sed 's/-//g'
1xxxxABxxxxxxxxxxxx
3ABABABxxxxxxxxxxxx

## sed -r 内の " (AB) " に検索したい文字列を指定する（今回は "AB"）
## egrep 内の" {5,7} " で指定文字列の繰り返し回数を指定する（今回は5~7回）

```



## 検索範囲を指定したい場合
行の中で**L文字目～R文字目**だけを検索範囲にしたい場合をのやり方を最後にご紹介します。やや長いです。

上記の**test_data2.txt**に対して、例えば`-`の中の範囲（**5~14文字目**）に対して" **<font color=red>AB</font>** " を任意の個数含む行を抽出したい場合を考えます。

```sh:test_data2.txt
## 5 ~ 14文字目にある "AB" のみを検索対象にしたい
$ cat test_data2.txt | sed -r 's/(^.{4})(.*)(.{5}$)/\1-\2-\3/g'
0xxx-xxxxxxxxxx-xxxxx
1xxx-xABxxxxxxx-xxxxx
2xxx-ABxxxxxABx-xxxxx
3ABA-BABxxxxxxx-xxxxx
4xxx-xABxxABABA-Bxxxx
5ABx-xxABABxxxx-xABAB
6ABA-BxABxxABxx-xABAB
7ABA-BABABABABx-xABxx
8ABx-ABABABABAB-ABABx
9ABA-BABABABABA-BABAB
```

このような場合には、例えば以下のように書きます。

```sh:5~14文字目に"AB"を2~3回含む文字列の検索
## 検索に該当する文字列の行番号を取得（file.txt）
$ cat test_data2.txt | sed -r 's/(^.{4})(.*)(.{5}$)/\2/g' | grep -n "" | sed -r 's/(AB)/-\1/g' | egrep "^([^-]*-){2,3}[^-]*$" | sed 's/-//g' | sed 's/:.*$/:-/g' > file.txt; cat file.txt

## file.txtの行の文字列だけを取得（抽出完了）
$ cat test_data2.txt | sed 's/^/-/g' | grep -n "" | grep -f file.txt | sed 's/.*-//g'
2xxxABxxxxxABxxxxxx
4xxxxABxxABABABxxxx
5ABxxxABABxxxxxABAB
6ABABxABxxABxxxABAB

## "-" はファイル内に存在しない文字を使用する。（"-"以外を使用したい場合は、両方の命令文で全ての"-"を変更する。）
## " ^.{4} " や " .{5}$ " は検索範囲外の文字列を示す（先頭から4文字 と 末尾から5文字 は検索範囲外）

## sed -r 内の " (AB) " に検索したい文字列を指定する（今回は "AB"）
## egrep 内の" {2,3} " で指定文字列の繰り返し回数を指定する（今回は2~3回）

## 5~14文字目の抽出は、" sed -re 's/^(.{14}).*$/\1/g' -re 's/^(.{4})//g' " などでもできます
```



## やっていることの解説
コードブロックが折りたたみできないので、やむなく別で書くことにしました。

#### **特定の文字列（2文字以上）を、m~n個含む行の抽出**

1文字の時と同じ要領でやりたいところですが、`[`...`]`の中では正規表現や後方参照が機能しないので、文字列を`(`...`)`でグループ化することができません。そこで少し工夫をします。

```sh:test_data2.txt
$ cat test_data2.txt | sed -r 's/(AB)/-\1/g'   ## "-" の部分はファイル内に存在しない文字を使う（事前に調べる）
0xxxxxxxxxxxxxxxxxx
1xxxx-ABxxxxxxxxxxxx
2xxx-ABxxxxx-ABxxxxxx
3-AB-AB-ABxxxxxxxxxxxx
4xxxx-ABxx-AB-AB-ABxxxx
5-ABxxx-AB-ABxxxxx-AB-AB
6-AB-ABx-ABxx-ABxxx-AB-AB
7-AB-AB-AB-AB-AB-ABxx-ABxx
8-ABx-AB-AB-AB-AB-AB-AB-ABx
9-AB-AB-AB-AB-AB-AB-AB-AB-AB
```
上のように、探したい**特定の文字列** (**<font color=red>AB</font>**)の前に**<font color=blue>ファイル内に存在しない文字</font>**(ここでは`-`)を挿入することで、**`AB`の個数と`-`の個数が対応する**ようになるので、あとは`-`の個数で行を抽出してから`-`を消すことで、**検索したい文字列**が何文字であろうと欲しい行を抽出することが可能になります。

※「文字の挿入」については**後方参照**を調べてください。
[【IT初心者向け】sedの後方参照についてわかりやすく解説してみた](https://qiita.com/tamcchi103/items/2a9232abd0c25ce3210d)

---

　**ちなみに...**

例えば、" **xx** "のようにがっつり連続してしまっている文字列を検索する場合には、どのような検索のされ方をするのかを見てみます。

```sh:test_data2.txt
$ cat test_data2.txt | sed -r 's/(xx)/-\1/g'
0-xx-xx-xx-xx-xx-xx-xx-xx-xx
1-xx-xxAB-xx-xx-xx-xx-xx-xx
2-xxxAB-xx-xxxAB-xx-xx-xx
3ABABAB-xx-xx-xx-xx-xx-xx
4-xx-xxAB-xxABABAB-xx-xx
5AB-xxxABAB-xx-xxxABAB
6ABABxAB-xxAB-xxxABAB
7ABABABABABAB-xxAB-xx
8ABxABABABABABABABx
9ABABABABABABABABAB
```
上のように、初めに引っかかった箇所から重複なく検索されていることがわかります。
なので例えば**最初の " x " は無視して欲しい**といった場合にはまた特殊な書き方が必要になります。<font color=Silver>（ここではやりません）</font>

何にせよ、特殊な事をする場合でもそうでない場合でも、抽出する前にまず`sed -r`で**どういう検索のされ方をするか**を事前に確かめておくのが安全かもしれません。

---

#### 検索範囲を指定したい場合

行内の特定の範囲だけを検索に掛けたい場合は、**<font color=red>①まず検索に掛けたい部分だけをトリミングし、②その部分だけで該当行の検索を行う。③そして該当行の行番号を取得し（file.txt）**、**<font color=blue>④最後に元のファイルからその行番号のもののみを取得する（grep -f file.txt）**という流れで行います。

④で行番号から行を取得するときに、間違った抽出が起こらないように**ファイル内に存在しない文字**(ここでは`-`)を活用します。

```sh:file.txtの作成　(検索に該当した行の行番号を取得する)
## step1 : 検索範囲のみを抽出 & 行番号を付ける
$ cat test_data2.txt | sed -r 's/(^.{4})(.*)(.{5}$)/\2/g' | grep -n "" > step1.txt; cat step1.txt
1:xxxxxxxxxx
2:xABxxxxxxx
3:ABxxxxxABx
4:BABxxxxxxx
5:xABxxABABA
6:xxABABxxxx
7:BxABxxABxx
8:BABABABABx
9:ABABABABAB
10:BABABABABA


## step2 : 検索に該当する行のみを取得
$ cat step1.txt | sed -r 's/(AB)/-\1/g' | egrep "^([^-]*-){2,3}[^-]*$" | sed 's/-//g' > step2.txt; cat step2.txt
3:ABxxxxxABx
5:xABxxABABA
6:xxABABxxxx
7:BxABxxABxx

## step3 : ファイル内に存在しない文字("-")を付ける
## .* は最長一致なので、「末尾から行番号の : 直後までの文字列」が指定される
$ cat step2.txt | sed 's/:.*$/:-/g' > file.txt; cat file.txt
3:-
5:-
6:-
7:-
```

```sh:file.txtの行番号の行のみを抽出（検索に該当した行の抽出）

## file.txtのフォーマットで行番号をつける（ファイル内に存在しない文字 "-" を活用）
$ cat test_data2.txt | sed 's/^/-/g' | grep -n "" > step1.txt; cat step1.txt
1:-0xxxxxxxxxxxxxxxxxx
2:-1xxxxABxxxxxxxxxxxx
3:-2xxxABxxxxxABxxxxxx
4:-3ABABABxxxxxxxxxxxx
5:-4xxxxABxxABABABxxxx
6:-5ABxxxABABxxxxxABAB
7:-6ABABxABxxABxxxABAB
8:-7ABABABABABABxxABxx
9:-8ABxABABABABABABABx
10:-9ABABABABABABABABAB

## file.txtにある行の抽出
$ cat step1.txt | grep -f file.txt > step2.txt; cat step2.txt
3:-2xxxABxxxxxABxxxxxx
5:-4xxxxABxxABABABxxxx
6:-5ABxxxABABxxxxxABAB
7:-6ABABxABxxABxxxABAB

## 行番号を削除（データの抽出完了）
$ cat step2.txt | sed 's/.*-//g'
2xxxABxxxxxABxxxxxx
4xxxxABxxABABABxxxx
5ABxxxABABxxxxxABAB
6ABABxABxxABxxxABAB
```

## 最後に
本稿の内容は以上となります。
正規表現をうまく駆使すればもっとコンパクトな書き方があるのかもしれませんが、思いつきません！<font color=blue>（→コメントもご参照ください）</font>

ご覧いただきありがとうございました！🙇‍♂️🙇‍♂️
[[一番上に戻る](#はじめに)]