web-dev-qa-db-fra.com

preg_match et UTF-8 dans PHP

J'essaye de rechercher une chaîne encodée en UTF8 en utilisant preg_match .

preg_match('/H/u', "\xC2\xA1Hola!", $a_matches, PREG_OFFSET_CAPTURE);
echo $a_matches[0][1];

Cela devrait imprimer 1, car "H" est à l'index 1 dans la chaîne "¡Hola!". Mais il en imprime 2. Il semble donc que le sujet ne soit pas traité comme une chaîne encodée en UTF8, même si je passe le "u" modificateur dans l'expression régulière.

J'ai les paramètres suivants dans mon php.ini, et d'autres fonctions UTF8 fonctionnent:

mbstring.func_overload = 7
mbstring.language = Neutral
mbstring.internal_encoding = UTF-8
mbstring.http_input = pass
mbstring.http_output = pass
mbstring.encoding_translation = Off

Des idées?

31
JW.

On dirait que c'est une "fonctionnalité", voir http://bugs.php.net/bug.php?id=37391

Le commutateur 'u' n'a de sens que pour pcre, PHP lui-même n'en est pas conscient.

Du point de vue de PHP, les chaînes sont des séquences d'octets et retourner un offset d'octets semble logique (je ne dis pas "correct"). 

18
user187291

Bien que le modificateur u fasse en sorte que le motif et le sujet soient interprétés comme UTF-8, les décalages capturés sont toujours comptés en octets.

Vous pouvez utiliser mb_strlen pour obtenir la longueur en caractères UTF-8 plutôt qu'en octets:

$str = "\xC2\xA1Hola!";
preg_match('/H/u', $str, $a_matches, PREG_OFFSET_CAPTURE);
echo mb_strlen(substr($str, 0, $a_matches[0][1]));
34
Gumbo

Essayez d’ajouter ceci (* UTF8) avant la regex:

preg_match('(*UTF8)/H/u', "\xC2\xA1Hola!", $a_matches, PREG_OFFSET_CAPTURE);

Magie, grâce à un commentaire in http://www.php.net/manual/es/function.preg-match.php#95828

25
Natxet

Excusez-moi pour le nécropostage, mais peut-être que quelqu'un le trouvera utile: le code ci-dessous peut remplacer les fonctions preg_match et preg_match_all et renvoie des correspondances correctes avec correct offset pour les chaînes codées en UTF8.

     mb_internal_encoding('UTF-8');

     /**
     * Returns array of matches in same format as preg_match or preg_match_all
     * @param bool   $matchAll If true, execute preg_match_all, otherwise preg_match
     * @param string $pattern  The pattern to search for, as a string.
     * @param string $subject  The input string.
     * @param int    $offset   The place from which to start the search (in bytes).
     * @return array
     */
    function pregMatchCapture($matchAll, $pattern, $subject, $offset = 0)
    {
        $matchInfo = array();
        $method    = 'preg_match';
        $flag      = PREG_OFFSET_CAPTURE;
        if ($matchAll) {
            $method .= '_all';
        }
        $n = $method($pattern, $subject, $matchInfo, $flag, $offset);
        $result = array();
        if ($n !== 0 && !empty($matchInfo)) {
            if (!$matchAll) {
                $matchInfo = array($matchInfo);
            }
            foreach ($matchInfo as $matches) {
                $positions = array();
                foreach ($matches as $match) {
                    $matchedText   = $match[0];
                    $matchedLength = $match[1];
                    $positions[]   = array(
                        $matchedText,
                        mb_strlen(mb_strcut($subject, 0, $matchedLength))
                    );
                }
                $result[] = $positions;
            }
            if (!$matchAll) {
                $result = $result[0];
            }
        }
        return $result;
    }

    $s1 = 'Попробуем русскую строку для теста';
    $s2 = 'Try english string for test';

    var_dump(pregMatchCapture(true, '/обу/', $s1));
    var_dump(pregMatchCapture(false, '/обу/', $s1));

    var_dump(pregMatchCapture(true, '/lish/', $s2));
    var_dump(pregMatchCapture(false, '/lish/', $s2));

Sortie de mon exemple:

    array(1) {
      [0]=>
      array(1) {
        [0]=>
        array(2) {
          [0]=>
          string(6) "обу"
          [1]=>
          int(4)
        }
      }
    }
    array(1) {
      [0]=>
      array(2) {
        [0]=>
        string(6) "обу"
        [1]=>
        int(4)
      }
    }
    array(1) {
      [0]=>
      array(1) {
        [0]=>
        array(2) {
          [0]=>
          string(4) "lish"
          [1]=>
          int(7)
        }
      }
    }
    array(1) {
      [0]=>
      array(2) {
        [0]=>
        string(4) "lish"
        [1]=>
        int(7)
      }
    }
4
Guy Fawkes

Si tout ce que vous voulez faire est de trouver la position sur plusieurs octets de H, essayez mb_strpos ()

mb_internal_encoding('UTF-8');
$str = "\xC2\xA1Hola!";
$pos = mb_strpos($str, 'H');
echo $str."\n";
echo $pos."\n";
echo mb_substr($str,$pos,1)."\n";

Sortie:

¡Hola!
1
H
1
velcrow

J'ai écrit une petite classe pour convertir les offsets retournés par preg_match en offsets appropriés:

final class NonUtfToUtfOffset
{
    /** @var int[] */
    private $utfMap = [];

    public function __construct(string $content)
    {
        $contentLength = mb_strlen($content);

        for ($offset = 0; $offset < $contentLength; $offset ++) {
            $char = mb_substr($content, $offset, 1);
            $nonUtfLength = strlen($char);

            for ($charOffset = 0; $charOffset < $nonUtfLength; $charOffset ++) {
                $this->utfMap[] = $offset;
            }
        }
    }

    public function convertOffset(int $nonUtfOffset): int
    {
        return $this->utfMap[$nonUtfOffset];
    }
}

Vous pouvez l'utiliser comme ça:

$content = 'aą bać d';
$offsetConverter = new NonUtfToUtfOffset($content);

preg_match_all('#(bać)#ui', $content, $m, PREG_OFFSET_CAPTURE);

foreach ($m[1] as [$Word, $offset]) {
    echo "bad: " . mb_substr($content, $offset, mb_strlen($Word))."\n";
    echo "good: " . mb_substr($content, $offsetConverter->convertOffset($offset), mb_strlen($Word))."\n";
}

https://3v4l.org/8Y32J

1
bronek89

Vous voudrez peut-être regarder T-Regx library.

pattern('/Hola/u')->match('\xC2\xA1Hola!')->first(function (Match $match) 
{
    echo $match->offset();     // characters
    echo $match->byteOffset(); // bytes
});

Ce $match->offset() est le décalage de sécurité UTF-8. 

0
Danon