Видаліть символи non-utf8 із рядка


112

У мене виникають проблеми з видаленням символів non-utf8 з рядка, які не відображаються належним чином. Персонажі такі 0x97 0x61 0x6C 0x6F (шістнадцяткове представлення)

Який найкращий спосіб їх видалити? Регулярне вираження чи щось інше?


1
Перелічені тут рішення не спрацювали для мене, тому я знайшов свою відповідь тут у розділі "Перевірка символів": webcollab.sourceforge.net/unicode.html
bobef

Пов’язаний з цим , але не обов’язково дублікатом, більше схожий на близького двоюрідного брата :)
Wayne Weibel

Відповіді:


87

Використання методу регулярного вираження:

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

Він здійснює пошук послідовностей UTF-8 та фіксує їх у групу 1. Він також відповідає одиничним байтам, які не можна було б визначити як частину послідовності UTF-8, але не захоплює їх. Заміна - все, що було захоплено в групу 1. Це ефективно видаляє всі недійсні байти.

Можна відновити рядок, кодуючи недійсні байти як символи UTF-8. Але якщо помилки випадкові, це може залишити деякі дивні символи.

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

Редагувати:

  • !empty(x)відповідатиме не порожнім значенням ( "0"вважається порожнім).
  • x != ""відповідатиме не порожнім значенням, у тому числі "0".
  • x !== ""відповідатиме будь-що, крім "".

x != "" здається найкращим, який можна використати в цьому випадку.

Я також трохи прискорив матч. Замість відповідності кожному символу окремо, він відповідає послідовностям дійсних символів UTF-8.


що використовувати замість $regex = <<<'END'PHP <5.3.x?
Серхіо

Ви можете замість цього перетворити їх у формат heredoc, з легким покаранням для читабельності. Інша можливість - використовувати рядки з цитатами, але тоді вам доведеться видалити коментарі.
Маркус Джардеро

У цьому рядку є невелика помилка друку, elseif (!empty($captures([2])) {яку слід використовувати !== ""замість порожньої, оскільки "0"вона вважається порожньою. Також ця функція дуже повільна, чи можна це зробити швидше?
Кендалл Хопкінс

2
Цей вираз має головне питання пам'яті, дивіться тут .
Ja͢ck

1
@MarkusJarderot, Regex ....... хм, чи готова ця функція виробництва? Чи є тестові випадки для цієї функції?
Pacerier

132

Якщо ви застосуєте utf8_encode()до вже рядка UTF8, він поверне зібраний вихід UTF8.

Я зробив функцію, яка вирішує всі ці проблеми. Це називається Encoding::toUTF8().

Вам не потрібно знати, що таке кодування ваших рядків. Це може бути Latin1 (ISO8859-1), Windows-1252 або UTF8, або рядок може мати їх суміш.Encoding::toUTF8()перетворить все в UTF8.

Я зробив це через те, що сервіс давав мені канал даних, які всі переплутали, змішуючи ці кодування в тій же строці.

Використання:

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

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

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

Я включив ще одну функцію, Encoding :: fixUTF8 (), яка буде виправляти кожну рядок UTF8, який виглядає скасованим продуктом того, що був закодований в UTF8 кілька разів.

Використання:

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

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

Приклади:

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

виведе:

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

Завантажити:

https://github.com/neitanod/forceutf8


13
Видатні речі! Усі інші рішення відкидають недійсні символи, але це виправляє. Дивовижно.
giorgio79

4
Ви зробили чудову функцію! Я багато працював з XML-каналами в минулому і завжди мав проблеми з кодуванням. Дякую.
Костанос

5
Я ТЕБЕ ЛЮБЛЮ. Ви врятували мені ГОЛОВНУ "bloomoin" роботи на поганих символах UTF8. Дякую.
Джон Баллінгер

4
Це фантастично. Дякую
EdgeCaseBerg

2
дивовижно, молодець! Радий, що я це знайшов. Я б хотів проголосувати за +100 ;-)
Codebeat

61

Ви можете використовувати mbstring:

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

... видалить недійсні символи.

Див.: Заміна недійсних символів UTF-8 знаками запитання, mbstring.substitute_character здається ігнорованим


1
@Alliswell, які з них? Чи можете ви надати приклад?
Морозний Z

звичайно,<0x1a>
Alliswell

1
@Alliswell Якщо я не помиляюся <0x1a>, хоча і не надрукований символ, це цілком правильна послідовність UTF-8. У вас можуть виникнути проблеми з символами, які не можна друкувати? Перевірте це: stackoverflow.com/questions/1176904/…
Frosty Z

так, саме так. Дякую, друже!
Alliswell

Перш ніж викликати mb convert, я повинен був встановити символ замінника mbstring жодним ini_set('mbstring.substitute_character', 'none');інакше, я отримував запитання в результаті.
cby016

21

Ця функція видаляє всі символи NON ASCII, це корисно, але не вирішує питання:
Це моя функція, яка завжди працює, незалежно від кодування:

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

Як це працює:

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

8
Чому імена функцій all-caps? Ewww
Кріс Бейкер

5
це ASCII і навіть не близьке до того, чого хотіли запитання.
misaxi

1
Цей працював. Я зіткнувся з проблемою, коли API Карт Google повідомив про помилку через "не-UTF-8 символ" у URL-адресі запиту API. Винуватець був íсимволом у адресному полі, яке є дійсним символом UTF-8, див. Таблицю . Мораль: не довіряйте повідомленням про помилки API :)
Валентина Ши

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

