見出し画像

【実践】note APIを使って自動でスキを押すPythonを書く

目次

表題の通り、note API(非公式)を利用したPython3のスクリプトを作りました。途中初見殺しの謎仕様があって作成には若干詰まりましたが、スクリプト自体はシンプルなものです。ザっと見てくれればnote APIの概要も理解できるかと思います。



スクリプト作成要件

note APIを使って、自分のフォロワーが書いた記事をスキしまくるPythonスクリプトを作ります。Seleniumなどのブラウザ自動化モジュールは使いません。一日1回の実行を想定しています。LinuxのcronやWindowsのタスクスケジューラで実行すれば自動化できますね。


処理の流れ

①noteにログインする
②ログインユーザのフォロワー情報を取得する
③②で取得したフォロワーの記事を取得する(過去1日分)
④③で取得した記事にスキをする


あらかじめ準備しておくもの

三つの情報、"user_name"、"email_address"、"password"を用意してください。user_nameはnote内にある自分のクリエイターページに記載されるURLに表示されます。自分のニックネーム(いつも表示される名前)とは違うので注意してください。email_addressはログイン時のメールアドレスです。


出来上がったコード

いきなりできたコードを記載します。requestsが入ってなかったらpipで予めインストールしておいてください。スクリプト内に記載されている変数の"user_name"、"email_address"、"password"は置き換えてください。

import requests
import json
import traceback
import time
from datetime import datetime,timedelta

####################################################
# フォロワーの記事をいいねを押すスクリプト
####################################################

####################################################
# Variable
####################################################
user_name = "ユーザ名"
email_address = "ログイン用メールアドレス"
password = "パスワード"

# スキ実行間隔(秒)
sleep_time = 1

####################################################
# Function 
####################################################

# noteの認証を実施する
def note_auth(session):
    
    user_data = {
        "login":    email_address,
        "password": password
    }

    # 認証
    url = 'https://note.com/api/v1/sessions/sign_in'
    r = session.post(url,json=user_data)

    # ログインエラー判定
    r2 = json.loads(r.text)
    if "error" in r2:
        raise Exception("Login Error")
    else:
        return session

# APIからデータを取得する関数
def get_api_data(session,url,method="get",headers=""):
    # メソッド変更
    if method == "post":
        r = session.post(url,headers=headers)
    else:
        r = session.get(url,headers=headers)
    return r.json()


# 該当ユーザのフォロワー情報を出力する
def get_followers(session):
    
    followers = []
    page = 1

    while True:
        url = f'https://note.com/api/v2/creators/{user_name}/followers?page={page}'
        datas = get_api_data(session,url)
        for i in datas["data"]["follows"]:
            followers.append(i["urlname"])

        # 最終ページチェック
        if datas["data"]["isLastPage"]:
            break
        else:
            page += 1
    return followers

# フォロワーの記事データを一気に出力する
# 結果は記事IDで出力する
def get_article(session,followers):

    articles = []

    # 1日前のUNIX時間を取得する
    n = datetime.now() - timedelta(days=1)
    yesterday = int(n.timestamp())

    for follower in followers:

        # フォロワーの記事を取得する
        url = f'https://note.com/api/v2/creators/{follower}/contents?kind=note'
        datas = get_api_data(session,url)

        for i in datas["data"]["contents"]:
            # 記事の発行日時を取得する
            n = datetime.fromisoformat(i["publishAt"])
            publish_time = int(n.timestamp())
            # 今日発行された日時であるかどうかをチェックする
            if publish_time > yesterday:
                articles.append(i["key"])

    return articles

# 該当記事にスキを付与する
def hit_like(session,articles):
    for article in articles:
        url = f'https://note.com/api/v3/notes/{article}/likes'
        headers = {
            'X-Requested-With': 'XMLHttpRequest',
            'Sec-Fetch-Dest': 'empty',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Site': 'same-origin',
            'Content-Length': '0',
            'Te': 'trailers',
            'Host': 'note.com',
            'Origin': 'https://note.com',
        }
        datas = get_api_data(session,url,"post",headers)
        # 画面出力
        print(f'記事ID:{article} response:{datas}')
        # API負荷軽減のため
        time.sleep(sleep_time)


