質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.47%
PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

正規表現

正規表現とは特定の文字列によるパターンマッチングを行う際に用いられる宣言型プログラミングです。

Q&A

解決済

2回答

1484閲覧

PHP正規表現で 太郎「こんにちは」 等を分別したい

nikuatsu

総合スコア177

PHP

PHPは、Webサイト構築に特化して開発されたプログラミング言語です。大きな特徴のひとつは、HTMLに直接プログラムを埋め込むことができるという点です。PHPを用いることで、HTMLを動的コンテンツとして出力できます。HTMLがそのままブラウザに表示されるのに対し、PHPプログラムはサーバ側で実行された結果がブラウザに表示されるため、PHPスクリプトは「サーバサイドスクリプト」と呼ばれています。

正規表現

正規表現とは特定の文字列によるパターンマッチングを行う際に用いられる宣言型プログラミングです。

0グッド

2クリップ

投稿2021/12/16 19:09

前提・実現したいこと

PHP正規表現で「」の内外で文字列を分別したいです。

例えば$str = "太郎「よろしくね」"が与えられれば、["name"=>"太郎","comment"=>"よろしくね"]に分別するというものです。

与えられる値と、目的の値

次の$aryが与えられます。
右のコメントアウトの["name"=>"●●","comment"=>●●]が分別された目的の値です

php

1$ary = [ 2 3 /* 4 * 正しい値が与えられた場合、一方か両方に値が入る 5 *----------------------------------------------*/ 6 7 // 以下のように「」の内外で name と comment に分別する( 尚、「」はなくても「」だけでもOK ) 8 '一郎「こんにちは」', // ["name"=>"一郎","comment"=>"こんにちは"] 9 '二郎「男の中の「男」です」', // ["name"=>"二郎","comment"=>"男の中の「男」です"] 10 '三郎', // ["name"=>"三郎","comment"=>"null"] 11 '「こんばんは」', // ["name"=>"null","comment"=>"こんばんは"] 12 13 /* 14 * 不正な値が与えられた場合、いずれも null を返す 15 *----------------------------------------------*/ 16 17 //(1)以下のように「」の後に文字がある場合 null とする 18 '「こんばんは」です', // ["name"=>"null","comment"=>"null"] 19 20 //(2)以下のように「」がセットじゃない場合 null とする 21 '五郎「男の中の「男」かも', // ["name"=>"null","comment"=>"null"] 22 '「男の中の「男」かも', // 同上 23 '「やばい', // 同上 24 'ねむい」', // 同上 25 '「', // 同上 26 '」', // 同上 27 '「「」', // 同上 28 29 //(3)以下のように記号だけの場合 null とする 30 '「」', // ["name"=>"null","comment"=>"null"] 31 '「「」」', // 同上 32 '「★」', // 同上 33 '「!!」', // 同上 34]; 35 36// 分別を実行 37foreach( $ary as $str ){ 38 sort_kakko( hoge($str) ); 39}

発生している問題

正規表現で躓いています。

該当のソースコード

作ったのはsort_kakko()という分別の関数です。
まず正しい値を判定し、続いて不正な3つのパターンである上記(1)(2)(3)を判定する流れで書きました。
ですが早速、正しい値の判定の正規表現で躓いています。

php

1// 分別を実行 2function sort_kakko($str){ 3 4 // 正しい値の判定 5 preg_match('/^(.+)?「?(.+)?」?$/', $str, $matches); 6 $name = $matches[1] ?? null; 7 $comment = $matches[2] ?? null; 8 9 // 不正な値(1)の判定 10 $ok_1 = true; 11 if ( mb_substr_count($str,'」') === 1 && substr($str, -1) !== '」' ) { // 」があるのに最後が」じゃない場合 12 $ok_1 = false; 13 } 14 15 // 不正な値(2)の判定 16 $ok_2 = true; 17 if ( mb_substr_count($str,'「') === 1 || mb_substr_count($str,'」') === 1 ) { //「」のようにセットじゃない場合 18 $ok_2 = false; 19 } 20 21 // 不正な値(3)の判定 22 $ok_3 = true; 23 if ( kigo_remove($str) === '' ) { // すべての記号を除去した結果、空文字だった場合 24 $ok_3 = false; 25 } 26 27 // (1)(2)(3)どれかが不正なら null とする 28 if ( !$ok_1 || !$ok_2 || !$ok_3 ) { 29 $name = null; 30 $comment = null; 31 } 32 33 return ["name"=>$name,"comment"=>$comment]; 34} 35 36// すべての記号を除去する 37function kigo_remove($str){ 38 return preg_replace('/[^ぁ-んァ-ンーa-zA-Z0-9一-0-9\-\r]+/u',"" ,$str); 39}

試したこと

最初の正規表現のみで試してみました。

php

1// 分別 2function sort_kakko($str){ 3 4 // 正しい値の判定 5 preg_match('/^(.+)?「(.+)?」$/', $str, $matches); 6 $name = $matches[1] ?? null; 7 $comment = $matches[2] ?? null; 8 9 return ["name"=>$name,"comment"=>$comment]; 10}

まず「「」」「★」「!!」ができていませんが、これらは上記$ok_3で除外できるから問題ありません。

しかし三郎ができない理由がわかりません。

自分の理解では上記/^(.+)?「(.+)?」$/という正規表現は

^(.+)?
→ 先頭の(.+)はなくてもよくて、あれば$matchesに取得する

「?(.+)?」?
「」はなくてもよくて、(.+)もなくてもよくて、あれば$matchesに取得する
というもので、なぜ三郎"name"nullになってしまうのか不明です。

これについては/^(.+)?「?(.+)?」?$/のように、かっこの後にも?をつけるなど試してみたものの分別は実現できませんでした。

そして二郎「男の中の「男」です」も分別できず、きっと「一番外側のかっこを基準とする」という正規表現が必要そうに思えます。
これについてはteratailの質問で類似のものを見かけたので応用して/[[^「\」]*(?:[「^\」]*][^[]]*)*]/のように試したものの、やはり分別は実現できませんでした。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

