ハイパーマッスルエンジニア

Vim、ShellScriptについてよく書く

はてなAPIをcurlでサクッと実行する

とりあえずはてなAPIをサクッとshellで実行したい人に向けて。

自分の記事を取得する

curl -u {はてなID}:{APIキー} https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry

はてなAPIはOAuth認証、WSSE認証、Basic認証のいずれかを行う必要があるので、curlで一番楽なBasic認証を使用する。
curlでのBasic認証はcurl -u ユーザーID:ユーザーPASSでいけますね。

はてなID、ブログID、APIキーは「はてぶのダッシュボード > 設定 > 詳細設定 > AtomPub」から取得できる。

f:id:rasukarusan:20181227154724p:plain
ダッシュボード>設定>詳細設定>AtomPub

記事の取得のエンドポイントは/entryなので、 実際に記事を取得するときは以下のような感じ。

curl -u hatena_id:XXXXXXXXX https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry

これで最新10件の記事を取得できる。
ドキュメントには最新7件と書いてあるが、現在は10件取得できるようだ。

スクリプトにしておくと楽。 hatena.sh

#!/bin/sh

API_KEY='XXXXXXXX'
HATENA_ID='hatena_id'
BLOG_ID='blog_id'

ENDPOINT_ROOT="https://blog.hatena.ne.jp/${HATENA_ID}/${BLOG_ID}/atom"
ENDPOINT_ENTRY='/entry'

# 記事の取得
curl -su ${HATENA_ID}:${API_KEY} ${ENDPOINT_ROOT}${ENDPOINT_ENTRY}

特定の記事を取得する

自分の書いた特定の記事を取得したい場合、/entry/エントリーIDで取得する。
ただ、このエントリーIDの取得が少々面倒くさく、/entryで叩いたときのレスポンスからしか取得できない。
/entryのレスポンス内の <link rel="edit" href="https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry/10257846132688559263"/>10257846132688559263の部分。これがエントリーID。

なのでcurlで叩くとしたらこう。

# 特定の記事を取得
curl -su ${HATENA_ID}:${API_KEY} ${ENDPOINT_ROOT}${ENDPOINT_ENTRY}/10257846132688559263

また、/entryでは最新10件しか取得できないので10件目以降を取得するには/entry?page={ページID}で取得する必要がある。
このページIDも/entryを叩いたときのレスポンスからしか取得できない。

/entryのレスポンス

<link rel="first" href="https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry" />
<link rel="next" href="https://blog.hatena.ne.jp/{はてなID}/{ブログID}/atom/entry?page=1536762675" />

ページIDを指定して取得するとしたらこう

# 2ページ目の記事一覧を取得
curl -su ${HATENA_ID}:${API_KEY} ${ENDPOINT_ROOT}${ENDPOINT_ENTRY}?page=1536762675

少しまとめると

  • 特定の記事を取得したい場合、その記事のエントリーIDが必要になる
  • エントリーIDは/entryのレスポンスからしか取得できない
  • 1ページ目以降の記事を取得したい場合、ページIDを指定する必要がある
  • ページIDは/entryのレスポンスからしか取得できない

ちょっと面倒くさく感じるけど/entryのレスポンスに欲しい情報が全部詰まってるから楽っちゃ楽かな。他のAPI叩く必要がない。

全ての記事のエントリーIDを取得する

もうここからは単純にシェル芸になる。
流れとしては

  1. /entryを叩く
  2. 各記事のエントリーIDを抽出
  3. 2ページ目もあればページIDを抽出
  4. /entry?page=ページIDを叩く
  5. 2~4を繰り返す
#!/bin/sh 

# ローカルにアカウント情報をまとめたファイルを置いているのでそこから取得
# 別に直接書いても問題ない
API_KEY=`cat ~/account.json | jq -r .hatena.api_key`
HATENA_ID=`cat ~/account.json | jq -r .hatena.user_id`
BLOG_ID=`cat ~/account.json | jq -r .hatena.blog_id`

ENDPOINT_ROOT="https://blog.hatena.ne.jp/${HATENA_ID}/${BLOG_ID}/atom"
ENDPOINT_ENTRY='/entry'

