こんにちは、にょろりんこの備忘録的技術ブログです。
今回は、検索エンジンのインデクシングでよく使われる「回転型プロキシ」について、実際に契約してみたところ、全く回転しなかったという話と、そこから自前でIPローテーションの制御コードを作った過程について書いていきます。
まず大前提として、プロキシは切り替え方式で以下の2つに分けることができます。
- 回転型プロキシ(Rotating):リクエストや時間ごとにIPが変わる。切り替えを自動でやってくれるので便利。
- 固定型プロキシ(Static/Dedicated):同じIPを継続して使う。切り替えは自前でやるしかないが、そのぶん制御の自由度は高い。
これはトレードオフなのですが、私は「IPの切り替え構造は自前では持たないでおこう」と思っていたので、自動で切り替えてくれる回転型を使おうと決め、「Rotating」と明記されていた Stormproxies と契約しました。
しかし、送られてきたのは──ただのIPアドレスの羅列。
「ん?これって回転型じゃなくない?」と思いながらも使ってみましたが、やはり自動では切り替わらない。 つまり、「回転型」と謳っていても、実態は「自分でローテーション制御しないと何も変わらない固定型」に近いものでした。
しかも、補足すると──テストしたIPはすべて接続不能でした(泣)
ここら辺の顛末はこちらにまとめてありますので、よろしければ見ていってください。
というわけで、今回の記事では「想定と違った回転型プロキシ」に直面した私が、どうやって自前でIPローテーションを実装したかをまとめていきます。

構造としては、MySQLに登録されたプロキシ一覧から「最後に使った時間が5分以上前のプロキシ」を1件取得し、使用後に last_used を更新する、というシンプルなローテーション構造です。
コードは以下のような感じです。
// get_random_proxy.js
/**
* proxiesテーブルから、status='working' かつ
* last_usedが5分以上前のプロキシをランダムに1件取得する
*
* ※実際に接続可能かどうかの確認は行わない
* @returns {Promise<{id: number, ip: string, port: number, username: string, password: string}>}
*/
async function getRandomProxy() {
try {
const [rows] = await pool.query(`
SELECT * FROM proxies
WHERE status = 'working'
AND (last_used IS NULL OR last_used < NOW() - INTERVAL 5 MINUTE)
ORDER BY RAND()
LIMIT 1
`);if (rows.length === 0) {
throw new Error(' 使用可能なプロキシがありません(working かつ last_used > 5分前)');
}return rows[0];
} catch (err) {
console.error('プロキシ取得中にエラー:', err);
throw err;
}
}
この処理は「IPが生きているか、帯域が死んでいないか、403が返るか」といった通信レベルの健全性確認はしていません。
「status = 'working'」 フラグが立っていて、「last_used」が5分以上前という「事前に登録されたプロキシ一覧の中で使ってもよさそうな条件」に基づいて選んでいるだけです。また、このローテーション制御は、以下のような `proxies` テーブルで管理しています:
| カラム名 | 説明 |
|-------------|------|
| `ip` | プロキシのIPアドレス |
| `port` | ポート番号(例:8080) |
| `username` | 認証用ユーザー名 |
| `password` | 認証用パスワード |
| `status` | 使用可否(`working` or `blocked`) |
| `last_used` | 最後に使った時刻(5分以上空けて再利用) |
このように、`last_used` により連続使用を避け、`status` によって死んだIPやブロック中のものを除外しているというシンプルな構図です。
// update_proxy_last_used.js
/**
* 指定したプロキシIDの last_used を現在時刻に更新する
* @param {number} proxyId - proxies テーブルの id
* @returns {Promise<void>}
*/
async function updateProxyLastUsed(proxyId) {
try {
await pool.execute(
'UPDATE proxies SET last_used = NOW() WHERE id = ?',
[proxyId]
);
console.log(`プロキシID=${proxyId} の last_used を更新`);
} catch (err) {
console.error('updateProxyLastUsed エラー:', err);
}
}
`last_used` カラムは、直近に使用された時刻を記録するためのものです。 プロキシを使い終わった後に `UPDATE proxies SET last_used = NOW()` を実行することで、 次の選定時に一定の「クールダウン」を設けることができます。
これによって、「生きている/使用可能なプロキシ」を選別し、 一度使用したプロキシを「一定時間(ここでは仮に5分)」休ませるという構造にしています。
なお、この種の管理は JSON ファイルなどでローカルに行うことも可能ですが、 DBで管理したほうが細かい制御(並列処理、再試行の記録、ブロック判定など)がしやすく、運用面でも楽になるため、今回は DBベースの設計を採用しています。
今回のように「Rotating」と書かれていても、実際には回転しないプロキシというのは存在します。 そうした場合でも、自前で制御コードを書けば、ローテーション管理は十分可能です。
特に `last_used` を軸にしたクールダウン管理と、`status` フラグによる生死判定を組み合わせれば、スケーラブルなプロキシ制御が簡単に構築できます。
現行の Sae-Porns では回転型プロキシを採用しているため、今回紹介したローテーション制御コードは実運用では使っていません。
とはいえ、プロキシを多用する用途(インデクシング、スクレイピング、分散クロール、SEO検証など)では、 こういった「自前管理によるIPローテーション設計」が、実は最もトラブルに強い構造だったりします。
ここはあくまで私の推測ですが── Google、Bing、Baidu、Yandex などの大手検索エンジンサービスは、おそらく固定型IPを大量に保有した上で、使用IPの選定・クールダウンなどを「自前で制御している」のではないかと思います。
それではみなさん、よい開発ライフを。
※このコードが実際に使われている「追跡されないアダルト動画の検索エンジンSae-Porns」はこちら!よかったら見ていってください。(18歳未満の方はご利用できません。)