Qué es un servidor proxy en Python

Un servidor proxy en Python permite que se produzca el enrutamiento de peticiones HTTP/S a través de una gran red de varias IP en código Python. Admite funciones como la rotación de IP, la persistencia de la sesión y el «geotargeting».
14 min read
Ilustración de Python Proxy Server con computadora portátil y servidores.

En este tutorial, aprenderás:

¡Vamos allá!

¿Qué es un servidor proxy en Python?

Un servidor proxy en Python es una aplicación de Python que funciona como intermediaria entre los clientes e internet. Intercepta las peticiones de los clientes, las reenvía a los servidores de destino y devuelve la respuesta al cliente. De esta forma, la identidad del cliente queda oculta en los servidores de destino.

Lee este artículo para saber más sobre qué es un servidor proxy y cómo funciona.

Las capacidades de programación de sockets de Python facilitan la implementación de un servidor proxy básico y esto permite que los usuarios puedan inspeccionar, modificar o redirigir el tráfico de red. Los servidores proxy son ideales para almacenar en caché, para mejorar el rendimiento y para aumentar la seguridad en lo que respecta al raspado web.

Cómo implementar un servidor proxy HTTP en Python

Sigue los pasos que aparecen a continuación y aprende a crear una secuencia de comandos de servidor proxy en Python.

Paso 1: crear tu proyecto de Python

Antes de empezar, comprueba que tienes Python 3+ instalado en tu ordenador. Si no lo tienes, descarga el instalador, ábrelo y sigue las instrucciones de instalación.

A continuación, utiliza los siguientes comandos para crear una carpeta python-http-proxy-server y un proyecto de Python que incluya un entorno virtual: 

mkdir python-http-proxy-server

cd python-http-proxy-server

python -m venv env

Abre la carpeta python-http-proxy-server en tu IDE de Python y crea un archivo proxy_server.py vacío.

¡Estupendo! Ya tienes todo lo que necesitas para crear un servidor proxy HTTP en Python.

Paso 2: crear un socket entrante

Primero tienes que crear un servidor WebSocket para aceptar las peticiones de entrada. Si no conoces bien este concepto, un socket es una abstracción de programación de nivel bajo que permite que haya un flujo de datos bidireccional entre un cliente y un servidor. Con respecto a los servidores web, se usa un socket de servidor para escuchar las conexiones entrantes de los clientes. 

Utiliza las siguientes líneas para crear un servidor web basado en sockets en Python:

port = 8888
# bind the proxy server to a specific address and port
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# accept up to 10 simultaneous connections
server.bind(('127.0.0.1', port))
server.listen(10)

Así se crea un servidor de socket entrante y se vincula a la dirección local http://127.0.0.1:8888. Esto permite que el servidor acepte conexiones con el método listen () .

Nota: puedes cambiar el número del puerto que tiene que escuchar el proxy web. También puedes modificar la secuencia de comandos para leer esa información desde la línea de comandos y así tener la máxima flexibilidad.

El socket proviene de la biblioteca estándar de Python. Por lo tanto, tendrás que importar lo siguiente en la parte superior de tu secuencia de comandos:

import socket

Para comprobar que el servidor proxy en Python se ha iniciado correctamente, registra este mensaje:

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

Paso 3: aceptar las peticiones de los clientes

Cuando un cliente se conecta al servidor proxy, hay que crear un socket nuevo para gestionar la comunicación con ese cliente concreto. Puedes hacerlo en Python de la siguiente manera:

# listen for incoming requests

while True:

    client_socket, addr = server.accept()

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

    # create a thread to handle the client request

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

    client_handler.start()

Para gestionar las peticiones de varios clientes a la vez, tienes que utilizar multihilos tal y como hemos indicado anteriormente. No olvides importar los hilos de la biblioteca estándar de Python:

import threading

Como puedes ver, el servidor proxy se encarga de las peticiones de entrada gracias a la función personalizada handle_client_request (). Vamos a ver a continuación cómo se define.

Paso 4: procesar las peticiones de entrada

Una vez creado el socket de cliente, tienes que usarlo para:

  1. Leer los datos de las peticiones de entrada.
  2. Extraer el host y el puerto del servidor de destino de esos datos.
  3. Reenviar la petición del cliente al servidor de destino.
  4. Enviar al cliente original la respuesta que has recibido.

En esta sección, nos vamos a centrar en los dos primeros pasos. Define la función handle_client_request () y úsala para leer los datos de la petición de entrada:

