doodle-on-web

自分で調べたことや、仕事の中で質問されたことなどをまとめています。

はてなブログにAPIで投稿する方法(AtomPub完全ガイド)

スポンサーリンク

はてなブログにAPIで投稿する方法(AtomPub完全ガイド)

記事を書いたあと、タイトル入力・タグ設定・カテゴリ選択……管理画面での手作業に5〜10分とられていませんか? Pythonスクリプト1本で、この作業を10秒以下に縮められます。この記事では、はてなブログにAPIで投稿する方法(AtomPub API への自動投稿)を、コピペで動くコードと一緒に解説します。

この記事だけで自動投稿の仕組みは完成します。GitHub Actions による完全自動化は次の記事で扱いますが、まずはローカルで動かすことを目標にしてください。

この記事の結論(先に知りたい方向け)

  • PythonからはてなブログにAPI投稿できる(約10秒)
  • 必要なのは「はてなID・APIキー・ブログドメイン」の3つだけ
  • <app:draft>yes</app:draft> で下書き、no で公開を切り替え可能
  • フロントマター連携で「Markdownファイルを置くだけ」の完全自動投稿まで拡張できる

この記事でわかること

  • はてなブログ AtomPub API の基本的な仕組みと使える操作の範囲
  • Pythonですぐ使える投稿コード(コピペOK)
  • Markdown フロントマターを読んで自動投稿する実践コード
  • 認証エラー・文字化け・多重投稿など、よくあるトラブルの対処法
  • 下書き保存と公開投稿を状況で切り替える方法

はてなブログ AtomPub API とは

はてなブログの AtomPub API を使うと、Python から記事の自動投稿・更新・削除ができます。AtomPub(Atom Publishing Protocol) とは、ブログ投稿を外部から操作するための Web 標準規格です。つまり、コマンド1行でブラウザ操作を代替できます。

はてなブログをAPIで自動投稿できること・できないこと

できること(HTTP メソッドと対応操作):

操作 HTTP メソッド 概要
記事を新規投稿 POST サーバーへ新しいデータを「送信」する命令
記事を更新(上書き) PUT 既存データを「上書き」する命令
記事を取得 GET データを「取得」する命令
記事を削除 DELETE データを「削除」する命令

できないこと:

  • 画像のアップロード(別途 Fotolife API が必要。本シリーズでは扱いません)
  • カテゴリ一覧の取得
  • PV 統計などアナリティクスデータの取得

エンドポイント(API の窓口 URL)

https://blog.hatena.ne.jp/{はてなID}/{ブログドメイン}/atom/entry

事前準備:API キー取得から有効化まで

1. AtomPub API を有効化する

  1. はてなブログの管理画面を開く
  2. 設定 → 詳細設定 を選択
  3. ページ下部の「AtomPub」セクションで 「使用する」 に変更して保存

2. API キーを確認する

同じ「詳細設定」ページに AtomPub API キー が表示されています。このキーをメモしておきます(パスワードと同じ扱いで管理してください)。

3. 必要な情報を整理する

変数名 内容
HATENA_ID はてな ID yourname
HATENA_API_KEY API キー xxxxxxxxxxxx
HATENA_BLOG_DOMAIN ブログドメイン yourname.hatenablog.com

4. ライブラリをインストールする

pip install requests python-frontmatter

pip は Python のライブラリ管理ツールです。このコマンドで、HTTP 通信用の requests とフロントマター解析用の python-frontmatter の2つをインストールします。 Windows の場合、pip が見つからないときは python -m pip install ... と書き換えてください。

準備が整ったら、以下の手順でまず動作確認まで進めましょう。

今日中に動かすための5ステップ

  1. python --version で Python 3.8 以上を確認
  2. pip install requests python-frontmatter を実行
  3. はてなブログ管理画面で AtomPub を有効化し、API キーをメモ
  4. 基本コードの3つの変数(ID・キー・ドメイン)を書き換え
  5. python post.py を実行 → ブログに「テスト投稿」が現れれば成功

→ ここまで完了すると、「APIから記事が投稿される体験」が10分以内に確認できます。

詳細は各セクションで解説します。まずは全体の流れを掴むだけで十分です。


Python で記事を投稿する基本コード

