Rimuove i caratteri non utf8 dalla stringa


112

Ho un problema con la rimozione di caratteri non utf8 dalla stringa, che non vengono visualizzati correttamente. I caratteri sono come questo 0x97 0x61 0x6C 0x6F (rappresentazione esadecimale)

Qual è il modo migliore per rimuoverli? Espressione regolare o qualcos'altro?


1
Le soluzioni elencate qui non hanno funzionato per me, quindi ho trovato la mia risposta qui nella sezione "Convalida del carattere": webcollab.sourceforge.net/unicode.html
bobef

In relazione a questo , ma non necessariamente un duplicato, più simile a un cugino stretto :)
Wayne Weibel

Risposte:


87

Utilizzando un approccio regex:

$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);

Cerca le sequenze UTF-8 e le acquisisce nel gruppo 1. Trova anche singoli byte che non è stato possibile identificare come parte di una sequenza UTF-8, ma non li acquisisce. La sostituzione è tutto ciò che è stato catturato nel gruppo 1. Ciò rimuove efficacemente tutti i byte non validi.

È possibile riparare la stringa codificando i byte non validi come caratteri UTF-8. Ma se gli errori sono casuali, questo potrebbe lasciare dei simboli strani.

$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);

MODIFICARE:

  • !empty(x)corrisponderà a valori non vuoti ( "0"è considerato vuoto).
  • x != ""corrisponderà a valori non vuoti, inclusi "0".
  • x !== ""corrisponderà a qualsiasi cosa tranne "".

x != "" sembra il migliore da usare in questo caso.

Ho anche accelerato un po 'la partita. Invece di abbinare ogni carattere separatamente, corrisponde a sequenze di caratteri UTF-8 validi.


cosa usare invece $regex = <<<'END'per PHP <5.3.x?
serhio

Potresti invece convertirli nel formato heredoc, con una leggera penalità per la leggibilità. Un'altra possibilità è usare stringhe di virgolette singole, ma poi dovrai rimuovere i commenti.
Markus Jarderot

C'è un piccolo errore di battitura in questa riga elseif (!empty($captures([2])) {e dovresti usare !== ""invece di vuoto poiché "0"è considerato vuoto. Anche questa funzione è molto lenta, potrebbe essere eseguita più velocemente?
Kendall Hopkins

2
Questa espressione ha un grosso problema di memoria, vedi qui .
Ja͢ck

1
@MarkusJarderot, Regex ....... hmm, questa funzione è pronta per la produzione? Esistono casi di test per questa funzione?
Pacerier

132

Se si applica utf8_encode()a una stringa già UTF8, verrà restituito un output UTF8 confuso.

Ho creato una funzione che risolve tutti questi problemi. Si chiama Encoding::toUTF8().

Non hai bisogno di sapere qual è la codifica delle tue stringhe. Può essere Latin1 (ISO8859-1), Windows-1252 o UTF8 oppure la stringa può avere una combinazione di essi. Encoding::toUTF8()convertirà tutto in UTF8.

L'ho fatto perché un servizio mi dava un feed di dati tutto incasinato, mescolando quelle codifiche nella stessa stringa.

Uso:

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

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

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

Ho incluso un'altra funzione, Encoding :: fixUTF8 (), che risolverà ogni stringa UTF8 che sembra un prodotto confuso di essere stata codificata in UTF8 più volte.

Uso:

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

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

Esempi:

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");

produrrà:

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

Scarica:

https://github.com/neitanod/forceutf8


13
Roba eccezionale! Tutte le altre soluzioni eliminano i caratteri non validi, ma questa lo risolve. Eccezionale.
giorgio79

4
Hai fatto la grande funzione! Ho lavorato molto con i feed XML in passato e ho sempre avuto problemi con la codifica. Grazie.
Kostanos

5
TI AMO. Mi hai risparmiato ORE di lavoro "bloomoin" su caratteri UTF8 non validi. Grazie.
John Ballinger

4
È fantastico. Grazie
EdgeCaseBerg

2
meraviglioso, ben fatto! Sono contento di aver trovato questo. Vorrei poter votare con +100 ;-)
Codebeat

61

