2.4 RSS 匹配器
最后要看的一部分代码是 RSS 匹配器的实现代码。我们之前看到的代码搭建了一个框架,以便能够实现不同的匹配器来搜索内容。RSS 匹配器的结构与默认匹配器的结构很类似。每个匹配器为了匹配接口, Search
方法的实现都不同,因此匹配器之间无法互相替换。
代码清单 2-47 中的 RSS 文档是一个例子。当我们访问数据源列表里 RSS 数据源的链接时,期望获得的数据就和这个例子类似。
代码清单 2-47 期望的 RSS 数据源文档
<rss xmlns:npr="http://www.npr.org/rss/" xmlns:nprml="http://api"
<channel>
<title>News</title>
<link>...</link>
<description>...</description>
<language>en</language>
<copyright>Copyright 2014 NPR - For Personal Use
<image>...</image>
<item>
<title>
Putin Says He'll Respect Ukraine Vote But U.S.
</title>
<description>
The White House and State Department have called on the
</description>
如果用浏览器打开代码清单 2-47 中的任意一个链接,就能看到期望的 RSS 文档的完整内容。RSS 匹配器的实现会下载这些 RSS 文档,使用搜索项来搜索标题和描述域,并将结果发送给 results
通道。让我们先看看 rss.go 代码文件的前 12 行代码,如代码清单 2-48 所示。
代码清单 2-48 matchers/rss.go:第 01 行到第 12 行
01 package matchers
02
03 import (
04 "encoding/xml"
05 "errors"
06 "fmt"
07 "log"
08 "net/http"
09 "regexp"
10
11 "github.com/goinaction/code/chapter2/sample/search"
12 )
和其他代码文件一样,第 1 行定义了包名。这个代码文件处于名叫 matchers
的文件夹中,所以包名也叫 matchers
。之后,我们从标准库中导入了 6 个库,还导入了 search
包。再一次,我们看到有些标准库的包是从标准库所在的子文件夹导入的,如 xml
和 http
。就像 json
包一样,路径里最后一个文件夹的名字代表包的名字。
为了让程序可以使用文档里的数据,解码 RSS 文档的时候需要用到 4 个结构类型,如代码清单 2-49 所示。
代码清单 2-49 matchers/rss.go:第 14 行到第 58 行
14 type (
15 // item 根据 item 字段的标签,将定义的字段
16 // 与 rss 文档的字段关联起来
17 item struct {
18 XMLName xml.Name `xml:"item"`
19 PubDate string `xml:"pubDate"`
20 Title string `xml:"title"`
21 Description string `xml:"description"`
22 Link string `xml:"link"`
23 GUID string `xml:"guid"`
24 GeoRssPoint string `xml:"georss:point"`
25 }
26
27 // image 根据 image 字段的标签,将定义的字段
28 // 与 rss 文档的字段关联起来
29 image struct {
30 XMLName xml.Name `xml:"image"`
31 URL string `xml:"url"`
32 Title string `xml:"title"`
33 Link string `xml:"link"`
34 }
35
36 // channel 根据 channel 字段的标签,将定义的字段
37 // 与 rss 文档的字段关联起来
38 channel struct {
39 XMLName xml.Name `xml:"channel"`
40 Title string `xml:"title"`
41 Description string `xml:"description"`
42 Link string `xml:"link"`
43 PubDate string `xml:"pubDate"`
44 LastBuildDate string `xml:"lastBuildDate"`
45 TTL string `xml:"ttl"`
46 Language string `xml:"language"`
47 ManagingEditor string `xml:"managingEditor"`
48 WebMaster string `xml:"webMaster"`
49 Image image `xml:"image"`
50 Item []item `xml:"item"`
51 }
52
53 // rssDocument 定义了与 rss 文档关联的字段
54 rssDocument struct {
55 XMLName xml.Name `xml:"rss"`
56 Channel channel `xml:"channel"`
57 }
58 )
如果把这些结构与任意一个数据源的 RSS 文档对比,就能发现它们的对应关系。解码 XML 的方法与我们在 feed.go 代码文件里解码 JSON 文档一样。接下来我们可以看看 rssMatcher
类型的声明,如代码清单 2-50 所示。
代码清单 2-50 matchers/rss.go:第 60 行到第 61 行
60 // rssMatcher 实现了 Matcher 接口
61 type rssMatcher struct{}
再说明一次,这个声明与 defaultMatcher
类型的声明很像。因为不需要维护任何状态,所以我们使用了一个空结构来实现 Matcher
接口。接下来看看匹配器 init
函数的实现,如代码清单 2-51 所示。
代码清单 2-51 matchers/rss.go:第 63 行到第 67 行
63 // init 将匹配器注册到程序里
64 func init() {
65 var matcher rssMatcher
66 search.Register("rss", matcher)
67 }
就像在默认匹配器里看到的一样, init
函数将 rssMatcher
类型的值注册到程序里,以备后用。让我们再看一次 main.go 代码文件里的导入部分,如代码清单 2-52 所示。
代码清单 2-52 main.go:第 07 行到第 08 行
07 _ "github.com/goinaction/code/chapter2/sample/matchers"
08 "github.com/goinaction/code/chapter2/sample/search"
main.go 代码文件里的代码并没有直接使用任何 matchers
包里的标识符。不过,我们依旧需要编译器安排调用 rss.go 代码文件里的 init
函数。在第 07 行,我们使用下划线标识符作为别名导入 matchers
包,完成了这个调用。这种方法可以让编译器在导入未被引用的包时不报错,而且依旧会定位到包内的 init
函数。我们已经看过了所有的导入、类型和初始化函数,现在来看看最后两个用于实现 Matcher
接口的方法,如代码清单 2-53 所示。
代码清单 2-53 matchers/rss.go:第 114 行到第 140 行
114 // retrieve 发送 HTTP Get 请求获取 rss 数据源并解码
115 func (m rssMatcher) retrieve(feed *search.Feed) (*rssDocument, error) {
116 if feed.URI == "" {
117 return nil, errors.New("No rss feed URI provided")
118 }
119
120 // 从网络获得 rss 数据源文档
121 resp, err := http.Get(feed.URI)
122 if err != nil {
123 return nil, err
124 }
125
126 // 一旦从函数返回,关闭返回的响应链接
127 defer resp.Body.Close()
128
129 // 检查状态码是不是 200,这样就能知道
130 // 是不是收到了正确的响应
131 if resp.StatusCode != 200 {
132 return nil, fmt.Errorf("HTTP Response Error %d\n", resp.StatusCode)
133 }
134
135 // 将 rss 数据源文档解码到我们定义的结构类型里
136 // 不需要检查错误,调用者会做这件事
137 var document rssDocument
138 err = xml.NewDecoder(resp.Body).Decode(&document)
139 return &document, err
140 }
方法 retrieve
并没有对外暴露,其执行的逻辑是从 RSS 数据源的链接拉取 RSS 文档。在第 121 行,可以看到调用了 http
包的 Get
方法。我们会在第 8 章进一步介绍这个包,现在只需要知道,使用 http
包,Go 语言可以很容易地进行网络请求。当 Get
方法返回后,我们可以得到一个指向 Response
类型值的指针。之后会监测网络请求是否出错,并在第 127 行安排函数返回时调用 Close
方法。
在第 131 行,我们检测了 Response
值的 StatusCode
字段,确保收到的响应是 200
。任何不是 200
的请求都需要作为错误处理。如果响应值不是 200
,我们使用 fmt
包里的 Errorf
函数返回一个自定义的错误。最后 3 行代码很像之前解码 JSON 数据文件的代码。只是这次使用 xml
包并调用了同样叫作 NewDecoder
的函数。这个函数会返回一个指向 Decoder
值的指针。之后调用这个指针的 Decode
方法,传入 rssDocument
类型的局部变量 document
的地址。最后返回这个局部变量的地址和 Decode
方法调用返回的错误值。
最后我们来看看实现了 Matcher
接口的方法,如代码清单 2-54 所示。
代码清单 2-54 matchers/rss.go: 第 69 行到第 112 行
69 // Search 在文档中查找特定的搜索项
70 func (m rssMatcher) Search(feed *search.Feed, searchTerm string)
([]*search.Result, error) {
71 var results []*search.Result
72
73 log.Printf("Search Feed Type[%s] Site[%s] For Uri[%s]\n",
feed.Type, feed.Name, feed.URI)
74
75 // 获取要搜索的数据
76 document, err := m.retrieve(feed)
77 if err != nil {
78 return nil, err
79 }
80
81 for _, channelItem := range document.Channel.Item {
82 // 检查标题部分是否包含搜索项
83 matched, err := regexp.MatchString(searchTerm, channelItem.Title)
84 if err != nil {
85 return nil, err
86 }
87
88 // 如果找到匹配的项,将其作为结果保存
89 if matched {
90 results = append(results, &search.Result{
91 Field: "Title",
92 Content: channelItem.Title,
93 })
94 }
95
96 // 检查描述部分是否包含搜索项
97 matched, err = regexp.MatchString(searchTerm, channelItem.Description)
98 if err != nil {
99 return nil, err
100 }
101
102 // 如果找到匹配的项,将其作为结果保存
103 if matched {
104 results = append(results, &search.Result{
105 Field: "Description",
106 Content: channelItem.Description,
107 })
108 }
109 }
110
111 return results, nil
112 }
我们从第 71 行 results
变量的声明开始分析,如代码清单 2-55 所示。这个变量用于保存并返回找到的结果。
代码清单 2-55 matchers/rss.go:第 71 行
71 var results []*search.Result
我们使用关键字 var
声明了一个值为 nil
的切片,切片每一项都是指向 Result
类型值的指针。 Result
类型的声明在之前 match.go 代码文件的第 08 行中可以找到。之后在第 76 行,我们使用刚刚看过的 retrieve
方法进行网络调用,如代码清单 2-56 所示。
代码清单 2-56 matchers/rss.go:第 75 行到第 79 行
75 // 获取要搜索的数据
76 document, err := m.retrieve(feed)
77 if err != nil {
78 return nil, err
79 }
调用 retrieve
方法返回了一个指向 rssDocument
类型值的指针以及一个错误值。之后,像已经多次看过的代码一样,检查错误值,如果真的是一个错误,直接返回。如果没有错误发生,之后会依次检查得到的 RSS 文档的每一项的标题和描述,如果与搜索项匹配,就将其作为结果保存,如代码清单 2-57 所示。
代码清单 2-57 matchers/rss.go:第 81 行到第 86 行
81 for _, channelItem := range document.Channel.Item {
82 // 检查标题部分是否包含搜索项
83 matched, err := regexp.MatchString(searchTerm, channelItem.Title)
84 if err != nil {
85 return nil, err
86 }
既然 document.Channel.Item
是一个 item
类型值的切片,我们在第 81 行对其使用 for range
循环,依次访问其内部的每一项。在第 83 行,我们使用 regexp
包里的 MatchString
函数,对 channelItem
值里的 Title
字段进行搜索,查找是否有匹配的搜索项。之后在第 84 行检查错误。如果没有错误,就会在第 89 行到第 94 行检查匹配的结果,如代码清单 2-58 所示。
代码清单 2-58 matchers/rss.go:第 88 行到第 94 行
88 // 如果找到匹配的项,将其作为结果保存
89 if matched {
90 results = append(results, &search.Result{
91 Field: "Title",
92 Content: channelItem.Title,
93 })
94 }
如果调用 MatchString
方法返回的 matched
的值为真,我们使用内置的 append
函数,将搜索结果加入到 results
切片里。 append
这个内置函数会根据切片需要,决定是否要增加切片的长度和容量。我们会在第 4 章了解关于内置函数 append
的更多知识。这个函数的第一个参数是希望追加到的切片,第二个参数是要追加的值。在这个例子里,追加到切片的值是一个指向 Result
类型值的指针。这个值直接使用字面声明的方式,初始化为 Result
类型的值。之后使用取地址运算符( &
),获得这个新值的地址。最终将这个指针存入了切片。
在检查标题是否匹配后,第 97 行到第 108 行使用同样的逻辑检查 Description
字段。最后,在第 111 行, Search
方法返回了 results
作为函数调用的结果。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论