Varje inlägg om den "snabbaste Python-HTML-parsern" nämner förr eller senare selectolax, och nästan alla avslutar där med att den är "mycket snabbare än BeautifulSoup". Det stämmer. Det som nästan aldrig tas med är vad som händer när man ställer selectolax mot lxml i stället — för då blir ordet "snabbast" plötsligt mer komplicerat.
Så jag testade ordentligt: selectolax (båda dess backends) mot lxml, BeautifulSoup med html.parser och med lxml, samt parsel, över fem sidstorlekar från 1 KB till 10 MB. Varje mätning är medianen av tre separata körningar i egna processer. selectolax krossade BeautifulSoup och gick jämt upp med ren lxml — för att sedan förlora själva parsningen till lxml. Alla siffror nedan är preliminära och kommer från en maskin (macOS arm64, Python 3.14.2); skripten finns committade, så kör dem gärna själv innan du citerar mig.
Vad selectolax faktiskt är (och inte är)
selectolax är ett Python-bindinglager till två C-motorer — Modest och Lexbor — som tolkar HTML5 och låter dig söka med CSS-selektorer. Det är inte en crawler, inte en browser och inte en "scraper" i betydelsen ett knapptryck. Det är verktyget du skickar en HTML-klump till efter att du redan hämtat sidan. Projektets egen kortversion lyder: "A fast HTML5 parser with CSS selectors, written in Cython, using Modest and Lexbor engines."
Det finns två backends, och skillnaden spelar större roll än dokumentationen antyder:
LexborHTMLParser(Lexbor-motorn) — den som README rekommenderar från och med 2024.HTMLParser(Modest-motorn) — originalet, vars underliggande C-bibliotek enligt samma README "is not maintained anymore".
Några saker är bra att känna till innan vi pratar fart. Vid repo-snapshotten som togs 2026-07-10 hade selectolax 1 653 stjärnor, och senaste releasen var v0.4.10 (maj 2026). PyPI anger stöd för Python >=3.9,<3.15. Installationen är den minst dramatiska delen av hela historien: pip install selectolax hämtade en färdigbyggd cp314-wheel på 2,3 MB och fungerade direkt i Python 3.14 — ingen browser-nedladdning, inget doctor-steg, ingen kompilering. Det är den stillsamma fördelen med en ren parser jämfört med ett browser-baserat verktyg. Den importeras bara och körs.
En licensdetalj är värd att nämna direkt: Python-bindningen är MIT-licensierad, men wheelen innehåller de inbyggda motorerna, och de har egna licenser — Modest är LGPL-2.1, Lexbor är Apache-2.0. Så "selectolax är MIT" stämmer för Python-koden men är ofullständigt för den binär du faktiskt distribuerar. Om ditt juristteam bryr sig om vidarepacketerade komponenter är det just den distinktionen du ska lyfta.
Fartfrågan, besvarad med riktiga siffror
Det här var uppgiften jag mätte: tolka HTML-strängen, hämta all text i <h3 class="title">, och hämta varje <a>-href. Latensen anges i millisekunder, som medianen av tre separata processkörningar; spridningen mellan körningar låg under ungefär 5 % för de C-baserade parserna i de flesta storlekar. Innan någon cell tidtagits reducerades varje parsers output till ett innehållshash, så att en parser som i tysthet gjorde mindre jobb skulle avslöjas och uteslutas — på de här sidorna matchade alla sex vid varje storlek, så detta är en äkta jämförelse mellan lika saker. Hela datamängden finns i den committade bench_parse.json.

