背景
QiitaやはてなブログではMarkdownで記事がかけます。Markdownエディタ(Obsidian)をつかっているので、エディタの保管庫(Vault)に記事のソース(Markdown)を貼り付けておけると便利だろうなとおもうのです。
Markdownは軽量なので、エディタの保管庫にデータを持っていても軽快に動きます。エディタの検索機能で過去に書いた記事が検索できると、自分用ナレッジデータベースとしてはとても便利になりそうです。
なのですが、ソースMarkdownを取得するのに、手を動かさなければならなくて、面倒臭いなあとおもっていました。面倒くさいことはやらなくなります。抜け漏れが出てくると、データベースとしてはイマイチになってしまいます。
そこで、キーボードショートカットで、さっくりとソースMarkdownを取得できるようなスクリプトを書きました。
環境
ここで書いていることは、下記のバージョンで実施しました。
- Alfred 4.6.1
- VS Code 1.63.2
- macOS Monterey 12.1
- MacBook Pro (13-inch, 2020, Four Thunderbolt 3 Ports)
概要
操作の概要
↓ `option + command + E`を押すと といったふうに`[記事タイトル][記事URL]`と記事のソースMarkdownを、クリップボードに取得します。スクリプトの概要
- Alfredを利用して、
option + command + E
で起動します - 選択した部分が
Qiita
なら、curl
コマンドでソースを取得します(シェルスクリプト) - 選択した部分が
はてなブログ
なら、はてなブログのAPIを叩いてソースを取得します(Pythonスクリプト)
詳細
Alfred Workflowの構成
4つのブロックで構成しています。
以下にそれぞれのブロックについて補足します。
トリガー
-
Hotkey
にキーボードショートカットを登録します -
Argument
をSelection in macOS
に変更しています。選択した範囲が、次のブロックのクエリで使えます
スクリプトの実行
-
Language
をzsh
にしています。べつにbash
でもいいですがなんとなく -
with input as {query}
を選択しています。スクリプト内で{query}
と書くと、macOSで選択した部分を使えます(今回の場合はURLを想定)
Qiitaの場合
# 変数・定数をセット
url="{query}"
qiita="qiita.com"
hatena="hatenablog.com"
suffix=".md"
# はてブロの場合とQiitaの場合で処理をわけた
if [[ "$url" == *$hatena* ]]; then
/usr/local/bin/python3.9 220117_copyHatenaSource.py $url
elif [[ "$url" == *$qiita* ]]; then
curl -s $url | grep -o '<title>.*</title>' | sed 's#<title>\(.*\)</title>#\1#' | pbcopy
title=`pbpaste`
echo "[$title]($url)\n"
curl "$url$suffix"
else
exit
fi
- 選択した範囲のURLがはてなブログであるなら、
$url
(macOSでの選択範囲)を引数にして、Pythonスクリプトを起動します → Pythonスクリプトは後述します - 選択した範囲のURLがQiitaなら、シェルコマンドを走らせます
curl -s $url | grep -o '<title>.*</title>' | sed 's#<title>\(.*\)</title>#\1#' | pbcopy
title=`pbpaste`
echo "[$title]($url)\n"
curl "$url$suffix"
- まず1つ目の
curl
でタイトルを取得します-
<title>
タグで囲まれているところをgrep
で抽出し、sed
でタグを消してます - 改行が含まれているとダメとか、
[]
が含まれているとダメとかありますが、ここではあまり細かいことは気にしません(それぐらいは手で直せばよいのではという考え)
-
-
[記事タイトル](記事URL)
を標準出力に吐いておきます - Qiitaのばあいは、
記事URL.md
でMarkdownが取れます。2つ目のcurl
でMarkdownを取得しています
curl
でタイトルを抜き出す方法は、下記の記事を参考にさせていただきました(なかでも簡易な方法を使いました)
- HTMLコンテンツをcurlで取得して特定タグ内だけ抜き出す - Qiita
- ウェブページのタイトルをターミナルから取得する方法
- Bash で HTML のタイトルを取得する - Sarchitect
はてなブログの場合
/usr/local/bin/python3.9 220117_copyHatenaSource.py $url
のところで、Pythonスクリプトを起動しています(引数にはmacOSの選択範囲)。
スクリプトは、Alfred Workflowのフォルダに入れておきます(Workflowリストを右クリック > Open in Finder...
が便利)。
スクリプトの内容は下記のとおりです。
#!/usr/local/bin python3.9
# -*- coding: utf-8 -*-
import sys
import requests
import bs4
import re
import time
# コレクションURIを取得
def get_collection_uri(hatena_id, blog_id, password):
service_doc_uri = "https://blog.hatena.ne.jp/{hatena_id:}/{blog_id:}/atom".format(hatena_id=hatena_id, blog_id=blog_id)
res_service_doc = requests.get(url=service_doc_uri, auth=(hatena_id, password))
if res_service_doc.ok:
soup_servicedoc_xml = bs4.BeautifulSoup(res_service_doc.content, features="xml")
collection_uri = soup_servicedoc_xml.collection.get("href")
return collection_uri
return False
# エントリIDを指定し、contentを取得
def get_entry_content_str(hatena_id, blog_id, password, entry_id):
member_uri = "https://blog.hatena.ne.jp/{hatena_id}/{blog_id}/atom/entry/{entry_id}".format(hatena_id=hatena_id, blog_id=blog_id,entry_id=entry_id)
res_member = requests.get(member_uri, auth=(hatena_id, password))
if not res_member.ok:
print("Failed: status_code: " + str(res_member.status_code))
return False
soup_response_xml = bs4.BeautifulSoup(res_member.content, features="xml")
return soup_response_xml.find("content").string
# エントリIDを指定し、タイトルを取得
def get_entry_title_str(hatena_id, blog_id, password, entry_id):
member_uri = "https://blog.hatena.ne.jp/{hatena_id}/{blog_id}/atom/entry/{entry_id}".format(hatena_id=hatena_id, blog_id=blog_id,entry_id=entry_id)
res_member = requests.get(member_uri, auth=(hatena_id, password))
if not res_member.ok:
print("Failed: status_code: " + str(res_member.status_code))
return False
soup_response_xml = bs4.BeautifulSoup(res_member.content, features="xml")
return soup_response_xml.find("title").string
# エントリIDを指定し、URL(link)を取得
def get_entry_link_str(hatena_id, blog_id, password, entry_id):
member_uri = "https://blog.hatena.ne.jp/{hatena_id}/{blog_id}/atom/entry/{entry_id}".format(hatena_id=hatena_id, blog_id=blog_id,entry_id=entry_id)
res_member = requests.get(member_uri, auth=(hatena_id, password))
if not res_member.ok:
print("Failed: status_code: " + str(res_member.status_code))
return False
soup_response_xml = bs4.BeautifulSoup(res_member.content, features="xml")
return soup_response_xml.find("link", rel="alternate").get("href")
hatena_id="<hatena_id>"
blog_id="<blog_id>"
password="<password>"
collection_uri = get_collection_uri(hatena_id, blog_id, password)
entry_id_list = []
# 記事一覧を2回(過去記事を20件)取得し、エントリIDのリストを作成
# NOTE: したがって古い記事は取得できない
MAX_ITERATER_NUM = 2
for i in range(MAX_ITERATER_NUM):
# Basic認証で記事一覧を取得
res_collection = requests.get(collection_uri, auth=(hatena_id, password))
if not res_collection.ok:
print("faild")
continue
# Beatifulsoup4でDOM化
soup_collectino_xml = bs4.BeautifulSoup(res_collection.content, features="xml")
# entry elementのlistを取得
entries = soup_collectino_xml.find_all("entry")
# 下書きを無視
pub_entry_list = list(filter(lambda e: e.find("app:draft").string != "yes", entries))
# entry idを取得
entry_id_list.extend([re.search(r"-(\d+)$", string=e.id.string).group(1) for e in pub_entry_list])
# next
link_next = soup_collectino_xml.find("link", rel="next")
if not link_next:
break
collection_uri = link_next.get("href")
if not collection_uri:
break
time.sleep(0.01)# 10ms
# 引数で指定したURLに合致するエントリがあれば、MD形式のリンクとソーステキストを出力
for entry_id in entry_id_list:
# print (entry_id)
link = get_entry_link_str(hatena_id, blog_id, password, entry_id)
# print (entry_id, link, sys.argv[1])
if link == sys.argv[1]:
title = get_entry_title_str(hatena_id, blog_id, password, entry_id)
content = get_entry_content_str(hatena_id, blog_id, password, entry_id)
print ("[" + title + "](" + link + ")\n" )
print ("---\n")
print ("# " + title + "\n")
print (content)
break
モジュールが必要なので、下記コマンドでインストールします。
$ python3.9 -m pip install requests
$ python3.9 -m pip install beautifulsoup4
処理の大まかな流れは下記のとおりです。
- コレクションURIを取得
- 記事一覧(最新10件)を取得し、そのなかからエントリIDを取得(10件)
- 次の10件の記事を取得し、エントリIDを取得(合計20件)
- 20件の記事のうち、macOSで選択したURLと一致する記事を探す
- もし見つかれば、ソースを取得して標準出力に吐く
あまり古い記事を探しに行かないという前提で、記事の取得は20件で止めています。もっと過去の記事を取得する場合は、個別に対応するつもりで書いています。また、パフォーマンス的にベストの方法ではないとおもうんですが、記事の取得件数を抑えているので、改善せずここで止めています。といったところはジェネラルなコードになっていないのでご留意ください。
はてなブログのAPIを叩く方法は、下記の記事を参考にさせていただきました。
また、macOSのPythonは鬼門の方向ですので、macOS環境のPythonを参考にさせていただきました。
- macOSに入っているpythonを使おうとするな
- Homebrewでインストールせよ
- pip3でなくてpython3.9 -m pip xxxとせよ
- venvなど仮想環境で動かせ
とのことです。仮想環境はつかいませんでしたが、モジュールのインストールには$ python3.9 -m pip install xxx
をつかいました。
画面への出力
AlfredのLarge Type
というモジュールが便利なのでつかいました。スクリプトが標準出力に吐いたテキストを、画面に表示します。もともとはデバッグ用なのですけれども、さくっとプレビューできるのは便利なので、消さずに残しています。動作が安定して、ぜったい要らんわとなったら、消そうとおもいます。
クリップボードへのコピー
標準出力に吐いたテキストを、クリップボードにコピーします。エディタ(Obsidian)にペーストすれば一連のタスクは終了です。
おわりに
- Qiitaとはてなブログにしか対応していない
- 選択範囲が適切か否かなど、エラーハンドリングはしていない
- はてなブログは最近20件の記事からしか取得できない
などとおよそジェネリックとは言えないスクリプトですけれども、実運用上はそこそこいい感じかなとおもいます。「判断が複雑なところは人間がやって、単純作業はコンピュータがやる」の比率が、まあまあいいところに落ち着いたのではないかなあと。しばらく使ってみて、手に馴染むかどうか、たしかめてみたいとおもいます。