API自動化 Python

Pythonでnoteにログインする方法|Cookie認証とreCAPTCHAの壁

認証の突破

ログインAPIを叩いても、Cookieは返ってこない。だが、認証を通す方法はある。

ブラウザで取得したセッションを、Pythonに引き継ぐという発想です

📋 この記事でわかること

  • noteの認証がCookieセッション方式である仕組み
  • ログインAPIを直接叩いても通らない理由
  • ブラウザからCookieを取り出してPythonで使う手順
  • Cookieを安全に保存・再利用するコード
  • Playwrightでログインを自動化する方法
  • Cookie失効の検知と、認証情報の管理ルール

note非公式APIには、ログインしなくても使えるエンドポイントと、認証が必須のエンドポイントがあります。自分の下書き一覧、PV統計、記事の投稿・編集といった重要な操作はすべて後者です。noteの認証はCookieベースのセッション方式で、ログインすると _note_session_v5 という名前のCookieが発行され、以降のリクエストではこれが身分証明として機能します。問題は、そのCookieを取得する過程です。ログインAPIをPythonから直接叩く方法は、reCAPTCHAの導入によって実質的に塞がれました。この記事では、その現実を踏まえたうえで実務的に動く認証手段を解説します。

この記事を読む前に

  1. ここで扱うのは自分のアカウントへの認証です。他人のアカウントに対して行えば不正アクセスにあたります
  2. Cookieはパスワードと同等の重要情報です。コードに直書きせず、共有もしないでください
  3. 非公式APIのため、認証方式は予告なく変更される可能性があります
  4. 基本的な使い方はnote非公式API入門で解説しています

noteの認証はどう成り立っているか

noteは、リクエストヘッダーに含まれるCookieでユーザーを識別します。ブラウザでログインすると、サーバーからセッションCookieが発行され、以降そのブラウザから送られるリクエストには自動的にCookieが添付されます。サーバー側はCookieの値からセッションを引き当て、「これは誰のリクエストか」を判定しています。

認証方式 仕組み Pythonからの扱いやすさ
APIキー 固定の文字列をヘッダーに付ける 非常に簡単
OAuth 認可フローを経てトークンを取得 手順は多いが確立している
Cookieセッション(noteはこれ) ログイン後に発行されるCookieを使う ログイン処理の突破が課題になる

Cookieセッション方式そのものは難しくありません。有効なCookieさえ手元にあれば、requestsから認証済みのリクエストを送れます。難しいのは、そのCookieをプログラムから取得する部分です。

ログインAPIを直接叩くとどうなるか

noteのログインは POST /api/v1/sessions/sign_in というエンドポイントで処理されます。パラメータはメールアドレスとパスワード、そしてreCAPTCHAのトークンです。まず、この形を確認しておきます。

01_signin_direct.py(参考・現在は通らない)
"""
sign_in APIを直接叩く例。
現在は reCAPTCHA トークンが必須のため、この方法では
2xx が返ってもセッションCookieが発行されない。
仕組みの理解のために掲載している。
"""
import os

import requests
from dotenv import load_dotenv

load_dotenv()

UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
      "AppleWebKit/537.36 (KHTML, like Gecko) "
      "Chrome/120.0.0.0 Safari/537.36")


def try_sign_in(login: str, password: str) -> requests.Session:
    session = requests.Session()
    session.headers.update({
        "User-Agent": UA,
        "Content-Type": "application/json",
        "Accept": "application/json",
    })

    payload = {
        "login": login,
        "password": password,
        # 本来はここに reCAPTCHA v3 のトークンが必要
        # "g_recaptcha_response": "...",
    }

    res = session.post(
        "https://note.com/api/v1/sessions/sign_in",
        json=payload,
        timeout=20,
    )

    print("ステータス:", res.status_code)
    print("受け取ったCookie:", session.cookies.get_dict().keys())

    # 重要: ステータスが200でも _note_session_v5 が無ければ失敗
    if "_note_session_v5" not in session.cookies.get_dict():
        print("→ セッションCookieが発行されていません")

    return session


if __name__ == "__main__":
    try_sign_in(
        os.getenv("NOTE_EMAIL", ""),
        os.getenv("NOTE_PASSWORD", ""),
    )

