Eliminar caracteres que no sean utf8 de la cadena

112

Tengo un problema con la eliminación de caracteres que no son utf8 de la cadena, que no se muestran correctamente. Los caracteres son así 0x97 0x61 0x6C 0x6F (representación hexadecimal)

¿Cuál es la mejor forma de eliminarlos? ¿Expresión regular o algo más?

Dan Sosedoff
fuente
1
Las soluciones enumeradas aquí no funcionaron para mí, así que encontré mi respuesta aquí en la sección "Validación de caracteres": webcollab.sourceforge.net/unicode.html
bobef
Relacionado con esto , pero no necesariamente un duplicado, más como un primo cercano :)
Wayne Weibel

Respuestas:

87

Usando un enfoque de expresiones regulares:

$regex = <<<'END'
/
  (
    (?: [\x00-\x7F]                 # single-byte sequences   0xxxxxxx
    |   [\xC0-\xDF][\x80-\xBF]      # double-byte sequences   110xxxxx 10xxxxxx
    |   [\xE0-\xEF][\x80-\xBF]{2}   # triple-byte sequences   1110xxxx 10xxxxxx * 2
    |   [\xF0-\xF7][\x80-\xBF]{3}   # quadruple-byte sequence 11110xxx 10xxxxxx * 3 
    ){1,100}                        # ...one or more times
  )
| .                                 # anything else
/x
END;
preg_replace($regex, '$1', $text);

Busca secuencias UTF-8 y las captura en el grupo 1. También coincide con bytes individuales que no se pudieron identificar como parte de una secuencia UTF-8, pero no los captura. El reemplazo es lo que se capturó en el grupo 1. Esto elimina efectivamente todos los bytes no válidos.

Es posible reparar la cadena codificando los bytes no válidos como caracteres UTF-8. Pero si los errores son aleatorios, esto podría dejar algunos símbolos extraños.

$regex = <<<'END'
/
  (
    (?: [\x00-\x7F]               # single-byte sequences   0xxxxxxx
    |   [\xC0-\xDF][\x80-\xBF]    # double-byte sequences   110xxxxx 10xxxxxx
    |   [\xE0-\xEF][\x80-\xBF]{2} # triple-byte sequences   1110xxxx 10xxxxxx * 2
    |   [\xF0-\xF7][\x80-\xBF]{3} # quadruple-byte sequence 11110xxx 10xxxxxx * 3 
    ){1,100}                      # ...one or more times
  )
| ( [\x80-\xBF] )                 # invalid byte in range 10000000 - 10111111
| ( [\xC0-\xFF] )                 # invalid byte in range 11000000 - 11111111
/x
END;
function utf8replacer($captures) {
  if ($captures[1] != "") {
    // Valid byte sequence. Return unmodified.
    return $captures[1];
  }
  elseif ($captures[2] != "") {
    // Invalid byte of the form 10xxxxxx.
    // Encode as 11000010 10xxxxxx.
    return "\xC2".$captures[2];
  }
  else {
    // Invalid byte of the form 11xxxxxx.
    // Encode as 11000011 10xxxxxx.
    return "\xC3".chr(ord($captures[3])-64);
  }
}
preg_replace_callback($regex, "utf8replacer", $text);

EDITAR:

  • !empty(x)coincidirá con los valores no vacíos ( "0"se considera vacío).
  • x != ""coincidirá con valores no vacíos, incluidos "0".
  • x !== ""coincidirá con cualquier cosa excepto "".

x != "" parece el mejor para usar en este caso.

También he acelerado un poco el partido. En lugar de hacer coincidir cada carácter por separado, hace coincidir secuencias de caracteres UTF-8 válidos.

