Python プロキシサーバーとは

Python プロキシサーバーは、Python コードを通じて膨大な数の IP 経由で HTTP/S リクエストをルーティングできます。IP のローテーション、セッションの維持、地理的ターゲティングなどの機能をサポートしています。
4 分読
Python Proxy Server illustration with laptop and servers.

このチュートリアルでは、以下の内容を学びます:

  • Python プロキシサーバーとは何か、どのように動作するか
  • Python で HTTP プロキシサーバーを構築するために必要な手順。
  • このアプローチの利点と欠点。

早速始めましょう!

Python プロキシサーバーとは?

Python プロキシサーバーは、クライアントとインターネットの間の仲介役として機能する Python アプリケーションです。クライアントからのリクエストを傍受し、ターゲットサーバーに転送し、レスポンスをクライアントに返送します。これにより、宛先サーバーに対してクライアントの身元を隠蔽します。 

プロキシサーバーとは何か、どのように動作するかについて詳しく知るには、こちらの記事をお読みください。 

Python のソケットプログラミング機能により、基本的なプロキシサーバーを簡単に実装でき、ユーザーはネットワークトラフィックを検査、変更、またはリダイレクトできます。プロキシサーバーは、ウェブスクレイピングにおけるキャッシング、パフォーマンスの向上、セキュリティの強化に最適です。

Python で HTTP プロキシサーバーを実装する方法

以下の手順に従って、Python プロキシサーバーのスクリプトを構築する方法を学びましょう。

ステップ1:Python プロジェクトを初期化する

始める前に、マシンに Python 3+ がインストールされていることを確認してください。インストールされていない場合は、インストーラーをダウンロードし、実行して、インストールウィザードに従ってください。

次に、以下のコマンドを使用して python-http-proxy-server フォルダを作成し、その中に仮想環境を持つ Python プロジェクトを初期化します: 

mkdir python-http-proxy-server

cd python-http-proxy-server

python -m venv env

Python IDE で python-http-proxy-server フォルダを開き、空の proxy_server.py ファイルを作成します。

素晴らしい!Python で HTTP プロキシサーバーを構築するために必要なものがすべて揃いました。

ステップ2:受信ソケットを初期化する

まず、着信リクエストを受け付けるためのウェブソケットサーバーを作成する必要があります。この概念に馴染みがない場合、ソケットとはクライアントとサーバー間の双方向データフローを可能にする低レベルのプログラミング抽象化です。ウェブサーバーの文脈では、サーバーソケットはクライアントからの着信接続をリッスンするために使用されます。 

以下の行を使用して、Python でソケットベースのウェブサーバーを作成します:

port = 8888
# プロキシサーバーを特定のアドレスとポートにバインドする
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 最大10個の同時接続を受け入れる
server.bind(('127.0.0.1', port))
server.listen(10)

これにより、受信ソケットサーバーが初期化され、http://127.0.0.1:8888 のローカルアドレスにバインドされます。次に、listen() メソッドを使ってサーバーが接続を受け入れられるようにします。

注:ウェブプロキシがリッスンすべきポート番号は自由に変更できます。最大限の柔軟性を持たせるため、コマンドラインからその情報を読み取るようにスクリプトを変更することもできます。 

socket は Python 標準ライブラリに由来します。そのため、スクリプトの先頭には以下のインポートが必要です:

import socket

Python プロキシサーバーが必要に応じて起動したことを監視するため、このメッセージをログに記録します:

 print(f"Proxy server listening on port {port}...")

ステップ3:クライアントのリクエストを受け入れる

クライアントがプロキシサーバーに接続すると、その特定のクライアントとの通信を処理するために新しいソケットを作成する必要があります。Python では次のように行います:

# 着信リクエストをリッスンする

while True:

    client_socket, addr = server.accept()

    print(f"Accepted connection from {addr[0]}:{addr[1]}")

    # クライアントのリクエストを処理するスレッドを作成する

    client_handler = threading.Thread(target=handle_client_request, args=(client_socket,))

    client_handler.start()

複数のクライアントリクエストを同時に処理するには、上記のようにマルチスレッディングを使用する必要があります。Python 標準ライブラリから threading をインポートするのを忘れないでください:

import threading

ご覧の通り、プロキシサーバーはカスタムの handle_client_request() 関数を通じて着信リクエストを処理します。次のステップでこの関数がどのように定義されているかを見てみましょう。

ステップ4:着信リクエストを処理する

クライアントソケットが作成されたら、以下の処理にそれを使用する必要があります:

  1. 着信リクエストからデータを読み取る。
  2. そのデータからターゲットサーバーのホストとポートを抽出する。
  3. それを使用してクライアントのリクエストを宛先サーバーに転送する。
  4. レスポンスを取得し、元のクライアントに転送する。

このセクションでは、最初の2つのステップに焦点を当てましょう。handle_client_request() 関数を定義し、それを使って着信リクエストからデータを読み取ります:

def handle_client_request(client_socket):

    print("Received request:n")

    # クライアントがリクエストで送信したデータを読み取る

    request = b''

    client_socket.setblocking(False)

    while True:

        try:

            # ウェブサーバーからデータを受信する

            data = client_socket.recv(1024)

            request = request + data

            # 元の宛先サーバーからデータを受信する

            print(f"{data.decode('utf-8')}")

        except:

            break

setblocking(False) はクライアントソケットを非ブロッキングモードに設定します。次に、recv() を使用して着信データを読み取り、バイト形式で request に追加します。着信リクエストデータのサイズがわからないため、一度に1つのチャンクずつ読み取る必要があります。この場合、1024バイトのチャンクが指定されています。非ブロッキングモードでは、recv() がデータを見つけられない場合、エラー例外を発生させます。そのため、except 命令が操作の終了を示します。

Python プロキシサーバーが何をしているかを追跡するために、ログに記録されたメッセージに注目してください。

着信リクエストを取得した後、そこから宛先サーバーのホストとポートを抽出する必要があります:

host, port = extract_host_port_from_request(request)

具体的には、extract_host_port_from_request() 関数は以下のようになります:

def extract_host_port_from_request(request):

    # "Host:" 文字列の後の値を取得する

    host_string_start = request.find(b'Host: ') + len(b'Host: ')

    host_string_end = request.find(b'rn', host_string_start)

    host_string = request[host_string_start:host_string_end].decode('utf-8')

    webserver_pos = host_string.find("/")

    if webserver_pos == -1:

        webserver_pos = len(host_string)

    # 特定のポートがある場合

    port_pos = host_string.find(":")

    # ポートが指定されていない場合

    if port_pos == -1 or webserver_pos < port_pos:

        # デフォルトポート

        port = 80

        host = host_string[:webserver_pos]

    else:

        # ホスト文字列から特定のポートを抽出する

        port = int((host_string[(port_pos + 1):])[:webserver_pos - port_pos - 1])

        host = host_string[:port_pos]

    return host, port

これが何をするかをより理解するために、以下の例を考えてみましょう。これは通常、着信リクエストのエンコードされた文字列に含まれる内容です:

GET http://example.com/your-page HTTP/1.1

Host: example.com

User-Agent: curl/8.4.0