このコードを実行すると、多くの場合ステータスコードは200系が返ります。しかし session.cookies を確認すると _note_session_v5 が含まれていません。「エラーにはならないが、認証もされない」という、もっとも気づきにくい失敗の形です。

ステータス200を成功と判定してはいけない

非公式APIを扱ううえで重要な教訓です。HTTPステータスコードだけを見て成功判定すると、このケースを見逃します。認証処理の後は必ず「目的のCookieが実際に得られたか」を検証してください。次章以降のコードでは、すべてこの検証を入れています。

現実的な解:ブラウザのCookieを使う

reCAPTCHAは「人間が操作していること」を検証する仕組みなので、プログラムから正規のトークンを得るのは困難です。そこで発想を変えます。ログインはブラウザで人間が行い、その結果得られたCookieだけをPythonに渡すという方法です。

  • 1
    ブラウザでnoteに普通にログインします。ここは手作業で構いません。
  • 2
    F12でDevToolsを開き、「アプリケーション」タブ(Chromeの場合)を選択します。
  • 3
    左メニューの Cookies → https://note.com を選択すると、保存されているCookieの一覧が表示されます。
  • 4
    _note_session_v5 という名前の行を探し、その Value をコピーします。長い文字列です。
  • 5
    その値を .env ファイルに保存し、Pythonから読み込んで使います。

まず、認証情報を置くファイルを用意します。.env はコードとは別のファイルで、Gitで管理しないよう除外設定するのが鉄則です。

.env
# noteのセッションCookie(DevToolsからコピーした値)
NOTE_SESSION=ここに_note_session_v5の値を貼る

# 自分のnote ID(プロフィールURLの note.com/【ここ】)
NOTE_URLNAME=your_note_id
.gitignore
# 認証情報は絶対にコミットしない
.env
cookies.json
*.log

# Python
__pycache__/
venv/
*.pyc

Cookieを載せて疎通確認する

Cookieが手元にあれば、あとは requests.Session に設定するだけです。認証が通っているかを確認する最短の方法は、/api/v2/current_user を叩くことです。

02_check_auth.py
import os

import requests
from dotenv import load_dotenv

load_dotenv()

UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
      "AppleWebKit/537.36 (KHTML, like Gecko) "
      "Chrome/120.0.0.0 Safari/537.36")


def build_session(session_cookie: str) -> requests.Session:
    """セッションCookieを載せた Session を返す"""
    s = requests.Session()
    s.headers.update({
        "User-Agent": UA,
        "Accept": "application/json",
    })
    s.cookies.set("_note_session_v5", session_cookie, domain="note.com")
    return s


def check_auth(session: requests.Session) -> dict:
    """ログイン状態を確認する。失敗なら空dictを返す"""
    res = session.get("https://note.com/api/v2/current_user", timeout=15)

    if res.status_code != 200:
        print(f"認証失敗: HTTP {res.status_code}")
        return {}

    data = res.json().get("data", {})
    if not data.get("id"):
        print("認証失敗: ユーザー情報が空です(Cookieが無効の可能性)")
        return {}

    return data


if __name__ == "__main__":
    cookie = os.getenv("NOTE_SESSION", "")
    if not cookie:
        raise SystemExit(".env に NOTE_SESSION を設定してください")

    sess = build_session(cookie)
    user = check_auth(sess)

    if user:
        print("ログイン成功")
        print("  ユーザーID :", user.get("id"))
        print("  note ID    :", user.get("urlname"))
        print("  表示名     :", user.get("nickname"))
        print("  フォロワー :", user.get("followerCount",
                                          user.get("follower_count", "-")))

「ログイン成功」と表示され、自分の情報が出れば認証は通っています。ここから先、認証が必要なすべてのエンドポイントを叩けるようになります。

domainの指定を忘れない

s.cookies.set()domain="note.com" を指定しています。これを省くと、リクエスト先によってCookieが送られないことがあります。サブドメインを含めて確実に送りたい場合は domain=".note.com" と先頭にドットを付ける書き方もあります。

認証付きクライアントクラスを作る

