2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Qiita記事200本以上をDev.toへ半自動移植する仕組みを作った

2
Posted at

はじめに

Qiitaに長年記事を書いてきて、ふと「この日本語記事という資産を英語圏でも活かせないだろうか」と思いました。

せっかく過去に書いた記事があるなら、英語化して別の読者層にも届けてみたい。英語の勉強にもなりますし、過去記事を見直すきっかけにもなります。

ただ、Qiitaに投稿してきた記事は200本を超えています。1本ずつ手で翻訳していたら本業の片手間では終わりません。であればAIに大半を任せて自動化できないかと考えました。

実際に作って運用してみると、最初の素朴な計画が現実とぶつかり、想像以上に複雑な仕組みになっていきました。

結果として、これは単なる翻訳スクリプトではなく、過去に書いた日本語記事を英語圏へ届けるための小さな編集・配信システムのようなものになりました。その過程を記事として残しておきます。

完成したもの

最終的にできたのは、こういう仕組みです。

  • Qiitaの自分の記事を全件ダウンロードして状態管理する
  • OpenAI APIで英訳する
  • 記事内の画像を自分のホスティング先に差し替える
  • 記事内の他記事へのリンクを翻訳済み記事へ差し替える
  • スマホからアクセスできるレビューGUIで人間が最終チェック
  • 合格した記事を1日1本のペースでDev.toに自動投稿

裏側ではCloudflare KVでステータスを一元管理し、GitHub Actionsのcronで翻訳や投稿を少しずつ進めています。

image.png
Dev.to に投稿された記事

