The First Iterations
Comenzaremos implementando el programa poco a poco, asegurándonos siempre de alcanzar un hito donde el código funcione, aunque no esté completamente terminado. Incluso si tenemos una idea clara de cómo se implementará el código y cómo se verá al final, sigue siendo una buena idea ir despacio y construirlo capa por capa.
Dado que queremos terminar con un programa que pueda obtener todas las palabras de una página web y tal vez también tenga algunas otras características, primero escribamos el código necesario para realizar la tarea más básica: imprimir el HTML de una página web. En resumen, esto es lo que deberíamos lograr:
- El código descargará e imprimirá el HTML completo de una página web.
- La URL de la página web estará fija dentro del código.
- Escribiremos el código en su forma más simple y reescribiremos partes según sea necesario.
- Usaremos la biblioteca
requests
.
Así que, primero lo primero, importemos la biblioteca requests
y almacenemos la URL de destino en una variable. Luego usamos la biblioteca requests
para obtener la URL proporcionada e imprimir el HTML.
Printing Web Page Source Code
import requests
PAGE_URL = 'http://target:port'
resp = requests.get(PAGE_URL)
html_str = resp.content.decode()
print(html_str)
Ahora, ¿qué pasa si escribimos mal la URL? Probémoslo en nuestro terminal interactivo de Python y veamos:
Experimenting in IDLE
>>> r = requests.get('http://target:port/missing.html')
>>> r.status_code
404
>>> print(r.content.decode())
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
<title>Error response</title>
</head>
<body>
<h1>Error response</h1>
<p>Error code: 404</p>
<p>Message: File not found.</p>
<p>Error code explanation: HTTPStatus.NOT_FOUND - Nothing matches the given URI.</p>
</body>
</html>
En el lado positivo, obtenemos un status_code
adecuado del servidor web, que en este ejemplo es el módulo del servidor web incluido con Python (http.server
). Sin embargo, si esperábamos que la salida HTML contuviera elementos específicos que luego intentáramos acceder y usar, por ejemplo, un <div id="products">
, nuestro programa en Python fallaría al intentar usar elementos que no existen. ¡No hay productos en esta página de error! Vaya. Implementemos una verificación simple para evitar trabajar con enlaces rotos.
Naive "error" Handling
import requests
PAGE_URL = 'http://target:port'
resp = requests.get(PAGE_URL)
if resp.status_code != 200:
print(f'HTTP status code of {resp.status_code} returned, but 200 was expected. Exiting...')
exit(1)
html_str = resp.content.decode()
print(html_str)
Ahora bien, tenemos un código que hace algo, pero no está dentro de una función. Para evitar desordenar el código, es recomendable mantener las cosas simples
y separadas
, así que avancemos y hagamos un refactor
al código, es decir, cambiémoslo y mejorémoslo.
The get_html_of Function
Echemos un vistazo al siguiente código:
import requests
PAGE_URL = 'http://target:port'
def get_html_of(url):
resp = requests.get(url)
if resp.status_code != 200:
print(f'HTTP status code of {resp.status_code} returned, but 200 was expected. Exiting...')
exit(1)
return resp.content.decode()
print(get_html_of(PAGE_URL))
Movimos la parte del código que obtiene el HTML dentro de una función y luego cambiamos la última línea para imprimir el resultado de esta llamada en lugar de una variable (que ya no existe). Además, nota la indentación dentro de la función.
Teniendo la funcionalidad más básica en su lugar, podemos comenzar a trabajar con la página HTML. Tomemos un momento para pensar qué necesitamos hacer. Para este tipo de ejercicio, puede ser útil listar las acciones en un papel y, para cada acción, preguntarse: "¿Cómo hago esto?" y escribir esos pasos junto a ellas. En nuestro caso, necesitamos:
- Encontrar todas las palabras en la página, ignorando etiquetas HTML y otros metadatos.
- Contar la ocurrencia de cada palabra y anotarla.
- Ordenar por ocurrencia.
- Hacer algo con las palabras más frecuentes, por ejemplo, imprimirlas.
¿Cómo encontramos todas las palabras en la página, ignorando etiquetas HTML y otros metadatos? Aquí es donde BeautifulSoup entra en juego. Un vistazo rápido a la documentación (https://www.crummy.com/software/BeautifulSoup/bs4/doc/) muestra que podemos llamar a la función get_text()
del objeto BeautifulSoup para obtener todo el texto de la página web como una cadena.
A continuación, necesitamos contar las ocurrencias de cada palabra. Hay muchas formas de hacerlo. Podríamos elegir la primera palabra, contar todas sus ocurrencias, anotarla y marcarla como "contada". Luego, podríamos pasar a la siguiente palabra y, si no ha sido contada ya, contar su ocurrencia y anotarla. Este proceso es relativamente simple, pero también bastante lento. Imagínate haciendo este ejercicio para un libro entero lleno de palabras. Es relativamente ineficiente.
Seamos más innovadores y pensemos por un momento si alguna máquina o aplicación ya hace algo similar en la vida real. Las máquinas expendedoras antiguas vienen a la mente.
Old School Vending Machines
Antes de los teléfonos inteligentes y la digitalización de muchas máquinas, las máquinas expendedoras tenían que contar el número de monedas insertadas y cuántas de cada tipo. Algunas máquinas lograban esto haciendo que la moneda se deslizara por una rampa y cayera en el agujero más pequeño posible, comenzando de pequeño a grande. Así, una moneda grande pasaría por encima de un agujero pequeño, mientras que una moneda pequeña caería por el agujero. Una moneda se contaría una vez (por ejemplo, activando un pequeño brazo o interruptor de metal al caer).
Si contamos palabras de la misma manera en que algunas máquinas expendedoras cuentan monedas, podemos contar todas las ocurrencias de todas las palabras y solo necesitamos recorrer el texto una vez. Tendremos un dictionary
de ocurrencias de palabras y luego, para cada palabra, verificamos si ya ha sido vista antes. Si es así, incrementamos el conteo en uno. Si no ha sido añadida antes, agregamos un registro de la palabra y una ocurrencia.
Después de esto, tenemos que ordenar por ocurrencia para ver qué palabras ocurrieron más y luego decidir imprimir las diez palabras más utilizadas. Alternativamente, podríamos filtrar las palabras y solo mirar aquellas con más de cuatro caracteres o agregar variaciones de números y símbolos para generar un diccionario para ataques de password. Más sobre eso más adelante.
Regex
El primer paso fue encontrar todas las palabras en el HTML mientras ignoramos las etiquetas HTML. Si usamos la función get_text()
que discutimos anteriormente, podemos usar el módulo re
de regular expression
para ayudarnos. Este módulo tiene una función findall
que toma una cadena de regex
(abreviatura de "reg
gular ex
pression") y un texto como parámetros, y luego devuelve todas las coincidencias en una lista. Usaremos la cadena de regex \w+
, que coincide con todos los caracteres de palabras, es decir, a-z
, A-Z
, 0-9
y _
. Aquí está el código actualizado:
Finding All Words in HTML
import requests
import re
from bs4 import BeautifulSoup
PAGE_URL = 'http://target:port'
def get_html_of(url):
resp = requests.get(url)
if resp.status_code != 200:
print(f'HTTP status code of {resp.status_code} returned, but 200 was expected. Exiting...')
exit(1)
return resp.content.decode()
html = get_html_of(PAGE_URL)
soup = BeautifulSoup(html, 'html.parser')
raw_text = soup.get_text()
all_words = re.findall(r'\w+', raw_text)
Una nueva adición es la cadena r'...'
. Esto es un string r
aw, lo que significa que Python debe asumir que los caracteres dentro de la cadena son los caracteres reales a usar. Normalmente, un \
se usa como escape-character
, que nos ayuda a definir caracteres especiales, por ejemplo, \n
o \t
, que son salto de línea y tabulación respectivamente. Aquí r'\w+'
le dice a Python que interprete la parte \w
de la cadena como dos caracteres individuales y no como un carácter escapado w
.
Cuando ejecutamos esto, nada sucede excepto en memoria. La variable all_words
es, suponiendo que todo vaya bien, una lista de todas las palabras de la página web en orden de ocurrencia e incluyendo duplicados. Luego recorreremos esta lista y contaremos cada palabra. Una forma de lograrlo es el siguiente fragmento de código:
Counting Word Occurrences
# Previous code omitted
all_words = re.findall(r'\w+', raw_text)
word_count = {}
for word in all_words:
if word not in word_count:
word_count[word] = 1
else:
current_count = word_count.get(word)
word_count[word] = current_count + 1
This snippet should look familiar
Para resumir rápidamente, declaramos una nueva variable word_count
como un diccionario vacío - una estructura de datos de pares clave/valor que permite buscar algún valor dado una clave. Luego recorremos cada palabra en all_words
y verificamos si ya existe. Configuramos la clave (palabra) con un valor de 1
si no existe. De lo contrario (else), obtenemos el valor actual establecido para word
y configuramos el nuevo valor de word
como el valor anterior más uno.
Ahora tenemos un diccionario con todas las palabras encontradas en el sitio web y su respectiva frecuencia.
Advanced Tricks: "Python is easy" |
---|
A menudo se dice que Python es fácil, a lo que mi respuesta siempre es "Python simple es fácil, Python complejo no lo es". El ejemplo anterior de contar palabras de hecho se puede reducir a estas dos líneas: for word in all_words: word_count[word] = word_count.setdefault(word, 0) + 1 Sin embargo, la cantidad de cosas que suceden aquí es bastante sorprendente. En resumen, "setdefault" establecerá el valor de la clave ("word") al valor especificado (0) si el diccionario no contiene ya una clave "word", O devolverá el valor actual de la clave "word". La segunda línea, por lo tanto, establece un valor de 1 para la clave "word", O recupera el valor actual y lo incrementa en uno. ¿Confuso? Sí. ¿Nuestro punto? El código sofisticado no siempre es la mejor opción, así que mantenlo simple e inteligente. No estamos aquí para presumir, estamos aquí para resolver problemas. |
Para obtener una lista ordenada de las palabras de modo que podamos centrarnos en las que ocurren con más frecuencia, ya sea que se nos ocurra mágicamente el siguiente fragmento de código o, más realísticamente, busquemos ayuda en Google ("python sort dictionary by values" y términos de búsqueda similares) encontraremos la siguiente respuesta.
Sorting Words in a List
top_words = sorted(word_count.items(), key=lambda item: item[1], reverse=True)
Como con todo lo que está en línea, no confíes ciegamente en que no es malicioso. En cuanto a contenido altamente valorado y respuestas con muchos comentarios positivos, un consejo es el viejo dicho: confía, pero verifica. Una vez que estemos seguros de que el fragmento de código que encontramos es lo que necesitamos, finalmente podemos imprimir las 10 palabras principales de esta manera:
Printing 10 Elements
>>> top_words = sorted(word_count.items(), key=lambda item: item[1], reverse=True)
>>> for i in range(10):
... print(top_words[i])
Hacer esto imprimirá una salida similar a:
>>> top_words = sorted(word_count.items(), key=lambda item: item[1], reverse=True)
>>> for i in range(10):
... print(top_words[i])
('foo', 6)
('bar', 5)
('bas', 5)
('hello', 4)
('academy', 4)
('birb', 1)
Esto puede parecer un poco extraño o al menos no muy útil para nuestro progreso. Lo que podemos hacer es imprimir la palabra real en lugar de imprimir cada tupla de (word, occurrence) seleccionando el primer elemento de la tupla para cada tupla (top_words[i][0]
). La iteración actual de todo el código se ve así:
The First Iteration
import requests
import re
from bs4 import BeautifulSoup
PAGE_URL = 'http://target:port'
def get_html_of(url):
resp = requests.get(url)
if resp.status_code != 200:
print(f'HTTP status code of {resp.status_code} returned, but 200 was expected. Exiting...')
exit(1)
return resp.content.decode()
html = get_html_of(PAGE_URL)
soup = BeautifulSoup(html, 'html.parser')
raw_text = soup.get_text()
all_words = re.findall(r'\w+', raw_text)
word_count = {}
for word in all_words:
if word not in word_count:
word_count[word] = 1
else:
current_count = word_count.get(word)
word_count[word] = current_count + 1
top_words = sorted(word_count.items(), key=lambda item: item[1], reverse=True)
for i in range(10):
print(top_words[i][0])