共计 6850 个字符,预计需要花费 18 分钟才能阅读完成。
背景
上一篇文章 Go 每日一库之 bubbletea 咱们介绍了炫酷的 TUI 程序框架 — bubbletea
。最初实现了一个拉取 GitHub Trending 仓库,并显示在控制台的程序。因为 GitHub 没有提供官网的 Trending API,咱们用 goquery
本人实现了一个。上篇文章因为篇幅关系,没有介绍如何实现。本文我整顿了一下代码,并以独自的代码库模式凋谢进去。
先察看
首先,咱们来察看一下 GitHub Trending 的构造:
左上角能够切换仓库(Repositories)和开发者(Developers)。左边能够抉择语言(Spoken Language,本地语言,汉语、英文等)、语言(Language,编程语言,Golang、C++ 等)和工夫范畴(Date Range,反对 3 个维度,Today、This week、This month)。
而后上面是每个仓库的信息:
① 仓库作者和名字
② 仓库形容
③ 次要应用的编程语言(创立仓库时设置的),也可能没有
④ 星数
⑤ 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😄
参考
- ghtrending GitHub:github.com/darjun/ghtrending
- Go 每日一库之 goquery:https://darjun.github.io/2020/10/11/godailylib/goquery
- Go 每日一库 GitHub:https://github.com/darjun/go-daily-lib
我
我的博客:https://darjun.github.io
欢送关注我的微信公众号【GoUpUp】,独特学习,一起提高~