php正则表达式匹配字母。 Aka u =ü或ê=é=è= e

时间:2016-11-05 09:30:31

标签: php regex unicode collation diacritics

我正在努力搜索文本中的特定单词并突出显示它们。代码工作得很好,除了我希望它也匹配相似的字母。 我的意思是,搜索fête应该匹配fêté,fete,...

有一个简单的&优雅的方式来做到这一点?

这是我目前的代码:

$regex='/(' . preg_replace('/\s+/', '|', preg_quote($usersearchstring)) .')/iu';

$higlightedtext = preg_replace($regex, '<span class="marked-search-text">\0</span>', $text);

我的文字不是html编码的。在MariaDB中搜索匹配相似的结果。

[编辑] 这里有一个较长的问题示例:

$usersearchstring='fête';
$text='la paix fêtée avec plus de 40 cultures';
$regex='/(' . preg_replace('/\s+/', '|', preg_quote($usersearchstring)) .')/iu';
$higlightedtext = preg_replace($regex, '<span class="marked-search-text">\0</span>', $text);

结果是$ higlightedtext与$ text

相同

当更改$ higlightedtext时,单词“fêté”然后$ higlightedtext是

'la paix <span class="marked-search-text">fêté</span>e avec plus de 40 cultures'

然而,我希望它能够“始终”匹配字母的所有变体,因为可能存在(并且实际上)可能的单词的许多变体。 而且我们在数据库中有很多可能的食物,甚至可能还有盛宴。

我一直在考虑这个,但我看到的唯一解决方案是拥有一个包含所有字母替换选项的巨大数组,然后循环遍历它们并尝试每个变体。但这并不优雅而且会很慢。(因为对于很多字母我至少有5种变体:aáàâä,如果这个单词有3个元音,我需要做75x(5x5x5)preg_replace。

[/编辑]

4 个答案:

答案 0 :(得分:4)

您的问题是整理,处理自然语言文字的艺术,并使用有关语言的知识对其进行比较。词汇规则。您正在寻找不区分大小写且对变音符号不敏感的排序规则。

常见的整理规则是 B A 之后。一个不太常见的规则,但对您的问题很重要, êe是等效的。排序规则包含许多这样的规则,经过多年的仔细研究。如果您使用不区分大小写的排序规则,则希望 aA等规则等效。

在大多数欧洲语言中,但不是西班牙语的变音规则是: ÑN相同。在西班牙语中, Ñ来自N

现代数据库知道这些排序规则。例如,如果您使用MySQL,则可以设置一个字符编码为utf8mb4且排序规则为utf8mb4_unicode_ci的列。这将适用于大多数语言(但不适合西班牙语)。

正则表达式技术对整理工作不是很有用。如果您使用正则表达式,那么您正在尝试重新发明轮子,而您可能会重新发明轮胎。

与大多数现代编程语言一样,PHP包含内置于其Collator class的归类支持。这是一个使用Collat​​or对象作为重音字符用例的简单示例。它使用Collator::PRIMARY collation strength来执行不区分大小写和重音的比较。

mb_internal_encoding("UTF-8");
$collator  = collator_create('fr_FR');
$collator->setStrength(Collator::PRIMARY);
$str1 = mb_convert_encoding('fêté', 'UTF-8');
$str2 = mb_convert_encoding('fete', 'UTF-8');
$result = $collator->compare($str1, $str2);
echo $result;

$result此处为零,表示字符串相等。这就是你想要的。

如果要在字符串中搜索匹配的子字符串,则需要使用显式子字符串匹配。正则表达式技术并没有提供这一点。

这是一个执行搜索和注释的功能(例如,添加<span>标签)。它充分利用了Collat​​or类的字符平等方案。

function annotate_ci ($haystack, $needle, $prefix, $suffix, $locale="FR-fr") {

    $restoreEncoding = mb_internal_encoding();
    mb_internal_encoding("UTF-8");
    $len = mb_strlen($needle);
    if ( mb_strlen( $haystack ) < $len ) {
        mb_internal_encoding($restoreEncoding);
        return $haystack;
    }
    $collator = collator_create( $locale );
    $collator->setStrength( Collator::PRIMARY );

    $result = "";
    $remain = $haystack;
    while ( mb_strlen( $remain ) >= $len ) {
        $matchStr = mb_substr($remain, 0, $len);
        $match = $collator->compare( $needle, $matchStr );
        if ( $match == 0 ) {
            /* add the matched $needle string to the result, with annotations.
             * take the matched string from $remain
             */
            $result .= $prefix . $matchStr . $suffix;
            $remain = mb_substr( $remain, $len );
        } else {
            /* add one char to $result, take one from $remain */
            $result .= mb_substr( $remain, 0, 1 );
            $remain = mb_substr( $remain, 1 );
        }
    }
    $result .= $remain;
    mb_internal_encoding($restoreEncoding);
    return $result;
}

以下是使用该功能的一个例子。

$needle = 'Fete';  /* no diacriticals here! mixed case! */
$haystack= mb_convert_encoding('la paix fêtée avec plus de 40 cultures', 'UTF-8');

$result = annotate_ci($haystack, $needle, 
                      '<span class="marked-search-text">' , '</span>');

它回馈

 la paix <span class="marked-search-text">fêté</span>e avec plus de 40 cultures

答案 1 :(得分:3)

您不能合理仅使用RegExp执行此操作。 (你可以,但它不会是理智的!)

选项1:搜索前的音译

你应该做的是 音译 你的针和干草堆字符串到它们的ASCII等价物之前用正则表达式测试它们。

所以 1)暂时将字符串转换为ASCII和 2)正则表达式匹配。

