複数のスレッドが小さなプロキシプールを共有すると、ランダムに 1 件を選ぶ方法では 2 つの処理に同じプロキシが渡ることがあります。キューは現在借用できる項目を保持できます:

from contextlib import contextmanager
from queue import Queue

available = Queue()
for endpoint in dict.fromkeys(proxy_urls):
    available.put(endpoint)

@contextmanager
def borrow_proxy():
    endpoint = available.get(timeout=5)
    try:
        yield endpoint
    finally:
        available.put(endpoint)

def process_url(url):
    with borrow_proxy() as endpoint:
        return fetch_one(url, endpoint)

ワーカースレッドを開始する前に、キューを一度だけ構築します。proxy_urls には完全なエンドポイント URL が入っています。公開候補は無料プロキシリストにあります。fetch_one が対応するプロトコルと URL 形式を使ってください。既存の同期 fetch_one 関数は、戻る前にネットワーク処理を終え、応答を閉じる必要があります。そうしないと、接続がまだそのプロキシを使っている間に貸与が終わってしまいます。

Queue.get は利用可能な項目を 1 つアトミックに取り出します。枯渇したプールは共有フラグを繰り返し走査するのではなく待機します。5 秒以内に項目が届かなければ queue.Empty が呼び出し元に届きます。その結果に対して、アプリケーションの再試行・失敗・キャンセルの方針を選んでください。

finally ブロックは、成功時でも例外時でも取得した項目を返します。失敗したエンドポイントが健全だと宣言するものではありません。隔離・交換・クールダウンの方針には明示的な経路が必要で、一時的なエラーを見たプロキシをすべて黙って捨てるべきではありません。

同一の URL 文字列を重複排除すると、キュー内の重複項目を防げます。異なる URL が同じ物理プロキシや同じ出口を指すこともあります。その共有リソースに排他性が必要なら、意図的にグループ化してください。キューが調整するのはこのプロセス内のスレッドであり、他のマシンのワーカーではありません。

関連記事