BeautifulSoup é a biblioteca para a qual quase todo mundo corre na primeira vez que faz web scraping de uma página em Python — e ela é, de fato, a mais lenta entre os parsers HTML sérios. As duas coisas são verdadeiras, e nenhuma delas é uma crítica. O interessante é que esse “mais lenta” acaba sendo um número preciso e mensurável, não só uma impressão.
Testei o bs4 (isto é, beautifulsoup4, versão 4.15.0, lançada em junho de 2026, sob licença MIT) com uma combinação de testes funcionais recentes e dados de tempo reaproveitados do mesmo conjunto de benchmark, e o quadro é consistente: você troca uma penalidade de velocidade na ordem de grandeza por uma API extremamente amigável e a maior tolerância a erros do mercado. Se essa troca faz sentido ou não depende totalmente da sua carga de trabalho — então esta análise mantém os dois lados da moeda sempre em foco.
O que o BeautifulSoup realmente é — e o que ele não é
A maioria dos tutoriais pula justamente a parte mais importante: o BeautifulSoup não faz a análise do HTML. Ele é um wrapper. Nos bastidores, ele entrega o seu documento a um entre três parsers de verdade — o html.parser nativo do Python, o lxml ou o html5lib — e depois envolve a árvore resultante numa única API de navegação e busca, muito amigável. O trabalho do bs4 não é “parsear”. É deixar o resultado agradável de explorar.
O próprio autor o descreve como uma “biblioteca de screen scraping”, e a proposta sempre foi essa: apontar para um HTML tão quebrado que faria um navegador torcer o nariz, e ainda assim conseguir extrair os dados pedidos. Essa fama é merecida — com um asterisco que vamos ver adiante.
Antes de qualquer coisa, vale fixar alguns fatos:
| Campo | Valor |
|---|---|
| Pacote | beautifulsoup4 (importado como bs4) |
| Versão testada | 4.15.0 (enviada em 2026-06-07) |
| Requisito de Python | >=3.7.0 |
| Licença | MIT |
| Página oficial | crummy.com/software/BeautifulSoup |
| Código-fonte + rastreador de bugs | Launchpad — não é GitHub |
| Manutenção | Ativa (4.15.0 em junho de 2026, seis releases no último ano) |
Essa observação de “não é GitHub” importa mais do que parece. O bs4 é uma biblioteca com 20 anos de vida, hospedada em crummy.com e no Launchpad — então aquele filtro mental de “quantas estrelas tem no GitHub?” não se aplica aqui. O melhor indicador de saúde é a frequência de releases, e nesse ponto ele está bem vivo.
Há ainda um detalhe importante sobre licenciamento, para quem precisa prestar contas a equipes de compliance: o wrapper é MIT, mas o que exatamente entra na sua árvore de dependências ao usar bs4 depende do backend instalado. html.parser vem da biblioteca padrão do Python (licença PSF, sem dependências extras). lxml é BSD, mas depende de libxml2/libxslt — uma dependência C externa que você compila ou recebe como wheel pré-compilado. html5lib é puro Python e MIT. Se você quer a pegada de dependências mais limpa possível, o html.parser nativo entrega isso — e, por coincidência, também é o backend com a maior ressalva. Já já chegamos lá.
O custo de velocidade, em números
Vamos colocar o número na mesa logo de cara, porque ele é a manchete — esconder isso seria desonesto. Em uma tarefa realista de parse e extração — analisar a string, buscar todos os <h3 class="title"> e todos os <a href> — o BeautifulSoup é o parser mais lento deste comparativo, e nem chega perto dos demais.