有些人已经完成了音译问题的工作,你可以利用它:见https://github.com/nicolas-grekas/Patchwork-UTF8/blob/master/src/Patchwork/Utf8.php

或者,如果您只想要法语输入,则可以手动构建特殊字符及其ASCII等效字符的映射。据我所知,法语只需要考虑几个元音和ç

准备好替换映射后,只需通过replace所有特殊字符及其ASCII等效字符运行字符串,然后就可以对“普通”字符串进行正则表达式搜索。

根据您的表现问题,我不会担心。对于以下各项:

à : a
â : a
è : e
é : e
ê : e
ë : e
î : i
ï : i
ô : o
ù : u
ü : u
û : u
ç : c

指针 haystack 字符串上运行replace

在这13次迭代之后,您将获得两个纯ASCII字符串进行测试。

选项2:原生数据库功能

并且...... 如果您的数据位于数据库中,则可能不需要执行任何操作,只需使用已存在的数据:http://dev.mysql.com/doc/refman/5.7/en/charset.html

选项3:动态生成的搜索模式

您可以创建一个给定的函数:

  • 相应字符的地图,如上图和
  • 要找的话

生成正则表达式模式,其中包含具有有效替换项的每个字符的匹配字符集。

在这种情况下,如果您搜索féte,您的函数会创建一个像/(f[eéèêë]t[eéèêë])/iu这样的正则表达式模式,然后您可以使用它来查找文本。

唯一耗时的部分是为所有语言创建好的角色地图......

答案 2 :(得分:3)

一种简单的方法是convert the input text to Unicode Normalization Form D执行规范分解,将重音字符分成基本字符,然后组合标记。然后可以使用PCRE的Unicode功能轻松匹配基本字符和标记的序列。将标记can be matched\p{M}组合在一起。然后,将文本转换回NFC。 fetee的示例:

$string = "la paix fêtée avec plus de 40 cultures";

$nfd = Normalizer::normalize($string, Normalizer::FORM_D);
$highlighted = preg_replace('/f\p{M}*e\p{M}*t\p{M}*e\p{M}*e\p{M}*/iu',
                            '<b>\0</b>', $nfd);
$nfc = Normalizer::normalize($highlighted, Normalizer::FORM_C);

print $nfc;

为搜索字符串生成正则表达式非常简单。分解搜索字符串,删除所有组合标记,并在每个字符后插入\p{M}*

$string = "la paix fêtée avec plus de 40 cultures";
$keyword = "fêtée";

# Create regex.
$nfd = Normalizer::normalize($keyword, Normalizer::FORM_D);
$regex = preg_replace_callback('/(.)\p{M}*/su', function ($match) {
    return preg_quote($match[1]) . '\p{M}*';
}, $nfd);

# Highlight.
$nfd = Normalizer::normalize($string, Normalizer::FORM_D);
$highlighted = preg_replace('/' . $regex . '/iu', '<b>\0</b>', $nfd);
$nfc = Normalizer::normalize($highlighted, Normalizer::FORM_C);

此解决方案并不依赖于硬编码字符表,而是使用ISO-8859-1以外的重音拉丁字符,这些字符通常用于东欧语言。它甚至适用于非拉丁文字,例如希腊语变音符号。

答案 3 :(得分:1)

遗憾的是,php regex(我所知道的)中没有神奇的角色类或技巧可以解决这个问题。我反而选择了另一条路线:

$search = '+  fête   foret   ca rentrée w0w !!!';
$text = 'La paix fêtée avec plus de 40 cultures dans une forêt. Ça commence bien devant la rentrée...<br> Il répond: w0w tros cool!!! En + il fait chaud!';
$left_token = '<b>';
$right_token = '</b>';
$encoding = 'UTF-8';

