Beautiful Soup

Uit De Vliegende Brigade
Ga naar: navigatie, zoeken

Beautiful Soup (BS) lijkt de standaard-bibliotheek te zijn voor webscraping, oftewel het parsen van HTML (of XML). De actuele versie (zomer 2019) is BS4. Deze is beschikbaar voor zowel Python 2.7 en 3.4. Beautiful Soup converteert een complexe hiërarchische HTML-of XML-boom om naar een complexe hiërarchische Python-boom. De documentatie op hun site van de maker vind ik geweldig: https://www.crummy.com/software/BeautifulSoup/bs4/doc.

Dit artikel beperkt zich tot het parsen van HTML. Parsen van XML wordt hier niet behandeld.

HTML-syntaxis

Zonder deze basis wordt het heel ingewikkeld:

Unair element zonder attribuut of inhoud

<br>

Analyse:

  • Dit is een unair [1] (unary) HTML-element
  • Dit element kent één tag, label of markeerder
  • Er is hier geen verschil tussen het element en de tag
  • Geen attribuut (zie verderop) of inhoud.

Binair element + waarde

<title> Welkom! </title>

Analyse:

* <title>  - Begintag of ''start tag''
* </title> - Eindtag of ''end tag''
* Welkom!  - Inhoud (content) van het element

Unair element + 1 attribuut - zonder inhoud

<meta charset="utf-8">

Analyse:

  • Een unair element: meta
  • Eén attribuut: charset="utf-8"
  • Attribuut-naam: charset
  • Attribuut-waarde: utf-8
  • Geen inhoud.

Unair element + 2 attributen - geen inhoud

<meta property="og:title" content="Blub">
  • Element dat maar één tag kent
  • Eerste attribuut: property="og:title"
  • Tweede attribuut: content="Blub"
  • Geen inhoud.

Hyperlink: Binair element + attribuut + inhoud

<a href="https://example.com"> example.com</a>
  • Een binair element, want begin- & eindtag
  • Eén attribuut: href="https://example.com"
  • Attribuut-naam: href
  • Attribuut-waarde (value): "https://example.com" - Dit lijkt overigens niet argument te worden genoemd [2]
  • Inhoud van het element: example.com.

Hyperlink met 3 attributen + inhoud

<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>
  • Eerste attribuut: href="http://example.com/tillie"
  • Tweede attribuut: class="sister"
  • Derde attribuut: id="link3">
  • Inhoud: Tillie

Meerwaardige attributen

Er bestaan attributen, die per attribuut meerdere waardes kunnen bevatten. class is hiervan waarschijnlijk het beste voorbeeld. Bv:

<a href="http://example.com/tillie" class="sister, nog_een_class, nog_eentje, kappen_nou" id="link3">Tillie</a>

Daar houdt BS rekening mee, door de waarde van de betreffende attributen in lijsten onder te brengen.

Hiërarchie van een HTML-document

In het volgende hoofstuk over navigeren, is het soms handig om een idee te hebben van de hiërarchie binnen een HTML-document. In het HTML-script dat in de voorbeelden wordt gebruikt, kun je de hiërarchie eenvoudig aflezen aan het inspringen. Dat kun je verifiëren met

print(s.prettify())

(waarbij s het bs-object bevat - Zie verderop).

Zie daarnaast het hoofdstuk over .children voor meer details.

Objecten

Het lijkt te helpen om een goed idee te hebben van het soort van objecten waarmee je te maken krijgt.

Volgends de documentatie, heb je bij parsen en navigeren met BS, met vier soorten BS-objecten te maken, die elk diverse methodes en argumenten kennen:

  1. Tag - Gewoon, HTML-tags
  2. NavigableString - Tekstgedeeltes van tags
  3. BeautifulSoup - Boom; het document-als-geheel - aka. bs4.BeautifulSoup
  4. Comment - Broertje van NavigableString.

Deze indeling is tot op heden (4 aug. 2019) nog niet erg nuttig voor me geweest.

Wat ik in de praktijk zoal aan objecten tegenkom:

  • <class 'bs4.BeautifulSoup'> - Eén van de vier die in de documentatie genoemd worden!
  • <class 'bs4.element.Tag'>
  • <class 'bs4.element.ResultSet'>.

Daarnaast heb je met een handjevol Python-objecten te maken. Waarschijnlijk handig als je daar goed mee uit de voeten kan:

  • list
  • list_iterator
  • dictionaries.

bs4.BeautifulSoup

Een BeautifulSoup-object betreft het document-als-geheel of een deel daarvan:

>>> type(s)
<class 'bs4.BeautifulSoup'>

Comment

bs4.element.Tag

bs4.element.ResultSet

list

list_operator

dictionaries

Navigating the tree

Voorbeeldscript:

#! /usr/bin/python3
#
print ("\n\n\n")
print (">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
print (">>> 10-navigatie-voorbeeld")
print (">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>")
print ("\n\n")

from bs4 import BeautifulSoup

d_html="""
<html>
   <head>
        <title>Dit is de titel</title>
     </head>
     <body>

        <p class="titel">
           <b>Dit is de titel</b>
        </p>

        <p class="verhaal oneven">
           Het eerste verhaal gaat over
           <a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>
        </p>

        <p class="verhaal even">
           Het tweede verhaal gaat over
           <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a>
        </p>

        <p class="verhaal oneven">
           Het derde verhaal gaat over
           <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>
        </p>

        En ter afsluiting, nog een <a href="http://example.com/verloren_link">verloren link</a>.
"""

s=BeautifulSoup(d_html, 'lxml')

Aanroep + interactieve modus:

p3 -i 10-navigatie-voorbeeld.py

Tag names

Waarschijnlijk de simpelste manier om 'iets' terug te vinden in een HTML-document: Gewoon de HTML-tag benoemen die je zoekt. Als er meerder tags zijn met dezelfde naam, wordt de eerste gekozen:

>>> s.head
<head>
<title>Dit is de titel</title>
</head>

>>> s.p # De eerste p-tag wordt geretourneerd
<p class="titel">
<b>Dit is de titel</b>
</p>

Er is trouwens een verschil tussen (in dit geval) s.head en s.head(). Ik denk dat technisch gezien, het eerste statement een object-attribuut opvraagt, en het tweede statement een object-methode aanroept. De resulterende objecten zijn van verschillende klasses.

Zonder haakjes:

>>> s.head
<head>
<title>Dit is de titel</title>
</head>

>>> type(s.head)
<class 'bs4.element.Tag'>

Met haakjes:

>>> s.head()
[<title>Dit is de titel</title>]

>>> type(s.head())
<class 'bs4.element.ResultSet'>

Tag names cascaderen

Een child is een directe afstammeling van een object. Een descendant is een directe of indirecte afstammeling. Je kunt descendants cascaderen. Bv.:

>>> s.p
<p class="titel">
<b>Dit is de titel</b>
</p>

>>> s.p.b
<b>Dit is de titel</b>

Text-attribuut

>>> s.p.b
<b>Dit is de titel</b>

>>> s.p.b.text
'Dit is de titel'

Het staat me bij dat er een gemakkelijke manier was om de aanhalingstekens weg te toveren.

Find

Met find kun je op allerlei manieren zoeken. Voorbeeld waarbij de operaties hetzelfde object vinden - Maar wel op verschillende manieren:

>>> s.p
<p class="titel">
<b>Dit is de titel</b>
</p>

>>> type(s.p)
<class 'bs4.element.Tag'>

>>> s.find(class_="titel")
<p class="titel">
<b>Dit is de titel</b>
</p>

>>> type(s.find(class_="titel"))
<class 'bs4.element.Tag'>

Zie aparte hoofdstuk voor details.

Tag names & find_all

Met find_all creëer je een bs4.element.ResultSet-object. Je kunt zoeken op tag-naam, klasses, attribuut-namen en vermoedelijk nog meer. Voorbeeld:

>>> s.find_all('a')

[<p class="titel">
<b>Dit is de titel</b>
</p>, <p class="verhaal oneven">
   Het eerste verhaal gaat over
   <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
</p>, <p class="verhaal even">
   Het tweede verhaal gaat over
   <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
</p>, <p class="verhaal oneven">
   Het derde verhaal gaat over
   <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
</p>]

>>> s.find_all('a')
[<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>, <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>, <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]

type(s.find_all('a'))
<class 'bs4.element.ResultSet'>

Het objecttype bs4.element.ResultSet is vergelijkbaar met het standaard-objecttype list:

>>> tmp=['a', 'b']
>>> type(tmp)
<class 'list'>

Voorbeeld waarbij er gefiltered wordt op element-naam, attribuut-naam en attribuut-waarde:

cs = p_soup.findAll("div",{"class":"item-container"})

Itereren over een ResultSet, lijkt op dezelfde manier te gaan als voor een list. De elemten zijn van het type bs4.element.Tag:

print (">>> Create resultset cs...")

cs = p_soup.findAll("div",{"class":"item-container"})

type(cs)	# <class 'bs4.element.ResultSet'>
len(cs)		# Aantal elementen = 40

for a in cs:
	print("\n\n>>>>>>>>> Volgende element")
	print(a)

Tag names & .contents

Met .contents selecteer je het gedeelte van de boom dat zich onder de gegeven tag vindt. Het resulterende object is van het type list - En dus niet een specifiek bs4-objecttype (ik had eigenlijk een BeautifulSoup-object verwacht).

Voorbeeld:

>>> s.head.contents
['\n', <title>Dit is de titel</title>, '\n'] # enige onder 'head', zijn de argumenten van 'head'

>>> s.body.contents         # Onder 'body' vind je alles, behalve 'head'
['\n', <p class="titel">
<b>Dit is de titel</b>
</p>, '\n', <p class="verhaal oneven">
  			Het eerste verhaal gaat over
  			<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
</p>, '\n', <p class="verhaal even">
  			Het tweede verhaal gaat over
  			<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
</p>, '\n', <p class="verhaal oneven">
  			Het derde verhaal gaat over
  			<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
</p>, '\n']

>>> s.p.contents   # Eerste p-tag
['\n', <b>Dit is de titel</b>, '\n']

>>> s.p.b.contents   # b binnen eerste p-tag
['Dit is de titel']

>>> type(s.p.b.contents)
<class 'list'>

Tag names & .children

.children Lijkt een broertje te zijn van .contents. Met .children creëer je een list_operator-object (en dus geen bs4-specifiek object). Daarmee kun je itereren, ofzo:

>>> s.body.children
<list_iterator object at 0x7fcefdaab908>

>>>type(s.body.children)
<class 'list_iterator'>

>>> list(s.body.children)
['\n', <p class="titel">
<b>Dit is de titel</b>
</p>, '\n', <p class="verhaal oneven">
  			Het eerste verhaal gaat over
  			<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
</p>, '\n', <p class="verhaal even">
  			Het tweede verhaal gaat over
  			<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
</p>, '\n', <p class="verhaal oneven">
  			Het derde verhaal gaat over
  			<a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
</p>, '\n\n  \t\tEn ter afsluiting, nog een ', <a href="http://example.com/verloren_link">verloren link</a>, '.\n']

>>> len(list(s.body.children))
11

Om te achterhalen wat precies die 11 elementen zijn:

i=0
for child in s.body.children:
	i=i+1
	print(i)
	print(child)

resulteert in

1


2
<p class="titel">
<b>Dit is de titel</b>
</p>
3


4
<p class="verhaal oneven">
           Het eerste verhaal gaat over
           <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
</p>
5


6
<p class="verhaal even">
           Het tweede verhaal gaat over
           <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>
</p>
7


8
<p class="verhaal oneven">
           Het derde verhaal gaat over
           <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>
</p>
9


        En ter afsluiting, nog een 
10
<a href="http://example.com/verloren_link">verloren link</a>
11
.


Het lijkt erop, dat je children niet kunt gebruiken icm. een ResultSet

Tag names & .descendants

Met descendants bereik je alle onderliggende elementen, onafhankelijk van de afstand. In het voorbeeld hierboven (s.body) heb je 11 children en 28 descendants.

Get the nth-occurence of an object

Dit blijkt ongelofelijk eenvoudig te zijn: met find_all krijg je een array. Met indexes zoals [2] pluk je het juiste object uit de lijst. Voorbeeld:

#! /usr/bin/python3
#
from bs4 import BeautifulSoup


###################################################################
# Instantiate html-string
###################################################################
#
d_html="""
<html>
   <head>
        <title>Dit is de titel</title>
     </head>
     <body>

        <p class="titel">
           <b>Dit is de titel</b>
        </p>

        <p class="verhaal oneven">
           Het eerste verhaal gaat over
           <a href="http://example.com/elsie" class="sister lastiglastig" id="link1">Elsie</a>
        </p>

        <p class="verhaal even">
           Het tweede verhaal gaat over
           <a href="http://example.com/lacie" class="sister" id="link2">Lacie</a>
        </p>

        <p class="verhaal oneven">
           Het derde verhaal gaat over
           <a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>
   
           En een extra URL, die niet zo gemakkelijk te filteren is:
           <a href="http://example.com/last-url" class="sister lastiglastig"</a>
        </p>

        En ter afsluiting, nog een <a href="http://example.com/verloren_link">verloren link</a>.
"""


###################################################################
# Instantiate BS-object
###################################################################
#
# * Objecttype "p": <class 'bs4.beautifulSoup'> - "page"
# * Ik weet niet meer wat "lxml" betekent
#
p=BeautifulSoup(d_html, 'lxml')


###################################################################
# Filter URL in last paragraph
###################################################################
#
# Soms is het zo gemakkelijk:
#
i1=p.find_all(class_="oneven")[1]

# Of nog een stapje verder
#
i2=p.find_all(class_="oneven")[1].find(class_="lastiglastig")

find()

  • Iets meer in detail hoe find() werkt
  • Het resultaat lijkt altijd van het type 'bs4.element.ResultSet' te zijn

Zoeken op elementnaam

Zonder verdere toevoegingen, zoekt find op elementnaam:

In dit geval (script eerder in dit hoofdstuk) krijg je één element terug:

p.find("head")

En hier een groot deel van het document:

p.find("body")

Zoeken op klasse

Je kunt zoeken op generieke attribuut-namen & -waardes, maar voor klasses lijkt er een specifieke functionaliteit te zijn ingebouwd. Let op de underscore na classe. Da's omdat dit een gereserveerd woord is:

p.find(class_="grote_klasse")

Zoeken op alleen attribuut-naam

Zie [3] voor vermoedelijk twee verschillende manieren om dit te doen.

Zoeken op alleen attribuut-waarde

p.find(attrs={'even'})

Zoeken op elementnaam + attribuutnaam + attribuutwaarde

Zo dus!

p.find("p", {"class":"even"})

Zie ook [4]

Zie ook

Bronnen