Це я і використовую. Здається, працює досить добре. Взято з http://planetozh.com/blog/2005/01/remove-invalid-characters-in-utf-8/


не працювало для мене. Я б хотів, щоб я міг прикріпити тестований рядок, але, на жаль, він має недійсні символи.
Нір О.

3
Вибачте, після ще одного тестування я зрозумів, що це насправді не те, що я думав. Я тепер з допомогою stackoverflow.com/a/8215387/138023
Znarkus

14

спробуйте це:

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

Відповідно до посібника з iconv , функція буде приймати перший параметр як вхідну схему, другий параметр як вихідний діапазон, а третій - як фактичний рядок введення.

Якщо встановити як вхідний, так і вихідний діапазон на UTF-8 і додати //IGNOREпрапор до вихідної діаграми, функція видалить (викреслить) всі символи в рядок введення, які не можуть бути представлені вихідною схемою. Таким чином, фільтруючи вхідний рядок по суті.


Поясніть, що робить ваша відповідь, а не скидання фрагмента коду.
Томаш Ковальчик

3
Я спробував це, і //IGNORE, схоже, не пригнічує повідомлення про наявність недійсного UTF-8 (що, звичайно, я знаю і хочу виправити). Високо оцінений коментар у посібнику, здається, вважає, що це помилка вже кілька років.
половина

Завжди краще використовувати iconv. @halfer Можливо, ваші вхідні дані не з utf-8. Інший варіант - зробити повторну конвертацію в ascii, потім знову повернутися до utf-8. У моєму випадку я використовував, iconvяк$output = iconv("UTF-8//", "ISO-8859-1//IGNORE", $input );
m3nda

@ erm3nda: Я точно не пам’ятаю свого випадку використання для цього - можливо, був би проаналізований веб-сайт UTF-8, оголошений неправильною схемою. Дякую за замітку, я впевнений, що стане в нагоді майбутньому читачеві.
півзахисник

Так, якщо ви чогось не знаєте, просто тестуйте це, і нарешті ви натиснете на ключ ;-)
m3nda


6

UConverter можна використовувати з PHP 5.5. UConverter - кращий вибір, якщо ви використовуєте розширення intl і не використовуєте 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 можна використовувати для видалення недійсної послідовності байтів з PHP 5.4. Htmlspecialchars краще, ніж preg_match для обробки великих розмірів байтів та точності. Багато неправильної реалізації за допомогою регулярного вираження можна побачити.

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

У вас є три приємних рішення, але незрозуміло, як користувач обрав би їх серед них.
Боб Рей

6