前回の記事で作った NoteClient を拡張し、認証に対応させます。以降の記事では、このクラスを使い回します。

note_auth_client.py
"""
認証付き note クライアント
- .env からセッションCookieを読み込む
- 起動時にログイン状態を検証する
- Cookie失効を検知して分かりやすく通知する
"""
import os
import time
import random
import logging

import requests
from dotenv import load_dotenv

load_dotenv()

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
)
logger = logging.getLogger(__name__)

BASE_URL = "https://note.com"
DEFAULT_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
              "AppleWebKit/537.36 (KHTML, like Gecko) "
              "Chrome/120.0.0.0 Safari/537.36")


class CookieExpiredError(Exception):
    """セッションCookieが無効になったときに投げる"""


class NoteAuthClient:

    def __init__(self, session_cookie: str = None,
                 interval: float = 1.2, max_retry: int = 3):
        self.session = requests.Session()
        self.session.headers.update({
            "User-Agent": DEFAULT_UA,
            "Accept": "application/json",
        })

        cookie = session_cookie or os.getenv("NOTE_SESSION", "")
        if not cookie:
            raise ValueError("セッションCookieが指定されていません")

        self.session.cookies.set("_note_session_v5", cookie, domain="note.com")

        self.interval = interval
        self.max_retry = max_retry
        self._last_request_at = 0.0
        self.user = None

    # ---- 内部処理 ----

    def _wait(self):
        elapsed = time.time() - self._last_request_at
        if elapsed < self.interval:
            time.sleep(self.interval - elapsed)
        self._last_request_at = time.time()

    def _backoff(self, attempt: int, base: float = 1.5):
        wait = base * (2 ** (attempt - 1)) + random.uniform(0, 1.0)
        logger.info("%.1f 秒待機します", wait)
        time.sleep(wait)

    def request(self, method: str, path: str, **kwargs) -> requests.Response:
        url = f"{BASE_URL}{path}"
        kwargs.setdefault("timeout", 20)

        for attempt in range(1, self.max_retry + 1):
            self._wait()
            try:
                res = self.session.request(method, url, **kwargs)
            except requests.RequestException as e:
                logger.warning("通信エラー(%s/%s): %s", attempt, self.max_retry, e)
                self._backoff(attempt)
                continue

            if res.status_code == 401:
                raise CookieExpiredError(
                    "認証が切れています。.env の NOTE_SESSION を更新してください"
                )
            if res.status_code == 429:
                logger.warning("レート制限(429)")
                self._backoff(attempt, base=5.0)
                continue
            if 500 <= res.status_code < 600:
                logger.warning("サーバーエラー(%s)", res.status_code)
                self._backoff(attempt)
                continue

            return res

        raise RuntimeError(f"リトライ上限: {method} {path}")

    # ---- 公開メソッド ----

    def get_json(self, path: str, params: dict = None) -> dict:
        res = self.request("GET", path, params=params)
        if res.status_code != 200:
            logger.error("HTTP %s: %s", res.status_code, path)
            return {}
        try:
            return res.json().get("data", {})
        except ValueError:
            logger.error("JSONとして解釈できません: %s", path)
            return {}

    def post_json(self, path: str, payload: dict = None,
                  params: dict = None) -> dict:
        res = self.request("POST", path, json=payload, params=params)
        if res.status_code not in (200, 201):
            logger.error("HTTP %s: %s / %s",
                         res.status_code, path, res.text[:200])
            return {}
        try:
            return res.json().get("data", {})
        except ValueError:
            return {}

    def verify(self) -> dict:
        """ログイン状態を検証し、ユーザー情報を保持する"""
        data = self.get_json("/api/v2/current_user")
        if not data.get("id"):
            raise CookieExpiredError("ログインできていません")
        self.user = data
        logger.info("ログイン確認: %s (@%s)",
                    data.get("nickname"), data.get("urlname"))
        return data


if __name__ == "__main__":
    client = NoteAuthClient()
    client.verify()

ポイントは CookieExpiredError という専用の例外を用意していることです。認証切れは定期実行スクリプトでもっともよく起きる障害なので、他のエラーと区別して検知できる形にしておくと運用が楽になります。

