BeautifulSoup คว้าข้อความหน้าเว็บที่มองเห็นได้


124

โดยพื้นฐานแล้วฉันต้องการใช้ BeautifulSoup เพื่อจับข้อความที่มองเห็นได้บนหน้าเว็บอย่างเคร่งครัด ตัวอย่างเช่นหน้าเว็บนี้เป็นกรณีทดสอบของฉัน และส่วนใหญ่ฉันต้องการรับข้อความเนื้อหา (บทความ) และอาจจะเป็นชื่อแท็บสองสามชื่อที่นี่และที่นั่น ฉันได้ลองใช้คำแนะนำในคำถาม SOนี้ซึ่งส่งคืน<script>แท็กและความคิดเห็น html จำนวนมากซึ่งฉันไม่ต้องการ ฉันไม่สามารถหาข้อโต้แย้งที่ต้องการสำหรับฟังก์ชันนี้findAll()เพื่อให้ได้ข้อความที่มองเห็นได้บนหน้าเว็บ

ดังนั้นฉันจะค้นหาข้อความที่มองเห็นได้ทั้งหมดยกเว้นสคริปต์ความคิดเห็น css และอื่น ๆ ได้อย่างไร

คำตอบ:


239

ลองสิ่งนี้:

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 ที่soup.findAll(text=True)ไม่เคยรู้มาก่อนเกี่ยวกับคุณลักษณะนั้น
Hartley Brody

7
สำหรับ BS4 ล่าสุด (อย่างน้อย) คุณสามารถระบุความคิดเห็นได้isinstance(element, Comment)แทนที่จะจับคู่กับ regex
tripleee

5
ฉันเชื่อว่าบรรทัดที่ 2 ควรเป็นsoup = BeautifulSoup(html)
jczaplew

11
ในฟังก์ชันที่มองเห็นได้ดูเหมือนว่า elif สำหรับการค้นหาความคิดเห็นจะใช้งานไม่ได้ elif isinstance(element,bs4.element.Comment):ฉันได้เพื่ออัปเดตการ ฉันยังเพิ่ม 'meta' ในรายชื่อผู้ปกครอง
Russ Savage

4
ตัวกรองด้านบนมี \ n จำนวนมากในผลลัพธ์ให้เพิ่มรหัสต่อไปนี้เพื่อกำจัดช่องว่างสีขาวและขึ้นบรรทัดใหม่: elif re.match(r"[\s\r\n]+",str(element)): return False
天才小飞猫

37

คำตอบที่ได้รับอนุมัติจาก @jbochi ใช้ไม่ได้กับฉัน การเรียกใช้ฟังก์ชัน str () ทำให้เกิดข้อยกเว้นเนื่องจากไม่สามารถเข้ารหัสอักขระที่ไม่ใช่ ascii ในองค์ประกอบ BeautifulSoup ต่อไปนี้เป็นวิธีที่รวบรัดมากขึ้นในการกรองหน้าเว็บตัวอย่างเป็นข้อความที่มองเห็นได้

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

1
หากstr(element)ล้มเหลวด้วยปัญหาการเข้ารหัสคุณควรลองunicode(element)ใช้แทนถ้าคุณใช้ 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
คำตอบก่อนหน้านี้ไม่ได้ผลสำหรับฉัน แต่สิ่งนี้ทำได้ :)
rjurney

หากฉันลองใช้สิ่งนี้ใน url imfuna.com มันจะส่งคืนคำเพียง 6 คำเท่านั้น (Imfuna Property Inventory and Inspection Apps) แม้ว่าจะมีข้อความ / คำในหน้ามากกว่านี้ก็ตาม ... แนวคิดใด ๆ ที่ทำให้คำตอบนี้ไม่ได้ผล URL? @bumpkin
the_t_test_1

10

ฉันเคารพการใช้ Beautiful Soup เพื่อให้ได้เนื้อหาที่แสดงผล แต่อาจไม่ใช่แพ็คเกจที่เหมาะสำหรับการรับเนื้อหาที่แสดงผลบนหน้าเว็บ

ฉันมีปัญหาคล้ายกันในการรับเนื้อหาที่แสดงผลหรือเนื้อหาที่มองเห็นได้ในเบราว์เซอร์ทั่วไป โดยเฉพาะอย่างยิ่งฉันมีหลายกรณีที่ผิดปกติในการทำงานกับตัวอย่างง่ายๆด้านล่างนี้ ในกรณีนี้แท็กที่ไม่สามารถแสดงได้จะซ้อนอยู่ในแท็กสไตล์และไม่สามารถมองเห็นได้ในหลาย ๆ เบราว์เซอร์ที่ฉันได้ตรวจสอบ มีรูปแบบอื่น ๆ เช่นการกำหนดการตั้งค่าแท็กคลาสแสดงเป็นไม่มี จากนั้นใช้คลาสนี้สำหรับ 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>

ทางออกหนึ่งที่โพสต์ไว้ด้านบนคือ:

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