| Sida | selectolax (Lexbor) | selectolax (Modest) | lxml | parsel | BS (lxml) | BS (html.parser) |
|---|---|---|---|---|---|---|
| 1 KB | 0.027 | 0.029 | 0.036 | 0.044 | 0.281 | 0.323 |
| 10 KB | 0.160 | 0.171 | 0.166 | 0.228 | 1.705 | 2.049 |
| 100 KB | 1.464 | 1.565 | 1.423 | 2.020 | 16.5 | 20.6 |
| 1 MB | 14.901 | 16.9 | 14.177 | 20.9 | 181.9 | 232.6 |
| 10 MB | 159.9 | 247.9 | 172.9 | 231.9 | 2261.6 | 2788.7 |
Mot BeautifulSoup: ungefär 12–17x, och ryktet underskattar skillnaden
Om man gör om det till förhållanden hamnar selectolax-Lexbor på ungefär 12x snabbare än BeautifulSoup(html.parser) på en 1 KB-sida, och runt 17x snabbare vid 10 MB, samt cirka 10–14x snabbare än BeautifulSoup(lxml) inom samma spann. Den siffra man ofta ser online — "selectolax är ungefär 4–5x snabbare än BeautifulSoup" — är för låg mot html.parser och ungefär rätt först när man jämför med BeautifulSoup som använder lxml. Det verkliga multiplikatorn beror på vilken BeautifulSoup-variant du menar och hur mycket du extraherar per sida.
Det stämmer också med README:ns egen benchmark, som antyder en 25,5x fördel mot BeautifulSoup(html.parser). Ingen av uppgifterna är fel. README:ns test (titel, länkar, scripts och meta från små startsidor) gör mindre extrahering på små sidor, vilket gör BeautifulSoups overhead per parse mer dominerande. Sammantaget ser det ut så här: selectolax är ungefär 10–15x snabbare än BeautifulSoup i realistiskt parse-och-extrahera-arbete, ännu mer på pyttesmå sidor och vid lätt extrahering.
Om din nuvarande flaskhals är ett berg av BeautifulSoup-kod som tuggar igenom sidor, är det här en migration som snabbt betalar sig. Det är okontroversiellt. Nästa jämförelse är det inte.
Mot lxml: oavgjort — och lxml vinner det moment alla glömmer att isolera
Titta på raderna för 100 KB och 1 MB. Lexbor och lxml ligger inom ungefär 5 % från varandra, deras intervall överlappar, och enligt min metodik är det oavgjort — ingen vinnare, inget "snabbare". Det enda ställe där selectolax faktiskt går förbi är 10 MB-sidan (159,9 ms mot 172,9 ms, alltså en skillnad på 8,1 % med icke-överlappande intervall). På hela uppgiften matchar alltså selectolax lxml och slår den bara på de allra största dokumenten.

Sedan isolerade jag trädbyggandet från CSS-sökningen, och då vänder resultatet på ett sätt som många genomgångar missar. För ren parsing, utan någon query alls, var lxml konsekvent cirka 33–34 % snabbare än selectolax-Lexbor på min maskin — 77,9 ms mot 116,6 ms på 10 MB-sidan. I helheten jämnar det ändå ut sig, och min arbetshypotes (inte något jag bevisade med ett attributionsexperiment) är att CSS-frågan på dessa sidor bara tar en liten del av totalen, så lxml:s försprång i parse-steget späds ut tills totalsiffrorna möts.
Detta är det mest ifrågasättbara påståendet i hela genomgången, och jag vill vara tydlig med varför. Det går emot den gängse uppfattningen, och den enda publicerade benchmark jag hittade som isolerar parse-only — aows.jpt.sh — rapporterar motsatsen, alltså att selectolax är ungefär 4x snabbare. Därför satte jag tydliga räcken runt testet: resultatet kommer från en enda plattform (macOS arm64, Python 3.14, förbyggda cp314-wheels — Linux x86_64 och source build är inte testade), det verifierades över fyra sidstorlekar och höll i alla, och jag kontrollerade det igen med två olika lxml-API:er för att utesluta ett API-artifakt. Båda lxml-API:erna slog selectolax-Lexbor i varje storlek. Jag presenterar inte "lxml parsar snabbare" som en slutgiltig sanning — jag presenterar det som det min benchmark gav, med skriptet bifogat, mot det mesta som publicerats. Kör det på din egen maskin.
En detalj till: när jag körde urval av 100 000 <a>-element på en platt sida, så gick lxml och selectolax-Modest jämt upp (33,30 ms mot 34,19 ms, med överlappande intervall), medan selectolax-Lexbor låg ungefär 15 % efter båda. Det alla tre C-motorer har gemensamt är att de är 5–7x snabbare än parsel eller BeautifulSoup vid massutplock, vars modell med ett Python-objekt per nod är den verkliga bromsklossen. Så påståendet "selectolax är snabbast på bulk CSS-selektion" håller inte heller — Modest går bara jämt upp med lxml, och Lexbor förlorar mot den.
Den slutsats jag faktiskt skulle stå bakom: selectolax:s fördel över lxml handlar inte om bred, generell helhastighet. Den vinner bara på de största sidorna. Dess styrka ligger i annat — API-känsla, beteende på skräpinput och modern CSS — och det är där resten av den här genomgången landar.
Minne och kallstart: rangordna efter RSS, inte efter din profiler
Minnesanvändning är där jag måste korrigera mina egna tidigare siffror, och den rättelsen är viktig. Mätt som RSS-ökning på 10 MB-sidan med tracemalloc avstängt använder BeautifulSoup ungefär 1,5–1,8x mer minne än selectolax eller lxml — spannet går från 1,51x (BS-lxml på 218,4 MB mot Lexbor på 144,6 MB) upp till 1,75x i den övre delen. selectolax och lxml ligger i samma snåla klass; lxml är snålast enligt RSS.

