¿Cómo raspar solo el texto visible de la página web con BeautifulSoup?

7 minutos de lectura

avatar de usuario de user233864
usuario233864

Básicamente, quiero usar BeautifulSoup agarrar estrictamente el texto visible en una página web. Por ejemplo, esta página web es mi caso de prueba. Y principalmente quiero obtener el texto del cuerpo (artículo) y tal vez incluso algunos nombres de pestañas aquí y allá. Probé la sugerencia en esta pregunta SO que devuelve muchos <script> etiquetas y comentarios html que no quiero. No puedo descifrar los argumentos que necesito para la función. findAll() con el fin de obtener sólo los textos visibles en una página web.

Entonces, ¿cómo debo encontrar todo el texto visible excluyendo scripts, comentarios, css, etc.?

  • Hay una actualización para más nuevos BeautifulSoup Versiones 4.9.+ stackoverflow.com/a/73701993/14460824

    – Erizo

    13 de septiembre a las 11:48

avatar de usuario de jbochi
jbochi

Prueba esto:

from bs4 import BeautifulSoup
from bs4.element import Comment
import urllib.request


def tag_visible(element):
    if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
        return False
    if isinstance(element, Comment):
        return False
    return True


def text_from_html(body):
    soup = BeautifulSoup(body, 'html.parser')
    texts = soup.findAll(text=True)
    visible_texts = filter(tag_visible, texts)  
    return u" ".join(t.strip() for t in visible_texts)

html = urllib.request.urlopen('http://www.nytimes.com/2009/12/21/us/21storm.html').read()
print(text_from_html(html))

  • +1 para soup.findAll(text=True) nunca supe de esa característica

    – Hartley Brody

    12 de junio de 2012 a las 17:19

  • Para BS4 reciente (al menos) podría identificar comentarios con isinstance(element, Comment) en lugar de hacer coincidir con una expresión regular.

    – triplete

    2 de octubre de 2013 a las 12:48

  • Creo que la línea 2 debería ser soup = BeautifulSoup(html)

    – jczaplew

    3 de abril de 2014 a las 4:43


  • En la función visible, el elif para encontrar comentarios no parecía funcionar. Tuve que actualizarlo a elif isinstance(element,bs4.element.Comment):. También agregué ‘meta’ a la lista de padres.

    -Russ salvaje

    08/04/2015 a las 19:04


  • El filtro anterior tiene mucho \n en el resultado, agregue el siguiente código para eliminar los espacios en blanco y las líneas nuevas: elif re.match(r"[\s\r\n]+",str(element)): return False

    – 夜一林风

    4 de diciembre de 2015 a las 10:07


La respuesta aprobada de @jbochi no me funciona. La llamada a la función str() genera una excepción porque no puede codificar los caracteres que no son ascii en el elemento BeautifulSoup. Aquí hay una forma más sucinta de filtrar la página web de ejemplo a texto visible.

html = open('21storm.html').read()
soup = BeautifulSoup(html)
[s.extract() for s in soup(['style', 'script', '[document]', 'head', 'title'])]
visible_text = soup.getText()

  • Si str(element) falla con problemas de codificación, deberías probar unicode(element) en cambio, si está utilizando Python 2.

    – mknaf

    13 de febrero de 2016 a las 15:48

avatar de usuario de bumpkin
patán

import urllib
from bs4 import BeautifulSoup

url = "https://www.yahoo.com"
html = urllib.urlopen(url).read()
soup = BeautifulSoup(html)

# kill all script and style elements
for script in soup(["script", "style"]):
    script.extract()    # rip it out

# get text
text = soup.get_text()

# break into lines and remove leading and trailing space on each
lines = (line.strip() for line in text.splitlines())
# break multi-headlines into a line each
chunks = (phrase.strip() for line in lines for phrase in line.split("  "))
# drop blank lines
text="\n".join(chunk for chunk in chunks if chunk)

print(text.encode('utf-8'))

  • Las respuestas anteriores no me funcionaron, pero esta sí 🙂

    – rjurney

    5 de marzo de 2016 a las 0:59

  • Si pruebo esto en la url imfuna.com, solo devuelve 6 palabras (Imfuna Property Inventory and Inspection Apps) a pesar de que hay mucho más texto/palabras en la página… alguna idea de por qué esta respuesta no funciona para eso. URL? @patán

    – el_t_test_1

    22 de junio de 2017 a las 14:57


  • hay alguna manera de reemplazar <br> etiquetas con \n ¿caracteres de nueva línea?

    – 12 rombos en cuadrícula sin esquinas

    13 de agosto de 2020 a las 19:46

Avatar de usuario de Paul
Pablo

Respeto completamente el uso de Beautiful Soup para obtener contenido renderizado, pero puede que no sea el paquete ideal para adquirir el contenido renderizado en una página.