โซลูชันนี้มีแอปพลิเคชันในหลาย ๆ กรณีอย่างแน่นอนและทำงานได้ค่อนข้างดีโดยทั่วไป แต่ใน html ที่โพสต์ด้านบนจะยังคงรักษาข้อความที่ไม่แสดงผล หลังจากค้นหา SO แล้วมีโซลูชันคู่หนึ่งเกิดขึ้นที่นี่BeautifulSoup get_text ไม่ได้ตัดแท็กและ JavaScript ทั้งหมด และที่นี่แสดง HTML เป็นข้อความธรรมดาโดยใช้ Python

ฉันลองใช้ทั้งสองวิธีนี้: html2text และ nltk.clean_html และรู้สึกประหลาดใจกับผลการจับเวลาดังนั้นคิดว่าพวกเขารับประกันคำตอบสำหรับลูกหลาน แน่นอนความเร็วขึ้นอยู่กับเนื้อหาของข้อมูล ...

หนึ่งคำตอบที่นี่จาก @Helge คือการใช้ nltk ของทุกสิ่ง

import nltk

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

มันทำงานได้ดีมากในการส่งคืนสตริงที่มี html ที่แสดงผล โมดูล nltk นี้เร็วกว่าแม้กระทั่ง html2text แม้ว่า html2text อาจมีประสิทธิภาพมากกว่า

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

3

หากคุณสนใจเรื่องประสิทธิภาพนี่เป็นอีกวิธีที่มีประสิทธิภาพมากขึ้น:

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เป็นตัววนซ้ำและจะส่งคืนNavigableStringเพื่อให้คุณสามารถตรวจสอบชื่อแท็กของผู้ปกครองได้โดยตรงโดยไม่ต้องวนซ้ำหลายครั้ง


2

ชื่ออยู่ใน<nyt_headline>แท็กซึ่งซ้อนอยู่ภายใน<h1>แท็กและ<div>แท็กที่มี id "article"

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

ควรทำงาน.

เนื้อหาบทความอยู่ภายใน<nyt_text>แท็กซึ่งซ้อนอยู่ภายใน<div>แท็กที่มี id "articleBody" ภายใน<nyt_text> องค์ประกอบข้อความนั้นมีอยู่ใน<p> แท็ก รูปภาพไม่ได้อยู่ใน<p>แท็กเหล่านั้น เป็นเรื่องยากสำหรับฉันที่จะทดลองกับไวยากรณ์ แต่ฉันคาดว่าการขูดที่ใช้งานได้จะมีลักษณะเช่นนี้

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

ฉันแน่ใจว่ามันใช้ได้กับกรณีทดสอบนี้อย่างไรก็ตามกำลังมองหาคำตอบทั่วไปที่สามารถนำไปใช้กับเว็บไซต์อื่น ๆ ได้ ... จนถึงตอนนี้ฉันได้ลองใช้ regexps เพื่อค้นหาแท็ก <script> </script> และ < ! -. * -> แสดงความคิดเห็นและแทนที่ด้วย "" แต่นั่นก็ยังพิสูจน์ได้ยากด้วยเหตุผลสรุป ..
user233864

2

ในขณะที่ฉันขอแนะนำให้ใช้ซุปที่สวยงามโดยทั่วไปหากใครต้องการแสดงส่วนที่มองเห็นได้ของ html ที่มีรูปแบบไม่ถูกต้อง (เช่นคุณมีเพียงส่วนหรือบรรทัดของหน้าเว็บ) ไม่ว่าจะด้วยเหตุผลใดก็ตามดังต่อไปนี้ จะลบเนื้อหาระหว่าง<และ>แท็ก:

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

2

การใช้ BeautifulSoup เป็นวิธีที่ง่ายที่สุดโดยใช้โค้ดน้อยลงเพื่อรับสตริงโดยไม่ต้องมีบรรทัดว่างและอึ

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

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

0

getattr()วิธีที่ง่ายที่สุดในการจัดการกับกรณีนี้คือการใช้ คุณสามารถปรับตัวอย่างนี้ตามความต้องการของคุณ:

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)

สิ่งนี้จะค้นหาองค์ประกอบข้อความ"3.7"ภายในออบเจ็กต์แท็ก<span class="ratingsContent">3.7</span>เมื่อมีอยู่อย่างไรก็ตามค่าเริ่มต้นจะเป็นNoneTypeเมื่อไม่มี

getattr(object, name[, default])

ส่งคืนค่าของแอตทริบิวต์ที่ตั้งชื่อของวัตถุ ชื่อต้องเป็นสตริง หากสตริงเป็นชื่อของหนึ่งในแอตทริบิวต์ของวัตถุผลลัพธ์คือค่าของแอตทริบิวต์นั้น ตัวอย่างเช่น getattr (x, 'foobar') เทียบเท่ากับ x.foobar หากไม่มีแอตทริบิวต์ที่ระบุชื่อค่าดีฟอลต์จะถูกส่งกลับหากระบุไว้มิฉะนั้นจะมีการเพิ่ม AttributeError


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))
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.