// Let's normalize both search and needle
$search_normalized = normalize($search);
$text_normalized = normalize($text);

// Fixed preg_quote() and match UTF whitespaces
$search_needles = preg_split('/\s+/u', $search_normalized);

// We'll save the output in a separate variable
$text_output = $text;

// Since we made the tokens a variable, we'll need to calculate the offsets
$offset_size = strlen($left_token . $right_token);

// Start searching
foreach($search_needles as $needle) {
    // Reset for each word
    $search_offset = 0;

    // We may have several occurences
    while(true) {
        if($search_offset > mb_strlen($text_normalized)) { // No more needles
            break;
        } else {
            $pos = mb_stripos($text_normalized, $needle, $search_offset, $encoding);
        }

        if($pos === false) { // No more needles here
            break;
        }
        $len = mb_strlen($needle);

        // Insert tokens
        $text_output = mb_substr($text_output, 0, $pos, $encoding) . // Left side
                       $left_token . 
                       mb_substr($text_output, $pos, $len, $encoding) . // The enclosed word
                       $right_token .
                       mb_substr($text_output, $pos + $len, NULL, $encoding); // Right side

        // We need to update this too otherwise the positions won't be the same
        $text_normalized = mb_substr($text_normalized, 0, $pos, $encoding) . // Left side
                       $left_token . 
                       mb_substr($text_normalized, $pos, $len, $encoding) . // The enclosed word
                       $right_token .
                       mb_substr($text_normalized, $pos + $len, NULL, $encoding); // Right side

        // Advance in the search
        $search_offset = $pos + $len + $offset_size;
    }
}

echo($text_output);
var_dump($text_output);

// Credits: http://stackoverflow.com/a/10064701
function normalize($input) {
    $normalizeChars = array(
        'Š'=>'S', 'š'=>'s', 'Ð'=>'Dj','Ž'=>'Z', 'ž'=>'z', 'À'=>'A', 'Á'=>'A', 'Â'=>'A', 'Ã'=>'A', 'Ä'=>'A',
        'Å'=>'A', 'Æ'=>'A', 'Ç'=>'C', 'È'=>'E', 'É'=>'E', 'Ê'=>'E', 'Ë'=>'E', 'Ì'=>'I', 'Í'=>'I', 'Î'=>'I',
        'Ï'=>'I', 'Ñ'=>'N', 'Ń'=>'N', 'Ò'=>'O', 'Ó'=>'O', 'Ô'=>'O', 'Õ'=>'O', 'Ö'=>'O', 'Ø'=>'O', 'Ù'=>'U', 'Ú'=>'U',
        'Û'=>'U', 'Ü'=>'U', 'Ý'=>'Y', 'Þ'=>'B', 'ß'=>'Ss','à'=>'a', 'á'=>'a', 'â'=>'a', 'ã'=>'a', 'ä'=>'a',
        'å'=>'a', 'æ'=>'a', 'ç'=>'c', 'è'=>'e', 'é'=>'e', 'ê'=>'e', 'ë'=>'e', 'ì'=>'i', 'í'=>'i', 'î'=>'i',
        'ï'=>'i', 'ð'=>'o', 'ñ'=>'n', 'ń'=>'n', 'ò'=>'o', 'ó'=>'o', 'ô'=>'o', 'õ'=>'o', 'ö'=>'o', 'ø'=>'o', 'ù'=>'u',
        'ú'=>'u', 'û'=>'u', 'ü'=>'u', 'ý'=>'y', 'ý'=>'y', 'þ'=>'b', 'ÿ'=>'y', 'ƒ'=>'f',
        'ă'=>'a', 'î'=>'i', 'â'=>'a', 'ș'=>'s', 'ț'=>'t', 'Ă'=>'A', 'Î'=>'I', 'Â'=>'A', 'Ș'=>'S', 'Ț'=>'T',
    );
    return strtr($input, $normalizeChars);
}

基本上:

  1. 标准化:将needle和haystack转换为普通的ASCII字符。
  2. 查找位置:在规范化的大海捞针中搜索标准化针的位置。
  3. 插入:相应地将开始和结束标记插入原始字符串。
  4. 重复:有时您可能会多次出现。重复此过程,直到不再发生。
  5. 示例输出:

    La paix <b>fêté</b>e avec plus de 40 cultures dans une <b>forêt</b>. <b>Ça</b> commence bien devant la <b>rentrée</b>...<br> Il répond: <b>w0w</b> tros cool<b>!!!</b> En <b>+</b> il fait chaud!