Porovnanie HTML parserov (lxml vs BeautifulSoup)

Pracujem na jednom projekte v ktorom potrebujem získavať informácie z približne 200.000 webových stránok. Rozhodol som sa použiť knižnicu BeautifulSoup kvôli jej jednoduchosti. Keď som naimplementoval parser, tak som zistil, že by som potreboval cez 30 dní na to, aby som prešiel všetky stránky, čo bolo na môj vkus až príliš veľa. Tak som sa rozhodol, že spravím porovnanie výkonnosti HTML parserov pre Python, aby som vedel ako sú na tom s výkonom.

Na test som sa rozhodol použiť jednoduchý skript, ktorý z detailu firmy na obchodnom registry získa názov firmy. V teste som použil dva najpoužívanejšie frameworky na parsovanie formátu HTML: lxml a BeautifulSoup.

Knižnica BeautifulSoup umožňuje použiť na parsovanie HTML rôzne knižnice (html, html5lib, lxml), tak som porovnal výkonnosť všetkých variantov. Pre BeautifulSoup a lxml bolo treba napísať zvlášť dotaz, pretože každý framework používa iný dotazovací jazyk. Nakoniec som zistil že lxml podporuje aj dotazy CSS pomocou knižnice cssselect (ktorú je ale potrebné dodatočne nainštalovať). Dotazy som neoptimalizoval, napísal som ich ako to bolo pre mňa prirodzené a najjednoduchšie.

Po vykonaní testovacieho skriptu (uvedený na konci článku) boli výsledky nasledovné:

Vysledky zoradene od najrychlejsieho (pre 100 cyklov)

0.31626s: lxml  
0.32352s: lxml (cssselect)  
3.30499s: beautifulsoup_lxml  
4.52193s: beautifulsoup_html  
10.93316s: beautifulsoup_html5lib  

Ako vidieť z výsledkov, tak knižnica lxml je takmer 10x rýchlejšia ako najrýchlejší variant s BeautifulSoup. Takže pre mňa je jasným víťazom knižnica lxml.

Doplnené

V komentároch bolo vytknuté že dotazy BeautifulSoup VS lxml neboli "rovnaké", keďže v BeautifulSoup variante sa volala viackrát funkcia: .find_all(). Tak som spravil porovnatelnú verziu dotazov ale výsledok bol v podstate taký istý.

def beautifulsoup_lxml(html):  
    bs = BeautifulSoup(html, "lxml")
    return bs.find_all("table")[4].find_all("td")[1].find("span").get_text().strip()

VS

def lxml(html):  
    parser = etree.HTMLParser()
    root = etree.fromstring(html, parser)
    elements = root.xpath("//table[6]//td[1]/span[1]/text()")
    return elements[0].strip()

VYSLEDOK

0.32326s: lxml  
3.31563s: beautifulsoup_lxml  

Skript pre meranie rýchlosti HTML parserov:

# pip install requests lxml html5lib beautifulsoup4 cssselect
import time  
import requests  
from lxml import etree  
from lxml.cssselect import CSSSelector  
from bs4 import BeautifulSoup


def load_html():  
    r = requests.get("http://orsr.sk//vypis.asp?ID=11422&SID=2&P=0")
    r.encoding = "windows-1250"
    return r.text


def beautifulsoup_html(html):  
    bs = BeautifulSoup(html, "html.parser")
    return bs.find_all("table")[4].find_all("td")[1].find("span").get_text().strip()


def beautifulsoup_html5lib(html):  
    bs = BeautifulSoup(html, "html5lib")
    return bs.find_all("table")[4].find_all("td")[1].find("span").get_text().strip()


def beautifulsoup_lxml(html):  
    bs = BeautifulSoup(html, "lxml")
    return bs.find_all("table")[4].find_all("td")[1].find("span").get_text().strip()


def lxml(html):  
    parser = etree.HTMLParser()
    root = etree.fromstring(html, parser)
    elements = root.xpath("/html/body/table[4]/tr/td[2]/table/tr/td[1]/span[1]/text()")
    return elements[0].strip()


def lxml_cssselect(html, selector):  
    parser = etree.HTMLParser()
    root = etree.fromstring(html, parser)

    elements = selector(root)
    return elements[0].text.strip()

html = load_html()

# cssselect: css -> xpath
selector = CSSSelector("table:nth-of-type(4) tr > td:nth-of-type(2) > table td:nth-of-type(1) > span")

print()  
print("Vysledky parsingu")  
print()

print("beautifulsoup_html: " + beautifulsoup_html(html))  
print("beautifulsoup_html5lib: " + beautifulsoup_html5lib(html))  
print("beautifulsoup_lxml: " + beautifulsoup_lxml(html))  
print("lxml_cssselect: " + lxml_cssselect(html, selector))  
print("lxml: " + lxml(html))  
print()  
print()

COUNT = 100  
results = []

# beautifulsoup_html
ts = time.time()  
for i in range(COUNT):  
    beautifulsoup_html(html)
te = time.time()  
results.append(("beautifulsoup_html", round(te - ts, 5)))

# beautifulsoup_html5lib
ts = time.time()  
for i in range(COUNT):  
    beautifulsoup_html5lib(html)
te = time.time()  
results.append(("beautifulsoup_html5lib", round(te - ts, 5)))

# beautifulsoup_lxml
ts = time.time()  
for i in range(COUNT):  
    beautifulsoup_lxml(html)
te = time.time()  
results.append(("beautifulsoup_lxml", round(te - ts, 5)))

# lxml
ts = time.time()  
for i in range(COUNT):  
    lxml(html)
te = time.time()  
results.append(("lxml", round(te - ts, 5)))

# lxml cssselect
ts = time.time()  
for i in range(COUNT):  
    lxml_cssselect(html, selector)
te = time.time()  
results.append(("lxml (cssselect)", round(te - ts, 5)))

print("Vysledky zoradene od najrychlejsieho")  
print()  
sorted_results = []  
for r in results:  
    sorted_results.append("{:.5f}".format(r[1]) + "s: " + r[0])

print("\n".join(sorted(sorted_results)))  
print()  
Show Comments