PyQuery coloca uma API no estilo jQuery sobre o lxml. Em cinco tamanhos de página, de 1 KB a 10 MB, todas as cinco medianas exibidas ficaram abaixo das do lxml puro, incluindo uma diferença de 1,5% no maior tamanho. O benchmark não prova que o wrapper seja mais rápido; ele não encontrou diferença grande o bastante para mudar esta decisão entre selecionar e ler.
Também ficou dentro de poucos por cento do selectolax a partir de 10 KB nas medianas exibidas. Sem uma margem de equivalência definida מראש, isso é um resultado próximo, não um empate estatístico.
O que é o PyQuery
PyQuery é uma biblioteca Python que oferece a API de seleção e encadeamento do jQuery sobre uma árvore de documentos lxml. Versão testada: 2.1.0, licença BSD, 2.380 estrelas no GitHub, 59 issues abertas, último push em 2026-07-27.
Referência oficial: documentação do PyQuery.

from pyquery import PyQuery as pq
d = pq(html)
titles = [e.text_content() for e in d("h3.title")]
hrefs = [e.get("href") for e in d("a")]
Os elementos retornados são elementos lxml, então tudo o que você sabe fazer com lxml continua funcionando. Esse é o objetivo do projeto: PyQuery é uma camada de ergonomia, não um parser. pip install pyquery instala 3 pacotes — lxml, cssselect e o próprio PyQuery — e ocupa 20,1 MiB, quase tudo vindo das extensões compiladas do lxml.
Se você já usou cheerio no Node, a ideia geral da API é a mesma em Python. Os dois seletores testados aqui funcionaram em ambos; o teste não comprova paridade completa da linguagem de seleção entre cssselect e cheerio.
A medição
Esta base de pesquisa já tinha um benchmark de parser com uma propriedade que a maioria não tem: uma etapa de paridade que calcula hash do conteúdo extraído — títulos ordenados mais hrefs ordenados — contra um parser de referência, para que uma biblioteca que pule trabalho não possa registrar um tempo artificialmente rápido. Cinco tamanhos de página, 50 iterações, três execuções independentes.
Adicionar PyQuery a isso exigiu duas coisas.
Executar novamente a referência. selectolax rodou de novo no mesmo processo. O hash do conteúdo foi reproduzido em 5 de 5 tamanhos e o p50 ficou entre 0,989× e 1,079× do valor publicado — portanto, é a mesma máquina e o mesmo ambiente de teste.
Rodar lxml no mesmo processo também. O benchmark publicado registra a máquina e a versão do Python, mas não as versões das bibliotecas, então a linha de lxml poderia ter vindo de uma versão diferente daquela que o PyQuery encapsula aqui. Comparar atravessando essa diferença teria comparado duas versões de lxml e chamado isso de custo do wrapper. Executar lxml junto elimina a dúvida — ambos estão em lxml 6.1.1 neste ambiente virtual.
| Tamanho da página | selectolax | PyQuery | lxml | PyQuery vs lxml |
|---|---|---|---|---|
| 1 KB | 0,0286 ms | 0,0456 ms | 0,0508 ms | 0,90× |
| 10 KB | 0,1725 ms | 0,1728 ms | 0,1802 ms | 0,96× |
| 100 KB | 1,4855 ms | 1,4093 ms | 1,4145 ms | 1,00× |
| 1 MB | 14,97 ms | 14,96 ms | 15,03 ms | 1,00× |
| 10 MB | 158,10 ms | 162,86 ms | 165,25 ms | 0,99× |
p50 em milissegundos, mediana de três execuções, tudo no mesmo processo. parser-bench.json. Todos os três hashes de conteúdo corresponderam à referência em todos os tamanhos.
O custo do wrapper que não apareceu

PyQuery ficou no mesmo nível ou abaixo do lxml puro em todas as medianas exibidas. Isso não é prova de que um wrapper torne o parsing mais rápido. Três medianas de execução e nenhuma margem de equivalência predefinida sustentam apenas a conclusão mais restrita de que, neste fixture, não houve overhead de seletor relevante para a decisão.
Em 10 MB, as três execuções do PyQuery foram 162,86, 163,17 e 161,13 ms; as do lxml foram 169,46, 165,25 e 164,18. Os intervalos são próximos, mas não se sobrepõem. Em 1 MB, as duas medianas diferem em 0,5%. Essas execuções pequenas sustentam uma avaliação prática, não uma afirmação estatística de equivalência.

