Как обрабатывать ввод пользователем недопустимых символов UTF-8?

Я ищу общую стратегию / рекомендации относительно того, как обрабатывать недопустимый вход UTF-8 от пользователей.

Хотя мой webapp использует UTF-8, некоторые пользователи вводят недопустимые символы. Это вызывает ошибки в json_encode () PHP, и в целом кажется, что это плохая идея.

W3C I18N Часто задаваемые вопросы: Многоязычные формы говорят: «Если получены данные, отличные от UTF-8, сообщение об ошибке должно быть отправлено обратно».

  • Как именно это должно быть сделано практически на всем сайте с десятками разных мест, где можно вводить данные?
  • Как вы представляете ошибку полезной для пользователя?
  • Как временно хранить и отображать данные плохой формы, чтобы пользователь не потерял весь свой текст? Разбить плохие персонажи? Использовать заменяющий символ и как?
  • Для существующих данных в базе данных, когда обнаружены недопустимые данные UTF-8, я должен попытаться преобразовать его и сохранить его обратно (как? Utf8_encode ()? Mb_convert_encoding () ?) Или оставить как-есть в базе данных, но что-то сделать (что?) перед json_encode ()?

EDIT: Я очень хорошо знаком с расширением mbstring и не спрашиваю: «Как работает UTF-8 в PHP». Я бы хотел получить совет от людей с опытом в реальных ситуациях, как они справились с этим.

EDIT2: Как часть решения, мне бы очень хотелось увидеть быстрый метод преобразования недопустимых символов в U + FFFD

Атрибут accept-charset="UTF-8" является лишь ориентиром для браузеров, которым они следуют, они не вынуждены сообщать, что таким образом, дрянные формы представления ботов являются хорошим примером …

То, что я обычно делаю, это игнорировать неправильные символы, либо с помощью iconv() либо с менее надежными utf8_encode() / utf8_decode() , если вы используете iconv вас также есть возможность транслитерировать плохие символы.

Вот пример использования iconv() :

 $str_ignore = iconv('UTF-8', 'UTF-8//IGNORE', $str); $str_translit = iconv('UTF-8', 'UTF-8//TRANSLIT', $str); 

Если вы хотите отобразить сообщение об ошибке своим пользователям, я бы сделал это глобально, вместо того, чтобы получить полученное значение, что-то вроде этого, вероятно, будет очень хорошо:

 function utf8_clean($str) { return iconv('UTF-8', 'UTF-8//IGNORE', $str); } $clean_GET = array_map('utf8_clean', $_GET); if (serialize($_GET) != serialize($clean_GET)) { $_GET = $clean_GET; $error_msg = 'Your data is not valid UTF-8 and has been stripped.'; } // $_GET is clean! 

Вы также можете нормализовать новые строки и полосы (не) видимые контрольные символы, например:

 function Clean($string, $control = true) { $string = iconv('UTF-8', 'UTF-8//IGNORE', $string); if ($control === true) { return preg_replace('~\p{C}+~u', '', $string); } return preg_replace(array('~\r\n?~', '~[^\P{C}\t\n]+~u'), array("\n", ''), $string); } 

Код для конвертации из UTF-8 в Unicode:

 function Codepoint($char) { $result = null; $codepoint = unpack('N', iconv('UTF-8', 'UCS-4BE', $char)); if (is_array($codepoint) && array_key_exists(1, $codepoint)) { $result = sprintf('U+%04X', $codepoint[1]); } return $result; } echo Codepoint('à'); // U+00E0 echo Codepoint('ひ'); // U+3072 

Вероятно, быстрее, чем любая другая альтернатива, они не тестировали ее широко.


Пример:

 $string = 'hello world '; // U+FFFEhello worldU+FFFD echo preg_replace_callback('/[\p{So}\p{Cf}\p{Co}\p{Cs}\p{Cn}]/u', 'Bad_Codepoint', $string); function Bad_Codepoint($string) { $result = array(); foreach ((array) $string as $char) { $codepoint = unpack('N', iconv('UTF-8', 'UCS-4BE', $char)); if (is_array($codepoint) && array_key_exists(1, $codepoint)) { $result[] = sprintf('U+%04X', $codepoint[1]); } } return implode('', $result); } 

Это то, что ты искал?

Получение недопустимых символов из вашего веб-приложения может иметь отношение к наборам символов, принятым для форм HTML. Вы можете указать, какой набор символов использовать для форм с атрибутом accept-charset :

 <form action="..." accept-charset="UTF-8"> 

Вы также можете взглянуть на подобные вопросы в StackOverflow для указателей на то, как обрабатывать недопустимые символы, например, в столбце справа, но я думаю, что сигнализация ошибки пользователю лучше, чем попытка очистить те, которые недействительны символов, которые вызывают неожиданную потерю значительных данных или неожиданное изменение входов пользователя.

Я собрал довольно простой класс, чтобы проверить, является ли вход в UTF-8 и выполняется через utf8_encode() мере необходимости:

 class utf8 { /** * @param array $data * @param int $options * @return array */ public static function encode(array $data) { foreach ($data as $key=>$val) { if (is_array($val)) { $data[$key] = self::encode($val, $options); } else { if (false === self::check($val)) { $data[$key] = utf8_encode($val); } } } return $data; } /** * Regular expression to test a string is UTF8 encoded * * RFC3629 * * @param string $string The string to be tested * @return bool * * @link http://www.w3.org/International/questions/qa-forms-utf-8.en.php */ public static function check($string) { return preg_match('%^(?: [\x09\x0A\x0D\x20-\x7E] # ASCII | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3 | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15 | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16 )*$%xs', $string); } } // For example $data = utf8::encode($_POST); 

