Usuń znaki inne niż utf8 z ciągu


112

Mam problem z usunięciem znaków innych niż utf8 z łańcucha, które nie wyświetlają się poprawnie. Znaki są takie jak ten 0x97 0x61 0x6C 0x6F (reprezentacja szesnastkowa)

Jaki jest najlepszy sposób ich usunięcia? Wyrażenie regularne czy coś innego?


1
Wymienione tutaj rozwiązania nie zadziałały, więc znalazłem odpowiedź tutaj w sekcji „Walidacja postaci”: webcollab.sourceforge.net/unicode.html
bobef

Związane z tym , ale niekoniecznie duplikat, bardziej jak bliski kuzyn :)
Wayne Weibel

Odpowiedzi:


87

Korzystanie z podejścia 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);

Wyszukuje sekwencje UTF-8 i przechwytuje je w grupie 1. Pasuje również do pojedynczych bajtów, których nie można zidentyfikować jako części sekwencji UTF-8, ale ich nie przechwytuje. Zastąpieniem jest wszystko, co zostało przechwycone w grupie 1. To skutecznie usuwa wszystkie nieprawidłowe bajty.

Możliwa jest naprawa ciągu poprzez zakodowanie nieprawidłowych bajtów jako znaków UTF-8. Ale jeśli błędy są przypadkowe, może to pozostawić dziwne symbole.

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

EDYTOWAĆ:

  • !empty(x)dopasuje niepuste wartości ( "0"jest uważane za puste).
  • x != ""dopasuje niepuste wartości, w tym "0".
  • x !== ""dopasuje wszystko oprócz "".

x != "" wydaje się najlepszy do użycia w tym przypadku.

Trochę przyśpieszyłem też mecz. Zamiast dopasowywać każdy znak osobno, dopasowuje sekwencje prawidłowych znaków UTF-8.


czego użyć zamiast $regex = <<<'END'PHP <5.3.x?
serhio

Zamiast tego można przekonwertować je do formatu heredoc, z niewielką utratą czytelności. Inną możliwością jest użycie pojedynczych cudzysłowów, ale wtedy będziesz musiał usunąć komentarze.
Markus Jarderot

W tym wierszu jest mała literówka elseif (!empty($captures([2])) {i powinieneś użyć !== ""zamiast pustego, ponieważ "0"jest uważany za pusty. Ta funkcja jest również bardzo powolna, czy można to zrobić szybciej?
Kendall Hopkins

2
To wyrażenie ma poważny problem z pamięcią, patrz tutaj .
Ja͢ck

1
@MarkusJarderot, Regex ....... hmm, czy ta funkcja jest gotowa do produkcji? Czy istnieją przypadki testowe dla tej funkcji?
Pacerier

132

Jeśli zastosujesz się utf8_encode()do już napisu UTF8, zwróci to zniekształcone wyjście UTF8.

Stworzyłem funkcję, która rozwiązuje wszystkie te problemy. To się nazywa Encoding::toUTF8().

Nie musisz wiedzieć, jakie jest kodowanie twoich ciągów. Może to być Latin1 (ISO8859-1), Windows-1252 lub UTF8 albo ciąg znaków może mieć ich mieszankę. Encoding::toUTF8()przekonwertuje wszystko do UTF8.

Zrobiłem to, ponieważ usługa dostarczała mi wszystkie pomieszane dane, mieszając te kodowania w tym samym ciągu.

Stosowanie:

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

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

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

Dołączyłem inną funkcję, Encoding :: fixUTF8 (), która naprawia każdy ciąg znaków UTF8, który wygląda na zniekształcony produkt wielokrotnego zakodowania w UTF8.

Stosowanie:

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

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

Przykłady:

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

wyświetli:

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

Pobieranie:

https://github.com/neitanod/forceutf8


13
Znakomite rzeczy! Wszystkie inne rozwiązania odrzucają nieprawidłowe znaki, ale ten rozwiązuje problem. Niesamowite.
giorgio79

4
Zrobiłeś świetną funkcję! W przeszłości dużo pracowałem z XML Feeds i zawsze miałem problem z kodowaniem. Dziękuję Ci.
Kostanos

5
KOCHAM CIĘ. Zaoszczędziłeś mi GODZINY pracy "bloomoin" na złych znakach UTF8. Dzięki.
John Ballinger

4
To jest fantastyczne. Dziękuję
EdgeCaseBerg

2
wspaniale, dobra robota! Cieszę się, że to znalazłem. Chciałbym móc głosować z +100 ;-)
Codebeat

61

Możesz użyć mbstring:

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

... usunie nieprawidłowe znaki.

Zobacz: Zastępowanie nieprawidłowych znaków UTF-8 znakami zapytania, mbstring.substitute_character wydaje się ignorowane


1
@Alliswell, które z nich? Czy mógłbyś podać przykład?
Frosty Z

<0x1a>
jasne

1
@Alliswell Jeśli się nie mylę <0x1a>, chociaż nie jest to znak drukowalny, jest to całkowicie poprawna sekwencja UTF-8. Możesz mieć problemy ze znakami niedrukowalnymi? Sprawdź to: stackoverflow.com/questions/1176904/…
Frosty Z

tak, o to chodzi. Dzięki stary!
Alliswell,

Przed wywołaniem mb convert, musiałem ustawić znak zastępczy mbstring na none, w ini_set('mbstring.substitute_character', 'none');przeciwnym razie otrzymywałem znaki zapytania w wyniku.
cby016

21

Ta funkcja usuwa wszystkie znaki inne niż ASCII, jest przydatna, ale nie rozwiązuje pytania:
To moja funkcja, która zawsze działa, niezależnie od kodowania:

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

Jak to działa:

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

8
Dlaczego nazwy funkcji pisane wielkimi literami? Ewww.
Chris Baker,

5
jest to ASCII i nawet nie jest zbliżone do tego, o co chodziło w pytaniu.
misaxi

1
Ten działał. Napotkałem problem, gdy interfejs API Map Google zgłosił błąd z powodu „znaku innego niż UTF-8” w adresie URL żądania API. Winowajcą był íznak w polu adresu, który JEST prawidłowym znakiem UTF-8, patrz tabela . Morale: nie ufaj komunikatom o błędach API :)
Valentine Shi

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

