noteへの手動投稿、毎回30分以上かけていませんか?公式APIがないnote.comでも、Playwrightを使えばログインから予約投稿まで全自動化できます。この記事では、実際に動くPythonコードとともに、ゼロから自動投稿パイプラインを構築する手順を丁寧に解説します。
📋 この記事の目次
フェーズ1:なぜnote自動投稿が難しいのか——公式API不在という壁
note.comには公式APIが存在しない
WordPressにはREST APIがあり、はてなブログにはAtomPub APIがあります。しかしnote.comは2024年時点においても、外部から記事を投稿・管理できる公式APIを一般ユーザー向けに公開していません。これがnote自動投稿を難しくしている根本的な原因です。
サードパーティのnote自動投稿ツールも事実上ゼロに近い状況です。ZapierやMakeといったノーコード自動化ツール(プログラミング不要で複数サービスを連携できるツール)にもnoteの投稿コネクタは存在しません。Buffer・Hootsuite・Sprout Socialといったソーシャルメディア管理ツールにも対応していません。つまり、既存の便利ツールでは「noteへの自動投稿」は実現できない、というのが現実です。
手動投稿フローがいかに時間を奪っているか
実際の手動投稿フローを振り返ってみましょう。ハナが計測したところ、1記事あたりの作業時間はざっくり以下のようになります。
- ブラウザを開いてnote.comにアクセス・ログイン:2〜3分
- 新規投稿画面を開き、タイトルを入力:1〜2分
- 本文をコピー&ペーストし、書式を整える:10〜15分
- タグを1つずつ入力する:3〜5分
- アイキャッチ画像を設定する:5〜10分
- 予約投稿の日時をカレンダーUIで設定する:3〜5分
- プレビュー確認と最終チェック:5〜10分
合計すると1記事あたり30〜50分。週3本投稿するだけで月に6〜8時間が手動投稿作業に消えていきます。副業ライターとしてこの時間を執筆や案件獲得に充てられたら、どれだけ生産性が上がるか——そう考えると、自動化の価値は明らかです。
ブラウザ自動化がAPIの代替手段として有効な理由
APIがない場合、次善策として有力なのが「ブラウザ自動化(RPA的アプローチ)」です。RPA(Robotic Process Automation)とは、人間がブラウザ上で行う操作をプログラムに代替させる技術です。APIがなくても、ブラウザ画面上の要素をプログラムから操作できれば、ログイン・入力・ボタン押下という一連の作業を自動化できます。
ただし重要な前提として、利用規約の確認は必須です。note.comの利用規約では、自動化ツールによるアクセスについての記述を必ず確認してください。過度な連続アクセスはサーバーに負荷をかけるため、記事投稿間に適切な待機時間(sleep)を設けることが重要です。本記事のコードはあくまで技術的な可能性を示すものであり、自己責任での利用をお願いします。
より詳しいステップバイステップの手順については、note自動投稿の完全手順はこちらもあわせてご覧ください。
フェーズ2:Playwright×Pythonで自動投稿を設計する全体像
PlaywrightをSeleniumやPuppeteerより選ぶ理由
ブラウザ自動化ツールの選択肢として、Selenium・Puppeteer・Playwrightの3つが主流です。ハナがPlaywrightを推す理由は以下の通りです。
| ツール | 言語サポート | 非同期処理 | セレクタの柔軟性 | セットアップのしやすさ |
|---|---|---|---|---|
| Selenium | Python/Java/他 | △(追加実装が必要) | △(XPath中心) | △(WebDriver管理が煩雑) |
| Puppeteer | JavaScript/TypeScript | ◎ | ○ | ○(Node.js環境が必要) |
| Playwright | Python/JS/TS/他 | ◎ | ◎(テキスト・role等) | ◎(pipで一発) |
Playwrightの最大の強みは、非同期処理(async/await)をネイティブサポートしている点と、ブラウザドライバを自動管理してくれる点です。Seleniumではブラウザのバージョンに合わせてWebDriverを手動管理する必要がありましたが、Playwrightならplaywright installの一発コマンドで完結します。また、ヘッドレスモード(画面を表示せずにバックグラウンドで動作するモード)での安定性も高く、サーバー上での定期実行に向いています。
詳しくは→Playwrightとは?AI副業自動化での活用法【gpirot.com実践例】
自動投稿フローの設計図
全体のフローを整理すると、以下のステップで構成されます。
- 認証情報の安全な管理:メールアドレス・パスワードを
.envファイルに格納し、コードにハードコードしない - 初回ログインとセッション保存:
storage_state(CookieとlocalStorageの保存機能)でセッションをJSONファイルに保存し、以降は再ログイン不要にする - Markdownファイルの読み込み:タイトル・タグ・予約日時・本文をフロントマター(Markdownファイルの先頭に記述するメタデータ領域)で管理する
- 投稿フォームへの自動入力:タイトル・本文・タグを順次入力する
- 予約日時のカレンダーUI操作:最も難易度が高い部分。日時指定UIを操作して予約投稿を設定する
- 公開ボタン押下:最終確認後に投稿を実行する
- バッチ処理:
posts/ディレクトリ内の複数Markdownファイルを順次処理する
利用規約とレート制限の考え方
自動化ツールを使う際は必ずnote.comの利用規約を確認してください。また、複数記事を連続投稿する場合は記事間にasyncio.sleep(60)のような待機時間を設けることを強く推奨します。人間が手動で操作するペースを超えないようにすることが、アカウント停止リスクを下げる基本的な考え方です。
フェーズ3:実装手順——環境構築からコード全文まで
ここからは実際のコードを使って手順を説明します。こちらも参考に→Pythonとは?AI副業自動化での活用法【gpirot.com実践例】
ディレクトリ構成は以下を想定しています。
note-autoposter/
├── .env
├── login.py
├── post_note.py
├── utils/
│ └── parser.py
└── posts/
├── article_001.md
└── article_002.md
フェーズ3-1:環境セットアップと.env設定
必要なパッケージをインストールし、認証情報を管理する.envファイルを作成します。
pip install playwright python-dotenv
playwright install chromium
# .env
NOTE_EMAIL=your_email@example.com
NOTE_PASSWORD=your_password
SESSION_FILE=session.json
python-dotenvは.envファイルの内容をPythonの環境変数として読み込むためのライブラリです。パスワードをコードに直書きするのは絶対に避けてください。GitHubなどにコードをアップロードする際に情報漏洩につながります。.gitignoreに.envとsession.jsonを追記しておくことも必須です。
フェーズ3-2:ログイン自動化とセッション保存スクリプト
初回のみ実行するログインスクリプトです。storage_stateにCookieとlocalStorageを保存し、以降の投稿スクリプトで再利用します。
# login.py
import asyncio
import os
from dotenv import load_dotenv
from playwright.async_api import async_playwright
load_dotenv()
EMAIL = os.getenv("NOTE_EMAIL")
PASSWORD = os.getenv("NOTE_PASSWORD")
SESSION_FILE = os.getenv("SESSION_FILE", "session.json")
async def login():
async with async_playwright() as p:
browser = await p.chromium.launch(headless=False)
context = await browser.new_context()
page = await context.new_page()
await page.goto("https://note.com/login")
await page.wait_for_selector('input[name="email"]')
await page.fill('input[name="email"]', EMAIL)
await page.fill('input[name="password"]', PASSWORD)
await page.click('button[type="submit"]')
await page.wait_for_url("https://note.com/", timeout=15000)
print("ログイン成功")
await context.storage_state(path=SESSION_FILE)
print(f"セッションを {SESSION_FILE} に保存しました")
await browser.close()
asyncio.run(login())
このスクリプトのポイントはheadless=Falseで実際のブラウザ画面を表示している点です。初回ログイン時に二段階認証(2FA)が求められた場合も、画面を見ながら手動で対応できます。ログイン成功後にstorage_stateでセッション情報をJSONファイルに保存するため、以降の投稿スクリプトでは毎回ログイン操作を繰り返す必要がありません。
フェーズ3-3:Markdownファイルの形式定義
投稿コンテンツはMarkdownファイルで管理します。フロントマターにタイトル・タグ・予約日時を記述します。
# posts/article_001.md
---
title: Playwrightで自動化を極める実践ガイド
tags: Python,自動化,Playwright
scheduled: 2025-02-01 10:00
---
## はじめに
この記事ではPlaywrightを使ったブラウザ自動化の実践的な手法を解説します。
## セットアップ
まずは環境構築から始めましょう。
```python
pip install playwright
playwright install
```
## まとめ
自動化によって繰り返し作業を削減できます。
フロントマターのscheduledフィールドに予約日時をYYYY-MM-DD HH:MM形式で記述します。記事の量産管理がしやすく、Gitでバージョン管理することで投稿履歴の追跡も可能になります。
フェーズ3-4:Markdownパーサーユーティリティ
フロントマターとMarkdown本文を分離して取得するユーティリティ関数です。
# utils/parser.py
import re
from pathlib import Path
from datetime import datetime
def parse_markdown(filepath: str) -> dict:
content = Path(filepath).read_text(encoding="utf-8")
pattern = r"^---\n(.*?)\n---\n(.*)$"
match = re.match(pattern, content, re.DOTALL)
if not match:
raise ValueError(f"フロントマターが見つかりません: {filepath}")
front_matter_raw = match.group(1)
body = match.group(2).strip()
meta = {}
for line in front_matter_raw.strip().splitlines():
if ":" in line:
key, _, value = line.partition(":")
meta[key.strip()] = value.strip()
return {
"title": meta.get("title", "無題"),
"tags": [t.strip() for t in meta.get("tags", "").split(",") if t.strip()],
"scheduled": meta.get("scheduled"),
"body": body,
}
正規表現(reモジュール)を使ってフロントマターと本文を分離しています。re.DOTALLフラグにより、改行を含む複数行のマッチングが可能になります。タグはカンマ区切りでリスト化されるため、後の投稿処理でループ処理しやすい構造になっています。
フェーズ3-5:バッチ投稿メインスクリプト
セッションを再利用してnoteの投稿フォームに自動入力し、予約日時を設定して公開するスクリプトです。posts/ディレクトリ内の全Markdownファイルを順次処理します。
# post_note.py
import asyncio
import os
import glob
from dotenv import load_dotenv
from playwright.async_api import async_playwright
from utils.parser import parse_markdown
load_dotenv()
SESSION_FILE = os.getenv("SESSION_FILE", "session.json")
POSTS_DIR = "posts"
async def post_article(page, article: dict):
await page.goto("https://note.com/notes/new")
await page.wait_for_selector('div[data-placeholder="本文を入力してください"]', timeout=15000)
# タイトル入力
title_selector = 'input[placeholder="記事タイトル"]'
await page.wait_for_selector(title_selector)
await page.fill(title_selector, article["title"])
# 本文入力
body_editor = 'div[data-placeholder="本文を入力してください"]'
await page.click(body_editor)
await page.keyboard.press("Control+a")
await page.keyboard.type(article["body"], delay=10)
# タグ入力
for tag in article["tags"]:
tag_input = 'input[placeholder="タグを追加"]'
await page.wait_for_selector(tag_input)
await page.fill(tag_input, tag)
await page.keyboard.press("Enter")
await asyncio.sleep(0.5)
# 予約投稿設定
if article["scheduled"]:
# 「投稿設定」ボタンをクリック
await page.click('button:has-text("投稿設定")')
await asyncio.sleep(1)
# 予約投稿ラジオボタンを選択
await page.click('label:has-text("予約投稿")')
await asyncio.sleep(0.5)
# 日時入力フィールドに値を設定
scheduled_dt = article["scheduled"] # "2025-02-01 10:00"
date_part, time_part = scheduled_dt.split(" ")
year, month, day = date_part.split("-")
hour, minute = time_part.split(":")
# 日付・時刻の入力(UIに応じてセレクタを調整してください)
await page.fill('input[name="scheduledDate"]', f"{year}/{month}/{day}")
await page.fill('input[name="scheduledTime"]', f"{hour}:{minute}")
await asyncio.sleep(0.5)
# 公開ボタン押下
await page.click('button:has-text("公開する")')
await asyncio.sleep(2)
print(f"投稿完了: {article['title']}")
async def main():
md_files = sorted(glob.glob(f"{POSTS_DIR}/*.md"))
if not md_files:
print("投稿するMarkdownファイルが見つかりません")
return
async with async_playwright() as p:
browser = await p.chromium.launch(headless=True)
context = await browser.new_context(storage_state=SESSION_FILE)
page = await context.new_page()
for md_file in md_files:
print(f"処理中: {md_file}")
article = parse_markdown(md_file)
await post_article(page, article)
await asyncio.sleep(60) # 記事間に60秒の待機時間を設ける
await browser.close()
print("全記事の投稿が完了しました")
asyncio.run(main())
このスクリプトのキーポイントをいくつか補足します。
storage_state=SESSION_FILE:保存済みのセッション情報を読み込み、ログイン済み状態でブラウザを起動しますdelay=10:キーボード入力に10ミリ秒の遅延を設けることで、人間の入力に近いペースを再現していますasyncio.sleep(60):記事間の待機時間。サーバー負荷を抑えるために必ず入れてください- 予約投稿UIの操作:note.comのUIは定期的に変更される可能性があります。セレクタ(HTML要素を特定するための識別子)が変わった場合は後述のデバッグ手順で確認・修正してください
また、外部の参考実装としてClaude Codeからnoteに自動投稿するPlaywright完全ガイド(株式会社Uravation)やPlaywright ブラウザ自動化ツールでnoteに自動投稿する(Qiita)も参考になります。さらに、PythonライブラリとしてNote Clientというアプローチ(なおくんのブログ)もあるので、自分のスキルレベルに合わせて選択するとよいでしょう。
フェーズ4:動作確認・トラブルシュートと応用展開
ヘッドレスモードOFFで目視確認する
スクリプトがうまく動かない場合、まずheadless=Falseに変更して実際のブラウザ画面を表示しながら動作を確認しましょう。どのステップで止まっているか一目でわかります。
browser = await p.chromium.launch(headless=False, slow_mo=500)
slow_mo=500(各操作の間に500ミリ秒の遅延を入れるオプション)を追加すると、操作がスローモーションになり問題箇所を特定しやすくなります。また、スクリーンショットを保存することでCI/CD(継続的インテグレーション/デリバリー)環境でのデバッグにも役立ちます。
# エラー発生時にスクリーンショットを保存する例
try:
await post_article(page, article)
except Exception as e:
await page.screenshot(path=f"error_{article['title'][:20]}.png")
print(f"エラー発生: {e}")
よくあるエラーと対処法
① セレクタが見つからない(TimeoutError)
note.comのUIが更新されてHTMLの構造が変わると、セレクタが機能しなくなります。ブラウザの開発者ツール(F12キー)で実際のHTML要素を確認し、セレクタを更新してください。page.locator()のテキストベースセレクタ(例:page.get_by_text("投稿する"))はUI変更に強いのでおすすめです。
② ログインセッション切れ
長期間使用しているとsession.jsonのCookieが期限切れになります。その場合はlogin.pyを再実行してセッションを更新してください。GitHub Actionsで定期実行する場合は、セッションの有効期限を考慮して定期的に再ログインする仕組みを組み込みましょう。
③ 本文の入力が途中で止まる
長文の場合、keyboard.type()の入力が途中で途切れることがあります。page.evaluate()でJavaScriptを直接実行してテキストをセットする方法も有効です。
# JavaScriptで直接テキストをセットする方法
await page.evaluate(
"(el, text) => { el.innerText = text; el.dispatchEvent(new Event('input')); }",
await page.query_selector(body_editor),
article["body"]
)
GitHub ActionsやCronジョブと組み合わせた完全自動化
スクリプトが安定して動くようになったら、次のステップは「スケジュール実行」です。
Cronジョブ(Linux/Mac)での設定例:
# 毎週月曜・水曜・金曜の朝9時に実行
0 9 * * 1,3,5 cd /path/to/note-autoposter && python post_note.py >> logs/post.log 2>&1
GitHub Actionsでの設定例(.github/workflows/post.yml):
name: Auto Post to note
on:
schedule:
- cron: '0 0 * * 1,3,5' # UTC 0:00 = JST 9:00
workflow_dispatch: # 手動実行も可能
jobs:
post:
runs-on: ubuntu-latest

コメント