11

私は次の文字列を持っています:

$string = "The man has {NUM_DOGS} dogs."

次の関数を実行してこれを解析しています。

function parse_text($string)
{
    global $num_dogs;

    $string = str_replace('{NUM_DOGS}', $num_dogs, $string);

    return $string;
}

parse_text($string);

$num_dogsプリセット変数はどこにありますか。に応じて$num_dogs、これは次の文字列のいずれかを返す可能性があります。

  • 男は犬を一匹飼っている。
  • 男は2匹の犬を飼っている。
  • 男は500匹の犬を飼っている。

問題は、「男が犬を1匹飼っている」場合、が複数形になり、望ましくないことです。parse_textこれは、関数を使用せず、代わりに次のようなことを行うだけで解決できることを私は知っています。

if($num_dogs = 1){
    $string = "The man has 1 dog.";
}else{
    $string = "The man has $num_dogs dogs.";
}

しかし、私のアプリケーションでは、構文解析だけでなく{NUM_DOGS}、すべての条件を書き込むのに多くの行が必要になります。

パーサーを実行できるイニシャルに書き込むことができる簡単な方法が必要です。これは$string、理想的には2つの真/偽の可能性に制限されません。

たとえば、

$string = 'The man has {NUM_DOGS} [{NUM_DOGS}|0=>"dogs",1=>"dog called fred",2=>"dogs called fred and harry",3=>"dogs called fred, harry and buster"].';

最後に何が起こったのかは明らかですか?縦棒の後の角かっこ内の部分を使用して配列の作成を開始し、新しい配列のキーを解析された{NUM_DOGS}の値(これで$ num_dogs変数になります)と比較しようとしました。垂直バーの左側にあります)、そのキーを使用して配列エントリの値を返します。

それが完全に混乱しない場合、preg_ *関数を使用することは可能ですか?

4

4 に答える 4

12

あなたの質問の前提は、特定のパターンを一致させ、一致したテキストに対して追加の処理を実行した後にそれを置き換えることです。

の理想的な候補のようですpreg_replace_callback

一致した括弧、引用符、中括弧などをキャプチャするための正規表現は非常に複雑になる可能性があり、正規表現ですべてを行うことは実際には非常に非効率的です。実際、それが必要な場合は、適切なパーサーを作成する必要があります。

この質問では、限られたレベルの複雑さを想定し、正規表現を使用した2段階の解析でそれに取り組みます。


まず第一に、中括弧の間のトークンをキャプチャするために私が考えることができる最も単純な正規表現。

/{([^}]+)}/

それを分解しましょう。

{        # A literal opening brace
(        # Begin capture
  [^}]+  # Everything that's not a closing brace (one or more times)
)        # End capture
}        # Literal closing brace

結果のある文字列に適用すると、preg_match_all次のようになります。

array (
  0 => array (
    0 => 'A string {TOK_ONE}',
    1 => ' with {TOK_TWO|0=>"no", 1=>"one", 2=>"two"}',
  ),
  1 => array (
    0 => 'TOK_ONE',
    1 => 'TOK_TWO|0=>"no", 1=>"one", 2=>"two"',
  ),
)

これまでのところよさそうだ。

文字列に中括弧がネストされている場合、つまり{TOK_TWO|0=>"hi {x} y"}、この正規表現は機能しないことに注意してください。これが問題にならない場合は、次のセクションにスキップしてください。

トップレベルのマッチングを行うことは可能ですが、私がこれまでに行うことができた唯一の方法は、再帰を使用することです。ほとんどの正規表現のベテランは、正規表現に再帰を追加するとすぐに、正規表現ではなくなると言うでしょう。

これは、追加の処理の複雑さが始まる場所であり、長く複雑な文字列を使用すると、スタックスペースを使い果たしてプログラムをクラッシュさせるのは非常に簡単です。使用する必要がある場合は、慎重に使用してください。

私の他の回答の1つから取得し、少し変更した再帰正規表現。

`/{((?:[^{}]*|(?R))*)}/`

故障した。

{                   # literal brace
(                   # begin capture
    (?:             # don't create another capture set
        [^{}]*      # everything not a brace
        |(?R)       # OR recurse
    )*              # none or more times
)                   # end capture
}                   # literal brace

そして今回の出力はトップレベルの中括弧にのみ一致します

array (
  0 => array (
    0 => '{TOK_ONE|0=>"a {nested} brace"}',
  ),
  1 => array (
    0 => 'TOK_ONE|0=>"a {nested} brace"',
  ),
)

繰り返しますが、必要がない限り、再帰的な正規表現を使用しないでください。(古いPCREライブラリがある場合、システムはそれらをサポートしない可能性があります)


それが邪魔にならないように、トークンにオプションが関連付けられているかどうかを確認する必要があります。質問のように2つのフラグメントを照合する代わりに、私の例のようにトークンを使用してオプションを保持することをお勧めします。{TOKEN|0=>"option"}

一致したトークンが含まれていると仮定$matchし、パイプをチェックし、|その後にすべてのサブストリングを取得すると、オプションのリストが残ります。ここでも、正規表現を使用してそれらを解析できます。(最後にすべてをまとめますのでご安心ください)

/(\d)+\s*=>\s*"([^"]*)",?/

故障した。

