Question

Supposons que les deux chaînes suivantes contiennent des expressions régulières. Comment puis-je les combiner? Plus précisément, je souhaite utiliser les deux expressions comme alternatives.

$a = '# /[a-z] #i';
$b = '/ Moo /x';
$c = preg_magic_coalesce('|', $a, $b);
// Desired result should be equivalent to:
// '/ \/[a-zA-Z] |Moo/'

Bien sûr, cela n’est pas pratique, car cela impliquerait d’analyser les expressions, de construire des arbres de syntaxe, de les fusionner, puis de produire une autre expression régulière équivalente à l’arbre. Je suis complètement heureux sans cette dernière étape. Malheureusement, PHP n'a pas de classe RegExp (ou le fait-il?).

Existe-t-il un moyen d'y parvenir? Incidemment, une autre langue offre-t-elle un moyen? N'est-ce pas un scénario plutôt normal? Je ne pense pas. : - (

Sinon , existe-t-il un moyen de vérifier efficacement si l'une des deux expressions correspond, et laquelle correspond le plus tôt (et si elles correspondent au même match est plus long)? C'est ce que je fais en ce moment. Malheureusement, je le fais sur de longues chaînes, très souvent, pour plus de deux modèles. Le résultat est lent (et oui, c’est définitivement le goulot d’étranglement).

EDIT:

J'aurais dû être plus précis & # 8211; Pardon. $ a et $ b sont des variables , leur contenu est hors de mon contrôle! Sinon, je les fusionnerais manuellement. Par conséquent, je ne peux faire aucune hypothèse sur les délimiteurs ou les modificateurs de regex utilisés. Notez, par exemple, que ma première expression utilise le modificateur i (ignorer la casse) alors que la seconde utilise x (syntaxe étendue). Par conséquent, je ne peux pas simplement concaténer les deux, car la deuxième expression n'ignore pas les majuscules / minuscules et la première n'utilise pas la syntaxe étendue (et tout espace blanc est significatif!

Était-ce utile?

La solution

Je vois que porneL en a décrit un tas, mais gère la majeure partie du problème. Il annule les modificateurs définis dans les sous-expressions précédentes (que l'autre réponse manquait) et les modifie comme spécifié dans chaque sous-expression. Il gère également les délimiteurs non-slash (je ne pouvais pas spécifier ici quels caractères sont autorisés , aussi j’ai utilisé . , vous souhaiterez peut-être préciser davantage).

Une des faiblesses est qu'il ne gère pas les références arrière dans les expressions. Ce qui me préoccupe le plus, ce sont les limites des références arrières elles-mêmes. Je laisserai cela comme un exercice au lecteur / questionneur.

// Pass as many expressions as you'd like
function preg_magic_coalesce() {
    $active_modifiers = array();

    $expression = '/(?:';
    $sub_expressions = array();
    foreach(func_get_args() as $arg) {
        // Determine modifiers from sub-expression
        if(preg_match('/^(.)(.*)\1([eimsuxADJSUX]+)$/', $arg, $matches)) {
            $modifiers = preg_split('//', $matches[3]);
            if($modifiers[0] == '') {
                array_shift($modifiers);
            }
            if($modifiers[(count($modifiers) - 1)] == '') {
                array_pop($modifiers);
            }

            $cancel_modifiers = $active_modifiers;
            foreach($cancel_modifiers as $key => $modifier) {
                if(in_array($modifier, $modifiers)) {
                    unset($cancel_modifiers[$key]);
                }
            }
            $active_modifiers = $modifiers;
        } elseif(preg_match('/(.)(.*)\1$/', $arg)) {
            $cancel_modifiers = $active_modifiers;
            $active_modifiers = array();
        }

        // If expression has modifiers, include them in sub-expression
        $sub_modifier = '(?';
        $sub_modifier .= implode('', $active_modifiers);

        // Cancel modifiers from preceding sub-expression
        if(count($cancel_modifiers) > 0) {
            $sub_modifier .= '-' . implode('-', $cancel_modifiers);
        }

        $sub_modifier .= ')';

        $sub_expression = preg_replace('/^(.)(.*)\1[eimsuxADJSUX]*$/', $sub_modifier . '$2', $arg);

        // Properly escape slashes
        $sub_expression = preg_replace('/(?<!\\\)\//', '\\\/', $sub_expression);

        $sub_expressions[] = $sub_expression;
    }

    // Join expressions
    $expression .= implode('|', $sub_expressions);

    $expression .= ')/';
    return $expression;
}

Edit: j'ai réécrit ceci (parce que je suis TOC) et j'ai fini avec:

function preg_magic_coalesce($expressions = array(), $global_modifier = '') {
    if(!preg_match('/^((?:-?[eimsuxADJSUX])+)$/', $global_modifier)) {
        $global_modifier = '';
    }

    $expression = '/(?:';
    $sub_expressions = array();
    foreach($expressions as $sub_expression) {
        $active_modifiers = array();
        // Determine modifiers from sub-expression
        if(preg_match('/^(.)(.*)\1((?:-?[eimsuxADJSUX])+)$/', $sub_expression, $matches)) {
            $active_modifiers = preg_split('/(-?[eimsuxADJSUX])/',
                $matches[3], -1, PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
        }

        // If expression has modifiers, include them in sub-expression
        if(count($active_modifiers) > 0) {
            $replacement = '(?';
            $replacement .= implode('', $active_modifiers);
            $replacement .= ':$2)';
        } else {
            $replacement = '$2';
        }

        $sub_expression = preg_replace('/^(.)(.*)\1(?:(?:-?[eimsuxADJSUX])*)$/',
            $replacement, $sub_expression);

        // Properly escape slashes if another delimiter was used
        $sub_expression = preg_replace('/(?<!\\\)\//', '\\\/', $sub_expression);

        $sub_expressions[] = $sub_expression;
    }

    // Join expressions
    $expression .= implode('|', $sub_expressions);

    $expression .= ')/' . $global_modifier;
    return $expression;
}

Il utilise maintenant (? modificateurs: sous-expression) plutôt que (? modificateurs) sous-expression | (?? annuleurs-modificateurs) sous-expression mais I ' Nous avons remarqué que les deux ont des effets secondaires étranges. Par exemple, dans les deux cas, si une sous-expression a un modificateur / u , elle ne pourra pas correspondre (mais si vous transmettez 'u' comme second argument du paramètre nouvelle fonction, qui correspondra très bien).

Autres conseils

  1. Supprimez les délimiteurs et les drapeaux de chacun. Cette regex devrait le faire:

    /^(.)(.*)\1([imsxeADSUXJu]*)$/
    
  2. Réunissez les expressions. Vous aurez besoin de parenthèses non capturées pour injecter des drapeaux:

    "(?$flags1:$regexp1)|(?$flags2:$regexp2)"
    
  3. S'il existe des références arrière, comptez la capture de parenthèses et mettez à jour les références arrière en conséquence (par exemple, / (.) correctement jointes x \ 1 / et / (.) y \ 1 / est / (.) X \ 1 | (.) Y \ 2 / ).

EDIT

J'ai réécrit le code! . Il contient désormais les modifications répertoriées ci-dessous. De plus, j’ai fait des tests approfondis (que je n’afficherai pas ici car ils sont trop nombreux) pour rechercher des erreurs. Jusqu’à présent, je n’en ai trouvé aucun.

  • La fonction est maintenant divisée en deux parties: Il existe une fonction séparée preg_split qui prend une expression régulière et retourne un tableau contenant l'expression nue (sans délimiteurs) et un tableau de modificateurs. Cela pourrait être utile (il l’a déjà fait; c’est pourquoi j’ai fait ce changement).

  • Le code gère désormais correctement les références arrière. Cela était nécessaire pour mon objectif. Il n’était pas difficile d’ajouter, l’expression régulière utilisée pour capturer les références arrière a l’air bizarre (et peut même être extrêmement inefficace, elle m’a l'air très difficile, mais ce n’est qu’une intuition et ne s’applique que dans des cas étranges) . À propos, est-ce que quelqu'un connaît un meilleur moyen de vérifier un nombre impair de matches que moi? Les arrières négatives ne fonctionneront pas ici car elles n'acceptent que des chaînes de longueur fixe au lieu d'expressions régulières. Cependant, j’ai besoin de la regex ici pour vérifier si la barre oblique inverse précédente s’est réellement échappée.

    De plus, je ne sais pas comment PHP réussit à mettre en cache l'utilisation create_function anonyme. En termes de performances, il ne s’agit peut-être pas de la meilleure solution, mais cela semble suffisant.

  • J'ai corrigé un bogue dans le contrôle d'intégrité.

  • J'ai supprimé l'annulation des modificateurs obsolètes car mes tests montrent que ce n'est pas nécessaire.

Au fait, ce code est l’un des composants essentiels d’un surligneur de syntaxe pour différentes langues sur lesquelles je travaille en PHP, car je ne suis pas satisfait des solutions de remplacement ailleurs .

Merci!

porneL , l'absence de paupière , un travail incroyable! Merci beaucoup. J'avais effectivement abandonné.

J'ai développé votre solution et j'aimerais la partager ici. Je n'ai pas mis en œuvre de références de renumérotation car ce n'est pas pertinent dans mon cas (je pense…). Peut-être que cela deviendra nécessaire plus tard, cependant.

Quelques questions…

Une chose, @eyelidlessness : Pourquoi ressentez-vous la nécessité d’annuler les anciens modificateurs? Autant que je sache, cela n'est pas nécessaire car les modificateurs ne sont appliqués que localement. Ah oui, une autre chose. Votre fuite du délimiteur semble trop compliquée. Voulez-vous expliquer pourquoi vous pensez que cela est nécessaire? Je pense que ma version devrait fonctionner aussi bien, mais je pourrais très bien me tromper.

De plus, j'ai modifié la signature de votre fonction pour répondre à mes besoins. Je pense aussi que ma version est plus généralement utile. Encore une fois, je peux me tromper.

BTW, vous devriez maintenant réaliser l’importance des vrais noms sur SO. ;-) Je ne peux pas vous donner de crédit réel dans le code. : - /

Le code

Quoi qu'il en soit, j'aimerais partager mon résultat jusqu'à présent car je ne peux pas croire que personne d'autre n'a besoin de quelque chose comme ça. Le code semble fonctionner très bien. De nombreux tests restent cependant à faire. Faites un commentaire!

Et sans plus tarder…

/**
 * Merges several regular expressions into one, using the indicated 'glue'.
 *
 * This function takes care of individual modifiers so it's safe to use
 * <em>different</em> modifiers on the individual expressions. The order of
 * sub-matches is preserved as well. Numbered back-references are adapted to
 * the new overall sub-match count. This means that it's safe to use numbered
 * back-refences in the individual expressions!
 * If {@link $names} is given, the individual expressions are captured in
 * named sub-matches using the contents of that array as names.
 * Matching pair-delimiters (e.g. <code>"{…}"</code>) are currently
 * <strong>not</strong> supported.
 *
 * The function assumes that all regular expressions are well-formed.
 * Behaviour is undefined if they aren't.
 *
 * This function was created after a {@link https://stackoverflow.com/questions/244959/
 * StackOverflow discussion}. Much of it was written or thought of by
 * “porneL” and “eyelidlessness”. Many thanks to both of them.
 *
 * @param string $glue  A string to insert between the individual expressions.
 *      This should usually be either the empty string, indicating
 *      concatenation, or the pipe (<code>|</code>), indicating alternation.
 *      Notice that this string might have to be escaped since it is treated
 *      like a normal character in a regular expression (i.e. <code>/</code>)
 *      will end the expression and result in an invalid output.
 * @param array $expressions    The expressions to merge. The expressions may
 *      have arbitrary different delimiters and modifiers.
 * @param array $names  Optional. This is either an empty array or an array of
 *      strings of the same length as {@link $expressions}. In that case,
 *      the strings of this array are used to create named sub-matches for the
 *      expressions.
 * @return string An string representing a regular expression equivalent to the
 *      merged expressions. Returns <code>FALSE</code> if an error occurred.
 */
function preg_merge($glue, array $expressions, array $names = array()) {
    // … then, a miracle occurs.

    // Sanity check …

    $use_names = ($names !== null and count($names) !== 0);

    if (
        $use_names and count($names) !== count($expressions) or
        !is_string($glue)
    )
        return false;

    $result = array();
    // For keeping track of the names for sub-matches.
    $names_count = 0;
    // For keeping track of *all* captures to re-adjust backreferences.
    $capture_count = 0;

    foreach ($expressions as $expression) {
        if ($use_names)
            $name = str_replace(' ', '_', $names[$names_count++]);

        // Get delimiters and modifiers:

        $stripped = preg_strip($expression);

        if ($stripped === false)
            return false;

        list($sub_expr, $modifiers) = $stripped;

        // Re-adjust backreferences:

        // We assume that the expression is correct and therefore don't check
        // for matching parentheses.

        $number_of_captures = preg_match_all('/\([^?]|\(\?[^:]/', $sub_expr, 

EDIT

J'ai réécrit le code! . Il contient désormais les modifications répertoriées ci-dessous. De plus, j’ai fait des tests approfondis (que je n’afficherai pas ici car ils sont trop nombreux) pour rechercher des erreurs. Jusqu’à présent, je n’en ai trouvé aucun.

  • La fonction est maintenant divisée en deux parties: Il existe une fonction séparée preg_split qui prend une expression régulière et retourne un tableau contenant l'expression nue (sans délimiteurs) et un tableau de modificateurs. Cela pourrait être utile (il l’a déjà fait; c’est pourquoi j’ai fait ce changement).

  • Le code gère désormais correctement les références arrière. Cela était nécessaire pour mon objectif. Il n’était pas difficile d’ajouter, l’expression régulière utilisée pour capturer les références arrière a l’air bizarre (et peut même être extrêmement inefficace, elle m’a l'air très difficile, mais ce n’est qu’une intuition et ne s’applique que dans des cas étranges) . À propos, est-ce que quelqu'un connaît un meilleur moyen de vérifier un nombre impair de matches que moi? Les arrières négatives ne fonctionneront pas ici car elles n'acceptent que des chaînes de longueur fixe au lieu d'expressions régulières. Cependant, j’ai besoin de la regex ici pour vérifier si la barre oblique inverse précédente s’est réellement échappée.

    De plus, je ne sais pas comment PHP réussit à mettre en cache l'utilisation create_function anonyme. En termes de performances, il ne s’agit peut-être pas de la meilleure solution, mais cela semble suffisant.

  • J'ai corrigé un bogue dans le contrôle d'intégrité.

  • J'ai supprimé l'annulation des modificateurs obsolètes car mes tests montrent que ce n'est pas nécessaire.

Au fait, ce code est l’un des composants essentiels d’un surligneur de syntaxe pour différentes langues sur lesquelles je travaille en PHP, car je ne suis pas satisfait des solutions de remplacement ailleurs .

Merci!

porneL , l'absence de paupière , un travail incroyable! Merci beaucoup. J'avais effectivement abandonné.

J'ai développé votre solution et j'aimerais la partager ici. Je n'ai pas mis en œuvre de références de renumérotation car ce n'est pas pertinent dans mon cas (je pense…). Peut-être que cela deviendra nécessaire plus tard, cependant.

Quelques questions…

Une chose, @eyelidlessness : Pourquoi ressentez-vous la nécessité d’annuler les anciens modificateurs? Autant que je sache, cela n'est pas nécessaire car les modificateurs ne sont appliqués que localement. Ah oui, une autre chose. Votre fuite du délimiteur semble trop compliquée. Voulez-vous expliquer pourquoi vous pensez que cela est nécessaire? Je pense que ma version devrait fonctionner aussi bien, mais je pourrais très bien me tromper.

De plus, j'ai modifié la signature de votre fonction pour répondre à mes besoins. Je pense aussi que ma version est plus généralement utile. Encore une fois, je peux me tromper.

BTW, vous devriez maintenant réaliser l’importance des vrais noms sur SO. ;-) Je ne peux pas vous donner de crédit réel dans le code. : - /

Le code

Quoi qu'il en soit, j'aimerais partager mon résultat jusqu'à présent car je ne peux pas croire que personne d'autre n'a besoin de quelque chose comme ça. Le code semble fonctionner très bien. De nombreux tests restent cependant à faire. Faites un commentaire!

Et sans plus tarder…

<*>

PS: J'ai rendu cette page wiki modifiable. Vous savez ce que cela signifie…!

); if ($number_of_captures === false) return false; if ($number_of_captures > 0) { // NB: This looks NP-hard. Consider replacing. $backref_expr = '/ ( # Only match when not escaped: [^\\\\] # guarantee an even number of backslashes (\\\\*?)\\2 # (twice n, preceded by something else). ) \\\\ (\d) # Backslash followed by a digit. /x'; $sub_expr = preg_replace_callback( $backref_expr, create_function( '$m', 'return $m[1] . "\\\\" . ((int)$m[3] + ' . $capture_count . ');' ), $sub_expr ); $capture_count += $number_of_captures; } // Last, construct the new sub-match: $modifiers = implode('', $modifiers); $sub_modifiers = "(?$modifiers)"; if ($sub_modifiers === '(?)') $sub_modifiers = ''; $sub_name = $use_names ? "?<$name>" : '?:'; $new_expr = "($sub_name$sub_modifiers$sub_expr)"; $result[] = $new_expr; } return '/' . implode($glue, $result) . '/'; } /** * Strips a regular expression string off its delimiters and modifiers. * Additionally, normalize the delimiters (i.e. reformat the pattern so that * it could have used '/' as delimiter). * * @param string $expression The regular expression string to strip. * @return array An array whose first entry is the expression itself, the * second an array of delimiters. If the argument is not a valid regular * expression, returns <code>FALSE</code>. * */ function preg_strip($expression) { if (preg_match('/^(.)(.*)\\1([imsxeADSUXJu]*)$/s', $expression, $matches) !== 1) return false; $delim = $matches[1]; $sub_expr = $matches[2]; if ($delim !== '/') { // Replace occurrences by the escaped delimiter by its unescaped // version and escape new delimiter. $sub_expr = str_replace("\\$delim", $delim, $sub_expr); $sub_expr = str_replace('/', '\\/', $sub_expr); } $modifiers = $matches[3] === '' ? array() : str_split(trim($matches[3])); return array($sub_expr, $modifiers); }

PS: J'ai rendu cette page wiki modifiable. Vous savez ce que cela signifie…!

Je suis presque sûr qu'il n'est pas possible de mettre des expressions rationnelles ensemble dans n'importe quelle langue, elles pourraient avoir des modificateurs incompatibles.

Je les aurais probablement simplement rangés dans un tableau et parcourus en boucle, ou combinés à la main.

Éditer: si vous les exécutez un par un, comme décrit dans votre édition, vous pourrez peut-être exécuter le second sur une sous-chaîne (du début à la première correspondance). Cela pourrait aider les choses.

function preg_magic_coalasce($split, $re1, $re2) {
  $re1 = rtrim($re1, "\/#is");
  $re2 = ltrim($re2, "\/#");
  return $re1.$split.$re2;
}

Vous pouvez le faire de la manière alternative suivante:

$a = '# /[a-z] #i';
$b = '/ Moo /x';

$a_matched = preg_match($a, $text, $a_matches);
$b_matched = preg_match($b, $text, $b_matches);

if ($a_matched && $b_matched) {
    $a_pos = strpos($text, $a_matches[1]);
    $b_pos = strpos($text, $b_matches[1]);

    if ($a_pos == $b_pos) {
        if (strlen($a_matches[1]) == strlen($b_matches[1])) {
            // $a and $b matched the exact same string
        } else if (strlen($a_matches[1]) > strlen($b_matches[1])) {
            // $a and $b started matching at the same spot but $a is longer
        } else {
            // $a and $b started matching at the same spot but $b is longer
        }
    } else if ($a_pos < $b_pos) {
        // $a matched first
    } else {
        // $b matched first
    }
} else if ($a_matched) {
    // $a matched, $b didn't
} else if ($b_matched) {
    // $b matched, $a didn't
} else {
    // neither one matched
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top