BeautifulSoup Grab Visible Webpage Text

124

Básicamente, quiero usar BeautifulSoup para tomar 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á. He intentado la sugerencia en esta pregunta SO que devuelve muchas <script>etiquetas y comentarios html que no quiero. No puedo entender los argumentos que necesito para la función findAll()con el fin de obtener los textos visibles en una página web.

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

usuario233864
fuente

Respuestas:

239

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))
jbochi
fuente
47
+1 para soup.findAll(text=True)nunca supo de esa característica
Hartley Brody
77
Para BS4 reciente (al menos) podría identificar comentarios en isinstance(element, Comment)lugar de coincidir con una expresión regular.
tripleee
55
Creo que la línea 2 debería sersoup = BeautifulSoup(html)
jczaplew
11
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 Savage
44
El filtro anterior tiene una gran cantidad de \ n en el resultado, agregue el siguiente código para eliminar espacios en blanco y nuevas líneas: elif re.match(r"[\s\r\n]+",str(element)): return False
小飞 猫
37

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()
nmgeek
fuente
1
Si str(element)falla con los problemas de codificación, debería intentarlo unicode(element)si está usando Python 2.
mknaf
31
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'))
patán
fuente
44
Las respuestas anteriores no me funcionaron, pero esto sí :)
rjurney
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? @bumpkin
the_t_test_1
10

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 verificado. Existen otras variaciones, como definir una visualización de configuración de etiqueta de clase en none. 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 arriba retiene el texto que no se muestra. Después de buscar SO, surgieron un par de soluciones BeautifulSoup get_text no elimina todas las etiquetas y JavaScript y aquí se procesó HTML en texto sin formato usando Python

Probé ambas soluciones: html2text y nltk.clean_html y me sorprendieron los resultados de tiempo, así que pensé que justificaban 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 usar 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 procesado. Este módulo nltk fue más rápido que incluso html2text, aunque quizás html2text es más robusto.

betterHTML = html.decode(errors='ignore')
%timeit html2text.html2text(betterHTML)
%3.09 ms per loop
Pablo
fuente
3

Si te importa 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.stringses un iterador y regresa NavigableStringpara que pueda verificar el nombre de la etiqueta del padre directamente, sin pasar por varios bucles.

Cerveza cerveza
fuente
2

El título está dentro de una <nyt_headline>etiqueta, que está anidada dentro de una <h1>etiqueta y una <div>etiqueta con id "artículo".

soup.findAll('nyt_headline', limit=1)

Deberia trabajar.

El cuerpo del artículo está dentro de una <nyt_text>etiqueta, que está anidada dentro de una <div>etiqueta con el ID "articleBody". Dentro del <nyt_text> elemento, el texto en sí está contenido dentro de las <p> etiquetas. Las imágenes no están dentro de esas <p>etiquetas. Es difícil para mí experimentar con la sintaxis, pero espero que un raspado funcional se vea así.

text = soup.findAll('nyt_text', limit=1)[0]
text.findAll('p')
Ewan Todd
fuente
Sin embargo, estoy seguro de que esto funciona para este caso de prueba, buscando una respuesta más genérica que pueda aplicarse a varios otros sitios web ... Hasta ahora, he intentado usar expresiones regulares para encontrar etiquetas <script> </script> y < ! -. * -> comentarios y reemplazarlos con "" pero eso es incluso un poco difícil para probar suma razón ..
user233864
2

Si bien, sugeriría completamente usar una sopa hermosa en general, si alguien está buscando mostrar las partes visibles de un html con formato incorrecto (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))
Kyrenia
fuente
2

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

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

for i in soup.stripped_strings:
    print repr(i)
Diego Suárez
fuente
0

La forma más sencilla de manejar este caso es mediante el uso getattr(). Puede adaptar este ejemplo a sus necesidades:

from bs4 import BeautifulSoup

source_html = """
<span class="ratingsDisplay">
    <a class="ratingNumber" href="https://www.youtube.com/watch?v=oHg5SJYRHA0" target="_blank" rel="noopener">
        <span class="ratingsContent">3.7</span>
    </a>
</span>
"""

soup = BeautifulSoup(source_html, "lxml")
my_ratings = getattr(soup.find('span', {"class": "ratingsContent"}), "text", None)
print(my_ratings)

Esto encontrará el elemento de texto "3.7", dentro del objeto de etiqueta <span class="ratingsContent">3.7</span>cuando existe, sin embargo, predeterminado NoneTypecuando no existe.

getattr(object, name[, default])

Devuelve el valor del atributo nombrado del objeto. El nombre debe ser una cadena. Si la cadena es el nombre de uno de los atributos del objeto, el resultado es el valor de ese atributo. Por ejemplo, getattr (x, 'foobar') es equivalente a x.foobar. Si el atributo nombrado no existe, se devuelve el valor predeterminado si se proporciona; de lo contrario, se genera AttributeError.

David Yerrington
fuente
0
from bs4 import BeautifulSoup
from bs4.element import Comment
import urllib.request
import re
import ssl

def tag_visible(element):
    if element.parent.name in ['style', 'script', 'head', 'title', 'meta', '[document]']:
        return False
    if isinstance(element, Comment):
        return False
    if re.match(r"[\n]+",str(element)): return False
    return True
def text_from_html(url):
    body = urllib.request.urlopen(url,context=ssl._create_unverified_context()).read()
    soup = BeautifulSoup(body ,"lxml")
    texts = soup.findAll(text=True)
    visible_texts = filter(tag_visible, texts)  
    text = u",".join(t.strip() for t in visible_texts)
    text = text.lstrip().rstrip()
    text = text.split(',')
    clean_text = ''
    for sen in text:
        if sen:
            sen = sen.rstrip().lstrip()
            clean_text += sen+','
    return clean_text
url = 'http://www.nytimes.com/2009/12/21/us/21storm.html'
print(text_from_html(url))
kamran kausar
fuente