Powershell入門 - 14.正規表現②

前回はPowershellで正規表現を扱うための演算子やメソッドについて解説しました。
今回は、番外編として正規表現自体の文法を学んでいきたいと思います。  

正規表現はPowershell固有のものではなく、他のプログラミング言語やテキストエディタの検索機能でも使えたりします。  
非常に便利な記法ですので「Powershellじゃないからいいや」と思わず理解していきましょう。

# 1. 1文字を表す記法
例えば、「I am Sam.」と「I am Tom.」は別の文字列ですが、「I am XXX.」という形は共通しています。  
このように、共通部分以外(XXX部分)に幅を持たせるための方法から学んでいきます。

## 1.1. 任意の文字「.」
ドット(.)を使うことで、任意の1文字を表現できます。  
正規表現の最も基本となる表現なので、覚えておきましょう。  

```powershell
$text = "私はキリンです。 私はサルです。 私はウマです。 私は私です。"
$m = [regex]::Matches($text, '私は..です。')
foreach($el in $m){
    $el.Value
}
```
上記の処理を実行すると、「私はサルです。」と「私はウマです。」が検索対象となります。  
このことから、ドット(.)の数に対応した文字列のみが検索されていることが分かります。
そのため、ドット(.)の数を1つや3つに変更すると、検索結果が変わりますので確認してみましょう。

---

#### 例題1. 
以下の文字列の中から、「私はXXXがXXです。」に該当する文字列をすべて抜き出しなさい。  
「X」は任意の1文字入ることを表している。

```powershell
$text = "私は運動が苦手です。私はプリンが好きです。私は逆上りが得意です。僕はゲームが嫌いです。私がたかしの母親です。"
```

#### 例題2.
以下のhtmlの文字列から、4文字のタグ(例:\<head\>など)をすべて抽出しなさい。  
※\</nav\>も4文字として扱うものとする。また同じタグが複数回登場してもよいものとする。

```powershell
$html = '
<!DOCTYPE html> 
<html lang="ja"> 
	<head> 
		<meta charset="UTF-8"> 
		<title>例題2</title>
		<link rel="stylesheet" type="text/css" media="all" href="css/enshu61.css">
	</head> 
	<body>
		<div id="wrap">
			<header>
				<h1>everyday, everybody</h1>
				<p>html5に挑戦している私のブログです。</p>
			</header>
			<div id="sidebar">
				<nav>
					<h2>menu</h2>
					<ul>
						<li><a href="profile.html">プロフィール</a></li>
						<li><a href="link.html">参考サイト</a></li>
					</ul>
				</nav>
			</div>
			<div id="contents">
				<article>
					<h3>タイトル</h3>
					<div class="maintext">
						<p>毎日奮闘しています。</p>
					</div>
				</article>
			</div>
			<footer>
				<address>当ブログに関するご意見等は　sample@〇〇〇.com　までどうぞ。</address> 
			</footer>  
		</div>
	</body> 
</html>
'
```
---

## 1.2. 特定の文字、文字種の絞り込み
ドット(.)による絞り込みはどんな1文字でも対象となるため、ひらがな、数字、アルファベットなどを区別できません。  
特定の文字に対して絞り込みを行う場合は、鍵カッコ([])により使用可能な文字種を制限することが出来ます。

```powershell
[0123456789]                  # 数字のみ1文字
[abcdefghijklmnopqrstuvwxyz]  # アルファベット小文字1文字
```

ただし、上記のように全て並べて書くのは、1文字を表すにしては非効率です。  
※漢字一文字を上記の方法で表そうとしたら、非常に大変ですよね。。
正規表現では**連続した文字については、ハイフン(-)を使って省略することが可能となっています。

```powershell
[0-9]    # 数字のみ1文字
[a-z]    # アルファベット小文字1文字
[a-zA-Z] # 大文字小文字アルファベット1文字
```

また、特定の文字種については、絞り込むためのキーワードが予め用意されています。  
表にまとめましたので、確認してみましょう。  
なお、対応表現の列にある通り、カギ括弧(\[\])による対応表現があるものも存在します。

| 記号 | 説明 | 対応表現 |
| :-- | :-- | :-- |
| \t | タブ文字 | なし |
| \n | 改行文字 | なし |
| \d | すべての数字 | [0-9] |
| \D | 数字以外のすべての文字 | [^0-9] |
| \s | 空白文字 | [\t\f\r\n] |
| \S | 空白文字以外のすべての文字 | [^\t\f\r\n] |
| \w | アルファベット、アンダーバー、数字 | [a-zA-Z_0-9] |
| \W | アルファベット、アンダーバー、数字以外の文字 | [^a-zA-Z_0-9] |
| \b | 単語の先頭と単語の末尾にマッチ | なし | 

---
#### 例題3.
以下の文字列から、**アルファベットの大文字**をすべて抜き出し、1行ずつ表示しなさい。