####################################################
# MAIN
####################################################
if __name__ == '__main__':
    try:
        now = datetime.now()
        print('---Start Script---')
        print(f'start :{now}')
        print('------')

        # sessionオブジェクト生成
        session = requests.session()

        # 認証
        session = note_auth(session)

        # フォロワー情報を取得する
        followers = get_followers(session)

        # 記事を取得する
        articles = get_article(session,followers)

        # 記事にスキをする
        hit_like(session,articles)

    except Exception as e:
        print(f'cauth {type(e)}: {e}')
        print(traceback.format_exc())

    finally:
        finish = datetime.now()
        print(' ---Finish---')
        print(f'start :{finish}')


解説

⓪APIを利用する場合の注意点

ログインが正しく行われると認証情報がcookieに保存されます。APIを利用する際には認証情報を同時に送る必要があります。この情報を簡単に利用するため、Pythonではrequestsのsessionを使います。ざっくりした概要についてはこちらを参照してください。

①ログイン処理

# noteの認証を実施する
def note_auth(session):
    
    user_data = {
        "login":    email_address,
        "password": password
    }

    # 認証
    url = 'https://note.com/api/v1/sessions/sign_in'
    r = session.post(url,json=user_data)

    # ログインエラー判定
    r2 = json.loads(r.text)
    if "error" in r2:
        raise Exception("Login Error")
    else:
        return session

note_auth関数で認証を実施し、sessionオブジェクトを返してます(返さなくても動きますが)

認証を行うnote APIのURLがあります。

'https://note.com/api/v1/sessions/sign_in'

ここにPOSTでログイン情報を渡します。ログイン情報自体はログイン名、パスワードをJSON形式にしたものとなります。

sessionには認証情報が自動的に保存されるので、このオブジェクトを通じてPOST/GETすれば自動的に認証情報も同時に送信されます。


②ログインユーザのフォロワー情報を取得する

# 該当ユーザのフォロワー情報を出力する
def get_followers(session):
    
    followers = []
    page = 1

    while True:
        url = f'https://note.com/api/v2/creators/{user_name}/followers?page={page}'
        datas = get_api_data(session,url)
        for i in datas["data"]["follows"]:
            followers.append(i["urlname"])

        # 最終ページチェック
        if datas["data"]["isLastPage"]:
            break
        else:
            page += 1
    return followers

ユーザのフォロワーリストを取得します。Note APIにそれ用のURLがあります。

'https://note.com/api/v2/creators/{user_name}/followers?page={page}'

user_nameは自分のログイン名が入ります。ここで取得できるのはフォロワーのユーザ名です。

このAPIは表示数制限がありまして、pageを変更することで複数のフォロワーのユーザ名を取得することができます。

またここでは詳細を省きますが、JSON内に「isLastPage」というキーがあって、ここのパラメータがtrueになると出力されているデータが最終ページであることを表しています。


③②で取得したフォロワーの記事を取得する(過去1日分)

# フォロワーの記事データを一気に出力する
# 結果は記事IDで出力する
def get_article(session,followers):

    articles = []

    # 1日前のUNIX時間を取得する
    n = datetime.now() - timedelta(days=1)
    yesterday = int(n.timestamp())

    for follower in followers:

        # フォロワーの記事を取得する
        url = f'https://note.com/api/v2/creators/{follower}/contents?kind=note'
        datas = get_api_data(session,url)

        for i in datas["data"]["contents"]:
            # 記事の発行日時を取得する
            n = datetime.fromisoformat(i["publishAt"])
            publish_time = int(n.timestamp())
            # 今日発行された日時であるかどうかをチェックする
            if publish_time > yesterday:
                articles.append(i["key"])

    return articles

②で取得したフォロワーのユーザ名はlistで保存されますので、今度はこのフォロワーが書いた記事を出力させます。取得するのは記事のIDです。

フォロワーの記事を出力させるAPIは以下です。

'https://note.com/api/v2/creators/{follower}/contents?kind=note'

実はこのAPIにpage要素を追加してあげればもっと古い記事の情報が取得できます。デフォルト値ですと最新の7記事ぐらいが表示されます。今回の要件では過去1日分のnote記事にイイネをするため、1日7記事以上書いてる人がいたら漏れることになりますが、まああんまりいないだろうと思って決め打ちにしてます。


