如何將豆瓣觀影記錄實時同步至部落格中

Meng小羽發表於2020-06-12

友情提示:此篇文章大約需要閱讀 27分鐘32秒,不足之處請多指教,感謝你的閱讀。? 訂閱本站

事情的起因是這樣的,前幾日在看 idealclover 大佬的部落格,不經意間看到了他的豆瓣觀影記錄,他部落格中關於豆瓣觀影記錄是實時同步的,很好奇是如何實現的,經過檢視,他是爬取的豆瓣觀影介面來實現的,其實關於豆瓣觀影記錄,網上也有很多的教程,恰巧自己所學的 Go 語言也可以做簡單的爬蟲實現其效果,於是開始上手造輪子了,PS:瞭解到非法爬取網站資訊是違法的,之前豆瓣 API 介面,關閉訪問,在豆瓣上找了好久,終於在我的主頁中找到了對於觀影記錄的官方提供 RSS 訂閱,開啟訂閱,看到有自己所需要的欄位,比較好獲取,於是就開始了此專案。

分析

首先,需要獲取豆瓣提供的 XML 檔案,在我的主頁右下角就可以看到 RSS 訂閱連結:

豆瓣訂閱地址

找到了訂閱地址,點選檢視 XML 結構,可以看到豆瓣提供的結構還是挺理想的:

<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
<channel>
    <title>Meng小羽 的收藏</title>
    <link>https://www.douban.com/people/debuginn/</link>
    <description>
        <![CDATA[ Meng小羽 的收藏:想看、在看和看過的書和電影,想聽、在聽和聽過的音樂 ]]>
    </description>
    <language>zh-cn</language>
    <copyright>© 2013, douban.com.</copyright>
    <pubDate>Sat, 30 May 2020 09:14:08 GMT</pubDate>
    <item>
        <title>看過黑衣人:全球追緝</title>
        <link>http://movie.douban.com/subject/19971676/</link>
        <description>
        <![CDATA[ <table><tr> <td width="80px"><a href="https://movie.douban.com/subject/19971676/" title="Men in Black International"> <img src="https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2558701068.webp" alt="Men in Black International"></a></td> <td> <p>推薦: 力薦</p> </td></tr></table> ]]>
        </description>
        <dc:creator>Meng小羽</dc:creator>
        <pubDate>Sat, 30 May 2020 09:14:08 GMT</pubDate>
        <guid isPermaLink="false">https://www.douban.com/people/debuginn/interests/2402808825</guid>
    </item>
    ......
<channel>

其實,我們提取的主要就是 item 標籤下對應的電影資訊內容:

<item>
    <title>看過黑衣人:全球追緝</title>
    <link>http://movie.douban.com/subject/19971676/</link>
    <description>
    <![CDATA[ <table><tr> <td width="80px"><a href="https://movie.douban.com/subject/19971676/" title="Men in Black International"> <img src="https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2558701068.webp" alt="Men in Black International"></a></td> <td> <p>推薦: 力薦</p> </td></tr></table> ]]>
    </description>
    <dc:creator>Meng小羽</dc:creator>
    <pubDate>Sat, 30 May 2020 09:14:08 GMT</pubDate>
    <guid isPermaLink="false">https://www.douban.com/people/debuginn/interests/2402808825</guid>
</item>

設計

根據豆瓣官方提供的 XML 標籤資料,可以利用 Go 語言中 encoding/xml 包來進行對資料的對映,可以設計成如下結構體:

// 豆瓣 xml 描述結構體
type Attributes struct {
    XMLName xml.Name `xml:"rss"`
    Version string   `xml:"version,attr"`
    Channel Channel  `xml:"channel"`
}

// XML 主題結構拆分
type Channel struct {
    Title       string      `xml:"title"`
    Link        string      `xml:"link"`
    Description string      `xml:"description"`
    Language    string      `xml:"language"`
    Copyright   string      `xml:"copyright"`
    Pubdate     string      `xml:"pubDate"`
    MovieItem   []MovieItem `xml:"item"`
}

// 豆瓣 電影列表結構體
type MovieItem struct {
    Title       string `xml:"title"`
    Link        string `xml:"link"`
    Description string `xml:"description"`
    Pubdate     string `xml:"pubDate"`
}

可以和 XML 檔案對應欄位進行匹配,可以從上面的結構體中我們可以看到,最終我們想獲取到的資料就是結構體 MovieItem 的資料。

由於是從網上鍊接獲取資料的,在這裡首先我們需要將網上豆瓣提供的 XML 檔案轉換成 []byte 型別的資料:

// 獲取 xml 檔案資料
func getXMLData(url string) (data []byte, err error) {
    // 讀取 xml 檔案
    client := &http.Client{}
    req, _ := http.NewRequest("GET", url, nil)
    // 自定義Header
    req.Header.Set("User-Agent", "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)")
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close() // 關閉檔案
    // 讀取所有檔案內容儲存至 []byte
    data, err = ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    return
}

上面這個函式實現的就是將 XML 檔案儲存至 Go 語言的資料結構的操作,現在可以將 XML 檔案成功讀取出來,接下來就是要進行 XML 欄位與上面作出的結構體之間的對映,其實對映至結構體的過程是比較簡單的,首先宣告 Attributes{} 型別的結構體,之後通過 xml.Unmarshal 來實現對映拷貝,就可以得到對應的結構體型別的資料,由於我們想要的資料是結構體資料中的一部分,即 MovieItem,在得到結構體資料後就可以將想要的這一部分的資料選擇抽取出來:

v := Attributes{}
unMarshalErr := xml.Unmarshal(data, &v)
if unMarshalErr != nil {
    fmt.Printf("xml unmarshal failed, err:%v\n", err)
}

movieItem := v.Channel.MovieItem

Map 轉換

在這裡我們可以得到結構體中巢狀的結構體,在結構體中有一些欄位我們是不想要的,需要進行處理,對於 description 這個欄位中,官方提供的是一段 HTML 描述串,其中電影的描述檔案是我們所需要的,對於 HTML 字串的拆分,我們可以藉助strings.Split 函式來實現擷取,使用\" 符號擷取,雖然可以獲取到我們想要的資料了,但是由於這個是巢狀的結構體,我們需要做一個匹配的 map 來進行儲存處理好的資料,可以看程式碼中我的設計:

MoviesMap := make(map[int]interface{})
for i := 0; i < len(movieItem); i++ {
    movie := make(map[string]string)
    description := strings.Split(movieItem[i].Description, "\"")
    movie["Title"] = string([]rune(movieItem[i].Title)[2:])
    movie["Link"] = movieItem[i].Link
    movie["Img"] = description[7]
    movie["Pubdate"] = movieItem[i].Pubdate

    MoviesMap[i] = movie
}

外層 map 是採用 map[int]interface{} 型別,在 interface{} 中儲存這內層 map map[string]string 型別。

針對於 Img 地址的獲取,是現根據特定符號拆分,之後獲取制定位置的資料獲取的。