# 全記事のエンドポイントを取得する
function getEntryId() {
    page=`curl -su ${HATENA_ID}:${API_KEY} ${ENDPOINT_ROOT}${ENDPOINT_ENTRY}?page=$1`

    # 各記事のエンドポイントを取得
    # 極稀にgrepでBinary file (standard input) matchesと表示され処理が止まるのを防ぐため-aをつける
    echo "$page" \
    | grep -a 'link rel="edit"' \
    | grep -oP 'href=".*"' \
    | sed 's/href="//g' \
    | tr -d '"'

    # 次のページがある場合、再帰して各記事のエンドポイントを出力する
    next=`echo "$page" | grep 'link rel="next"'`
    if [ $? -eq 0 ] ; then
        pageId=`echo "$next" | grep -oP "page=[0-9]*" | tr -d "page="`
        getEntryId $pageId
    fi
}

結果

$ sh hatena.sh
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/9801XXXXX55524080
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/9801XXXXX39215842
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/9801XXXXX39148152
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/1025XXXXX32700158645
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/1025XXXXX32690969669
https://blog.hatena.ne.jp/hatena_id/blog_id/atom/entry/1025XXXXX32688559263
...(省略)

簡単ですね。grepのところはもう少しスマートになりそうだけどとりあえずこれで良しとしよう。

エントリーIDというか各記事へのエンドポイントを取得ですね。もしエントリーIDだけ欲しかったらここから更にgrep -oPで抽出すればいいんじゃないかな。

画像も記事内容も全てローカルにバックアップする

もはや気軽でもなんでもないが、そもそも私がしたかったのは全記事のバックアップ
シェルを叩いたら一撃で全ての記事の内容、画像をローカルにダウンロードしてくる、というのを実現したかった。
ということでここからはもう完全にオナニーだがせっかく作ったのでお披露目しておこう。

github.com

本スクリプトを実行すると、日付・記事タイトル・記事で使用されている画像が出力される。

f:id:rasukarusan:20190203002203g:plain
動作画面

hatena.sh

#!/bin/sh

API_KEY=`cat ~/account.json | jq -r .hatena.api_key`
HATENA_ID=`cat ~/account.json | jq -r .hatena.user_id`
BLOG_ID=`cat ~/account.json | jq -r .hatena.blog_id`

ENDPOINT_ROOT="https://blog.hatena.ne.jp/${HATENA_ID}/${BLOG_ID}/atom"
ENDPOINT_ENTRY='/entry'

# エンコードされた特殊文字(&,",',<,>)をデコードする
function decodeSpecialChars() {
     sed 's/&amp;/&/g'  \
   | sed 's/&quot;/"/g' \
   | sed "s/&#39;/'/g"  \
   | sed 's/&lt;/</g'   \
   | sed 's/&gt;/</g'
}

# 全記事のエンドポイントを取得する
function getEntryId() {
    page=`curl -su ${HATENA_ID}:${API_KEY} ${ENDPOINT_ROOT}${ENDPOINT_ENTRY}?page=$1`

    # entry_idの取得
    # たまにBinary file (standard input) matchesと表示され処理が止まるのを防ぐため-aをつける
    echo "$page" \
    | grep -a 'link rel="edit"' \
    | grep -oP 'href=".*"' \
    | sed 's/href="//g' \
    | tr -d '"'

    # 次のページがある場合、再帰してエントリーIDを出力する
    next=`echo "$page" | grep 'link rel="next"'`
    if [ $? -eq 0 ] ; then
        pageId=`echo "$next" | grep -oP "page=[0-9]*" | tr -d "page="`
        getEntryId $pageId
    fi
}