Markus Jarderot
fuente
¿Qué usar en su lugar $regex = <<<'END'para PHP <5.3.x?
serhio
En su lugar, podría convertirlos al formato heredoc, con una pequeña penalización en la legibilidad. Otra posibilidad es utilizar cadenas de comillas simples, pero luego deberá eliminar los comentarios.
Markus Jarderot
Hay un pequeño error tipográfico en esta línea elseif (!empty($captures([2])) {y debe usar en !== ""lugar de vacío, ya que "0"se considera vacío. Además, esta función es muy lenta, ¿podría hacerse más rápido?
Kendall Hopkins
2
Esta expresión tiene un problema de memoria importante, consulte aquí .
Ja͢ck
1
@MarkusJarderot, Regex ... hmm, ¿esta función está lista para producción? ¿Existen casos de prueba para esta función?
Pacerier
132

Si aplica utf8_encode()a una cadena ya UTF8, devolverá una salida UTF8 distorsionada.

Hice una función que aborda todos estos problemas. Se llama Encoding::toUTF8().

No necesita saber cuál es la codificación de sus cadenas. Puede ser Latin1 (ISO8859-1), Windows-1252 o UTF8, o la cadena puede tener una combinación de ellos. Encoding::toUTF8()convertirá todo a UTF8.

Lo hice porque un servicio me estaba dando una fuente de datos en mal estado, mezclando esas codificaciones en la misma cadena.

Uso:

require_once('Encoding.php'); 
use \ForceUTF8\Encoding;  // It's namespaced now.

$utf8_string = Encoding::toUTF8($mixed_string);

$latin1_string = Encoding::toLatin1($mixed_string);

He incluido otra función, Encoding :: fixUTF8 (), que solucionará todas las cadenas UTF8 que se vean distorsionadas por haber sido codificadas en UTF8 varias veces.

Uso:

require_once('Encoding.php'); 
use \ForceUTF8\Encoding;  // It's namespaced now.

$utf8_string = Encoding::fixUTF8($garbled_utf8_string);

Ejemplos:

echo Encoding::fixUTF8("Fédération Camerounaise de Football");
echo Encoding::fixUTF8("Fédération Camerounaise de Football");
echo Encoding::fixUTF8("FÃÂédÃÂération Camerounaise de Football");
echo Encoding::fixUTF8("Fédération Camerounaise de Football");

dará salida:

Fédération Camerounaise de Football
Fédération Camerounaise de Football
Fédération Camerounaise de Football
Fédération Camerounaise de Football

Descargar:

https://github.com/neitanod/forceutf8

Sebastián Grignoli
fuente
13
¡Cosas excepcionales! Todas las demás soluciones descartan caracteres no válidos, pero esta lo corrige. Increíble.
giorgio79
4
¡Hiciste una gran función! Trabajé mucho con XML Feeds en el pasado y siempre tuve problemas con la codificación. Gracias.
Kostanos
5
TE QUIERO. Me ha ahorrado HORAS de trabajo "bloomoin" en caracteres UTF8 incorrectos. Gracias.
John Ballinger
4
Esto es fantástico. Gracias
EdgeCaseBerg
2
maravilloso, bien hecho! Me alegro de haber encontrado esto. Ojalá pudiera votar con +100 ;-)
Codebeat
61

Puede usar mbstring:

$text = mb_convert_encoding($text, 'UTF-8', 'UTF-8');

... eliminará los caracteres no válidos.

Ver: Reemplazo de caracteres UTF-8 no válidos por signos de interrogación, mbstring.substitute_character parece ignorado

Frosty Z
fuente
1
@Alliswell ¿cuáles? ¿Podría darnos un ejemplo?
Frosty Z
seguro,<0x1a>
Alliswell
1
@Alliswell Si no me equivoco <0x1a>, aunque no es un carácter imprimible, es una secuencia UTF-8 perfectamente válida. ¿Podría tener problemas con los caracteres no imprimibles? Compruebe esto: stackoverflow.com/questions/1176904/…
Frosty Z
sí, ese es el caso. ¡Gracias amigo!
Alliswell
Antes de llamar a mb convert, tuve que establecer el carácter sustituto mbstring en ninguno, de lo ini_set('mbstring.substitute_character', 'none');contrario, obtenía signos de interrogación en el resultado.
cby016
21

Esta función elimina todos los caracteres NO ASCII, es útil pero no resuelve la pregunta:
esta es mi función que siempre funciona, independientemente de la codificación:

function remove_bs($Str) {  
  $StrArr = str_split($Str); $NewStr = '';
  foreach ($StrArr as $Char) {    
    $CharNo = ord($Char);
    if ($CharNo == 163) { $NewStr .= $Char; continue; } // keep £ 
    if ($CharNo > 31 && $CharNo < 127) {
      $NewStr .= $Char;    
    }
  }  
  return $NewStr;
}

Cómo funciona:

echo remove_bs('Hello õhowå åare youÆ?'); // Hello how are you?
David D
fuente
8
¿Por qué nombres de funciones en mayúsculas? Ewww.
Chris Baker
5
es ASCII y ni siquiera se acerca a lo que quería la pregunta.
misaxi
1
Este funcionó. Enfrenté el problema cuando la API de Google Maps informó el error debido a un "carácter no UTF-8" en la URL de solicitud de la API. El culpable fue un ícarácter en el campo de dirección que ES un carácter UTF-8 válido, consulte la tabla . La moral: no confíe en los mensajes de error de la API :)
Valentine Shi
17
$text = iconv("UTF-8", "UTF-8//IGNORE", $text);