▶ 認証が通ったら、次は自分の記事データを一括で取り出す番です。
全記事をCSVに落とす手順を解説しています。

全記事の一括取得を見る

Cookieをファイルに保存して再利用する

毎回 .env を書き換えるのは面倒です。取得したCookieをJSONファイルに保存し、有効期限を管理する仕組みを作っておくと運用が安定します。

cookie_store.py
"""
Cookieの保存・読み込みを管理する。
cookies.json は .gitignore に必ず追加すること。
"""
import json
import os
from datetime import datetime, timedelta

COOKIE_FILE = "cookies.json"
# noteのセッションは長期間有効だが、安全のため定期的に更新する
VALID_DAYS = 14


def save_cookie(value: str, path: str = COOKIE_FILE):
    """Cookieを取得日時つきで保存する"""
    data = {
        "_note_session_v5": value,
        "saved_at": datetime.now().isoformat(),
    }
    with open(path, "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)

    # 他ユーザーから読めないようにパーミッションを絞る(Unix系)
    try:
        os.chmod(path, 0o600)
    except OSError:
        pass

    print(f"{path} に保存しました")


def load_cookie(path: str = COOKIE_FILE) -> str:
    """保存済みCookieを読み込む。古い場合は警告を出す"""
    if not os.path.exists(path):
        raise FileNotFoundError(
            f"{path} がありません。save_cookie() で保存してください"
        )

    with open(path, encoding="utf-8") as f:
        data = json.load(f)

    saved_at = datetime.fromisoformat(data["saved_at"])
    age = datetime.now() - saved_at

    if age > timedelta(days=VALID_DAYS):
        print(f"警告: Cookie保存から {age.days} 日経過しています。"
              "更新を検討してください")

    return data["_note_session_v5"]


if __name__ == "__main__":
    # 初回だけ実行してCookieを保存する
    value = input("_note_session_v5 の値を貼り付けてください: ").strip()
    if value:
        save_cookie(value)
        print("読み込みテスト:", load_cookie()[:20] + "...")

ファイルのパーミッションを 0o600(所有者のみ読み書き可)に設定しています。Windowsでは効果がありませんが、Linuxサーバーで動かす場合は重要な設定です。

ブラウザのCookieを自動で取り出す

手動コピーが面倒な場合、ブラウザが保存しているCookieをPythonから直接読み出す方法もあります。browser_cookie3 というライブラリを使います。

03_browser_cookie.py
"""
インストール:
    pip install browser-cookie3

ブラウザにログイン済みであれば、そのCookieを読み出せる。
OSやブラウザのバージョンによっては読めない場合がある。
"""
import browser_cookie3


def get_note_session_from_browser(browser: str = "chrome") -> str:
    """ブラウザから note.com のセッションCookieを取り出す"""
    loaders = {
        "chrome": browser_cookie3.chrome,
        "edge": browser_cookie3.edge,
        "firefox": browser_cookie3.firefox,
    }

    loader = loaders.get(browser)
    if loader is None:
        raise ValueError(f"未対応のブラウザです: {browser}")

    jar = loader(domain_name="note.com")

    for cookie in jar:
        if cookie.name == "_note_session_v5":
            return cookie.value

    raise RuntimeError(
        "セッションCookieが見つかりません。"
        "そのブラウザでnoteにログインしているか確認してください"
    )


if __name__ == "__main__":
    for name in ("chrome", "edge", "firefox"):
        try:
            value = get_note_session_from_browser(name)
            print(f"{name}: 取得成功(先頭20文字 {value[:20]}...)")
            break
        except Exception as e:
            print(f"{name}: {e}")

この方法は便利ですが、環境依存が強く、ブラウザのバージョンアップで読めなくなることがあります。本番運用では手動コピー+ファイル保存のほうが安定します。

Playwrightでログインを自動化する

より確実な自動化を目指すなら、実際のブラウザを操作してログインし、そこからCookieを取り出す方法があります。reCAPTCHAは本物のブラウザ操作であれば通過できる場合が多いためです。

04_playwright_login.py
"""
インストール:
    pip install playwright
    playwright install chromium

実ブラウザでログインし、セッションCookieを取り出す。
headless=False にすると画面が見えるので、初回は目視推奨。
"""
import os

from dotenv import load_dotenv
from playwright.sync_api import sync_playwright

from cookie_store import save_cookie

load_dotenv()

LOGIN_URL = "https://note.com/login"


def login_and_get_cookie(email: str, password: str,
                         headless: bool = False) -> str:
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=headless)
        context = browser.new_context()
        page = context.new_page()

        page.goto(LOGIN_URL, wait_until="networkidle")

        # 入力欄のセレクタは変更される可能性があるため複数試す
        for selector in ('input[type="email"]',
                         'input[name="login"]',
                         '#email'):
            if page.locator(selector).count():
                page.fill(selector, email)
                break

        for selector in ('input[type="password"]',
                         'input[name="password"]',
                         '#password'):
            if page.locator(selector).count():
                page.fill(selector, password)
                break

        page.click('button[type="submit"]')

        # ログイン完了まで待つ(URLの遷移を確認)
        try:
            page.wait_for_url(lambda url: "/login" not in url, timeout=30000)
        except Exception:
            print("自動遷移しませんでした。画面で操作を完了してください")
            page.wait_for_timeout(30000)

        cookies = context.cookies()
        browser.close()

    for c in cookies:
        if c["name"] == "_note_session_v5":
            return c["value"]

    raise RuntimeError("セッションCookieを取得できませんでした")


