<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          用 Go 實現一個 GitHub Trending API

          共 11254字,需瀏覽 23分鐘

           ·

          2021-09-18 13:54

          背景

          上一篇文章Go 每日一庫之 bubbletea我們介紹了炫酷的 TUI 程序框架 — bubbletea。最后實現了一個拉取 GitHub Trending 倉庫,并顯示在控制臺的程序。由于 GitHub 沒有提供官方的 Trending API,我們用goquery自己實現了一個。上篇文章由于篇幅關系,沒有介紹如何實現。本文我整理了一下代碼,并以單獨的代碼庫形式開放出來。

          先觀察

          首先,我們來觀察一下 GitHub Trending 的結構:

          左上角可以切換倉庫(Repositories)和開發(fā)者(Developers)。右邊可以選擇語言(Spoken Language,本地語言,漢語、英文等)、語言(Language,編程語言,Golang、C++等)和時間范圍(Date Range,支持 3 個維度,Today、This week、This month)。

          然后下面是每個倉庫的信息:

          ① 倉庫作者和名字

          ② 倉庫描述

          ③ 主要使用的編程語言(創(chuàng)建倉庫時設置的),也可能沒有

          ④ 星數

          ⑤ fork 數

          ⑥ 貢獻者列表

          ⑦ 選定的時間范圍內(Today、This week、This month)新增多少星數

          開發(fā)者頁面也是類似的,只不過信息少了很多:

          ① 作者信息

          ② 最火的倉庫信息

          注意到切換的開發(fā)者頁面后,URL 變成為github.com/trending/developers。另外當我們選擇本地語言為中文、開發(fā)語言為 Go 和時間范圍為 Today 后,URL 變?yōu)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">https://github.com/trending/go?since=daily&spoken_language_code=zh,通過在 query-string 中增加相應的鍵值對表示這種選擇。

          準備

          在 GitHub 上創(chuàng)建倉庫ghtrending,clone 到本地,執(zhí)行go mod init初始化:

          $ go mod init github.com/darjun/ghtrending

          然后執(zhí)行go get下載goquery庫:

          $ go get github.com/PuerkitoBio/goquery

          根據倉庫和開發(fā)者的信息定義兩個結構:

          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 開發(fā)者工具,選擇Elements頁簽,即可看到網頁結構:

          使用左上角的按鈕就可以很快速的查看網頁上任何內容的結構,我們點擊單個倉庫條目:

          右邊Elements窗口顯示每個倉庫條目對應一個article元素:

          可以使用標準庫net/http獲取整個網頁的內容:

          resp, err := http.Get("https://github.com/trending")

          然后從resp對象中創(chuàng)建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()移除。

          接口設計

          基本工作完成之后,我們來看看如何設計接口。我想提供一個類型和一個創(chuàng)建該類型對象的方法,然后調用對象的FetchRepos()FetchDevelopers()方法就可以獲取倉庫和開發(fā)者列表。但是我不希望用戶了解這個類型的細節(jié)。所以我定義了一個接口:

          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()

          簡單方法

          另外,我們還提供一個不需要創(chuàng)建trending對象,直接調用接口獲取倉庫和開發(fā)者列表的方法(懶人專用):

          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的接口設計。在編寫一個庫的時候,應該提供易用的、最小化的接口。用戶不需要了解庫的實現細節(jié)就可以使用。ghtrending使用函數式選項就是一個例子,有需要才傳遞,無需要可不提供。

          自己通過爬取網頁的方式來獲取 Trending 列表比較容易受限制,例如過段時間 GitHub 網頁結構變了,代碼就不得不做適配。在官方沒有提供 API 的情況下,目前也只能這么做了。

          大家如果發(fā)現好玩、好用的 Go 語言庫,歡迎到 Go 每日一庫 GitHub 上提交 issue??

          參考

          1. ghtrending GitHub:github.com/darjun/ghtrending
          2. Go 每日一庫之 goquery:https://darjun.github.io/2020/10/11/godailylib/goquery
          3. Go 每日一庫 GitHub:https://github.com/darjun/go-daily-lib

          原文鏈接:用 Go 實現一個 GitHub Trending API


          推薦閱讀


          福利

          我為大家整理了一份從入門到進階的Go學習資料禮包,包含學習建議:入門看什么,進階看什么。關注公眾號 「polarisxu」,回復 ebook 獲??;還可以回復「進群」,和數萬 Gopher 交流學習。

          瀏覽 36
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  亚洲午夜福利电影 | 亚洲日韩在线网站 | 处破初破苞wwww视频 | 天堂在线aaa | 天堂视频免费在线观看 |