Tabla de Contenidos
Construyendo un cliente HTTP con urllib.request
Módulo perteneciente al curso Python avanzado para proyectos de seguridad
En esta sección, aprenderemos cómo usar urllib y cómo podemos construir clientes HTTP con este módulo.
urllib puede leer datos de una URL usando varios protocolos, como HTTP, HTTPS, FTP o Gopher. Este módulo proporciona la función urlopen utilizada para crear un objeto similar a un archivo con el que puede leer desde la URL. Este objeto tiene métodos como read(), readline(), readlines() y close(), que funcionan exactamente igual que en los objetos de archivo, aunque en realidad estamos trabajando con un contenedor que nos abstrae del uso de un socket a bajo nivel.
El método read() se utiliza para leer el “archivo” completo o la cantidad de bytes especificados como parámetro, readline() permite leer un fichero para leer una línea y readlines() permite leer todas las líneas y devuelve una lista con cada una de ellas.
El módulo urllib.request permite el acceso a un recurso publicado en Internet a través de su dirección.
La documentación oficial la podemos encontrar en https://docs.python.org/3/library/urllib.request.html#module-urllib.request
Si accedemos a la documentación del módulo, veremos todas las funciones que tienen esta clase.
Recuperar el contenido de una URL es un proceso sencillo cuando se hace usando urllib. Puede abrir el intérprete de Python y ejecutar las siguientes instrucciones:
>>> from urllib.request import urlopen >>> response = urlopen('http://www.python.org') >>> response <http.client.HTTPResponse object at 0x7fa3c53059b0> >>> response.readline()
En el ejemplo anterior utilizamos la función urllib.request.urlopen() para enviar una petición y recibir una respuesta para el recurso en http://www.python.org, en este caso una página HTML. Luego imprimiremos la primera línea del HTML que recibimos, con el método readline() del objeto de respuesta.
Ejemplo con el método urlopen()
En el siguiente ejemplo, realizamos la petición a una página web usando el método urlopen(). Cuando pasamos una URL al método urlopen(), devolverá un objeto, podemos usar el atributo read() para obtener los datos de este objeto en un formato de cadena.
La función urlopen tiene un parámetro de datos opcional con el cual enviar información a direcciones HTTP usando POST (los parámetros se envían en la solicitud misma), por ejemplo, para responder a un formulario.
Puede encontrar un ejemplo en el archivo urllib_basico.py:
import urllib.request #peticion get try: response = urllib.request.urlopen("http://www.python.org") print(response.read().decode('utf-8')) response.close() except Exception as error: print("Ocurrió un error",error) except HTTPError as error: print("Ocurrió un error",error) except URLError as error: print("Ocurrió un error",error)
Cuando trabajamos con el módulo urllib, también necesitamos administrar errores y el tipo de excepción URLError. Si trabajamos con HTTP, también podemos encontrar errores en la subclase HTTPError, que se generan cuando el servidor devuelve un código de error HTTP, como el error 404 cuando no se encuentra el recurso.
Objeto de respuesta
Exploremos el objeto de respuesta en detalle. Podemos ver en el ejemplo anterior que el método urlopen() devuelve un objeto de la clase http.client.HTTPResponse.
El objeto de respuesta devuelve información sobre los datos de recursos solicitados y las propiedades y metadatos de la respuesta.
El siguiente código realiza una petición con urllib al dominio python.org:
>>> response = urllib.request.urlopen('http://www.python.org') >>> response.read() b'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">\n<html >>> response.read(100)
En la salida anterior vemos que el método read() nos permite leer los datos de recursos solicitados y devolver el número especificado de bytes.
Códigos de estado
Las respuestas HTTP nos proporcionan una forma de verificar el estado de la respuesta a través de códigos de estado. Podemos leer el código de estado de una respuesta usando su propiedad de estado.
El valor de 200 es un código de estado HTTP que nos dice que la petición ha devuelto una respuesta correcta:
>>> response.status >>> 200
Los códigos de estado se clasifican en los siguientes grupos:
100: informativo.200: éxito.300: redirección.400: error del cliente.500: error del servidor.
Los códigos de estado nos ayudan a ver si nuestra respuesta fue exitosa o no. Cualquier código en el rango 200 indica un éxito, mientras que cualquier código en el rango 400 o el rango 500 indica un error en el servidor.
La IANA mantiene la lista oficial de códigos de estado y puede encontrarla en https://www.iana.org/assignments/http-status-codes
Manejo de excepciones con urllib.request
Los códigos de estado siempre deben verificarse para que nuestro programa pueda responder adecuadamente si algo sale mal.
El paquete urllib nos ayuda a verificar los códigos de estado al generar una excepción si encuentra un problema. Veamos cómo capturarlos y manejarlos de manera útil.
Puede encontrar el siguiente código en el archivo urllib_exceptions.py:
#!/usr/bin/env python3 import urllib.error from urllib.request import urlopen try: urlopen('https://www.ietf.org/rfc/rfc0.txt') except urllib.error.HTTPError as e: print('Exception', e) print('status', e.code) print('reason', e.reason) print('url', e.url)
El resultado del script anterior es:
Exception HTTP Error 404: Not Found status 404 reason Not Found url https://www.ietf.org/rfc/rfc0.tx
En el script anterior, solicitamos un documento rfc0.txt, que no existe. Entonces, el servidor devuelve un código de estado 404, y urllib ha capturado esto y ha generado una excepción del tipo HTTPError. Puede ver que HTTPError proporciona atributos útiles con respecto a la petición realizada. En el ejemplo anterior, obtenemos los atributos de estado, razón y URL para obtener información sobre la respuesta.
Comprobación de cabeceras HTTP con urllib.request
Las peticiones HTTP constan de dos partes principales: cabeceras y un cuerpo. Las cabeceras son las líneas de información que contienen metadatos específicos sobre la respuesta que devuelve el servidor y le dice al cliente cómo interpretarla. Con una simple llamada podemos verificar si las cabeceras de respuesta pueden proporcionar información extra sobre el servidor web que está detrás de un dominio.
La declaración http_response.headers proporciona las cabeceras de respuesta del servidor web. Antes de acceder a esta propiedad, es importante verificar si el código de respuesta es igual a 200, indicando que la respuesta es OK.
En el siguiente ejemplo realizamos una petición a dominio que el usuario introduce por la entrada estándar y si el código de estado de la respuesta es 200 entonces muestras las cabeceras de la respuesta accediendo a la propiedad headers del objeto http_response. Puede encontrar el siguiente código en el archivo urllib_headers_basic.py:
import urllib.request url = input("Introduce la URL:") http_response = urllib.request.urlopen(url) print('Código de estado: '+ str(http_response.code)) if http_response.code == 200: print(http_response.headers)
Otra forma de recuperar las cabeceras de la respuesta es mediante el uso del método info() del objeto de respuesta, que devolverá un diccionario con las diferentes cabeceras en formato clave-valor. Puede encontrar el siguiente código en el archivo urllib_headers_info.py:
import urllib.request url = input("Introduce la URL:") http_response = urllib.request.urlopen(url) print('Código de estado: '+ str(http_response.code)) if http_response.code == 200: print(http_response.info())
>>> response_headers = response.headers >>> print(response_headers.keys()) ['content-length', 'via', 'x-cache', 'accept-ranges', 'x-timer', 'vary', 'strict-transport-security', 'server', 'age', 'connection', 'x-xssprotection', 'x-cache-hits', 'x-served-by', 'date', 'x-frame-options','content-type', 'x-clacks-overhead']
El siguiente script obtendrá los cabeceras del sitio a través de las cabeceras del objeto de respuesta. Para esta tarea, podemos usar la propiedad headers o el método getheaders(). El método getheaders() devuelve las cabeceras de la respuesta como una lista de tuplas en formato (nombre de la cabeceras, valor de la cabecera). Puede encontrar el siguiente código en el archivo get_headers.py>:
#!/usr/bin/env python3 import urllib.request url = input("Enter the URL:") http_response = urllib.request.urlopen(url) if http_response.code == 200: print(http_response.headers) for key,value in http_response.getheaders(): print(key,value)
Ejecución del script
En la salida del script podemos ver las cabeceras de la respuesta para el dominio http://wwww.python.org:
Enter the URL:http://www.python.org Connection: close Content-Length: 49012 Server: nginx Content-Type: text/html; charset=utf-8 X-Frame-Options: DENY Via: 1.1 vegur Via: 1.1 varnish Accept-Ranges: bytes Date: Sun, 24 May 2020 18:53:48 GMT Via: 1.1 varnish Age: 2979 X-Served-By: cache-bwi5136-BWI, cache-mad22026-MAD X-Cache: HIT, HIT X-Cache-Hits: 1, 2 X-Timer: S1590346428.092705,VS0,VE0 Vary: Cookie Strict-Transport-Security: max-age=63072000; includeSubDomains
Personalización de cabeceras con urllib
En esta sección, veremos cómo añadir nuestras propias cabeceras utilizando el encabezado User-Agent.
Por ejemplo, podríamos personalizar la cabeceras que se envían para recuperar una versión específica de un sitio web. Para esta tarea, podemos usar la cabecera Accept-Language, que le dice al servidor nuestro idioma para el recurso que devuelve.
User-Agent es un encabezado que se utiliza para identificar el navegador y el sistema operativo que estamos utilizando para realizar peticiones a un determinado dominio.
Por defecto, urllib se identifica como Python-urllib/version como podemos comprobar al ejecutar las siguientes instrucciones en el intérprete:
>>> from urllib.request import Request >>> from urllib.request import urlopen >>> request = Request('http://www.python.org') >>> urlopen(req) <http.client.HTTPResponse object at 0x034AEBF0> >>> request.get_header('User-agent') 'Python-urllib/3.7'
Si queremos identificarnos, por ejemplo, como un navegador Chrome, podríamos redifinir el parámetro de cabeceras al realizar la petición.
En este ejemplo, creamos la misma solicitud GET utilizando la clase Request pasando como parámetro una cabecera de User-Agent HTTP personalizada.
Para hacer uso de la funcionalidad que proporcionan las cabeceras, las añadimos antes de enviar la petición. Para hacer esto, podemos seguir estos pasos:
- Crear un objeto de solicitud (
Request) - Añadir cabeceras al objeto
Request. - Llamar al método
urlopen()para enviar el objetoRequest.
Puede encontrar el siguiente código en el archivo urllib_requests_headers.py:
import urllib.request url = "http://www.python.org" headers= {'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'} request = urllib.request.Request(url,headers=headers) response = urllib.request.urlopen(request) print('User-agent',request.get_header('User-agent')) if response.code == 200: print(response.headers)
En el código anterior estamos haciendo uso del User Agent correspondiente al navegador Chrome en un sistema operativo Linux.
Se pueden consultar los valores de este User Agent para otros sistemas operativos en la URL https://www.whatismybrowser.com/guides/the-latest-user-agent/chrome
Con la clase Request del módulo urllib, es posible crear cabeceras personalizadas, para esto es necesario definir en el argumento de headers un diccionario con el formato de clave y valor. En el ejemplo anterior, establecemos la configuración del encabezado del agente de usuario (user-agent) y le asignamos el valor relativo al de un navegador Chrome.
Obtener correos electrónicos de una URL con urllib.request
En este ejemplo podemos ver cómo extraer correos electrónicos usando urllib y expresiones regulares.
Puede encontrar el siguiente código en el archivo get_emails_from_url.py:
import urllib.request import re web = input("Introduce una url(sin http://): ") # https://www.adrformacion.com/ # obtener la respuesta response = urllib.request.Request('http://'+web) # obtener el contenido de la página a partir de la respuesta content = urllib.request.urlopen(response).read() # expression regular para detectar emails pattern = re.compile("[-a-zA-Z0-9._]+@[-a-zA-Z0-9_]+.[a-zA-Z0-9_.]+") # obtener emails a partir de una expresión regular mails = re.findall(pattern,str(content)) print(mails)
Obtener enlaces de una URL con urllib.request
En este ejemplo podemos ver cómo extraer enlaces usando urllib y expresiones regulares.
Puede encontrar el siguiente código en el archivo urlib_link_extractor.py:
#!/usr/bin/env python3 from urllib.request import urlopen import re def download_page(url): return urlopen(url).read() def extract_links(page): link_regex = re.compile('<a[^>]+href=["\'](.*?)["\']',re.IGNORECASE) return link_regex.findall(page) if __name__ == '__main__': target_url = 'http://www.adrformacion.com' content = download_page(target_url) links = extract_links(str(content)) for link in links: print(link)
Actividad práctica: extraer imágenes usando urllib y expresiones regulares
En esta actividad el objetivo cómo extraer imágenes de una url usando el módulo urllib y haciendo uso de expresiones regulares. La manera fácil de extraer imágenes de una URL es usar el módulo re para encontrar elementos img en la URL de destino.
#!/usr/bin/env python3 from urllib.request import urlopen, urljoin import re def download_page(url): return urlopen(url).xxx() def extract_image_locations(page): img_regex = re.xxx('<expresion_regular>',re.IGNORECASE) return img_regex.xxx(page) if __name__ == '__main__': target_url = 'http://www.adrformacion.com' content = xxx(target_url) image_locations = xxx(str(content)) for src in xxx: print(xxx(target_url, src))
Completar las xxx del siguiente script para obtener por variables y métodos declarados en el script. Pensar también cual podría ser la expresión regular para detectar las etiquetas img dentro del código html.
Solución
#!/usr/bin/env python3 from urllib.request import urlopen, urljoin import re def download_page(url): return urlopen(url).read() def extract_image_locations(page): img_regex = re.compile('<img[^>]+src=["\'](.*?)["\']',re.IGNORECASE) return img_regex.findall(page) if __name__ == '__main__': target_url = 'http://www.adrformacion.com' content = download_page(target_url) image_locations = extract_image_locations(str(content)) for src in image_locations : print(urljoin(target_url, src))
Módulo urllib3
Se trata de un módulo que extiende las funcionalidades de urllib permitiendo aprovechar algunas de las características más llamativas del protocolo HTTP 1.1 de forma automática. Añade nuevas funcionalidades al módulo urllib diferenciándose principalmente en su capacidad de soportar características avanzadas del protocolo HTTP 1.1.
Documentación del módulo urllib3
Como característica más relevante, permite la reutilización de conexiones TCP para realizar múltiples peticiones y soporta la validación de certificados en conexiones HTTPS.
Una característica interesante es que le podemos indicar el número de conexiones que vamos a reservar para el pool de conexiones que estamos creando utilizando la clase PoolManager.
Esta clase se encarga de gestionar la conexión de forma persistente y reutilizar las conexiones HTTP que va creando gracias a un pool de conexiones.
Para realizar una petición con urllib3 se emplea el método request del objeto pool que hayamos creado. El método request acepta por parámetros el tipo de petición (GET, POST) y la url del dominio.
>>> import urllib3 >>> pool = urllib3.PoolManager(10) >>> response = pool.request('GET','url_dominio')
La respuesta la obtenemos en el objeto response y si accedemos a la propiedad response.status, se obtiene el código HTTP de la respuesta, que en el caso de ir bien devolverá un 200 OK.
A continuación, mostramos un ejemplo completo con urllib3 donde realizamos una petición GET al dominio python.org y obtenemos las cabeceras de la respuesta en formato clave-valor. Puede encontrar el siguiente código en el archivo get_headers_urllib3.py:
import urllib3 pool = urllib3.PoolManager(10) response = pool.request('GET','http://www.python.org') print(response.status) print("Keys\n-------------") print(response.headers.keys()) print("Values\n-------------") print(response.headers.values()) for header,value in response.headers.items(): print(header + ":" + value)
Como resultado de la ejecución vemos que hemos obtenido un código 200 de petición OK y recorremos las cabeceras de la respuesta.
