我想详细说明一个涵盖以下场景的正则表达式:
搜索的词是"土豆"。
如果用户搜索"土豆",它匹配(他打字很快,"o"指比"t"指快。(完成)
如果用户搜索"ptato"(他忘记了一个字母),它就会匹配。(完成)
以我对正则表达式的了解,我可以走得更远:
(?=[potato]{5,6})p?o?t?a?t?o?
这样做的问题在于它匹配了相反的单词,如"otatop",这有点聪明,但有点奇怪,以及"ooooo",这是完全不可取的。所以我不描述我不想要的东西。
我不希望重复的字母与"ooooo","ooopp"等匹配(无法)
顺便说一下,我使用的是 C#。
不要使用正则表达式。
最好的解决方案是简单的解决方案。只有 11 个可能的不精确匹配,因此只需枚举它们:
List<string> inexactMatches = new List<string> {
"otato", "ptato", "poato", "potto", "potao", "potat",
"optato", "ptoato", "poatto", "pottao", "potaot"};
...
bool hasInexactMatch = inexactMatches.Contains(whatever);
输入这些内容花了不到一分钟的时间;使用简单的特定解决方案,而不是尝试做一些疯狂的正则表达式,这将花费您数小时才能找到和调试。
如果你要坚持使用正则表达式,这里有一个有效的:
otato|ptato|poato|potto|potao|potat|optato|ptoato|poatto|pottao|potaot
再次:越简单越好。
现在,人们可能会认为您想用比"土豆"更多的词来解决这个问题。 在这种情况下,你可能已经这么说了 - 但无论如何,我们可以提出一些简单的解决方案。
首先,让我们枚举目标字符串中省略一个字母的所有字符串。 字符串是IEnumerable<char>
所以让我们解决一般问题:
static IEnumerable<T> OmitAt<T>(this IEnumerable<T> items, int i) =>
items.Take(i).Concat(items.Skip(i + 1));
枚举序列两次有点粗暴,但我不会强调它。 现在让我们为字符串创建一个特定的版本:
static IEnumerable<string> Omits(this string s) =>
Enumerable
.Range(0, s.Length)
.Select(i => new string(s.OmitAt(i).ToArray()));
伟大。现在我们可以说"frob".Omits()
并回到rob, fob, frb, fro
.
现在让我们进行掉期。再次,先解决一般问题:
static void Swap<T>(ref T x, ref T y)
{
T t = x;
x = y;
y = t;
}
static T[] SwapAt<T>(this IEnumerable<T> items, int i)
{
T[] newItems = items.ToArray();
Swap(ref newItems[i], ref newItems[i + 1]);
return newItems;
}
现在我们可以为字符串求解它:
static IEnumerable<string> Swaps(this string s) =>
Enumerable
.Range(0, s.Length - 1)
.Select(i => new string(s.SwapAt(i)));
现在我们完成了:
string source = "potato";
string target = whatever;
bool match =
source.Swaps().Contains(target) ||
source.Omits().Contains(target);
简单易行。使用简单、直接、正确的算法解决一般问题,这些算法可以组合成更大的解决方案。我的算法都没有超过三行,很容易看出它们是正确的。
这里选择的武器是相似性(或距离)匹配算法。 比较相似性算法可以很好地概述最常见的距离指标/算法。
问题是没有单一的最佳指标。选择取决于输入类型、精度要求、速度、资源可用性等。然而,比较算法可能会很混乱。
两个最常建议的指标是Levenshtein距离和Jaro-Winkler:
-
Levenshtein 距离提供了两个字符串之间的相似性度量,可以说比其他一些指标更不宽容,并且更直观地理解。(Levenshtein 距离有修改版本,例如 Damerau-Levenshtein 距离,其中包括换位,可能更适合您的用例。
-
有人声称Jaro-Winkler提供了两个字符串之间的相似性度量,允许字符换位在一定程度上调整常见前缀的权重,距离是"目前可用的最高性能和最准确的近似字符串匹配算法之一[Cohen等人],[Winkler]。然而,选择仍然在很大程度上取决于用例,人们不能从特定的研究中得出一般结论,例如名称匹配Cohen等人,2003年。
你可以在 NuGet 上找到大量程序包,这些程序包为你提供各种相似性算法(a、b、c)、模糊匹配、语音等,以将此功能添加到你的网站或应用。
模糊匹配也可以直接在数据库层上使用。Levenshtein距离的实现可以在大多数数据库系统(例如MySQL,SQL Server)中找到,或者已经内置(Oracle,PostgreSQL)。
根据您的确切用例,您还可以使用基于云的解决方案(即使用基于 AWS、Azure 等的微服务或自己的微服务)来获得类似自动建议的模糊搜索/匹配。
这样做最容易
:static void Main(string[] args)
{
string correctWord = "Potato";
string incorrectSwappedWord = "potaot";
string incorrectOneLetter = "ptato";
// Returns true
bool swapped = SwappedLettersMatch(correctWord, incorrectSwappedWord);
// Returns true
bool oneLetter = OneLetterOffMatch(correctWord, incorrectOneLetter);
}
public static bool OneLetterOffMatch(string str, string input)
{
int ndx = 0;
str = str.ToLower();
input = input.ToLower();
if (string.IsNullOrWhiteSpace(str) || string.IsNullOrWhiteSpace(input))
{
return false;
}
while (ndx < str.Length)
{
string newStr = str.Remove(ndx, 1);
if (input == newStr)
{
return true;
}
ndx++;
}
return false;
}
public static bool SwappedLettersMatch(string str, string input)
{
if (string.IsNullOrWhiteSpace(str) || string.IsNullOrWhiteSpace(input))
{
return false;
}
if (str.Length != input.Length)
{
return false;
}
str = str.ToLower();
input = input.ToLower();
int ndx = 0;
while (ndx < str.Length)
{
if (ndx == str.Length - 1)
{
return false;
}
string newStr = str[ndx + 1].ToString() + str[ndx];
if (ndx > 0)
{
newStr = str.Substring(0, ndx) + newStr;
}
if (str.Length > ndx + 2)
{
newStr = newStr + str.Substring(ndx + 2);
}
if (newStr == input)
{
return true;
}
ndx++;
}
return false;
}
OneLetterOffMatch
会返回 true 是有一个匹配只缺少一个字符。 当只有两个字母被交换时,SwappedLettersMatch
将返回 true。这些函数不区分大小写,但如果您想要区分大小写的版本,只需删除对.ToLower()
的调用。