ぷらすのブログ

複数のRSSや任意の情報をまとめて1つのRSSやJSONを生成するライブラリを作成した

#開発#Go#RSS#OSS

こんにちは @p1ass です。

皆さん RSS 使ってますか?RSS を使えば簡単にブログの更新を受け取れたりして便利ですよね。

でも、流れてくる情報多くて辛かったり、RSS に対応していないサイトの情報を受け取りたかったりすることがたまにありませんか?

そんな悩みを解決するための Go のライブラリを 1 年前に作ったのですが、ブログに書く機会を逃していたので、今更ですが紹介記事を書きます。

発表スライドはこちらです。

背景

RSS はニュースやブログなど各種ウェブサイトの更新情報を配信するための文書フォーマットです。ブログのタイトルや URL、公開日などが xml 形式で記述されています。

RSS は広く使われているフォーマットで、はてなブログや Qiita、Zenn はデフォルトで対応しています。(似た形式の Atom の場合もある)

他にも、GCP のリリースノートYouTube チャンネルの更新情報なども RSS で配信されています。

これらを RSS リーダーであるFeedlyや Slack の RSS アプリに登録すれば、記事の更新を素早くキャッチできます。

しかし、個人的に RSS にはいくつか辛い点がありました。

こういった辛みをいい感じに解決したいと考えてライブラリを作成しました。

p1ass/feeder

作成したライブラリはp1ass/feederです。

ロゴ それっぽいロゴを作った

基本的な使い方はこちらの例を見ればなんとなく掴めると思います。

func crawl(){
	rss1 := feeder.NewRSSCrawler("https://example.com/rss1")
	rss2 := feeder.NewRSSCrawler("https://example.com/rss2")

	items, err := feeder.Crawl(rss1, rss2)

	feed := &feeder.Feed{
		Items:       items,
		// 細かいパラメータは省略
	}

	rss, err := feed.ToRSS() // rss is string
	rssReader, err := feed.ToRSSReader() // jsonReader is a io.Reader
}

エラーハンドリングをきちんとしていない点に注意してください

feeder.NewRSSCrawler() でクローラーを作成し、feeder.Crawl() に渡すことで記事の一覧を取得しています。 その後、 *feeder.Feed 構造体を作成すると、RSS や JSON、Atom を生成できます。 後はそれをファイルに保存したり、HTTP で配信したりと好きなように使えます。

また、feeder では次のようなこともできます。

独自にインタフェースを満たした構造体を作ることで、任意のサイトの情報からフィードを作成

feeder では Crawler というインターフェースを提供しています。

type Crawler interface {
	Crawl() ([]*Item, error)
}

この Crawler は feeder.Crawl(crawlers ...Crawler) のように Crawl() 関数に渡すことができます。

つまり、この Crawler インターフェースを満たす構造体を作成すれば、任意の情報から RSS フィードを作成てきるようになります。

例としてサマソニのチケットサイトをクロールする SamasoniCrawler のコードを載せておきます。

コード
func (crawler *SamasoniCrawler) Crawl() ([]*feeder.Item, error) {
query := url.Values{}
query.Add("perform_id", "85895")
query.Add("sort_key", "sale_start_at")
query.Add("sort_order", "asc")
res, err := http.Get(crawler.url + "?" + query.Encode())
if err != nil {
return nil, errors.New("failed to get html document")
}
defer res.Body.Close()
doc, err := goquery.NewDocumentFromReader(res.Body)
if err != nil {
return nil, errors.New("failed to read from response body")
}

sec := doc.Find("div#tickets").Find("div.list-ticket")
items := make([]*feeder.Items)
sec.Each(func(index int, s *goquery.Selection) {
if s.HasClass("list-ticket") {
title := s.Find("h2").Find("a").Text()
path, _ := s.Find("h2").Find("a").Attr("href")
t := time.Now()
item := &feeder.Item{Title: title, Link: &feeder.Link{Href: "https://tiketore.com" + path,
Rel: "", Type: "", Length: ""},
Id:      path,
Created: &t,
}
items = append(items, item)
}
})
return items, nil
}

これと Slack の RSS の通知に登録すれば、チケットが販売されたタイミングで通知を受け取ることができるようになります。便利ですね。1

出力は RSS だけでなく、Atom や JSON にも対応

出力は RSS だけでなく Atom や JSON にも対応しています。

json, err := feed.ToJSON() // json is string
atom, err := feed.ToAtom() // atom is string

jsonReader, err := feed.ToJSONReader() // jsonReader is a io.Reader
atomReader, err := feed.ToAtomReader() // jsonReader is a io.Reader

私はこれを使ってブログの記事一覧を返す JSON API を建てており、ポートフォリオサイトに API 経由で取得した情報を載せています。

RSS の記事をフィルタして新たな RSS を作成

記事情報は []*feeder.Item というスライスになっているので、スライスの中から必要な情報のみをフィルターする関数を作れば OK です。

func filterIfTitleContainsGo(items []*feeder.Item) []*feeder.Items {
	filtered = make([]*filter.Item, 0, len(items))
	for _, item := range items {
		if strings.Contains(item.Title, "Go") {
			filtered = append(filtered, item)
		}
	}
	return filtered
}

終わりに

p1ass/feeder を使えば RSS を自由自在に扱えます。
色んな用途に使えるライブラリだと思うので、良かったら使ってみてください。 スターも待ってます。

Footnotes

  1. Slack の RSS 更新間隔では到底チケット戦争に勝てることはなかった。

← ISUCON10の予選でFAILして学生枠での本戦出場を逃したCaddyfileを分割する方法 →
Topへ戻る