notebook

都内でWEB系エンジニアやってます。

はてなブログ、フォトライフのAPIを使って投稿を自動化する

ふと思い立ってめちゃくちゃ今更だけど当ブログ(はてなブログ)への記事投稿に関わる作業を自動化した

自分は現在MkDocsではてなブログに書く記事、下書き、ただのメモ書きとすべて保存してプライベートリポジトリに持っています(後で見返す時用にいつもMkDocsをローカルに起動している)

そこから記事にするものを書き上げてWebのUIでコピペして画像をフォトライフに上げてっていう感じのだいぶ面倒な作業をしていました

ライブラリなども軽く調べてみたものの画像とセットでやってくれるようなものが見つからなかったので自分で書くかということになった

「はてなブログに記事上げるのを自動化しました」という記事はそれなりにあるが画像も含めて上げたみたいな記事が見つからなかったので残しておく

はてなのAPI

OAuth認証で認証してXMLを送ることで記事の取得や投稿などができる

だいたいWSSEでやりましたーって記事が多い

というか更新履歴見たらほとんど更新されてないんですね。。。

Consumer key を取得して OAuth 開発をはじめよう - Hatena Developer Center

developer.hatena.ne.jp

↑を読んで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

developer.hatena.ne.jp

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

developer.hatena.ne.jp

# 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を貼っておきます

はてなブログのAPIを使って画像も一緒にポストするサンプル

gist.github.com

おまけ

色々書いたもののこの記事をアップしようとしたらXMLパースエラーで投稿できなかったw

コードにXML(HTMLも?)が含まれている場合にAPIで投稿できないようだったので後で調べてみる。。。