Tuve un problema similar para obtener contenido renderizado o el contenido visible en un navegador típico. En particular, tuve muchos casos quizás atípicos para trabajar con un ejemplo tan simple a continuación. En este caso, la etiqueta no visualizable está anidada en una etiqueta de estilo y no está visible en muchos navegadores que he comprobado. Existen otras variaciones, como definir una visualización de configuración de etiqueta de clase en ninguno. Luego, usando esta clase para el div.

<html>
  <title>  Title here</title>

  <body>

    lots of text here <p> <br>
    <h1> even headings </h1>

    <style type="text/css"> 
        <div > this will not be visible </div> 
    </style>


  </body>

</html>

Una solución publicada anteriormente es:

html = Utilities.ReadFile('simple.html')
soup = BeautifulSoup.BeautifulSoup(html)
texts = soup.findAll(text=True)
visible_texts = filter(visible, texts)
print(visible_texts)


[u'\n', u'\n', u'\n\n        lots of text here ', u' ', u'\n', u' even headings ', u'\n', u' this will not be visible ', u'\n', u'\n']

Esta solución ciertamente tiene aplicaciones en muchos casos y hace el trabajo bastante bien en general, pero en el html publicado anteriormente conserva el texto que no se muestra. Después de buscar SO, aparecieron un par de soluciones aquí BeautifulSoup get_text no elimina todas las etiquetas y JavaScript y aquí renderizó HTML a texto sin formato usando Python

Probé ambas soluciones: html2text y nltk.clean_html y me sorprendieron los resultados del tiempo, así que pensé que garantizaban una respuesta para la posteridad. Por supuesto, las velocidades dependen en gran medida del contenido de los datos…

Una respuesta aquí de @Helge fue sobre el uso de nltk de todas las cosas.

import nltk

%timeit nltk.clean_html(html)
was returning 153 us per loop

Funcionó muy bien para devolver una cadena con html renderizado. Este módulo nltk fue más rápido incluso que html2text, aunque quizás html2text sea más robusto.

betterHTML = html.decode(errors="ignore")
%timeit html2text.html2text(betterHTML)
%3.09 ms per loop

Avatar de usuario de Diego Suárez
diego suarez

Usando BeautifulSoup de la manera más fácil con menos código para obtener solo las cadenas, sin líneas vacías ni basura.

tag = <Parent_Tag_that_contains_the_data>
soup = BeautifulSoup(tag, 'html.parser')

for i in soup.stripped_strings:
    print repr(i)

  • De lejos, la mejor y más directa respuesta, ¡gracias!

    – Zhink

    4 sep 2021 a las 19:16

  • Tenga en cuenta, sin embargo, que stripped_strings incluirá el título de la página que no se muestra en la página.

    – Mermaldad

    20 de noviembre de 2021 a las 15:10

  • Leyendo más, veo que la solución de cerveza @polor usa stripped_strings pero corrige el título de la página.

    – Mermaldad

    20 de noviembre de 2021 a las 15:36

Avatar de usuario de Polor Beer
Cerveza Polo

Si te preocupa el rendimiento, aquí hay otra forma más eficiente:

import re

INVISIBLE_ELEMS = ('style', 'script', 'head', 'title')
RE_SPACES = re.compile(r'\s{3,}')

def visible_texts(soup):
    """ get visible text from a document """
    text=" ".join([
        s for s in soup.strings
        if s.parent.name not in INVISIBLE_ELEMS
    ])
    # collapse multiple spaces to two spaces.
    return RE_SPACES.sub('  ', text)

soup.strings es un iterador, y devuelve NavigableString para que pueda verificar el nombre de la etiqueta principal directamente, sin pasar por múltiples bucles.

  • De lejos, la mejor y más directa respuesta, ¡gracias!

    – Zhink

    4 sep 2021 a las 19:16

  • Tenga en cuenta, sin embargo, que stripped_strings incluirá el título de la página que no se muestra en la página.

    – Mermaldad

    20 de noviembre de 2021 a las 15:10

  • Leyendo más, veo que la solución de cerveza @polor usa stripped_strings pero corrige el título de la página.

    – Mermaldad

    20 de noviembre de 2021 a las 15:36

avatar de usuario de kyrenia
Kirenia

Si bien, sugeriría completamente usar beautiful-soup en general, si alguien está buscando mostrar las partes visibles de un html mal formado (por ejemplo, donde solo tiene un segmento o línea de una página web) por cualquier razón, lo siguiente eliminará el contenido entre < y > etiquetas:

import re   ## only use with malformed html - this is not efficient
def display_visible_html_using_re(text):             
    return(re.sub("(\<.*?\>)", "",text))

¿Ha sido útil esta solución?