Esses tempos foram reaproveitados do benchmark do selectolax (mesma máquina, mesma metodologia de 3 execuções, até 2026-07-13); esta análise não rodou seus próprios benchmarks de tempo para evitar concorrência de CPU e trabalho duplicado. Latência mediana p50, em milissegundos:
| Tamanho da página | bs4 (html.parser) | bs4 (lxml) | selectolax-Lexbor | lxml | bs4-hp mais lento | bs4-lxml mais lento |
|---|---|---|---|---|---|---|
| 1 KB | 0.323 | 0.281 | 0.027 | 0.036 | 12.0x | 10.5x |
| 10 KB | 2.049 | 1.705 | 0.160 | 0.166 | 12.8x | 10.7x |
| 100 KB | 20.599 | 16.540 | 1.464 | 1.423 | 14.1x | 11.3x |
| 1 MB | 232.558 | 181.855 | 14.901 | 14.177 | 15.6x | 12.2x |
| 10 MB | 2788.746 | 2261.557 | 159.935 | 172.933 | 17.4x | 14.1x |
Então o bs4(html.parser) roda cerca de 12 a 17x mais devagar que um parser C como o selectolax-Lexbor, e trocar para o backend lxml só melhora isso para 10.5 a 14x — ainda uma desvantagem de uma ordem de grandeza inteira. O motivo é estrutural, não um bug: independentemente de quem faz o parse, o bs4 cria um objeto Python completo (Tag ou NavigableString) para cada nó. Essa camada de materialização em Python é um custo que os parsers em C simplesmente não pagam.
Repare que o multiplicador cresce conforme as páginas aumentam — 12.0x em 1 KB, 17.4x em 10 MB. Isso mostra que não se trata de um overhead fixo de inicialização que você “dilui” com o tempo. É um custo por nó, que escala linearmente com a quantidade de nós criados.
Agora, a reinterpretação importante: “10x mais lento” soa pior do que costuma ser na prática. Em uma página de 1 MB, isso significa 232 ms contra 15 ms. Se o seu trabalho é “raspar algumas centenas ou alguns milhares de páginas de alguns centenas de KB”, essa diferença absoluta é praticamente irrelevante — você não vai sentir isso, e otimizar isso não traz ganho real. Mas se o seu trabalho é um pipeline com um milhão de páginas, a mesma razão de diferença separa um job que termina de um que nunca acaba. É o mesmo número, mas com veredictos opostos. Compare com o seu volume real, não com o benchmark.
Não, trocar o backend não resolve isso
Existe um mito persistente de que basta passar o backend lxml ao bs4 para obter a velocidade do lxml. Não funciona assim — e vale entender por quê. Em uma consulta CSS em lote com 100.000 nós (selecionando todos os <a> e lendo seus href, com a árvore já construída), a diferença de throughput é clara:
| Parser | Query p50 | Nós/segundo |
|---|---|---|
| lxml | 33.30 ms | 3,002,646 |
| selectolax-Modest | 34.19 ms | 2,924,550 |
| selectolax-Lexbor | 39.46 ms | 2,534,027 |
| bs4 (lxml) | 250.56 ms | 399,111 |
O bs4(lxml) chega a cerca de 399 mil nós/segundo — aproximadamente 6.3 a 7.5x mais lento que os três motores em C, mesmo usando o lxml como backend. O backend acelera a construção da árvore. Mas a consulta e a navegação continuam passando pelo soupsieve e pelos objetos Tag do bs4, e cada nó selecionado ainda precisa ser encapsulado em Python. Ou seja: a ideia de “usar lxml no bs4 para ficar rápido como lxml” está errada. O backend acelera uma fase, mas a fase mais lenta não é essa.
Memória e tempo de cold start completam o custo. Em um documento de 10 MB, o bs4 usa cerca de 1.5 a 1.75x mais memória residente que selectolax ou lxml (218–226 MB contra 129–145 MB) — mesma causa raiz: um objeto Python por nó. E importar bs4 leva cerca de 33.4 ms contra 14.1 ms para lxml.html, então ele é 2.36x mais lento para importar. Esse último ponto é irrelevante em um processo de longa duração, mas em uma CLI ou função serverless que faz cold start o tempo todo, é um custo pequeno, porém real, que vale conhecer.
Por que mais threads não vão salvar você
Se o seu instinto para uma tarefa lenta e CPU-bound é “joga threads nisso”, o bs4 vai punir esse instinto. Em uma página de 1 MB analisada 48 vezes, comparando execução em thread única contra quatro threads:
| Parser | 1 thread | 4 threads | Speedup |
|---|---|---|---|
| selectolax-Lexbor | 0.563 s | 0.159 s | 3.54x |
| lxml | 0.459 s | 0.378 s | 1.21x |
| bs4 (lxml) | 6.945 s | 26.842 s | 0.26x |
Leia a última linha duas vezes. Quatro threads deixaram o bs4 cerca de 3.9x mais lento, não mais rápido. O sinal empírico é que ele provavelmente “segura o GIL”: a construção da árvore no bs4 é pura Python, então tudo serializa sob o Global Interpreter Lock, e encher o processo de threads só adiciona overhead de escalonamento a um trabalho que não pode realmente rodar em paralelo. O selectolax ganha cerca de 3.5x porque seu núcleo em C libera o lock; o bs4 não tem essa folga.
Para a era do free-threading, a conclusão prática é esta: se você precisa paralelizar BeautifulSoup, use multiprocessing (ProcessPoolExecutor), não threads. selectolax e lxml podem escalar em threads; o bs4 não. Uma observação de rigor: isso é uma única medição com um único número de threads (4) em uma única página (1 MB), e o mecanismo de “segura o GIL” é uma hipótese inferida pelo tempo de execução, não algo que eu tenha confirmado instrumentando o código para ver exatamente qual trecho mantém o lock. A tendência é clara; o mecanismo exato ainda é provisório.
O backend padrão é a armadilha. Leia isto primeiro.
Se você levar só uma coisa desta análise, leve esta. Um BeautifulSoup(html) simples, sem segundo argumento, usa html.parser, e html.parser não implementa as regras de optional-end-tag do HTML5. Isso soa acadêmico até corromper seus dados de forma silenciosa.

