php 类似于字母的正则表达式匹配。又名 u=ü 或 ê=é=è=e

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">[=11=]</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">[=12=]</span>', $text);

结果是 $higlightedtext 与 $text

相同

当更改 $higlightedtext 单词 "fêté" 时 $higlightedtext 是

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

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

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

[/edit]

您不能合理地仅使用 RegExp 执行此操作。 (你可以,但它不正常!)


选项 1:搜索前音译

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

因此 1) 暂时将您的字符串转换为 ASCII 和 2) 正则表达式匹配。

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

或者,如果您只需要法语输入,您可以手动构建一个 map 特殊字符及其 ASCII 等价物。据我所知法语只需要考虑几个元音和 ç.

一旦您准备好替换映射,只需 运行 您的字符串通过一个函数 replace 将所有特殊字符及其 ASCII 等值,然后您可以在 "plain" 字符串.

根据您对性能的担忧,我不会担心。对于每个:

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

运行 replace 在你的 needlehaystack 字符串上。

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


选项 2:本机数据库函数

还有……如果您的数据在数据库中,您可能不需要做任何事情,只需使用已经存在的数据即可:http://dev.mysql.com/doc/refman/5.7/en/charset.html


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

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

  • a map 对应的字符,如上面的和
  • 找一个词

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

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

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

不幸的是,在 php 正则表达式(据我所知)中没有魔术字符 class 或技巧可以开箱即用地解决这个问题。我选择了另一条路线:

$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: 
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. 重复: 有时您可能会出现多次。重复此过程,直到不再出现为止。

示例输出:

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!

一个简单的方法是 convert the input text to Unicode Normalization Form D which performs a Canonical Decomposition, splitting accented characters into a base character followed by combining marks. Sequences of base characters and marks can then be matched easily using PCREs Unicode features. Combining marks 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>[=10=]</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>[=11=]</b>', $nfd);
$nfc = Normalizer::normalize($highlighted, Normalizer::FORM_C);

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

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

一个常见的排序规则是 BA 之后。一个不太常见但对您的问题很重要的规则是 êe 是等价的。 归类包含很多这样的规则,经过多年精心制定。如果您使用的是不区分大小写的排序规则,则需要像 aA 这样的规则是等效的。

在大多数欧洲语言(但不是西班牙语)中适用的变音规则是:ÑN 是等价的。 在西班牙语中,ÑN 之后。

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

Regex 技术对于校对工作不是很有用。如果您为此使用正则表达式,那么您就是在尝试重新发明轮子,而您很可能会重新发明漏气的轮胎。

PHP,与大多数现代编程语言一样,包含归类支持,内置于其 Collator class. Here's a simple example of the use of a Collator object for your accented-character use case. It uses the 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 class 的字符平等方案。

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