Esto es lo que estoy usando. Parece que funciona bastante bien. Tomado de http://planetozh.com/blog/2005/01/remove-invalid-characters-in-utf-8/

Znarkus
fuente
no funcionó para mí. Ojalá pudiera adjuntar la línea probada, pero desafortunadamente tiene caracteres no válidos.
Nir O.10 de
3
Lo siento, después de algunas pruebas más me di cuenta de que esto no estaba haciendo realmente lo que pensaba. Ahora estoy usando stackoverflow.com/a/8215387/138023
Znarkus
14

prueba esto:

$string = iconv("UTF-8","UTF-8//IGNORE",$string);

De acuerdo con el manual de iconv , la función tomará el primer parámetro como el juego de caracteres de entrada, el segundo parámetro como el juego de caracteres de salida y el tercero como la cadena de entrada real.

Si establece tanto el juego de caracteres de entrada como de salida en UTF-8 , y agrega la //IGNOREbandera al juego de caracteres de salida, la función eliminará (eliminará) todos los caracteres en la cadena de entrada que no pueden ser representados por el juego de caracteres de salida. Por lo tanto, filtrar la cadena de entrada en efecto.

tecnoarya
fuente
Explique lo que hace su respuesta en lugar de descargar un fragmento de código.
Tomasz Kowalczyk
3
He intentado esto y //IGNOREno parece suprimir el aviso de que está presente UTF-8 no válido (que, por supuesto, conozco y quiero corregir). Un comentario altamente calificado en el manual parece pensar que ha sido un error durante algunos años.
halfer
Siempre es mejor usarlo iconv. @halfer Quizás sus datos de entrada no sean de utf-8. Otra opción es hacer una reconversión a ascii y luego volver a utf-8 nuevamente. En mi caso utilicé iconvcomo$output = iconv("UTF-8//", "ISO-8859-1//IGNORE", $input );
m3nda
@ erm3nda: No recuerdo exactamente mi caso de uso para esto, podría haber analizado un sitio web UTF-8 declarado con el juego de caracteres incorrecto. Gracias por la nota, estoy seguro de que será de utilidad para un futuro lector.
halfer
Sí, si no sabes algo, solo
pruébalo
6

UConverter se puede utilizar desde PHP 5.5. UConverter es la mejor opción si usa la extensión intl y no usa mbstring.

function replace_invalid_byte_sequence($str)
{
    return UConverter::transcode($str, 'UTF-8', 'UTF-8');
}

function replace_invalid_byte_sequence2($str)
{
    return (new UConverter('UTF-8', 'UTF-8'))->convert($str);
}

htmlspecialchars se puede utilizar para eliminar secuencias de bytes no válidas desde PHP 5.4. Htmlspecialchars es mejor que preg_match para manejar un gran tamaño de bytes y la precisión. Se puede ver una gran cantidad de implementación incorrecta mediante el uso de expresiones regulares.

function replace_invalid_byte_sequence3($str)
{
    return htmlspecialchars_decode(htmlspecialchars($str, ENT_SUBSTITUTE, 'UTF-8'));
}
masakielástica
fuente
Tiene tres buenas soluciones, pero no está claro cómo un usuario elegiría entre ellas.
Bob Ray
6

