BeautifulSoup Grab Widoczny tekst strony internetowej

125

Zasadniczo chcę używać BeautifulSoup do ścisłego przechwytywania widocznego tekstu na stronie internetowej. Na przykład ta strona internetowa jest moim przypadkiem testowym. I przede wszystkim chcę uzyskać tekst podstawowy (artykuł) i może nawet kilka nazw kart tu i tam. Wypróbowałem sugestię w tym pytaniu SO, która zwraca wiele <script>tagów i komentarzy html, których nie chcę. Nie mogę znaleźć argumentów potrzebnych do funkcji findAll(), aby uzyskać tylko widoczne teksty na stronie internetowej.

Jak więc mam znaleźć cały widoczny tekst z wyjątkiem skryptów, komentarzy, css itp.?

user233864
źródło

Odpowiedzi:

239

Spróbuj tego:

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
źródło
47
+1 za to, soup.findAll(text=True)że nigdy nie wiedziałem o tej funkcji
Hartley Brody
7
W przypadku ostatniego BS4 (przynajmniej) można było identyfikować komentarze z, isinstance(element, Comment)zamiast dopasowywać je do wyrażenia regularnego.
tripleee
5
Uważam, że linia 2 powinna byćsoup = BeautifulSoup(html)
jczaplew
11
W widocznej funkcji elif do wyszukiwania komentarzy nie działał. musiałem go zaktualizować do elif isinstance(element,bs4.element.Comment):. Dodałem też „meta” do listy rodziców.
Russ Savage
4
Powyższy filtr zawiera dużo \ nw wyniku, dodaj następujący kod, aby wyeliminować spacje i nowe wiersze: elif re.match(r"[\s\r\n]+",str(element)): return False
天才 小飞 猫
37

Zatwierdzona odpowiedź od @jbochi nie działa dla mnie. Wywołanie funkcji str () zgłasza wyjątek, ponieważ nie może zakodować znaków innych niż ASCII w elemencie BeautifulSoup. Oto bardziej zwięzły sposób filtrowania przykładowej strony internetowej do widocznego tekstu.

html = open('21storm.html').read()
soup = BeautifulSoup(html)
[s.extract() for s in soup(['style', 'script', '[document]', 'head', 'title'])]
visible_text = soup.getText()
nmgeek
źródło
1
Jeśli str(element)nie uda się rozwiązać problemów z kodowaniem, spróbuj unicode(element)zamiast tego, jeśli używasz Pythona 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'))
gamoń
źródło
4
Poprzednie odpowiedzi nie działały dla mnie, ale tak było :)
rjurney
Jeśli spróbuję tego na adresie url imfuna.com, zwraca tylko 6 słów (Imfuna Property Inventory and Inspection Apps) pomimo faktu, że na stronie jest znacznie więcej tekstu / słów ... wszelkie pomysły, dlaczego ta odpowiedź nie działa w tym przypadku url? @bumpkin
the_t_test_1
10

Całkowicie szanuję używanie Beautiful Soup do renderowania treści, ale może to nie być idealny pakiet do pozyskiwania renderowanej treści na stronie.

Miałem podobny problem, aby uzyskać renderowaną treść lub widoczną zawartość w typowej przeglądarce. W szczególności miałem wiele być może nietypowych przypadków do pracy z tak prostym przykładem poniżej. W tym przypadku niewyświetlalny znacznik jest zagnieżdżony w znaczniku stylu i nie jest widoczny w wielu przeglądarkach, które sprawdziłem. Istnieją inne odmiany, takie jak zdefiniowanie ustawienia wyświetlania znacznika klasy na none. Następnie używając tej klasy dla 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>

Jednym z opublikowanych powyżej rozwiązań jest:

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

To rozwiązanie z pewnością ma aplikacje w wielu przypadkach i generalnie działa całkiem dobrze, ale w zamieszczonym powyżej html zachowuje tekst, który nie jest renderowany. Po przeszukaniu tak pojawiło się kilka rozwiązań BeautifulSoup get_text nie usuwa wszystkich tagów i JavaScript, a tutaj Rendered HTML na zwykły tekst za pomocą Pythona

Wypróbowałem oba te rozwiązania: html2text i nltk.clean_html i byłem zaskoczony wynikami synchronizacji, więc pomyślałem, że uzasadniają odpowiedź dla potomności. Oczywiście prędkości w dużym stopniu zależą od zawartości danych ...

Jedna odpowiedź od @Helge dotyczyła używania nltk wszystkich rzeczy.

import nltk

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

Zwrócenie ciągu znaków z wyrenderowanym kodem HTML działało naprawdę dobrze. Ten moduł nltk był szybszy niż nawet html2text, chociaż być może html2text jest bardziej niezawodny.

betterHTML = html.decode(errors='ignore')
%timeit html2text.html2text(betterHTML)
%3.09 ms per loop
Paweł
źródło
3

Jeśli zależy Ci na wydajności, oto inny bardziej wydajny sposób:

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.stringsjest iteratorem i zwraca, NavigableStringdzięki czemu można bezpośrednio sprawdzić nazwę znacznika rodzica, bez przechodzenia przez wiele pętli.

Piwo Polor
źródło
2

Tytuł znajduje się wewnątrz <nyt_headline>tagu, który jest zagnieżdżony w <h1>tagu i <div>tagu o identyfikatorze „artykuł”.

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

Powinno działać.

Treść artykułu znajduje się wewnątrz <nyt_text>tagu, który jest zagnieżdżony w <div>tagu o identyfikatorze „articleBody”. Wewnątrz <nyt_text> elementu sam tekst jest zawarty w <p> tagach. Obrazy nie znajdują się w tych <p>tagach. Trudno mi eksperymentować ze składnią, ale spodziewam się, że działająca skrawka będzie wyglądać mniej więcej tak.

text = soup.findAll('nyt_text', limit=1)[0]
text.findAll('p')
Ewan Todd
źródło
Jestem pewien, że to działa w tym przypadku testowym, jednak szukam bardziej ogólnej odpowiedzi, którą można zastosować na różnych innych stronach internetowych ... Do tej pory próbowałem użyć wyrażeń regularnych, aby znaleźć tagi <script> </script> i < ! -. * -> komentuje i zamień je na "", ale to nawet okazuje się trudne z sumarycznego powodu ..
user233864
2

Chociaż całkowicie sugerowałbym używanie beautiful-soup w ogóle, jeśli ktoś chce wyświetlić widoczne części źle sformułowanego html (np. Tam, gdzie masz tylko segment lub linię strony internetowej) z jakiegokolwiek powodu, następujące usunie zawartość między tagami <i >:

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

Korzystanie z BeautifulSoup jest najłatwiejszym sposobem z mniejszą ilością kodu, aby po prostu uzyskać ciągi znaków, bez pustych linii i bzdur.

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

for i in soup.stripped_strings:
    print repr(i)
Diego Suarez
źródło
0

Najprostszym sposobem rozwiązania tego przypadku jest użycie getattr(). Możesz dostosować ten przykład do swoich potrzeb:

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)

Spowoduje to znalezienie elementu tekstowego "3.7"w obiekcie znacznika, <span class="ratingsContent">3.7</span>jeśli on istnieje, jednak domyślnie, NoneTypejeśli go nie ma.

getattr(object, name[, default])

Zwraca wartość nazwanego atrybutu obiektu. nazwa musi być ciągiem. Jeśli ciąg jest nazwą jednego z atrybutów obiektu, wynikiem jest wartość tego atrybutu. Na przykład getattr (x, 'foobar') jest równoważne z x.foobar. Jeśli nazwany atrybut nie istnieje, zwracany jest domyślny, jeśli został podany, w przeciwnym razie zostanie zgłoszony AttributeError.

David Yerrington
źródło
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
źródło