PyQuery porta un’API in stile jQuery sopra lxml. Su cinque dimensioni di pagina, da 1 KB a 10 MB, tutte le mediane mostrate sono risultate inferiori a quelle di lxml puro, incluso uno scarto dell’1,5% alla dimensione maggiore. Il benchmark non dimostra che il wrapper sia più veloce; ha semplicemente trovato una differenza troppo piccola per incidere su questa scelta tra selezione e lettura.
Anche rispetto a selectolax, dalle pagine da 10 KB in su le mediane mostrate sono rimaste entro pochi punti percentuali. Senza un margine di equivalenza definito a priori, questo è un risultato vicino, non un pareggio statistico.
Cos’è PyQuery
PyQuery è una libreria Python che offre la stessa API a selettori e concatenazione di jQuery sopra un albero di documenti lxml. Versione testata: 2.1.0, licenza BSD, 2.380 stelle su GitHub, 59 issue aperte, ultimo push il 2026-07-27.
Riferimento ufficiale: documentazione di 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")]
Gli elementi restituiti sono elementi lxml, quindi tutto ciò che sai fare con lxml continua a funzionare. È proprio questo il punto: PyQuery è uno strato di comodità, non un parser. pip install pyquery porta con sé 3 pacchetti — lxml, cssselect e PyQuery stesso — e 20,1 MiB, quasi tutto dovuto alle estensioni compilate di lxml.
Se hai usato cheerio in Node, l’idea di API è la stessa, solo in Python. I due selettori testati qui hanno funzionato in entrambi; il test non stabilisce una piena equivalenza del linguaggio dei selettori tra cssselect e cheerio.
La misurazione
Questa base di ricerca aveva già un benchmark sui parser con una proprietà che molti benchmark non hanno: un controllo di parità che confronta l’hash del contenuto estratto — titoli ordinati più href ordinati — con un parser di riferimento, così una libreria che “salta lavoro” non può semplicemente segnare un tempo più veloce. Cinque dimensioni di pagina, 50 iterazioni, tre esecuzioni indipendenti.
Aggiungere PyQuery ha richiesto due cose.
Rieseguire il riferimento. selectolax è stato eseguito di nuovo nello stesso processo. Il suo hash del contenuto è tornato identico su 5 dimensioni su 5 e il p50 è risultato tra 0,989× e 1,079× rispetto alla cifra pubblicata — quindi stessa macchina e stesso ambiente di test.
Eseguire anche lxml nello stesso processo. Il benchmark pubblicato riporta macchina e versione di Python ma non le versioni delle librerie, quindi la riga di lxml potrebbe provenire da una release diversa da quella che PyQuery incapsula qui. Confrontare attraverso quel divario avrebbe significato confrontare due versioni di lxml e chiamarlo “costo del wrapper”. Eseguire lxml affiancato elimina il dubbio — qui entrambi sono lxml 6.1.1 in questo venv.
| Dimensione pagina | 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 in millisecondi, mediana di tre esecuzioni, tutto nello stesso processo. parser-bench.json. Tutti e tre gli hash del contenuto corrispondevano al riferimento a ogni dimensione.
Un costo del wrapper che qui non si vede

PyQuery è risultato uguale o migliore di lxml puro in tutte le mediane mostrate. Questo non prova che un wrapper renda il parsing più veloce. Tre mediane di esecuzione e nessun margine di equivalenza predefinito consentono solo un giudizio più ristretto: in questo fixture non è emerso un sovraccarico di selezione rilevante per la scelta.
A 10 MB, le tre esecuzioni di PyQuery sono state 162,86, 163,17 e 161,13 ms; quelle di lxml 169,46, 165,25 e 164,18. Gli intervalli sono vicini ma non si sovrappongono. A 1 MB le due mediane differiscono dello 0,5%. Queste piccole serie supportano un giudizio pratico, non una dichiarazione di equivalenza statistica.

