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

文本提取URL指北(二)

上一篇指北,大致介绍了设计背景和思路,然后科普了一下正则表达式的基本常识,在这一篇中,将详细介绍整个URL提取的详细实现。包括获取Scheme和TLDs,Schemes、Domain、IP+Port….等URL模式的regex表达式拼接。

1
[协议类型]://[服务器地址]:[端口号]/[资源层级UNIX文件路径][文件名]?[查询]#[片段ID]

资源准备

IANA全称为Internet Assigned Numbers Authority, 负责分配和维护用于驱动互联网的技术标准(“协议”)的唯一代码和编号系统。

IANA的官网上可以获取到官方的Schemes和TLDs的文件。

1
2
Schemes: "https://www.iana.org/assignments/uri-schemes/uri-schemes-1.csv"
TLDs: "https://data.iana.org/TLD/tlds-alpha-by-domain.txt"

将资源get下来,并利用go的template模板库生成go文件,文件内容是string[]的常量。Scheme.go生成代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 构建template
var schemesTmpl = template.Must(template.New("schemes").Parse(`// gen by gen/schemesgen

package extraURLs

// schemes是由IANA认证的所有访问策略的有序列表
// IANA资源地址:https://www.iana.org/assignments/uri-schemes/uri-schemes-1.csv

var Schemes = []string{
{{range $scheme := .Schemes}}` + "\t`" + `{{$scheme}}` + "`" + `,
{{end}}}`))

// get资源的string切片
func getSchemes() []string {
resp, err := http.Get(source)
if err != nil {
log.Fatal(err) // pull fail -> fatal
}
defer resp.Body.Close()
reader := csv.NewReader(resp.Body)
schemes := make([]string,0)
for {
record, err := reader.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
schemes = append(schemes, record[0])
}
return schemes
}

// 按template生成go文件
func genSchemesFile(schemes []string) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
return schemesTmpl.Execute(file, struct {
Schemes []string
}{
schemes,
})
}

经过以上脚本的操作,分别get了Schemes和TLDs的资源,并生成了如下实例的go文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
package extraURLs
/ TLDs 维护了一个顶级域名的有序集合
//
// 资源地址:
// * https://data.iana.org/TLD/tlds-alpha-by-domain.txt
// * https://publicsuffix.org/list/effective_tld_names.dat
var TLDs = []string{
`aaa`,
`aarp`,
`abarth`,
`abb`
// ...... 省略
}
1
2
3
4
5
6
7
8
9
10
11
12
package extraURLs

// schemes是由IANA认证的所有访问策略的有序列表
// IANA资源地址:https://www.iana.org/assignments/uri-schemes/uri-schemes-1.csv

var Schemes = []string{
`URI Scheme`,
`aaa`,
`aaas`,
`about`
// ...... 省略
}

Domain匹配

根据Domain+TLds格式进行域名匹配,域名的规范如下:

域名命名规范

  • 只能使用英文字母(a-z,不区分大小写)、数字(0~9)以及连接符(-)。不支持使用空格及以下字符:
    !?%$等
  • 连接符(-)不能连续出现、不能单独注册,也不能放在开头和结尾。
1
2
3
4
5
6
letter := `\p{L}`
number := `\p{N}`
iriChar := letter + number
iri := [iriChar]([iriChar + \-]*[iriChar])? // len=1 [iriChar] len>1 [iriChar][iriChar]*[iriChar]
domain := (iri + \.)+
siteDomain = domain + (?i)[TLDs](?-i) // tlds可以忽略大小写

WebURL匹配

网络URL可能包含domain,IPAddress,port,pathCont。其中 domain和IP只能出现一个,port、pathCont可存在可不存在。

1
WebURL = (domain | ip):(port)?(pathCont)?

Domain我们已经拼接好了。那么考虑如何实现IPAddress和port的拼接。

ipV4规范

  • 1.0.0.0 ~ 255.255.255.255

ipV6规范

  • IPv6二进位制下为128位长度,以16位为一组,每组以冒号“:”隔开,可以分为8组,每组以4位十六进制方式表示
  • 例如:2001:0db8:86a3:08d3:1319:8a2e:0370:7344
  • 注意:IPv6IPv6在某些条件下可以省略:
    • 每项数字前导的0可以省略,省略后前导数字仍是0则继续
    • 可以用双冒号“::”表示一组0或多组连续的0,但只能出现一次
    • 如果这个地址实际上是IPv4的地址,后32位可以用10进制数表示
1
2
3
4
5
6
7
8
9
// ip地址分为 ipv4 ipv6
octet := 25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9]
ipV4 = \b + octet.octet.octet.octet + \b
ipv6 = `([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}:`
ipAddress = ipV4 + ipV6
// port匹配 ?实现可以出现也可以不出现
port = (:[0-9]*)?

WebURL = (domain | ipAddress) + port + ("/" + pathCont)?

PathCont匹配