(既存のコードに関する添削は割愛します)

参考にされた回答には「正規表現では入れ子構造のパースは無理」という回答しかないですが, PHP の正規表現エンジンである PCRE は,正規表現以上の表現能力を持っており,カッコのネストに対応することができます。すなわち,1個の正規表現を書くだけで対処が可能です。また同様に,**「この文字種を最低限1文字含む」**も表現することができます。

「記号だけ」という定義が非常に曖昧なので, Unicode 文字プロパティの定義を見て,どれを含めるか自分で決めた上で調整してください。ここでは \p{L}\p{N} が最低限含まれている必要がある,と規定します。また,全角の 「」のみを考慮し,半角の 「」は考慮しないことにします。

php

1<?php 2 3function parse(string $input): array 4{ 5 static $pattern = <<<'EOD' 6/ 7 8 # 先頭 9 ^ 10 11 # name 部分 12 (?<name> 13 # \p{L}\p{N} を最低1個は含む 14 (?=[^「」\p{C}\p{L}\p{N}]*+[\p{L}\p{N}]) 15 # カッコと制御文字以外の繰り返し 16 [^「」\p{C}]++ 17 )? 18 19 # カッコ+空白だけのコメントの場合は読み飛ばす条件分岐 20 (?: 21 # empty_comment 部分 22 (?<empty_comment> 2324 # カッコの中身 25 (?: 26 # empty_comment の再帰 27 (?&empty_comment) 28 )*+ 2930 ) 31 # empty_comment がマッチしたらそこまでをすべて読み飛ばす 32 (*SKIP)(*FAIL) 33 34 | 35 36 # comment 部分 37 (?<comment> 3839 40 # カッコの中身 41 (?: 42 # \p{L}\p{N} を最低1個は含む 43 (?=[^「」\p{C}\p{L}\p{N}]*+[\p{L}\p{N}]) 44 # カッコと制御文字以外の繰り返し 45 [^「」\p{C}]*+ 46 47 # または 48 | 49 50 # comment の再帰 51 (?&comment) 52 )*+ 53 5455 56 )? 57 ) 58 59 # 末尾 60 $ 61 62 /ux 63EOD; 64 65 preg_match($pattern, $input, $match); 66 67 $name = $match['name'] ?? ''; 68 $comment = $match['comment'] ?? ''; 69 70 // 外側のカッコを切り取り 71 $comment = mb_substr($comment, 1, -1, 'UTF-8'); 72 73 $name = $name === '' ? null : $name; 74 $comment = $comment === '' ? null : $comment; 75 76 return compact('name', 'comment'); 77}

テスト実行: https://3v4l.org/6REYe#v8.1.0

投稿2021/12/16 21:27

編集2021/12/16 21:50
mpyw

総合スコア5223

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

mpyw

2021/12/16 22:00

すいません,若干作りが粗く,空白判定にミスがある可能性がありますがそろそろ寝ます…
mpyw

2021/12/16 22:05

この手のやつは,本気でミスなく解析したかったら正規表現ではなく「字句解析」と「構文解析」を行ったほうがいいかもしれないですね。もしご興味がありましたら,このへんのキーワードで調べてみてください。
nikuatsu

2021/12/17 06:49 編集

どうもありがとうございます。<<<を使ってコメントを書く方法など大変参考になりました。 いろいろ試しましたが空白についてのミスというのは見つからなかったので完璧なご回答に思えます。 「字句解析」と「構文解析」についても情報ありがとうございます。
guest

0

とりあえずこうしておいて

PHP

1$a=[ 2 '一郎「こんにちは」', 3 '二郎「男の中の「男」です」', 4 '三郎', 5 '「こんばんは」', 6'「こんばんは」です', 7 '五郎「男の中の「男」かも', 8 '「男の中の「男」かも', 9 '「やばい', 10 'ねむい」', 11 '「', 12 '」', 13 '「「」', 14 '「」', 15 '「「」」', 16 '「★」', 17 '「!!」', 18 '「男の中の「男」かも' , 19 '「男の中の「男」かも」' , 20 ]; 21 22foreach($a as $str){ 23 if(preg_match("/\A(?<name>[^「」]+?)(「(?<comment>.+)」\z)/u",$str,$match) or 24 preg_match("/\A「(?<comment>.+?|$)」\z/u",$str,$match) or 25 preg_match("/\A(?<name>[^「」]+)\z/u",$str,$match)){ 26 $match=array_filter($match,function($x,$y){ 27 return in_array($y,["name","comment"],true); 28 }, ARRAY_FILTER_USE_BOTH); 29 } 30 $result=array_merge(["org"=>$str,"name"=>null,"comment"=>null],$match); 31 print_r($result); 32}

あとはcommentを再検査すればよいでしょう
※調整版

投稿2021/12/17 00:35

編集2021/12/17 06:46
yambejp

総合スコア114968

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

nikuatsu

2021/12/17 06:37

残念ながら '「男の中の「男」かも' から ["name"=>"男の中の", "comment"=>"男"] が取得されてしまうため、commentを再検査するだけではクリアできなそうです。 しかし正規表現における<>という書き方があることを知れて勉強になりました。ありがとうございます。
yambejp

2021/12/17 06:47

想定するデータがたりなかったですね 調整版アップしてあります
nikuatsu

2021/12/17 06:51

調整版ありがとうございます。再検査でなんとななりそうです。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.47%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問