if __name__ == "__main__":
    email = os.getenv("NOTE_EMAIL", "")
    password = os.getenv("NOTE_PASSWORD", "")

    if not email or not password:
        raise SystemExit(".env に NOTE_EMAIL / NOTE_PASSWORD を設定してください")

    value = login_and_get_cookie(email, password, headless=False)
    save_cookie(value)
    print("Cookieを保存しました")

ブラウザ自動操作を使うときの注意

  1. 頻繁に実行しない。ログイン処理は必要なときだけ行い、取得したCookieを使い回す
  2. reCAPTCHAが表示された場合は、headless=False で人間が対応する
  3. 2段階認証を有効にしている場合、自動ログインは通りません。コード入力の手動介在が必要です
  4. ログイン失敗を繰り返すと、アカウント保護のため一時的にロックされることがあります

実務的には、Playwrightでのログインは月に1回程度、Cookie更新のためだけに手動で実行するという使い方が現実的です。日々のスクリプトは保存済みCookieで動かします。

認証が必要なエンドポイントを叩く

認証が通れば、ログイン状態でしか見えない情報にアクセスできます。代表的なものを実際に取得してみます。

05_authed_endpoints.py
from note_auth_client import NoteAuthClient, CookieExpiredError


def show_current_user(client: NoteAuthClient):
    user = client.get_json("/api/v2/current_user")
    print("=== 自分の情報 ===")
    print("  note ID   :", user.get("urlname"))
    print("  表示名    :", user.get("nickname"))
    print("  記事数    :", user.get("noteCount", user.get("note_count", "-")))


def show_notice_counts(client: NoteAuthClient):
    data = client.get_json("/api/v3/notice_counts")
    print("=== 通知 ===")
    print("  未読通知  :", data.get("notificationCount",
                                    data.get("notification_count", 0)))
    print("  未読DM    :", data.get("messageCount",
                                    data.get("message_count", 0)))


def show_my_magazines(client: NoteAuthClient):
    data = client.get_json("/api/v1/my/magazines")
    magazines = data.get("magazines", data if isinstance(data, list) else [])
    print("=== マガジン ===")
    for m in magazines[:10]:
        name = m.get("name", "")
        count = m.get("notesCount", m.get("notes_count", 0))
        print(f"  {name}({count}本)")


def show_pv_stats(client: NoteAuthClient):
    data = client.get_json(
        "/api/v1/stats/pv",
        params={"filter": "all", "page": 1, "sort": "pv"},
    )
    notes = data.get("note_stats", data.get("noteStats", []))
    print("=== PV上位 ===")
    for n in notes[:5]:
        print(f"  {n.get('pv', 0):>6} PV | {n.get('name', '')}")


