1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

正規表現を構造的に記述する方法

Last updated at Posted at 2024-12-25

正規表現はより厳密になればなるほど、より複雑になればなるほど、目視では読めなくなってしまいます。まるで minify された JavaScript コードのように。

なので私は、普通のプログラミング言語のようにインデントとかを付けて正規表現文の構造を分かりやすく表現できないものか、と常々考えています。とはいえ、正規表現の文字列中にスペースやら改行やらを入れてしまうと、それがパターンとして認識されてしまうので、直接入れるのは良くないです。

そこで、コード中に正規表現の文字列を埋め込むのであれば、文字列を結合させていくことでインデントっぽくできそうな気がしました。

ここでは、自分なりに構造を分かりやすくするように努めた正規表現を用意するためのコード列の例を紹介します。

具体例: Eメールアドレス

Email Address Regular Expression That 99.99% Works. にはほぼ全てのメールアドレスにマッチする正規表現が記載されています。

(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])

これを見ても正直よくわからないです。
JavaScript でメールアドレスにマッチする RegExp オブジェクトを作成するのであれば、次のように記載するのでしょうか。

const emailRe = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/;

これを構造的に書き表すと次のようになります。

const r = String.raw;
const bt = "`";
const emailRe = RegExp("".concat(
	r`(?:${ [
		"".concat(
			r`[a-z0-9!#$%&'*+/=?^_${bt}{|}~-]+`,
			r`(?:`,
				r`\.`,
				r`[a-z0-9!#$%&'*+/=?^_${bt}{|}~-]+`,
			r`)*`
		),
		r`"(?:${ [
			r`[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]`,
			r`\\[\x01-\x09\x0b\x0c\x0e-\x7f]`,
		].join("|") })*"`,
	].join("|") })`,
	r`@`,
	r`(?:${ [
		"".concat(
			r`(?:`,
				r`[a-z0-9]`,
				r`(?:`,
					r`[a-z0-9-]*`, r`[a-z0-9]`,
				r`)?`,
				r`\.`,
			r`)+`,
			r`[a-z0-9]`,
			r`(?:`,
				r`[a-z0-9-]*`, r`[a-z0-9]`,
			r`)?`
		),
		"".concat(
			r`\[`,
				r`(?:`,
					r`(?:${ [
						r`25[0-5]`, r`2[0-4][0-9]`,
						r`[01]?[0-9][0-9]?`,
					].join("|") })`,
					r`\.`,
				r`){3}`,
				r`(?:${ [
					r`25[0-5]`, r`2[0-4][0-9]`,
					r`[01]?[0-9][0-9]?`,
					"".concat(
						r`[a-z0-9-]*`,
						r`[a-z0-9]`,
						r`:`,
						r`(?:${ [
							r`[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]`,
							r`\\[\x01-\x09\x0b\x0c\x0e-\x7f]`,
						].join("|") })+`,
					)
				].join("|") })`,
			r`\]`
		)
	].join("|") })`
));

特徴としては

  • 正規表現リテラルの代わりに文字列リテラルを使って表し、結合することで正規表現を作っています
    • Python の r"" リテラルのように String.raw を使ってバックスラッシュをエスケープしないようにしています
    • 文字 ` は文字列リテラル中でエスケープせずに表記することはできないので、予め用意した上で、埋め込むという形をとりました
  • グループのネストの深さをインデントを付けて表しました
  • | 演算子による選択を配列として渡して、 .join("|") で結合するようにしました
  • なるべく構文の区切りごとに別の文字列に分け、後で結合してひとまとめにしています

minify しなくなったので縦に膨らんでしまいましが、見やすくなったのではないでしょうか。
しかし、決まったパターンの表記とかを丁寧に毎回記載しているので、それが見づらさを引き起こしている可能性があります。そこで、事前に関数を導入して、定型の表記を意味が分かりやすい形に書き直しました。

const r = String.raw;
const bt = "`";

/** 文字列の結合 */
const concat = (items: string[]): string => "".concat(...items);

/** 任意選択のパターン */
const or = (items: string[], suffix: string = ""): string => (
	r`(?:${items.join("|")})${suffix}`
);

// 特定のパターン (この部分の意味を知らないので、適当に `pat1`, `pat2` ... などと名付けています)
const pat1 = r`[a-z0-9!#$%&'*+/=?^_${bt}{|}~-]+`;
const pat2 = (suffix: ""|"+"|"*" = ""): string => (
	or([
		r`[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]`,
		r`\\[\x01-\x09\x0b\x0c\x0e-\x7f]`,
	], suffix)
);
const pat3 = concat([ r`[a-z0-9-]*`, r`[a-z0-9]` ]);
const pat4 = concat([
	r`[a-z0-9]`,
	r`(?:`, pat3, r`)?`
]);
const pat5 = [
	// そのまま `or` に渡される
	r`25[0-5]`,
	r`2[0-4][0-9]`,
	r`[01]?[0-9][0-9]?`
];

const emailRe = RegExp( concat([
	or([
		concat([
			pat1,
			r`(?:`, r`\.`, pat1, r`)*`
		]),
		pat2("*")
	]),
	r`@`,
	or([
		concat([
			r`(?:`,
				pat4,
				r`\.`,
			r`)+`,
			pat4
		]),
		concat([
			r`\[`,
				r`(?:`,
					or(pat5),
					r`\.`,
				r`){3}`,
				or([
					...pat5,
					concat([
						pat3, r`:`,
						pat2("+")
					])
				]),
			r`\]`
		])
	]),
]) );