He creado una función que elimina caracteres UTF-8 no válidos de una cadena. Lo estoy usando para borrar la descripción de 27000 productos antes de que genere el archivo de exportación XML.

public function stripInvalidXml($value) {
    $ret = "";
    $current;
    if (empty($value)) {
        return $ret;
    }
    $length = strlen($value);
    for ($i=0; $i < $length; $i++) {
        $current = ord($value{$i});
        if (($current == 0x9) || ($current == 0xA) || ($current == 0xD) || (($current >= 0x20) && ($current <= 0xD7FF)) || (($current >= 0xE000) && ($current <= 0xFFFD)) || (($current >= 0x10000) && ($current <= 0x10FFFF))) {
                $ret .= chr($current);
        }
        else {
            $ret .= "";
        }
    }
    return $ret;
}
mumin
fuente
De todas las complejas respuestas anteriores, ¡esta me sirvió! Gracias.
Emin Özlem
Estoy confundido por esta función. ord()devuelve resultados en el rango 0-255. El gigante ifde esta función prueba los rangos Unicode que ord()nunca volverán. Si alguien quiere aclarar por qué esta función funciona de la manera en que lo hace, agradecería la información.
i336_
4

Bienvenido a 2019 y al /umodificador en expresiones regulares que manejará caracteres multibyte UTF-8 por usted

Si solo lo usa mb_convert_encoding($value, 'UTF-8', 'UTF-8'), aún terminará con caracteres no imprimibles en su cadena

Este método:

  • Elimine todos los caracteres multibyte UTF-8 no válidos con mb_convert_encoding
  • Elimine todos los caracteres no imprimibles como \r, \x00(byte NULL) y otros caracteres de control conpreg_replace

método:

function utf8_filter(string $value): string{
    return preg_replace('/[^[:print:]\n]/u', '', mb_convert_encoding($value, 'UTF-8', 'UTF-8'));
}

[:print:]coincidir con todos los caracteres imprimibles y \nnuevas líneas y eliminar todo lo demás

Puede ver la tabla ASCII a continuación. Los caracteres imprimibles van de 32 a 127, pero la nueva línea \nes parte de los caracteres de control que van de 0 a 31, por lo que tenemos que agregar una nueva línea a la expresión regular/[^[:print:]\n]/u

https://cdn.shopify.com/s/files/1/1014/5789/files/Standard-ASCII-Table_large.jpg?10669400161723642407

Puede intentar enviar cadenas a través de la expresión regular con caracteres fuera del rango imprimible como \x7F(DEL), \x1B(Esc), etc. y ver cómo se eliminan

function utf8_filter(string $value): string{
    return preg_replace('/[^[:print:]\n]/u', '', mb_convert_encoding($value, 'UTF-8', 'UTF-8'));
}

$arr = [
    'Danish chars'          => 'Hello from Denmark with æøå',
    'Non-printable chars'   => "\x7FHello with invalid chars\r \x00"
];

foreach($arr as $k => $v){
    echo "$k:\n---------\n";
    
    $len = strlen($v);
    echo "$v\n(".$len.")\n";
    
    $strip = utf8_decode(utf8_filter(utf8_encode($v)));
    $strip_len = strlen($strip);
    echo $strip."\n(".$strip_len.")\n\n";
    
    echo "Chars removed: ".($len - $strip_len)."\n\n\n";
}

https://www.tehplayground.com/q5sJ3FOddhv1atpR

Clarkk
fuente
Bienvenido en 2047, donde php-mbstringno está empaquetado en php por defecto.
NVRM
3
$string = preg_replace('~&([a-z]{1,2})(acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml);~i', '$1', htmlentities($string, ENT_COMPAT, 'UTF-8'));
Alix Axel
fuente
2

Desde el parche reciente al módulo analizador JSON Feeds de Drupal:

//remove everything except valid letters (from any language)
$raw = preg_replace('/(?:\\\\u[\pL\p{Zs}])+/', '', $raw);