Accept: */*

Proxy-Connection: Keep-Alive

extract_host_port_from_request() は “Host:” フィールドからウェブサーバーのホストとポートを抽出します。この場合、host は example.com で、port は 80 です(特定のポートが指定されていないため)。 

ステップ5:クライアントのリクエストを転送し、レスポンスを処理する

ターゲットホストとポートが得られたら、クライアントのリクエストを宛先サーバーに転送する必要があります。handle_client_request() 内で、新しいウェブソケットを作成し、それを使って元のリクエストを目的の宛先に送信します:

# 元の宛先サーバーに接続するためのソケットを作成する

destination_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 宛先サーバーに接続する

destination_socket.connect((host, port))

# 元のリクエストを送信する

destination_socket.sendall(request)

その後、サーバーのレスポンスを受信し、それを元のクライアントに伝える準備をします:

# サーバーから受信したデータを読み取る

# 一度に1つのチャンクずつ、クライアントに送信する

print("Received response:n")

while True:

    # ウェブサーバーからデータを受信する

    data = destination_socket.recv(1024)

    # 元の宛先サーバーからデータを受信する

    print(f"{data.decode('utf-8')}")

    # 送信するデータがなくなった場合

    if len(data) > 0:

        # クライアントに送り返す

        client_socket.sendall(data)

    else:

        break

レスポンスのサイズがわからないため、ここでも一度に1つのチャンクずつ処理する必要があります。data が空の場合、これ以上受信するデータはなく、操作を終了できます。

関数内で定義した2つのソケットを閉じるのを忘れないでください:

# ソケットを閉じる

destination_socket.close()

client_socket.close()

素晴らしい!Python で HTTP プロキシサーバーを作成しました。コード全体を見て、起動し、期待通りに動作することを確認しましょう!

ステップ6:すべてをまとめる

これが Python プロキシサーバースクリプトの最終的なコードです:

import socket

import threading

def handle_client_request(client_socket):

    print("Received request:n")

    # クライアントがリクエストで送信したデータを読み取る

    request = b''

    client_socket.setblocking(False)

    while True:

        try:

            # ウェブサーバーからデータを受信する

            data = client_socket.recv(1024)

            request = request + data

            # 元の宛先サーバーからデータを受信する

            print(f"{data.decode('utf-8')}")

        except:

            break

    # リクエストからウェブサーバーのホストとポートを抽出する

    host, port = extract_host_port_from_request(request)

    # 元の宛先サーバーに接続するためのソケットを作成する

    destination_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    # 宛先サーバーに接続する

    destination_socket.connect((host, port))

    # 元のリクエストを送信する

    destination_socket.sendall(request)

    # サーバーから受信したデータを読み取る

    # 一度に1つのチャンクずつ、クライアントに送信する

    print("Received response:n")

    while True:

        # ウェブサーバーからデータを受信する

        data = destination_socket.recv(1024)

        # 元の宛先サーバーからデータを受信する

        print(f"{data.decode('utf-8')}")

        # 送信するデータがなくなった場合

        if len(data) > 0:

            # クライアントに送り返す

            client_socket.sendall(data)

        else:

            break

    # ソケットを閉じる

    destination_socket.close()

    client_socket.close()

def extract_host_port_from_request(request):

    # "Host:" 文字列の後の値を取得する

    host_string_start = request.find(b'Host: ') + len(b'Host: ')

    host_string_end = request.find(b'rn', host_string_start)

    host_string = request[host_string_start:host_string_end].decode('utf-8')

    webserver_pos = host_string.find("/")

    if webserver_pos == -1:

        webserver_pos = len(host_string)

    # 特定のポートがある場合

    port_pos = host_string.find(":")

    # ポートが指定されていない場合

    if port_pos == -1 or webserver_pos < port_pos:

        # デフォルトポート

        port = 80

        host = host_string[:webserver_pos]

    else:

        # ホスト文字列から特定のポートを抽出する

        port = int((host_string[(port_pos + 1):])[:webserver_pos - port_pos - 1])

        host = host_string[:port_pos]

    return host, port

def start_proxy_server():

    port = 8888

    # プロキシサーバーを特定のアドレスとポートにバインドする

    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    server.bind(('127.0.0.1', port))

    # 最大10個の同時接続を受け入れる

    server.listen(10)

    print(f"Proxy server listening on port {port}...")

    # 着信リクエストをリッスンする

    while True:

        client_socket, addr = server.accept()

        print(f"Accepted connection from {addr[0]}:{addr[1]}")

        # クライアントのリクエストを処理するスレッドを作成する

        client_handler = threading.Thread(target=handle_client_request, args=(client_socket,))

        client_handler.start()

if __name__ == "__main__":

    start_proxy_server()

以下のコマンドで起動します:

python proxy_server.py

ターミナルに以下のメッセージが表示されるはずです:

Proxy server listening on port 8888...

サーバーが機能していることを確認するために、cURL でプロキシリクエストを実行します。プロキシで cURL を使用する方法について詳しくは、こちらのガイドをお読みください。

新しいターミナルを開き、以下を実行します:

curl --proxy "http://127.0.0.1:8888" "http://httpbin.org/ip"

これにより、http://127.0.0.1:8888 プロキシサーバーを通じて http://httpbin.org/ip の宛先に GET リクエストが送信されます。

次のような結果が得られるはずです:

{

  "origin": "45.12.80.183"

}

それがプロキシサーバーの IP です。なぜでしょうか?HTTPBin プロジェクトの /ip エンドポイントは、リクエストの送信元の IP を返すからです。サーバーをローカルで実行している場合、”origin” はあなたの IP に対応します。 

注:ここで構築した Python プロキシサーバーは HTTP の宛先でのみ機能します。HTTPS 接続を処理できるように拡張するのはかなり困難です。

では、プロキシサーバーの Python アプリケーションが書き込んだログを確認してみましょう。以下の内容が含まれているはずです:

Received request:

GET http://httpbin.org/ip HTTP/1.1

Host: httpbin.org

User-Agent: curl/8.4.0

Accept: */*