Il meccanismo è abbastanza semplice: pq(html) costruisce una volta l’albero lxml, d("h3.title") compila il selettore CSS tramite cssselect, come fa tree.cssselect(), e gli elementi restituiti sono elementi lxml. In questo percorso caldo misurato, PyQuery fa poco lavoro aggiuntivo. Traversal, manipolazione, query ripetute, import e memoria restano fuori dalla tesi sul tempo del selettore.
Risultati vicini dalle dimensioni da 10 KB in su
Il risultato più utile è la prima colonna.
Dalle dimensioni da 10 KB in su, lo scarto tra il più veloce e il più lento, considerando le mediane di selectolax, lxml e PyQuery, è stato del 4,5% a 10 KB, del 5,4% a 100 KB, 0,5% a 1 MB, e del 4,5% a 10 MB. Questa prova non era un test di equivalenza; il giudizio pratico è che questi divari non cambierebbero la maggior parte delle scelte del parser per questo carico di lavoro.
selectolax è davvero più veloce a 1 KB — 0,0286 ms contro 0,0456 e 0,0508 — ma quella riga non è utilizzabile. Tra i tre parser, lo scarto a quella dimensione è del 77,6%, e le tre esecuzioni di selectolax variavano da 0,0267 a 0,0404 ms. A 28 microsecondi, timer e scheduler dominano. Non mi sentirei di classificare nulla lì.
Per questo tipo di lavoro, basato su selezione e lettura, conviene scegliere tra questi tre in base all’API e ai fatti misurati sulle dipendenze, non su una presunta gerarchia di velocità. PyQuery non ha mostrato un costo penalizzante rispetto a lxml. selectolax usa uno stack di parsing diverso, ma questo articolo non ha misurato nello stesso modo il suo ingombro installato, la copertura delle wheel o i requisiti di build.
Per confronto, il benchmark pubblicato ha inserito altre due opzioni Python sulla stessa fixture da 10 MB, ed è lì che le differenze diventano davvero evidenti:
| 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 |
Dati pubblicati da bench_parse.json.
Le righe storiche del benchmark collocano BeautifulSoup oltre un ordine di grandezza sopra le mediane dei parser più veloci su questa fixture. Quelle righe non sono state rieseguite con la coppia PyQuery/lxml del processo corrente, quindi sono utili come contesto e non come moltiplicatore controllato per il verdetto principale.
La riga salvata di cheerio era 2.927,89 ms (2927.8857 in parser-bench.json) con hash del contenuto estratto corrispondenti. Anche quel risultato cross-runtime dipende da Node, dalle versioni dei pacchetti e dai controlli storici dell’esecuzione; non va letto come moltiplicatore isolato della velocità della libreria.
La realtà della configurazione
| Libreria | Pacchetti | Disco | Licenza | Stelle | Ultimo 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 |
Riferimento ufficiale: PyQuery su PyPI.
Tre pacchetti sono una dipendenza piuttosto ordinata, e due di questi — lxml e cssselect — sono già presenti in moltissimi progetti Python di scraping. In questo caso, il costo marginale di PyQuery è di poche decine di kilobyte.
I 20,1 MiB sono delle estensioni compilate di lxml, non di PyQuery. È lo stesso costo di circa 20 MiB che paghi usando direttamente lxml.
La libreria è sotto licenza BSD. Alla data dello snapshot aveva 59 issue aperte e un push tre settimane prima del test; queste osservazioni da sole non bastano a stabilire qualità di manutenzione o compatibilità futura.
Memoria e cosa fa l’HTML rotto
Due aspetti che ogni recensione di questo gruppo segnava come non testati, ora sono stati misurati.
Il contesto più ampio dello stress test è nella confronto tra memoria e HTML malformato di dieci librerie.
Memoria residente massima, via /usr/bin/time -l, un processo nuovo per ogni cella — il livello base di import è ciò che costa la libreria caricata e inattiva, i picchi includono il documento.
| Libreria | Runtime | Livello base di import | Picco 226 KB | Picco 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. I valori base di Python e Node non sono tra loro confrontabili; l’interprete è presente in entrambi.
PyQuery è più leggero di resiliparse sul documento grande — 172,5 MiB contro 225,1 — nonostante un livello base di import più alto. L’albero di lxml è compatto, e gran parte dei 30,3 MiB di base di PyQuery è dovuta al caricamento di lxml, non a PyQuery in sé.
HTML rotto. Dodici documenti che rompevano esattamente una cosa ciascuno — tag non chiusi, elementi inline annidati male, attributi senza virgolette con spazi, chiusure fuori posto, assenza totale di <html>, attributi duplicati, un documento troncato a metà tag, entità non valide, un <script> non chiuso, una dichiarazione di charset falsa, un commento che contiene markup e 600 livelli di annidamento — più due controlli ben formati di dimensioni corrispondenti, perché “non ha restituito nulla” dice qualcosa sulla malformazione solo se la libreria non tace anche su un documento pulito della stessa dimensione.
pyquery ha generato un’eccezione su 0 su 14 e non ha restituito nulla su 1, recuperando 10/22 sentinelle tra i fixture rotti (malformed-results.json). Un fixture è escluso da quel conteggio: secondo HTML5, tutto ciò che segue un <script> non chiuso è contenuto dello script, quindi perderlo lì è corretto e recuperarlo sarebbe la deviazione. Senza i risultati delle sentinelle delle alternative dirette accanto a questo dato, 10/22 è un’osservazione di robustezza, non una classifica tra parser.
Pro e contro
A favore. Sintassi jQuery, familiare a chi ha scritto JavaScript frontend o ha usato cheerio. In questa prova non è emerso alcun sovraccarico di selezione rilevante rispetto a lxml puro. Solo 3 pacchetti, e 2 probabilmente sono già nel tuo albero delle dipendenze. Restituisce elementi lxml, quindi restano disponibili tutte le tecniche di lxml. BSD. Gli hash del contenuto hanno coinciso con il riferimento a tutte e cinque le dimensioni.
Contro. 20,1 MiB, per via di lxml. 2.380 stelle significano una community molto più piccola rispetto alle 30.449 di cheerio — meno esempi pratici quando qualcosa si comporta in modo strano. È uno strato di comodità, quindi tutto ciò che lxml non può fare, non può farlo nemmeno lui. E se speravi che l’API jQuery comprasse prestazioni, non è così: compra ergonomia, mentre il parser sottostante è quello che fa il lavoro.
Chi dovrebbe usarlo e chi no
Usa PyQuery se tu o il tuo team preferite selettori in stile jQuery in Python. La costruzione misurata, due selezioni e letture non hanno mostrato penalità rilevanti rispetto a lxml; altre operazioni di PyQuery non sono state cronometrate.
Usa direttamente lxml se preferisci XPath o vuoi un pacchetto in meno. Questa prova non ha mostrato un motivo legato alla velocità del selettore per sceglierne uno rispetto all’altro.
Valuta selectolax se la sua API di parsing e il suo stack di dipendenze si adattano al tuo progetto. La riga da 1 KB è esplicitamente non classificata, e questo articolo non supporta una tesi sul “minor ingombro di dipendenze”.
In Node, cheerio è l’equivalente più vicino come forma di API. Le righe cross-runtime salvate qui erano più lente, ma le differenze di runtime e di esecuzione storica impediscono una conclusione pulita basata solo sulla libreria.
Dove si inserisce un’API gestita
PyQuery analizza HTML che hai già in mano. Non scarica pagine, non renderizza JavaScript e non gestisce un livello anti-bot — nessun parser in questo confronto lo fa, e su molti target reali questa è la parte più difficile.
Nota dell’autore: Thunderbit è la nostra opzione gestita per recupero/rendering delle URL ed estrazione. Non è stata confrontata con PyQuery in questo articolo. Il confine rilevante è se hai già l’HTML e vuoi selettori locali, oppure se vuoi acquisizione e estrazione della pagina come servizio.
La lettura onesta è questa: se hai l’HTML e conosci i tuoi selettori, PyQuery è gratuito e piacevole. Se i selettori continuano a rompersi, o stai estraendo dati su larga scala, allora parliamo di un acquisto diverso.
Per una visione più ampia, il nostro rassunto delle API di web scraping copre le soluzioni ospitate e il pilastro degli scraper open source quelle self-hosted. Se l’output parsato finisce in un modello, convertire HTML in Markdown in Python è il punto in cui si perde fedeltà.
Prova Thunderbit per l’estrazione di dati web
Conviene usare PyQuery?
Sì, se vuoi la sintassi jQuery in Python e il percorso misurato di selezione/lettura rappresenta il tuo carico di lavoro.
Il benchmark non ha rilevato un sovraccarico di selezione rilevante rispetto a lxml, mantenendo però la parità degli hash del contenuto. Non ha dimostrato costo zero per l’intera libreria.
Su cinque dimensioni, le mediane dei tre parser Python sono rimaste abbastanza vicine da far pensare che, per questo compito, l’aderenza all’API conti più della velocità pura. Definisci un margine di equivalenza e riesegui le alternative esatte prima di trasformare questo giudizio in una classifica più ampia dei parser.
Prova Thunderbit per l’estrazione di dati web Get Started Free
FAQ
PyQuery rallenta lxml?
In questa esecuzione non è emerso alcun sovraccarico di selezione rilevante. Su cinque dimensioni di pagina, le mediane sono risultate uguali o inferiori a quelle di lxml puro, usando entrambi lxml 6.1.1 nello stesso processo. A 10 MB gli intervalli vicini non si sovrapponevano: PyQuery 161,13–163,17 ms e lxml 164,18–169,46 ms. pq(html) costruisce un albero lxml e i selettori testati passano attraverso cssselect.
selectolax è più veloce di PyQuery? La mediana a 1 KB era più bassa, ma quella riga non è classificata perché la variabilità domina alla scala dei microsecondi. Dalle dimensioni da 10 KB in su, gli scarti mediani erano tra lo 0,5% e il 5,4%. Per questo carico di lavoro è un risultato vicino, non una prova di equivalenza o di intervalli sovrapposti in ogni caso.
Perché rieseguire lxml invece di citare il numero pubblicato? Perché il benchmark pubblicato riporta macchina e versione di Python ma non le versioni delle librerie. La sua riga di lxml potrebbe provenire da una versione diversa da quella che PyQuery avvolge oggi, e un divario di versione sarebbe sembrato un costo del wrapper che in realtà non c’è. Eseguire entrambi nello stesso processo su lxml 6.1.1 elimina l’ambiguità.
Come si confronta con cheerio? Stessa idea generale di API, ecosistema diverso. I due selettori testati qui hanno funzionato in entrambi e gli hash del contenuto coincidevano a tutte le cinque dimensioni; questo non dimostra una compatibilità completa dei selettori. I tempi salvati di cheerio erano più lenti, ma i controlli cross-runtime e storici impediscono una dichiarazione di moltiplicatore limitata alla libreria.
Cosa non è stato testato qui? La memoria è stata misurata come RSS di picco per solo import, per un documento da 226 KB e per un documento da 10 MB. L’HTML malformato è stato provato con 12 documenti rotti più due controlli allineati. Restano non testate le prestazioni di manipolazione e traversal di PyQuery, la cache delle query ripetute, il recupero delle URL, la concorrenza e carichi rappresentativi di siti reali. La misurazione a 1 KB resta non classificata.


