2009-12-20 23 views
86

Básicamente, quiero utilizar BeautifulSoup para captar estrictamente el texto visible en una página web. Por ejemplo, this webpage 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 este SO question que devuelve muchas etiquetas <script> 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.BeautifulSoup Grab Visible página de texto

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

Respuesta

142

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

@jbochi He reemplazado la línea 3 de visible() con re.match ('. * . *', cadena, re.DOTALL). El suyo parece funcionar solo si el contenido * completo * del texto es un comentario, pero si hay un espacio inicial o una línea nueva, se devolverá el html 'invisible'. Mi solución es demasiado agresiva, ya que marcará todo el elemento como invisible, pero para mis propósitos eso está bien. – Trindaz

+25

+1 para 'soup.findAll (text = True)' nunca se supo acerca de esa característica –

+6

Para BS4 reciente (al menos) puede identificar los comentarios con 'isinstance (element, Comment)' en lugar de coincidir con una expresión regular. – tripleee

1

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

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

Debería funcionar.

El cuerpo del artículo está dentro de una etiqueta <nyt_text>, que está anidada dentro de una etiqueta <div> con id "articleBody". Dentro del elemento <nyt_text>, el texto en sí está dentro de las etiquetas <p>. Las imágenes no están dentro de esas etiquetas <p>. Es difícil para mí experimentar con la sintaxis, pero espero que un rasguño funcione de esta manera.

text = soup.findAll('nyt_text', limit=1)[0] 
text.findAll('p') 
+0

Estoy seguro de que esto funciona para este caso de prueba, sin embargo, buscando una respuesta más genérica que pueda aplicarse a otros sitios web ... Hasta ahora, he intentado usar expresiones regulares para encontrar etiquetas y comentarios y reemplázalos con "", pero eso incluso es un poco difícil por razones de suma. – user233864

25

La respuesta aprobada de @jbochi no funciona para mí. 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 manera más sucinta de filtrar la página web de ejemplo al 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() 
+1

Si 'str (elemento)' falla con problemas de codificación, debe intentar 'unicode (elemento)' en su lugar si está usando Python 2. – mknaf

8

estoy totalmente de respetar el uso de sopa Hermosa para obtener contenido representado, pero puede que no sea el paquete ideal para adquirir el contenido representado en una página.

Tuve un problema similar al contenido renderizado o al 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 pantalla 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 publicado 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, sin duda tiene aplicaciones en muchos casos y hace el trabajo bastante bien en general, pero en el html publicado por encima de ella conserva el texto que no se representa. Después de buscar por lo que un par de soluciones vinieron aquí y aquí BeautifulSoup get_text does not strip all tags and JavaScriptRendered HTML to plain text using Python

Probé estas dos soluciones: html2text y nltk.clean_html y fue sorprendido por los resultados de distribución para pensaron que justifica 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 que incluso html2text, aunque quizás html2text sea más robusto.

betterHTML = html.decode(errors='ignore') 
%timeit html2text.html2text(betterHTML) 
%3.09 ms per loop 
21
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')) 
+3

Las respuestas anteriores no funcionaron para mí, pero esto sí :) – rjurney

+0

Si intento 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 esa url? @patán –

1

Mientras, sugeriría completamente usando bella-sopa en general, si alguien está buscando para mostrar las partes visibles de un HTML incorrecto (por ejemplo, donde hay sólo un segmento o una línea de web- página) por cualquier-razón, el siguiente eliminará 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)) 
2

Usando BeautifulSoup la forma más fácil con menos código para obtener sólo las cuerdas, 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) 
0

Si se preocupan por el rendimiento, aquí es 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 vuelve NavigableString de modo que usted puede comprobar la etiqueta de los padres directamente, sin pasar por múltiples bucles.

Cuestiones relacionadas