Regexクラスを使う
Powershellにはいくつかの置換方法が用意されている。
1. "文字列".Replace("old","new")
一番シンプルで高速。正規表現は使えない。
"Hello World!".Replace("World","Qiita")
# Hello Qiita!
2. -replace
演算子
正規表現が使える。
"Hello World!" -replace "\s","-"
# Hello-World!
キャプチャができる。
"I am a programmer." -replace "\ba (\w+)","not a good `$1"
# I am not a good programmer.
ただしキャプチャした文字はそのまま使う以外に操作できない。
3. Regex
クラスのReplace()
マッチした値を変数として扱える。
$s = "this_is_camelcase"
[regex]::Replace($s, "_(\w)", { $args.groups[1].value.toupper() }) #
# thisIsCamelcase
3つ目はC#ではよく使われてるであろうラムダ式渡すやつなんですが、Powershellでもスクリプトブロックを渡すことで同じメソッドを使えますよってことを言いたいだけです。この記事はこの3つ目の使い方をツラツラと書き散らかします。
マッチした値になんらかの操作をして置換する
マッチ部分を大文字に
[regex]::replace("abcd3e-34a", "\w+(?=-\w+)", { $args.value.toUpper() }) #
# abcd3e-34a → ABCD3E-34a
(?=pattern)
は肯定的先読み。直後にpatternがある場所に一致する。
スクリプトブロック内の$args
の型はSystem.Text.RegularExpressions.Match
。
(本当は$args[0]
とすべきだろうが、どうせ引数は一つしか入ってこないので省略。)
$args.value
は一致した部分の文字列。
ToUpper()
で大文字に変換する。
マッチした数値部分の0埋め
[regex]::replace("abcd3e-34a", "(?<=\w+-)\d+(?=\w+)", { $args.value.padleft(5,'0') }) #
# abcd3e-34a → abcd3e-00034a
(?<=pattern)
は肯定的後読み。直前にpatternがある場所に一致する。
PadLeft()
で文字埋めできる。
数値に+10
[regex]::replace("abcd3e-34a", "(?<=\w+-)\d+(?=\w+)", { 10 + $args.value }) #
# abcd3e-34a → abcd3e-44a
数字にマッチすれば計算もできる。計算順序を気にしないでいいように[int]$args.value
でキャストした方が確実かもしれない。
和暦を西暦に
$s = "おじいさんは明治43年生まれです。"
[regex]::Replace($s, "(明治|大正|昭和|平成)[\d元]+年",
{ Get-Date $args.value.replace("元",1) -format "yyyy年" } )
# おじいさんは1910年生まれです。
Get-Date -format
を使う。([datetime]::parse(value).toString(yyyy年)
でもいい。)
西暦→和暦は日付も関係するのでパス。
全角数字→半角数字
$s = "2018年10月8日の降水確率は20%です。"
[regex]::replace($s,"[0-9]", { $args.value[0] - 65248 -as "char" })
# 2018年10月8日の降水確率は20%です。
全角と半角の文字コードの差を引く。
"文字列"[0]
でchar
型の値になる。-as "char"
も同様。
キャプチャ
円をドルに
$s = "PC本体が347831円、モニタは12345円、その他周辺機器が5923円でした。"
[regex]::Replace($s, "(\d+)円", { "{0:0,0.00}ドル" -f (1/112.21 * $args.groups[1].value) }) #
# PC本体が3,099.82ドル、モニタは110.02ドル、その他周辺機器が52.78ドルでした。
グループ化してキャプチャした値は$args.groups[index]
でとれる。
{0:0,0.00}
は数値の書式指定文字。ここでは桁区切りで小数点第二位まで表示する指定。
条件別
マッチした値によって置換後の文字をかえる。
数値の範囲による場合分け
$s = "リサの体重は40kgです。メアリーの体重は52kgです。キャサリンの体重は87kgです。"
[regex]::replace($s, "(\d+)kg", {
switch($args.groups[1].value) {
{$_ -lt 45} { "とても軽い" }
{$_ -gt 70} { "秘密" }
default { $args.value } }
})
# リサの体重はとても軽いです。メアリーの体重は52kgです。キャサリンの体重は秘密です。
マッチした数字の範囲によって場合分け。switch
文の条件にはスクリプトブロックも使える。
CSS色指定方法の変換
cssの色指定をrgb()
に変える。
$s = @"
a {
color:#ff7f00;
background:#235;
}
"@
[regex]::Replace($s,"#([\da-f]{6}|[\da-f]{3})", {
$v = $args.groups[1].value
if($v.length -eq 6) {
"rgb({0},{1},{2})" -f ([regex]::Matches($v,"..").value | foreach { [Convert]::ToInt32($_, 16) })
} else {
"rgb({0},{1},{2})" -f ([char[]]$v | foreach { 17 * [Convert]::ToInt32($_, 16) })
}
} , "ignorecase")
# a {
# color:rgb(255,127,0);
# background:rgb(34,51,85);
# }
3桁で指定する場合と6桁で指定する場合があるので処理を分ける。
n文字毎に分割するには[regex]::Matches($v,".{n}").value
が手っ取り早い。
オプションのignorecase
は大文字小文字を区別しない。
辞書による置換
ハッシュテーブルを作って、対応する値に変換する。
ハッシュテーブルで置換1
$s = "藪から棒に、清水の舞台から飛び降りる。"
$h = @{
棒 = "スティック"
舞台 = "ステージ"
飛び降りる = "ダイブする"
}
[regex]::replace($s, ($h.Keys -join "|"), { $h[$args.value] }) #
# 藪からスティックに、清水のステージからダイブする。
ハッシュテーブルで置換2
$s = "昨日から雨が続いていますが、明日は全国的に晴れるでしょう。"
$ht = @{ 昨日 = -1; 今日 = 0; 明日 = 1 }
[regex]::replace($s,($ht.Keys -join "|"), { (date).AddDays($ht[$args.value]).ToString("d日(ddd)") })
# 24日(水)から雨が続いていますが、26日(金)は全国的に晴れるでしょう。
キャプチャスタックを使う
一般的にグループの繰り返しでは最後にキャプチャしたものしか使えないが、.Netでは以下のようにキャプチャしたものが全てスタックされる。これを利用する。
[regex]::Match("22-33-44", "(?:(\d+)|-)+").groups[1].captures
# Index Length Value
# ----- ------ -----
# 0 2 22
# 3 2 33
# 6 2 44
算数
繰り返し回数が不定でさらにその値をまとめて処理する必要がある時はキャプチャスタックが便利。
$s = @"
3たす4ひく1は?です。
2たす1たす100ひく40たす23は?です。
"@
$calc = @{
たす = { $args[0] + $args[1] }
ひく = { $args[0] - $args[1] }
}
[regex]::Replace($s,"(\d)(?:($($h.Keys -join "|"))(\d+))+は?", {
$acum = $args.groups[1].value -as "int" # 3
$opes = $args.groups[2].captures.value # たす,ひく
$digits = $args.groups[3].captures.value -as "int[]" # 4,1
foreach ($i in 0..($digits.length -1)) {
$acum = invoke-command $calc[$opes[$i]] -Args $acum,$digits[$i]
}
$args.value.Replace("?", $acum)
})
# 3たす4ひく1は6です。
# 2たす1たす100ひく40たす23は86です。
入れ子になった括弧の中の置換
括弧のようなネストされるものを扱う時、グループ定義の均一化というものを使う。
上の例のように.NETにはグループ文字列の繰り返しや名前付きキャプチャなどでキャプチャしたものをスタックする機能がある。
名前付きキャプチャは(?<group1>expression)
とすることで同じグループとしてキャプチャできる(<group1>
は'group1'
と書いてもよい)。
$s = "108円と450円です"
$match = [regex]::Match($s,"(:?(?<group1>\d+円)|\D)*")
$match.Groups["group1"].Captures
# Index Length Value
# ----- ------ -----
# 0 3 108円
# 5 3 450円
グループ定義の均一化とは(?<-group1>expression)
とすることでexpression
にマッチした時、group1
グループのキャプチャスタックをポップする機能をいう。
$s = "108円と450円、じゃなくて430円です"
$match = [regex]::Match($s,"(:?(?<group1>\d+円)|(?<-group1>じゃなくて)|\D)*")
$match.Groups["group1"].Captures
# Index Length Value
# ----- ------ -----
# 0 4 108円
# 15 4 430円
もしgroup1
のスタックが空の時は全体が失敗する。
つまり開き括弧をgroup1
でキャプチャしておけば、閉じ括弧が多い場合などには失敗とすることができる。
これだけだと開き括弧が過多の場合には通ってしまうが、末尾に条件付きパターンを使って(?(group1)?!")
とすることでgroup1
スタックが空でない場合に失敗させることができる。
これらによって開き括弧と閉じ括弧の整合性をチェックすることができる。
さらに(?<group2-group1>expression)
とするとgroup1
のスタックをポップした後に、ポップした文字列とexpression
の間の文字列をgroup2
グループにスタックできる。
$s = "あいう(えお(かきく)けこ)さし(すせ)そ"
$regex = [regex]"^(?:[^()]|(?<open>[(])|(?<content-open>[)]))*(?(open)(?!))$"
$regex.Match($s).groups["content"].captures
# Index Length Value
# ----- ------ -----
# 7 3 かきく
# 4 9 えお(かきく)けこ
# 17 2 すせ
最上位の括弧の中を置き換える
$s = "a(bc(def)ghi(jk(lmn))opqr)st(uvw)xyz"
[regex]::Replace($s, "(?:[^()]|(?'open'[(])|(?'content-open'[)]))*", {
$result = $args.value
$end = 0
foreach($content in $args.groups[2].captures | sort index) {
$i = $content.index + $content.length
if($i -gt $end) {
$end = $i
$result = $result.Replace("($content)","(hello)")
}
}
$result
})
# a(hello)st(hello)xyz
上の例は括弧の整合性のチェックが目的ではないので末尾での開き括弧過多のチェックは省略してる。
まずopen
グループで開き括弧をキャプチャする。次にcontent-open
で閉じ括弧にマッチした時、open
グループのキャプチャスタックをポップし、同時にそこから閉じ括弧までの文字列をcontent
グループにスタックする。つまりそれぞれの括弧の中の文字はすべてcontent
グループにスタックされる。
スクリプトブロックではスタックされた文字列のインデックス情報から最上位のものだけを抽出して置き換えている。
(名前付きキャプチャしたものは$args.groups["content"]
で得られると思ったが[regex]::Replace
の引数のスクリプトブロック内ではなぜかできなかったのでインデックスで指定してる。バグ?)
連番
マッチ部分に連番を付加
$s = @"
PDCA
・ 計画
・ 実行
・ 評価
・ 改善
"@
$i = 1
[regex]::Replace($s, "・\s*(\w+)", { "{0}. {1}" -f $script:i++, $args.groups[1].value}) #
# PDCA
# 1. 計画
# 2. 実行
# 3. 評価
# 4. 改善
スクリプトブロック内でカウンタをインクリメントする。
$script:i
はスクリプトスコープの$i
を指定している。
パターンを使いまわす
同じパターンを何度も使うときはパターン文字列をRegex
オブジェクトにキャストすると少しすっきりする。
$s = "i have an apple."
$pattern = [regex]"\ba\w*"
$pattern.Replace($s, "Aword")
# i have Aword Aword.
最後に
速度的にはあまり速いとは言えないんで大量のファイルを置換するのには向かないかもしれない。