Executei 15 exemplos de HTML propositalmente malformado em todos os três backends, com uma asserção estrutural agnóstica ao backend pré-registrada para cada caso antes da execução (para ninguém poder escolher o vencedor depois de ver o resultado). As pontuações:
| Backend | Atende à expectativa / 15 |
|---|---|
| lxml | 15 |
| html5lib | 15 |
| html.parser | 12 |
As três falhas têm a mesma causa raiz. Pegue uma tabela sem fechamento: <table><tr><td>a<td>b<tr><td>c<td>d</table>. No html.parser, o texto das células extraído sai como ['abcd','bcd','cd','d'] — cada <td> engole tudo o que vem depois, porque o parser aninha as células em vez de fechá-las. O lxml e o html5lib retornam corretamente ['a','b','c','d']. Itens de lista sem fechamento se comportam do mesmo jeito: <li>a<li>b<li>c gera ['abc','bc','c'] aninhado no html.parser, e o limpo ['a','b','c'] nos outros dois. Atributos duplicados também divergem — <div id="first" id="second"> mantém "second" no html.parser, mas fica com "first" no lxml/html5lib, e o padrão HTML5 diz para preservar o primeiro.
O motivo de isso ser perigoso, e não só irritante, é que acontece sem lançar erro. Um scraper que usa BeautifulSoup(html) no automático e encontra uma tabela ou lista sem fechamento — algo tristemente comum em sites antigos, HTML escrito à mão e templates que esqueceram uma tag final — vai misturar texto de células adjacentes em um único campo, entregar dados sujos e nunca reclamar. A correção é um argumento: BeautifulSoup(html, "lxml") ou BeautifulSoup(html, "html5lib").
Para ser justo com html.parser, os outros 12 dos 15 exemplos malformados saíram idênticos nos três backends — tags mal aninhadas como <b><i></b></i>, ausência das estruturas html/body, atributos sem aspas, tags de fechamento órfãs, comentários não fechados, forms aninhados, mistura de maiúsculas e minúsculas e mais. A tolerância do bs4 é realmente forte no geral; a divergência fica concentrada quase toda na família de optional-end-tag. E nada disso é uma descoberta nova — a própria documentação do bs4 sobre “Differences between parsers” já diz, em linguagem simples, que o html.parser é “less permissive”. O que a matriz de HTML malformado acrescenta são os casos específicos e reproduzíveis em que “less permissive” vira resultado errado.
O que você não perde: a API e o CSS são a melhor parte
Então o bs4 é lento, não se beneficia de threads e ainda tem a armadilha do backend padrão. Mesmo assim, muita gente continua escolhendo essa biblioteca — porque a metade “amigável” dessa troca é totalmente real, e os testes confirmam isso.

