文本提取URL指北(一) | Enplee's blog
0%

文本提取URL指北(一)

项目背景

在字节实习的时候遇到了一个需求,简单来说就是支持用户上传音乐链接,并对链接做风险检查。但是考虑到用户可能上传的是文本中蕴含着链接,常用分享的朋友可能经常会见到这种格式的连接分享:

1
9微xR52XUVGuBR嘻 https://m.tb.cn/h.4HiWOMs?sm=c079e2  【淘饱店】Xraypad Aqua Control+ 纯黑纯白鼠标垫AC+粗面多尺寸

当时的状况是,公司内部有团队提供了连接风险检测的接口(实际是结合google的API), 但是只支持纯连接风险检测,不支持以上这种分享格式。同时,前端的同学也希望后端能够帮助提取连接。于是,当时就参考了组内之前使用python提取网易云链接的思路和一些开源的库,使用Go中的regex正则框架实现了一个文本中提取链接的工具库。

设计思路

本身项目设计的初衷,是乐观的认为用户分享的文本格式不会出现极端的案例,而是属于正常的分享行为。所以,并没有对一些特别极端的案例进行覆盖。首先介绍以下整体的设计思路。

wiki上关于URI的定义如下图所示,变化非常多:

URI语法图

但是一般用户上传的URL,一般呈现出以下格式:

格式一:[Scheme] + [Host] + [PathCont]
1
http://www.example.com/index.html
格式二:[Domain] + [Top Level Domain] + [PathCont] (web URL 格式)
1
www.example.com/index.html
格式三:[hostName] + [Port] + [PathCont]
1
192.168.178.111:9090/index.html

对于URL匹配,我想最关键的是找到起始点和终止点,那么基于以上三种格式,分别设计了不同的正则表达式来进行匹配。

首先对于格式一特征的URL,这种格式的链接最显著的特点就是Scheme,而且Scheme的构成是固定的,我们可以首先将对所有Scheme集合进行匹配,匹配成功,那么就找到了URL的起点。下面的网址是国际上维护的Scheme列表:

1
https://www.iana.org/assignments/uri-schemes/uri-schemes-1.csv

对于格式二特征的URL,这种域名格式链接,低等级域名并不固定,但是我们发现顶级域名的注册是固定的,所以还是老方法,先匹配出顶级域名的位置,再根据响应的正则前后进行匹配,具体细节见后文。这两处维护了最新的顶级域名列表(TLDs):

1
2
"https://data.iana.org/TLD/tlds-alpha-by-domain.txt"
"https://publicsuffix.org/list/effective_tld_names.dat"

对于格式三特征的URL,hostName有可能是格式二的域名形式,也可能是IP地址,但是无论是以上哪种格式,都是可以圈定范围的。比如IPv4地址的范围和IPv6的地址范围:

1
2
`(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])`
`([0-9a-fA-F]{1,4}:([0-9a-fA-F]{1,4}:([0-9a-fA-F]{1,4}:([0-9a-fA-F]{1,4}:([0-9a-fA-F]{1,4}:[0-9a-fA-F]{0,4}|:[0-9a-fA-F]{1,4})?|(:[0-9a-fA-F]{1,4}){0,2})|(:[0-9a-fA-F]{1,4}){0,3})|(:[0-9a-fA-F]{1,4}){0,4})|:(:[0-9a-fA-F]{1,4}){0,5})((:[0-9a-fA-F]{1,4}){2}|:(25[0-5]|(2[0-4]|1[0-9]|[1-9])?[0-9])(\.(25[0-5]|(2[0-4]|1[0-9]|[1-9])?[0-9])){3})|(([0-9a-fA-F]{1,4}:){1,6}|:):[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){7}:`

对于文本URL提取,我们就从以上这三个角度分别做匹配。但是正如我之前所述,这种匹配方式并不能保证完全的匹配,但是在实现的功能完全符合业务的需求。

正则表达式基础

正则表达式描述了一种字符串匹配模式,通过这种模式来检验字符串中是否含有对应模式的子串,子串替换,取出子串等操作。

https://www.regular-expressions.info/

普通字符

1
2
3
4
5
6
7
8
9
10
[ABCD]:允许匹配[]中所有的字符
[^ABC]:^表示取反,除了[^……],都可以匹配
[A-Z]: 区间表示,允许匹配ABC...Z所有字符
[\s\S]: 匹配所有字符,\s表示空白字符,\S表示非空白字符
+++++++++++基于Unicode特性的匹配方案+++++++++++++++
\p{L}: 匹配属于字母的单个字符 letter
\p{M}: 匹配à之类组合标记的码点 mark
\p{N}: 匹配数字字符
\p{Sc}:Currency_Symbol 任何货币符号
\P{So}:Other_Symbol 不是数学符号、货币符号、组合字符的各种符号

特殊字符

1
2
3
4
5
6
(): 标记子表达式的起始位置 \进行转义
*: 匹配前面的子表达式零次或多次。要匹配 * 字符,请使用 \*
+: 匹配前面的子表达式一次或多次。要匹配 + 字符,请使用 \+
.: 匹配除换行符 \n 之外的任何单字符。要匹配 . ,请使用 \.
?: 匹配前面的子表达式零次或一次,或指明一个非贪婪限定符
|: 指明两项之间的一个选择。

限定符

1
2
3
4
5
6
7
限定符用来指定一个组件满足要求的匹配次数
*: 任意次数
+: 一次或者多次
?: 一次或者零次
{n}: 匹配n次
{n,}: 至少匹配n次
{n,m}: 匹配n-m次

定位符

1
2
3
4
^: 匹配输入字符串开始的位置。
$: 匹配输入字符串结尾的位置
\b: 匹配一个单词边界,即字与空格间的位置。
\B: 非单词边界匹配。
-------------本文结束感谢您的阅读-------------