To jest to, czego używam. Wydaje się, że działa całkiem nieźle. Zaczerpnięte z http://planetozh.com/blog/2005/01/remove-invalid-characters-in-utf-8/


nie działa dla mnie. Chciałbym móc dołączyć testowaną linię, ale niestety ma ona nieprawidłowe znaki.
Nir O.

3
Przepraszam, po kilku dalszych testach zdałem sobie sprawę, że to nie działa tak, jak myślałem. Teraz używam stackoverflow.com/a/8215387/138023
Znarkus

14

Spróbuj tego:

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

Zgodnie z instrukcją iconv , funkcja przyjmie pierwszy parametr jako zestaw znaków wejściowych, drugi parametr jako zestaw znaków wyjściowych, a trzeci jako rzeczywisty łańcuch wejściowy.

Jeśli ustawisz zarówno wejściowy, jak i wyjściowy zestaw znaków na UTF-8 i dodasz //IGNOREflagę do wyjściowego zestawu znaków, funkcja usunie (usunie) wszystkie znaki w ciągu wejściowym, których nie może reprezentować wyjściowy zestaw znaków. W ten sposób działa filtrowanie ciągu wejściowego.


Wyjaśnij, co robi twoja odpowiedź, zamiast porzucać fragment kodu.
Tomasz Kowalczyk

3
Wypróbowałem to i //IGNOREnie wydaje się, aby pomijał informację, że obecny jest nieprawidłowy UTF-8 (co oczywiście wiem i chcę naprawić). Wysoko oceniany komentarz w podręczniku wydaje się sugerować, że był to błąd od kilku lat.
halfer

Zawsze lepiej jest używać iconv. @halfer Może twoje dane wejściowe nie pochodzą z utf-8. Inną opcją jest dokonanie ponownej konwersji na ascii, a następnie z powrotem na utf-8. W moim przypadku użyłem iconvtak$output = iconv("UTF-8//", "ISO-8859-1//IGNORE", $input );
m3nda

@ erm3nda: Dokładnie nie pamiętam swojego przypadku użycia w tym przypadku - mogłem przeanalizować witrynę internetową UTF-8 zadeklarowaną z niewłaściwym zestawem znaków. Dzięki za notatkę, jestem pewien, że przyda się przyszłemu czytelnikowi.
halfer

Tak, jeśli czegoś nie wiesz, po prostu przetestuj, a na koniec uderzysz w klawisz ;-)
m3nda


6

UConverter może być używany od PHP 5.5. UConverter jest lepszym wyborem, jeśli używasz rozszerzenia intl i nie używasz 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 może służyć do usuwania nieprawidłowej sekwencji bajtów od PHP 5.4. Htmlspecialchars jest lepszy niż preg_match do obsługi dużych rozmiarów bajtów i dokładności. Widać wiele nieprawidłowych implementacji przy użyciu wyrażeń regularnych.

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

Masz trzy fajne rozwiązania, ale nie jest jasne, jaki użytkownik wybrałby spośród nich.
Bob Ray

6