Существует многобитное расширение для PHP, проверьте его: http://www.php.net/manual/en/book.mbstring.php

Вы должны попробовать функцию mb_check_encoding () .

Удачи!

За полноту этого вопроса (не обязательно лучший ответ) …

 function as_utf8($s) { return mb_convert_encoding($s, "UTF-8", mb_detect_encoding($s)); } 

Я рекомендую просто не разрешать загрузку мусора. Не полагайтесь на пользовательские функции, которые могут заглушить вашу систему. Просто пройдите представленные данные против созданного вами алфавита. Создайте приемлемую строку алфавита и пройдите представленные данные байтом по байтам, как если бы это был массив. Вставьте допустимые символы в новую строку и опустите неприемлемые символы. Данные, которые вы храните в своей базе данных, тогда являются данными, инициируемыми пользователем, но не фактически предоставленными пользователем данными.

РЕДАКТИРОВАТЬ № 4: Заменить плохой символ с помощью entiy:

EDIT # 3: Обновлено: 22 сентября 2010 г., 13:32 Причина: Теперь возвращена строка UTF-8, плюс я использовал тестовый файл, который вы предоставили как доказательство.

 <?php // build alphabet // optionally you can remove characters from this array $alpha[]= chr(0); // null $alpha[]= chr(9); // tab $alpha[]= chr(10); // new line $alpha[]= chr(11); // tab $alpha[]= chr(13); // carriage return for ($i = 32; $i <= 126; $i++) { $alpha[]= chr($i); } /* remove comment to check ascii ordinals */ // /* // foreach ($alpha as $key=>$val){ // print ord($val); // print '<br/>'; // } // print '<hr/>'; //*/ // // //test case #1 // // $str = 'afsjdfhasjhdgljhasdlfy42we875y342q8957y2wkjrgSAHKDJgfcv kzXnxbnSXbcv '.chr(160).chr(127).chr(126); // // $string = teststr($alpha,$str); // print $string; // print '<hr/>'; // // //test case #2 // // $str = ''.'©?™???'; // $string = teststr($alpha,$str); // print $string; // print '<hr/>'; // // $str = '©'; // $string = teststr($alpha,$str); // print $string; // print '<hr/>'; $file = 'http://www.cl.cam.ac.uk/~mgk25/ucs/examples/UTF-8-test.txt'; $testfile = implode(chr(10),file($file)); $string = teststr($alpha,$testfile); print $string; print '<hr/>'; function teststr(&$alpha, &$str){ $strlen = strlen($str); $newstr = chr(0); //null $x = 0; if($strlen >= 2){ for ($i = 0; $i < $strlen; $i++) { $x++; if(in_array($str[$i],$alpha)){ // passed $newstr .= $str[$i]; }else{ // failed print 'Found out of scope character. (ASCII: '.ord($str[$i]).')'; print '<br/>'; $newstr .= '&#65533;'; } } }elseif($strlen <= 0){ // failed to qualify for test print 'Non-existent.'; }elseif($strlen === 1){ $x++; if(in_array($str,$alpha)){ // passed $newstr = $str; }else{ // failed print 'Total character failed to qualify.'; $newstr = '&#65533;'; } }else{ print 'Non-existent (scope).'; } if(mb_detect_encoding($newstr, "UTF-8") == "UTF-8"){ // skip }else{ $newstr = utf8_encode($newstr); } // test encoding: if(mb_detect_encoding($newstr, "UTF-8")=="UTF-8"){ print 'UTF-8 :D<br/>'; }else{ print 'ENCODED: '.mb_detect_encoding($newstr, "UTF-8").'<br/>'; } return $newstr.' (scope: '.$x.', '.$strlen.')'; } 

Как насчет удаления всех символов вне вашего подмножества. По крайней мере, в некоторых частях моего приложения я не разрешаю использовать символы вне [aZ] [0-9 наборов], например, имена пользователей. Вы можете создать функцию фильтра, которая беззвучно удаляет все символы вне этого диапазона или возвращает ошибку, если она обнаруживает их и подталкивает решение к пользователю.

Попробуйте сделать то, что делает Rails, чтобы заставить все браузеры всегда публиковать данные UTF-8:

 <form accept-charset="UTF-8" action="#{action}" method="post"><div style="margin:0;padding:0;display:inline"> <input name="utf8" type="hidden" value="&#x2713;" /> </div> <!-- form fields --> </form> 

См. Railssnowman.info или исходный патч для объяснения.

  1. Чтобы браузер отправил данные отправки формы в кодировку UTF-8, просто отрисуйте страницу с заголовком Content-Type «text / html; charset = utf-8» (или используйте meta http-equiv tag).
  2. Чтобы браузер отправил данные о представлении формы в кодировке UTF-8, даже если пользователь возится со страничным кодированием (браузеры позволяют пользователям делать это), используйте accept-charset="UTF-8" в форме.
  3. Чтобы браузер отправил данные отправки формы в кодировку UTF-8, даже если пользователь возится со страничным кодированием (браузеры позволяют пользователям делать это), и даже если браузер является IE, и пользователь переключил кодировку страницы на корейский и введите корейские символы в поля формы, добавьте скрытый ввод в форму со значением, таким как &#x2713; который может быть только из кодировки Unicode (и в этом примере не корейской кодировки).