Proxy-Connection: Keep-Alive

Received response:

HTTP/1.1 200 OK

Date: Thu, 14 Dec 2023 14:02:08 GMT

Content-Type: application/json

Content-Length: 31

Connection: keep-alive

Server: gunicorn/19.9.0

Access-Control-Allow-Origin: *

Access-Control-Allow-Credentials: true

{

  "origin": "45.12.80.183"

}

これは、プロキシサーバーが HTTP プロトコルで指定された形式でリクエストを受信したことを示しています。その後、宛先サーバーに転送し、レスポンスデータをログに記録し、レスポンスをクライアントに送り返しました。なぜそれが確実にわかるのでしょうか?”origin” の IP が同じだからです!

おめでとうございます!Python で HTTP プロキシサーバーを構築する方法を学びました!

カスタム Python プロキシサーバーを使用する利点と欠点

Python でプロキシサーバーを実装する方法がわかったので、このアプローチの利点と限界を見てみましょう。

利点:

  • 完全な制御:このようなカスタム Python スクリプトを使えば、プロキシサーバーの動作を完全に制御できます。怪しい活動やデータ漏洩の心配はありません!
  • カスタマイズ性:プロキシサーバーは、パフォーマンスを向上させるためのロギングやリクエストのキャッシングなどの便利な機能を追加して拡張できます。

欠点:

  • インフラコスト:プロキシサーバーのアーキテクチャをセットアップするのは簡単ではなく、ハードウェアや VPS サービスの面で多額の費用がかかります。
  • 保守が困難:プロキシのアーキテクチャ、特にそのスケーラビリティと可用性を維持する責任があります。これは経験豊富なシステム管理者のみが対応できるタスクです。
  • 信頼性が低い:このソリューションの主な問題は、プロキシサーバーの出口 IP が変わらないことです。その結果、アンチボット技術がその IP をブロックし、サーバーが目的のリクエストにアクセスできなくなる可能性があります。つまり、プロキシは最終的に機能しなくなります。

これらの制限と欠点は、本番環境でカスタム Python プロキシサーバーを使用するにはあまりにも大きな問題です。解決策は?Bright Data のような信頼できるプロキシプロバイダーです!アカウントを作成し、本人確認を行い、プロキシを取得して、お好きなプログラミング言語で使用してください。例えば、requests を使って Python スクリプトにプロキシを統合することができます。

私たちの巨大なプロキシネットワークには、世界中の高速で信頼性が高く、安全なプロキシサーバーが何百万も含まれています。私たちがなぜ最高のプロキシサーバープロバイダーであるのかをぜひご確認ください。

まとめ

このガイドでは、プロキシサーバーとは何か、そして Python でどのように動作するかを学びました。詳しくは、ウェブソケットを使ってゼロから構築する方法を学びました。これで、あなたは Python のプロキシのマスターになりました。このアプローチの主な問題は、プロキシサーバーのスタティックな出口 IP が最終的にブロックされてしまうことです。それを避けるには、Bright Data のローテーションプロキシをご利用ください!

Bright Data は世界最高のプロキシサーバーを管理しており、フォーチュン500企業や 50,000+ のお客様にサービスを提供しています。その提供内容には、幅広い種類のプロキシが含まれます:

その信頼性が高く、高速でグローバルなプロキシネットワークは、あらゆるサイトから簡単にデータを取得するための多数のウェブスクレイピングサービスの基盤にもなっています。