LoginSignup
1
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-06-28

はじめに

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

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

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

egrep "^([^A]*A){m,n}[^A]*$"

特定の文字列(2文字以上)(AB)を、m~n個含む行の抽出

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

## "-" は「ファイル内に存在しない文字」を使う。

検索範囲を指定したい場合 (L~R文字目の間に、特定の文字列(2文字以上)(AB)を、m~n個含む行の抽出) (2ステップで行う)

## 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文字目))
  • (↓↓コメントの方もご参照ください。)[最後に]

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

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

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

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回)

やっていることの解説
[^A]は、「Aでない1文字」を表します。
したがって、[^A]*A「Aでない文字が0~文字」+「A」となり、これを先頭から任意の個数つなげて^([^A]*A){n}とすることで、xxAx...xA...Aのように、任意の位置に計n個のAを持つ文字列が指定されます。

あとは以降の文字列をすべてA以外([^A]*$)で指定してやれば、Aをn個だけもつ文字列が指定されます。(つまりすべてつなげると、^([^A]*A){n}[^A]*$となる。)

また、egrepgrep -Eを使用することで、(...){...}の前に\を書かずに済むようになります。

特定の文字列(2文字以上)を、n個含む行の抽出

$ cat test_data2.txt
0xxxxxxxxxxxxxxxxxx      ## ABが0個
1xxxxABxxxxxxxxxxxx      ## ABが1個
2xxxABxxxxxABxxxxxx      ## ...
3ABABABxxxxxxxxxxxx
4xxxxABxxABABABxxxx
5ABxxxABABxxxxxABAB
6ABABxABxxABxxxABAB
7ABABABABABABxxABxx
8ABxABABABABABABABx
9ABABABABABABABABAB      ## ABが9個

上のようなtest_data2.txtに対して、" AB " を任意の個数含む行を抽出したい場合は、例えば以下のように書きます。

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文字目)に対して" AB " を任意の個数含む行を抽出したい場合を考えます。

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

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

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

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

上のように、探したい特定の文字列 (AB)の前にファイル内に存在しない文字(ここでは-)を挿入することで、ABの個数と-の個数が対応するようになるので、あとは-の個数で行を抽出してから-を消すことで、検索したい文字列が何文字であろうと欲しい行を抽出することが可能になります。

※「文字の挿入」については後方参照を調べてください。
【IT初心者向け】sedの後方参照についてわかりやすく解説してみた


 ちなみに...

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

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 " は無視して欲しいといった場合にはまた特殊な書き方が必要になります。(ここではやりません)

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


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

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

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

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:-
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

最後に

本稿の内容は以上となります。
正規表現をうまく駆使すればもっとコンパクトな書き方があるのかもしれませんが、思いつきません!(→コメントもご参照ください)

ご覧いただきありがとうございました!🙇‍♂️🙇‍♂️
[一番上に戻る]

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1