def handle_client_request(client_socket):

    print("Received request:\n")

    # read the data sent by the client in the request

    request = b''

    client_socket.setblocking(False)

    while True:

        try:

            # receive data from web server

            data = client_socket.recv(1024)

            request = request + data

            # Receive data from the original destination server

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

        except:

            break

setblocking (False) establece el socket del cliente sin bloqueo. Después, utiliza recv () para leer los datos de entrada y añádelos a la petición en formato byte. Como no sabes el tamaño de los datos de la petición de entrada, tienes que leer los bloques de uno en uno. En este caso, se ha especificado un bloque de 1024 bytes. En el modo «sin bloqueo», si recv () no encuentra datos, generará una excepción de error. Por lo tanto, el comando de excepción marca el final de la operación.

Anota los mensajes registrados para saber lo que está haciendo el servidor proxy en Python.

Después de recuperar la petición de entrada, tienes que extraer el host y el puerto del servidor de destino:

host, port = extract_host_port_from_request(request)

In particular, this is what the extract_host_port_from_request() function looks like:

def extract_host_port_from_request(request):

    # get the value after the "Host:" string

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

    host_string_end = request.find(b'\r\n', 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)

    # if there is a specific port

    port_pos = host_string.find(":")

    # no port specified

    if port_pos == -1 or webserver_pos < port_pos:

        # default port

        port = 80

        host = host_string[:webserver_pos]

    else:

        # extract the specific port from the host string

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

        host = host_string[:port_pos]

    return host, port

To better understand what it does, consider the example below. This is what the encoded string of an incoming request usually contains:

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 () extrae el host y el puerto del servidor web del campo «Host:». En este caso, el host es example.com y el puerto es 80, ya que no se ha indicado un puerto específico. 

Paso 5: reenviar la petición del cliente y gestionar la respuesta

Cuando ya tengas el host y el puerto de destino, tienes que reenviar la petición del cliente al servidor de destino. En handle_client_request (), crea un socket web nuevo y úsalo para enviar la petición original al destino que quieras:

# create a socket to connect to the original destination server

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

# connect to the destination server

destination_socket.connect((host, port))

# send the original request

destination_socket.sendall(request)

Then, get ready to receive the server response and propagate it to the original client:

# read the data received from the server

# once chunk at a time and send it to the client

print("Received response:\n")

while True:

    # receive data from web server

    data = destination_socket.recv(1024)

    # Receive data from the original destination server

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

    # no more data to send

    if len(data) > 0:

        # send back to the client

        client_socket.sendall(data)

    else:

        break

Aquí también tienes que trabajar los bloques de uno en uno porque no sabes el tamaño de la respuesta. Cuando los datos están vacíos, ya no hay más datos que recibir y puedes terminar la operación.

No olvides cerrar los dos sockets que definiste en la función:

# close the sockets

destination_socket.close()

client_socket.close()

¡Genial! Acabas de crear un servidor proxy HTTP en Python. Ahora toca ver el código completo, ejecutarlo y comprobar que funciona correctamente.

Paso 6: juntarlo todo

Este es el código final de la secuencia de comandos de tu servidor proxy en Python:

import socket

import threading

def handle_client_request(client_socket):

    print("Received request:\n")

    # read the data sent by the client in the request

    request = b''

    client_socket.setblocking(False)

    while True:

        try:

            # receive data from web server

            data = client_socket.recv(1024)

            request = request + data

            # Receive data from the original destination server

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

        except:

            break

    # extract the webserver's host and port from the request

    host, port = extract_host_port_from_request(request)

    # create a socket to connect to the original destination server

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

    # connect to the destination server

    destination_socket.connect((host, port))

    # send the original request

    destination_socket.sendall(request)

    # read the data received from the server

    # once chunk at a time and send it to the client

    print("Received response:\n")

    while True:

        # receive data from web server

        data = destination_socket.recv(1024)

        # Receive data from the original destination server

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

        # no more data to send

        if len(data) > 0:

            # send back to the client

            client_socket.sendall(data)

        else:

            break

    # close the sockets

    destination_socket.close()

    client_socket.close()

def extract_host_port_from_request(request):

    # get the value after the "Host:" string

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

    host_string_end = request.find(b'\r\n', 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)

    # if there is a specific port

    port_pos = host_string.find(":")

    # no port specified

    if port_pos == -1 or webserver_pos < port_pos:

        # default port

        port = 80

        host = host_string[:webserver_pos]

    else:

        # extract the specific port from the host string

        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

    # bind the proxy server to a specific address and port

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

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

    # accept up to 10 simultaneous connections

    server.listen(10)

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

    # listen for incoming requests

    while True:

        client_socket, addr = server.accept()

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

        # create a thread to handle the client request

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

        client_handler.start()