if __name__ == "__main__":
    try:
        client = NoteAuthClient()
        client.verify()

        show_current_user(client)
        show_notice_counts(client)
        show_my_magazines(client)
        show_pv_stats(client)

    except CookieExpiredError as e:
        print("認証エラー:", e)
        print("→ ブラウザで再ログインし、Cookieを取り直してください")

このスクリプトが一通り動けば、認証まわりの実装は完成です。ここから先は目的に応じてエンドポイントを追加していくだけになります。

Cookie失効を検知して通知する

定期実行するスクリプトでもっとも困るのが、気づかないうちにCookieが失効して処理が止まっているケースです。失効を検知したら能動的に知らせる仕組みを入れておきます。

06_expire_check.py
"""
Cookieの生存確認を行い、切れていたら通知する。
タスクスケジューラやcronで1日1回実行する想定。
"""
import sys
import logging
from datetime import datetime

import requests

from note_auth_client import NoteAuthClient, CookieExpiredError

logging.basicConfig(
    filename="note_auth.log",
    level=logging.INFO,
    format="%(asctime)s [%(levelname)s] %(message)s",
    encoding="utf-8",
)

# 通知先(Discord Webhook の例。不要なら空文字のまま)
WEBHOOK_URL = ""


def notify(message: str):
    """Webhookで通知する。設定が無ければ標準出力のみ"""
    print(message)
    if not WEBHOOK_URL:
        return
    try:
        requests.post(WEBHOOK_URL, json={"content": message}, timeout=10)
    except requests.RequestException as e:
        logging.warning("通知の送信に失敗: %s", e)


def main() -> int:
    try:
        client = NoteAuthClient()
        user = client.verify()
    except CookieExpiredError:
        msg = ("【note API】セッションCookieが失効しました。"
               "ブラウザで再ログインして .env を更新してください")
        logging.error(msg)
        notify(msg)
        return 1
    except Exception as e:
        msg = f"【note API】想定外のエラー: {e}"
        logging.exception(msg)
        notify(msg)
        return 2

    stamp = datetime.now().strftime("%Y-%m-%d %H:%M")
    logging.info("認証OK: @%s (%s)", user.get("urlname"), stamp)
    print(f"認証OK: @{user.get('urlname')}")
    return 0


if __name__ == "__main__":
    sys.exit(main())

終了コードを返しているのがポイントです。タスクスケジューラやCIから呼び出したとき、失敗を検知して次の処理を止められるようになります。

認証情報の管理ルール

セッションCookieは、パスワードと同等かそれ以上に危険な情報です。これを他人が入手すれば、パスワードを知らなくても自分のアカウントとして操作できてしまいます。

やること 理由
.env / cookies.json を .gitignore に入れる GitHubへの流出事故が実際に多い
コードに直書きしない コピーして共有した瞬間に漏れる
スクリーンショットに写さない ブログやSNSでの公開事故を防ぐ
共有PCで実行しない ファイルが他ユーザーから読める可能性
不要になったらブラウザでログアウトする ログアウトでセッションが無効化される
ログに出力しない ログファイルが流出経路になる

もしCookieが漏れたら

すぐにブラウザでnoteからログアウトしてください。ログアウトによってサーバー側のセッションが破棄されるため、漏れたCookieも同時に無効化されます。あわせてパスワードの変更と、2段階認証の設定を行ってください。2段階認証を有効にしておけば、パスワードが漏れた場合の被害も抑えられます。

▼ RECOMMENDATION ▼

認証まわりの手間そのものを無くすという選択

ここまで読んで分かるとおり、noteの自動化は認証を通すだけでも相応の手間がかかります。アメブロであれば、アメプレスProがログインからいいね・フォロー・アクセス獲得までを一括で処理します。Cookieの管理もスクリプトの保守も不要で、設定するだけで24時間動きます。noteと違ってASPアフィリエイトも使えるため、収益化まで含めた効率は大きく変わります。月額2,980円、365日LINEサポート付きです。

✦ 認証・保守が不要 ✦ 365日LINEサポート ✦ 複数ブログ対応 ✦ WordPress連携
アメプレスPro 公式ページを確認する →

※ 当サイトはアフィリエイト広告を含みます。

認証が通らないときの切り分け手順

うまくいかない場合は、上から順に確認してください。原因はほぼこの範囲に収まります。