Rodei 29 testes de API cobrindo busca, CSS, navegação na árvore, extração de texto e modificação do DOM. Os 29 passaram, e o resultado de cada teste foi calculado comparando o retorno real com um valor esperado — não por inspeção visual. Duas dessas capacidades são ergonomias que os parsers em C simplesmente não oferecem:
- Predicados por função em
find/find_all. Você pode escreversoup.find(lambda t: t.name == "a" and "btn" in t.get("class", []))e expressar uma condição complexa em uma única linha de Python — sem o truque de “seleciona tudo e depois filtra”. - Navegação nomeada e bidirecional na árvore.
.parent,.next_sibling,.find_parent,.stripped_strings,.descendants— as travessias são legíveis, quase em linguagem natural, e funcionam nas duas direções. O selectolax exige mais etapas em alguns casos, ou simplesmente não oferece o recurso.
Essa é a metade em que o bs4 “economiza tempo de desenvolvimento”. Não é marketing; são 29 verificações verdes.
Dois pontos de atenção, porque uma boa análise precisa apontar os dois lados. Primeiro, atributos booleanos: <input disabled> retorna a string vazia "" para disabled no bs4 (selectolax retorna None). Ambas são falsy, então if node.get("disabled") pode ignorar silenciosamente um atributo booleano que de fato existe em qualquer uma das bibliotecas — o teste seguro é "disabled" in tag.attrs. Segundo, get_text(strip=True) concatena o texto dos nós sem separador depois de remover espaços, então "...with " + "link1" vira "withlink1". Passe separator=" " quando precisar preservar fronteiras entre palavras. Nenhuma dessas armadilhas é exclusiva do bs4; as duas aparecem em várias bibliotecas.
E agora a parte que surpreende muita gente: escolher bs4 não faz você perder cobertura de CSS. O motor de CSS dele, o soupsieve, é a implementação mais completa de todo este comparativo. Na matriz base de 41 casos (reaproveitada do benchmark do selectolax), o soupsieve marcou 41/41 — a única pontuação perfeita do conjunto, à frente do selectolax-Lexbor com 39/41 e do cssselect (lxml/parsel) com 37/41. Depois, testei mais 20 casos estendidos que a documentação do soupsieve destaca, e ele foi 20/20, incluindo seletores que o Lexbor rejeita diretamente: :lang(en), o :-soup-contains('featured') exclusivo do soupsieve, :is(), :where() e :has(> a). As únicas lacunas reais são XPath (soupsieve é apenas CSS) e os pseudo-elementos ::text / ::attr() do parsel, que são extensões do Scrapy. Se você vive em XPath, essa migração vai doer.
O veredito desta seção é direto: o que você sacrifica ao escolher BeautifulSoup é velocidade. Não é ergonomia de API — e certamente não é cobertura de CSS.
Dois problemas de produção que merecem orçamento
Além do backend padrão, dois comportamentos vão morder você, especialmente em workloads longos ou fora de UTF-8.
Ciclos de referência: chame decompose() em loops longos
Cada Tag do bs4 mantém uma referência ao pai e aos filhos, formando um ciclo de referência. O reference counting do CPython não consegue remover ciclos sozinho — essa é a função do garbage collector geracional. Para medir o quanto isso importa, eu construí e destruí uma árvore 300 vezes com o GC desligado e depois contei quantos objetos Tag ainda estavam vivos na memória:

| Cenário | Tags retidas após del |
|---|---|
| GC desligado | 120,900 (300 ciclos, nada recolhido) |
| GC ligado | 26,598 (o GC geracional entrou no meio do loop) |
Após gc.collect() forçado | 0 (tudo recolhido) |
| Controle sem ciclo (lista de strings, GC desligado) | delta 0 |
Com o GC desligado, del soup não liberou nada — todos os 120.900 objetos continuaram residentes, porque o ciclo de referências derrota o reference counting. Um único gc.collect() eliminou todos eles. O grupo de controle sem ciclo (uma lista simples de strings, conhecida por não formar ciclo) ficou com delta zero, o que prova que o acúmulo veio do ciclo do bs4, e não de ruído de medição. A própria documentação do bs4 diz que os objetos são “densely interconnected ... exactly the sort a garbage collector would have trouble with”, então esse é um comportamento documentado; o teste aqui acrescenta a contagem de objetos retidos e a prova de que collect() zera tudo.
A regra prática é simples: em um pipeline que analisa muitas páginas grandes em loop apertado, se o seu código — ou alguma configuração de alta vazão — desativa o GC ou não o aciona com frequência suficiente, as árvores do bs4 vão permanecer na memória e o consumo vai subir. Chame soup.decompose() após cada página — o bs4 oferece isso justamente para quebrar o ciclo e liberar memória cedo. As árvores em C do selectolax e do lxml não têm esse problema.
Codificação: UnicodeDammit é a vantagem silenciosa do bs4
O bs4 traz um componente que os parsers rápidos não têm: UnicodeDammit, que identifica a codificação de um documento e o converte automaticamente para Unicode. Eu o submeti a uma matriz de 8 casos de “codificação declarada versus real”:

| Caso | Codificação real | UnicodeDammit inferiu | Recuperado? |
|---|---|---|---|
| utf8_no_decl | utf-8 | utf-8 | Sim |
| utf16_bom | utf-16 | utf-16le | Sim |
| gbk_chinese | gbk | gb18030 | Sim (superconjunto) |
| shiftjis | shift_jis | cp932 | Sim (superconjunto) |
| latin1_declared_utf8 | latin-1 (declarado utf-8) | iso-8859-1 | Sim (ignorou a mentira) |
| latin1_no_decl | latin-1 | cp720 | Não |
| cp1252_no_decl | cp1252 | cp862 | Não |
| utf8_declared_latin1 | utf-8 (declarado latin-1) | iso-8859-1 | Não (seguiu a mentira) |
Cinco de oito foram recuperados. UTF-8, UTF-16 com BOM, GBK, Shift-JIS e até latin-1 rotulado incorretamente voltaram corretamente, e os palpites por superconjunto (GBK→gb18030, Shift-JIS→cp932) continuam decodificando sem problemas. Os dois modos de falha merecem atenção: amostras curtas em latin-1/cp1252 são frequentemente confundidas com code pages DOS, porque o detector estatístico não é confiável em entradas pequenas e os caracteres de desenho de caixas de DOS se sobrepõem aos pontos de código do Latin-1; e, quando a declaração <meta charset> simplesmente está errada, o UnicodeDammit confia nela. A documentação do bs4 aponta ambos os casos — uma amostra pode ser “so short that Unicode, Dammit can't get a lock on it”, e mais dados geram uma inferência melhor.
Em comparação com o selectolax, que corrompe silenciosamente bytes fora de UTF-8 e espera que você faça a decodificação por conta própria, essa é uma vantagem real: o bs4 pelo menos tenta inferir e muitas vezes acerta. Mas não é garantia. Se a codificação for conhecida, dispense a adivinhação e seja explícito: BeautifulSoup(bytes, from_encoding="...").
Os backends realmente divergem em páginas reais?
A matriz de HTML malformado mostra os backends divergindo em entrada propositalmente quebrada. A próxima pergunta óbvia é se isso acontece no mundo real — então rodei os três backends em 11 páginas reais capturadas: BBC, Wikipedia, Craigslist, MDN, old.reddit, documentação do Python, Hacker News, Books to Scrape, webscraper.io, whitehouse.gov e uma página de citações renderizada por JS — comparando contagens de links, títulos e imagens.
Todos os três concordaram em todas as 11 páginas. Divergência zero. Isso significa que a diferença entre backends vista na seção da armadilha só aparece em HTML propositalmente malformado; quando um site de produção moderno é bem estruturado — mesmo que “bagunçado” — a escolha do backend não muda o que você extrai. A leitura prática é: para sites mainstream e bem formados, html.parser é perfeitamente aceitável e ainda economiza dependência. Só quando você estiver raspando HTML visivelmente fora do padrão, escrito à mão ou muito antigo, a escolha do backend passa a alterar o resultado — e aí vale mudar para lxml ou html5lib.
Uma observação desse teste, porque é um caso-limite real. A página da MDN contém um elemento <template>, e todos os backends do bs4 retornaram 508 links — ou seja, o bs4 achata o conteúdo de <template> dentro da árvore principal. Isso coloca o bs4 do mesmo lado que o lxml e do lado oposto ao selectolax-Lexbor, que segue estritamente a especificação HTML5 (um <template> é um DocumentFragment inerte) e retorna 497, omitindo silenciosamente os 11 links dentro do template. Então o bs4 captura dados dentro de <template> — útil, mas também uma forma de coletar conteúdo “fantasma” que o navegador nunca renderizaria. Nenhum dos comportamentos está errado; são interpretações diferentes da especificação, e você precisa saber qual delas está recebendo.
Onde o BeautifulSoup se encaixa — e onde não se encaixa
Em vez de condensar tudo em uma nota única de 0 a 100 — o que esconderia justamente as trocas mais importantes —, aqui está a avaliação por dimensão, com uma ressalva em cada linha:
| Dimensão | O que os testes encontraram | Observação para o leitor |
|---|---|---|
| Instalação / primeira execução | Wrapper puro, sem browser/configuração; html.parser sem dependências; wheels pré-compiladas | O backend lxml precisa de uma dependência C |
| Velocidade vs parsers em C | 12–17x mais lento (html.parser) / 10.5–14x (backend lxml), em todos os tamanhos | Um único ambiente; dados do selectolax reaproveitados |
| Throughput de consulta CSS | ~6–7.5x mais lento em 100k nós; backend lxml não salva | Reaproveitado; paga o custo dos objetos Python Tag |
| Memória | 1.5–1.75x selectolax/lxml; o mais pesado | Reaproveitado; medido por RSS |
| Cold start de importação | 2.36x mais lento (33.4 vs 14.1 ms) | Reaproveitado; item pequeno |
| Escalonamento com threads | bs4-lxml ~3.9x mais lento com 4 threads (segura o GIL) | Uma observação; use multiprocessing |
| Ergonomia da API | 29/29 testes; find com predicado-função + navegação bidirecional | Armadilhas: atributo booleano como string vazia e strip sem separador |
| Cobertura CSS | soupsieve é o mais completo: 41/41 base + 20/20 estendidos; suporta :lang | Sem XPath, sem ::text |
| Tolerância entre 3 backends | lxml/html5lib 15/15; html.parser 12/15 | Divergência só em HTML malformado |
| Consistência em páginas reais | 3 backends concordam em 11/11; todos achatam <template> (508) | Em sites bem formados, o backend não importa |
| GC de ciclos de referência | A árvore forma um ciclo; 300 loops retiveram 120.900 objetos, collect() zerou tudo | Loops longos precisam de decompose() |
| Codificação | UnicodeDammit recupera 5/8; erra amostras curtas e segue declarações ruins | Uma única observação |
| Manutenção | Ativo (4.15.0, junho de 2026); MIT | Hospedado em crummy/Launchpad, não no GitHub |
Então, para quem o BeautifulSoup serve? Para quem valoriza uma API legível e um parse tolerante mais do que throughput bruto, trabalhando em volume moderado — protótipos, scraping pontual, ferramentas internas, equipes em que o tempo do desenvolvedor custa mais do que o tempo de execução. Quem deveria procurar outra coisa? Pipelines de milhões de páginas, em que o custo de velocidade se acumula em dinheiro de verdade; workloads que precisam de paralelismo por thread; e qualquer pessoa presa a XPath.
Uma nota sobre onde isso se encaixa em uma stack real de scraping — e onde nossa própria ferramenta entra. O BeautifulSoup assume que você já tem o HTML. Ele não faz fetch, não renderiza JavaScript e não resolve defesas anti-bot ou CAPTCHAs — isso é um trabalho separado, e genuinamente difícil na web moderna. É aí que uma API de scraping com IA entra em outra camada: a stack para desenvolvedores do Thunderbit — uma API REST, um servidor MCP e uma CLI — cuida do fetch, da renderização JS e do problema anti-bot, e devolve Markdown limpo (POST /distill) ou JSON estruturado compatível com um schema (POST /extract) sem que você precise escrever seletores. Os dois não competem; se complementam. O bs4 faz parse do HTML que você já tem; a API, MCP e CLI do Thunderbit conseguem o HTML que você não alcança facilmente em primeiro lugar. Se o seu gargalo é o parse, bs4 é uma boa resposta. Se o gargalo é a aquisição, isso é outra camada.
Experimente o Thunderbit para extração de dados da web
Em resumo
O BeautifulSoup entrega a API mais amigável, a maior tolerância a HTML malformado e o motor CSS mais completo deste comparativo — tudo isso comprado com um custo de velocidade na ordem de grandeza e o maior consumo de memória. Essa é a troca inteira, dita de forma direta. O backend padrão html.parser é a verdadeira armadilha: ele corrompe silenciosamente tabelas e listas sem fechamento, então passe "lxml" ou "html5lib" sempre que a entrada puder estar feia. Threads não aceleram isso — multiprocessing, sim. E, em loops longos, use decompose() em cada página para evitar o acúmulo de ciclos de referência.
Duas limitações finais. Tudo aqui foi medido em uma única plataforma (macOS arm64, Python 3.14, wheels pré-compiladas), e os multiplicadores de tempo foram reaproveitados do benchmark do selectolax (mesmo ambiente, até 2026-07-13), e não rodados novamente — então eles herdam essa limitação de plataforma única, e uma configuração Linux x86_64 ou compilada a partir do código-fonte pode alterar os números exatos. E nada nestes resultados é uma descoberta inédita: o bs4 é uma biblioteca com 20 anos, então tudo o que foi testado já é documentado ou publicamente conhecido. O valor não está no furo. Está em colocar números reais em trocas que a documentação só descreve em termos qualitativos.
Perguntas frequentes
O BeautifulSoup é lento?
Sim, e isso é mensurável. Em uma tarefa de parse + extração, ele roda cerca de 12 a 17x mais devagar que um parser em C como o selectolax-Lexbor com o backend padrão html.parser, e 10.5 a 14x mais devagar com o backend lxml, porque cria um objeto Python para cada nó. Se isso importa ou não depende da escala: em uma página de 1 MB são 232 ms contra 15 ms — irrelevante para alguns milhares de páginas, mas decisivo para um pipeline de um milhão de páginas.
Qual parser do BeautifulSoup devo usar — html.parser, lxml ou html5lib?
Para sites mainstream e bem formados, o html.parser padrão é suficiente e não adiciona dependências. Mas ele não implementa as regras de optional-end-tag do HTML5, então em tabelas ou listas sem fechamento ele mistura texto adjacente sem erro. Quando a entrada puder estar malformada, escrita à mão ou antiga, passe "lxml" ou "html5lib" explicitamente — ambos fizeram 15/15 em uma matriz de HTML malformado, enquanto o html.parser ficou em 12/15.
O BeautifulSoup consegue processar em paralelo com threads?
Não. A construção da árvore no bs4 é pura Python e segura o GIL, então adicionar threads o deixa mais lento, não mais rápido — nos testes, quatro threads processaram uma página de 1 MB cerca de 3.9x mais devagar do que uma thread. Para paralelizar o bs4, use multiprocessing (ProcessPoolExecutor). Bibliotecas com núcleo em C, como selectolax e lxml, são as que realmente se beneficiam de paralelismo por thread.
O BeautifulSoup lida bem com HTML quebrado?
De forma geral, sim — em uma variedade de amostras malformadas (tags mal aninhadas, estrutura faltando, atributos sem aspas e mais), os três backends recuperaram bem. O ponto fraco é o html.parser padrão e os optional-end-tags: <td>/<li> sem fechamento ficam aninhados em vez de fechados, corrompendo o texto extraído. Troque para o backend lxml ou html5lib e essa classe de problema desaparece.
BeautifulSoup vs lxml — qual é melhor?
São ferramentas diferentes. O lxml é muito mais rápido tanto para construir a árvore quanto para fazer consultas, e suporta XPath. O BeautifulSoup encapsula o lxml (entre outros) em uma API muito mais amigável e, de fato, oferece cobertura CSS mais ampla via soupsieve. Só não espere que usar o backend lxml torne o bs4 tão rápido quanto o lxml — o backend só acelera o parse, enquanto as consultas e navegações ainda pagam o custo dos objetos Python por nó do bs4, deixando-o cerca de 6 a 7.5x mais lento em seleções em lote grandes.
Experimente o Thunderbit para extração de dados da web Get Started Free