Si le preocupa, sí, conserva los espacios como caracteres válidos.

Hice lo que necesitaba. Elimina los caracteres emoji difundidos hoy en día que no encajan en el conjunto de caracteres 'utf8' de MySQL y que me dieron errores como "SQLSTATE [HY000]: Error general: 1366 Valor de cadena incorrecto".

Para obtener más información, consulte https://www.drupal.org/node/1824506#comment-6881382

Oleksii Chekulaiev
fuente
El iconves mucho mejor que el antiguo basado en expresiones regulares preg_replace, que está en desuso hoy en día.
m3nda
3
preg_replace no está obsoleto
Oleksii Chekulaiev
1
Tienes toda la razón, lo ereg_replace()siento.
m3nda
2

Quizás no sea la solución más precisa, pero hace el trabajo con una sola línea de código:

echo str_replace("?","",(utf8_decode($str)));

utf8_decodeconvertirá los caracteres en un signo de interrogación;
str_replaceeliminará los signos de interrogación.

usuario12602477
fuente
Después de probar cientos de soluciones, la única solución que funcionó es la suya.
Haritsinh Gohil
1

Entonces, las reglas son que el primer octlet UTF-8 tiene el bit alto establecido como marcador, y luego de 1 a 4 bits para indicar cuántos octlets adicionales; entonces cada uno de los octlets adicionales debe tener los dos bits altos establecidos en 10.

La pseudo-pitón sería:

newstring = ''
cont = 0
for each ch in string:
  if cont:
    if (ch >> 6) != 2: # high 2 bits are 10
      # do whatever, e.g. skip it, or skip whole point, or?
    else:
      # acceptable continuation of multi-octlet char
      newstring += ch
    cont -= 1
  else:
    if (ch >> 7): # high bit set?
      c = (ch << 1) # strip the high bit marker
      while (c & 1): # while the high bit indicates another octlet
        c <<= 1
        cont += 1
        if cont > 4:
           # more than 4 octels not allowed; cope with error
      if !cont:
        # illegal, do something sensible
      newstring += ch # or whatever
if cont:
  # last utf-8 was not terminated, cope

Esta misma lógica debería ser traducida a php. Sin embargo, no está claro qué tipo de eliminación se debe realizar una vez que se obtiene un personaje mal formado.

Será
fuente
c = (ch << 1)hará (c & 1)cero la primera vez, saltando el ciclo. La prueba probablemente debería ser(c & 128)
Markus Jarderot
1

Para eliminar todos los caracteres Unicode fuera del plano de idioma básico Unicode:

$str = preg_replace("/[^\\x00-\\xFFFF]/", "", $str);
Daniel Powers
fuente
0

Ligeramente diferente a la pregunta, pero lo que estoy haciendo es usar HtmlEncode (cadena),

pseudo código aquí

var encoded = HtmlEncode(string);
encoded = Regex.Replace(encoded, "&#\d+?;", "");
var result = HtmlDecode(encoded);

entrada y salida

"Headlight\x007E Bracket, &#123; Cafe Racer<> Style, Stainless Steel 中文呢?"
"Headlight~ Bracket, &#123; Cafe Racer<> Style, Stainless Steel 中文呢?"

Sé que no es perfecto, pero hace el trabajo por mí.

misaxi
fuente
0
static $preg = <<<'END'
%(
[\x09\x0A\x0D\x20-\x7E]
| [\xC2-\xDF][\x80-\xBF]
| \xE0[\xA0-\xBF][\x80-\xBF]
| [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2}
| \xED[\x80-\x9F][\x80-\xBF]
| \xF0[\x90-\xBF][\x80-\xBF]{2}
| [\xF1-\xF3][\x80-\xBF]{3}
| \xF4[\x80-\x8F][\x80-\xBF]{2}
)%xs
END;
if (preg_match_all($preg, $string, $match)) {
    $string = implode('', $match[0]);
} else {
    $string = '';
}

funciona en nuestro servicio

llluo
fuente
2
¿Puede agregar algo de contexto para explicar cómo esto responderá la pregunta, en lugar de una respuesta de solo código?
Arun Vinoth