Clases principales del analizador¶
Antes de empezar
- Lee la página Buscar elementos para saber cómo buscar y extraer elementos de un objeto Selector.
Después de ver las formas de seleccionar elementos y sus funciones, vamos a conocer la clase Selector y los demás objetos. Así entenderás mejor cómo funciona el analizador.
La clase Selector es la base del analizador de Scrapling. Analiza HTML y permite seleccionar elementos. Puedes importarla de cualquiera de estas formas:
Después úsala directamente, como vimos en la introducción:page = Selector(
'<html>...</html>',
url='https://example.com'
)
# Then select elements as you like
elements = page.css('.product')
La página es un Selector, y cada elemento dentro de ella también es un Selector. El texto de los elementos y de sus atributos usa TextHandler. Los atributos de cada elemento se guardan en AttributesHandler. Veremos ambos más adelante. Primero, vamos con Selector.
Selector¶
Argumentos¶
El más importante es content: recibe el HTML que quieres analizar, como str o bytes.
url establece la URL de la página. Se usa para resolver enlaces relativos y separar los datos adaptativos por dominio. Los argumentos adaptive, storage y storage_args configuran la función adaptativa. No afectan al uso normal si no la activas.
Estos argumentos controlan cómo se procesa el HTML:
- encoding: codificación del HTML. Por defecto es
UTF-8. - keep_comments: indica si se conservan los comentarios HTML al analizar la página. Está desactivado por defecto, ya que los comentarios pueden causar problemas al extraer datos.
Dejaremos huge_tree y root para más adelante para no complicar esta página.
Son funciones avanzadas que no necesitas para empezar. La sección de desarrollo cubre esos detalles si quieres profundizar.
La mayoría de las propiedades de la página y sus elementos se calculan solo cuando las usas, como el contenido de texto. Esta carga diferida es una de las razones de la velocidad de Scrapling :)
Propiedades¶
Ya vimos parte de esto en la introducción, pero aquí lo explicaremos con más detalle y ejemplos más completos. Las propiedades para recorrer la página están en su propia sección de navegación.
Para los ejemplos, vamos a analizar esta página HTML:
<html>
<head>
<title>Some page</title>
</head>
<body>
<div class="product-list">
<article class="product" data-id="1">
<h3>Product 1</h3>
<p class="description">This is product 1</p>
<span class="price">$10.99</span>
<div class="hidden stock">In stock: 5</div>
</article>
<article class="product" data-id="2">
<h3>Product 2</h3>
<p class="description">This is product 2</p>
<span class="price">$20.99</span>
<div class="hidden stock">In stock: 3</div>
</article>
<article class="product" data-id="3">
<h3>Product 3</h3>
<p class="description">This is product 3</p>
<span class="price">$15.99</span>
<div class="hidden stock">Out of stock</div>
</article>
</div>
<script id="page-data" type="application/json">
{
"lastUpdated": "2024-09-22T10:30:00Z",
"totalProducts": 3
}
</script>
</body>
</html>
>>> page.get_all_text()
'Some page\n\n \n\n \nProduct 1\nThis is product 1\n$10.99\nIn stock: 5\nProduct 2\nThis is product 2\n$20.99\nIn stock: 3\nProduct 3\nThis is product 3\n$15.99\nOut of stock'
get_all_text admite estos argumentos opcionales:
- separator: separador con el que se unen los textos encontrados. Por defecto es '\n'.
- strip: quita los espacios de los extremos de cada texto antes de unirlos. Está desactivado por defecto.
- ignore_tags: tupla de etiquetas que se ignoran junto con todo su contenido. Por defecto es
('script', 'style',). - valid_values: recoge solo textos con contenido. Ignora los vacíos y los que solo tienen espacios. Está activado por defecto.
El texto devuelto es un TextHandler, no una cadena normal. Lo veremos después. Si contiene JSON válido, puedes usar .json():
>>> script = page.find('script')
>>> script.json()
{'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}
html:
La misma idea se aplica tanto a page como a element, así que no la repetiremos.
Obtén los atributos del elemento:
Accede a un atributo concreto de cualquiera de estas formas: Comprueba si existe un atributo con alguno de estos métodos: Obtén el HTML del elemento:>>> article.html_content
'<article class="product" data-id="1"><h3>Product 1</h3>\n <p class="description">This is product 1</p>\n <span class="price">$10.99</span>\n <div class="hidden stock">In stock: 5</div>\n </article>'
<article class="product" data-id="1"><h3>Product 1</h3>
<p class="description">This is product 1</p>
<span class="price">$10.99</span>
<div class="hidden stock">In stock: 5</div>
</article>
.body para obtener el contenido original de la página. Desde v0.4, en los objetos Response de las herramientas de descarga, .body siempre devuelve bytes.
Obtén todos los antecesores del elemento en el árbol DOM:
>>> article.path
[<data='<div class="product-list"> <article clas...' parent='<body> <div class="product-list"> <artic...'>,
<data='<body> <div class="product-list"> <artic...' parent='<html><head><title>Some page</title></he...'>,
<data='<html><head><title>Some page</title></he...'>]
>>> article.generate_css_selector
'body > div > article'
>>> article.generate_full_css_selector
'body > div > article'
>>> article.generate_xpath_selector
"//body/div/article"
>>> article.generate_full_xpath_selector
"//body/div/article"
Navegación¶
Con los elementos anteriores, veamos las propiedades y métodos para recorrer la página.
Si no conoces el árbol DOM o las estructuras de árbol, esta sección puede resultar algo confusa. Te recomiendo leer sobre esos conceptos para entenderlos mejor.
Aquí tienes una explicación rápida para empezar.
El elemento html es la raíz del árbol de la página, ya que toda página empieza con un elemento html.
Justo debajo están elementos como head y body. Son hijos de html, y ese elemento html es su padre. A su vez, head y body son hermanos porque comparten el mismo padre.
Accede al padre de un elemento:
>>> article.parent
<data='<div class="product-list"> <article clas...' parent='<body> <div class="product-list"> <artic...'>
>>> article.parent.tag
'div'
>>> article.children
[<data='<h3>Product 1</h3>' parent='<article class="product" data-id="1"><h3...'>,
<data='<p class="description">This is product 1...' parent='<article class="product" data-id="1"><h3...'>,
<data='<span class="price">$10.99</span>' parent='<article class="product" data-id="1"><h3...'>,
<data='<div class="hidden stock">In stock: 5</d...' parent='<article class="product" data-id="1"><h3...'>]
children, pero recorre todos los niveles:
>>> article.below_elements
[<data='<h3>Product 1</h3>' parent='<article class="product" data-id="1"><h3...'>,
<data='<p class="description">This is product 1...' parent='<article class="product" data-id="1"><h3...'>,
<data='<span class="price">$10.99</span>' parent='<article class="product" data-id="1"><h3...'>,
<data='<div class="hidden stock">In stock: 5</d...' parent='<article class="product" data-id="1"><h3...'>]
children, porque sus hijos no tienen otros hijos.
Este ejemplo con el elemento de clase product-list muestra mejor la diferencia entre children y below_elements:
>>> products_list = page.css('.product-list')[0]
>>> products_list.children
[<data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'>,
<data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>,
<data='<article class="product" data-id="3"><h3...' parent='<div class="product-list"> <article clas...'>]
>>> products_list.below_elements
[<data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'>,
<data='<h3>Product 1</h3>' parent='<article class="product" data-id="1"><h3...'>,
<data='<p class="description">This is product 1...' parent='<article class="product" data-id="1"><h3...'>,
<data='<span class="price">$10.99</span>' parent='<article class="product" data-id="1"><h3...'>,
<data='<div class="hidden stock">In stock: 5</d...' parent='<article class="product" data-id="1"><h3...'>,
<data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>,
...]
>>> article.siblings
[<data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>,
<data='<article class="product" data-id="3"><h3...' parent='<div class="product-list"> <article clas...'>]
>>> article.next
<data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>
previous:
>>> article.previous # It's the first child, so it doesn't have a previous element
>>> second_article = page.css('.product[data-id="2"]')[0]
>>> second_article.previous
<data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'>
True si cumple la condición, o False si no:
>>> article.find_ancestor(lambda ancestor: ancestor.has_class('product-list'))
<data='<div class="product-list"> <article clas...' parent='<body> <div class="product-list"> <artic...'>
>>> article.find_ancestor(lambda ancestor: ancestor.css('.product-list')) # Same result, different approach
<data='<div class="product-list"> <article clas...' parent='<body> <div class="product-list"> <artic...'>
Selectors¶
Selectors es la versión en lista de Selector. Hereda del tipo List de Python, así que conserva las propiedades y métodos de List. Además, añade métodos para trabajar con los objetos Selector que contiene.
En Selector, los métodos y propiedades que devuelven varios elementos los entregan como un objeto Selectors.
Desde v0.4, todos los métodos de selección devuelven objetos Selector/Selectors, incluso al seleccionar texto o atributos. Los nodos de texto obtenidos con ::text, /text(), ::attr() o /@attr se envuelven en objetos Selector. Su tag es "#text" y su propiedad text devuelve el valor de texto. Las demás propiedades devuelven valores vacíos o por defecto sin causar errores.
page.css('a::text') # -> Selectors (of text node Selectors)
page.xpath('//a/text()') # -> Selectors
page.css('a::text').get() # -> TextHandler (the first text value)
page.css('a::text').getall() # -> TextHandlers (all text values)
page.css('a::attr(href)') # -> Selectors
page.xpath('//a/@href') # -> Selectors
page.css('.price_color') # -> Selectors
Métodos para extraer datos¶
Desde v0.4, Selector y Selectors ofrecen get(), getall() y sus alias extract_first y extract, siguiendo las convenciones de Scrapy. El antiguo método get_all() se eliminó.
En un objeto Selector:
get()devuelve unTextHandler: el valor de texto si es un nodo de texto, o el HTML externo completo como cadena si es un elemento HTML.getall()devuelve una listaTextHandlerscon esa única cadena.extract_firstes un alias deget(), yextractes un alias degetall().
>>> page.css('h3')[0].get() # Outer HTML of the element
'<h3>Product 1</h3>'
>>> page.css('h3::text')[0].get() # Text value of the text node
'Product 1'
En un objeto Selectors:
get(default=None)devuelve la cadena del primer elemento, odefaultsi la lista está vacía.getall()convierte todos los elementos en cadenas y devuelve una listaTextHandlers.extract_firstes un alias deget(), yextractes un alias degetall().
>>> page.css('.price::text').get() # First price text
'$10.99'
>>> page.css('.price::text').getall() # All price texts
['$10.99', '$20.99', '$15.99']
>>> page.css('.price::text').get('') # With default value
'$10.99'
Estos métodos sirven con todos los tipos de selección, como CSS, XPath y find. Son la forma recomendada de extraer texto y atributos con un estilo compatible con Scrapy.
Veamos ahora qué más ofrece Selectors.
Propiedades¶
Además de las operaciones normales de las listas de Python, como recorrerlas o tomar una parte,
puedes hacer lo siguiente:
Ejecutar selectores CSS y XPath directamente sobre los objetos Selector de la lista. Los tipos devueltos son los mismos que en css y xpath de Selector. Los argumentos también son parecidos, salvo que aquí no está disponible adaptive. Esto facilita encadenar métodos.
>>> page.css('.product h3::text').getall()
['Product 1', 'Product 2', 'Product 3']
>>> page.css('.product').css('h3::text').getall()
['Product 1', 'Product 2', 'Product 3']
re y re_first directamente, con los mismos argumentos que en Selector. Los explicaremos en la sección de TextHandler.
Aquí re_first funciona de otra forma: ejecuta re sobre cada Selector y devuelve el primero que tenga un resultado. re devuelve un TextHandlers que reúne todos los objetos TextHandler en un solo TextHandlers.
>>> page.css('.price::text').re(r'[\d.]+')
['10.99', '20.99', '15.99']
>>> page.css('.product h3::text').re(r'Product (\d+)')
['1', '2', '3']
search puedes buscar entre los objetos Selector de la lista. Pasa una función que reciba un Selector como primer argumento y devuelva True o False. El método devuelve el primer Selector que cumpla la condición, o None si no hay ninguno.
>>> product = page.css('.product').search(lambda p: p.css('.price::text').get() == '$20.99')
>>> product.css('h3::text').get()
'Product 2'
filter. Recibe una función como search, pero devuelve un Selectors con todos los objetos Selector que cumplan la condición.
>>> products = page.css('.product').filter(lambda p: float(p.css('.price::text').re_first(r'[\d.]+')) > 15)
>>> products.css('h3::text').getall()
['Product 2', 'Product 3']
>>> page.css('.product').first # First Selector or None
<data='<article class="product" data-id="1"><h3...'>
>>> page.css('.product').last # Last Selector or None
<data='<article class="product" data-id="3"><h3...'>
>>> page.css('.nonexistent').first # Returns None instead of raising IndexError
Para saber cuántos objetos Selector hay en un Selectors, puedes hacer esto:
Es lo mismo que: Sí, como en JavaScript :)TextHandler¶
Esta clase es importante: los métodos y propiedades que devuelven una cadena usan TextHandler. Los que devuelven una lista de cadenas usan TextHandlers.
TextHandler hereda de la cadena de texto de Python, así que permite todas sus operaciones. Entonces, ¿por qué tiene otro nombre?
Porque añade métodos y propiedades que las cadenas normales no tienen. Como se usa para el texto en toda la biblioteca, puedes combinar esas funciones para escribir código más corto y claro. También puedes importarla y usarla con cualquier cadena, como veremos más adelante.
Uso¶
Las operaciones como tomar una parte, acceder por índice o llamar a split, replace y strip vuelven a devolver TextHandler. Así puedes encadenarlas. Si encuentras una que devuelva una cadena normal en lugar de TextHandler, abre una incidencia para que también la adaptemos.
Empecemos por re y re_first. Son los mismos métodos que en Selector, Selectors y TextHandlers, y aceptan los mismos argumentos.
-
rerecibe como primer argumento una expresión regular en forma de cadena o ya compilada. Busca todas las coincidencias y las devuelve en un TextHandlers.re_firstacepta los mismos argumentos, pero devuelve solo la primera coincidencia comoTextHandler.También admite estos argumentos:
- replace_entities: reemplaza las referencias a entidades por sus caracteres. Está activado por defecto.
- clean_match: ignora los espacios en blanco, incluidos los espacios seguidos, al comparar. Está desactivado por defecto.
- case_sensitive: distingue entre mayúsculas y minúsculas. Está activado por defecto. Si lo desactivas, la expresión regular se compila sin esa distinción.
Ya vimos estos ejemplos. Devuelven TextHandlers porque usan
re:Para mostrar los otros argumentos, usaremos una cadena distinta en cada ejemplo:>>> page.css('.price::text').re(r'[\d.]+') ['10.99', '20.99', '15.99'] >>> page.css('.product h3::text').re(r'Product (\d+)') ['1', '2', '3']Usar>>> from scrapling import TextHandler >>> test_string = TextHandler('hi there') # Hence the two spaces >>> test_string.re('hi there') >>> test_string.re('hi there', clean_match=True) # Using `clean_match` will clean the string before matching the regex ['hi there'] >>> test_string2 = TextHandler('Oh, Hi Mark') >>> test_string2.re_first('oh, hi Mark') >>> test_string2.re_first('oh, hi Mark', case_sensitive=False) # Hence disabling `case_sensitive` 'Oh, Hi Mark' # Mixing arguments >>> test_string.re('hi there', clean_match=True, case_sensitive=False) ['hi there']TextHandleren toda la biblioteca tiene otra ventaja: comohtml_contentdevuelveTextHandler, puedes aplicar expresiones regulares directamente al HTML: -
.json()intenta convertir el contenido a un objeto JSON de forma rápida. Si no es posible, produce un error:Si al seleccionar un elemento no indicas un nodo de texto o un atributo, se usa automáticamente su contenido de texto: La clase Selector añade otro detalle. Supongamos que trabajamos con esta página:>>> page.css('#page-data::text').get() '\n {\n "lastUpdated": "2024-09-22T10:30:00Z",\n "totalProducts": 3\n }\n ' >>> page.css('#page-data::text').get().json() {'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}Selector tiene el método<html> <body> <div> <script id="page-data" type="application/json"> { "lastUpdated": "2024-09-22T10:30:00Z", "totalProducts": 3 } </script> </div> </body> </html>get_all_text, que devuelve unTextHandler.
Si haces esto: Obtendrás un error porque eldivno tiene texto directo que se pueda convertir a JSON. De hecho, no tiene texto directo de ningún tipo.
En este caso puedes usarget_all_text:Usé>>> page.css('div')[0].get_all_text(ignore_tags=[]).json() {'lastUpdated': '2024-09-22T10:30:00Z', 'totalProducts': 3}ignore_tagsporque su valor por defecto es('script', 'style',).
Hay otro caso relacionado cuando usas una herramienta de descarga. Supongamos que recibes una respuesta JSON como esta: Como Selector está pensado para páginas HTML, trata ese contenido como HTML incompleto y lo corrige. Si usashtml_content, obtienes esto: Aquí puedes usarjsondirectamente: Quizá te preguntes cómo funciona sihtmlno tiene texto directo.
Para respuestas como JSON, Selector conserva una copia del contenido original. Al llamar a.json(), primero intenta convertir esa copia. Si no la tiene, como ocurre con los elementos internos, usa el texto directo del elemento. Si tampoco lo hay, llama aget_all_text.
-
También puedes pasar.clean()elimina los espacios en blanco y los espacios seguidos, y devuelve un nuevoTextHandler:remove_entitiespara quecleanreemplace las entidades HTML por sus caracteres. -
O en orden inverso:.sort()ordena los caracteres de una cadena, de forma parecida a como ordenas una lista:
Se añadirán otros métodos y propiedades con el tiempo. Recuerda que esta clase reemplaza a las cadenas normales en casi toda la biblioteca.
TextHandlers¶
Esta clase sigue una idea parecida a Selectors y Selector. Hereda las operaciones de las listas normales y añade re y re_first.
Aquí re_first ejecuta re sobre cada TextHandler y devuelve el primer resultado, o None. El resto funciona como ya vimos. Se añadirán más métodos con el tiempo.
AttributesHandler¶
Es una versión de solo lectura del diccionario dict de Python. Se usa para guardar los atributos de cada elemento u objeto Selector.
>>> print(page.find('script').attrib)
{'id': 'page-data', 'type': 'application/json'}
>>> type(page.find('script').attrib).__name__
'AttributesHandler'
Añade dos funciones sencillas:
-
El método
search_values:En un diccionario puedes usar
dict.get("key_name")para buscar una clave. Buscar por valores requiere más código. Este método lo hace por ti: busca entre los valores de los atributos y devuelve un diccionario por cada coincidencia.Un ejemplo sencillo:
También admitepartialpara buscar por una parte del valor:Un caso más habitual sería combinarlo con>>> for i in page.find('script').attrib.search_values('page', partial=True): print(i) {'id': 'page-data'}find_allpara buscar elementos que tengan un valor concreto en sus atributos:Todos estos elementos tienen 'product' como valor de>>> page.find_all(lambda element: list(element.attrib.search_values('product'))) [<data='<article class="product" data-id="1"><h3...' parent='<div class="product-list"> <article clas...'>, <data='<article class="product" data-id="2"><h3...' parent='<div class="product-list"> <article clas...'>, <data='<article class="product" data-id="3"><h3...' parent='<div class="product-list"> <article clas...'>]class.Uso
listporquesearch_valuesdevuelve un generador. Sin convertirlo a lista, la condición seríaTruepara todos los elementos. -
La propiedad
json_string:Convierte los atributos a una cadena JSON. Si no se pueden convertir, produce un error.