```powershell
$text = 'Elon Reeve Musk is a business magnate and investor.
He is the founder, CEO and chief engineer of SpaceX; angel investor,
CEO and product architect of Tesla, Inc.; owner and CEO of Twitter, Inc.;
founder of The Boring Company; co-founder of Neuralink and OpenAI;
and president of the philanthropic Musk Foundation.'
```

#### 例題4.
例題2のhtmlが書かれた文字列から、4文字の**開始タグ**をすべて抽出しなさい。

---

# 2. 複数文字の扱い
ここからは、1文字だけでなく、複数の文字を組み合わせて使う方法を学びます。

## 2.1. 直前の文字の繰り返し
例えば、アルファベットの小文字が3文字連続したものを取得することを考えてみましょう。
ここまでで覚えた知識のみ使って書くと以下のようになります。

```powershell
[a-z][a-z][a-z]
```

この表現の場合、文字の長さ分だけ同じ表記を書くのが面倒ですし、  
そもそも文字の長さが決まっていない場合には対応できません。  

そのため、直前に指定した文字を繰り返して使いたい場合の表現が存在します。  
こちらも表で説明します。

| 記号 | 説明 | 正規表現の例 | マッチする文字列の例 | 
| :-- | :-- | :-- | :-- |
| + | 直前の文字を**１回以上**繰り返す場合 | go+gle | google<br>goooogle |
| * | 直前の文字を**0回以上**繰り返す場合 | go\*gle | ggle<br>google |
| ? | 直前の文字が**0 or 1個**の場合 | go?gle | ggle<br>gogle |
| {n} | 直前の文字を**n回**繰り返す | go{3}gle | gooogle |
| {m,n} | 直前の文字を**m回～n回**繰り返す | go{1,5}gle | gogle<br>gooooogle |

例えば、先ほど説明した`[a-z][a-z][a-z]`については、以下のように書き換えることが出来ます。

```powershell
[a-z]{3}
```

#### 例題5.
例題4を繰り返しを使った手法に書き換えなさい。

#### 例題6.
以下の文章から、**IPアドレス**を抜き出しなさい。  
ここでのIPアドレスは、
[1~3桁の数字].[1~3桁の数字].[1~3桁の数字].[1~3桁の数字]
で書かれていればOKとする。

※本来、`999.999.999.999`はIPアドレスの要件を満たさないが今回の例題ではOKとする。

```powershell
$text = "
  1     6 ms     1 ms     1 ms  201.10.30.196
  2    13 ms     1 ms     1 ms  192.168.3.3
  3     8 ms     7 ms     8 ms  192.168.3.193
  4     9 ms     9 ms     9 ms  202.15.40.200
  5     9 ms     8 ms     8 ms  202.15.40.38
"
```
## 2.2. 文字列のグループ化
2.1で学んだ繰り返しは、特定の1文字について繰り返していました。  
ですが、例えば「abcを3回繰り返した文字」を検索したい場合など、  
文字列ごとの繰返しを扱いたい場合があります。  

この場合は、「(?:グループ化したい文字)」を使うことで、文字列をグループ化して扱うことが出来ます。

```powershell
$text = "IPv4 アドレス . . . . . . . . . . . .: 172.030.255.196(優先)"

$m = [regex]::Matches($text, '\d{3}(?:\.\d{3}){3}')
foreach($el in $m){
    $el.Value
}
```
`\d{3}(?:\.\d{3}){3}`の部分について見ていきましょう。

グループ化の最初の1文字目は`\.`です。
これは、**エスケープシーケンス**といって、正規表現として意味を持ってしまう記号を、ただの文字として使いたい場合に使用します。
今回の場合、ドット(.)を任意の文字ではなく、ドット(.)1文字として使いたいため使用しました。

グループ化された文字を確認すると、`.xxx`(xは数字1文字)の形になっています。  
これが3回繰り返されるため、`.xxx.xxx.xxx`という文字列を探すことが出来ます。

よって、`\d{3}(?:\.\d{3}){3}`は`xxx.xxx.xxx.xxx`という形式の文字列を検索するパターンとして使うことが出来ます。

---
#### 例題7.
以下の条件を満たす文字列を$text中から全て抜き出しなさい。  

(アルファベットの小文字1文字の後ろに数字2文字)が3回繰り返された文字列
```powershell
$text = "a11b234cd23f1j89k23g12mk45q98c12fx22zx45"
```

#### 例題8.
以下の文字列から、日時を取得しなさい。

```powershell
time,upload(Mbps),download(Mbps)
2022/12/09 08:58:09,238.87,183.25
2022/12/09 09:02:16,261.4,216.61
2022/12/09 11:13:17,33.43,37.7
2022/12/09 11:20:34,35.26,21.46
```

# 3. その他の記法

ここからは、上記の記法で紹介できなかったものをいくつか紹介します。

## 3.1. 文字列の先頭、末尾の指定

正規表現では、文字が行の先頭にある場合や末尾にある場合のみ検索するという表現も可能です。  

| 記号 | 説明 | 正規表現の例 | マッチする文字列の例 | 
| :-- | :-- | :-- | :-- |
| ^ | 直前の文字が文字列の先頭にある場合 | ^(?:google).* | google chrome |
| \$ | 直前の文字が文字列の末尾にある場合 | .*(?:google)$ | OK google |