En tidigare version av mina siffror visade "cirka 3x", och det var fel av en lärorik anledning: mätningen gjordes med tracemalloc aktivt, och tracemallocs bokföring per allokering fördubblar ungefär den upplevda RSS:n för den parser som allokerar mest. Så en tydlig rekommendation till alla som benchmarkar parserminne: rangordna efter RSS med din profiler avstängd. Att rangordna efter tracemalloc-peak missgynnar just de C-baserade parserna — det fick selectolax-Lexbor att se tyngre ut än Modest, trots att de ligger nära varandra i verklig RSS. BeautifulSoup är verkligen tyngst här; den är bara inte 3x tyngre, som ett kontaminerat instrument råkade visa.
Kallstarten är liten men verklig: selectolax importeras på ungefär 14 ms, alltså ungefär i nivå med lxml och runt 2,3x snabbare än bs4 eller parsel. Om du levererar ett CLI-verktyg eller en serverless-funktion där importtid ingår i varje anrop, är det en skillnad värd att notera.
CSS-selektortäckning: stark, men med ett par riktiga hål
CSS-täckningen byggdes som en matris med 41 fall, där varje selector testades mot en fixture med ett känt korrekt facit, plus en avsiktlig feljakt som var gjord för att få Lexbor-motorn att gå sönder. Varje fall kördes i en egen subprocess, vilket visade sig nödvändigt — ett av dem kraschar hela tolken. Resultatet:

| Motor | PASS | FELAKTIGT | EJ STÖDD | PROCESS_ABORT |
|---|---|---|---|---|
| soupsieve | 41 | 0 | 0 | 0 |
| selectolax Lexbor | 39 | 0 | 2 | 0 |
| lxml (cssselect) | 37 | 1 | 3 | 0 |
| parsel (cssselect) | 37 | 1 | 3 | 0 |
| selectolax Modest | 35 | 3 | 2 | 1 |
När de aggressiva selektorerna väl är med i mixen är Lexbor inte den självklara vinnaren — det är soupsieve, med rena 41/41 mot Lexbors 39/41. Lexbors två missar är :lang(en) och :dir(rtl), som den avvisar med parse error. I övrigt klarar den allt, inklusive :has(), :is(), :where() och attribut med skiftlägesokänslighet.
Det som gör Lexbor särskilt starkt är jämfört med cssselect-stacken. README:ns flaggskepps-selektor — div > :nth-child(2n+1):not(:has(a)) — ger korrekt resultat i båda selectolax-motorerna och i soupsieve, men fel resultat i lxml och parsel, utan att något fel kastas. En scraper som kopierar den selektorn till Scrapy eller parsel får alltså tyst felaktiga träffar. För att vara helt precis: cssselect har kunnat parsa :has() sedan version 1.2.0 (2022), och jag testade 1.4.0, så detta är "stöds men utvärderas fel i den sammansatta selektorn", inte "stöds inte". Att den här kombinationen ger tyst fel mängd står inte i cssselects ärendehanterare, som i stället dokumenterar :has()-begränsningarna som explicita fel. Lexbor hanterar också attributflaggan för skiftlägesokänslighet [data-role="LEAD" i], som cssselect avvisar helt.
Två luckor avgör dock migreringar. selectolax stödjer ingen XPath alls — ingen av backends exponerar xpath() — och inga ::text / ::attr()-pseudoelement, eftersom det är en parsel/Scrapy-extension och inte riktig CSS. Om dina nuvarande scrapers lutar sig mot XPath är det den största väggen du stöter på; du måste skriva om selektorerna, inte bara byta bibliotek. Å andra sidan levererar Lexbor en :lexbor-contains("text" i)-pseudoklass för skiftlägesokänslig textmatchning som varken lxml, parsel eller vanlig CSS erbjuder, och den fungerar som dokumenterat.
Robusthet på ful HTML, där selectolax verkligen förtjänar sin plats
Verklig scraping innebär att mata parsern med skräp och hoppas att den inte faller över. Jag körde 18 adversariella indata, och det här är kategorin där selectolax:s argument mot lxml är som starkast.
Ge lxml.html.fromstring en tom sträng eller bara blanksteg så kastar den ParserError("Document is empty"). Båda selectolax-motorerna returnerar i stället ett giltigt, tomt träd. För en scraper som loopar över en lista med URL:er där vissa svar kommer tillbaka tomma betyder det ett try/except mindre runt alltihop. selectolax klarade också 100 000 element utan stack overflow.
Djup nästling gav den tydligaste skillnaden. Vid 1 000 och 5 000 nivåer av nästlade <div>, tappar lxml tyst det djupaste innehållet medan selectolax behåller det. libxml2 begränsar parse-djup runt 256 nivåer och kapar trädet utan fel, så den djupaste texten blir helt enkelt oåtkomlig. Båda selectolax-motorerna returnerar hela trädet. Det är spegelbilden av <template>-fällan jag kommer till strax: där tappar Lexbor innehåll som de andra behåller; här tappar lxml innehåll som selectolax behåller.
Inte varje ruta var en vinst. Modest-backenden avslutar hela Python-tolken med SIGABRT när den stöter på :dir() — inte ett fångbart undantag, utan en hård processkrasch. Det är en verklig robusthetsrisk för den som fortfarande använder legacy-backenden, och exakt den sortens sak som är osynlig tills den fäller ett produktionsjobb klockan tre på natten.
Två tysta dataförlustfällor du bör känna till innan drift
Inget av detta är nytt i sig — båda är dokumenterade uppströms — men båda kan kosta dig verklig data, utan att skrika i README:n.
Lexbor tappar <a> inne i <template>
På den live-MDN-sida jag testade hittade selectolax-Lexbor 497 länkar medan lxml, båda BeautifulSoup-backends och till och med selectolax:s egen Modest-backend hittade 508. De elva som saknades var en språkväxlare och en diskussionslänk som låg inne i <template>-element (sidan använder Lit web components).