Puoi usare mbstring:

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

... rimuoverà i caratteri non validi.

Vedi: Sostituzione di caratteri UTF-8 non validi con punti interrogativi, mbstring.substitute_character sembra ignorato


1
@Alliswell quali? Potresti fornire un esempio?
Frosty Z

certo,<0x1a>
Alliswell

1
@Alliswell Se non sbaglio <0x1a>, anche se non è un carattere stampabile, è una sequenza UTF-8 perfettamente valida. Potresti avere problemi con i caratteri non stampabili? Controlla questo: stackoverflow.com/questions/1176904/…
Frosty Z

sì, questo è il caso. Grazie compagno!
Alliswell

Prima di chiamare mb convert, dovevo impostare il carattere sostitutivo mbstring su nessuno ini_set('mbstring.substitute_character', 'none');altrimenti stavo ottenendo punti interrogativi nel risultato.
cby016

21

Questa funzione rimuove tutti i caratteri NON ASCII, è utile ma non risolve la domanda:
questa è la mia funzione che funziona sempre, indipendentemente dalla codifica:

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;
}

Come funziona:

echo remove_bs('Hello õhowå åare youÆ?'); // Hello how are you?

8
Perché i nomi delle funzioni in maiuscolo? Ewww.
Chris Baker

5
è ASCII e nemmeno vicino a ciò che voleva la domanda.
misaxi

1
Questo ha funzionato. Ho riscontrato il problema quando l'API di Google Maps ha segnalato l'errore dovuto a "carattere non UTF-8" nell'URL della richiesta API. Il colpevole era un ícarattere nel campo dell'indirizzo che È un carattere UTF-8 valido vedi tabella . Il morale: non fidarti dei messaggi di errore API :)
Valentine Shi


14

prova questo:

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

Secondo il manuale di iconv , la funzione prenderà il primo parametro come set di caratteri di input, il secondo parametro come set di caratteri di output e il terzo come stringa di input effettiva.

Se imposti sia il set di caratteri di input che quello di output su UTF-8 e aggiungi il //IGNOREflag al set di caratteri di output, la funzione eliminerà (eliminerà) tutti i caratteri nella stringa di input che non possono essere rappresentati dal set di caratteri di output. Pertanto, filtrando la stringa di input in vigore.


Spiega cosa fa la tua risposta piuttosto che scaricare uno snippet di codice.
Tomasz Kowalczyk

3
L'ho provato e //IGNOREnon sembra sopprimere l'avviso che è presente UTF-8 non valido (che, ovviamente, conosco e voglio correggere). Un commento molto apprezzato nel manuale sembra pensare che sia stato un bug per alcuni anni.
halfer

È sempre meglio usare iconv. @halfer Forse i tuoi dati di input non provengono da utf-8. Un'altra opzione è fare una riconversione in ascii e poi di nuovo in utf-8. Nel mio caso ho usato iconvcome$output = iconv("UTF-8//", "ISO-8859-1//IGNORE", $input );
m3nda

@ erm3nda: Non ricordo esattamente il mio caso d'uso per questo - potrebbe aver analizzato un sito Web UTF-8 dichiarato con il set di caratteri sbagliato. Grazie per la nota, sono sicuro che sarà utile per un futuro lettore.
halfer

Sì, se non sai qualcosa,
provalo


6

UConverter può essere utilizzato da PHP 5.5. UConverter è la scelta migliore se usi l'estensione intl e non usi 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 può essere utilizzato per rimuovere una sequenza di byte non valida a partire da PHP 5.4. Htmlspecialchars è migliore di preg_match per la gestione di byte di grandi dimensioni e la precisione. Si possono vedere molte delle implementazioni sbagliate usando espressioni regolari.

function replace_invalid_byte_sequence3($str)
{
    return htmlspecialchars_decode(htmlspecialchars($str, ENT_SUBSTITUTE, 'UTF-8'));
}

Hai tre belle soluzioni, ma non è chiaro come un utente sceglierebbe tra di loro.
Bob Ray

6

Ho creato una funzione che elimina i caratteri UTF-8 non validi da una stringa. Lo sto usando per cancellare la descrizione di 27000 prodotti prima che generi il file di esportazione 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;
}