症状 確認すること 対処
401が返る Cookieの値が正しいか ブラウザで取り直す
current_userが空 ログアウトしていないか 再ログインしてCookie更新
Cookie設定したのに未認証 domain指定を忘れていないか domain="note.com" を明示
値をコピーしたのに動かない 前後に空白や改行が入っていないか .strip() で除去する
昨日まで動いていた ブラウザでログアウトしなかったか Cookieを取り直す
403が返る そもそも権限のある操作か 操作対象と条件を見直す

特に多いのが、コピーした値の前後に空白が混ざっているケースです。.env から読み込んだ値は、必ず .strip() を通しておくと安全です。

07_debug_auth.py
"""
認証がうまくいかないときの診断スクリプト。
どこで失敗しているかを段階的に確認する。
"""
import os

import requests
from dotenv import load_dotenv

load_dotenv()

UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
      "AppleWebKit/537.36 (KHTML, like Gecko) "
      "Chrome/120.0.0.0 Safari/537.36")


def diagnose():
    print("--- 1. .env の読み込み ---")
    raw = os.getenv("NOTE_SESSION")
    if raw is None:
        print("  NG: NOTE_SESSION が読めていません(.env の場所を確認)")
        return
    print(f"  OK: {len(raw)} 文字を読み込みました")

    print("--- 2. 値の前後の空白 ---")
    cleaned = raw.strip()
    if raw != cleaned:
        print(f"  注意: 前後に空白があります({len(raw)} → {len(cleaned)} 文字)")
    else:
        print("  OK: 余計な空白はありません")

    print("--- 3. 認証なしリクエスト ---")
    res = requests.get("https://note.com/api/v2/current_user",
                       headers={"User-Agent": UA}, timeout=15)
    print(f"  Cookieなし: HTTP {res.status_code}(401や空なら想定どおり)")

    print("--- 4. 認証ありリクエスト ---")
    s = requests.Session()
    s.headers.update({"User-Agent": UA})
    s.cookies.set("_note_session_v5", cleaned, domain="note.com")

    res = s.get("https://note.com/api/v2/current_user", timeout=15)
    print(f"  Cookieあり: HTTP {res.status_code}")

    if res.status_code == 200:
        data = res.json().get("data", {})
        if data.get("id"):
            print(f"  OK: @{data.get('urlname')} として認証されています")
        else:
            print("  NG: 200が返りましたが、ユーザー情報が空です")
            print("      → Cookieが無効です。取り直してください")
    else:
        print("  NG: 認証に失敗しています")
        print("      レスポンス:", res.text[:200])


if __name__ == "__main__":
    diagnose()

まとめ:ログインは人間、実行はプログラム

noteの非公式APIで認証を通す現実的な方法は、ブラウザでログインして得たCookieをPythonに引き継ぐことです。ログインAPIを直接叩く方法はreCAPTCHAによって塞がれており、無理に突破しようとするより、役割を分けたほうが安定します。

この記事の要点

  1. noteの認証は _note_session_v5 というCookieで行われる
  2. sign_in APIは200が返ってもCookieが発行されない(reCAPTCHA必須)
  3. DevToolsの「アプリケーション」タブからCookieを取り出す
  4. session.cookies.set() の際は domain を明示する
  5. 認証確認は /api/v2/current_user で行う
  6. Cookieは .env または専用ファイルで管理し、必ず .gitignore に入れる
  7. 失効は専用の例外として検知し、通知する仕組みを入れる
  8. ログインの自動化はPlaywrightで可能だが、実行は最小限に留める

認証が通れば、note APIでできることの範囲は一気に広がります。次の記事では、この認証を使って自分の全記事データを取得し、手元に保存する手順を解説します。

FAQ|noteのAPI認証についてよくある質問

Q. メールアドレスとパスワードだけでログインできませんか?

現在は難しい状況です。/api/v1/sessions/sign_in はreCAPTCHAのトークンを要求するようになっており、トークン無しでリクエストすると2xxが返るもののセッションCookieが発行されません。エラーにならないぶん原因に気づきにくいので、認証後は必ずCookieの有無を確認してください。実務ではブラウザで取得したCookieを使う方法が確実です。

