Многобайтовая обрезка в PHP?

Видимо нет mb_trim в mb_* семья, поэтому я пытаюсь реализовать один для себя.

Я недавно нашел это регулярное выражение в комментарии в http://php.net/:

/(^\s+)|(\s+$)/u

Итак, я бы реализовал это следующим образом:

function multibyte_trim($str)
{
    if (!function_exists("mb_trim") || !extension_loaded("mbstring")) {
        return preg_replace("/(^\s+)|(\s+$)/u", "", $str);
    } else {
        return mb_trim($str);
    }
}

Регулярное выражение кажется мне верным, но я очень недоволен регулярными выражениями. Будет ли это эффективно удалять любое пространство Юникода в начале / конце строки?

7 ответов

Решение

Стандарт trim Функция обрезает несколько пробелов и пробелов. Они определены как символы ASCII, что означает определенные конкретные байты из 0 в 0100 0000,

Правильный ввод UTF-8 никогда не будет содержать многобайтовые символы, состоящие из байтов 0xxx xxxx, Все байты в правильных многобайтовых символах UTF-8 начинаются с 1xxx xxxx,

Это означает, что в правильной последовательности UTF-8 байты 0xxx xxxx может ссылаться только на однобайтовые символы. РНР trim Поэтому функция никогда не обрезает "половину символа", если у вас есть правильная последовательность UTF-8. (Будьте очень осторожны с неправильными последовательностями UTF-8.)


\s в ASCII регулярные выражения будут в основном соответствовать тем же символам, что и trim,

preg функции с /u модификатор работает только с регулярными выражениями в кодировке UTF-8, и /\s/u соответствуют также UTF8. Такое поведение с неразрывными пробелами является единственным преимуществом его использования.

Если вы хотите заменить пробелы в других, не ASCII-совместимых кодировках, ни один из методов не будет работать.

Другими словами, если вы пытаетесь обрезать обычные пробелы ASCII-совместимой строкой, просто используйте trim, Когда используешь /\s/u будьте осторожны со значением nbsp для вашего текста.


Береги себя:

  $s1 = html_entity_decode(" Hello   "); // the NBSP
  $s2 = "  exotic test ホ  ";

  echo "\nCORRECT trim: [". trim($s1) ."], [".  trim($s2) ."]";
  echo "\nSAME: [". trim($s1) ."] == [". preg_replace('/^\s+|\s+$/','',$s1) ."]";
  echo "\nBUT: [". trim($s1) ."] != [". preg_replace('/^\s+|\s+$/u','',$s1) ."]";

  echo "\n!INCORRECT trim: [". trim($s2,' ') ."]"; // DANGER! not UTF8 safe!
  echo "\nSAFE ONLY WITH preg: [". 
       preg_replace('/^[\s]+|[\s]+$/u', '', $s2) ."]";

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

function mb_trim($str) {
  return preg_replace("/(^\s+)|(\s+$)/us", "", $str); 
}

Эта версия поддерживает второй необязательный параметр $charlist:

function mb_trim ($string, $charlist = null) 
{   
    if (is_null($charlist)) {
        return trim ($string);
    } else {
        $charlist = str_replace ('/', '\/', preg_quote ($charlist));
        return preg_replace ("/(^[$charlist]+)|([$charlist]+$)/us", '', $string);
    }
}

Хотя не поддерживает ".." для диапазонов.

Итак, я взял решение @edson-medina, исправил ошибку и добавил несколько юнит-тестов. Вот 3 функции, которые мы используем, чтобы дать аналогам mb параметры trim, rtrim и ltrim.

////////////////////////////////////////////////////////////////////////////////////
//Add some multibyte core functions not in PHP
////////////////////////////////////////////////////////////////////////////////////
function mb_trim($string, $charlist = null) {
    if (is_null($charlist)) {
        return trim($string);
    } else {
        $charlist = preg_quote($charlist, '/');
        return preg_replace("/(^[$charlist]+)|([$charlist]+$)/us", '', $string);
    }
}
function mb_rtrim($string, $charlist = null) {
    if (is_null($charlist)) {
        return rtrim($string);
    } else {
        $charlist = preg_quote($charlist, '/');
        return preg_replace("/([$charlist]+$)/us", '', $string);
    }
}
function mb_ltrim($string, $charlist = null) {
    if (is_null($charlist)) {
        return ltrim($string);
    } else {
        $charlist = preg_quote($charlist, '/');
        return preg_replace("/(^[$charlist]+)/us", '', $string);
    }
}
////////////////////////////////////////////////////////////////////////////////////

