返回介绍

2.4 RSS 匹配器

发布于 2024-10-11 12:38:58 字数 10154 浏览 0 评论 0 收藏 0

最后要看的一部分代码是 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 包。再一次,我们看到有些标准库的包是从标准库所在的子文件夹导入的,如 xmlhttp 。就像 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文