O mecanismo é simples: pq(html) constrói a árvore lxml uma única vez, d("h3.title") compila o seletor CSS via cssselect, como tree.cssselect() faz, e os elementos devolvidos são elementos lxml. Há pouco trabalho do PyQuery nesse caminho quente medido. Traversal, manipulação, consultas repetidas, importação e memória ficam fora da afirmação sobre tempo de seleção.
Resultados próximos a partir de 10 KB
A descoberta mais útil é a primeira coluna.
A partir de 10 KB, a diferença entre o mais rápido e o mais lento entre selectolax, lxml e PyQuery foi de 4,5% em 10 KB, 5,4% em 100 KB, 0,5% em 1 MB, e 4,5% em 10 MB. Esta execução não foi um teste de equivalência; o julgamento prático é que essas diferenças não mudariam a maioria das escolhas de parser para esta carga de trabalho.
selectolax é de fato mais rápido em 1 KB — 0,0286 ms contra 0,0456 e 0,0508 — mas essa linha não serve para ranqueamento. Entre os três parsers, a variação nesse tamanho é de 77,6%, e as três execuções do próprio selectolax variaram de 0,0267 a 0,0404 ms. Em 28 microssegundos, o timer e o escalonador dominam. Eu não classificaria nada ali.
Para esta carga de trabalho de seleção e leitura, escolha entre esses três com base na API e nos fatos medidos de dependência, e não por uma hierarquia presumida de velocidade. PyQuery não mostrou penalidade relevante em relação ao lxml. selectolax usa uma pilha de parsing diferente, mas este artigo não mediu seu footprint instalado, cobertura de wheels ou requisitos de build no mesmo critério.
Para contraste, o benchmark publicado colocou mais duas opções Python no mesmo fixture de 10 MB, e elas são as que realmente diferem:
| Parser (10 MB) | p50 |
|---|---|
| selectolax (lexbor) | 159,93 ms |
| lxml | 172,93 ms |
| parsel | 231,85 ms |
| selectolax (modest) | 247,95 ms |
| BeautifulSoup + lxml | 2.261,56 ms |
| BeautifulSoup + html.parser | 2.788,75 ms |
Valores publicados de bench_parse.json.
As linhas históricas do benchmark colocam o BeautifulSoup mais de uma ordem de grandeza acima das medianas dos parsers mais rápidos neste fixture. Essas linhas não foram repetidas com o par PyQuery/lxml no processo atual, então servem como contexto, não como multiplicador controlado para o veredito principal.
A linha armazenada do cheerio foi 2.927,89 ms (2927.8857 em parser-bench.json) com hashes de conteúdo extraído correspondentes. Esse resultado entre runtimes também depende do Node, das versões dos pacotes e dos controles da execução histórica; não deve ser lido como um multiplicador isolado de velocidade da biblioteca.
Realidade de configuração
| Biblioteca | Pacotes | Disco | Licença | Estrelas | Último push |
|---|---|---|---|---|---|
| PyQuery | 3 | 20,1 MiB | BSD | 2.380 | 2026-07-27 |
| cheerio (Node) | 22 (npm) | 9,0 MiB | MIT | 30.449 | 2026-08-11 |
Referência oficial: PyQuery no PyPI.
Três pacotes é uma pegada de dependência enxuta, e dois deles — lxml e cssselect — já fazem parte de muitos projetos Python de scraping. Nesse caso, o custo marginal do PyQuery é de apenas algumas dezenas de kilobytes.
Os 20,1 MiB são das extensões compiladas do lxml, não do PyQuery. É o mesmo custo de cerca de 20 MiB que você paga para usar lxml diretamente.
A biblioteca tem licença BSD. No snapshot datado, havia 59 issues abertas e um push três semanas antes do teste; essas observações, sozinhas, não provam qualidade de manutenção nem compatibilidade futura.
Memória, e o que HTML quebrado faz com ela
Duas coisas que todas as avaliações deste lote listavam como não testadas agora foram medidas.
O contexto mais amplo do teste de estresse está na comparação de memória e HTML malformado entre dez bibliotecas.
Memória residente máxima, via /usr/bin/time -l, um processo novo por célula — o piso de importação é o custo da biblioteca carregada e ociosa; os picos incluem o documento.
| Biblioteca | Runtime | Piso de importação | Pico 226 KB | Pico 10 MB |
|---|---|---|---|---|
| html2text | python3.14 | 18,7 | 19,9 | 71,2 |
| pyquery | python3.14 | 30,3 | 33,9 | 172,5 |
| resiliparse | python3.14 | 20,5 | 25,1 | 225,1 |
| markdownify | python3.14 | 23,9 | 28,9 | 278,5 |
| goose3 | python3.14 | 44,1 | 52,4 | 398,5 |
| cheerio | node22 | 66,8 | 76,5 | 398,5 |
| justext | python3.14 | 30,3 | 36,6 | 431,2 |
| newspaper4k | python3.14 | 52,6 | 61,8 | 668,5 |
| trafilatura | python3.14 | 52,5 | 64,8 | 927,1 |
| turndown | node22 | 47,8 | 68,4 | 2947,1 |
memory-results.json. Os baselines de Python e Node não são comparáveis entre si; o interpretador está dentro dos dois.
PyQuery é mais leve que resiliparse no documento grande — 172,5 MiB contra 225,1 — apesar de ter um piso de importação maior. A árvore do lxml é compacta, e a maior parte dos 30,3 MiB do piso do PyQuery vem do carregamento do lxml, não de algo que o PyQuery faça.
HTML quebrado. Doze documentos, cada um quebrando exatamente uma coisa — tags não fechadas, elementos inline mal aninhados, atributos sem aspas com espaços, fechamentos soltos, ausência total de <html>, atributos duplicados, documento truncado no meio de uma tag, entidades inválidas, um <script> não fechado, uma declaração de charset mentirosa, um comentário contendo marcação e 600 níveis de aninhamento — mais dois controles bem formados em tamanhos correspondentes, porque “não retornou nada” só diz algo sobre malformação se a biblioteca também não ficar muda num documento limpo do mesmo tamanho.
pyquery lançou exceção em 0 de 14 e não retornou nada em 1, recuperando 10/22 marcadores sentinela nos arquivos quebrados (malformed-results.json). Um arquivo é excluído dessa conta: segundo HTML5, tudo depois de um <script> não fechado é conteúdo de script, então perder isso ali está correto e recuperá-lo seria a divergência. Sem os resultados dos sentinelas das alternativas diretas lado a lado, 10/22 é uma observação de robustez, não um ranking de seleção de parser.
Prós e contras
A favor. Sintaxe jQuery, familiar para quem escreveu JavaScript de front-end ou usou cheerio. Não apareceu overhead de seletor relevante em relação ao lxml puro neste fixture. Apenas 3 pacotes, e 2 deles provavelmente já estão na sua árvore. Ele retorna elementos lxml, então as técnicas do lxml continuam disponíveis. BSD. Os hashes de conteúdo corresponderam à referência em todos os cinco tamanhos.
Contra. 20,1 MiB, por causa do lxml. 2.380 estrelas significam uma comunidade bem menor que a do cheerio, com 30.449 — menos exemplos práticos quando algo sai do padrão. É uma camada de conveniência, então tudo o que o lxml não faz, ele também não faz. E, se você esperava que a API do jQuery trouxesse performance, não traz: ela traz ergonomia, e o parser por baixo é quem faz o trabalho.
Quem deve usar e quem não deve
Use PyQuery se você ou seu time preferem seletores no estilo jQuery em Python. A construção medida, as duas seleções e as leituras não mostraram penalidade relevante em relação ao lxml; outras operações do PyQuery não foram medidas.
Use lxml diretamente se você prefere XPath ou quer um pacote a menos. Esta execução não mostrou motivo de velocidade de seletor para escolher entre eles.
Avalie selectolax se a API do parser e a pilha de dependências combinarem com seu projeto. A linha de 1 KB está explicitamente sem classificação, e este artigo não sustenta uma alegação de “menor conjunto de dependências”.
No Node, cheerio é a forma de API equivalente. As linhas históricas entre runtimes foram mais lentas aqui, mas as diferenças entre runtime e execução histórica impedem uma conclusão limpa apenas sobre a biblioteca.
Onde uma API gerenciada faz sentido
PyQuery faz parsing do HTML que você já tem. Ele não busca páginas, não renderiza JavaScript e não lida com camadas anti-bot — nenhum parser nesta comparação faz isso, e, em muitos alvos reais, essa é a metade mais difícil.
Nota do autor: Thunderbit é nossa opção gerenciada para busca/renderização por URL e extração. Ela não foi comparada com PyQuery aqui. A fronteira relevante é: você já tem o HTML e quer seletores locais, ou quer aquisição da página e extração operadas como serviço.
A formulação honesta é: se você já tem o HTML e conhece seus seletores, PyQuery é gratuito e agradável de usar. Se os seletores continuam quebrando, ou se você está extraindo em escala, isso é uma compra diferente.
Para o panorama mais amplo, nosso resumo de APIs de web scraping cobre opções hospedadas, e o guia principal de scrapers open source cobre as opções auto-hospedadas. Se a saída parseada alimentar um modelo, converter HTML em Markdown em Python é onde a fidelidade costuma se perder.
Experimente Thunderbit para Extração de Dados Web
Vale a pena usar PyQuery?
Sim, se você quer sintaxe no estilo jQuery em Python e o caminho medido de seleção/leitura representa sua carga de trabalho.
O benchmark não encontrou overhead de seletor relevante contra o lxml, mantendo a paridade de hash do conteúdo. Ele não provou custo zero para a biblioteca como um todo.
Em cinco tamanhos, as três medianas dos parsers Python ficaram próximas o suficiente para que o encaixe da API provavelmente pese mais nesta tarefa. Defina uma margem de equivalência e rode as alternativas exatas novamente antes de transformar esse julgamento em um ranking mais amplo de parsers.
Experimente Thunderbit para Extração de Dados Web Get Started Free
Perguntas frequentes
PyQuery deixa o lxml mais lento?
Não apareceu overhead de seletor relevante nesta execução. Em cinco tamanhos de página, as medianas ficaram iguais ou abaixo das do lxml puro, ambos usando lxml 6.1.1 no mesmo processo. Em 10 MB, os intervalos próximos não se sobrepuseram: PyQuery 161,13–163,17 ms e lxml 164,18–169,46 ms. pq(html) constrói uma árvore lxml, e os seletores testados são compilados via cssselect.
selectolax é mais rápido que PyQuery? A mediana de 1 KB foi menor, mas essa linha não entra no ranking porque a variação domina na escala de microssegundos. A partir de 10 KB, as diferenças entre medianas ficaram entre 0,5% e 5,4%. Isso é próximo para esta carga de trabalho, não uma prova de equivalência nem de faixas sobrepostas em todos os casos.
Por que rodar lxml de novo em vez de citar o número publicado? Porque o benchmark publicado registra a máquina e a versão do Python, mas não as versões das bibliotecas. A linha de lxml poderia vir de um lxml diferente daquele encapsulado pelo PyQuery hoje, e uma diferença de versão apareceria como um custo do wrapper que não existe. Rodar ambos no mesmo processo com lxml 6.1.1 elimina a ambiguidade.
Como ele se compara com cheerio? A mesma ideia geral de API, ecossistema diferente. Os dois seletores testados aqui funcionaram em ambos, e os hashes de conteúdo corresponderam em todos os cinco tamanhos; isso não comprova compatibilidade completa de seletores. Os tempos armazenados do cheerio foram mais lentos, mas os controles entre runtimes e execuções históricas impedem uma afirmação de multiplicador apenas da biblioteca.
O que não foi testado aqui? A memória foi medida como RSS máxima para importação apenas, um documento de 226 KB e um documento de 10 MB. O HTML malformado foi exercitado com 12 documentos quebrados mais dois controles correspondentes. Ainda não foram testados o desempenho de manipulação e traversal do PyQuery, cache de consultas repetidas, busca por URL, concorrência e cargas representativas de sites reais. O tempo de 1 KB continua sem classificação.