Di tutte le risposte complesse sopra, questa ha fatto il trucco per me! Grazie.
Emin Özlem

Sono confuso da questa funzione. ord()restituisce risultati nell'intervallo 0-255. Il gigante ifin questa funzione verifica gli intervalli Unicode che ord()non torneranno mai. Se qualcuno vuole chiarire perché questa funzione funziona in questo modo, apprezzerei l'intuizione.
i336_

4

Benvenuto nel 2019 e il /umodificatore in regex che gestirà i caratteri multibyte UTF-8 per te

Se usi solo, mb_convert_encoding($value, 'UTF-8', 'UTF-8')ti ritroverai comunque con caratteri non stampabili nella stringa

Questo metodo:

  • Rimuovi tutti i caratteri multibyte UTF-8 non validi con mb_convert_encoding
  • Rimuovi tutti i caratteri non stampabili come \r, \x00(NULL-byte) e altri caratteri di controllo conpreg_replace

metodo:

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

[:print:]abbina tutti i caratteri stampabili e le \nnuove righe e rimuove tutto il resto

Puoi vedere la tabella ASCII qui sotto .. I caratteri stampabili vanno da 32 a 127, ma la nuova riga \nfa parte dei caratteri di controllo che vanno da 0 a 31 quindi dobbiamo aggiungere una nuova riga alla regex/[^[:print:]\n]/u

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

Puoi provare a inviare stringhe attraverso la regex con caratteri al di fuori dell'intervallo stampabile come \x7F(DEL), \x1B(Esc) ecc. E vedere come vengono rimossi

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


Benvenuto nel 2047, dove php-mbstringnon è incluso in php per impostazione predefinita.
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'));

2

Dalla patch recente al modulo parser JSON di Feeds di Drupal:

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

Se sei preoccupato sì, mantiene gli spazi come caratteri validi.

Ho fatto quello di cui avevo bisogno. Rimuove i caratteri emoji più diffusi al giorno d'oggi che non rientrano nel set di caratteri "utf8" di MySQL e che mi hanno dato errori come "SQLSTATE [HY000]: Errore generale: 1366 Valore stringa errato".

Per i dettagli, vedere https://www.drupal.org/node/1824506#comment-6881382


Il iconvè di gran lunga migliore rispetto al vecchio stile regexp base preg_replace, wich è deprecato al giorno d'oggi.
m3nda

3
preg_replace non è deprecato
Oleksii Chekulaiev

1
Hai assolutamente ragione, è il ereg_replace(), mi dispiace.
m3nda

2

Forse non è la soluzione più precisa, ma porta a termine il lavoro con una singola riga di codice:

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

utf8_decodeconvertirà i caratteri in un punto interrogativo;
str_replaceeliminerà i punti interrogativi.


Dopo aver provato centinaia di soluzioni, l'unica soluzione che ha funzionato è la tua.
Haritsinh Gohil

1

Quindi le regole sono che il primo ottoletto UTF-8 ha il bit alto impostato come indicatore, e quindi da 1 a 4 bit per indicare quanti ottetti aggiuntivi; quindi ciascuno degli ottetti aggiuntivi deve avere i due bit alti impostati su 10.

Lo pseudo-pitone sarebbe:

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

Questa stessa logica dovrebbe essere traducibile in php. Tuttavia, non è chiaro che tipo di spogliatura debba essere eseguita una volta ottenuto un personaggio malformato.


c = (ch << 1)farà (c & 1)zero la prima volta, saltando il ciclo. Il test dovrebbe probabilmente essere(c & 128)
Markus Jarderot

1

Per rimuovere tutti i caratteri Unicode al di fuori del piano linguistico di base Unicode:

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

0

Leggermente diverso dalla domanda, ma quello che sto facendo è usare HtmlEncode (stringa),

pseudo codice qui

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

ingresso e uscita

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

So che non è perfetto, ma fa il lavoro per me.


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 = '';
}

funziona sul nostro servizio


2
Puoi aggiungere un po 'di contesto per spiegare come questo risponderà alla domanda, invece di una risposta di solo codice.
Arun Vinoth il

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.