動作確認用の最小構成コードです。外部ライブラリは requests のみで、コピーして ID・キーを書き換えればすぐに動きます。なお、投稿する記事本文を Claude API で自動生成したい場合は、Claude APIでブログ記事を自動生成する方法も参考にしてください。

このコードを実行すると、「下書き記事」が1件投稿されます。ステータスコード 201 が返れば成功です。

import requests

HATENA_ID = "your_hatena_id"
HATENA_API_KEY = "your_api_key"
HATENA_BLOG_DOMAIN = "yourname.hatenablog.com"

ENDPOINT = f"https://blog.hatena.ne.jp/{HATENA_ID}/{HATENA_BLOG_DOMAIN}/atom/entry"

xml_body = f"""<?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>
  <author><name>{HATENA_ID}</name></author>
  <content type="text/x-markdown">## 本文

これはAPIからの投稿テストです。</content>
  <app:control>
    <app:draft>yes</app:draft>
  </app:control>
</entry>"""

response = requests.post(
    ENDPOINT,
    auth=(HATENA_ID, HATENA_API_KEY),  # Basic認証(IDとAPIキーをセットで送る方式)
    data=xml_body.encode("utf-8"),
    headers={"Content-Type": "application/atom+xml; charset=utf-8"},
    timeout=30,
)

print(response.status_code)  # 201 なら成功

動作確認中は <app:draft>yes</app:draft>(下書き)で実行することをおすすめします。 テスト中に誤公開すると読者に通知が届いてしまいます。下書きモードを習慣化することで、そのリスクをゼロにできます。

レスポンスの HTTP ステータスコードが 201 なら投稿成功です。記事 URL はレスポンス XML の rel="alternate" 属性から取得できます。

import re
alt_match = re.search(r'rel="alternate"[^>]*href="([^"]+)"', response.text)
url = alt_match.group(1) if alt_match else ""
print(f"投稿URL: {url}")

基本的な投稿が確認できたら、次は Markdown ファイルから自動でタイトルやタグを読み取る実践的なコードに進みましょう。


フロントマターから自動で投稿する方法

最初は記事ごとにコードの変数を書き換えていましたが、10記事目あたりで面倒になってフロントマター管理に切り替えました。

変更前: 記事ごとにスクリプト内の変数を書き換え 変更後: Markdownファイルを置くだけ(スクリプトはそのまま)

Markdown ファイルの先頭に フロントマター--- で囲んだメタ情報欄)を書いておけば、タイトル・タグ・カテゴリを自動で読み取れます。10記事投稿するなら10回分の書き換えが0回になります。

フロントマター付きの Markdown ファイル例

---
title: はてなブログ自動投稿の実践ガイド
tags: [Python, はてなブログ, 自動化]
category: AIツール活用
---

## 本文

ここから記事本文を書きます。

このファイルを読み込む投稿スクリプトが以下です。主要な処理を順に確認してください。

投稿スクリプト全文

import json
import re
import sys
import requests
import frontmatter
from pathlib import Path

HATENA_ID = "your_hatena_id"
HATENA_API_KEY = "your_api_key"
HATENA_BLOG_DOMAIN = "yourname.hatenablog.com"
ENDPOINT = f"https://blog.hatena.ne.jp/{HATENA_ID}/{HATENA_BLOG_DOMAIN}/atom/entry"

HISTORY_FILE = Path("posted/history.json")


def escape_xml(text: str) -> str:
    return (
        text.replace("&", "&amp;")
            .replace("<", "&lt;")
            .replace(">", "&gt;")
            .replace('"', "&quot;")
    )


def is_already_posted(filename: str) -> bool:
    """投稿済みかどうかを確認する"""
    if not HISTORY_FILE.exists():
        return False
    history = json.loads(HISTORY_FILE.read_text())
    return any(e["file"] == filename for e in history.get("posted", []))


def record_posted(filename: str, url: str):
    """投稿済みとして記録する"""
    history = json.loads(HISTORY_FILE.read_text()) if HISTORY_FILE.exists() else {"posted": []}
    history["posted"].append({"file": filename, "url": url})
    HISTORY_FILE.parent.mkdir(parents=True, exist_ok=True)
    HISTORY_FILE.write_text(json.dumps(history, ensure_ascii=False, indent=2))