Я створив функцію, яка видаляє з рядка недійсні символи UTF-8. Я використовую його, щоб очистити опис 27000 продуктів, перш ніж він створить файл експорту 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;
}

З усіх складних відповідей, наведених вище, цей зробив для мене трюк! Дякую.
Емін Озлем

Мене бентежить ця функція. ord()повертає результати в діапазоні 0-255. Гігант ifу цій функції тестує діапазони унікоду, які ord()ніколи не повернуться. Якщо хтось хоче уточнити, чому ця функція працює так, як вона працює, я би вдячний прозрінню.
i336_

4

Ласкаво просимо до 2019 року та /uмодифікатора в регулярному вираженні, який буде обробляти багатоканальні символи UTF-8 для вас

Якщо ви користуєтесь лише, mb_convert_encoding($value, 'UTF-8', 'UTF-8')ви все одно матимете недруковані символи у рядку

Цей метод:

  • Видаліть усі недійсні багатобайтові символи UTF-8 mb_convert_encoding
  • Видалити всі недруковані символи , такі як \r, \x00(NULL-байт) і інші символи управління зpreg_replace

метод:

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

[:print:]відповідати всім знакам для друку та \nновим рядкам та знімати все інше

Таблицю ASCII ви можете побачити нижче. Характеристики для друку варіюються від 32 до 127, але новий рядок \nє частиною контрольних символів, які варіюються від 0 до 31, тому нам потрібно додати нову рядок до регулярного виразу/[^[:print:]\n]/u

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

Ви можете спробувати надіслати рядки через регулярний вираз із символами поза межами діапазону для друку, як-от \x7F(DEL), \x1B(Esc) тощо, і подивитися, як вони знімаються

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


Ласкаво просимо в 2047 рік, де php-mbstringза замовчуванням не пакується у 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

З недавнього виправлення до модуля розбору JSON для каналів Drupal's Feeds:

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

Якщо ви стурбовані так, він зберігає пробіли як дійсні символи.

Зробив те, що мені потрібно. Він видаляє широко поширені в даний час емоджи-символи, які не вписуються в набір символів 'utf8' MySQL, і це дало мені помилки типу "SQLSTATE [HY000]: Загальна помилка: 1366 Неправильне значення рядка".

Докладніше див. Https://www.drupal.org/node/1824506#comment-6881382


Це iconvнабагато краще, ніж засноване на старомодному регулярному вираженні preg_replace, яке в даний час застаріле.
м3нда


1
Ви абсолютно праві ereg_replace(), вибачте.
m3nda

2

Можливо, не найточніше рішення, але це завдання виконується за допомогою одного рядка коду:

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

utf8_decodeперетворить символів на знак питання;
str_replaceвикреслить знаки питання.


Спробувавши сотні рішень, єдине рішення, яке спрацювало, це ваше.
Харісінь Гохіль

1

Тож правила такі, що перший UTF-8 октлет має високий біт, встановлений як маркер, а потім 1 - 4 біти, щоб вказати, скільки додаткових октлетів; то кожен з додаткових октлетів повинен мати високі два біти, встановлені на 10.

Псевдопітон буде:

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

Ця ж логіка повинна бути перекладена на php. Однак незрозуміло, яку зачистку потрібно зробити, як тільки ви отримаєте неправильний характер.


c = (ch << 1)зробить (c & 1)нуль перший раз, пропустивши цикл. Тест, мабуть, повинен бути(c & 128)
Маркус Джардеро

1

Щоб видалити всі символи Unicode за межами базової мовної площини Unicode:

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

0

Трохи інше питання, але те, що я роблю, це використовувати HtmlEncode (рядок),

тут псевдокод

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

вхід і вихід

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

Я знаю, що це не ідеально, але робить роботу для мене.


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

це працює на нашій службі


2
Чи можете ви додати якийсь контекст, щоб пояснити, як це відповість на запитання, а не відповідь лише для коду.
Арун Вінот

-1

Як щодо iconv:

http://php.net/manual/en/function.iconv.php

Не використовував його всередині PHP, але він завжди працював добре для мене в командному рядку. Ви можете отримати його для заміни недійсних символів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.