(\d)+    # Capture one or more decimal digits
\s*      # Any amount of whitespace (allows you to do 0    =>    "")
=>       # Literal pointy arrow
\s*      # Any amount of whitespace
"        # Literal quote
([^"]*)  # Capture anything that isn't a quote
"        # Literal quote
,?       # Maybe followed by a comma

そして、例の一致

array (
  0 => array (
    0 => '0=>"no",',
    1 => '1 => "one",',
    2 => '2=>"two"',
  ),
  1 => array (
    0 => '0',
    1 => '1',
    2 => '2',
  ),
  2 => array (
    0 => 'no',
    1 => 'one',
    2 => 'two',
  ),
)

引用符の中で引用符を使用する場合は、独自の再帰正規表現を作成する必要があります。


まとめ、これが実際の例です。

いくつかの初期化コード。

$options = array(
    'WERE' => 1,
    'TYPE' => 'cat',
    'PLURAL' => 1,
    'NAME' => 2
);

$string = 'There {WERE|0=>"was a",1=>"were"} ' .
    '{TYPE}{PLURAL|1=>"s"} named bob' . 
    '{NAME|1=>" and bib",2=>" and alice"}';

そしてすべて一緒に。

$string = preg_replace_callback('/{([^}]+)}/', function($match) use ($options) {
    $match = $match[1];

    if (false !== $pipe = strpos($match, '|')) {
        $tokens = substr($match, $pipe + 1);
        $match = substr($match, 0, $pipe);
    } else {
        $tokens = array();
    }

    if (isset($options[$match])) {
        if ($tokens) {
            preg_match_all('/(\d)+\s*=>\s*"([^"]*)",?/', $tokens, $tokens);

            $tokens = array_combine($tokens[1], $tokens[2]);

            return $tokens[$options[$match]];
        }
        return $options[$match];
    }
    return '';
}, $string);

エラーチェックは最小限であることに注意してください。存在しないオプションを選択すると、予期しない結果が発生します。

これらすべてを行うにはおそらくもっと簡単な方法がありますが、私はそのアイデアを採用して実行しました。

于 2012-08-10T08:27:01.830 に答える
6

まず第一に、それは少し議論の余地がありますが、それを簡単に回避できる場合は、$num_dogsほとんどの人がグローバル変数が悪であると信じているので、関数への引数として渡すだけです!

次に、「s」を取得するために、私は通常、次のようなことを行います。

$dogs_plural = ($num_dogs == 1) ? '' : 's';

次に、次のようにします。

$your_string = "The man has $num_dogs dog$dogs_plural";

これは基本的にif/elseブロックを実行するのと同じことですが、コード行が少なく、テキストを1回だけ書き込む必要があります。

他の部分に関しては、私はあなたが何をしようとしているのかまだ混乱していますが、あなたは変換するための何らかの方法を探していると思います

{NUM_DOGS}|0=>"dogs",1=>"dog called fred",2=>"dogs called fred and harry",3=>"dogs called fred, harry and buster"]

の中へ:

switch $num_dogs {
    case 0:
        return 'dogs';
        break;
    case 1:
        return 'dog called fred';
        break;
    case 2:
        return 'dogs called fred and harry';
        break;
    case 3:
        return 'dogs called fred, harry and buster';
        break;
}

最も簡単な方法は、と正規表現の組み合わせを使用して、explode()上記のようなことを実行することです。

于 2012-08-08T02:11:02.410 に答える
6

ピンチでは、以下のコードのような漠然とした実装で、あなたが求めているものと同様のことをしました。

これは、@Mikeの回答ほど機能が豊富ではありませんが、過去にそのトリックを実行しました.

/**
 * This function pluralizes words, as appropriate.
 *
 * It is a completely naive, example-only implementation.
 * There are existing "inflector" implementations that do this
 * quite well for many/most *English* words.
 */
function pluralize($count, $word)
{
    if ($count === 1)
    {
        return $word;
    }
    return $word . 's';
}

/**
 * Matches template patterns in the following forms:
 *   {NAME}       - Replaces {NAME} with value from $values['NAME']
 *   {NAME:word}  - Replaces {NAME:word} with 'word', pluralized using the pluralize() function above.
 */
function parse($template, array $values)
{
    $callback = function ($matches) use ($values) {
        $number = $values[$matches['name']];
        if (array_key_exists('word', $matches)) {
            return pluralize($number, $matches['word']);
        }
        return $number;
    };

    $pattern = '/\{(?<name>.+?)(:(?<word>.+?))?\}/i';
    return preg_replace_callback($pattern, $callback, $template);
}

元の質問に似た例をいくつか示します...

echo parse(
    'The man has {NUM_DOGS} {NUM_DOGS:dog}.' . PHP_EOL,
    array('NUM_DOGS' => 2)
);

echo parse(
    'The man has {NUM_DOGS} {NUM_DOGS:dog}.' . PHP_EOL,
    array('NUM_DOGS' => 1)
);

出力は次のとおりです。

男は犬を2匹飼っている。

男性は犬を1匹飼っています。

大規模なプロジェクトでは、多言語が必要になった場合、GNU gettextを優先して、カスタムの丸められた語形変化を常に捨ててしまったことに言及する価値があるかもしれません。

于 2012-08-15T14:11:07.993 に答える
0

これは、この質問に応えて 2009 年にflussence によって投稿された回答からコピーされました。

gettext 拡張機能を見たいと思うかもしれません。より具体的には、それngettext()はあなたが望むことをするように聞こえます: カウントする数がある限り、単語を正しく複数形化します.

print ngettext('odor', 'odors', 1); // prints "odor"
print ngettext('odor', 'odors', 4); // prints "odors"
print ngettext('%d cat', '%d cats', 4); // prints "4 cats"

また、翻訳された複数形を正しく処理するようにすることもできます。これが主な目的ですが、これにはかなり多くの余分な作業が必要です。

于 2012-08-16T15:08:57.933 に答える