這陣子都在寫line bot, 本來都host在heroku上面的, 簡單且方便, 後來突發奇想, 想用Rasberry pi 跑看看(跑得動喔)

Line的webhook有一個需求就是要有SSL連結, 走https, 但我不想申請一個certificate, 在raspberry pi上弄, 所幸Cloudflare 有提供免費的SSL certificate, 利用他們的flexible SSL就可以了

flexible SSL的方式是client到他們CDN server走的是SSL沒錯, 但他們server到你的server則是可以走一般的http connection, 再來的第二個問題是, 我家的網路是浮動IP的(後來才去申請固定IP), 所以必須能動態更新Cloudflare上的DNS紀錄

還好Cloudflare是有API

直接自己自幹一個也是可以啦, 但Cloudflare其實也有一個客製版的ddclient:

Dynamic DNS Client: ddclient

步驟可以照著上面文件的步驟來做, 可以從My settings -> Account -> Global API Key取得API key當作ddclient的密碼

接下來碰到的問題是, 我ddclient是跑在raspberry pi上, ddclient預設是用local IP, 這很明顯不對, 因為會用到內部的IP而不是對外那個, 而我家的ASUS無線分享器並沒支援Cloudflare, 我也不太想改firmware, 但這還是有解的, 把ddclient.conf裡加上這行:

use=web, web=checkip.dyndns.org/, web-skip='IP Address' # found after IP Address

這是告訴ddclient不要用local ip而是用web api去找出IP

但這一切….都還是太麻煩了….raspbeery pi總是會不小心碰掉電源, 總是會當機或跑不動, 更何況, 我都已經跑一個server了, ddclient不要再來搶記憶體了啦

最後我的解法是: DNS-O-Matic

這是一個Dynamic DNS的服務, 我的無線分享器也有支援, 它不是自身有DNS server, 而是可以代你去更新妳的DNS紀錄, 而且, 有支援Cloudflare!!! OK, 結案 (偷懶!)

這篇是延續"使用AWS lambda和Github來提供中華職棒賽程資料", 之前的做法是用Cloud watch加上lambda來做這件事, 但我跑的東西並不是那麼的頻繁, 在AWS上還是會被收到流量的費用, 因此就打算用更經濟的方式, 利用heroku免費的額度來做這事(真是壞客戶 XD)

目的是定時(比如說每四小時)去爬一些網頁的資訊, 爬這些網頁其實也不需要花太久時間

用Cloud watch + lambda的好處是不用架一台server, 但用Heroku這種PAAS其實也不用太去管server這事

Heroku是可以設定scheduled tasks的, 但額外的work dyno是要另外付費的, 因此, 如果需求不是需要太頻繁, 也不需要執行太久的, 這時候就可以利用ifttt來定時觸發一個url的方式來做

要定時觸發一個URL, ifttt applet該怎麼設定呢? 首先"this"要選用的是Date & Time, 如下:

設定上並沒有很多, 就像是每小時, 每天之類的, 沒辦法訂多個, 如果需要一次多個設定, 那就多新增幾個Applets吧

這邊設定每小時, 就設定每小時的15分來觸發吧

那"that"的動作呢? 觸發URL的動作是利用"Maker", 這是設計給iot用的吧, 不過, 拿來做這用途也是沒問題的:

Maker只有一個選項"Make a web request"

設定很單純, 就給定URL, 使用的HTTP Method(GET, POST, PUT …), Content-type, 跟Body

這邊我用的是POST + Json, Json裡面會帶一個TOKEN來辨識, 以免有心人士利用了這個URL, go的檢查範例如下:

func checkID(body io.Reader) bool {
        data, err := ioutil.ReadAll(body)

        if err != nil {
                return false
        }

        var rbody struct {
                Id string
        }
        err = json.Unmarshal(data, &rbody)

        if err != nil {
                return false
        }

        return rbody.Token == os.Getenv("SEC_TOKEN") && rbody.Token != ""
}

接到request後, 其實是可以把執行的task丟給另一個go routine處理, 原本的就可以回傳給ifttt, 避免執行太久而timeout的問題, 不過對heroku來說, 這還是在同一個web dyno上就是了

最近因為寫bot, 處理不少的HTML資料, 其中最常用的就是去取的Open Graph的內容, 取這部分的資料是做啥用呢? 現今, 多數的網頁已會用Open GraphTwitter Card 來描述網頁的一些屬性, 比如說標題, 相關圖片, 關聯影片等等, 而不管是Open Graph還是Twitter Card 都以HTML的meta tags存在的, 像這樣:

<meta property="og:site_name" content="TechCrunch" />
<meta property="og:site" content="social.techcrunch.com" />
<meta property="og:title" content="Hugo Barra is leaving his position as head of international at Xiaomi after 3.5&nbsp;years" />
<meta property="og:description" content="Chinese smartphone maker Xiaomi is losing its head of international and primary English-language spokesperson Hugo Barra after he announced his exit from the.." />
<meta property="og:image" content="https://tctechcrunch2011.files.wordpress.com/2016/05/hugo-barra-2.jpg?w=764&amp;h=400&amp;crop=1" />
<meta property="og:url" content="http://social.techcrunch.com/2017/01/22/hugo-barra-is-leaving-his-position-as-head-of-international-at-xiaomi-after-3-5-years/" />
<meta property="og:type" content="article" />
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:image:src' content='https://tctechcrunch2011.files.wordpress.com/2016/05/hugo-barra-2.jpg?w=764&#038;h=400&#038;crop=1' />
<meta name='twitter:site' content='@techcrunch' />
<meta name='twitter:url' content='https://techcrunch.com/2017/01/22/hugo-barra-is-leaving-his-position-as-head-of-international-at-xiaomi-after-3-5-years/' />
<meta name='twitter:description' content='Chinese smartphone maker Xiaomi is losing its head of international and primary English-language spokesperson Hugo Barra after he announced his exit from the company.' />

(範例剛好跟上時事 :P)

在做我的bot新聞萬事通就是用這個來取得圖片跟標題的內容, 當然, 你在Facebook分享了一個連結, Facebook會自動帶上縮圖跟內容, 資料來源也是來自OG

OG

雖然Go也可以找到一兩個Open graph的parser, 看起來好像還算堪用, 不過因為自己用得多了, 所以索性自己寫一個叫OG:

GitHub/julianshen - OG

仿golang本身的json package, 也利用了Reflection, 因此還有點靈活度, 可以支援更多的資訊

安裝OG

go get -u github.com/julianshen/og

基本資料結構

type PageInfo struct {
	Title    string `meta:"og:title"`
	Type     string `meta:"og:type"`
	Url      string `meta:"og:url"`
	Site     string `meta:"og:site"`
	SiteName string `meta:"og:site_name"`
	Images   []*OgImage
	Videos   []*OgVideo
	Audios   []*OgAudio
	Twitter  *TwitterCard
	Content  string
}

PageInfo是預設的資料結構, 前面有提到, 有彈性可以支援更多的資訊, 所以這個除了直接使用外也可當作一個參考用的定義, 主要就是採用了Go的struct field tags, 定義了一個叫做"meta“的自訂tag, 對照前面的html範例就可知道, 後面的值 是html meta tag裡面的property, 因此你可以自定義自己的資料結構, 然後仿這個規則加上tag即可

也可支援巢狀式, Arrays, Pointer

GetPageInfo

GetPageInfo是以已經定義好的PageInfo這個struct為主, (希望)已經包含比較基本的og或twitter card tags了, 直接呼叫對應的API, 就可以取得這個url裡面的PageInfo的資料了

urlStr := "https://techcrunch.com/2017/01/22/yahoo-hacking-sec/"
pageInfo, e := og.GetPageInfoFromUrl(urlStr)

另外還有一個PageInfo.Content, 這是網頁去掉廣告跟多餘的東西的純文字內容, 這就是GetPageData所沒有的了

GetPageData

跟GetPageInfo不同, 這很適合用在自訂資料結構, 舉個例說, 如果我只想抓og:image的資料就像這樣:

ogImage := og.OgImage{}
urlStr := "https://techcrunch.com/2017/01/22/yahoo-hacking-sec/"
og.GetPageDataFromUrl(urlStr, &ogImage)

簡單談談Go reflection

這個package因為要仿json package使用struct field tags, 所以用了Go的Reflection機制

老實說, Go的reflection沒有像Java的設計那麼好, 寫到後面還蠻容易昏頭的, 而主要有三種要先搞清楚

以這個例子來說:

ogImage := og.OgImage{}
pImage := &ogImage

ogImage的Type是struct OgImage, 而Value是og.OgImage{}, pImage的Type即是*OgImage, 而ogImage的Kind則是struct, pImage則是ptr(pointer)

比較難搞懂的就是什麼時候要用Type, 什麼時候要用Value, Kind又是什麼時候? (作業?!)

type := reflect.TypeOf(ogImage)
value := reflect.Value(pImage)

用’TypeOf’可以取得變數的Type, 而’ValueOf‘則是值(value)的部分, 而value雖然代表變數的值, 但它是一個叫做Value的struct,並不是原本的資料型態 所以不要把它直接當參數呼叫函數用, 如果真有需要, 可以用Value.Interface{}轉成interface{}

StructField.Tag.Lookup則是可以查Field裡面的tag內容

所以這個og parser的原理就是走過所有fields, 只要有meta tags的話就拿去搜尋對應的html tags, 有的話再把html tag裡面content屬性填入值

最後小抱怨一下, Open GraphTwitter Card 實在很不一致, 一個用property, 一個用name, 因此在OG的做法是先用property, 如果再找不到用name去找

