PHP 使用 preg_match 或 str_replace 匹配最佳同义词
支付小周
结论
把每组同义词中的第一个词或短语作为匹配词,按长度从长到短排列并组成一个正则表达式,再用一次 preg_replace_callback() 完成所有替换。
需要注意两点:
be aware of必须排在be aware、aware of和aware前面。- 整个句子只能扫描一次。不能逐条调用
str_replace()或preg_replace(),否则后续规则还会处理新生成的{...}内容。
实现代码
<?php
$synonyms = [
'a company|an organization|a business',
'auto|automobile|car|vehicle',
'aware|conscious|mindful|informed|knowledgeable',
'aware of|conscious of|mindful of',
'be aware|bear in mind|remember',
'be aware of|concentrate on|pay attention to|know about|be familiar with',
'carefully|cautiously|meticulously|very carefully|thoroughly|properly',
'cases|instances|circumstances|situations|scenarios|conditions',
'comes|arrives|will come|happens',
'company|business|organization|firm|corporation|provider',
'coverage|protection',
'in most|in many',
'in most cases|generally|usually|normally|typically|most often',
'in the|within the|inside the|while in the|from the|during the',
'increases|raises|will increase|boosts|improves',
'information|info|data|details|facts|information and facts',
'insurance|insurance coverage|insurance policy|insurance plan|insurance policies',
'on the|around the|within the|to the|about the|over the',
'once|as soon as|when|after|the moment|at the time',
'once you|when you|as soon as you|after you|when you finally',
'or a|or perhaps a|or possibly a|or even a|or maybe a|or simply a',
'package|package deal|bundle|deal|offer',
'payment|fee|cost',
'policy|coverage|plan',
'premium|top quality|high quality',
'question|query|concern|issue|problem',
'rates|prices|charges|premiums|costs|fees',
'receive|get|obtain|acquire|be given',
'receive a|get a',
'representative|consultant',
'review|evaluation|assessment|critique|overview|evaluate',
'the new|the brand new|the newest|the modern',
'time to|time for you to',
'with your|together with your|along with your|with the',
"you have|you've|you've got|you might have|you may have|you have got",
];
$sentence = "When it comes time to renew your auto insurance policy, be aware of how your carrier handles renewals. In most cases, you receive a renewal package in the mail with your new policy and rates. Review this information carefully, and question any increases in premium with your agent or a company representative. In most cases, once you submit a payment on the new policy, you have renewed your coverage.";
/*
* 生成规则表:
*
* [
* 'be aware of' => [
* 'be aware of',
* 'concentrate on',
* ...
* ],
* ]
*/
$rules = [];
foreach ($synonyms as $row) {
$terms = array_map('trim', explode('|', $row));
$terms = array_values(array_filter(
$terms,
static fn(string $term): bool => $term !== ''
));
if ($terms === []) {
continue;
}
$search = strtolower($terms[0]);
$rules[$search] = $terms;
}
/*
* 期望输出中没有把 “a company” 作为整体替换,
* 所以必须明确禁用这条规则。
*
* 如果希望得到 {a company|an organization|a business},
* 删除下面这一行即可。
*/
unset($rules['a company']);
/*
* 较长短语必须出现在正则表达式前面。
* PCRE 的分支匹配在同一个起始位置按从左到右选择。
*/
$searches = array_keys($rules);
usort(
$searches,
static fn(string $a, string $b): int =>
strlen($b) <=> strlen($a)
);
/*
* preg_quote() 防止搜索短语中的正则特殊字符被误解析。
* 把普通空格改为 \s+,允许短语内部出现一个或多个空白字符。
*/
$alternatives = array_map(
static function (string $search): string {
$quoted = preg_quote($search, '~');
return str_replace(' ', '\s+', $quoted);
},
$searches
);
/*
* 自定义单词边界比 \b 更明确:
* 匹配内容前后不能直接连接字母、数字或下划线。
*
* 这样 “once” 不会错误匹配 “concern” 中间的字符。
*/
$pattern = '~(?<![\p{L}\p{N}_])(?:'
. implode('|', $alternatives)
. ')(?![\p{L}\p{N}_])~iu';
$output = preg_replace_callback(
$pattern,
static function (array $match) use ($rules): string {
$matchedText = $match[0];
/*
* 正则允许 \s+,所以查表前要把连续空白标准化。
*/
$key = strtolower(
preg_replace('/\s+/u', ' ', trim($matchedText))
);
$replacements = $rules[$key];
/*
* 原短语以大写字母开头时,将所有候选项的首字母大写。
* 例如:
*
* In most cases
* =>
* {In most cases|Generally|Usually|Normally|Typically|Most often}
*/
if (preg_match('/^\p{Lu}/u', $matchedText) === 1) {
$replacements = array_map(
static fn(string $term): string => ucfirst($term),
$replacements
);
}
return '{' . implode('|', $replacements) . '}';
},
$sentence
);
if ($output === null) {
throw new RuntimeException(
'Regular expression failed: ' . preg_last_error_msg()
);
}
echo $output;
为什么不会发生嵌套替换
preg_replace_callback() 只用一个正则表达式扫描原始 $sentence。回调返回的内容会直接写入结果,不会再次交给同一个正则表达式匹配。
例如:
insurance
替换为:
{insurance|insurance coverage|insurance policy|insurance plan|insurance policies}
其中的 coverage、policy 等内容不会再被处理,因此不会出现下面这样的嵌套结果:
insurance {coverage|protection}
逐条替换则会重复处理已经插入的内容:
foreach ($rules as $search => $replacement) {
$output = str_replace($search, $replacement, $output);
}
即使提前按长度排序,后续循环仍会扫描并替换前面循环插入的同义词。
最长短语为何能优先匹配
排序后,正则表达式中的相关部分相当于:
be aware of|be aware|aware of|aware
扫描到 be aware of 时,PCRE 会先尝试第一个分支,所以完整短语会被整体替换为:
{be aware of|concentrate on|pay attention to|know about|be familiar with}
同样:
in most cases
会优先于:
in most
cases
而:
receive a
会优先于:
receive
这种排序解决的是匹配起始位置相同时的候选冲突。正则表达式仍会从句子中最靠前的位置开始匹配。例如,在 on the new 中,正则会先从 on 开始匹配 on the。匹配完成后只剩 new,无法再从中间的 the 匹配 the new。
规则冲突需要明确处理
现有规则包含:
a company|an organization|a business
但期望结果是:
a {company|business|organization|firm|corporation|provider}
两者存在冲突。只要启用 a company 规则,按照最长短语优先的原则,结果就应该是:
{a company|an organization|a business} representative
如果必须得到期望结果,就要像示例代码那样禁用这条规则:
unset($rules['a company']);
实际项目中,更稳妥的做法是在数据结构中为规则增加 enabled、priority 或上下文条件,不能只依靠字符串长度。长度排序可以处理短语重叠,却无法决定业务上应该启用哪组同义词。
备注:内容仅供参考。