# contentタグの中身だけ取得。特殊文字はデコードして出力
# 第一引数に対象の記事のエンドポイントをとる
function getContent() {
    # contentタグの始めと終わりの行番号を取得するためのラベル
    START_CONTENT_LABEL='<content type="text\/x-markdown">'
    END_CONTENT_LABEL='<\/content>'

    endPoint=$1
    article=`curl -su ${HATENA_ID}:${API_KEY} ${endPoint}`

    # コンテンツ内容を投稿日時毎のディレクトリに保存し、ファイル名をタイトルにするため
    postDate=`echo "$article" | grep 'link rel="alternate"' | grep -oP "[0-9]{4}/[0-9]{2}/[0-9]{2}"`
    title=`echo "$article" | grep -oP "(?<=\<title\>).*(?=\<\/title\>)"`

    # 画像ファイルを取得するため
    blogUrl=`echo "$article" | grep 'link rel="alternate"' | grep -oP '(?<=href=").*(?=")'`
    blog=`curl -s ${blogUrl}`
    imgUrls=`echo "$blog" | grep -oP '<img src.*itemprop="image"' | grep -oP '(?<=src=").*(?=" alt)'`

    printf "\e[92m\e[1m$postDate\e[m\n"
    echo $title

    # 記事毎に内容と画像を保存したいので、投稿日時ごとのディレクトリを作成
    mkdir -p $postDate

    # 画像をダウンロードし、投稿日時ディレクトリに保存
    for imgUrl in `echo "$imgUrls"`; do
        imgName=`echo $imgUrl | grep -oP "[0-9]{12}.*"`
        echo "$imgName"
        echo "${postDate}/${imgName} $imgUrl"
        wget -q -O ${postDate}/${imgName} $imgUrl
    done

    # contentタグの中身のみ取得したいため、始めと終わりの行番号を取得
    contentLineNo=`
    echo "$article" \
    | grep -nE "(${START_CONTENT_LABEL}|${END_CONTENT_LABEL})" \
    | sed 's/:.*//g'
    `

    # 記事の内容を投稿日時ディレクトリにmd形式で保存
    start=`echo "$contentLineNo" | head -n 1`
    end=`echo "$contentLineNo" | tail -n 1`
    content=`echo "$article" | awk "NR==${start},NR==${end}"`
    echo "$content" | decodeSpecialChars \
    | sed "s/$START_CONTENT_LABEL//g" \
    | sed "s/$END_CONTENT_LABEL//g" \
    > $postDate/$title.md
}

# 全ての記事の内容を取得する
function main() {
    for i in `getEntryId`
    do
        getContent $i
    done
}

main

このシェルを実行すると記事投稿日毎にディレクトリが作成され、記事ファイルとその記事で使用されている画像ファイルが作成される。 私はMarkdownで記事を書いているのでmd形式の記事ファイル。はてぶに上げた形式のままになるよう整形しているので仮に記事が消えてもそのままこのmdファイルをアップロードすれば復元できる。

# シェル叩いた後
$ ls
2018      2019      hatena.sh

# ディレクトリ構成
$ tree 2019
.
\`-- 01
    |-- 07
    |   \`-- 【Swift4】UIImageでURLで画像を指定する.md
    |-- 26
    |   \`-- apacheのDOCUMENT_ROOTを知る方法.md
    \`-- 27
        |-- 20190126015937.png
        |-- 20190126020540.png
        \`-- laravel+apacheでTesting 123...と出てしまう問題の解決法.md

記事もそのまま

$ cat  01/27/laravel+apacheでTesting\ 123...と出てしまう問題の解決法.md

[f:id:rasukarusan:20190126020540p:plain]

サーバーにlaravelで作ったアプリを設置するときに若干詰まった。

結局シンプルな変更漏れっていうオチなんですけどね。

## 解決1

DOCUMENT_ROOTを設定するときにhttpd.confの設定で変更漏れがあった。
\```zsh
DocumentRoot /var/www/html/laravel-app/public
ServerName example.com

#<Directory "/var/www/html"<
<Directory "/var/www/html/laravel-app/public"< # ←こっちも変更する
...
\```
...(省略)

まとめ

APIを試す時は大体shellのcurlでサクッとやってしまうのだが他の人はどうなんだろうか。
今回はてぶAPIを試す際色んな記事を見たがrubyやpythonでやってる人が多かった。

私みたいに「APIを試す時はcurlで!」という方の助けになればと思い書いてみたが、最後は気軽じゃないスクリプト載っけてしまった。絶対他の人読めない気がする。(人の書いたShellって読みづらいよね)
まあでもやりたいことは出来たので満足。