意図がより分かりやすくなった気がします。

ただ、メールアドレスの正規表現に関しては記事 可能な限りRFCに準拠したEメールアドレス検証用正規表現 の表記の方がわかりやすいかもしれないですね。

具体例: CSSカラーコード

では次に、 CSS で指定可能な色表記を正規表現で表してみましょう。
データをパースすることに焦点を当ててやってみます。

仕様は MDN に書かれておりますが、このうち16進記法、 rgb, rgba, hsl, hsla のみに対応する正規表現を作ってみましょう。

/** 16進数の値 */
const hex = (name: string, digit: number): string => (
	concat([
		r`(?<${name}>`,
			r`[0-9a-fA-F]{${digit}}`,
		r`)`
	])
);

// 数値パターン / パーセント表記パターン / 角度表記パターン
const numberPat = concat([
	or([ r`[0-9]+`, r`[0-9]+\.[0-9]+`, r`\.[0-9]+` ]),
	or([ r`[eE][\+\-]?[0-9]+`, "" ])
]);
const signedNumberPat = concat([ r`[\+\-]?`, numberPat ]);
const percentPat = `${numberPat}%`;
const anglePat = or([
	"0",
	concat([
		signedNumberPat,
		or([ "deg", "rad", "grad", "turn" ])
	])
]);

/** 関数の引数型 */
type Arg = [name: string, pattern: string];
/** 引数のキャプチャグループを生成する */
const toArgGroup = ([name, pattern]: Arg) => r`(?<${name}>${pattern})`;

/** 関数表記を生成 */
const fn = (
	name: string,
	arg1: Arg, arg2: Arg, arg3: Arg, arg4: Arg
): string => {
	// 最初の3つのチャンネルのキャプチャグループを用意する
	const firstThree = [arg1, arg2, arg3].map(toArgGroup);

	// 関数全体を構成
	return concat([
		`(?<kind>${name})`,
		r`\(\s*`, or([
			// 旧式の表記: rgba(123,456,789,0.5)
			concat([
				// 最初の3つのチャンネル
				...firstThree.join(r`\s*\,\s*`),
				// アルファチャンネル
				or([ "", r`\s*\,\s*${toArgGroup(arg4)}` ])
			]),
			// 新式の表記: rgba(123 456 789 / 0.5)
			concat([
				// 最初の3つのチャンネル
				...firstThree.join(r`\s+`),
				// アルファチャンネル
				or([ "", r`\s*/\s*${toArgGroup(arg4)}` ])
			]),
		]), r`\s*\)`,
	]);
};

/** 色にマッチする正規表現 */
const colorRe = RegExp(concat([
	r`^\s*`, or([
		// 16進数表記
		concat([
			`(?<kind>#)`,
			or([
				// 3桁或いは4桁
				concat([
					hex("hexR1",1), hex("hexG1",1), hex("hexB1",1),
					or(["", hex("hexA1",1) ])
				]),
				// 6桁或いは8桁
				concat([
					hex("hexR2",2), hex("hexG2",2), hex("hexB2",2),
					or(["", hex("hexA2",2) ])
				]),
			]),
		]),
		// rgb, rgba
		fn(
			or([ "rgb", "rgba" ]),
			[ "r", or([ numberPat, percentPat, "none" ]) ],
			[ "g", or([ numberPat, percentPat, "none" ]) ],
			[ "b", or([ numberPat, percentPat, "none" ]) ],
			[ "a", or([ numberPat, percentPat, "none" ]) ]
		),
		// hsl, hsla
		fn(
			or([ "hsl", "hsla" ]),
			[ "h", or([ numberPat, anglePat  , "none" ]) ],
			[ "s", or([ numberPat, percentPat, "none" ]) ],
			[ "l", or([ numberPat, percentPat, "none" ]) ],
			[ "a", or([ numberPat, percentPat, "none" ]) ]
		)
	]), r`\s*$`
]));

/** キャプチャグループを表す型 */
type Groups = (
	| ( { kind: "#" } & (
		// 3/4桁表記と、6/8桁の表記のうち一方が `string` で他方が `undefined` になる
		| ( Hex1<string>    & Hex2<undefined> )
		| ( Hex1<undefined> & Hex2<string>    )
	) )
	| {
		kind: "rgb" | "rgba",
		r: string, g: string, b: string, a: string
	}
	| {
		kind: "hsl" | "hsla",
		h: string, s: string, l: string, a: string
	}
);

type Hex1<T> = { hexR1: T, hexG1: T, hexB1: T, hexA1: T | undefined };
type Hex2<T> = { hexR2: T, hexG2: T, hexB2: T, hexA2: T | undefined };

/** 色について何かしらの処理を行う関数 */
const handleColor = (input: string) => {
	// 正規表現とのマッチを行う
	const matchArray = input.match(colorRe);
	// マッチしているか判定する
	if (matchArray == null) return;
	// キャプチャグループを全て取り出す
	const groups = matchArray.groups! as Groups;
	// 以降諸々の処理が続く
};

このコードで生成される正規表現 colorRe は先ほどのメールアドレスよりも長い正規表現パターン列になっています。これを上記のように組み立てずに直接正規表現として書いてしまうと人間の目で追えるコードではなくなってしまいます。そういう意味でも、組み立てていくように正規表現を書いた方が良さそうです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?