上一篇文章Go 每日一库之 bubbletea咱们介绍了炫酷的 TUI 程序框架 — bubbletea
。最后实现了一个拉取 GitHub Trending 仓库,并显示在控制台的程序。因为 GitHub 没有提供官方的 Trending API,咱们用goquery
本身实现了一个。上篇文章因为篇幅关系zh,没有介绍如何实现。本文我整理了一下代码,并以单独的代码库形式开放出来。git
首先,咱们来观察一下 GitHub Trending 的结构:github
左上角能够切换仓库(Repositories)和开发者(Developers)。右边能够选择语言(Spoken Language,本地语言,汉语、英文等)、语言(Language,编程语言,Golang、C++等)和时间范围(Date Range,支持 3 个维度,Today、This week、This month)。golang
而后下面是每一个仓库的信息:chrome
① 仓库做者和名字编程
② 仓库描述微信
③ 主要使用的编程语言(建立仓库时设置的),也可能没有app
④ 星数框架
⑤ fork 数编程语言
⑥ 贡献者列表函数
⑦ 选定的时间范围内(Today、This week、This month)新增多少星数
开发者页面也是相似的,只不过信息少了不少:
① 做者信息
② 最火的仓库信息
注意到切换的开发者页面后,URL 变成为github.com/trending/developers
。另外当咱们选择本地语言为中文、开发语言为 Go 和时间范围为 Today 后,URL 变为https://github.com/trending/go?since=daily&spoken_language_code=zh
,经过在 query-string 中增长相应的键值对表示这种选择。
在 GitHub 上建立仓库ghtrending
,clone 到本地,执行go mod init
初始化:
$ go mod init github.com/darjun/ghtrending
而后执行go get
下载goquery
库:
$ go get github.com/PuerkitoBio/goquery
根据仓库和开发者的信息定义两个结构:
type Repository struct { Author string Name string Link string Desc string Lang string Stars int Forks int Add int BuiltBy []string } type Developer struct { Name string Username string PopularRepo string Desc string }
要想使用goquery
获取相应的信息,咱们首先要知道,对应的网页结构。按 F12 打开 chrome 开发者工具,选择Elements
页签,便可看到网页结构:
使用左上角的按钮就能够很快速的查看网页上任何内容的结构,咱们点击单个仓库条目:
右边Elements
窗口显示每一个仓库条目对应一个article
元素:
可使用标准库net/http
获取整个网页的内容:
resp, err := http.Get("https://github.com/trending")
而后从resp
对象中建立goquery
文档结构:
doc, err := goquery.NewDocumentFromReader(resp.Body)
有了文档结构对象,咱们能够调用其Find()
方法,传入选择器,这里我选择.Box .Box-row
。.Box
是整个列表div
的 class,.Box-row
是仓库条目的 class。这样的选择更精准。Find()
方法返回一个*goquery.Selection
对象,咱们能够调用其Each()
方法对每一个条目进行解析。Each()
接收一个func(int, *goquery.Selection)
类型的函数,第二个参数即为每一个仓库条目在 goquery 中的结构:
doc.Find(".Box .Box-row").Each(func(i int, s *goquery.Selection) { })
接下来咱们看看如何提取各个部分。在Elements
窗口中移动,能够很直观的看到每一个元素对应页面的哪一个部分:
咱们找到仓库名和做者对应的结构:
它被包在article
元素下的h1
元素下的a
元素内,做者名在span
元素内,仓库名直接在a
下,另外仓库的 URL 连接是a
元素的href
属性。咱们来获取它们:
titleSel := s.Find("h1 a") repo.Author = strings.Trim(titleSel.Find("span").Text(), "/\n ") repo.Name = strings.TrimSpace(titleSel.Contents().Last().Text()) relativeLink, _ := titleSel.Attr("href") if len(relativeLink) > 0 { repo.Link = "https://github.com" + relativeLink }
仓库描述在article
元素内的p
元素中:
repo.Desc = strings.TrimSpace(s.Find("p").Text())
编程语言,星数,fork 数,贡献者(BuiltBy
)和新增星数都在article
元素的最后一个div
中。编程语言、BuiltBy
和新增星数在span
元素内,星数和 fork 数在a
元素内。若是编程语言未设置,则少一个span
元素:
var langIdx, addIdx, builtByIdx int spanSel := s.Find("div>span") if spanSel.Size() == 2 { // language not exist langIdx = -1 addIdx = 1 } else { builtByIdx = 1 addIdx = 2 } // language if langIdx >= 0 { repo.Lang = strings.TrimSpace(spanSel.Eq(langIdx).Text()) } else { repo.Lang = "unknown" } // add addParts := strings.SplitN(strings.TrimSpace(spanSel.Eq(addIdx).Text()), " ", 2) repo.Add, _ = strconv.Atoi(addParts[0]) // builtby spanSel.Eq(builtByIdx).Find("a>img").Each(func(i int, img *goquery.Selection) { src, _ := img.Attr("src") repo.BuiltBy = append(repo.BuiltBy, src) })
而后是星数和 fork 数:
aSel := s.Find("div>a") starStr := strings.TrimSpace(aSel.Eq(-2).Text()) star, _ := strconv.Atoi(strings.Replace(starStr, ",", "", -1)) repo.Stars = star forkStr := strings.TrimSpace(aSel.Eq(-1).Text()) fork, _ := strconv.Atoi(strings.Replace(forkStr, ",", "", -1)) repo.Forks = fork
Developers 也是相似的作法。这里就不赘述了。使用goquery
有一点须要注意,由于网页层级结构比较复杂,咱们使用选择器的时候尽可能多限定一些元素、class,以确保找到的确实是咱们想要的那个结构。另外网页上获取的内容有不少空格,须要使用strings.TrimSpace()
移除。
基本工做完成以后,咱们来看看如何设计接口。我想提供一个类型和一个建立该类型对象的方法,而后调用对象的FetchRepos()
和FetchDevelopers()
方法就能够获取仓库和开发者列表。可是我不但愿用户了解这个类型的细节。因此我定义了一个接口:
type Fetcher interface { FetchRepos() ([]*Repository, error) FetchDevelopers() ([]*Developer, error) }
咱们定义一个类型来实现这个接口:
type trending struct{} func New() Fetcher { return &trending{} } func (t trending) FetchRepos() ([]*Repository, error) { } func (t trending) FetchDevelopers() ([]*Developer, error) { }
咱们上面介绍的爬取逻辑就是放在FetchRepos()
和FetchDevelopers()
方法中。
而后,咱们就能够在其余地方使用了:
import "github.com/darjun/ghtrending" t := ghtrending.New() repos, err := t.FetchRepos() developers, err := t.FetchDevelopers()
前面也说过,GitHub Trending 支持选定本地语言、编程语言和时间范围等。咱们但愿把这些设置做为选项,使用 Go 语言经常使用的选项模式/函数式选项(functional option)。先定义选项结构:
type options struct { GitHubURL string SpokenLang string Language string // programming language DateRange string } type option func(*options)
而后定义 3 个DataRange
选项:
func WithDaily() option { return func(opt *options) { opt.DateRange = "daily" } } func WithWeekly() option { return func(opt *options) { opt.DateRange = "weekly" } } func WithMonthly() option { return func(opt *options) { opt.DateRange = "monthly" } }
之后可能还有其余范围的时间,留一个通用一点的选项:
func WithDateRange(dr string) option { return func(opt *options) { opt.DateRange = dr } }
编程语言选项:
func WithLanguage(lang string) option { return func(opt *options) { opt.Language = lang } }
本地语言选项,国家和代码分开,例如 Chinese 的代码为 cn:
func WithSpokenLanguageCode(code string) option { return func(opt *options) { opt.SpokenLang = code } } func WithSpokenLanguageFull(lang string) option { return func(opt *options) { opt.SpokenLang = spokenLangCode[lang] } }
spokenLangCode
是 GitHub 支持的国家和代码的对照,我是从 GitHub Trending 页面爬取的。大概是这样的:
var ( spokenLangCode map[string]string ) func init() { spokenLangCode = map[string]string{ "abkhazian": "ab", "afar": "aa", "afrikaans": "af", "akan": "ak", "albanian": "sq", // ... } }
最后我但愿 GitHub 的 URL 也能够设置:
func WithURL(url string) option { return func(opt *options) { opt.GitHubURL = url } }
咱们在trending
结构中增长options
字段,而后改造一下New()
方法,让它接受可变参数的选项。这样咱们只须要设置咱们想要设置的,其余的选项均可以采用默认值,例如GitHubURL
:
type trending struct { opts options } func loadOptions(opts ...option) options { o := options{ GitHubURL: "http://github.com", } for _, option := range opts { option(&o) } return o } func New(opts ...option) Fetcher { return &trending{ opts: loadOptions(opts...), } }
最后在FetchRepos()
方法和FetchDevelopers()
方法中根据选项拼接 URL:
fmt.Sprintf("%s/trending/%s?spoken_language_code=%s&since=%s", t.opts.GitHubURL, t.opts.Language, t.opts.SpokenLang, t.opts.DateRange) fmt.Sprintf("%s/trending/developers?lanugage=%s&since=%s", t.opts.GitHubURL, t.opts.Language, t.opts.DateRange)
加入选项以后,若是咱们要获取一周内的,Go 语言 Trending 列表,能够这样:
t := ghtrending.New(ghtrending.WithWeekly(), ghtreading.WithLanguage("Go")) repos, _ := t.FetchRepos()
另外,咱们还提供一个不须要建立trending
对象,直接调用接口获取仓库和开发者列表的方法(懒人专用):
func TrendingRepositories(opts ...option) ([]*Repository, error) { return New(opts...).FetchRepos() } func TrendingDevelopers(opts ...option) ([]*Developer, error) { return New(opts...).FetchDevelopers() }
新建目录并初始化 Go Modules:
$ mkdir -p demo/ghtrending && cd demo/ghtrending $ go mod init github/darjun/demo/ghtrending
下载包:
编写代码:
package main import ( "fmt" "log" "github.com/darjun/ghtrending" ) func main() { t := ghtrending.New() repos, err := t.FetchRepos() if err != nil { log.Fatal(err) } fmt.Printf("%d repos\n", len(repos)) fmt.Printf("first repo:%#v\n", repos[0]) developers, err := t.FetchDevelopers() if err != nil { log.Fatal(err) } fmt.Printf("%d developers\n", len(developers)) fmt.Printf("first developer:%#v\n", developers[0]) }
运行效果:
最后,咱们加点文档:
一个小开源库就完成了。
本文介绍如何使用goquery
爬取网页。着重介绍了ghtrending
的接口设计。在编写一个库的时候,应该提供易用的、最小化的接口。用户不须要了解库的实现细节就可使用。ghtrending
使用函数式选项就是一个例子,有须要才传递,无须要可不提供。
本身经过爬取网页的方式来获取 Trending 列表比较容易受限制,例如过段时间 GitHub 网页结构变了,代码就不得不作适配。在官方没有提供 API 的状况下,目前也只能这么作了。
你们若是发现好玩、好用的 Go 语言库,欢迎到 Go 每日一库 GitHub 上提交 issue😄
欢迎关注个人微信公众号【GoUpUp】,共同窗习,一块儿进步~