Orsaken är legitim: enligt HTML5-specifikationen parsas innehållet i <template> till ett separat, inert fragment, inte till den vanliga DOM:en, och Lexbor följer det strikt — tree.css("a") går inte ned i template-innehåll. lxml, båda BeautifulSoup-backends och Modest plattar ut template-innehållet till huvudträdet, så de hittar de länkarna. Det här är ett dokumenterat öppet problem (selectolax#146, med rotorsaken i lexbor#170), och båda tolkningarna går att försvara — Lexbor är förmodligen mer standardspecifik. Men en utvecklare på den rekommenderade backenden missar ändå datan i tysthet, utan fel. Det omvända är också värt att säga: de andra parserna exponerar inert template-innehåll som en browser aldrig renderar, så de kan ge dig fantomdata som användaren inte kan se. Den säkraste utvägen just för den här typen av sida är Modest-backenden eller ett annat bibliotek.
Icke-UTF-8-bytes förstör .text() i tysthet
Ge selectolax bytes som inte är giltig UTF-8 och parsing lyckas — korruptionen visar sig senare, och det är värre än ett rent fel. På "<p>café éè</p>".encode("latin-1") returnerar Lexbors .text() ersättningssymboler, Modests .text() släpper de felaktiga bytena i tysthet, och båda motorerna kastar bara UnicodeDecodeError när du sedan läser .html. Bindningen avkodar alltså strikt UTF-8 vid återläsning, inte vid parsing. Det här hänger ihop med ett känt selectolax-problem om strikt encode/decode-hantering.
Fixen är en rad och bör sitta i muskelminnet: avkoda bytena själv först — LexborHTMLParser(resp.content.decode("latin-1")) — så returnerar båda motorerna 'café éè' korrekt. I praktiken: ge alltid selectolax en str, aldrig råa icke-UTF-8 bytes. Det här står inte uttryckligen i README:n.
Produktionsdimensioner (enstaka observationer, så tolka dem som riktning)
De här resultaten mätte jag bara en gång, inte över tre körningar, så jag markerar dem som signaler snarare än fastslagna siffror.
Trådskalning är den intressanta. När en 1 MB-sida parsades 48 gånger över fyra trådar visade selectolax ungefär 3,5–3,9x snabbare väggklocka — den empiriska signaturen av ett bibliotek som släpper GIL under C-parsning — medan BeautifulSoup(lxml) blev flera gånger långsammare i trådade körningar, vilket tyder på att arbetet serialiseras av GIL. lxml hamnade däremellan och var otydligt. I den era av free-threading som Python rör sig mot är selectolax:s parallella parsing över trådar, där BeautifulSoup inte gör det, en verklig om än preliminär fördel. Det gäller ett trådantal på en sidstorlek, och mekanismen är en hypotes, inte något jag verifierade genom att instrumentera C-koden.
När det gäller minnesläckor: över 2 000 iterationer av parse–extrahera–släpp på 1 MB visade ingen av de tre parserna den linjära RSS-ökning som en läcka skulle ge — varje verktyg stabiliserade sig i ett begränsat arbetsminnesband. Jag litar på just den observationen eftersom jag körde ett känt läckage-exempel genom samma instrument, och det steg till +198 MB som förväntat, vilket bevisar att instrumentet faktiskt kunde se en läcka och helt enkelt inte hittade någon i parserna. Och ett nodhandtag som hölls vid liv efter att dess träd gått ur scope förblev användbart, utan segfault. Allt detta är enstaka observationer, inget långtidssoak-test.
Var selectolax passar — och var den lämnar över
Allt ovan handlar om en sak: att göra HTML du redan har till strukturerad data, snabbt. selectolax är mycket bra på det. Det den medvetet inte gör är att hämta sidan, rendera JavaScript, rotera proxies, lösa CAPTCHAs eller lista ut vilka element du vill ha. Det är fortfarande din kod. selectolax är parse-lagret, och den låtsas inte vara mer än så.
Det är också gränsen där en hanterad extraktionstjänst ligger ovanpå en parser i stället för att ersätta den. Om du helst slipper bygga och underhålla hela kedjan för fetch–render–anti-bot–extract själv, erbjuder Thunderbit det via API, MCP-server och CLI — POST /distill gör om en sida till ren Markdown och POST /extract returnerar strukturerad JSON som matchar ditt schema, med JS-rendering och anti-bot-hantering inbyggt. Det är ett annat lager i problemet: du väljer selectolax när du redan har HTML:en och vill ha rå parsinghastighet under egen kontroll, och något i stil med Thunderbit:s API, MCP-server eller CLI när du vill att hämtning och extrahering sköts åt dig och bara vill ha strukturerad data tillbaka. Inte ett byte — bara en annan höjd i samma stack.
Testa Thunderbit för webbdatastraktion
Fördelar, nackdelar och vem som faktiskt bör använda det
Här vinner selectolax:
- Cirka 12–17x snabbare än BeautifulSoup i realistiskt parse-och-extrahera-arbete, stabilt över tre storleksordningar av sidstorlek.
- Lätt på minnet i lxml-klassen (ungefär 1,5–1,8x snålare än BeautifulSoup) och med en importtid på cirka 14 ms.
- Tål input som får lxml att falla: tomt innehåll, bara whitespace och patologiskt djupa nästlingar.
- Modern CSS inklusive
:has(),:is(),:where(), attribut med skiftlägesokänslighet och Lexbor-specifika:lexbor-contains(). - En DOM som är None-säker vid läsning och skrivning: saknade element ger
Noneeller[]i stället för undantag, och trädet går faktiskt att mutera och serialisera igen. - Aktivt underhåll (v0.4.10, mitten av 2026) och enkel installation.
Här gör den det inte:
- Inte generellt snabbare än lxml — oavgjort i heltasken, och den förlorar ren parsing i min benchmark.
- Ingen XPath och inga
::text/::attr()— ett hårt stopp för XPath-baserade scrapers. - Två tysta dataförlustfällor:
<template>-innehåll i Lexbor och icke-UTF-8-bytes via.text(). - Modest-backenden är legacy och kan SIGABRT:a på
:dir(). - Alla siffror här kommer från en enda plattform (macOS arm64, Python 3.14) och är preliminära.
Bör du använda selectolax? Ja, om du vill ha lxml-klassens parsninghastighet med ett trevligare, None-säkert API och märkbart bättre beteende på tom och trasig input — och du kan leva i CSS-only-land. Om din kodbas är byggd på XPath är omskrivningskostnaden reell och bör vägas ärligt. Och om du jagar "den absolut snabbaste parsern" är det korrekta svaret från den här benchmarken att selectolax och lxml ligger så nära varandra att avgörandet handlar om ergonomi och robusthet, inte rå fart. Det är ofta en bättre anledning att välja verktyg ändå.
Testa Thunderbit för webbdatastraktion Get Started Free
Vanliga frågor
Är selectolax snabbare än BeautifulSoup?
Ja, tydligt — ungefär 12–17x snabbare än BeautifulSoup(html.parser) och 10–14x snabbare än BeautifulSoup(lxml) i en realistisk parse-och-extrahera-uppgift, stabilt från 1 KB till 10 MB-sidor (macOS arm64, Python 3.14). Den ofta citerade "4–5x"-siffran underskattar gapet mot html.parser.
Är selectolax snabbare än lxml? Inte generellt. I den fulla parse-och-extrahera-uppgiften går de jämt upp vid 100 KB och 1 MB, och selectolax vinner bara på 10 MB-sidan. Vid ren parsing utan query var lxml faktiskt cirka 33–34 % snabbare på min maskin — ett resultat som går emot det vanliga och därför bör verifieras på din egen hårdvara.
Ska jag använda Lexbor- eller Modest-backenden?
Lexbor i nästan alla fall — det är den underhållna, funktionsrika motorn som README rekommenderar, med bättre CSS-täckning. Undantaget är sidor som gömmer innehåll i <template>-element, där Lexbors mer standardskorrekta beteende tappar innehållet medan Modest råkar behålla det. Modest har dessutom skarpa kanter, inklusive en hård interpreterkrasch på :dir().
Stöder selectolax XPath?
Nej. Ingen av backends exponerar metoden xpath() — selectolax är CSS-only. Om dina scrapers är beroende av XPath innebär migreringen att du måste skriva om selektorerna, vilket är den största kostnaden när man byter från en lxml- eller parsel-baserad stack.
Varför blir min selectolax-output konstig eller varför saknas element?
Två vanliga orsaker. Om text kommer tillbaka med ersättningssymboler eller saknade accenter har du troligen skickat råa icke-UTF-8-bytes — avkoda dem till en str först (resp.content.decode("latin-1")) innan parsing. Om länkar eller element saknas på en modern sida kan de ligga inne i <template>-taggar som Lexbor-backenden inte går ned i; byt till Modest eller en annan parser för just den sidan.