def post_markdown_file(file_path: str, draft: bool = False) -> str:
    """Markdown ファイルをはてなブログに投稿して URL を返す"""

    # 多重投稿チェック
    if is_already_posted(file_path):
        print("投稿済みのためスキップします")
        sys.exit(0)

    post = frontmatter.load(file_path)
    title = post.get("title", Path(file_path).stem)
    tags = post.get("tags", [])
    content = post.content

    tag_elements = "\n".join(
        f'  <category term="{t}" />' for t in tags
    )
    draft_val = "yes" if draft else "no"

    xml_body = f"""<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom"
       xmlns:app="http://www.w3.org/2007/app">
  <title>{escape_xml(title)}</title>
  <author><name>{HATENA_ID}</name></author>
  <content type="text/x-markdown">{escape_xml(content)}</content>
{tag_elements}
  <app:control>
    <app:draft>{draft_val}</app:draft>
  </app:control>
</entry>"""

    response = requests.post(
        ENDPOINT,
        auth=(HATENA_ID, HATENA_API_KEY),
        data=xml_body.encode("utf-8"),
        headers={"Content-Type": "application/atom+xml; charset=utf-8"},
        timeout=30,
    )

    if response.status_code == 201:
        alt_match = re.search(r'rel="alternate"[^>]*href="([^"]+)"', response.text)
        url = alt_match.group(1) if alt_match else ""
        print(f"投稿成功: {url}")
        record_posted(file_path, url)
        return url
    else:
        print(f"投稿失敗 ({response.status_code}): {response.text[:200]}")
        response.raise_for_status()  # エラー時に例外を発生させ処理を止める


if __name__ == "__main__":
    post_markdown_file(sys.argv[1])  # コマンド実行時に指定したファイルパスを受け取る

ファイルパスを引数に渡して実行します。

python post.py drafts/my-article.md

よくあるエラーとトラブルシューティング

実際に使ってみると、最初は認証エラーや文字化けで詰まることがあります。9割のケースは①APIキーの末尾への改行混入、②IDとキーの入れ違い、の2つが原因です。まず .strip() を試し、次に auth= の順番を確認してください。

切り分けの基本: まず 401 → 403 → 404 の順で確認すると効率的です。

HTTP 401 Unauthorized(認証失敗)

401 Client Error: Unauthorized

原因と対処:

  • HATENA_IDHATENA_API_KEY が間違っている → 管理画面で再確認
  • API キーをパスワード欄に貼り忘れている → auth=(ID, APIキー) の順序を確認
  • API キーにスペースや改行が混入している → .strip() で除去
HATENA_API_KEY = os.environ.get("HATENA_API_KEY", "").strip()

筆者も最初に 401 が出たとき、API キーの末尾に改行が混入していたことに気づくまで30分悩みました。まず .strip() を試してみてください。

HTTP 403 Forbidden(AtomPub が無効)

403 Client Error: Forbidden

対処: はてなブログ詳細設定で AtomPub を「使用する」に変更して保存する。

HTTP 404 Not Found(ドメインが間違い)

対処: HATENA_BLOG_DOMAIN を管理画面の URL から正確にコピーする。 例:yourname.hatenablog.com(末尾スラッシュなし)

文字化けする

XML は UTF-8 で送信する必要があります。encode("utf-8") を忘れずに。

data=xml_body.encode("utf-8"),
headers={"Content-Type": "application/atom+xml; charset=utf-8"},

同じ記事が何度も投稿される(多重投稿・事故パターン)

history.json の仕組みを入れる前は、テスト実行のたびに同じ記事が量産されて管理画面が散乱しました。このチェックは最初から入れておくことを強くおすすめします。前のセクションで紹介した is_already_posted()record_posted() を組み合わせることで防止できます。スクリプト冒頭でチェックし、投稿済みなら即終了する設計にしておくのがポイントです。


下書き投稿と公開投稿の切り替え

テスト中に誤公開すると読者に通知が届いてしまいます。<app:draft> タグによる切り替えを習慣化することで、そのリスクをゼロにできます。

<!-- 下書きとして保存 -->
<app:control>
  <app:draft>yes</app:draft>
</app:control>

<!-- 公開として投稿 -->
<app:control>
  <app:draft>no</app:draft>
</app:control>

前述のスクリプトでは draft 引数として持たせています。呼び出し側で切り替えられるため、テスト時と本番時でコードを書き換える必要がありません。

