試してみたブログ

AI関連・iPhone/Pixelなどのガジェット・音声入力・サーマルプリンタなど興味をある事をどんどん試してみた際の記録

pythonではてなブログに画像を投稿する

背景

  • はてなブログを有料にプランにしてからブログを1個増やした
  • 4コマ漫画を投稿するブログなので、タイトルと画像1枚の投稿となる
  • CLIからワンコマンドで画像アップ+投稿まで出来ないか試してみた

要件

  • 画像生成までは完了させている
  • 固定の画像名を投稿させる
  • タイトルは毎回異なるので、タイトルは毎回渡したい

試してみた

事前にexportしておく

export HATENA_ID="XXXXX"
export HATENA_API_KEY="XXXXXX"
export HATENA_BLOG_ID="XXXXX.hateblo.jp"
import os
import base64
import hashlib
import requests
import argparse
from datetime import datetime, timezone
from email.utils import format_datetime
from xml.etree import ElementTree as ET

HATENA_ID = os.environ["HATENA_ID"]
HATENA_API_KEY = os.environ["HATENA_API_KEY"]
HATENA_BLOG_ID = os.environ["HATENA_BLOG_ID"]

FIXED_IMAGE_PATH = "/path/to/fixed_image.png"


def build_wsse(username: str, api_key: str) -> str:
    created = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
    nonce = os.urandom(16)
    digest = hashlib.sha1(nonce + created.encode("utf-8") + api_key.encode("utf-8")).digest()
    b64nonce = base64.b64encode(nonce).decode("ascii")
    b64digest = base64.b64encode(digest).decode("ascii")
    return (
        f'UsernameToken Username="{username}", '
        f'PasswordDigest="{b64digest}", '
        f'Nonce="{b64nonce}", '
        f'Created="{created}"'
    )


def upload_to_fotolife(image_path: str, title: str) -> str:
    with open(image_path, "rb") as f:
        data_b64 = base64.b64encode(f.read()).decode("ascii")

    # base64埋め込み形式のエントリXML[web:29]
    entry_xml = f"""\
<entry xmlns="http://purl.org/atom/ns#">
  <title>{title}</title>
  <content mode="base64" type="image/png">
{data_b64}
  </content>
  <generator>python-script</generator>
</entry>
"""

    headers = {
        "X-WSSE": build_wsse(HATENA_ID, HATENA_API_KEY),
        "Content-Type": "application/xml",
        "Authorization": 'WSSE profile="UsernameToken"',
    }

    url = "https://f.hatena.ne.jp/atom/post"  
    resp = requests.post(url, data=entry_xml.encode("utf-8"), headers=headers)
    resp.raise_for_status()

    ns = {
        "atom": "http://purl.org/atom/ns#",
        "hatena": "http://www.hatena.ne.jp/info/xmlns#",
    }
    root = ET.fromstring(resp.content)
    syntax = root.find("hatena:syntax", ns)
    if syntax is None or not syntax.text:
        raise RuntimeError("hatena:syntax が取得できませんでした")
    return syntax.text.strip()



def post_hatena_entry(title: str, body: str, draft: bool = False) -> str:

    collection_url = f"https://blog.hatena.ne.jp/{HATENA_ID}/{HATENA_BLOG_ID}/atom/entry"  # [web:33]

    now = datetime.now(timezone.utc)
    updated_str = format_datetime(now)  # RFC形式の日時文字列[web:33]

    draft_value = "yes" if draft else "no"

    entry_xml = f"""\
<entry xmlns="http://www.w3.org/2005/Atom"
       xmlns:app="http://www.w3.org/2007/app">
  <title>{title}</title>
  <updated>{updated_str}</updated>
  <author><name>{HATENA_ID}</name></author>
  <content type="text/plain">
{body}
  </content>
  <app:control>
    <app:draft>{draft_value}</app:draft>
  </app:control>
</entry>
"""

    resp = requests.post(
        collection_url,
        data=entry_xml.encode("utf-8"),
        auth=(HATENA_ID, HATENA_API_KEY),  # Basic認証[web:33]
        headers={"Content-Type": "application/xml"},
    )
    resp.raise_for_status()

    # レスポンスから <link rel="alternate"> の href = 記事URL を取り出す[web:33]
    root = ET.fromstring(resp.content)
    ns = {"atom": "http://www.w3.org/2005/Atom"}
    for link in root.findall("atom:link", ns):
        if link.get("rel") == "alternate":
            return link.get("href")
    return ""



def post_with_title_and_image(post_title: str) -> str:
    image_syntax = upload_to_fotolife(FIXED_IMAGE_PATH, post_title)
    body = f"{post_title}\n\n{image_syntax}\n"
    entry_url = post_hatena_entry(post_title, body, draft=False)
    return entry_url


def main():
    parser = argparse.ArgumentParser(description="はてなブログに固定画像+タイトルで投稿するスクリプト")
    # タイトルを位置引数で受け取る[web:48][web:50]
    parser.add_argument("title", help="記事タイトル")
    args = parser.parse_args()

    url = post_with_title_and_image(args.title)
    print("posted:", url)


if __name__ == "__main__":
    main()

使い方

python3 hatena.py "テスト"

ちゃんと投稿された!!

振り返り

  • ブログ投稿する物が増えて来たので少しでも簡略出来てよかった
  • はてなブログであれば、画像のアップ+投稿も割と簡単にできた
  • 次は「試してみたブログ」でCLIの実行をメモの簡略化などを試していきたい