最近在把之前弄的新聞萬事通做優化, 話說, 好久沒來宣傳一下新聞萬事通了(毆飛~~)

加入新聞萬事通請按 :

加入好友 加入好友

好, 回歸正題, 之前新聞萬事通檢查假新聞的邏輯是:

  1. 載入新聞小幫手資料庫到記憶體(server啟動時), 四千多筆資料存放在一個map的資料結構
  2. 使用者輸入的訊息如果含有url去map裡面找到對應的url
  3. 但常常有類似內容的新聞有不同來源, 因此會用Gojieba取出標題的關鍵字去四千多筆資料的標題對應關鍵字出現的次數

聽起來很沒效率(四千筆在記憶體內, 其實也不算慢啦, 但就吃記憶體), 那就要優化囉? 那需要一個搜尋引擎囉?

第一個先想到的是Elastic search, 但我還不想搞那麼大, 為了四千多筆資料多拉一台search server,我只想 一台respberry pi就能搞定

Gojieba知道, 有Bleve Search這東西, Bleve Search也是一個full text index engine, 但跟Elastic search不同的是, 它是內嵌在你的程式內, 而不是獨立的server, 而且它是用Go寫的(開心), 而不是Java

Bleve

Bleve Search的來頭也不小,它是來自於著名的NoSQL DB : Couchbase, 這篇有介紹一下他的功能 : Bleve:来自Couchbase、基于Go语言的全文索引与检索库

使用Bleve Search也很簡單, 就建立index, 然後search:

import "github.com/blevesearch/bleve"

func main() {
    // open a new index
    mapping := bleve.NewIndexMapping()
    index, err := bleve.New("example.bleve", mapping)

    // index some data
    err = index.Index(identifier, your_data)

    // search for some text
    query := bleve.NewMatchQuery("text")
    search := bleve.NewSearchRequest(query)
    searchResults, err := index.Search(search)
}

Bleve Search會把indexes放到資料庫內, 預設是使用Bolt DB, Bolt跟leveldbRocksDb 類似, 都是一種Key-Value database, 只是Bolt是純粹以Go開發的

如果你不想要用Bolt, Bleve也是支援使用leveldb和Rocksdb的, 純粹是作者想做成全Go的方案才預設Bolt db, 我自己有實測幾次, 使用這三種DB, 搜尋的速度差不多, 但建立index時bolt較快, 需求的磁碟空間則是Rocksdb優(比Bolt好很多)

Bleve Search的架構做成很有彈性, 除了可以使用不同的DB(我自己也有實作以Redis當KV database的plugin, 不過實在也沒好多少就作罷了), 文字分析(Text Analysis)比如說斷詞, 也是可以用plugin擴增的

官方支援的語言為: Danish, Dutch, English, Finnish, French, German, Hungarian, Italian, Norwegian, Persian, Portuguese, Romanian, Russian, Sorani, Spanish, Swedish, Thai, Turkish

就是沒中文!

還好Gojieba後來也加入了bleve的analyzer和tokenizer, 這一部分可以獲得解決

使用Gojieba斷詞:

	indexMapping := bleve.NewIndexMapping()
    os.RemoveAll(INDEX_DIR)
    // clean index when example finished
    defer os.RemoveAll(INDEX_DIR)

    err := indexMapping.AddCustomTokenizer("gojieba",
        map[string]interface{}{
            "dictpath":     gojieba.DICT_PATH,
            "hmmpath":      gojieba.HMM_PATH,
            "userdictpath": gojieba.USER_DICT_PATH,
            "type":         "gojieba",
        },
    )
    if err != nil {
        panic(err)
    }
    err = indexMapping.AddCustomAnalyzer("gojieba",
        map[string]interface{}{
            "type":      "gojieba",
            "tokenizer": "gojieba",
        },
    )
    if err != nil {
        panic(err)
    }
    indexMapping.DefaultAnalyzer = "gojieba"

Wukong

除了Bleve外, 還有一個悟空 Wukong, 這孫猴子也好像蠻識字的嘛

這個Wukong是由阿里巴巴的陳輝所開發的, 一樣是內嵌的全文檢索引擎

package main

import (
    "github.com/huichen/wukong/engine"
    "github.com/huichen/wukong/types"
    "log"
)

var (
    // searcher是协程安全的
    searcher = engine.Engine{}
)

func main() {
    // 初始化
    searcher.Init(types.EngineInitOptions{
        SegmenterDictionaries: "github.com/huichen/wukong/data/dictionary.txt"})
    defer searcher.Close()

    // 将文档加入索引,docId 从1开始
    searcher.IndexDocument(1, types.DocumentIndexData{Content: "此次百度收购将成中国互联网最大并购"}, false)
    searcher.IndexDocument(2, types.DocumentIndexData{Content: "百度宣布拟全资收购91无线业务"}, false)
    searcher.IndexDocument(3, types.DocumentIndexData{Content: "百度是中国最大的搜索引擎"}, false)

    // 等待索引刷新完毕
    searcher.FlushIndex()

    // 搜索输出格式见types.SearchResponse结构体
    log.Print(searcher.Search(types.SearchRequest{Text:"百度中国"}))
}

架構上跟Bleve有點接近, 寫法也差不多, 但效率上來說, Bleve根本不能比, index的效率快上許多, 它的docId不像是Bleve用string而是uint64

比較快的原因目前我也還沒深究, 不過它存儲並沒用到BoltDB或LevelDB之類的, 而是自己的格式, 它也像Bleve一樣支援換資料庫引擎, 我嘗試想用BoltDB來取代它原生的, 想試看看是不是這原因, 但一直沒換成功過(後來也懶得追了)

另一點的差別在於是, 剛剛Bleve我用的斷詞器是Gojieba, 而Wukong用的是陳輝自己寫的sego, 這時我就好奇這會不會有影響?

BleveSego

好, 既然要確認斷詞器對index效率有沒影響, 我就得自己實作一個基於Sego的Bleve text analyzer和Tokenizer, 因此仿Gojieba的做了:

blevesego

使用blevesego跟使用Gojieba的有點類似:

	indexMapping := bleve.NewIndexMapping()
	err := indexMapping.AddCustomTokenizer("sego",
		map[string]interface{}{
			"dictpath": "dictionary.txt",
			"type":     "sego",
		},
	)

	if err != nil {
		getLogger().Fatal(err)
		return nil
	}

	err = indexMapping.AddCustomAnalyzer("sego",
		map[string]interface{}{
			"type":      "sego",
			"tokenizer": "sego",
		},
	)

	if err != nil {
		getLogger().Fatal(err)
		return nil
	}

	newsHelperIndexMapping.DefaultAnalyzer = "sego"

實驗的結果, 同樣Bleve, 用不同的斷詞, 似乎用sego有稍微快一點(在raspberry pi下, 約五千多筆資料大約快個一秒鐘), 但似乎不是Wukong效率高過於Bleve的主因

到最後, 我是選擇了Bleve, 原因是, 它目前看起來比較活躍, 而Wukong就比較沒啥更新

這陣子都在寫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去安裝相對應的套件了