すでに運用に乗っていて、毎日1本ずつDev.toにも記事が増えています。(投稿先: https://dev.to/segur

最初の素朴な計画

英語記事の投稿先には、APIで記事を投稿できるDev.toを選びました。

最初に思い描いていた構成は、本当にシンプルでした。

3ステップのバッチ処理。ローカルで叩けば終わるはず。

実際にはここから現実と衝突しながら、想定していなかった処理がいくつも積み重なっていくことになりました。

ぶつかった課題と、その都度の判断

課題1: 画像をどこに置くか問題

最初に気づいたのは、Qiita記事に貼られている画像がQiitaのCDNにホスティングされているという当たり前の事実でした。

これをそのままDev.toの記事から参照しても、規約上の明確な違反ではなさそうでした。とはいえ、Dev.toの記事を表示するたびにQiitaのサーバーへ画像を取りに行く状態になります。他社のサーバー資源に依存し続けるのは、エンジニアとして行儀が悪いと感じました。

そこで、自分がQiitaにアップロードした画像をすべてダウンロードして、別の場所にホスティングし直すことにしました。ホスティング先はいくつか候補がありましたが、現時点での選択はGitHubの公開リポジトリです。無料で管理しやすいのが理由です。

これで画像の問題は片付いたのですが、副作用として「翻訳済み記事」とは別に「画像差し替え済み記事」という新しい状態が生まれました。

課題2: 記事の状態が増えはじめる

もともと記事ごとに「翻訳済みかどうか」「投稿済みかどうか」程度のステータスは管理していました。ただ、画像差し替え処理を追加したことで、「画像差し替え済みかどうか」という新しい状態が加わります。

  • 翻訳済みかどうか
  • 画像が差し替え済みかどうか
  • Dev.toに投稿済みかどうか

「この記事、今どこまで進んだんだっけ?」が分からなくなってきます。なので、記事ごとのステータス管理を見直すことにしました。

本来であればDBなり何なりを使うべき場面だと思いますが、最初は複雑化を避けたくて、YAMLファイルで管理することにしました(後でこの選択は揺り戻すことになります)。

- qiita_id: xxxxxxxxxx
  title: "..."
  translated: true
  images_uploaded: true
  published: false

課題3: そもそも全部投稿すべきなのか問題

200本超の記事を眺めていて、もう一つ気づきました。全部をDev.toに投稿する必要はないのです。

  • 日本独自のサービスや事情に依存した記事
  • 数年前の情報で、現在は陳腐化してしまった記事
  • 当時の内輪ネタや、日本のエンジニア文化前提の記事

英語圏に出しても響かない、あるいは出すべきでない記事が一定数あります。

最初はこの取捨選択もAIに任せようとしました。記事の内容を渡して「これは英語圏向けに出すべきか?」を判定してもらう。結果はいまいちでした。文脈の機微を判断しきれず、明らかに古い記事を「投稿すべき」と返したり、逆に普遍的な内容を「ローカル過ぎる」と弾いたりします。

ここは諦めて、自分が目視で判断することにしました。レビューというフェーズが人間の手作業として明示的に組み込まれることになります。

課題4: 人間が入るならバッチ処理は破綻する

ここがこのプロジェクトでの一番大きな方針転換でした。

当初はすべてコマンドラインのバッチ処理で完結させるつもりでした。cronで定期実行して、朝起きたらDev.toに新しい記事が増えている、みたいな世界観です。

ところが「200本を1本ずつ目視レビューする」が必須になった瞬間、CLIでやるのは破綻します。理由は2つ。

  • ボトルネックがレビュー作業に移るので、CLIで他の処理をいくら速くしても意味がない
  • 人間は操作ミスをする。200本をCLIでポチポチ承認していくと、間違って却下したり、逆に問題のある記事を承認したりするリスクが無視できない

なので、CLIは諦めてGUIを作ることにしました。フィルタやステータス遷移を視覚的に扱えるWebアプリにします。実装はReact + Next.jsです(自分が普段使っている技術なので情緒的な選択も多少あります)。

image.png
記事の一覧をフィルタしている様子

こういう大胆な方針転換は、コードベースが肥大化する前にやるのが鉄則です。今回も、CLI 部分が成長しすぎる前に GUI へ舵を切れたのが幸いでした。

課題5: APIのレート制限とGitHub Actions

200記事を一気に処理するのは、API利用のマナーとしてもよくありません。Qiita API、Dev.to API、OpenAI API、それぞれにレート制限や負荷の懸念があります。特にOpenAIのトークン消費は気を付けたい。

なので、バッチ処理を完全に捨てたわけではなく、「人間が関わらない部分」はGitHub Actionsのcronで少しずつ実行する方針にしました。

たとえば翻訳処理は、1時間に数本ずつ進めるようなスケジュールで動かしています。人間のレビューを待っているうちに、未翻訳の記事が裏で少しずつ翻訳されていくイメージです。

課題6: 記事内リンクが日本語記事を指してしまう

レビューを始めて気づいた、地味だけど厄介な問題がありました。

記事の中に、自分の他のQiita記事へのリンクが貼られていることが多いのです。本文を英訳しても、リンク先は日本語記事のまま。英語圏の読者がリンクを踏むと、いきなり日本語のQiitaページに飛ばされる状態になります。

不親切です。どうせ自分の記事なのだから、リンク先もDev.toの英訳版に差し替えればいい。

ところが、これを実装しようとすると翻訳の順序に依存関係が生まれます。リンク先Bを先に翻訳して投稿しておかないと、記事A内のリンクを差し替えられません。

なので、記事ごとに「内部リンクが解決済みかどうか」という新しいステータスを増やしました。これで状態管理項目はさらに増えます。

- qiita_id: xxxxxxxxxx
  translated: true
  images_uploaded: true
  links_resolved: true   # ← 追加
  reviewed: false
  published: false

3つの自動処理(翻訳、画像差し替え、内部リンク差し替え)がすべて完了した記事だけがレビュー対象キューに入る、という整理になりました。

課題7: スマホでレビューしたい

ここまでの仕組みで運用は回っていたのですが、ボトルネックは依然としてレビュー作業でした。ローカルPCでReactアプリを起動して、机の前でポチポチやる。これだと隙間時間が活用できません。

移動中や外出中の隙間時間でレビューできれば、スループットは大きく上がる。なので、レビューGUIをCloudflare Pagesにホスティングしてスマホからアクセスできるようにしました。

スマホで英訳した記事をレビューしている様子

ついでにこのタイミングで、ステータス管理をYAMLからCloudflare KVに移行しました。YAML管理だと、記事のステータスが変わるたびにgit commitが発生してしまっていて運用上の摩擦になっていたためです。KVにしたことで、ステータス変更が単なるAPI呼び出しになり、フロントエンドからも気軽に書き換えられるようになりました。

投稿品質のために入れた細かい処理

主要な課題は以上ですが、運用しながら細かい改善も積み重ねていました。

  • canonical URLの設定: Dev.toには、元記事のURLをcanonicalとして登録できる仕組みがあります。Qiita記事をcanonicalに設定することで、検索エンジン的にも「翻訳版です」が明示でき、元記事への敬意も保てます。
  • サムネイル自動設定: Dev.to記事のサムネイルを、本文中の最初の画像から自動で抽出して設定するようにしました。
  • Qiita独自記法 → Dev.to記法への変換: 数式記法など、Qiita独自のMarkdown拡張がいくつかあるので、Dev.to側の記法に変換する処理を追加しました。
  • スキップ機能: レビューで「これは投稿すべきでない」と判断した記事は、スキップ状態にして投稿対象から外せるようにしました。

最終的なアーキテクチャ

最初の「ダウンロード → 翻訳 → 投稿」の3ステップが、最終的には以下の流れになっていました。

  1. Qiita APIから記事をダウンロード(GitHub Actions cron)
  2. 画像をGitHub公開リポジトリに再ホスティング(GitHub Actions cron)
  3. OpenAI APIで英訳(GitHub Actions cron)
  4. 記事内リンクを翻訳済み記事のURLに差し替え(GitHub Actions cron)
  5. Cloudflare PagesにホストされたレビューGUIで人間が確認(スマホ可)
  6. 合格した記事を1日1本ペースでDev.toに投稿(GitHub Actions cron)

ステータスはCloudflare KVで一元管理。

振り返って得た学び

このプロジェクトを通じて、自分のなかで言語化された原則がいくつかあります。

1. AIに判断を任せる範囲は、慎重に切り分ける

翻訳のような「変換」タスクはAIが得意ですが、「この記事を出すべきか」のような文脈依存の判断はAIに任せるとノイズが多くなりがちです。AIの得意領域と、人間が判断すべき領域を素直に分けたほうが結果的にトータルの品質が上がります。

2. コードよりも、運用設計の方が大事だった

Qiita APIを叩く、OpenAI APIで翻訳する、Dev.to APIで投稿する。
それぞれの処理だけを見れば、そこまで複雑ではありません。実装そのものはClaude Codeを活用したこともあって、トータルで8時間ほどで形になりました。

ただ、200本以上の記事を対象にして実際に運用しようとすると、単純なスクリプトでは足りませんでした。

どの記事を投稿対象にするのか。
画像をどこに置くのか。
記事内リンクをどう扱うのか。
どこで人間がレビューするのか。
投稿ペースをどうするのか。

このあたりを決めることの方が、今回の本質だった気がします。AIコーディング支援が普及した今、コードを書くこと自体のコストが下がったぶん、運用設計こそが書き手の見せ所になっているのかもしれません。

さいごに

Dev.toでの発信は始まったばかりですが、こうやって1つ作ってみると、過去の自分の資産を別の角度から再活用する楽しさがあります。同じようにQiitaに記事を書きためている方がいたら、似たような仕組みを試してみるのも面白いかもしれません。

なお、この記事自体も、後日この仕組みに通して英訳し、Dev.toに投稿する予定です。

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?