できない.dev

正規表現の特殊文字をエスケープするには

ユーザー入力のような外から来た文字列を、正規表現の中でそのままの文字として探すために、特殊文字をエスケープする方法を各言語で示す。
エスケープ関数の出力の違いと、置換文字列の側にも別のエスケープが要る点までを扱う。

公開:

選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。

Python

import re
 
text = "v1.2.3 / v1x2y3 / (beta) / a+b"
keyword = "1.2.3"
 
print(re.findall(keyword, text))             # ['1.2.3', '1x2y3']
print(re.findall(re.escape(keyword), text))  # ['1.2.3']
 
print(re.escape("a.b*c (1+1)?"))             # a\.b\*c\ \(1\+1\)\?
 
try:
    re.compile("(beta")
except re.error as e:
    print(e)                                 # missing ), unterminated subpattern at position 0
print(re.findall(re.escape("(beta"), text))  # ['(beta']
 
# 複数の語のどれかに一致させる
pattern = "|".join(map(re.escape, ["a+b", "1.2.3"]))
print(pattern)                               # a\+b|1\.2\.3
print(re.findall(pattern, text))             # ['1.2.3', 'a+b']
 
# 置換文字列の \ も特別扱いされる。そのまま入れたいときは関数で返す
try:
    re.sub(re.escape("a+b"), r"C:\users", text)
except re.error as e:
    print(e)                                 # bad escape \u at position 2
print(re.sub(re.escape("a+b"), lambda m: r"C:\users", text))  # v1.2.3 / v1x2y3 / (beta) / C:\users

