que - preg_match y UTF-8 en PHP
regex php string (6)
Estoy intentando buscar una cadena codificada en UTF8 usando preg_match .
preg_match(''/H/u'', "/xC2/xA1Hola!", $a_matches, PREG_OFFSET_CAPTURE);
echo $a_matches[0][1];
Esto debería imprimir 1, ya que "H" está en el índice 1 en la cadena "¡Hola!". Pero imprime 2. Parece que no trata al sujeto como una cadena codificada en UTF8, a pesar de que estoy pasando el modifier "u" en la expresión regular.
Tengo la siguiente configuración en mi php.ini, y otras funciones UTF8 están funcionando:
mbstring.func_overload = 7
mbstring.language = Neutral
mbstring.internal_encoding = UTF-8
mbstring.http_input = pass
mbstring.http_output = pass
mbstring.encoding_translation = Off
¿Algunas ideas?
Aunque el modificador u hace que tanto el patrón como el sujeto se interpreten como UTF-8, los desplazamientos capturados todavía se cuentan en bytes.
Puede usar mb_strlen
para obtener la longitud en caracteres UTF-8 en lugar de bytes:
$str = "/xC2/xA1Hola!";
preg_match(''/H/u'', $str, $a_matches, PREG_OFFSET_CAPTURE);
echo mb_strlen(substr($str, 0, $a_matches[0][1]));
Escribí una clase pequeña para convertir las compensaciones devueltas por preg_match a las compensaciones utf correctas:
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];
}
}
Puedes usarlo así:
$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";
}
Intente agregar esto (* UTF8) antes de la expresión regular:
preg_match(''(*UTF8)/H/u'', "/xC2/xA1Hola!", $a_matches, PREG_OFFSET_CAPTURE);
Magia, gracias a un comentario en http://www.php.net/manual/es/function.preg-match.php#95828
Parece que esta es una "característica", ver http://bugs.php.net/bug.php?id=37391
''u'' cambiar solo tiene sentido para pcre, PHP mismo no lo sabe.
Desde el punto de vista de PHP, las cadenas son secuencias de bytes y el desplazamiento del byte de retorno parece lógico (no digo "correcto").
Perdóneme por necroposting, pero puede ser que alguien lo encuentre útil: el código a continuación puede funcionar como reemplazo de las funciones preg_match y preg_match_all y devuelve las coincidencias correctas con el desplazamiento correcto para las cadenas codificadas 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));
Salida de mi ejemplo:
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)
}
}
Si todo lo que quiere hacer es encontrar la posición segura de múltiples bytes de H try 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";
Salida:
¡Hola!
1
H