if __name__ == "__main__":

    start_proxy_server()

Launch it with this command:

python proxy_server.py

Deberías ver el siguiente mensaje en el terminal:

Proxy server listening on port 8888...

Para comprobar que el servidor funciona, ejecuta una petición de proxy con cURL. Lee nuestra guía para saber más sobre cómo usar cURL con un proxy.

Abre un nuevo terminal y ejecuta:

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

Eso enviaría una petición GET al destino http://httpbin.org/ip a través del servidor proxy http://127.0.0.1:8888.

Deberías obtener algo como:

{

  "origin": "45.12.80.183"

}

Esa es la IP del servidor proxy. ¿Por qué? Porque el punto final /ip del proyecto HTTPBin devuelve la IP de la que viene la petición. Si estás ejecutando el servidor de forma local, «origin» corresponderá a tu IP. 

Nota: el servidor proxy en Python que hemos creado aquí solo funciona con destinos HTTP. Es bastante complicado extenderlo para gestionar conexiones HTTPS.

Ahora, echa un vistazo al registro que has escrito por la aplicación Python de tu servidor proxy. Debería incluir:

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"

}

Esto indica que el servidor proxy ha recibido la petición en el formato indicado por el protocolo HTTP. Después, lo ha reenviado al servidor de destino, ha registrado los datos de respuesta y ha devuelto la respuesta al cliente. ¿Por qué sabemos que eso es lo que ha hecho? Porque las IP que aparecen en «origin» son las mismas.

¡Enhorabuena! Acabas de aprender a crear un servidor proxy HTTP en Python.

Ventajas y desventajas de usar un servidor proxy en Python personalizado

Ahora que ya sabes cómo ejecutar un servidor proxy en Python, estás listo para conocer las ventajas y las limitaciones de este modelo.

Ventajas:

  • Control absoluto: con una secuencia de comandos personalizada en Python como esta, tienes un control absoluto de todo lo que hace tu servidor proxy. No hay actividad sospechosa ni filtración de datos.
  • Personalización: el servidor proxy se puede ampliar para incluir funciones útiles como el registro y el almacenamiento en caché de las peticiones para mejorar su rendimiento.

Desventajas:

  • Costes de infraestructura: configurar una arquitectura de servidor proxy no es fácil y cuesta mucho dinero en términos de hardware o de servicios VPS.
  • Mantenimiento complejo: tú eres el responsable de mantener la arquitectura del proxy, sobre todo de la escalabilidad y de la disponibilidad. Esta es una tarea que solo pueden abordar los administradores de sistemas profesionales.
  • Falta de fiabilidad: el problema principal de esta solución es que la IP de salida del servidor proxy nunca cambia. Como resultado, los sistemas anti-bots podrán bloquear la IP e impedir que el servidor acceda a las peticiones que necesites. En otras palabras, el proxy, en algún momento, dejará de funcionar.

Estas limitaciones y desventajas son bastante negativas como para usar un servidor proxy en Python personalizado en un entorno de producción. ¿Cuál es la solución? Un proveedor de proxy fiable como Bright Data. Crea tu cuenta, confirma tu identidad, consigue un proxy gratuito y utilizado en tu lenguaje de programación preferido. Por ejemplo, integra un proxy en tu secuencia de comandos de Python con peticiones.

Nuestra gran red proxy incluye millones de servidores proxy rápidos, fiables y seguros en todo el mundo. Descubre por qué somos el mejor proveedor de servidores proxy.

Conclusión

En esta guía, has aprendido qué es un servidor proxy y cómo funciona en Python. Además, has aprendido a construir uno desde cero usando sockets web. Ya puedes decir que eres un experto en proxies en Python. El principal problema de este modelo es que la IP estática de salida de tu servidor proxy te bloqueará en algún momento. Evítalo con los proxies rotativos de Bright Data.

Bright Data controla los mejores servidores proxy del mundo y trabaja para empresas de la lista Fortune 500 y para más de 20 000 clientes. La oferta incluye una amplia gama de tipos de proxy:

Una red proxy fiable, rápida y global también es la base de una serie de servicios de raspado web para recuperar datos de cualquier sitio de forma sencilla.