Вот модульные тесты, которые я написал для всех, кто интересуется:

public function test_trim() {
    $this->assertEquals(trim(' foo '), mb_trim(' foo '));
    $this->assertEquals(trim(' foo ', ' o'), mb_trim(' foo ', ' o'));
    $this->assertEquals('foo', mb_trim(' Åfooホ ', ' Åホ'));
}

public function test_rtrim() {
    $this->assertEquals(rtrim(' foo '), mb_rtrim(' foo '));
    $this->assertEquals(rtrim(' foo ', ' o'), mb_rtrim(' foo ', ' o'));
    $this->assertEquals('foo', mb_rtrim('fooホ ', ' ホ'));
}

public function test_ltrim() {
    $this->assertEquals(ltrim(' foo '), mb_ltrim(' foo '));
    $this->assertEquals(ltrim(' foo ', ' o'), mb_ltrim(' foo ', ' o'));
    $this->assertEquals('foo', mb_ltrim(' Åfoo', ' Å'));
}

Вы также можете обрезать не совместимые с ASCII пробелы (например, неразрывный пробел) в строках UTF-8 с помощью preg_replace('/^\p{Z}+|\p{Z}+$/u','',$str);

\s будет совпадать только с "ascii-совместимым" пробелом даже с u модификатор
но \p{Z} будет соответствовать всем известным символам пространства Юникод

(Портировано с дубликата Q на trimборется с NBSP.) Следующие примечания действительны для PHP 7.2+. Пробег может отличаться от более ранних версий (просьба сообщать в комментариях).

PHP trimигнорирует неразрывные пробелы. Он только обрезает пробелы в основном диапазоне ASCII. Для справки, исходный код обрезки выглядит следующим образом (т.е. без недокументированных функций с обрезкой):

(c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '\v' || c == '\0')

Из вышеперечисленного, помимо обычного пробела (32 ASCII, ), это все управляющие символы ASCII; LF (10:\n), CR (13: \r), HT (9: \t), VT (11: \v), NUL (0: \0). (Обратите внимание, что в PHP вы должны заключать в двойные кавычки экранированные символы:"\n", "\t" и т. д. В противном случае они обрабатываются как буквальные \n так далее.)

Ниже приведены простые реализации трех разновидностей trim (ltrim, rtrim, trim), с помощью preg_replace, которые работают со строками Unicode:

preg_replace('~^\s+~u', '', $string) // == ltrim
preg_replace('~\s+$~u', '', $string) // == rtrim
preg_replace('~^\s+|\s+$~us', '', $string) // == trim

Не стесняйтесь завернуть их в свои собственные mb_*trim функции.

Согласно спецификации PCRE,\s символ escape-последовательности "любой пробел" с u Включенный режим Unicode будет соответствовать всем следующим пробелам:

The horizontal space characters are:

U+0009     Horizontal tab (HT)
U+0020     Space
U+00A0     Non-break space
U+1680     Ogham space mark
U+180E     Mongolian vowel separator
U+2000     En quad
U+2001     Em quad
U+2002     En space
U+2003     Em space
U+2004     Three-per-em space
U+2005     Four-per-em space
U+2006     Six-per-em space
U+2007     Figure space
U+2008     Punctuation space
U+2009     Thin space
U+200A     Hair space
U+202F     Narrow no-break space
U+205F     Medium mathematical space
U+3000     Ideographic space

The vertical space characters are:

U+000A     Linefeed (LF)
U+000B     Vertical tab (VT)
U+000C     Form feed (FF)
U+000D     Carriage return (CR)
U+0085     Next line (NEL)
U+2028     Line separator
U+2029     Paragraph separator

Вы можете увидеть тест итерацию изpreg_replace с uФлаг Unicode, устраняющий все перечисленные пробелы. Все они обрезаны, как и ожидалось, в соответствии со спецификацией PCRE. Если вы настроили таргетинг только на горизонтальные пространства выше,\h будет соответствовать им, как \v бы со всеми вертикальными пробелами.

Использование \p{Z}замеченный в некоторых ответах, не удастся по некоторым пунктам; в частности, с большинством пробелов ASCII и, что шокирует, также с монгольским разделителем гласных. Хубилай-хан пришел бы в ярость. Вот список промахов с\p{Z}: U+0009 Горизонтальная табуляция (HT), U+000A Перевод строки (LF), U+000C Перенос страницы (FF), U+000D Возврат каретки (CR), U+0085 Следующая строка (NEL) и U+180E Монгольский разделитель гласных.

