文本中链接的智能识别与处理

在处理用户输入的长文本字段时,经常需要将其中的URL地址转换为HTML中的"A"元素,即实现所谓的"链接化"。本文将介绍选择的处理方式,以及为何主要忽略了URL的正式规范,而专注于文本中寻找URL的其他因素。

使用正则表达式,还是不使用?

如果需要在文本中找到URL,可能会在网上搜索现有的解决方案。会发现许多文章和示例展示了如何使用正则表达式模式来找到URL。找到的解决方案似乎都不适合应用程序。

发现的解决方案更关注URL的正式规范,而不是在用户输入的文本中找到URL的实际问题。多位作者描述了一个基本问题:URL是复杂的,很难在纯文本中检测到它们,因为决定URL中可以和不可以包含哪些字符的规则,往往与最终用户在文本中输入它们的方式不一致。

有效的URL可能包含广泛的字符,对于URL的域、路径、查询和片段部分有特定的字符规则。因此,匹配URL的正则表达式是长且复杂的。找到的模式从几十个字符到2000个字符不等,而且没有一个声称能够准确匹配任何给定文本中的所有有效URL

一些示例模式排除了作者想要忽略的URL,例如"http://localhost"等。移除或修改这些排除项需要逆向工程这些模式,虽然可能不得不在将模式包含在应用程序之前这样做,但并不期待这样做!

发现的模式的更重要的问题是未能准确检测URL的结尾。例如,URL可能包括括号,这在维基百科上很常见,这证明是具有挑战性的,因为人们经常将URL包裹在括号中,即"(http://www.example.com)"。发现的模式将尾随的)包含在URL中。

在需要处理的文本中,结尾标点经常跟随URL,像这样:"Is it http://example.com?"大多数标点符号在URL中是有效的,包括常见的句子结尾标点,如问号、逗号、句号和感叹号,但当这些标点符号后面跟着空格或文本结束时,标点符号通常不是URL的一部分。发现的模式没有考虑到这些情况。

与其使用一个不适合要求的复杂正则表达式模式,或者尝试修改这些模式以满足要求,选择了一种不同的方法。

完美是好的敌人

对于方法,决定专注于URL候选项周围的文本,并定义了一组相对简单的规则:

  • 所有URL必须以http://、https://或ftp://开头,并且
  • URL必须被分隔符对包围,例如(和)
  • URL必须以常见的结束标点符号结束,或者
  • URL必须以空白字符结束。

理解这些规则不会检测到所有URL(假阴性),并且会检测到无效的URL(假阳性)。对于应用程序,可以容忍这些失败。可以指导用户如何调整他们的文本以避免有问题的结构问题。

理解代码

ReplaceUrls()是一个字符串扩展,它接受一个委托方法。它找到了URL,但留给委托来提供替换文本。通常,委托会将URL包装在HTML中以链接化它。根据其自身的逻辑,委托可能会选择忽略URL,通过返回URL文本不变。

ReplaceUrls()使用一个简单的正则表达式来找到URL的开始,并且根据URL是否被分隔符对包围来使用两种方法之一来找到每个URL的结尾。

GetUrlDelimiter()方法检查文本以确定URL是否被分隔符对包围。有效的分隔符是(和)、[和]、«和»以及单引号或双引号。如果URL以分隔符对的初始字符开始,ReplaceUrls()使用FindEndOfDelimitedUrl()来找到匹配的字符。否则,ReplaceUrls()使用FindEndOfUrl()来找到结尾。

使用代码

以下是使用ReplaceUrls()的示例:

string text = text.ReplaceUrls(LinkifyUrl);

其中LinkifyUrl是一个方法,用于将找到的URL转换为HTML链接:

public string LinkifyUrl(string url) { return String.Format("", url); }

代码实现

using System.Text; using System.Text.RegularExpressions; namespace JohnCardinal.Html { public delegate string UrlEvaluator(string url); internal static class UrlExtensions { private const char kLeftPointingDoubleAngle = '\u00AB'; private const char kRightPointingDoubleAngle = '\u00BB'; private const char kNoDelimiter = '\0'; private static Regex UrlPrefix = new Regex("(https?|ftp)://", RegexOptions.IgnoreCase | RegexOptions.Compiled); public static string ReplaceUrls(this string text, UrlEvaluator evaluator) { var matches = UrlPrefix.Matches(text); if (matches.Count == 0) { return text; } int copied = 0; var sb = new StringBuilder(); foreach (Match match in matches) { if (match.Index > copied) { sb.Append(text, copied, match.Index - copied); } var delimiter = GetUrlDelimiter(text, match); var end = (delimiter == kNoDelimiter) ? FindEndOfUrl(text, match) : FindEndOfDelimitedUrl(text, match, delimiter); var url = text.Substring(match.Index, end - match.Index + 1); if (url.Length > match.Length) { sb.Append(evaluator(url)); } else { sb.Append(url); } copied = end + 1; } if (text.Length > copied) { sb.Append(text, copied, text.Length - copied); } return sb.ToString(); } private static int FindEndOfUrl(string text, Match match) { const string kWhitespace = "\r\n\t"; var index = match.Index; while (index < text.Length) { switch (text[index]) { case '\'': case '\r': case '\n': case '\t': // whitespace ends the URL return index - 1; case '.': case ',': case '!': case '?': case ':': case ';': // common punctuation followed by whitespace // ends the URL if (index < text.Length - 1) { if (kWhitespace.IndexOf(text[index + 1]) != -1) { return index - 1; } } // common punctuation at the end of the text // ends the URL else if (index == text.Length - 1) { return index - 1; } break; } index++; } return index - 1; } private static int FindEndOfDelimitedUrl(string text, Match match, char delimiter) { var nested = 1; var index = match.Index; while (index < text.Length) { switch (text[index]) { case '\'': case '\r': case '\n': case '\t': // whitespace ends the URL return index - 1; case '\"': if (delimiter == '\"') { return index - 1; } break; case '\'': if (delimiter == '\'') { return index - 1; } break; case '(': if (delimiter == '(') nested++; break; case ')': if (delimiter == '(') { nested--; if (nested == 0) { return index - 1; } } break; case kRightPointingDoubleAngle: if (delimiter == kLeftPointingDoubleAngle) { return index - 1; } break; case ']': if (delimiter == '[') { return index - 1; } break; } index++; } return index - 1; } private static char GetUrlDelimiter(string text, Match match) { const string kDelimiters = "\"'([\u00AB"; if (match.Index > 0) { var index = match.Index - 1; if (kDelimiters.IndexOf(text[index]) != -1) { return text[index]; } } return kNoDelimiter; } } }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485