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

Posted

技术标签:

【中文标题】php 正则表达式匹配类似于字母。又名 u=ü 或 ê=é=è=e【英文标题】:php regex match similar to letters. Aka u=ü or ê=é=è=e 【发布时间】:2017-03-19 02:43:55 【问题描述】:

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

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

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

[/编辑]

【问题讨论】:

【参考方案1】:

您不能合理地仅使用 RegExp 来做到这一点。 (你可以,但它不会是理智的!)


选项1:搜索前音译

您应该做的是音译您的 needle 和 haystack 字符串到它们的 ASCII 等价物,使用正则表达式测试它们之前。

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

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

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

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

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

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

在您的 needlehaystack 字符串上运行 replace

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


选项 2:本机 DB 函数

而且……如果您的数据在数据库中,您可能无需执行任何操作,只需使用已有的数据:http://dev.mysql.com/doc/refman/5.7/en/charset.html


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

你可以做一个给定的函数:

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

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

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

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

【讨论】:

感谢@tmslnz 提供的信息。我正在寻找像 /regex/i 这样的正则表达式的解决方案,这个 i 和 end 意味着不区分大小写。现在我们的网站有 38 种语言,而且很可能还会添加更多。这也是为什么我不仅仅考虑法语字符的原因。但是我们的搜索总是只运行一种语言。所以我可以为每种语言提供不同的替代品。 (现在大部分我都不懂,所以我不能做任何有用的事情。) 这只是大小写(Aa)。 特殊字符是完全不同的野兽。对此没有仅适用于 RegExp 的灵丹妙药。您需要先准备您的字符串,然后进行正则表达式匹配。 你看过我链接的图书馆吗?我没有测试它,但看起来写得很好。此外,如果此内容在数据库中,我建议您查看数据库的功能,寻找可以按照您的意愿进行搜索的内容。我怀疑你可能在错误的树上吠叫:) 是的,“对不起”我认为这是相同的 à vs a 与 A vs a 相同,因为我将数据库从 utf8_bin 更改为 utf8_unicode_ci。然后当我搜索 fête 时,MariaDB (MySQL) 自动开始匹配 fêté。 (我对此很满意。)。然后我(错误地)认为 PHP 也可以做到这一点。 (所以我对代码的工作方式很满意。) 我看了一点图书馆,但对我来说它看起来很复杂而且很大。我不是网络开发人员,而只是系统管理员,他抱怨网站代码(由无偿志愿者编写)并得到了我可以修复的答案。 (而且我也不是真正的 PHP 开发人员。)好吧,代码已经不是那么快了,所以我不喜欢添加镇流器,而且,我不喜欢做一些我不使用的语言破坏函数的事情不明白。 -- 我现在正在考虑是否可以将依赖于语言的 e 替换为 [eéèê] 并通过正则表达式运行它。【参考方案2】:

不幸的是,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://***.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);

基本上:

    规范化:将 needle 和 haystack 转换为正常的 ASCII 字符。 查找位置:在归一化的大海捞针中搜索归一化针的位置。 插入:将开始和结束标记相应地插入到原始字符串中。 重复:有时您可能会出现多次。重复此过程,直到不再发生。

示例输出:

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!

【讨论】:

【参考方案3】:

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

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

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

print $nfc;

为搜索字符串生成正则表达式很简单。分解搜索字符串,去掉所有组合标记,在每个字符后插入\pM*

$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('/(.)\pM*/su', function ($match) 
    return preg_quote($match[1]) . '\pM*';
, $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 以外的重音拉丁字符,这些字符通常用于东欧语言。它甚至适用于非拉丁文字,例如希腊变音符号。

【讨论】:

不错,不知道。 iconv() 就这么多……【参考方案4】:

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

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

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

现代数据库知道这些排序规则。例如,如果使用 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 为零,表示字符串相等。这就是你想要的。

如果您想以这种方式在字符串中搜索匹配的子字符串,则需要使用显式子字符串匹配来进行。正则表达式技术不提供这一点。

这是一个执行搜索和注释的函数(例如添加&lt;span&gt; 标签)。它充分利用了 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

【讨论】:

以上是关于php 正则表达式匹配类似于字母。又名 u=ü 或 ê=é=è=e的主要内容,如果未能解决你的问题,请参考以下文章

正则表达式(待补充)

求一个PHP正则表达式匹配(字母、数字、汉字、下划线)

Python 深入正则表达式

php 正则表达式 只能包含字母和数字

php正则表达式基本

php中文正则匹配