正規表現の特殊文字をエスケープするには
ユーザー入力のような外から来た文字列を、正規表現の中でそのままの文字として探すために、特殊文字をエスケープする方法を各言語で示す。
エスケープ関数の出力の違いと、置換文字列の側にも別のエスケープが要る点までを扱う。
公開:
選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。
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:\usersre.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(®ex::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(®ex::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 でも実行し、コメントに両方の結果を書いた。