できない.dev

正規表現で置換するには

パターンに一致した箇所を別の文字列へ置き換える基本形を各言語で示す。
既定が全件置換か 1 件だけかの違いと、置換文字列の中でキャプチャグループを参照する書き方までを扱う。

公開:

各言語見出しの横のバッジは検証状態を表す。実行確認済みはコードを実際に実行して確認したもの、静的確認は構文と公式 API ドキュメントで確認したものである。

Python 実行確認済み

import re
 
text = "tel: 090-1234-5678 / 080-8765-4321"
 
print(re.sub(r"\d{4}$", "****", text))                # 末尾だけ
print(re.sub(r"(\d{3})-(\d{4})-(\d{4})", r"\1-****-****", text))  # 後方参照
print(re.sub(r"\d", "#", text, count=3))              # 置換回数を制限する
print(re.sub(r"\d{4}", lambda m: str(len(m.group())), text))  # 関数で置換する

re.sub は既定で一致した箇所をすべて置き換え、count を渡すと回数を制限できる。
置換文字列の中では \1 のように後方参照が使えるので、raw 文字列で書くとバックスラッシュの二重化を避けられる。
関数を渡すと一致ごとに結果を組み立てられる。

JavaScript 実行確認済み

const text = "tel: 090-1234-5678 / 080-8765-4321";
 
// 文字列の replace は最初の 1 件しか置換しない
console.log(text.replace(/\d{4}/, "****"));
// すべて置換するには g 付きの正規表現か replaceAll を使う
console.log(text.replace(/\d{4}/g, "****"));
// $1 で後方参照する
console.log(text.replace(/(\d{3})-(\d{4})-(\d{4})/g, "$1-****-****"));
// 関数を渡すと一致ごとに結果を組み立てられる
console.log(text.replace(/\d{4}/g, (hit) => String(hit.length)));

String.prototype.replace は既定で最初の 1 件しか置換しない。
すべて置き換えるには g 付きの正規表現を渡すか replaceAll を使う。
置換文字列の中では $1 で後方参照でき、関数を渡した場合は一致文字列が引数として渡ってくる。

TypeScript 実行確認済み

const text: string = "tel: 090-1234-5678 / 080-8765-4321";
 
const masked: string = text.replace(/(\d{3})-(\d{4})-(\d{4})/g, "$1-****-****");
console.log(masked);
 
// 置換関数の引数は string で渡ってくるので、型注釈を付けると意図が読み取りやすい
const shortened: string = text.replace(/\d{4}/g, (hit: string): string =>
  String(hit.length)
);
console.log(shortened);

使い方は JavaScript と同じで、戻り値が string として型付けされるだけである。
置換関数の引数も string なので、明示的に型を書いておくと、後から引数を増やしたときに取り違えにくい。
元の文字列は変更されず、常に新しい文字列が返る。

Go 静的確認

package main
 
import (
	"fmt"
	"regexp"
)
 
func main() {
	text := "tel: 090-1234-5678 / 080-8765-4321"
	re := regexp.MustCompile(`(\d{3})-(\d{4})-(\d{4})`)
 
	fmt.Println(re.ReplaceAllString(text, "$1-****-****"))
	// $1x は「1x という名前のグループ」として読まれるので、続けるときは中括弧で囲む
	fmt.Println(re.ReplaceAllString(text, "${1}-hidden"))
}

ReplaceAllString は一致した箇所をすべて置き換える。
置換文字列の中では $1 で後方参照できるが、$1x と続けて書くと「1x という名前のグループ」として読まれてしまうため、直後に文字が続くときは ${1} と中括弧で囲む。

Rust 静的確認

use regex::Regex;
 
fn main() {
    let text = "tel: 090-1234-5678 / 080-8765-4321";
    let re = Regex::new(r"(\d{3})-(\d{4})-(\d{4})").unwrap();
 
    println!("{}", re.replace_all(text, "$1-****-****")); // 全件
    println!("{}", re.replace(text, "***"));              // 最初の 1 件だけ
}

replace_all が全件、replace が最初の 1 件で、どちらも Cow<str> を返すため元の文字列は変わらない。
置換文字列の中では $1 や $name で後方参照できる。
存在しない番号や名前を書いた場合は、エラーにならず空文字に置き換わる。

Java 実行確認済み

public class RegexReplace {
    public static void main(String[] args) {
        String text = "tel: 090-1234-5678 / 080-8765-4321";
 
        System.out.println(text.replaceAll("\\d{4}", "****"));   // 全件
        System.out.println(text.replaceFirst("\\d{4}", "****")); // 最初の 1 件だけ
        System.out.println(text.replaceAll("(\\d{3})-(\\d{4})-(\\d{4})", "$1-****-****"));
        // replace は正規表現ではなく文字列そのものを探す
        System.out.println(text.replace("090", "0XX"));
    }
}

String.replaceAll は正規表現として解釈し、replaceFirst は最初の 1 件だけを置き換える。
名前が似ている replace は正規表現ではなく文字列そのものを探すので、メタ文字をそのまま扱いたいときはこちらを使う。
置換文字列の中では $1 で後方参照する。

C# 実行確認済み

using System.Text.RegularExpressions;
 
var text = "tel: 090-1234-5678 / 080-8765-4321";
 
Console.WriteLine(Regex.Replace(text, @"\d{4}", "****"));                 // 全件
// 件数を絞りたいときは静的メソッドではなくインスタンス側の Replace を使う
Console.WriteLine(new Regex(@"\d{4}").Replace(text, "****", 1));
Console.WriteLine(Regex.Replace(text, @"(\d{3})-(\d{4})-(\d{4})", "$1-****-****"));
// MatchEvaluator を渡すと一致ごとに結果を組み立てられる
Console.WriteLine(Regex.Replace(text, @"\d{4}", m => m.Value.Length.ToString()));

Regex.Replace は既定で全件を置き換える。
件数を絞りたいときは静的メソッドではなく Regex インスタンスの Replace に count を渡す。
置換文字列の中では $1 で後方参照でき、MatchEvaluator を渡すと一致ごとに結果を組み立てられる。

つまずき

既定が全件置換なのか 1 件だけなのかは言語で分かれる。
Python の re.sub・Go の ReplaceAllString・C# の Regex.Replace は全件だが、JavaScript の replace と Java の replaceFirst は 1 件だけである。
「1 件しか変わらない」ときは、まずここを疑う。

置換文字列の中の $ と \

置換文字列は素のテキストではなく、後方参照を解釈する小さな構文を持つ。
$ や \ をそのまま出したい場合はエスケープが必要になる。
ユーザー入力をそのまま置換文字列に渡すと意図しない参照として解釈されるので、必要なら言語ごとのエスケープ関数を通す。

この記事は役立ちましたか?