Что касается того, почему это происходит, в приведенной выше спецификации PCRE также отмечается: "\s любой символ, который соответствует \p{Z} или \h или \v". То есть, \s это надмножество \p{Z}. Затем просто используйте\s на месте \p{Z}. Он более всеобъемлющий, и импорт более очевиден для тех, кто читает ваш код, кто может не помнить краткости для всех типов символов.

mb_ereg_replace, кажется, обходит это:

function mb_trim($str,$regex = "(^\s+)|(\s+$)/us") {
    return mb_ereg_replace($regex, "", $str);
}

... но я не знаю достаточно о регулярных выражениях, чтобы знать, как бы вы добавили параметр "charlist", который люди ожидают, что смогут передавать в trim() - то есть список символов для усечения - так что просто сделал регулярное выражение параметром.

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

Мои два цента

Фактическое решение вашего вопроса состоит в том, что вы должны сначала выполнить проверки кодировки, прежде чем работать над изменением внешних входных строк. Многие из них быстро узнают о "санации и проверке" входных данных, но не спешат освоить шаг определения базовой природы (кодировки символов) строк, с которыми они работают на ранних этапах.

Сколько байтов будет использовано для представления каждого символа? При правильно отформатированном UTF-8 это может быть 1 (символы trim имеет дело с), 2, 3 или 4 байта. Проблема возникает, когда в игру вступают устаревшие или искаженные представления UTF-8 - границы байтовых символов могут не совпадать, как ожидалось (говорит дилетант).

В PHP некоторые отстаивают необходимость принудительного соответствия всех строк правильной кодировке UTF-8 (1, 2, 3 или 4 байта на символ), где такие функции, как trim() будет по-прежнему работать, потому что граница байта / символа для символов, с которыми он работает, будет конгруэнтной для значений расширенного ASCII / 1-байта, которые trim() стремится устранить начало и конец строки ( страница справочника по отделке).

Однако, поскольку компьютерное программирование является разнообразной областью, невозможно иметь общий подход, который работает во всех сценариях. С учетом вышесказанного, напишите ваше приложение так, как оно должно быть для правильной работы. Просто делаете базовый веб-сайт, управляемый базой данных, с вводом формы? Да, за мои деньги заставь все быть UTF-8.

Примечание. У вас по-прежнему будут проблемы с интернационализацией, даже если проблема с UTF-8 остается стабильной. Зачем? Многие неанглийские наборы символов существуют в 2, 3 или 4-байтовом пространстве (кодовые точки и т. Д.). Очевидно, что если вы используете компьютер, который должен работать со скриптами на китайском, японском, русском, арабском или иврите, вы хотите, чтобы все работало с 2, 3 и 4 байтами! Помните, PHP trim Функция может обрезать символы по умолчанию или заданные пользователем. Это важно, особенно если вам нужна ваша trim чтобы объяснить некоторые китайские иероглифы.

Я бы предпочел иметь дело с проблемой невозможности доступа к моему сайту, а затем с проблемой доступа и ответов, которые не должны появляться. Когда вы думаете об этом, это соответствует принципам наименьших привилегий (безопасность) и универсального дизайна (доступность).

Резюме

Если входные данные не будут соответствовать надлежащей кодировке UTF-8, вы можете вызвать исключение. Вы можете попытаться использовать многобайтовые функции PHP для определения вашей кодировки или другую многобайтовую библиотеку. Если и когда PHP написан для полной поддержки Unicode (Perl, Java ...), PHP будет лучше для него. Усилия по созданию Unicode в PHP прекратились несколько лет назад, поэтому вы вынуждены использовать дополнительные библиотеки для правильной работы с многобайтовыми строками UTF-8. Просто добавив /u флаг для preg_replace() не смотрит на общую картину.

Обновить:

При этом, я считаю, что следующая многобайтовая обрезка будет полезна для тех, кто пытается извлечь ресурсы REST из компонента пути URL-адреса (естественно, за исключением строки запроса. Примечание: это будет полезно после очистки и проверки строки пути).

function mb_path_trim($path)
{
    return preg_replace("/^(?:\/)|(?:\/)$/u", "", $path);
}
Другие вопросы по тегам