【ITニュース解説】Publishing to Juejin Through Its Web API: The mark_content Lesson
2026年10月17日に「Dev.to」が公開したITニュース「Publishing to Juejin Through Its Web API: The mark_content Lesson」について初心者にもわかりやすく解説しています。
ITニュース概要
JuejinのAPIで自動投稿が急に失敗。原因はAPI側でマークダウン本文を送る`mark_content`フィールドが必須になったこと。開発者ツールでブラウザの通信を解析し、正しいリクエストを特定してスクリプトを修正した。未公開APIはブラウザの動きを参考にし、エラー再現で解決策を探すのが重要。
ITニュース解説
ウェブサービスを使った開発では、異なるシステム間で情報をやり取りする「Web API」を利用する場面がよくある。Web APIは、人間がブラウザでウェブサイトを見るように、プログラムがウェブサービスの機能を利用するための窓口のようなものだ。コンテンツを自動的に公開したり、データを連携させたりする自動化の仕組みを開発する際にも、このWeb APIが活用される。しかし、Web APIには公式に公開されていない「非公開API」も存在し、これを利用する際には注意が必要となる。今回解説する記事は、中国の有名な開発者コミュニティ「Juejin(掘金)」の非公開Web APIを利用してコンテンツを自動公開していたツールが、APIのサイレントな変更によって突然動かなくなった事例とその解決策、そしてそこから得られる教訓をまとめたものだ。システムエンジニアを目指す皆さんにとって、非公開APIの取り扱いや問題解決の手法を学ぶ良い機会となるだろう。
この自動公開ツールは数ヶ月にわたって問題なく動作していたが、ある日突然、作成される記事の本文が空になるという異常が発生した。記事のタイトルは正しく設定されているものの、中身が空白のままドラフトとして保存され、公開しようとすると「ドラフト内容が空です」というエラーメッセージ({"err_no": 1010, "err_msg": "草稿内容为空"})が返ってきた。これは、APIが何も予告せずに変更された「サイレントリグレッション」という現象だった。開発者側でプログラムを一切変更していないにもかかわらず、外部のシステムであるJuejinのAPIが新しくなり、今まで正しかったはずのリクエストが無効になったのである。問題の原因は、APIがMarkdown形式の本文データを「mark_content」という新しいフィールドで送信することを必須としたことだった。以前はレンダリング済みのHTML形式の本文だけを送っていたため、この新しい要件に対応できていなかったのである。
この原因を特定するために、記事の著者は「リバースエンジニアリング」と呼ばれる手法を用いた。これは、既存のシステムやプログラムの内部動作を分析し、その仕組みを解明することだ。非公開APIの挙動を理解する上で最も確実な方法は、実際にそのサービスが提供している公式クライアント、つまりWebブラウザの動きを観察することである。著者はJuejinのWebサイトをブラウザで開き、ブラウザに内蔵されている「開発者ツール(DevTools)」の「ネットワーク」タブを開いた。そして、手動で記事を書き、保存する一連の操作を行った。このとき、ブラウザからJuejinのサーバーへ送られるHTTPリクエスト(APIコール)がネットワークタブに記録される。
保存時のリクエスト(POST https://api.juejin.cn/content_api/v1/article_draft/saveのようなURL)を特定し、そのリクエストを右クリックして「cURLとしてコピー(bash)」を選択した。cURLは、コマンドラインからHTTPリクエストを送信するためのツールで、コピーすることで、ブラウザが実際に送ったHTTPリクエストの内容(メソッド、URL、ヘッダー、クッキー、そしてデータ本体であるJSON形式の「ペイロード」)を完全に再現できる文字列が得られる。このcURLの内容を詳しく分析すると、古いツールが送信していなかった「mark_content」というフィールドが、ブラウザからのリクエストには含まれていることが発見された。この観察により、新しいAPIがmark_contentフィールドを必須とし、これがないか空だとリクエストを拒否することが判明したのである。
次に、このエラーコード「err_no=1010」の意味を明確にするために、意図的な再現実験を行った。mark_contentフィールドを正しく含んだリクエストを送信すると公開が成功し、mark_contentフィールドを省略したり、空の文字列を設定したりすると、1010エラーが返ってきた。この実験結果から、このフィールドが必須であり、空の文字列も無効と判断されることが裏付けられた。また、エラーメッセージが「ドラフト内容が空」であることを示していたため、これは認証の問題(ログイン情報の間違いなど)ではなく、送信するデータの内容に関するサーバー側の検証エラーであることもわかった。
原因と解決策が判明したところで、Pythonのrequestsライブラリを使ってAPIコールを修正した。cURLでキャプチャした情報をもとに、APIのURL、必要なヘッダー情報(content-type、user-agentなど)、そして新たに判明したmark_contentフィールドを含むJSONペイロードを正しく設定した。ここで重要なのは、保存と公開の一連の操作でクッキー(セッション情報)が維持されるように、requests.Sessionオブジェクトを再利用したことだ。異なるセッションで保存されたドラフトを公開しようとすると失敗してしまうため、このセッションの維持は不可欠だった。
さらに、Windowsのバッチファイルからツールを起動している環境で新たな問題が発生した。クッキーの値に「%」記号が含まれている場合、バッチファイルはこれを環境変数として誤って解釈し、クッキー文字列を破壊してしまうのだ。しかも、この破壊はエラーにならずサイレントに発生するため、問題の特定が非常に困難だった。この問題を解決するため、クッキー情報をコマンドラインの文字列として直接渡すのではなく、cookies.jsonというファイルに保存し、Pythonコード内でそのファイルを読み込んで利用するように変更した。これにより、バッチファイルはシンプルになり、クッキーの誤解釈を防ぐことができた。セキュリティ上の観点からも、cookies.jsonのような認証情報はバージョン管理システムに含めず、.gitignoreなどで管理することが重要である。
単一の記事の自動公開がうまくいくようになった後、過去に本文が空のまま公開されてしまった20件の古い記事を修正する必要があった。これを手作業で行うのは非効率なため、「バックフィルスクリプト」を作成し、自動で修正を行うことにした。スクリプトは、ローカルのMarkdownファイルと、すでに公開されている記事のIDなどを紐付ける「マニフェストファイル」を利用した。マニフェストファイルには、どのMarkdownファイルがどの記事IDに対応するかという情報が保存されており、スクリプトはこの情報を参照しながら、各Markdownファイルを読み込み、mark_contentフィールドを適切に設定して再保存・再公開を実行した。
このバックフィルスクリプトを運用する上で、二つの重要な運用上の注意点が浮上した。一つは「レートリミット」だ。APIへの連続したアクセスは、サーバーに負荷をかけすぎたり、不正なアクセスと判断されたりする可能性がある。そのため、各記事の処理の間に数秒程度の遅延(time.sleep(2)など)を挿入し、APIへのリクエスト頻度を制御した。もう一つは「進捗の永続化」だ。20件もの記事を一度に処理するスクリプトが、途中で何らかの理由で停止する可能性も考慮し、記事を一つ修正するたびにマニフェストファイルを更新・保存するようにした。これにより、スクリプトが途中で停止しても、次回実行時に中断した箇所から再開でき、すでに修正済みの記事を無駄に再処理するのを避けることが可能になる。
今回の非公開APIを巡る一連の問題解決から、システムエンジニアとして重要な教訓を学ぶことができる。まず、非公開APIを扱う際は、推測に頼るのではなく、公式クライアント(ブラウザ)の実際の挙動を「開発者ツール」と「cURL」を使って正確にキャプチャし、それを「真実」として捉えることが重要だ。次に、新しい挙動と既存のプログラムの挙動を注意深く比較することで、変更点(今回の場合はmark_contentフィールドの追加)を素早く特定できる。また、不明なエラーが発生した場合は、原因と思われる値を一つずつ変更しながら「意図的にエラーを再現」し、そのエラーが発生する条件を明確にすることが、問題解決への近道となる。さらに、システムを自動化する際には、認証情報(クッキーなど)を安全な方法で管理し、特にWindowsのバッチファイルなど特定の環境に依存する落とし穴に注意する必要がある。そして、複数のデータを処理するバッチ処理を組む際には、途中で処理が中断しても、再開できるように「冪等性」(何度実行しても同じ結果になる性質)を考慮し、進捗を随時保存する仕組みを取り入れることが不可欠だ。これらの教訓は、非公開APIに限らず、システム開発全般において非常に役立つ考え方である。
文字数:1986文字