介绍Scheme匹配之前,需要先介绍以下PathCont的匹配方式。PathCont指的是出去Schemes和WebURL之后,后边URL附带的所属内容。包括文件路径、查询和片段等。一种思路是按照这种顺序依次匹配路径、查询和片段,但是比较复杂。这里使用的参考了xurls库中的实现,默认为只要后边path中出现的字符是合法的,都是路径的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 现在我们只需要划定字符范围就可以了 path可以分为: midchar endchar. 即路径中的字符和路径末尾的字符。
endchar := letter + mark + number + currenycy + otherSymb + `/\-_+&~%=#`
midchar = endchar + `_*` + otherPuncMinusDoubleQuote
// 关于特别字符,midChar 可以是任意字符 但是endChar 只能是: `/\-_+&~%=#`
{`<http://foo.com/bar>`, `http://foo.com/bar`},
{`<http://foo.com/bar>more`, `http://foo.com/bar`},
{`.http://foo.com/bar.`, `http://foo.com/bar`},
{`.http://foo.com/bar.more`, `http://foo.com/bar.more`},
{`,http://foo.com/bar,`, `http://foo.com/bar`},
{`,http://foo.com/bar,more`, `http://foo.com/bar,more`},
{`*http://foo.com/bar*`, `http://foo.com/bar`},
{`*http://foo.com/bar*more`, `http://foo.com/bar*more`},
{`_http://foo.com/bar_`, `http://foo.com/bar_`},
{`_http://foo.com/bar_more`, `http://foo.com/bar_more`},
{`(http://foo.com/bar)`, `http://foo.com/bar`},
{`(http://foo.com/bar)more`, `http://foo.com/bar`},
{`[http://foo.com/bar]`, `http://foo.com/bar`},
{`[http://foo.com/bar]more`, `http://foo.com/bar`},
{`'http://foo.com/bar'`, `http://foo.com/bar`},
{`'http://foo.com/bar'more`, `http://foo.com/bar'more`},
{`"http://foo.com/bar"`, `http://foo.com/bar`}
// 关于括号,在Paht中也是允许cont存在的,但是必须保证成对出现。
wellParen = `\([` + midChar + `]*(\([` + midChar + `]*\)[` + midChar + `]*)*\)`
wellBrack = `\[[` + midChar + `]*(\[[` + midChar + `]*\][` + midChar + `]*)*\]`
wellBrace = `\{[` + midChar + `]*(\{[` + midChar + `]*\}[` + midChar + `]*)*\}`
wellAll = wellParen + `|` + wellBrack + `|` + wellBrace
pathCont = `([` + midChar + `]*(` + wellAll + `|[` + endChar + `])+)+`

Scheme匹配

前文已经将大致的匹配模块都讲述完了,剩下最后一个Scheme匹配。之所以放在最后将,是因为Scheme可以适用两种匹配方式。第一种,我们强制的认为,合法的URL必须是Scheme + :// + WebURL。第二种,我们可以将WebURL中的host也当做path的一部分,即WebUR+PathCont。

两种方式各有利弊,第二种肯定可以匹配更全面的URL, 但是我在业务中更强调提取出连接的可用性,所以选择了第一种,强制提取Scheme+WebURL。

1
2
// 两种Scheme 第一种后接 :// 第二种后接 :
(Scheme1+:// | Scheme2+:) + WebURL

代码实现

将拼接好的String传入regexp生成regexp结构体对象,设置匹配格式为longest()。

使用的时候,用户只需调用结构体的匹配方法。regexp::FindString regexp::FindAllString

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//StrictExp = Schemes + pathCont
func getStrictExp() string {
schemes := `((` + anyOf(Schemes...) + `|` + anyOf(SchemesUnofficial...) + `)://|` + anyOf(SchemesNoAuthority...) + `:)`
return `(?i)` + schemes + `(?-i)` + pathCont
}

// webURL = hostName + port? + pathCont
// -> hostName = siteDomain + IpAddr -> siteDomain = domain + knownTlds
// -> pathCont = “” | / | /pathCont
// strictExp = Schemes + pathCont
func getRelaxedExp() string {
punycode := `xn--[a-z0-9-]+`
knownTLDs := anyOf(append(TLDs,PseudoTLDs...)...)
siteDomain := domain + `(?i)(` + punycode + `|` + knownTLDs + `)(?-i)`
hostName := `(` + siteDomain + `|` + ipAddr + `)`
webURL := hostName + port + `(/|/` + pathCont + `)?`
email := `[a-zA-Z0-9._%\-+]+@` + siteDomain
return getStrictExp() + `|` + webURL + `|` + email
}


func Strict() *regexp.Regexp{
re := regexp.MustCompile(getStrictExp())
re.Longest()
return re
}

func Relaxed() *regexp.Regexp {
re := regexp.MustCompile(getRelaxedExp())
re.Longest()
return re
}
-------------本文结束感谢您的阅读-------------