Heroku蠻好用的, 也用了好幾年了, 拿來做prototype真是方便, 不過慚愧的是, 我還沒付過錢給他(真惡劣) 最近chatbot玩得比較多, 不想花錢租server, 所以就比較頻繁的用它, 說到這裡, 照例, 先來廣告一下:

新的Line叭寇(Barcode)小幫手 (轉含有條碼的圖片給它, 它會幫你解讀):

好了, 回歸正題, Heroku 雖然是一個Paas的服務, 但它彈性非常大, 透過不同的buildpack也可以支援不同的語言跟框架, 不像Google的GAE, 支援的平台就比較有限

Heroku本身也是跑在Linux上, 因此, 如果你需要額外的套件, 其實也是沒問題的, 舉個例子, 雖然我這個叭寇小幫手是用Go來寫的 但卻會用到ZBar這個讀取條碼的C程式庫, Heroku上當然沒裝, 所以建置Go時, 會因為找不到 程式庫而失敗

在Linux上, 我們可以用apt-get去安裝套件, 以zbar這例子是apt-get install libzbar-dev, 但在Heroku上又該怎麼裝呢?

還是要透過buildpack, 在command line下執行下面指令:

heroku buildpacks:add --index 1 https://github.com/heroku/heroku-buildpack-apt

apt這個build pack是放在官方的Github上, 因為我們希望該需要的, 軟體在一開始就把它準備好了

除了加build pack外, 還是不夠的, 你的套件還是沒被安裝, 因此繼續加一個叫Aptfile的檔案, 內容是你需要安裝的套件

當你push你的程式到heroku上後, 它就會根據Aptfile去安裝相對應的套件了

好, 這算我以為我寫過但實際上沒有系列….啥? 剛剛把我以前寫的一個Go package - gopttcrawler更新後, 想說之前好像有寫過相關文章, 但實際上又沒找到(老了?)

沒關係, 把Readme拿來貼一貼就可以混一篇文了(混蛋!!!偷懶!!!)

gopttcrawler

這是我在新聞萬事通裡面用來抓取ptt文章的, 用法也很簡單, 基本上看最後兩種用法就好, 自己個人覺得那比較好用

原本的版本並沒處理18+那個擋在前面的畫面, 查了一下很多人(在python)的做法是去發post取得cookie, 再帶cookie去取, 後來發現, cookie是固定的, 只要這樣就好:

	cookie := http.Cookie{
		Name:  "over18",
		Value: "1",
	}
	req.AddCookie(&cookie)

所以現在像是八卦版這種18+的也可以抓取了

安裝方法: go get -u github.com/julianshen/gopttcrawler

使用方法請參考sample.go或是ptt_test.go

資料結構

type Article struct {
	ID       string //Article ID
	Board    string //Board name
	Title    string
	Content  string
	Author   string //Author ID
	DateTime string
	Nrec     int //推文數(推-噓)
}

type ArticleList struct {
	Articles     []*Article //Articles
	Board        string //Board
	PreviousPage int //Previous page id
	NextPage     int //Next page id
}

載入文章列表

  1. 載入最新一頁表特版文章
    articleList, _ := gopttcrawler.GetArticles("Beauty", 0)
    // the 1st parameter is the board name
    // the 2nd parameter is the page id. 0 indicates the latest page
  1. 載入前一頁文章列表
    prevArticleList, _ := articleList.GetFromPreviousPage()

載入文章內容

  1. 載入單篇文章詳細內容
    article := articleList.Articles[0]
    article.Load()
    fmt.Println(article.Content) //印出內文(html)
  1. 取得文章中所有圖片連結
    images := article.GetImageUrls()
  1. 取得文章中的連結
    links := article.GetLinks()

Iterator

新增Iterator功能:

	n := 100

	articles, e := gopttcrawler.GetArticles("movie", 0)
	
	if e != nil {
		....
	}

	iterator := articles.Iterator()

	i := 0
	for {
		if article, e := iterator.Next(); e == nil {
			if i >= n {
				break
			}
			i++

			log.Printf("%v %v", i, article)
		}
	}

上面這範例是抓取最新的100篇文章, 不用管第幾頁, 或是上一頁下一頁, 反正就一直抓

Go routine版本的GetArticles

	ch, done := gopttcrawler.GetArticlesGo("Beauty", 0)
	n := 100
	i := 0
	for article := range ch {
		if i >= n {
			done <- true
			break
		}
		i++
		log.Printf("%v %v", i, article)
	}

這範例一樣也是抓一百篇, 只是抓文章的部分被放到go routine去了, 會立即回傳兩個channel, 第一個是receive only channel, 跟Iterator類似, 一次拿一篇文章, 可以用range, 第二個是一個bool channel, 拿夠了送個訊息通知他終止go routine, 如果把receive部分放到select去, 就是non blocking了, 不會被讀上一頁下一頁的IO給卡住