2009-12-20 17 views
86

Fondamentalmente, voglio utilizzare BeautifulSoup per afferrare rigorosamente il testo visibile su una pagina web. Ad esempio, this webpage è il mio caso di prova. E voglio principalmente ottenere il testo del corpo (articolo) e magari anche qualche nome di tabulazione qua e là. Ho provato il suggerimento in questo SO question che restituisce molti tag <script> e commenti HTML che non desidero. Non riesco a capire gli argomenti che mi servono per la funzione findAll() al fine di ottenere solo i testi visibili su una pagina web.BeautifulSoup Grab Visible Webpage Text

Quindi, come dovrei trovare tutto il testo visibile escludendo script, commenti, css ecc.?

risposta

142

Prova questo:

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 Ho sostituito la riga 3 di visible() con re.match ('. * . * ', string, re.DOTALL). Il tuo sembra funzionare solo se il * intero * contenuto del testo è un commento, ma se c'è uno spazio iniziale o una nuova riga allora verrà restituito l'html 'invisibile'. La mia soluzione è eccessivamente aggressiva in quanto segnerà l'intero elemento come invisibile, ma per i miei scopi va benissimo. – Trindaz

+25

+1 per 'soup.findAll (text = True)' non sapeva mai di quella funzione –

+6

Per BS4 recente (almeno) è possibile identificare i commenti con 'isinstance (element, Comment)' invece di corrispondere con una regex. – tripleee

1

Il titolo si trova all'interno di un tag <nyt_headline>, che è annidato all'interno di un tag <h1> e di un tag <div> con ID "article".

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

Dovrebbe funzionare.

Il corpo dell'articolo si trova all'interno di un tag <nyt_text>, che è nidificato all'interno di un tag <div> con ID "articleBody". All'interno dell'elemento <nyt_text>, il testo stesso è contenuto nei tag <p>. Le immagini non sono comprese tra i tag <p>. Per me è difficile sperimentare la sintassi, ma mi aspetto che un rasco funzionante assomigli a qualcosa del genere.

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

Sono sicuro che questo funziona per questo test, tuttavia, cercando una risposta più generica che possa essere applicata a vari altri siti Web ... Finora, ho provato a usare espressioni regolari per trovare i tag e i commenti e sostituirli con "", ma ciò si sta dimostrando un po 'difficile per un motivo di somma .. – user233864

25

La risposta approvato dal @jbochi non funziona per me. La chiamata alla funzione str() solleva un'eccezione perché non può codificare i caratteri non-ascii nell'elemento BeautifulSoup. Ecco un modo più succinto di filtrare la pagina Web di esempio in testo visibile.

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

Se 'str (element)' fallisce con problemi di codifica, dovresti provare 'unicode (element)' invece se stai usando Python 2. – mknaf

8

ho completamente rispettare utilizzando Beautiful Soup per ottenere contenuti reso, ma non può essere il pacchetto ideale per acquisire il contenuto visualizzato su una pagina.

Ho avuto un problema simile per ottenere il rendering del contenuto o il contenuto visibile in un browser tipico. In particolare, ho avuto molti casi atipici per lavorare con un esempio così semplice di seguito. In questo caso il tag non visualizzabile è nidificato in un tag di stile e non è visibile in molti browser che ho selezionato. Esistono altre varianti come la definizione di un'impostazione di un'etichetta di classe su none. Quindi usando questa classe per il 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 soluzione postato sopra è:

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'] 

Questa soluzione ha certamente applicazioni in molti casi, e fa il lavoro abbastanza bene in generale ma in html postato sopra mantiene il testo che non viene eseguito il rendering. Dopo la ricerca quindi un paio di soluzioni venuto qui BeautifulSoup get_text does not strip all tags and JavaScript e qui Rendered HTML to plain text using Python

ho provato entrambe le soluzioni: html2text e nltk.clean_html e sono rimasto sorpreso dai risultati di temporizzazione così pensavano garantiti una risposta per i posteri. Ovviamente le velocità dipendono molto dal contenuto dei dati ...

Una risposta qui da @Helge riguardava l'utilizzo di nltk di tutte le cose.

import nltk 

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

Ha funzionato molto bene per restituire una stringa con il rendering html. Questo modulo nltk era più veloce persino di html2text, anche se forse html2text è più 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

Le risposte precedenti non hanno funzionato per me, ma questo ha fatto :) – rjurney

+0

Se provo questo sull'URL imfuna.com restituisce solo 6 parole (Imfuna Property Inventory e Inspection Apps) nonostante ci sia molto più testo/parole sulla pagina ... qualche idea per cui questa risposta non funziona per quell'URL? @bumpkin –

1

po ', mi sarebbe completamente suggerirei di usare bella zuppa, in generale, se qualcuno sta cercando di visualizzare le parti visibili di un html non valido (ad esempio, se si hanno solo un segmento o una linea di un web- pagina) per qualsiasi ragione-, il seguito rimuoverà il contenuto tra < e > tag:

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

Utilizzando BeautifulSoup il modo più semplice con meno codice per ottenere solo le corde, senza linee vuote e merda.

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

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

Se vi preoccupate per le prestazioni, Ecco un altro modo più efficiente:

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 è un iteratore, e restituisce NavigableString in modo da poter verificare direttamente nome del tag del genitore, senza passare attraverso multipla loop.