BeautifulSoup Grab Sichtbarer Webseitentext

124

Grundsätzlich möchte ich BeautifulSoup verwenden, um den sichtbaren Text auf einer Webseite genau zu erfassen. Zum Beispiel ist diese Webseite mein Testfall. Und ich möchte hauptsächlich nur den Text (Artikel) und vielleicht sogar ein paar Registerkartennamen hier und da bekommen. Ich habe den Vorschlag in dieser SO-Frage ausprobiert, der viele <script>Tags und HTML-Kommentare zurückgibt, die ich nicht möchte. Ich kann die Argumente nicht herausfinden, die ich für die Funktion benötige findAll(), um nur die sichtbaren Texte auf einer Webseite zu erhalten.

Wie soll ich also alle sichtbaren Texte außer Skripten, Kommentaren, CSS usw. finden?

user233864
quelle

Antworten:

239

Versuche dies:

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
quelle
47
+1 für soup.findAll(text=True)nie über diese Funktion gewusst
Hartley Brody
7
Für das aktuelle BS4 (zumindest) konnten Sie Kommentare identifizieren, isinstance(element, Comment)anstatt sie mit einem regulären Ausdruck abzugleichen.
Tripleee
5
Ich glaube, Zeile 2 sollte seinsoup = BeautifulSoup(html)
jczaplew
11
In der sichtbaren Funktion schien das Elif zum Auffinden von Kommentaren nicht zu funktionieren. Ich musste es aktualisieren elif isinstance(element,bs4.element.Comment):. Ich habe auch 'Meta' zur Liste der Eltern hinzugefügt.
Russ Savage
4
Der obige Filter hat eine Menge \ n im Ergebnis. elif re.match(r"[\s\r\n]+",str(element)): return False
Fügen Sie
37

Die genehmigte Antwort von @jbochi funktioniert bei mir nicht. Der Funktionsaufruf str () löst eine Ausnahme aus, da die Nicht-ASCII-Zeichen im BeautifulSoup-Element nicht codiert werden können. Hier ist eine prägnantere Möglichkeit, die Beispielwebseite nach sichtbarem Text zu filtern.

html = open('21storm.html').read()
soup = BeautifulSoup(html)
[s.extract() for s in soup(['style', 'script', '[document]', 'head', 'title'])]
visible_text = soup.getText()
nmgeek
quelle
1
Wenn str(element)Probleme mit der Codierung fehlschlagen, sollten Sie es unicode(element)stattdessen versuchen, wenn Sie Python 2 verwenden.
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'))
Bumpkin
quelle
4
Die vorherigen Antworten haben bei mir nicht funktioniert, aber das hat funktioniert :)
rjurney
Wenn ich dies auf der URL imfuna.com versuche, werden nur 6 Wörter zurückgegeben (Imfuna Property Inventory and Inspection Apps), obwohl die Seite viel mehr Text / Wörter enthält ... Ideen, warum diese Antwort dafür nicht funktioniert URL? @bumpkin
the_t_test_1
10

Ich respektiere es vollkommen, Beautiful Soup zu verwenden, um gerenderten Inhalt zu erhalten, aber es ist möglicherweise nicht das ideale Paket, um den gerenderten Inhalt auf einer Seite zu erfassen.

Ich hatte ein ähnliches Problem beim Abrufen gerenderter Inhalte oder der sichtbaren Inhalte in einem typischen Browser. Insbesondere hatte ich viele vielleicht atypische Fälle, um mit einem so einfachen Beispiel unten zu arbeiten. In diesem Fall ist das nicht anzeigbare Tag in einem Style-Tag verschachtelt und in vielen von mir überprüften Browsern nicht sichtbar. Es gibt andere Variationen, z. B. das Definieren einer Anzeige für die Einstellung von Klassen-Tags auf "Keine". Verwenden Sie dann diese Klasse für die 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>

Eine der oben genannten Lösungen ist:

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

Diese Lösung hat sicherlich in vielen Fällen Anwendungen und erledigt die Arbeit im Allgemeinen recht gut, aber im oben angegebenen HTML-Code wird der nicht gerenderte Text beibehalten. Nach der Suche nach SO wurden hier einige Lösungen gefunden. BeautifulSoup get_text entfernt nicht alle Tags und JavaScript und hier wird HTML mit Python in einfachen Text gerendert

Ich habe beide Lösungen ausprobiert: html2text und nltk.clean_html und war von den Timing-Ergebnissen überrascht, sodass ich dachte, dass sie eine Antwort für die Nachwelt rechtfertigen. Natürlich hängen die Geschwindigkeiten stark vom Inhalt der Daten ab ...

Eine Antwort von @Helge hier war, ausgerechnet nltk zu verwenden.

import nltk

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

Es hat sehr gut funktioniert, einen String mit gerendertem HTML zurückzugeben. Dieses nltk-Modul war schneller als sogar html2text, obwohl html2text möglicherweise robuster ist.

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

Wenn Sie Wert auf Leistung legen, finden Sie hier einen weiteren effizienteren Weg:

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.stringsist ein Iterator und wird zurückgegeben, NavigableStringsodass Sie den Tag-Namen des übergeordneten Elements direkt überprüfen können, ohne mehrere Schleifen durchlaufen zu müssen.

Polor Beer
quelle
2

Der Titel befindet sich in einem <nyt_headline>Tag, das in einem <h1>Tag und einem <div>Tag mit der ID "Artikel" verschachtelt ist .

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

Sollte arbeiten.

Der Artikelkörper befindet sich in einem <nyt_text>Tag, das in einem <div>Tag mit der ID "articleBody" verschachtelt ist . Innerhalb des <nyt_text> Elements ist der Text selbst in <p> Tags enthalten. Bilder befinden sich nicht in diesen <p>Tags. Es ist schwierig für mich, mit der Syntax zu experimentieren, aber ich erwarte, dass ein funktionierender Kratzer ungefähr so ​​aussieht.

text = soup.findAll('nyt_text', limit=1)[0]
text.findAll('p')
Ewan Todd
quelle
Ich bin mir jedoch sicher, dass dies für diesen Testfall funktioniert, da ich nach einer allgemeineren Antwort suche, die auf verschiedene andere Websites angewendet werden kann ... Bisher habe ich versucht, mithilfe von regulären Ausdrücken <script> </ script> -Tags und <zu finden ! -. * -> Kommentare und ersetzen Sie sie durch "", aber das erweist sich aus
Summengründen
2

Während ich generell empfehlen würde, schöne Suppe zu verwenden, wenn jemand die sichtbaren Teile eines fehlerhaften HTML-Codes (z. B. wenn Sie nur ein Segment oder eine Zeile einer Webseite haben) aus irgendeinem Grund anzeigen möchte, die folgenden entfernt Inhalte zwischen <und >Tags:

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

Verwenden Sie BeautifulSoup am einfachsten mit weniger Code, um nur die Zeichenfolgen zu erhalten, ohne leere Zeilen und Mist.

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

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

Der einfachste Weg, um diesen Fall zu behandeln, ist die Verwendung von getattr(). Sie können dieses Beispiel an Ihre Bedürfnisse anpassen:

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)

Dadurch wird das Textelement "3.7"innerhalb des Tag-Objekts gefunden, <span class="ratingsContent">3.7</span>wenn es vorhanden ist. Standardmäßig wird NoneTypees jedoch verwendet, wenn dies nicht der Fall ist.

getattr(object, name[, default])

Gibt den Wert des benannten Attributs des Objekts zurück. Name muss eine Zeichenfolge sein. Wenn die Zeichenfolge der Name eines der Attribute des Objekts ist, ist das Ergebnis der Wert dieses Attributs. Zum Beispiel entspricht getattr (x, 'foobar') x.foobar. Wenn das benannte Attribut nicht vorhanden ist, wird der Standardwert zurückgegeben, falls angegeben, andernfalls wird AttributeError ausgelöst.

David Yerrington
quelle
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
quelle