Texto da Página Visível da BeautifulSoup Grab


124

Basicamente, quero usar o BeautifulSoup para capturar estritamente o texto visível em uma página da web. Por exemplo, esta página é meu caso de teste. E quero principalmente obter o texto do corpo (artigo) e talvez até alguns nomes de guias aqui e ali. Eu tentei a sugestão nesta pergunta SO que retorna muitas <script>tags e comentários html que eu não quero. Não consigo descobrir os argumentos necessários para a função findAll()para obter apenas os textos visíveis em uma página da web.

Então, como devo encontrar todo o texto visível, exceto scripts, comentários, css etc.?

Respostas:


239

Tente o seguinte:

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))

47
+1 para soup.findAll(text=True)nunca soube sobre esse recurso
Hartley Brody

7
Para BS4 recente (pelo menos), você pode identificar comentários com, em isinstance(element, Comment)vez de corresponder a um regex.
Tripleee

5
Eu acredito que a linha 2 deve sersoup = BeautifulSoup(html)
jczaplew

11
Na função visível, o elif para encontrar comentários não parecia funcionar. eu tive que atualizá-lo para elif isinstance(element,bs4.element.Comment):. Eu também adicionei 'meta' à lista de pais.
Russ Savage

4
O filtro acima tem um monte de \ n no resultado, adicione o seguinte código para eliminar espaços em branco e novas linhas: elif re.match(r"[\s\r\n]+",str(element)): return False
天才小飞猫

37

A resposta aprovada do @jbochi não funciona para mim. A chamada da função str () gera uma exceção porque não pode codificar os caracteres não-ascii no elemento BeautifulSoup. Aqui está uma maneira mais sucinta de filtrar a página da Web de exemplo para o texto visível.

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)falhar com problemas de codificação, você deve tentar unicode(element)em vez se você estiver usando o 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'))

4
As respostas anteriores não funcionou para mim, mas isso fez :)
rjurney

Se eu tentar isso no url imfuna.com, ele retornará apenas 6 palavras (aplicativos de inspeção e inventário de propriedades da Imfuna), apesar de haver muito mais texto / palavras na página ... alguma idéia de por que essa resposta não funciona para isso url? @bumpkin
the_t_test_1

10

Eu respeito completamente o uso da Beautiful Soup para obter conteúdo renderizado, mas pode não ser o pacote ideal para adquirir o conteúdo renderizado em uma página.

Eu tive um problema semelhante para obter conteúdo renderizado ou o conteúdo visível em um navegador típico. Em particular, tive muitos casos talvez atípicos para trabalhar com um exemplo tão simples abaixo. Nesse caso, a marca não exibível está aninhada em uma marca de estilo e não é visível em muitos navegadores que verifiquei. Existem outras variações, como definir uma exibição de configuração de tag de classe como nenhuma. Em seguida, use esta classe para a 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>

Uma solução postada acima é:

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

Essa solução certamente possui aplicativos em muitos casos e funciona muito bem em geral, mas no html postado acima, ele mantém o texto que não é renderizado. Depois de pesquisar no SO, algumas soluções surgiram aqui BeautifulSoup get_text não remove todas as tags e JavaScript e aqui Renderizou HTML em texto sem formatação usando Python

Tentei as duas soluções: html2text e nltk.clean_html e fiquei surpreso com os resultados do tempo, por achar que eles mereciam uma resposta para a posteridade. Obviamente, as velocidades dependem muito do conteúdo dos dados ...

Uma resposta aqui do @Helge foi sobre o uso do nltk de todas as coisas.

import nltk

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

Funcionou muito bem para retornar uma string com html renderizado. Esse módulo nltk foi mais rápido que o html2text, embora talvez o html2text seja mais robusto.

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

3

Se você se preocupa com o desempenho, aqui está outra maneira mais 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é um iterador e retorna NavigableStringpara que você possa verificar diretamente o nome da tag pai, sem passar por vários loops.


2

O título está dentro de uma <nyt_headline>tag, que está aninhada dentro de uma <h1>tag e uma <div>tag com o ID "article".

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

Deveria trabalhar.

O corpo do artigo está dentro de uma <nyt_text>tag, que é aninhada dentro de uma <div>tag com o ID "articleBody". Dentro do <nyt_text> elemento, o próprio texto está contido nas <p> tags. As imagens não estão nessas <p>tags. É difícil para mim experimentar a sintaxe, mas espero que um rascunho de trabalho se pareça com isso.

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

Tenho certeza de que isso funciona para este caso de teste, no entanto, procurando uma resposta mais genérica que possa ser aplicada a vários outros sites ... Até agora, tentei usar o regexps para encontrar tags <script> </script> e < ! -. * -> comentários e substitua-os por "" mas isso é até um pouco difícil por uma razão
sumária

2

Embora eu sugira completamente o uso de beautiful-soup em geral, se alguém estiver olhando para exibir as partes visíveis de um html malformado (por exemplo, onde você tem apenas um segmento ou linha de uma página da web) por qualquer motivo, o seguinte removerá o conteúdo entre <e >tags:

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 da maneira mais fácil, com menos código, para obter apenas as strings, sem linhas vazias e porcaria.

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

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

0

A maneira mais simples de lidar com esse caso é usando getattr(). Você pode adaptar este exemplo às suas necessidades:

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)

Isso localizará o elemento de texto "3.7", no objeto de tag <span class="ratingsContent">3.7</span>quando ele existir, no entanto, será o padrão quando não existir NoneType.

getattr(object, name[, default])

Retorne o valor do atributo nomeado do objeto. O nome deve ser uma sequência. Se a sequência for o nome de um dos atributos do objeto, o resultado será o valor desse atributo. Por exemplo, getattr (x, 'foobar') é equivalente a x.foobar. Se o atributo nomeado não existir, o padrão será retornado se fornecido, caso contrário, AttributeError será gerado.


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))
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.