0 map[Img:https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2558701068.jpg Link:http://movie.douban.com/subject/19971676/ Pubdate:Sat, 30 May 2020 09:14:08 GMT Title:黑衣人:全球追緝]
1 map[Img:https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2263408369.jpg Link:http://movie.douban.com/subject/1294371/ Pubdate:Thu, 28 May 2020 10:06:23 GMT Title:摩登時代]
......

最後就是將這個 map 做一下序列化處理,這樣就可以返回給前臺資料了。

data, _ = json.Marshal(MoviesMap)

服務

處理好資料,做了對應的處理,怎麼將資料作為服務端提供給前臺,在這裡需要使用 Web 服務,Go 中可以使用原生 Web,不過我在這裡使用的是之前學過的 Gin 框架,來提供服務的:

r := gin.Default()
r.GET("/doubanmovies", func(context *gin.Context) {
    context.JSON(http.StatusOK, MoviesMap)
})
_ = r.Run(":8080")

api json資料

啟動服務,可以得到對應的 json 資料,你若以為現在就可以實現了,那麼你錯了,遠遠沒有那麼簡單……

前臺

由於我知曉我的部落格採用的前臺 UI 技術是 MDUI, 我利用自身的卡片 UI 迅速設計了一個模組,因為後期需要放在我的部落格頁面上,前端讀取資料採用的是 VUE 和 axios 技術:

<div class="mdui-container-fluid" id="app">
    <div class="mdui-row">
        <div v-for="item in info">
            <div class="mdui-col-xs-6 mdui-col-sm-4 mdui-col-md-3 mdui-col-lg-3 mdui-m-b-1 mdui-m-t-1">
                <a :href="item.Link" target="_blank">
                    <div class="mdui-card mdui-hoverable">
                        <div class="mdui-card-media">
                            <img :src="item.Img" style="height: 260px;" />
                            <div class="mdui-card-media-covered">
                                <div class="mdui-card-primary">
                                    <div class="mdui-card-primary-subtitle">{{ item.Title }}</div>
                                </div>
                            </div>
                        </div>
                    </div>
                </a>
            </div>
        </div>
    </div>
</div>
<script src="./static/js/mdui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.1.8/vue.min.js"></script>
<script src="https://cdn.staticfile.org/axios/0.18.0/axios.min.js"></script>
<script>
    new Vue({
        el: '#app',
        data() {
            return {
                info: null
            }
        },
        mounted() {
            axios
                .get('http://127.0.0.1:8080/doubanmovies')
                .then(response => (this.info = response.data))
                .catch(function (error) { // 請求失敗處理
                    console.log(error);
                });
        }
    })
</script>

設計好了以後,訪問頁面,卻載入不出來,emmmmmm

CORS

CORS

看到了是 CORS 同源策略的原因,接下來就是要解決同源問題了,方法比較簡單,就是將 Go 服務端加上 CORS 同源策略就可以了,方法如下:

r := gin.Default()
r.Use(Cors())
r.GET("/doubanmovies", func(context *gin.Context) {
    context.JSON(http.StatusOK, MoviesMap)
})

_ = r.Run(":8080")

在路由訪問中新增 Cors() 函式:

// 跨域
func Cors() gin.HandlerFunc {
    return func(c *gin.Context) {
        method := c.Request.Method               //請求方法
        origin := c.Request.Header.Get("Origin") //請求頭部
        var headerKeys []string                  // 宣告請求頭keys
        for k, _ := range c.Request.Header {
            headerKeys = append(headerKeys, k)
        }
        headerStr := strings.Join(headerKeys, ", ")
        if headerStr != "" {
            headerStr = fmt.Sprintf("access-control-allow-origin, access-control-allow-headers, %s", headerStr)
        } else {
            headerStr = "access-control-allow-origin, access-control-allow-headers"
        }
        if origin != "" {
            c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
            c.Header("Access-Control-Allow-Origin", "*")                                       // 這是允許訪問所有域
            c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, PUT, DELETE,UPDATE") //伺服器支援的所有跨域請求的方法,為了避免瀏覽次請求的多次'預檢'請求
            //  header的型別
            c.Header("Access-Control-Allow-Headers", "Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma")
            //              允許跨域設定                                                                                                      可以返回其他子段
            c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar") // 跨域關鍵設定 讓瀏覽器可以解析
            c.Header("Access-Control-Max-Age", "172800")                                                                                                                                                           // 快取請求資訊 單位為秒
            c.Header("Access-Control-Allow-Credentials", "false")                                                                                                                                                  //  跨域請求是否需要帶cookie資訊 預設設定為true
            c.Set("content-type", "application/json")                                                                                                                                                              // 設定返回格式是json
        }

        //放行所有OPTIONS方法
        if method == "OPTIONS" {
            c.JSON(http.StatusOK, "Options Request!")
        }
        // 處理請求
        c.Next() //  處理請求
    }
}

這樣就可以看到結果了,如下圖:

演示

看到結果後,心中竊喜,感覺成功了,接下來就需要將 Go 服務部署到我的伺服器中去了,部署步驟比較簡單,就不過多解釋了,最後訪問伺服器 IP 及對應單口可以呈現結果,最後將前臺程式碼貼上到新建的頁面中,生成預覽,emmmm,啥都沒有,瀏覽器居然報 HTTPS 請求 HTTP 資源是不安全的,吐了一口血,解決吧,唉,經過查詢資料,得出如下兩個解決方案:

  • Gin 框架服務本身使用 SSL 證照,實現 HTTPS 訪問,不過需要配置域名;
  • 使用 Nginx 服務做一下代理,將一個特定連結代理到本身服務中去。

作為學生黨的我,沒有太多的資金去申請過多的 SSL 證照(省著點用),於是我就在我的 demo.debuginn.cn 子域名下做了一個代理。

代理

Nginx 代理實現也是比較簡單的,就是將前端訪問某個介面代理至伺服器中某個埠的服務中,表面上看是 Nginx 在做資料處理,實際上是 Nginx 只做了一個代理轉發,由於我demo.debuginn.cn 子域名本身就是 https 的,所以設定好了代理之後,就可以使用固定的代理連結訪問了,配置如下:

server{
    .....
    location /doubanmovies {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host:80;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

這樣就可以實現 https 資源訪問了:demo.debuginn.cn/doubanmovies

效果

解決了 HTTPS 訪問 HTTP 資源的問題,就解決了所有問題,實現了效果。

我的觀影

具體效果如下:

我的觀影 - Debug客棧
動態同步豆瓣資料 | Github倉庫 | Go \ Gin \ VUE \ MDUI …

開源

針對於此小專案,我已經開源至 Github 中,若是你感興趣或者有什麼建議,可以聯絡我,我們一起改進,同時希望你可以給我一個 Star,萬分感謝!

GitHub
debuginn/DouBanMyMovies
如何將豆瓣觀影記錄實時同步至部落格中 www.debuginn.cn/6016.html

本作品採用《CC 協議》,轉載必須註明作者和本文連結

Debug客棧

相關文章