Help us understand the problem. What is going on with this article?

【PHP】PCRE関数で使える、置換に便利な参照記号$nを使いこなす(+JS・Perl・Ruby・Pythonでの動作検証)

元々は、PHP初級者向けに作った軽いネタです。正規表現におけるパターンマッチングのことは触れていても、案外置換における便利な記号リテラル(参照記号)のことに触れていない記事が多かった(というより、パターンマッチングの後方一致記号と被るので、そっちの記事が優先的に現れ検索されにくい…)のでおさらいしてみました。

…また、各言語によって記述の仕方に癖があることがわかったので、要点整理しています。

PHP

PHPには正規表現を用いた関数(PCRE関数というらしい)としてpreg_xxxxxというのがあります。代表的な関数としては

preg_match(パターン,対象文字列) //検索に用います
preg_replace(パターン,置換文字,対象文字列) //置換に用います

などがあります。では今回はこのpreg_replace関数についてです。そして、この関数は、エディタと同じように参照記号$nを使用することができます。なお、今回は正規表現の記法はある程度熟知しているものとして話を進めていきます。

実例

たとえば、住所録を整理していて、住所の表記揺れを修正したいとします。とりあえず、丁目は置いといて「xxのxx」や「xx番xx」を「xx-xx」に修正したい場合は参照記号を用いれば効果的に修正ができます。

PHP
$ary_addr = [
"兵庫県尼崎市東七松町1丁目23−1",
"兵庫県西宮市六湛寺町10の3",
"兵庫県宝塚市東洋町1番1"
];

置換のおさらい

ここで置換のおさらいです。置換元のパターンは、置換させたくないパターンを()で囲みます。そして置換先の文字列に合わせ、グループ化記号を用いた個数だけ参照記号$1,$2,$3,…と設定していきます。

たとえば、住所の「xxのxx」を「xx-xx」に置換したい場合はこのように記述します。

PHP
$addr = preg_replace("/(\d+)の(\d+)/u",$1-$2,$addr);

一方、パターンマッチングに用いる()はパターンのグループ化にも用いる記号なので、置換させたい場所と区別できない場合があります。たとえば、住所の「xxのxx」と「xx番地xx」を「xx-xx」に置換したい場合は以下のように記述することになるので、その場合は参照記号を飛ばして対応させるようにします(つまり、$2の部分が置換対象となるので、参照記号を表記せずに、置換したい文字をそのまま記述する)。

PHP
$addr = preg_replace("/(\d+)(番地|の)(\d+)/u",$1-$3,$addr);

実務的な使い方

これを踏まえて、実務的に搭載してみるとこのような記述になります。ここでは数字も半角に統一、そして「xx番」「xx番地」という表記も数値だけに修正しています。

PHP
//市役所の住所録を格納
$ary_addr = [
"兵庫県尼崎市東七松町1丁目23−1",
"兵庫県西宮市六湛寺町10の3",
"兵庫県宝塚市東洋町1番1号"
];

//※配列の各値に対し、処理を行う。なお、ジェネレータの方が速いです
array_walk($ary_addr,function(&addr){
    //複数繰り返しに対応させる
    $addr = mb_convert_kana($addr,"n"); //数字を半角に
    //「xxのxx」などがなくなるように繰り返す
    while(preg_match("/\d+(番地?|の)\d+/u",$addr) != false){
         $addr = preg_replace("/(\d+)(番地?|の)(\d+)/u","$1-$3",$addr);
    }
    $addr = preg_replace("/(\d+)(番地?|号)/u","$1",$addr);
});

これを出力すると以下のようになります。「丁目」も変更したい場合はそれもマッチングのグループ候補に入れるといいでしょう。

var_dump($ary_addr);
/*以下のように置換されていきます
兵庫県尼崎市東七松町1丁目23−1",
兵庫県西宮市六湛寺町10-3",
兵庫県宝塚市東洋町1-1"
*/

なお、今までは参照記号の説明のために回りくどいことをしていましたが、実際はデフォルトで繰り返し処理してくれるので、この書き方だけで実行してくれます。

PHP
array_walk($ar_addr,function(&$addr){$addr = preg_replace("/(\d+)(の|番地?)/u","$1-",$addr);});

応用編

参照記号の直後に数値が続く場合

このような場合、そのまま記述すると参照番号12なのか、それとも参照番号1の後に数字の2が来ているのか判別できません。この対処法は公式マニュアルにも説明があります。

$title = "基本1";
$title = preg_match("/(\W+)2/u","$12",$title);

その場合は、以下のように括弧を加えて書き換えてください。その際、PHPだとシングルクォートで囲まないとリテラルを認識しないので注意してください。

$title = "基本1";
//クォートなしだと構文エラー、ダブルクォートだと正しく参照番号を評価しません。
$title = preg_match("/(\W+)2/u",'${1}2',$title);
var_dump($title); //基本2 

グループが入れ子となっている場合

左から順に、括弧が始まるグループに対して、参照番号が振られていく仕様となっています。ただし、これで複数箇所の置換をやろうとしてもうまく行かないことが多いですが、部分的な値を抽出したい場合には知っておくと役立ちます。

PHP
$addr = "兵庫県神戸市中央区加納町6丁目5−1";
$pattern = "/((\S{2,3}[都道府県])((\S{1,6}市)(\S{1,4}区)?|(\S{1,4}郡)(\S{1,5}[町|村])))([^\d]+)?(((\d+[の|番地|-|丁目?])*\d+))/u";

