ふと思い立ってめちゃくちゃ今更だけど当ブログ(はてなブログ)への記事投稿に関わる作業を自動化した
自分は現在MkDocsではてなブログに書く記事、下書き、ただのメモ書きとすべて保存してプライベートリポジトリに持っています(後で見返す時用にいつもMkDocsをローカルに起動している)
そこから記事にするものを書き上げてWebのUIでコピペして画像をフォトライフに上げてっていう感じのだいぶ面倒な作業をしていました
ライブラリなども軽く調べてみたものの画像とセットでやってくれるようなものが見つからなかったので自分で書くかということになった
「はてなブログに記事上げるのを自動化しました」という記事はそれなりにあるが画像も含めて上げたみたいな記事が見つからなかったので残しておく
はてなのAPI
OAuth認証で認証してXMLを送ることで記事の取得や投稿などができる
だいたいWSSEでやりましたーって記事が多い
というか更新履歴見たらほとんど更新されてないんですね。。。
Consumer key を取得して OAuth 開発をはじめよう - Hatena Developer Center
↑を読んでConsumerKeyとSecretを取得しそれぞれ環境変数に埋め込んでおく
require 'oauth' header = { 'Accept' => 'application/xml', 'Content-Type' => 'application/xml' } consumer = OAuth::Consumer.new( ENV['HATENABLOG_CONSUMER_KEY'], ENV['HATENABLOG_CONSUMER_SECRET'], site: args[:site], # ブログのドメイン timeout: 300 # timeout値を上書き ) access_token = OAuth::AccessToken.new( consumer, ENV['HATENABLOG_ACCESS_TOKEN'], ENV['HATENABLOG_ACCESS_TOKEN_SECRET'] )
重めの画像などをアップロードするのに時間が足りずタイムアウトしてしまう場合があるためオプションとして渡して上書きしている
OAuthモジュールのデフォルトは30だった
oauth-ruby/consumer.rb at master · oauth-xx/oauth-ruby
画像の投稿
はてなフォトライフのAPIを使います
はてなフォトライフAtomAPI - Hatena Developer Center
require 'base64' require 'mime/types' require 'active_support/core_ext/hash' filename = 'docs/hoge/fuga.md' dirname = 'Hatena Blog' # はてなブログで画像を上げるときのデフォルトディレクトリ content = Base64.encode64(File.read(filename)) mime_type = MIME::Types.type_for(filename).first title = File.basename(filename, '.*') body = <<-"ENTRY" <entry xmlns="http://purl.org/atom/ns#"> <title>#{title}</title> <content mode="base64" type="#{mime_type}"> #{content} </content> <dc:subject>#{dirname}</dc:subject> </entry> ENTRY res = access_token.request(:post, '/atom/post', body, header)
bc:subject
でアップロードするディレクトリを指定できる
はてなブログで使う場合はHatena Blog
という文字列でディレクトリがあるので指定した
指定がないとトップレベルのディレクトリにアップロードされる
記事の投稿
はてなブログAtomPubAPIを使います
はてなブログAtomPub - Hatena Developer Center
# data[:categories] = ['Angular', 'Ruby' .....] require 'oga' categories = data[:categories].map do |c| '' end.join user = 'swfz' site = 'dummy-sample.hatenablog.com' # ブログのドメイン title = '○○について' draft = 'yes' # or 'no' markdown_content = File.read('docs/hoge/fuga.md') body = <<-"EOS" <?xml version="1.0" encoding="utf-8"?> <entry xmlns="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app"> <title>#{title]}</title> <author><name>#{user}</name></author> <content type="text/plain"> #{markdown_content} </content> <updated>#{Time.now.strftime('%Y-%m-%dT%H:%M%S')}</updated> #{categories} <app:control> <app:draft>#{draft}</app:draft> </app:control> </entry> EOS url = "https://blog.hatena.ne.jp/#{user}/#{site}/atom/entry" res = access_token.request(:post, url, Oga.parse_xml(body).to_xml, header)
request時にXMLをparseしてXMLに戻しているのは記事中にHTMLが混ざった場合に最終的にエスケープされた状態にする必要があるため
で、肝心のmarkdown_content
を本来ならゴニョゴニョして投稿する感じですね
記事の中身を変える
画像のタグの変換、広告タグの差し込みを行う
こんな感じ
markdown = File.read(filename) # はてフォトライフの画像へ差し替え markdown.gsub!(/!\[.*\]\((.*)\)/) { post_image($1, filename) } # 広告タグの差し込み markdown + append_ad_content(entry_json[:category])
フォトライフの画像へ差し替え
ローカルで確認しているときは![alt](hoge.png)
でローカルの画像でもを表示できるがそのまま上げてしまうと参照できなくなってしまうのでフォトライフに上げた画像のタグを差し替える
post_inmage
でイメージをアップしてレスポンスをパースしてはてなで参照できるイメージタグを返します
![alt](hoge.png)
-> [f:id:swfz:20190829141024p:image]
というような変換が行われます
広告タグの差し込み
カテゴリ毎にいくつかのタグを用意しておき、投稿対象のカテゴリにひもづくタグからランダムでいくつか引っ張り記事の最後に挿入する
毎度アソシエイトにログインしてどれにしようかなーなんて見ながらやっているといつの間にかそれなりに時間使ってたりしまっていた
正直この作業が一番だるいなと思っていたのでこのあたり何も考えなくて良くなったのが個人的には一番ありがたい
サンプルコード量を増やしたくなかったのでGistに上げたスクリプトからは削りました
カテゴリでやっていることと同じような処理をしてmarkdownの最後に追加してるだけです
記事カテゴリの調整
個人(MkDocs)の都合だがディレクトリ名にカテゴリを含めるようになっているので単一のカテゴリしか表現できない
jsonで管理するのも基本自家製のcli経由なのでマルチバイト文字はあまり入れたくない
ということで管理上持っておくカテゴリ名とブログで表示するカテゴリ名のマッピングを作った
こちらは増えたらその時マッピングを増やす対応みたいな感じになるがまぁしょうがない。。。
状態の保存
一度投稿した記事や画像とローカルファイルとのマッピングを保持しておかないと新規登録しかできなくなってしまいます
もともとMarkdownをこんなかんじのJSONで管理しているのでAPIのレスポンスを元にJSONを更新して保存するようにした
[ { "category" : [ "circleci", "cypress" ], "filename" : "docs/circleci/with_cypress_performance_tuning.md", "title" : "Cypress + CircleCIの高速化Tips", "hatena" : { "image" : { "with_cypress_performance_tuning01.PNG" : { "syntax" : "[f:id:swfz:20190831193017p:image]", "id" : "tag:hatena.ne.jp,2005:fotolife-swfz-20190831193017", "image_url" : "https://cdn-ak.f.st-hatena.com/images/fotolife/s/swfz/20190831/20190831193017.png" }, "with_cypress_performance_tuning02.PNG" : { "syntax" : "[f:id:swfz:20190831193027p:image]", "id" : "tag:hatena.ne.jp,2005:fotolife-swfz-20190831193027", "image_url" : "https://cdn-ak.f.st-hatena.com/images/fotolife/s/swfz/20190831/20190831193027.png" } }, "id" : "26006613412826174" } } ]
一枚のスクリプトにまとめてみる
記事用に一枚のスクリプトにまとめてみました
書き捨て感覚で書いたのでもし流用する場合はよしなに改善していただければ。。。
で、試し用に新たにブログを作って過去の記事を上げてみた
- entries.json
"cypress" ], "filename" : "docs/circleci/with_cypress_performance_tuning.md", - "title" : "Cypress + CircleCIの高速化Tips" + "title" : "Cypress + CircleCIの高速化Tips", + "hatena" : { + "image" : { + "with_cypress_performance_tuning01.PNG" : { + "syntax" : "[f:id:swfz:20190831193017p:image]", + "id" : "tag:hatena.ne.jp,2005:fotolife-swfz-20190831193017", + "image_url" : "https://cdn-ak.f.st-hatena.com/images/fotolife/s/swfz/20190831/20190831193017.png" + }, + "with_cypress_performance_tuning02.PNG" : { + "syntax" : "[f:id:swfz:20190831193027p:image]", + "id" : "tag:hatena.ne.jp,2005:fotolife-swfz-20190831193027", + "image_url" : "https://cdn-ak.f.st-hatena.com/images/fotolife/s/swfz/20190831/20190831193027.png" + } + }, + "id" : "26006613412826174" + } }
API経由で上げた記事のIDと画像のID、URLがJSONに保存されました
記事を修正して上げなおそうとした場合はすでにあるIDを取得してPUTするようになります
画像はタグだけ取得して再アップロードしないようにします
まぁ自分で使うツールだしとりあえず動くというところまででなんかモチベーションが上がらなくなってしまったのでいったんはこれで終わり
アイキャッチ画像の選択
API経由だと指定できなそうでした
記事の最初に挿入した画像が基本的にはアイキャッチになるようなので記事の書き方を工夫すればなんとかなるかも。。?
まとめ
最終的に完全自動化はできないなーということになったものの一番面倒な作業は自動化できたのでとりあえすは良かった
そもそも完全自動化できないなら他の静的サイトジェネレーターでやればいいじゃんとか思ったものの
独自ドメインもとってないから今更乗り換えるのもなーというのと移行にもそれなりに時間掛かりそうというネガティブな理由でこのまま続けることにした
MkDocs自体はカスタムしすぎたせいか最新バージョンで動かなくなってしまっているので時間とやる気を見つけて他のツールに移行したい
今回記事にまつわる部分に関してはだいたい自動化できた(できなかった部分は記事やメモの管理方法によるもの)ため、ちょっとアイデア出して自動投稿のブログを作って運用してみたいなと思います
どんどん記事書いていくぞー!
最後にサンプルのGistを貼っておきます
おまけ
色々書いたもののこの記事をアップしようとしたらXMLパースエラーで投稿できなかったw
コードにXML(HTMLも?)が含まれている場合にAPIで投稿できないようだったので後で調べてみる。。。