PHP正则表达式匹配类似于字母.又名u = & # 252;或& # 234;= & # 233;= & # 232;=


php regex match similar to letters. Aka u=ü or ê=é=è=e

我正在研究一种在文本中搜索特定单词并突出显示它们的方法。代码工作完美,除了我希望它也匹配类似的字母。我的意思是,搜索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);

结果是$ highlightedtext与$text

相同

当$ highlightedtext更改为"fêté"时,$ highlightedtext为

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

然而,我希望它"总是"匹配所有字母的变体,因为可能有(并且实际上是)许多可能的单词变体。我们在数据库中有fête fêté,甚至可能有fete。

我一直在思考这个问题,但我看到的唯一解决方案是有一个巨大的数组与所有字母替换选项,然后循环它们,并尝试每一个变化。但这并不优雅,而且会很慢。(因为对于许多字母,我至少有5个变体:aáàâä,因此,如果单词有3个元音,我需要执行75倍(5x5x5)的preg_replace.

[/edit]

你的问题是关于排序,处理自然语言文本的艺术,并使用有关语言词汇规则的知识进行比较。您正在寻找不区分大小写和不区分变音符标记的排序。

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

一个变音符规则在大多数欧洲语言中都是正确的,但西班牙语除外: ÑN是等效的。在西班牙语中, Ñ排在N之后。

现代数据库知道这些排序。例如,如果你使用MySQL,你可以设置一个字符编码为utf8mb4,排序规则为utf8mb4_unicode_ci的列。这对大多数语言来说都很有效(但对西班牙语来说并不完美)。

Regex技术对于排序工作不是很有用。如果你在这方面使用正则表达式,你是在试图重新发明轮子,而你很可能会重新发明轮胎。

像大多数现代编程语言一样,PHP包含内置在Collator类中的排序支持。下面是为重音字符用例使用Collator对象的简单示例。它使用Collator::PRIMARY排序规则强度来执行不区分大小写和重音的比较。

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为零,表示两个字符串相等。这就是你想要的。

如果你想用这种方式在字符串中搜索匹配的子字符串,你需要使用显式的子字符串匹配。Regex技术不提供这种功能。

下面是执行搜索和注释(例如,添加<span>标记)的函数。它充分利用了Collator类的字符相等方案。

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

一种简单的方法是将输入文本转换为Unicode Normalization Form D,它执行规范分解,将重音字符拆分为一个基本字符,然后组合标记。然后可以使用pcre Unicode特性轻松匹配基本字符和标记序列。组合标记可与'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以外的重音拉丁字符。它甚至可以处理非拉丁字母,例如希腊变音符号。

您不能合理地仅使用RegExp来执行此操作。


选项1:在搜索前音译

你应该做的是你的needle和haystack字符串音译为它们的ASCII等效,在用正则表达式测试它们之前

所以1)暂时将字符串转换为ASCII和2) Regex匹配。

一些人已经在音译问题上做了一些工作,你可以利用:参见https://github.com/nicolas-grekas/Patchwork-UTF8/blob/master/src/Patchwork/Utf8.php

或者,如果您只希望使用法语输入,您可以手动构建一个包含特殊字符及其等效ASCII字符的映射。就我所知的法语而言,只需要考虑几个元音和ç

一旦您准备好了替换映射,只需通过一个函数运行您的字符串,该函数replace将所有特殊字符与其ASCII对应,然后您可以在"普通"字符串上进行Regex搜索。

根据你的表现,我不会担心。

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

针头干草堆字符串运行replace

在这13次迭代之后,您得到要测试的两个纯ASCII字符串。


选项2:本地DB函数

和…如果你的数据在数据库中,你可能不需要做任何事情,但使用已经存在的东西:http://dev.mysql.com/doc/refman/5.7/en/charset.html


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

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

  • a 映射对应的字符,如上面的字符和
  • 查找

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

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

唯一耗时的部分是为所有语言创建良好的字符映射…

不幸的是,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. Normalize:将needle和haystack转换为正常的ASCII字符。
  2. 查找位置:查找归一化针在归一化干草堆中的位置
  3. Insert:在原始字符串中插入相应的开始和结束标记。
  4. Repeat:有时您可能会出现多次错误。这个过程会重复,直到没有出现。
样本输出:

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!