④③で取得した記事にスキをする【2024年1月現在】

# 該当記事にスキを付与する
def hit_like(session,articles):
    for article in articles:
        url = f'https://note.com/api/v3/notes/{article}/likes'
        headers = {
            'X-Requested-With': 'XMLHttpRequest',
            'Sec-Fetch-Dest': 'empty',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Site': 'same-origin',
            'Content-Length': '0',
            'Te': 'trailers',
            'Host': 'note.com',
            'Origin': 'https://note.com',
        }
        datas = get_api_data(session,url,"post",headers)
        # 画面出力
        print(f'記事ID:{article} response:{datas}')
        # API負荷軽減のため
        time.sleep(sleep_time)

ここが難関でした。詳細は後述。多くのnote APIを解説しているページで「スキ」をするためのAPIのバージョンがv1ですが2024年1月現在v3になっていて、仕様も変わっています。

'https://note.com/api/v3/notes/{article}/likes'

articleには③で取得した記事IDを入れます。v1の時はこのURLにGETアクセスするだけでスキが押せました。ところがv3はアクセス要件が変わっています。
スキを押すためにはPOST通信、スキを取り消すためにはDELETE通信が必要です。GETしたら該当記事にスキをした人の一覧が出てくるだけです。

加えてさらに罠が仕掛けられていて、何も考えずにPOSTしてもエラーになる仕様です。必要なのはHTTPヘッダー情報。

            'X-Requested-With': 'XMLHttpRequest',
            'Sec-Fetch-Dest': 'empty',
            'Sec-Fetch-Mode': 'cors',
            'Sec-Fetch-Site': 'same-origin',
            'Content-Length': '0',
            'Te': 'trailers',
            'Host': 'note.com',
            'Origin': 'https://note.com',

このヘッダーを付与したPOST通信をしないとエラーを吐いて終わります。なのでここではヘッダーを意図的に変更してアクセスしています。ここは初見殺しです。

見つけたのは開発者以外で僕が初めてだと思います(笑)


使用上の注意

①「上限に達したためご利用できません」のエラー

{'error': {'message': '上限に達したためご利用できません。しばらくお待ちいただいた後、再度お試しください。', 'type': 'too_many_requests'}}

noteの「スキ」はAPIの制限がかなり厳しいです。あれこれやったところ、50記事にイイネするとエラーが吐かれるようです。

その後最短1時間でエラーは一部解除されますが、再び実施するとあっという間に再度エラーに引っかかります。最大24時間ぐらいのBANがありうります。こうなったら諦めて時間をあけて実施してください。

②ログインエラー

cauth <class 'Exception'>: Login Error
Traceback (most recent call last):
  File "/space/www/component/note/note_fav_all_v2.py", line 132, in <module>
    session = note_auth(session)
  File "/space/www/component/note/note_fav_all_v2.py", line 40, in note_auth
    raise Exception("Login Error")
Exception: Login Error

上記、ログインエラーです。ユーザ名、パスワードが間違っている場合は当然起きます。

そのほかにスクリプトを何回も繰り返していると認証が多大に行われ、制限に引っかかって認証拒否される仕様になってログインできなくなります。この事象を回避するためには時間をおくか、認証情報を保管しているcookieを有効に使う必要がありますが、スクリプトが冗長のためここではご紹介しません。

③利用についての注意

このツールは非公式のAPIを利用していますので、いつ仕様が変更されるかはわかりません(2024年1月時点で動いています)。またこのツールをいたずら目的で利用されることは避けてください。ご自身の責任の下ご利用ください。


後記

スクリプト自体は特に何のひねりもありません。ただ、「スキ」を行うAPIについては上手くいかずに1日悩みました。

noteのサイトはVue.js(Nuxt.js)で構成され、ビューとAPIが完全分離しています。このためAPIはChromeのディベロッパーツールを使えば丸わかりなので見つけるのに大した苦労はありません。誰でもできます。

APIの動きはChromeのディベロッパーツールで見える

しかしながら「スキ」のAPIについては恐らくスパム対策のため厳密な設定を施されていました。

今回Chromeだけでは分からなかったため、BurpSuiteで通信をキャプチャしてようやくヘッダー情報がキーポイントであることにたどり着きました。


参考


この記事が気に入ったらサポートをしてみませんか?