tag:crieit.net,2005:https://crieit.net/tags/HarperDB/feed
「HarperDB」の記事 - Crieit
Crieitでタグ「HarperDB」に投稿された最近の記事
2021-06-27T09:05:46+09:00
https://crieit.net/tags/HarperDB/feed
tag:crieit.net,2005:PublicArticle/17450
2021-06-27T09:05:46+09:00
2021-06-27T09:05:46+09:00
https://crieit.net/posts/RSS-Slack
スーパー完全無料でRSSをSlackに投稿できるやつを作った
<h2 id="皆さん、どうやって技術ネタ、キャッチアップしてますか?"><a href="#%E7%9A%86%E3%81%95%E3%82%93%E3%80%81%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E6%8A%80%E8%A1%93%E3%83%8D%E3%82%BF%E3%80%81%E3%82%AD%E3%83%A3%E3%83%83%E3%83%81%E3%82%A2%E3%83%83%E3%83%97%E3%81%97%E3%81%A6%E3%81%BE%E3%81%99%E3%81%8B%EF%BC%9F">皆さん、どうやって技術ネタ、キャッチアップしてますか?</a></h2>
<p>皆さんはどうやって日々日進月歩な技術ネタをキャッチアップしてますか?</p>
<p>私はよく企業や個人が書いている技術ブログから情報を得ることが多いです。本当に技術ブログって手軽なのにすごい勉強になりますよね。</p>
<h2 id="皆さん、どうやってブログ記事を通知してますか?"><a href="#%E7%9A%86%E3%81%95%E3%82%93%E3%80%81%E3%81%A9%E3%81%86%E3%82%84%E3%81%A3%E3%81%A6%E3%83%96%E3%83%AD%E3%82%B0%E8%A8%98%E4%BA%8B%E3%82%92%E9%80%9A%E7%9F%A5%E3%81%97%E3%81%A6%E3%81%BE%E3%81%99%E3%81%8B%EF%BC%9F">皆さん、どうやってブログ記事を通知してますか?</a></h2>
<p>ブログ記事確認はもちろん定期的にブログに訪問するのが一番ですが、なかなか時間の取れない中でそれは酷なので何かしら皆さん工夫していると思います。</p>
<p>ブログの更新にあわせてTwitterを更新してくれる企業様であれば、Twitterのフォローをすればいいかもしれませんが、必ずしもそうでもないかもしれませんし、Twitterのフォローには技術以外の話題も飛び交うので、集中して記事を確認することも難しいかもしれません。</p>
<p>そういったときに役立つのがRSSです。RSSとは<strong>R</strong>ich <strong>S</strong>ite <strong>S</strong>ummaryの略で、ニュースやブログなど各種のウェブサイトの更新情報を配信するための仕組みやXMLフォーマットのことです。</p>
<p>RSSの更新を定期的に取得し、記事更新を教えてくれるRSSリーダーは皆さんお世話になっている人も多いのではないでしょうか?</p>
<p>私もGoogle Chromeに拡張としてRSSリーダーを入れていた時期もありました。</p>
<h2 id="問題点"><a href="#%E5%95%8F%E9%A1%8C%E7%82%B9">問題点</a></h2>
<p>RSSリーダーを使って技術ブログの更新を検知する方法はおそらくデファクトスタンダードだと思いますが、個人的にちょっと問題点がありました。</p>
<p>それは、<strong>通勤時間の時間をうまく使ってキャッチアップするのが面倒ということです。</strong></p>
<ul>
<li>携帯にPCと同じRSSを登録するのがめんどくさい</li>
<li>RSSリーダーを開かない
<ul>
<li>電車に乗っているとTwitterやSlackを開いている時間がほぼ全て</li>
<li>Kindleで読書するのも細かく乗り換えがあって中断が多く発生するためストレス</li>
</ul></li>
<li>(RSSリーダーによって違うのかもしれませんが)タイトルを見て中身を判断するのが難しい</li>
</ul>
<p>このような悩みがあるため、私は<strong>Slack</strong>の<strong>/feed</strong>機能を使ってRSSを購読してました。</p>
<p>が、しかしこれもまたもや問題点。Slackの無料ワークスペースには、Appsが10個までしか登録できないのです。(/feedもAppsを消費します)</p>
<p>Slackには他にもAppsをいくつか作って入れているため、実際登録できるRSSは5個くらいになってしまいちょっと心もとない感じになってしまいました。</p>
<h3 id="IFTTTはどうなの?"><a href="#IFTTT%E3%81%AF%E3%81%A9%E3%81%86%E3%81%AA%E3%81%AE%EF%BC%9F">IFTTTはどうなの?</a></h3>
<p>ちょっと詳しい人だと「じゃあIFTTT」はどうなんです?という意見が聞こえてきそうですが結果的にこちらも不採用。</p>
<p>理由は上記とほぼ同じで、無料版だと設定できる数に制限があるためこちらもあえなく不採用。</p>
<p>というより、お金出せよって声が聞こえてきますね。</p>
<h2 id="じゃあ作ろっか"><a href="#%E3%81%98%E3%82%83%E3%81%82%E4%BD%9C%E3%82%8D%E3%81%A3%E3%81%8B">じゃあ作ろっか</a></h2>
<p>ということで、作ります。</p>
<p>要求は次の通りのことを満たす必要があります。</p>
<ul>
<li>無制限にRSSを登録できること</li>
<li>更新がある場合のみSlackに投稿すること</li>
<li>SlackもAppsを消費しないこと(Custom Integration)</li>
<li>できれば内容を要約したものや、OGP画像も一緒に投稿して記事の選別に役立てられる付加機能を作ること</li>
</ul>
<h2 id="feedparser"><a href="#feedparser">feedparser</a></h2>
<p>今回は時間もない中だったのでサクッとPythonで作っていきます。</p>
<p>RSSの購読には<a target="_blank" rel="nofollow noopener" href="https://pythonhosted.org/feedparser/">feedparser</a>を使うと便利です。</p>
<p>RSS2.0だけでなく、Atomや古いRSSの形式でも難なく読み込んでくれます。</p>
<pre><code class="python">import feedparser
entries = feedparser.parse('http://feedparser.org/docs/examples/rss20.xml')
for e in entries:
print(e.title)
print(e.link)
print(e.summary)
</code></pre>
<p>Entry Itemへのアクセスはイテレーターになっているので取り出しもかんたんです。</p>
<p>RSSのEntry Itemの取り出しはこれで進めます。本当にかんたんでありがたい。</p>
<p>さらに便利なのは<strong>published_parsed</strong>という項目がEntry Itemから取れます。</p>
<p>こちら、RSSのpublished_dateをdatetimeオブジェクトにパースしてくれます。</p>
<p>おかげで、フォーマット差分をあまり意識することなく、更新差分チェック実装ができました。</p>
<h2 id="ステート管理"><a href="#%E3%82%B9%E3%83%86%E3%83%BC%E3%83%88%E7%AE%A1%E7%90%86">ステート管理</a></h2>
<p>RSSには記事の作成日付(Publish Date)があり、RSSの取得のたびに差分チェックとして活用することができます。</p>
<p>なので、以前取得した記事のPublish Dateを記憶して、更新があった場合のみ記事を取得するようにしたいのですが、それには何かしらのDB、もしくはデータ保存する仕組みが必要となります。</p>
<p>今回は無料という縛りがあるため、当初はGitHubのレポジトリ上にステートファイルをコミットするようにしようとも思ったのですが、コミットが伸び過ぎてしまうのは色々問題なのでやはりDBを使いたいです。</p>
<h3 id="HarperDB"><a href="#HarperDB">HarperDB</a></h3>
<p>HarperDBは、データ管理を容易にすることに重点を置いた分散型データベースで、ジョインを含むNoSQLとSQLをサポートしています。</p>
<p>NoSQLでSQLがかけるのは便利ですね!!</p>
<p>日本ではあまり聞きませんが、<a target="_blank" rel="nofollow noopener" href="https://dev.to/">dev.to</a>とかだとちょこちょこ話題に上がっております。</p>
<p>こちらのHarperDB、HarperDB Cloud Instanceというマネージドサービスも提供されており、インスタンスタイプを選ぶだけで、手軽にHarperDBを使うことができるようになっております。</p>
<p><img src="https://i.imgur.com/CA1sLCU.png" alt="harperdb" /></p>
<p><img src="https://i.imgur.com/48qXVQw.png" alt="img" /></p>
<p>え?でもお高いんじゃない?そんな声が聞こえてきますね。</p>
<p>なんと、今だけかもしれませんがHarperDB Cloud Instanceの一番最小のInstance構成だと無料で使うことができます!これは嬉しいですね。</p>
<div class="table-responsive"><table>
<thead>
<tr>
<th>Name</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>RAM</td>
<td>0.5GB</td>
</tr>
<tr>
<td>DISK</td>
<td>1GB</td>
</tr>
<tr>
<td>VERSION</td>
<td>3.0.0</td>
</tr>
<tr>
<td>IOPS</td>
<td>3000</td>
</tr>
</tbody>
</table></div>
<p>正直今回の使い方ではこのレベルで十分です。</p>
<p>Python上でのHarperDB操作も<a target="_blank" rel="nofollow noopener" href="https://pypi.org/project/harperdb/">専用のライブラリ</a>が用意されているためかんたんに実装できます。</p>
<pre><code class="python">HARPERDB_URL = os.getenv("HARPERDB_URL")
HARPERDB_USERNAME = os.getenv("HARPERDB_USERNAME")
HARPERDB_PASSWORD = os.getenv("HARPERDB_PASSWORD")
HARPERDB_SCHEMA = os.getenv("HARPERDB_SCHEMA", "prd")
FILEPATH = "entry.csv"
db = harperdb.HarperDB(
url=HARPERDB_URL,
username=HARPERDB_USERNAME,
password=HARPERDB_PASSWORD,)
test = db.search_by_hash(HARPERDB_SCHEMA, "last_published", [name], get_attributes=["time"])
for t in test:
print(t["time"])
</code></pre>
<p>このようにNoSQLライクにHash Attributeを使って検索する感じで実装できます。もちろんValue引きも可能です。(遅くなるのかは不明だがNoSQLなら全走査になりそうなので多分遅い)</p>
<p>UpdateやInsertも同様な感じで実施できます。</p>
<pre><code class="python">ef insert_last_published(name: str):
db.insert(HARPERDB_SCHEMA, "last_published", [{"name": name, "time": 123456789}])
return 123456789
def update_last_published(name: str, time: int):
result = db.update(HARPERDB_SCHEMA, "last_published", [{"name": name, "time": time}])
return result
</code></pre>
<p>また、便利だなと思ったのはやはりSQLでの走査です。</p>
<pre><code class="python">def get_entry_urls():
return [{"name": x["name"],
"url": x["url"],
"icon": x["icon"]} for x in db.sql(f"select * from {HARPERDB_SCHEMA}.entry_urls")]
</code></pre>
<p>といった具合にテーブルの*Selectやジョインなんかも書くことができます。テーブル全体をなめたいとき、これは楽でいいですね。</p>
<p>また、CSV load機能もあり、CSVをHarperDBに食わせることもできちゃったりします。</p>
<p>今回はこちらの機能はRSSのEntryURL登録機能として便利に使用させていただきました。</p>
<pre><code class="python">import os
import harperdb
HARPERDB_URL = os.getenv("HARPERDB_URL")
HARPERDB_USERNAME = os.getenv("HARPERDB_USERNAME")
HARPERDB_PASSWORD = os.getenv("HARPERDB_PASSWORD")
HARPERDB_SCHEMA = os.getenv("HARPERDB_SCHEMA", "prd")
FILEPATH = "entry.csv"
db = harperdb.HarperDB(
url=HARPERDB_URL,
username=HARPERDB_USERNAME,
password=HARPERDB_PASSWORD,)
db.csv_data_load(HARPERDB_SCHEMA, "entry_urls", FILEPATH, action="upsert")
</code></pre>
<p>無料開発で一番ネックになるのがDBですが、正直これだけで大概のアプリは作れてしまうのではないでしょうか?</p>
<h2 id="OGP画像を得るには?"><a href="#OGP%E7%94%BB%E5%83%8F%E3%82%92%E5%BE%97%E3%82%8B%E3%81%AB%E3%81%AF%EF%BC%9F">OGP画像を得るには?</a></h2>
<p>OGPとは<strong>O</strong>pen <strong>G</strong>raph <strong>P</strong>rotocolの略で、TwitterやFacebookにURLリンクを貼り付けると出てくるあれです。</p>
<p><img src="https://i.imgur.com/4LAaL3b.png" alt="img" /></p>
<p>実際OGP作成を実装された方ならわかりますが、OGPはHTMLのHeaderに決まりきったmetaタグを記載して表現しております。</p>
<pre><code class="html"><meta property="og:type" content="article" data-react-helmet="true">
<meta property="og:url" content="https://blog.tubone-project24.xyz/2021/01/01/mqtt-nenga" data-react-helmet="true">
<meta property="og:title" content="MQTTと電子ペーパーを使って年賀状を作る" data-react-helmet="true">
<meta property="og:description" content="年賀書きたくないマン Table of Contents 一年の計は元旦にあり 注意 年末年始はやってみようBOX MQTT React Hooks Tailwind CSS 電子ペーパー やらないことにしようBOX アーキテクチャー 辛かったこと Hooks…" data-react-helmet="true">
<meta property="og:image" content="https://i.imgur.com/tmkmoVA.png" data-react-helmet="true">
<meta name="twitter:title" content="MQTTと電子ペーパーを使って年賀状を作る" data-react-helmet="true">
<meta name="twitter:description" content="年賀書きたくないマン Table of Contents 一年の計は元旦にあり 注意 年末年始はやってみようBOX MQTT React Hooks Tailwind CSS 電子ペーパー やらないことにしようBOX アーキテクチャー 辛かったこと Hooks…" data-react-helmet="true">
<meta name="twitter:image" content="https://i.imgur.com/tmkmoVA.png" data-react-helmet="true">
</code></pre>
<p>Slackのattachmentsに入れる画像はOGPのImageから取るようにします。</p>
<h3 id="opengraph-py3"><a href="#opengraph-py3">opengraph-py3</a></h3>
<p>PythonでOGPを解析するなら<a target="_blank" rel="nofollow noopener" href="https://pypi.org/project/opengraph_py3/">opengraph</a>ライブラリが便利です。ただし、</p>
<pre><code>pip install opengraph
</code></pre>
<p>でインストールするとPython2用のライブラリがインストールされてしまいまともに動かないので、</p>
<pre><code>pip install opengraph_py3
</code></pre>
<p>でインストールするようにします。</p>
<p>使い方もかんたんで、<strong>opengraph_py3.OpenGraph</strong>でインスタンスを作ってあげれば、<strong>ogp["image"]</strong>にOGPイメージURLが保存されます。</p>
<p>一点注意としてopengraphは裏でBeautifulSoapが動いているようで、Headerのないページに対してOGPを取得しようとするとAttributeErrorが出てしまうので例外処理を入れております。</p>
<p>本家にPR出すか迷いましたが、2017年から更新がないので骨折り損になりそうなので、やめておきます。</p>
<pre><code class="python">import opengraph_py3
def get_ogp_image(link: str):
try:
ogp = opengraph_py3.OpenGraph(url=link)
if ogp.is_valid():
return ogp["image"]
else:
return ""
except AttributeError as e:
logger.debug(f"No Head contents: {e}")
return ""
</code></pre>
<h2 id="Favicon"><a href="#Favicon">Favicon</a></h2>
<p>できれば、Slack投稿するときに技術ブログのアイコンをブログごとに変えたいなと思ったので、Faviconを取る実装も入れます。</p>
<p>Pythonにはfavicon取るためのライブラリ<a target="_blank" rel="nofollow noopener" href="https://pypi.org/project/favicon/">favicon</a>があります。</p>
<p>使い方も超かんたんで、<strong>favicon.get</strong>で取得したオブジェクトの配列0番目が一番大きなfaviconなのでそれを取るだけです。</p>
<pre><code class="python">import favicon
def get_favicon(link):
icons = favicon.get(link)
if len(icons) == 0:
return ""
else:
return icons[0].url
</code></pre>
<h2 id="キーワード抽出"><a href="#%E3%82%AD%E3%83%BC%E3%83%AF%E3%83%BC%E3%83%89%E6%8A%BD%E5%87%BA">キーワード抽出</a></h2>
<p>さて、今回の醍醐味のキーワード抽出ですがこちらもかんたんに実装できます。</p>
<p><a target="_blank" rel="nofollow noopener" href="http://gensen.dl.itc.u-tokyo.ac.jp/pytermextract/">pytermextract</a>という専門用語抽出ツールと形態素解析ライブラリ<a target="_blank" rel="nofollow noopener" href="https://mocobeta.github.io/janome/">janome</a>を組み合わせることでかんたんに実現できます。</p>
<p>janomeは本当に便利で、特にCIに乗っけてぐるぐるしたい人にはmecabをインストールする必要も辞書をコンパイルする必要もなく、pipで一発入れれば使えるので重宝しています。</p>
<p>pytermextractはPyPI登録されているライブラリではないのでインストールは公式サイトから落としたZIPを展開しsetup.pyから行います。</p>
<p>また、janomeもpipでインストールします。</p>
<pre><code class="shell">unzip pytermextract-0_01.zip
cd pytermextract-0_01
python setup.py install
pip install janome
</code></pre>
<p>まずは、キーワード抽出したいテキストをjanomeのTokenizerにかけて、結果を頻出度から単名詞の左右の連接情報スコア(LR)を算出し、</p>
<p>重要度スコアとしてはじき出す、という仕組みらしいです。とは言っても私にはよくわからなったのでサンプルコード丸パクリです。</p>
<p>得られる結果は<strong>{"単語": スコア}</strong>となってますので、こちらをスコア順にリバースソートして上位6位を取得する形にしました。</p>
<p>しょうもない知識ですが、janomeのTokenizerインスタンス作るところは処理コストがちょっと高いので、リファクタでモジュールトップレベルでの宣言にしてます。</p>
<pre><code class="python">from janome.tokenizer import Tokenizer
import termextract.janome
import termextract.core
t = Tokenizer()
def extract_keyword(text):
tokenize_text = t.tokenize(text)
frequency = termextract.janome.cmp_noun_dict(tokenize_text)
lr = termextract.core.score_lr(
frequency,
ignore_words=termextract.janome.IGNORE_WORDS,
lr_mode=1, average_rate=1)
term_imp = termextract.core.term_importance(frequency, lr)
score_sorted_term_imp = sorted(term_imp.items(), key=lambda x: x[1], reverse=True)
logger.debug(f"keywords: {score_sorted_term_imp}")
return score_sorted_term_imp[:6]
</code></pre>
<h3 id="RSSのSummaryTextでは精度がでない、そりゃそうじゃ。"><a href="#RSS%E3%81%AESummaryText%E3%81%A7%E3%81%AF%E7%B2%BE%E5%BA%A6%E3%81%8C%E3%81%A7%E3%81%AA%E3%81%84%E3%80%81%E3%81%9D%E3%82%8A%E3%82%83%E3%81%9D%E3%81%86%E3%81%98%E3%82%83%E3%80%82">RSSのSummaryTextでは精度がでない、そりゃそうじゃ。</a></h3>
<p>見出し通りですが、当初はfeedparserから取得できるEntry ItemのSummaryをpytermextractに食わせてましたが、SummaryTextが短すぎて全く期待する動作になりませんでしたので、BeautifulSoupを使って、実際の記事の本文を取得しpytermextractに食わせる実装に変更しました。</p>
<pre><code class="python">from bs4 import BeautifulSoup
import urllib.request as req
def extract_html_text(url):
res = req.urlopen(url)
soup = BeautifulSoup(res, "html.parser")
p_tag_list = soup.find_all("p")
return " ".join([p.get_text() for p in p_tag_list])
</code></pre>
<p>本文はpタグと判断しfind_allするちんけな実装です。ごめんなさい。</p>
<h2 id="Slack投稿"><a href="#Slack%E6%8A%95%E7%A8%BF">Slack投稿</a></h2>
<p>いよいよSlack投稿部分の作成です。</p>
<p>Slack投稿はCustomIntegrationのIncoming Webhookで作ります。</p>
<p>なので、<a target="_blank" rel="nofollow noopener" href="https://api.slack.com/reference/messaging/attachments">Slack attachment</a>が使えます。</p>
<p>特質したことはないのですが、OGP画像はimage_urlに、faviconはauthor_imageにキーワードはfieldsに入れてます。</p>
<h2 id="GitHub Actions化"><a href="#GitHub+Actions%E5%8C%96">GitHub Actions化</a></h2>
<p>最後にGitHub Actionsに載せて、定期実行させます。</p>
<p>その前にの<a href="#harperdb">#harperdb</a>でも書いたとおり、RSS追加時のHarperDBへのEntry追加の定義を書いていきます。</p>
<p>特定のファイルに更新があった場合のみ動くGitHub Actionsを作る場合は、 on_pushなどの条件にpathsを入れることで実現できます。これだけです。</p>
<pre><code class="yml">on:
push:
branches:
- main
paths:
- "entry.csv"
pull_request:
branches:
- main
paths:
- "entry.csv"
</code></pre>
<p>また、定期実行にはschedule cronが便利です。</p>
<pre><code class="yml">on:
push:
branches:
- main
pull_request:
branches:
- main
schedule:
- cron: "*/30 * * * *"
</code></pre>
<h2 id="完成"><a href="#%E5%AE%8C%E6%88%90">完成</a></h2>
<p>ということでできました。</p>
<p><img src="https://i.imgur.com/Ip4IaYs.png" alt="img" /></p>
<p>entry.csvに書いたRSS feedを30分ごとに確認しにいき、前回よりpublish_dateの更新があったばあいはOGP, favicon, キーワード付きでSlack投稿します。</p>
<p>レポジトリはこちらです。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://github.com/tubone24/tech_blog_spider">https://github.com/tubone24/tech_blog_spider</a></p>
<p>ForkするとGitHubA ctionsがうまく発火しないっぽいので、もし利用する際はgit cloneして自身のレポジトリに再Pushして使っていただければと思います。</p>
<h2 id="結論"><a href="#%E7%B5%90%E8%AB%96">結論</a></h2>
<p>HarperDBを使って何でもつくれそうな予感がするこの頃です。</p>
tubone24