以下のサンプルでは、`163-8001`のみ検索対象となります。

```powershell
$text_list = 
"私の家の郵便番号は260-8722です。",
"163-8001は？これは、この都庁の郵便番号です。",
"ちなみに、皇居の住所は100-8111だそうです。"

foreach($text in $text_list){
    $m = [regex]::Matches($text, '^\d{3}-\d{4}')
    foreach($el in $m){
        $el.Value
    }
}
```
---
#### 例題9.
以下の文字列配列の中から、「`image`で始まる文字列」を抜き出し、その文字列の全文を表示しなさい。

```powershell
$text_list = 
"heapdict                  1.0.1              pyhd3eb1b0_0",
"html5lib                  1.1                pyhd3eb1b0_0",
"huggingface-hub           0.8.1                    pypi_0    pypi",
"humanfriendly             10.0                     pypi_0    pypi",
"icc_rt                    2019.0.0             h0cc432a_1",
"icu                       58.2                 ha925a31_3",
"idna                      3.2                pyhd3eb1b0_0",
"imagecodecs               2021.8.26        py39ha1f97ea_0",
"imageio                   2.9.0              pyhd3eb1b0_0",
"imageio-ffmpeg            0.4.7                    pypi_0    pypi",
"imagesize                 1.2.0              pyhd3eb1b0_0",
"importlib-metadata        4.8.1            py39haa95532_0",
"importlib_metadata        4.8.1                hd3eb1b0_0",
"inflection                0.5.1            py39haa95532_0",
"iniconfig                 1.1.1              pyhd3eb1b0_0",
"intel-openmp              2021.4.0          haa95532_3556",
"intervaltree              3.1.0              pyhd3eb1b0_0"
```

#### 例題10.
例題9と同じ配列から、「`pypi`で終わる文字列」を抜き出し、その文字列の全文を表示しなさい。

---


## 3.2. キャプチャグループの使用
例えば、「現在の時刻は 10:36:05です。」という文字列から、時刻部分を抜き出したいとします。
その場合、以下のように書けるでしょう。

```powershell
$text = "現在の時刻は 10:36:05です。"

# -match演算子を使う場合
$bln = $text -match '\d{2}:\d{2}:\d{2}'
Write-Host $Matches.Values[0]

# Matchesメソッドを使う場合
$m = [regex]::Matches($text, '\d{2}:\d{2}:\d{2}')
foreach($el in $m){
    Write-Host $el.Value
}
```

では、同じ文字列から「分」に関する情報だけ抜き出したい場合は、どうすればよいでしょうか。  
この場合、「時」も「分」も「秒」もすべて`\d{2}`で表せてしまうため、「分」だけを特定できません。  
分を特定するためには、「二つのコロン(:)に挟まれた2桁の数字」であるということを条件に加えてあげなければいけません。  

このような時に便利なのが**キャプチャグループ**です。

では、実際に上の例で分だけを抜き出してみます。

```powershell
$text = "現在の時刻は 10:36:05です。"
$bln = $text -match '(\d{2}):(\d{2}):(\d{2})'
Write-Host ("時刻 : " + $matches[0])
Write-Host ("時 : " + $matches[1])
Write-Host ("分 : " + $matches[2])
Write-Host ("秒 : " + $matches[3])
```

先ほどの例との違いは二けたの数値を表す正規表現`\d{2}`を括弧で括っただけです。  
正規表現内で括弧で括られた箇所は、正規表現内の部分文字列としてキャプチャできるようになります。  

結果は`$matches`の配列の1番目から、前から順に格納されます。  
0番目には正規表現全体にマッチした文字列が入ります。

キャプチャグループは、`Matchesメソッド`でも利用可能です。  
文法が少し異なりますが、こちらはサンプルのみ掲載します。

```powershell
$s = "名前: ああああ, 郵便番号：001-0001
名前:いいいい、郵便番号：001-0002
名前:うううう、郵便番号：001-0003
"

$m = [regex]::Matches($s, "(\d{3})-(\d{4})")
$m.Groups            # 検索結果のオブジェクトを取得したい場合
$m.Groups.Value      # 検索結果の文字列だけ取得したい場合
```

---
#### 例題11.
以下の文字列から、クレジットカード番号の下4桁を抜き出しなさい。
```powershell
$text = "あなたのクレジットカード番号は
1234567800019876
でお間違えないでしょうか？
"
```


#### 例題12.
以下の文字列の中から、「〇〇市」の部分を抜き出しなさい。  
例えば、一番上の文字列であれば、「西宮市」を抜き出すこと
```powershell
$address = "
兵庫県西宮市上鳴尾町7-1-2
埼玉県川口市芝新町2-14-5
埼玉県川口市安行領家8361
千葉県東金市下武射田3-9-3 フォルム下武射田
富山県富山市婦中町小倉1831
"
```

Prev : [13.正規表現①](./6953cbe20fff49d83fc4)


[目次へ戻る](./926b8053a031539437a3)