Q. Cookieの有効期限はどのくらいですか?

明示された期限は公開されていませんが、実感としては数週間から数ヶ月は有効です。ただし、ブラウザでログアウトした時点で即座に無効化されます。パスワードを変更した場合も無効になります。定期実行するスクリプトでは、実行のたびに current_user で生存確認を行い、切れていたら通知する仕組みを入れておくのが安全です。

Q. Cookieを使うのは危険ではありませんか?

取り扱いを誤れば危険です。セッションCookieを入手した第三者は、パスワードを知らなくてもあなたのアカウントとして操作できます。.gitignore への追加、コードへの直書き禁止、スクリーンショットへの写り込み防止を徹底してください。万一漏れた場合は、ブラウザからログアウトすればセッションが破棄され、そのCookieは無効になります。

Q. 2段階認証を有効にしていても使えますか?

使えます。2段階認証はログイン時の検証に関わるもので、ログイン後に発行されるセッションCookieの扱いは変わりません。ブラウザで2段階認証を通してログインし、そこで得たCookieを使えば通常どおり動作します。ただしPlaywrightによる完全自動ログインは、確認コードの入力が必要になるため成立しません。この場合は手動でCookieを取得する運用にしてください。

Q. 複数アカウントを扱えますか?

アカウントごとにCookieを用意すれば可能です。NoteAuthClient(session_cookie=...) のようにコンストラクタへ個別に渡す設計にしてあるのはこのためです。ただし、複数アカウントを機械的に運用して相互にスキやフォローを行うような使い方は、スパム判定の対象になります。あくまで自分が正当に運用しているアカウントの管理範囲で使ってください。

Q. Cookieを設定したのに401が返ります

まず値の前後に空白や改行が入っていないか確認してください。.env からの読み込み時に .strip() を通すと解決することが多いです。次に domain="note.com" を指定しているか確認します。それでも駄目なら、ブラウザでログアウトしていないか、別の端末でログインし直していないかを確認し、Cookieを取り直してください。記事内の診断スクリプトを使うと切り分けが早まります。

Q. サーバーで定期実行したいのですが認証はどうしますか?

ローカルのブラウザで取得したCookieを、サーバー上の .env に設定する形になります。サーバーにブラウザを入れる必要はありません。ファイルのパーミッションを所有者のみ読み書き可能に設定し、失効時に通知が飛ぶようにしておけば運用できます。更新の頻度は数週間から数ヶ月に1回程度なので、手間としては現実的な範囲です。

Q. browser_cookie3が動きません

このライブラリはブラウザのCookie保存形式に依存しているため、ブラウザやOSのバージョンによって読めなくなることがあります。Chromeの場合、暗号化方式の変更で失敗するケースが知られています。動かない場合は無理に対応せず、DevToolsから手動でコピーする方法に切り替えてください。認証は頻繁に行う処理ではないため、手動でも実用上の問題は小さいはずです。

Q. Playwrightでのログインは規約違反になりませんか?

自分のアカウントに、自分の認証情報でログインする行為そのものが問題になることは考えにくいでしょう。問題になるのは頻度と目的です。短時間に何度もログインを試みる、複数アカウントを機械的に量産するといった使い方は、明確にリスクがあります。ログイン処理は月に1回程度のCookie更新に留め、日常の処理は保存済みCookieで動かす設計にしてください。

Q. 認証なしでできることはどこまでですか?

公開されている記事の取得、クリエイター情報、検索、ハッシュタグ、カテゴリ一覧などは認証なしで取得できます。リサーチや分析が目的であれば、認証を通さずに済むケースは多いはずです。認証が必要になるのは、自分の下書き・PV統計・投稿や編集・マガジン管理・メンバーシップ運営といった、自分のアカウントに紐づく操作です。目的に照らして、認証が本当に必要かを先に検討してください。

アメプレスラボ編集部

AMEPRESS LAB EDITORIAL TEAM

アメブロ・note・WordPressを実際に運営し、それぞれの収益構造を数字で検証しているチーム。プラットフォームごとの得意・不得意を踏まえた、宣伝色に偏らない実践情報の発信を続けています。