マッチング結果をpreg_match関数の第3引数で出力(※戻り値に非ず)

これにpreg_match関数を用いて、各参照記号に置換すると以下の値を評価していくことになります。そして第3引数に変数を代入することで、マッチング結果を配列で抽出することができます。

PHP
preg_match($pattern,$addr,$match); //$matchにはマッチング結果が代入される
var_dump($match);
/*以下のように出力される
array(11) {
  [0]=>
  string(37) "兵庫県神戸市中央区加納町6"
  [1]=>
  string(27) "兵庫県神戸市中央区"
  [2]=>
  string(9) "兵庫県"
  [3]=>
  string(18) "神戸市中央区"
  [4]=>
  string(9) "神戸市"
  [5]=>
  string(9) "中央区"
  [6]=>
  string(0) ""
  [7]=>
  string(0) ""
  [8]=>
  string(9) "加納町"
  [9]=>
  string(1) "6"
  [10]=>
  string(1) "6"
}
*/

なので、参照記号を用いて以下のように、ピンポイントでデータを抽出することができます。

PHP
$pref = preg_replace($pattern,$2,$addr); //都道府県のみを取得
$address = preg_replace($pattern,$3,$addr); //市区町村のみを取得
$aza = preg_replace($pattern,$8,$addr); //大字だけを取得

町村の場合ならこうなります。同じパターンでマッチングさせる限りは、参照記号が示す値が変化することはありません。

PHP
$addr = "兵庫県川辺郡猪名川町上野北畑11−1";
preg_match($pattern,$addr,$match); //$matchにはマッチング結果が代入される
var_dump($match);
/*以下のように出力される
array(11) {
  [0]=>
  string(44) "兵庫県川辺郡猪名川町上野北畑11"
  [1]=>
  string(30) "兵庫県川辺郡猪名川町"
  [2]=>
  string(9) "兵庫県"
  [3]=>
  string(21) "川辺郡猪名川町"
  [4]=>
  string(0) ""
  [5]=>
  string(0) ""
  [6]=>
  string(9) "川辺郡"
  [7]=>
  string(12) "猪名川町"
  [8]=>
  string(12) "上野北畑"
  [9]=>
  string(2) "11"
  [10]=>
  string(2) "11"
}
*/

他言語での検証

正規表現で置換の働きを持つ関数ならば、概ね使えるようです(検証済)。ただし、色々と条件があるようなので、注意点をまとめておきました。なお、一部のオープン系環境しか構築していないので、C、C#、JAVA、GO、R、Swiftなどは未検証です。

JavaScript

  • jsの場合は置き換え文字列をダブルクォートで囲まないとエラーになる。
  • デフォルトは1回だけなので、複数回繰り返す場合はg修飾子を付ける。
JavaScript
   let str = "11の3の5";
   str1 = str.replace(/(\d+)(の|番地?)/,"$1-"); //単独
   str2 = str.replace(/(\d+)(の|番地?)/g,"$1-"); //複数回
   console.log(str1); //11-3の5
   console.log(str2); //11-3-5

Perl5

  • 置換文字列を使用する場合はe修飾子を忘れないこと。また、繰り返し処理する場合はg修飾子もつける。
  • パターンはダブルクォートで囲むとエラーになる。
  • また、置換文字をダブルクォートで囲まないと計算式として処理される場合がある。
Perl
 my $str =  "11の3の5";
 my $str1 = str;
 my $str2 = $str;
 $str1 =~ s/(\d+)(の|番地?)/"$1-"/e; #単独
 $str2 =~ s/(\d+)(の|番地?)/"$1-"/eg; #複数回
 print $str1; #11-3の5
 print $str2; #11-3-5        

Ruby2.x

  • パターンはクォートで囲っても囲まなくてもよいが、デリミタを付けないとパターンとして認識されない。
  • 単独の場合はsub、複数の場合はgsubを用いる。
  • 置換文字列はシングルクォートかダブルクォートで囲まないとエラーが起きる。
  • 置換の参照記号は\nを使うが、その場合はシングルクォートにしないと文字列として認識してしまう。
Ruby
str = "11の3の5"
str1 = str.sub(/(\d+)(の|番地?)/,'\1-') #単独
str2 = str.gsub(/(\d+)(の|番地?)/,'\1-') #複数回
print str1 #11-3の5 
print str2 #11-3-5

Python3

  • パターンと置換文字をシングル(ダブル)クォートで囲まないとエラーになる。
  • パターンはデリミタを囲まない(囲むとそのデリミタもパターンチェック対象となってしまう)。
  • replaceを使う場合は、参照記号に$nを用いる(\nは参照記号として認識しない)。
  • replaceだと単独でしか使えないので、複数回繰り返す場合はreモジュールのsubを用いる。
  • その場合、参照記号を用いる際の置換先にはr修飾子を付け、reモジュールの場合は参照記号に\nを用いる。
Python
import re
str = "11の3の5"
str1 = str.replace("(\d+)(の|番地?)","$1-") #単独
str2 = re.sub("(\d+)(|番地?)",r"\1-",str) #複数回
print(str1) #11-3の5
print(str2) #11-3-5
BRSF
職業、PG・SE・DBエンジニア。オープン環境のwebプログラムをメインにシステム構築担当。使用言語はPHP(cakePHP、Laravel含)jQuery、JavaScript、ExcelVBA、Perl、Ruby、Python。現在Vue、React、Angular強化中。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした