BeautifulSoup Grab Texte de page Web visible

124

Fondamentalement, je souhaite utiliser BeautifulSoup pour saisir strictement le texte visible sur une page Web. Par exemple, cette page Web est mon cas de test. Et je veux principalement obtenir le corps du texte (article) et peut-être même quelques noms d'onglets ici et là. J'ai essayé la suggestion dans cette question SO qui renvoie beaucoup de <script>balises et de commentaires html dont je ne veux pas. Je ne peux pas comprendre les arguments dont j'ai besoin pour la fonction findAll()afin d'obtenir simplement les textes visibles sur une page Web.

Alors, comment trouver tout le texte visible à l'exception des scripts, des commentaires, des css, etc.?

user233864
la source

Réponses:

239

Essaye ça:

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
la source
47
+1 pour soup.findAll(text=True)ne jamais connaître cette fonctionnalité
Hartley Brody
7
Pour BS4 récent (au moins), vous pouvez identifier les commentaires avec isinstance(element, Comment)au lieu de les faire correspondre avec une expression régulière.
tripleee
5
Je crois que la ligne 2 devrait êtresoup = BeautifulSoup(html)
jczaplew
11
Dans la fonction visible, elif pour trouver des commentaires ne semblait pas fonctionner. je devais le mettre à jour elif isinstance(element,bs4.element.Comment):. J'ai également ajouté «meta» à la liste des parents.
Russ Savage
4
Le filtre ci-dessus a beaucoup de \ n dans le résultat, ajoutez le code suivant pour éliminer les espaces blancs et les nouvelles lignes: elif re.match(r"[\s\r\n]+",str(element)): return False
天才 小飞 猫
37

La réponse approuvée de @jbochi ne fonctionne pas pour moi. L'appel de la fonction str () lève une exception car il ne peut pas encoder les caractères non-ascii dans l'élément BeautifulSoup. Voici une manière plus succincte de filtrer l'exemple de page Web en texte 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
la source
1
En cas d' str(element)échec avec des problèmes d'encodage, vous devriez essayer à la unicode(element)place si vous utilisez 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'))
plouc
la source
4
Les réponses précédentes n'ont pas fonctionné pour moi, mais cela a fonctionné :)
rjurney
Si j'essaie cela sur l'url imfuna.com, il ne renvoie que 6 mots (Imfuna Property Inventory and Inspection Apps) malgré le fait qu'il y ait beaucoup plus de texte / mots sur la page ... des idées pour lesquelles cette réponse ne fonctionne pas pour cela url? @bumpkin
the_t_test_1
10

Je respecte complètement l'utilisation de Beautiful Soup pour obtenir du contenu rendu, mais ce n'est peut-être pas le package idéal pour acquérir le contenu rendu sur une page.

J'ai eu un problème similaire pour obtenir le contenu rendu ou le contenu visible dans un navigateur typique. En particulier, j'ai eu de nombreux cas peut-être atypiques à travailler avec un exemple aussi simple ci-dessous. Dans ce cas, la balise non affichable est imbriquée dans une balise de style et n'est pas visible dans de nombreux navigateurs que j'ai vérifiés. D'autres variantes existent, telles que la définition d'un affichage de paramètre de balise de classe sur aucun. Ensuite, en utilisant cette classe pour le 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>

Une solution publiée ci-dessus est:

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

Cette solution a certainement des applications dans de nombreux cas et fait le travail assez bien en général, mais dans le html affiché ci-dessus, elle conserve le texte qui n'est pas rendu. Après avoir recherché, quelques solutions sont apparues ici, BeautifulSoup get_text ne supprime pas toutes les balises et JavaScript et a rendu le HTML en texte brut en utilisant Python

J'ai essayé ces deux solutions: html2text et nltk.clean_html et j'ai été surpris par les résultats de synchronisation alors j'ai pensé qu'ils justifiaient une réponse pour la postérité. Bien sûr, les vitesses dépendent fortement du contenu des données ...

Une réponse ici de @Helge concernait l'utilisation de nltk de toutes choses.

import nltk

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

Cela fonctionnait vraiment bien pour renvoyer une chaîne avec du HTML rendu. Ce module nltk était plus rapide que même html2text, bien que peut-être html2text soit plus robuste.

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

Si vous vous souciez de la performance, voici un autre moyen plus efficace:

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.stringsest un itérateur, et il retourne NavigableStringpour que vous puissiez vérifier directement le nom de la balise du parent, sans passer par plusieurs boucles.

Bière Polor
la source
2

Le titre est à l'intérieur d'une <nyt_headline>balise, qui est imbriquée dans une <h1>balise et une <div>balise avec l'ID "article".

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

Devrait marcher.

Le corps de l'article se trouve dans une <nyt_text>balise, qui est imbriquée dans une <div>balise avec l'ID "articleBody". À l'intérieur de l' <nyt_text> élément, le texte lui-même est contenu dans des <p> balises. Les images ne font pas partie de ces <p>balises. Il est difficile pour moi d'expérimenter la syntaxe, mais je m'attends à ce qu'une éraflure de travail ressemble à quelque chose comme ça.

text = soup.findAll('nyt_text', limit=1)[0]
text.findAll('p')
Ewan Todd
la source
Je suis sûr que cela fonctionne pour ce cas de test cependant, à la recherche d'une réponse plus générique qui peut être appliquée à divers autres sites Web ... Jusqu'à présent, j'ai essayé d'utiliser des expressions régulières pour trouver les balises <script> </script> et < ! -. * -> commente et remplacez-les par "" mais cela s'avère même un peu difficile pour des raisons de somme ..
user233864
2

Bien que, je suggère complètement d'utiliser beautiful-soup en général, si quelqu'un cherche à afficher les parties visibles d'un html malformé (par exemple où vous avez juste un segment ou une ligne d'une page Web) pour une raison quelconque, ce qui suit supprimera le contenu entre les balises <et >:

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

Utiliser BeautifulSoup de la manière la plus simple avec moins de code pour obtenir simplement les chaînes, sans lignes vides ni merde.

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

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

Le moyen le plus simple de gérer ce cas est d'utiliser getattr(). Vous pouvez adapter cet exemple à vos besoins:

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)

Cela trouvera l'élément de texte "3.7", dans l'objet de balise <span class="ratingsContent">3.7</span>quand il existe, cependant, par défaut NoneTypequand il n'existe pas.

getattr(object, name[, default])

Renvoie la valeur de l'attribut nommé de l'objet. nom doit être une chaîne. Si la chaîne est le nom de l'un des attributs de l'objet, le résultat est la valeur de cet attribut. Par exemple, getattr (x, 'foobar') équivaut à x.foobar. Si l'attribut nommé n'existe pas, la valeur par défaut est renvoyée s'il est fourni, sinon, AttributeError est déclenché.

David Yerrington
la source
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
la source