使い分けの目安:

  • 本番投稿前に 下書き(draft: yes) で動作確認する
  • GitHub Actions(クラウド上で処理を自動実行する CI/CD サービス)の本番ワークフローでは draft: no で公開する
  • テスト用スクリプトは常に draft: yes にする習慣をつける

はてなブログ API で投稿済み記事を更新する方法

投稿済み記事を修正したい場合は、POST ではなく PUT を使います。エンドポイントに記事固有の ID を付加して送信します。

まず、新規投稿時のレスポンス XML から entry_id を取り出す方法を確認しておきましょう。

# entry_id の取り出し方
id_match = re.search(r"<id>tag:blog\.hatena\.ne\.jp,[^:]+:/entry/(\d+)</id>", response.text)
entry_id = id_match.group(1) if id_match else ""

entry_idrecord_posted() で URL と一緒に保存しておくと後から取り出せて便利です。例:https://...entry/123456entry_id = "123456"history.json から url.split("/")[-1] で取り出せます。

def update_entry(entry_id: str, title: str, content: str, draft: bool = False) -> int:
    """投稿済み記事を更新する"""
    url = f"{ENDPOINT}/{entry_id}"
    draft_val = "yes" if draft else "no"

    xml_body = f"""<?xml version="1.0" encoding="utf-8"?>
<entry xmlns="http://www.w3.org/2005/Atom"
       xmlns:app="http://www.w3.org/2007/app">
  <title>{escape_xml(title)}</title>
  <author><name>{HATENA_ID}</name></author>
  <content type="text/x-markdown">{escape_xml(content)}</content>
  <app:control>
    <app:draft>{draft_val}</app:draft>
  </app:control>
</entry>"""

    response = requests.put(
        url,
        auth=(HATENA_ID, HATENA_API_KEY),
        data=xml_body.encode("utf-8"),
        headers={"Content-Type": "application/atom+xml; charset=utf-8"},
        timeout=30,
    )
    return response.status_code  # 200 なら更新成功

セキュリティ:API キーを環境変数で管理する

コード例では説明のしやすさを優先して HATENA_API_KEY = "your_api_key" と直書きしていますが、実際の運用ではソースコードに API キーを書いてはいけません。Git リポジトリに誤ってコミットすると、キーが外部に漏洩します。GitHubに公開リポジトリを使っている場合、この対策は必須です。

python-dotenv を使った安全な管理方法を推奨します。

pip install python-dotenv

プロジェクトルートに .env ファイルを作成します(.gitignore に追加するのを忘れずに)。

HATENA_ID=yourname
HATENA_API_KEY=xxxxxxxxxxxx
HATENA_BLOG_DOMAIN=yourname.hatenablog.com

スクリプト側でこのように読み込みます。

import os
from dotenv import load_dotenv

load_dotenv()

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

まとめ:Python × はてなブログ API で自動投稿を実現しよう

ポイントのおさらい:

  • AtomPub は POST/PUT/GET/DELETE の HTTP リクエストで操作する
  • 認証は Basic 認証(はてな ID + API キー)
  • XML に type="text/x-markdown" を指定すると Markdown がそのまま使える
  • <app:draft>yes</app:draft> で下書き保存、no で公開
  • 多重投稿防止には投稿済みファイル名の JSON 管理が有効
  • 記事更新は PUT メソッドに記事 ID を付加して送信する
  • API キーは .env ファイルで管理し、コードに直書きしない

手動投稿で毎回かかっていた5〜10分の作業が、Python スクリプト1本・コマンド1行で10秒以下に完了するようになります。まず draft: yes でテスト投稿を実行し、201 レスポンスが返ることを確認してください。


「ローカルで投稿できたら、次は自動化」です。次の記事(記事04)では GitHub Actions を使って、このスクリプトをクラウドで自動実行する仕組みを構築します。ローカルで毎回コマンドを打つ手間をなくし、「コミットしたら自動で投稿される」環境を一緒に作りましょう。


このシリーズの他の記事も合わせてどうぞ。


このシリーズの前後の記事: - ← 前の記事:Claude APIでブログ記事を自動生成する方法 - → 次の記事:GitHub Actionsでブログを定期投稿する方法(近日公開)

シリーズ一覧:ブログ運用を半自動化する実践ロードマップ