我正在研究一种在文本中搜索特定单词并突出显示它们的方法。代码工作完美,除了我希望它也匹配类似的字母。我的意思是,搜索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]
你的问题是关于排序,处理自然语言文本的艺术,并使用有关语言词汇规则的知识进行比较。您正在寻找不区分大小写和不区分变音符标记的排序。
常见的排序规则是 一个变音符规则在大多数欧洲语言中都是正确的,但西班牙语除外: 现代数据库知道这些排序。例如,如果你使用MySQL,你可以设置一个字符编码为 Regex技术对于排序工作不是很有用。如果你在这方面使用正则表达式,你是在试图重新发明轮子,而你很可能会重新发明轮胎。B
位于A
之后。一个不太常见的规则,但对你的问题很重要,是 ê
和e
是等效的。排序包含许多这样的规则,经过多年的仔细研究。如果您使用不区分大小写的排序,您希望像 a
和A
这样的规则是等效的。Ñ
和N
是等效的。在西班牙语中, Ñ
排在N
之后。utf8mb4
,排序规则为utf8mb4_unicode_ci
的列。这对大多数语言来说都很有效(但对西班牙语来说并不完美)。
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);
}
基本上:
- Normalize:将needle和haystack转换为正常的ASCII字符。
- 查找位置:查找归一化针在归一化干草堆中的位置
- Insert:在原始字符串中插入相应的开始和结束标记。
- 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!