re.escape は正規表現で特別な意味を持つ文字の前に \ を付けるので、外から来た文字列を re.findall や re.compile に渡す前に通す。
通さないと 1.2.3 の . が任意の 1 文字になって 1x2y3 にも一致し、(beta は re.error になる。
置換文字列の側は re.escape では守れないため、\ を含む文字列は re.sub に関数で返す。

JavaScript

const text = "v1.2.3 / v1x2y3 / (beta) / a+b";
const keyword = "1.2.3";
 
console.log(text.match(new RegExp(keyword, "g")));  // [ '1.2.3', '1x2y3' ]
 
// 標準の RegExp.escape は Node.js 24 以降。それ以前は自前の関数を使う
function escapeRegExp(s) {
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
console.log(text.match(new RegExp(escapeRegExp(keyword), "g")));  // [ '1.2.3' ]
console.log(escapeRegExp("a.b*c (1+1)?"));          // a\.b\*c \(1\+1\)\?
console.log(RegExp.escape?.("a.b*c (1+1)?"));       // 24: \x61\.b\*c\x20\(1\+1\)\?(22 は undefined)
 
try {
  new RegExp("(beta");
} catch (e) {
  console.log(e.message);                           // Invalid regular expression: /(beta/: Unterminated group
}
console.log(text.match(new RegExp(escapeRegExp("(beta"), "g")));  // [ '(beta' ]
 
// 複数の語のどれかに一致させる
const pattern = ["a+b", "1.2.3"].map(escapeRegExp).join("|");
console.log(pattern);                               // a\+b|1\.2\.3
console.log(text.match(new RegExp(pattern, "g")));  // [ '1.2.3', 'a+b' ]
 
// 置換文字列では $& などが展開される。そのまま入れたいときは関数で返す
const target = new RegExp(escapeRegExp("a+b"));
console.log(text.replace(target, "[$&]"));          // v1.2.3 / v1x2y3 / (beta) / [a+b]
console.log(text.replace(target, () => "[$&]"));    // v1.2.3 / v1x2y3 / (beta) / [$&]

標準の RegExp.escape は Node.js 24 以降でしか使えないので、それ以前は特殊文字の前に \ を付ける関数 escapeRegExp を用意する。
- は変えない作りにしてあり、u フラグの下では文字クラスの外の \- が SyntaxError になるためである。
置換文字列の $& は展開されるので、そのまま入れたいときは replace に関数を渡す。

TypeScript

function escapeRegExp(s: string): string {
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
 
// 検索語を強調するための正規表現。語は必ずエスケープしてから | でつなぐ
function keywordRegExp(words: readonly string[]): RegExp {
  // 空の配列だと空のパターンになり全位置に一致するので、何にも一致しない式を返す
  if (words.length === 0) return /(?!)/g;
  return new RegExp(words.map(escapeRegExp).join("|"), "g");
}
 
const text: string = "v1.2.3 / v1x2y3 / (beta) / a+b";
const re: RegExp = keywordRegExp(["a+b", "1.2.3"]);
 
console.log(re.source);                                      // a\+b|1\.2\.3
console.log(text.match(re));                                 // [ '1.2.3', 'a+b' ]
console.log(text.replace(re, (hit: string) => `[${hit}]`));  // v[1.2.3] / v1x2y3 / (beta) / [a+b]
console.log(text.match(keywordRegExp([])));                  // null
 
// 固定の文字列を置き換えるだけなら、正規表現を作らずに replaceAll へ文字列を渡せる
console.log(text.replaceAll("a+b", "X"));                    // v1.2.3 / v1x2y3 / (beta) / X
 
// 置換文字列の $& などは展開される。そのまま入れたいときは関数で返す
console.log(text.replaceAll("a+b", "[$&]"));                 // v1.2.3 / v1x2y3 / (beta) / [a+b]
console.log(text.replaceAll("a+b", () => "[$&]"));           // v1.2.3 / v1x2y3 / (beta) / [$&]

検索語を強調する正規表現を作る関数 keywordRegExp では、語を escapeRegExp に通してから | でつなぐ。
語の配列が空だと空のパターンになって全位置に一致してしまうため、何にも一致しない /(?!)/g を返している。
固定の文字列を置き換えるだけなら、正規表現を作らずに replaceAll へ文字列を渡せる。

Go

package main
 
import (
	"fmt"
	"regexp"
	"strings"
)
 
func main() {
	text := "v1.2.3 / v1x2y3 / (beta) / a+b"
	keyword := "1.2.3"
 
	fmt.Println(regexp.MustCompile(keyword).FindAllString(text, -1))                    // [1.2.3 1x2y3]
	fmt.Println(regexp.MustCompile(regexp.QuoteMeta(keyword)).FindAllString(text, -1)) // [1.2.3]
 
	fmt.Println(regexp.QuoteMeta("a.b*c (1+1)?")) // a\.b\*c \(1\+1\)\?
 
	_, err := regexp.Compile("(beta")
	fmt.Println(err) // error parsing regexp: missing closing ): `(beta`
 
	// 複数の語のどれかに一致させる
	var quoted []string
	for _, w := range []string{"a+b", "1.2.3"} {
		quoted = append(quoted, regexp.QuoteMeta(w))
	}
	pattern := strings.Join(quoted, "|")
	fmt.Println(pattern)                                             // a\+b|1\.2\.3
	fmt.Println(regexp.MustCompile(pattern).FindAllString(text, -1)) // [1.2.3 a+b]
 
	// 置換文字列の $1 は展開される(存在しないグループは空になる)。そのまま入れたいときは ReplaceAllLiteralString
	re := regexp.MustCompile(regexp.QuoteMeta("a+b"))
	fmt.Println(re.ReplaceAllString(text, "[$1]"))        // v1.2.3 / v1x2y3 / (beta) / []
	fmt.Println(re.ReplaceAllLiteralString(text, "[$1]")) // v1.2.3 / v1x2y3 / (beta) / [$1]
}

regexp.QuoteMeta は正規表現で特別な意味を持つ文字の前に \ を付ける。
不正なパターンを regexp.Compile に渡すとエラーが返り、regexp.MustCompile に渡すと panic するので、外から来た文字列は QuoteMeta に通してから渡す。
ReplaceAllString は置換文字列の $1 を展開し、存在しないグループは空にするため、そのまま入れたいときは ReplaceAllLiteralString を使う。

Rust

use regex::{NoExpand, Regex};
 
fn find_all<'a>(pattern: &str, text: &'a str) -> Vec<&'a str> {
    Regex::new(pattern).unwrap().find_iter(text).map(|m| m.as_str()).collect()
}
 
fn main() {
    let text = "v1.2.3 / v1x2y3 / (beta) / a+b";
    let keyword = "1.2.3";
 
    println!("{:?}", find_all(keyword, text));                // ["1.2.3", "1x2y3"]
    println!("{:?}", find_all(&regex::escape(keyword), text)); // ["1.2.3"]
 
    println!("{}", regex::escape("a.b*c (1+1)?")); // a\.b\*c \(1\+1\)\?
 
    let err = Regex::new("(beta").unwrap_err();
    println!("{}", err.to_string().lines().last().unwrap()); // error: unclosed group
 
    // 複数の語のどれかに一致させる
    let words = ["a+b", "1.2.3"];
    let pattern = words.iter().map(|w| regex::escape(w)).collect::<Vec<_>>().join("|");
    println!("{}", pattern);                   // a\+b|1\.2\.3
    println!("{:?}", find_all(&pattern, text)); // ["1.2.3", "a+b"]
 
    // 置換文字列の $name は展開される(存在しない名前は空になる)。そのまま入れたいときは NoExpand
    let re = Regex::new(&regex::escape("a+b")).unwrap();
    println!("{}", re.replace_all(text, "[$1]"));           // v1.2.3 / v1x2y3 / (beta) / []
    println!("{}", re.replace_all(text, NoExpand("[$1]"))); // v1.2.3 / v1x2y3 / (beta) / [$1]
}

regex::escape は regex クレートの関数で、特殊文字の前に \ を付けて返す。
Regex::new は不正なパターンに Err を返すので、外から来た文字列は regex::escape に通してから渡す。
置換文字列の $name は展開され、存在しない名前は空になるため、そのまま入れたいときは NoExpand で包む。

Java

import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
 
public class Main {
    static List<String> find(Pattern p, String text) {
        List<String> hits = new ArrayList<>();
        Matcher m = p.matcher(text);
        while (m.find()) hits.add(m.group());
        return hits;
    }
 
    public static void main(String[] args) {
        String text = "v1.2.3 / v1x2y3 / (beta) / a+b";
        String keyword = "1.2.3";
 
        System.out.println(find(Pattern.compile(keyword), text));                // [1.2.3, 1x2y3]
        System.out.println(find(Pattern.compile(Pattern.quote(keyword)), text)); // [1.2.3]
 
        System.out.println(Pattern.quote("a.b*c (1+1)?")); // \Qa.b*c (1+1)?\E
 
        try {
            Pattern.compile("(beta");
        } catch (PatternSyntaxException e) {
            System.out.println(e.getDescription()); // Unclosed group
        }
 
        // 複数の語のどれかに一致させる
        String pattern = String.join("|", Pattern.quote("a+b"), Pattern.quote("1.2.3"));
        System.out.println(pattern);                              // \Qa+b\E|\Q1.2.3\E
        System.out.println(find(Pattern.compile(pattern), text)); // [1.2.3, a+b]
 
        // 置換文字列の $ と \ も特別扱いされる。そのまま入れたいときは Matcher.quoteReplacement
        Pattern p = Pattern.compile(Pattern.quote("a+b"));
        try {
            p.matcher(text).replaceAll("[$1]");
        } catch (IndexOutOfBoundsException e) {
            System.out.println(e.getMessage()); // No group 1
        }
        System.out.println(p.matcher(text).replaceAll(Matcher.quoteReplacement("[$1]"))); // v1.2.3 / v1x2y3 / (beta) / [$1]
    }
}

Pattern.quote は文字列を \Q…\E で囲み、中身をそのままの文字として扱わせる。
文字列に \E が含まれていても、\Qx\E\\E\Qy\E のように囲み直して正しく扱う。
replaceAll の置換文字列は $ と \ が特別扱いされ、存在しないグループを $1 で参照すると IndexOutOfBoundsException になるので、Matcher.quoteReplacement を通す。

C#

using System;
using System.Linq;
using System.Text.RegularExpressions;
 
string text = "v1.2.3 / v1x2y3 / (beta) / a+b";
string keyword = "1.2.3";
 
static string Hits(string text, string pattern) =>
    string.Join(", ", Regex.Matches(text, pattern).Select(m => m.Value));
 
Console.WriteLine(Hits(text, keyword));               // 1.2.3, 1x2y3
Console.WriteLine(Hits(text, Regex.Escape(keyword))); // 1.2.3
 
Console.WriteLine(Regex.Escape("a.b*c (1+1)?")); // a\.b\*c\ \(1\+1\)\?
 
try
{
    Regex.IsMatch(text, "(beta");
}
catch (ArgumentException e)
{
    Console.WriteLine(e.Message); // Invalid pattern '(beta' at offset 5. Not enough )'s.
}
 
// 複数の語のどれかに一致させる
string pattern = string.Join("|", new[] { "a+b", "1.2.3" }.Select(Regex.Escape));
Console.WriteLine(pattern);             // a\+b|1\.2\.3
Console.WriteLine(Hits(text, pattern)); // 1.2.3, a+b
 
// 置換文字列の $0 などは展開される。そのまま入れたいときは $ を $$ にするか、関数で返す
string target = Regex.Escape("a+b");
Console.WriteLine(Regex.Replace(text, target, "[$0]"));      // v1.2.3 / v1x2y3 / (beta) / [a+b]
Console.WriteLine(Regex.Replace(text, target, "[$$0]"));     // v1.2.3 / v1x2y3 / (beta) / [$0]
Console.WriteLine(Regex.Replace(text, target, m => "[$0]")); // v1.2.3 / v1x2y3 / (beta) / [$0]

Regex.Escape は . や ( などの特殊文字と、#・空白の前に \ を付ける。
不正なパターンを Regex.IsMatch などに渡すと ArgumentException(実体は派生クラスの RegexParseException)になるので、外から来た文字列は先に通す。
置換文字列の $0 は展開されるため、そのまま入れたいときは $$ に重ねるか、関数で返す。

エスケープしないと何が起きるか

検索語やファイル名のように外から来た文字列をそのままパターンに入れると、. や + が正規表現として働く。
1.2.3 を探すつもりでも . が任意の 1 文字になり、1x2y3 にも一致した(7 言語すべてで確かめた)。

(beta のように括弧が閉じていない文字列は、パターンとして成立せず、コンパイルの時点で失敗する。

  • Python: re.error
  • JavaScript・TypeScript: SyntaxError
  • Java: PatternSyntaxException
  • C#: ArgumentException の派生クラスの RegexParseException
  • Go・Rust: regexp.Compile・Regex::new が返すエラー

外から来た文字列をパターンに入れるときは、必ずエスケープ関数を通す。

エスケープ関数の出力は言語で違う

a-b_c/d:e@f#g&h~i という文字列を各言語のエスケープ関数に通して、結果を比べた。

  • Python の re.escape: a\-b_c/d:e@f\#g\&h\~i(-・#・&・~ と空白が対象で、_・/・:・@ は対象外)
  • Rust の regex::escape: Python と同じ a\-b_c/d:e@f\#g\&h\~i
  • C# の Regex.Escape: a-b_c/d:e@f\#g&h~i(この文字列で変わるのは # だけ。ほかに空白がエスケープされ、] と } はエスケープされない)
  • Go の regexp.QuoteMeta と JavaScript の自前関数: 入力と同じ a-b_c/d:e@f#g&h~i
  • Java の Pattern.quote: 文字は変えず、全体を \Q…\E で囲む(\Qa.b*c (1+1)?\E)
  • JavaScript の RegExp.escape(Node.js 24 以降): 先頭の英数字を \x61、空白を \x20、- を \x2d のように書く

文字クラス [...] の中に文字列を埋め込むときは、- を変えない関数だと意味が変わる。
JavaScript の自前関数では "[" + escapeRegExp("a-z") + "]" が m に一致して - に一致せず、Go の QuoteMeta も同じだった。
Python の re.escape は - を \- にするので、m に一致せず - に一致した。

JavaScript の自前関数が - を変えないのは、u フラグの下では文字クラスの外の \- が SyntaxError になるためである(new RegExp("a\\-b", "u") で確かめた)。
RegExp.escape が書く \x2d は u フラグの下でも通り、文字クラスの中に入れても - として扱われた。

置換文字列の側にも別のエスケープが要る

パターンをエスケープしても、置換文字列の中の $ や \ は守られない。
置換文字列は別の小さな構文として解釈されるので、そのまま入れたいときは言語ごとに別の手段を使う。

  • Python: \ が特別扱いされる。r"C:\users" は re.error: bad escape \u at position 2 になり、r"C:\temp" はエラーにならず \t がタブに変わる。関数で返せば文字どおり入る
  • JavaScript・TypeScript: $& が一致した文字列に展開される。関数で返せば $& のまま入る
  • Go: ReplaceAllString が $1 を展開し、存在しないグループは空になる。ReplaceAllLiteralString を使う
  • Rust: replace_all が $name を展開し、存在しない名前は空になる。NoExpand で包む
  • Java: 存在しないグループを $1 で参照すると IndexOutOfBoundsException: No group 1 になる。Matcher.quoteReplacement を通す
  • C#: $0 が一致した文字列に展開される。$$0 と書くか、関数で返す

正規表現を使わずに済む場面

固定の文字列を探して置き換えるだけなら、正規表現を使わない API で足りる。
次の呼び出しは、axb a.b の a.b の . を文字どおりに扱い、a.b だけを置き換えた。

  • Python: str.replace
  • JavaScript・TypeScript: replaceAll に文字列を渡す
  • Go: strings.ReplaceAll
  • Rust: str::replace
  • Java: String.replace
  • C#: string.Replace

名前が似た Java の String.replaceAll は引数が正規表現で、"axb a.b".replaceAll("a.b", "X") は X X になった。
String.split も正規表現を取り、"a.b".split(".") は空の配列、"a.b".split(Pattern.quote(".")) は [a, b] を返した。

JavaScript の replaceAll("a+b", "[$&]") のように、文字列で探す場合でも置換文字列の $& は展開される。

出力例について

コード中のコメントは、Docker の公式イメージ(Python 3.14、Node.js 22、Go 1.27、Rust 1.99 と regex クレート 1.13、OpenJDK 21、.NET SDK 8.0)の Linux で 2026-10-20 に実行した結果である。
TypeScript は Node.js 22 上で tsx を使って実行し、tsc --strict の型検査も通した。
JavaScript の RegExp.escape の行は Node.js 24 でも実行し、コメントに両方の結果を書いた。

このコードで動いた?