Zrobiłem funkcję, która usuwa nieprawidłowe znaki UTF-8 z ciągu. Używam go do jasnego opisu 27000 produktów, zanim wygeneruje plik eksportu 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;
}

Ze wszystkich złożonych odpowiedzi powyżej, ta załatwiła mi sprawę! Dzięki.
Emin Özlem

Jestem zdezorientowany tą funkcją. ord()zwraca wyniki z zakresu 0-255. Gigant ifw tej funkcji testuje zakresy Unicode, ord()które nigdy nie wrócą. Jeśli ktoś chce wyjaśnić, dlaczego ta funkcja działa tak, jak działa, byłbym wdzięczny za wgląd.
i336_

4

Witamy w roku 2019 i /umodyfikatorze w wyrażeniu regularnym, który będzie obsługiwał za Ciebie wielobajtowe znaki UTF-8

Jeśli użyjesz tylko mb_convert_encoding($value, 'UTF-8', 'UTF-8')znaków, w swoim ciągu nadal będziesz mieć niedrukowalne znaki

Ta metoda:

  • Usuń wszystkie nieprawidłowe wielobajtowe znaki UTF-8 za pomocą mb_convert_encoding
  • Usuń wszystkie niedrukowalne znaki takie jak \r, \x00(null-bajt) i inne znaki kontrolne zpreg_replace

metoda:

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

[:print:]dopasuj wszystkie drukowalne znaki i znaki \nnowej linii oraz usuń wszystko inne

Możesz zobaczyć tabelę ASCII poniżej .. Znaki drukowalne mieszczą się w zakresie od 32 do 127, ale \nznak nowej linii jest częścią znaków kontrolnych z zakresu od 0 do 31, więc musimy dodać nową linię do wyrażenia regularnego/[^[:print:]\n]/u

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

Możesz spróbować wysłać ciągi przez wyrażenie regularne ze znakami spoza drukowalnego zakresu, jak \x7F(DEL), \x1B(Esc) itp. I zobaczyć, jak są one usuwane

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


Witamy w 2047 roku, gdzie php-mbstringdomyślnie nie jest spakowany w php.
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

Od ostatniej łatki do modułu parsera JSON Feeds w Drupalu:

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

Jeśli martwisz się, tak, zachowuje spacje jako prawidłowe znaki.

Zrobiłem to, czego potrzebowałem. Usuwa szeroko rozpowszechnione obecnie znaki emoji, które nie pasują do zestawu znaków MySQL „utf8”, co dało mi błędy typu „SQLSTATE [HY000]: Błąd ogólny: 1366 Niepoprawna wartość ciągu”.

Aby uzyskać szczegółowe informacje, zobacz https://www.drupal.org/node/1824506#comment-6881382


Jest iconvon znacznie lepszy niż staroświecki regexp preg_replace, który obecnie jest przestarzały.
m3nda

3
preg_replace nie jest przestarzałe
Oleksii Chekulaiev

1
Masz całkowitą rację ereg_replace(), przepraszam.
m3nda

2

Może nie jest to najbardziej precyzyjne rozwiązanie, ale wykonuje zadanie za pomocą jednej linii kodu:

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

utf8_decodezamieni znaki na znak zapytania;
str_replaceusunie znaki zapytania.


Po wypróbowaniu setek rozwiązań jedyne, które działało, jest Twoje.
Haritsinh Gohil

1

Reguły są więc takie, że pierwszy oktlet UTF-8 ma ustawiony wysoki bit jako znacznik, a następnie od 1 do 4 bitów, aby wskazać, ile dodatkowych oktletów; wtedy każdy z dodatkowych oktletów musi mieć dwa wysokie bity ustawione na 10.

Pseudo-Python wyglądałby tak:

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

Ta sama logika powinna być przetłumaczalna na php. Jednak nie jest jasne, jaki rodzaj strippingu należy wykonać, gdy pojawi się zdeformowana postać.


c = (ch << 1)zrobi (c & 1)zero za pierwszym razem, pomijając pętlę. Test prawdopodobnie powinien być(c & 128)
Markus Jarderot,

1

Aby usunąć wszystkie znaki Unicode spoza podstawowej płaszczyzny języka Unicode:

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

0

Trochę inaczej niż w pytaniu, ale ja robię to używając HtmlEncode (string),

pseudo kod tutaj

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

wejście i wyjście

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

Wiem, że to nie jest idealne, ale spełnia swoje zadanie.


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

to działa w naszej usłudze


2
Czy możesz dodać kontekst, aby wyjaśnić, jak to odpowie na pytanie, zamiast odpowiedzi obejmującej tylko kod.
Arun Vinoth

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.