Python Libraries for Web Scraping
The Python libraries for web scraping fall into four groups: HTTP clients that fetch pages (Requests, httpx, aiohttp), parsers that extract data from HTML (Beautiful Soup, lxml, parsel, selectolax), frameworks that run whole crawls (Scrapy, Crawlee), and browser tools for JavaScript pages (Playwright, Selenium). Beginners should start with Requests + Beautiful Soup, add Playwright for pages built by JavaScript, and move to Scrapy at thousands of pages. On speed, in our benchmark of 2026-10-27, selectolax parsed a 55.7 KB page and extracted 100 fields in 0.64 ms, lxml in 0.80 ms and Beautiful Soup in 20 to 28 ms; for fetching, Requests and httpx tie when you reuse a Session or Client, and async clients fetched 200 pages at 100 ms latency in 0.24 to 0.63 s instead of 20 s. Every fetching library takes a proxy in one setting: ProxyField residential proxies cost from $1.95/GB, and you can start with the Easy Trial: 1 GB for $2.95.
- residential IPs
- 412M+
- from, traffic never expires
- $1.95/GB
- Easy Trial
- 1 GB for $2.95
On this page 10 sections
- Python web scraping libraries compared
- Requests vs httpx, lxml vs selectolax: which is fastest?
- How to choose: four questions
- HTTP clients: Requests, httpx, aiohttp, curl_cffi
- Parsers: Beautiful Soup, lxml, parsel, selectolax
- Frameworks: Scrapy and Crawlee for Python
- Browser tools: Playwright, Selenium, SeleniumBase
- Recommended stacks
- Proxies for Python scrapers
- Python Libraries for Web Scraping FAQ
Start here.
pip install requests beautifulsoup4, then:import requests from bs4 import BeautifulSoup soup = BeautifulSoup(requests.get("https://quotes.toscrape.com/", timeout=30).text, "html.parser") print(soup.title.get_text()) # Quotes to ScrapeAdd Playwright when the data appears only after JavaScript runs, and Scrapy when you crawl thousands of pages.
Python web scraping libraries compared
Versions, licenses and release dates from PyPI. Each name links to the library's official documentation.
| Library | Group | Version (release) | License | JavaScript | Proxy setting | Use it for |
|---|---|---|---|---|---|---|
| Requests | HTTP client | 2.34.2 (May 2026) | Apache-2.0 | No | proxies={"http": ..., "https": ...} | Simple fetching, first scripts |
| httpx | HTTP client, sync and async | 0.28.1 (Dec 2024) | BSD-3-Clause | No | proxy="http://..." | Async fetching, HTTP/2 |
| aiohttp | Async HTTP client | 3.14.5 (Oct 2026) | Apache-2.0 AND MIT | No | proxy= per request | High-concurrency async fetching |
| curl_cffi | HTTP client on libcurl | 0.17.0 (Oct 2026), beta | MIT | No | proxies= like Requests | Requests that should look like a browser's at the TLS level |
| Beautiful Soup | Parser | 4.15.0 (Jun 2026) | MIT | No | (parses only) | Forgiving parsing, easy API |
| lxml | Parser | 6.1.3 (Sep 2026) | BSD-3-Clause | No | (parses only) | Fast parsing with XPath and CSS |
| parsel | Parser (Scrapy's selectors) | 1.12.1 (Sep 2026) | BSD-3-Clause | No | (parses only) | CSS and XPath in one API |
| selectolax | Parser | 1.0.2 (Oct 2026) | MIT | No | (parses only) | The fastest CSS-selector parsing in our test |
| MechanicalSoup | Browser-like client | 1.4.0 (May 2025) | MIT | No | Through its Requests session | Forms and simple logins, no JavaScript |
| Scrapy | Framework | 2.19.0 (Sep 2026) | BSD-3-Clause | Via a plugin | meta={"proxy": ...} | Large, recurring crawls |
| Crawlee for Python | Framework | 1.11.0 (Oct 2026) | Apache-2.0 | Yes (Playwright crawler) | ProxyConfiguration | Crawls that mix HTTP and browser pages |
| Playwright | Browser automation | 1.64.1 (Oct 2026) | Apache-2.0 | Yes | proxy={"server", "username", "password"} | JavaScript-heavy sites |
| Selenium | Browser automation | 4.51.0 (Oct 2026) | Apache-2.0 | Yes | Browser flag or proxy capability | Teams that already use Selenium |
| SeleniumBase | Framework on Selenium | 4.56.4 (Oct 2026) | MIT | Yes | proxy="user: | Selenium with helpers built in |
| Scrapling | Fetching and parsing toolkit | 0.5 (Oct 2026), beta | BSD-3-Clause | Optional browser fetchers | Per fetcher | Adaptive selectors, one package |
All of these are open source, so the library costs nothing; at scale you pay for servers and proxy traffic.
Requests vs httpx, lxml vs selectolax: which is fastest?
Fetching: Requests and httpx are equally fast one page at a time, as long as you reuse one requests.Session or httpx.Client; the network decides the time, not the library. For many pages, go async: httpx's AsyncClient or aiohttp fetched 200 pages at 100 ms latency in 0.63 s and 0.24 s, against 20.3 s one at a time. Parsing: selectolax is the fastest, lxml is close behind, and Beautiful Soup is 25 to 45 times slower than either; that matters only from hundreds of thousands of pages.
We measured both on 2026-10-27, on a server with an AMD Ryzen 9 5950X (8 vCPUs), Linux and Python 3.12.3, with the library versions in the table above. The fetch tests called a local test server in a separate process that returned a 16 KB page, so they measure the client, not the internet; the 100 ms delay stands in for a real site's latency. Each figure is the median of 3 runs.
Fetching: seconds for the whole batch (lower is better)
| Client, as called | 1,000 requests, no latency | 200 requests, 100 ms latency |
|---|---|---|
aiohttp ClientSession, all requests at once (asyncio.gather) | 0.15 s | 0.24 s |
httpx AsyncClient, all requests at once | 21.1 s (1.4 s with at most 100 in flight) | 0.63 s |
Requests Session shared by 20 threads | 0.69 s | 1.03 s |
httpx Client, one by one | 0.43 s | 20.3 s |
Requests Session, one by one | 0.53 s | 20.3 s |
requests.get() in a loop | 0.85 s | 20.4 s |
httpx.get() in a loop | 15.7 s | 23.5 s |
Parsing: milliseconds per page for a 55.7 KB product listing: build the tree, then extract 100 product names (lower is better; median of 3 runs of 1,000 pages)
| Parser | Build the tree | Query 100 names | Total per page | 1M pages of CPU time |
|---|---|---|---|---|
| selectolax (Lexbor), CSS | 0.60 ms | 0.04 ms | 0.64 ms | about 11 min |
| lxml.html, XPath | 0.69 ms | 0.11 ms | 0.80 ms | about 13 min |
| parsel, CSS | 0.70 ms | 0.25 ms | 0.95 ms | about 16 min |
| lxml.html, CSS (cssselect) | 0.69 ms | 0.38 ms | 1.07 ms | about 18 min |
| Beautiful Soup with the lxml parser | 17.0 ms | 3.1 ms | 20.1 ms | about 5.6 h |
Beautiful Soup with html.parser | 25.2 ms | 3.1 ms | 28.3 ms | about 7.9 h |
What the numbers say:
- Reuse one client.
httpx.get()in a loop was 37 times slower than onehttpx.Client(15.7 s against 0.43 s per 1,000), because each call builds a new client and its SSL context.requests.get()costs less, but aSessionstill keeps connections open. - Set a timeout everywhere. Requests has no default timeout and can wait forever on a stalled server; httpx's default is 5 seconds; aiohttp's is 5 minutes in total. Pass
timeout=on every call or client. - Cap the requests in flight. httpx's pool allows 100 connections and keeps 20 alive by default (aiohttp: 100 connections). Queuing all 1,000 requests on httpx at once took 21.1 s; the same 1,000 with an
asyncio.Semaphore(100)took 1.4 s. On a real site, cap it far lower, at the pace the site allows. - Beautiful Soup's parser choice matters less than people think. Switching it to lxml saved about 30%; the tree that Beautiful Soup builds on top costs the rest. If parsing is your bottleneck, change the library, not the backend.
- Query style matters. lxml's XPath query was 3.5 times faster than its CSS query on the same tree. With one simple query, lxml and selectolax come out nearly even.
- The network still wins. At 100 ms per page, a one-by-one scraper spends 100 ms waiting for each page and under 1 ms parsing it with lxml or selectolax.
Measure on a sample of your own pages before optimizing; this script reproduces the parse test on any saved page:
# pip install beautifulsoup4 lxml parsel selectolax
import time
import lxml.html
from bs4 import BeautifulSoup
from parsel import Selector
from selectolax.lexbor import LexborHTMLParser
html = open("page.html", encoding="utf-8").read() # any saved page
def bench(name, parse, n=1000):
t = time.perf_counter()
for _ in range(n):
parse(html)
print(f"{name:30} {(time.perf_counter() - t) / n * 1000:.2f} ms per page")
bench("Beautiful Soup + html.parser", lambda h: [a.get_text() for a in BeautifulSoup(h, "html.parser").select("h3 a")])
bench("Beautiful Soup + lxml", lambda h: [a.get_text() for a in BeautifulSoup(h, "lxml").select("h3 a")])
bench("lxml + XPath", lambda h: lxml.html.fromstring(h).xpath("//h3/a/text()"))
bench("parsel", lambda h: Selector(text=h).css("h3 a::text").getall())
bench("selectolax (Lexbor)", lambda h: [a.text() for a in LexborHTMLParser(h).css("h3 a")])More ways to speed a scraper up, from concurrency to caching: how to make web scraping faster.
How to choose: four questions
-
Is the data in the HTML?
Open the page source (Ctrl+U). If yes, an HTTP client plus a parser is enough. If the data appears only after scripts run, check the network tab for a JSON request you can call directly; only if there is none, use Playwright or Selenium.
-
How many pages?
Dozens to a few thousand: a script with Requests or httpx. Tens of thousands or a daily crawl: Scrapy or Crawlee, which bring queues, retries and exports.
-
Sync or async?
Requests is synchronous and simplest. httpx and aiohttp fetch many pages at once from one process; Scrapy is asynchronous inside.
-
How fast must parsing be?
For most jobs the network is the bottleneck, not the parser (see the tables above). From hundreds of thousands of pages, use selectolax or lxml instead of Beautiful Soup.
HTTP clients: Requests, httpx, aiohttp, curl_cffi
Requests is the default way to fetch a page in Python: one call, readable code, sessions that keep cookies. It is synchronous, so one script fetches one page at a time unless you add threads.
# pip install requests beautifulsoup4
import requests
from bs4 import BeautifulSoup
proxy = "http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000"
with requests.Session() as s:
s.proxies = {"http": proxy, "https": proxy}
r = s.get("https://quotes.toscrape.com/", timeout=30)
soup = BeautifulSoup(r.text, "html.parser")
print([q.get_text() for q in soup.select("span.text")][:3])httpx has a Requests-like API plus an async client and optional HTTP/2, which makes it the usual upgrade when a script must fetch hundreds of pages concurrently. aiohttp is the older async client with the same purpose, and the fastest in our fetch test. curl_cffi wraps libcurl and can make its TLS handshake match a chosen browser's; some sites treat Python's default TLS handshake differently from a browser's, which is when people reach for it. It is still marked beta. Through ProxyField, the Acceptable Use Policy applies whatever client you use: public pages only, and follow the site's terms; fraud, attacks and access to data behind logins you are not entitled to are not allowed.
# pip install httpx -> async fetching of many pages, at most 10 in flight
import asyncio, httpx
PROXY = "http://<login>-country-us-city-new_york:<password>@gate.proxyfield.io:7000"
async def main(urls):
sem = asyncio.Semaphore(10)
async with httpx.AsyncClient(proxy=PROXY, timeout=30) as client:
async def fetch(u):
async with sem:
return await client.get(u)
pages = await asyncio.gather(*(fetch(u) for u in urls))
print([p.status_code for p in pages])
asyncio.run(main([f"https://quotes.toscrape.com/page/{n}/" for n in range(1, 6)]))Proxy setup for each client, with SOCKS5 and rotation: Python proxy setup. Async patterns in depth: async web scraping in Python.
Parsers: Beautiful Soup, lxml, parsel, selectolax
A parser turns the HTML string into a tree you can query.
- Beautiful Soup is the most forgiving and the easiest to read:
soup.select("div.price"),tag.get_text(),tag["href"]. It builds its own tree on top of a parser; passing"lxml"instead of"html.parser"saved about 30% in our test. Tutorial: Beautiful Soup web scraping. - lxml is a fast C-based parser with full XPath 1.0 and CSS selectors through the cssselect package.
- parsel is the selector library inside Scrapy: one object with
.css()and.xpath(), usable without Scrapy. - selectolax is a Cython parser on the Lexbor engine, built for speed with CSS selectors. Since version 1.0, import
LexborHTMLParserfromselectolax.lexbor: the oldfrom selectolax.parser import HTMLParser(the Modest engine) now raises anImportErrorthat says the backend is deprecated.
Which selectors to write, CSS or XPath, and how to extract links and article text: HTML scraping with CSS selectors and XPath.
Frameworks: Scrapy and Crawlee for Python
When a script turns into a crawler, with link following, retries, deduplication, pacing and exports, a framework saves you from writing that plumbing.
- Scrapy has been the standard for more than a decade: asynchronous downloads, spiders, item pipelines, middlewares and feed exports. It does not render JavaScript by itself. See what Scrapy is and the Scrapy tutorial.
- Crawlee for Python combines HTTP crawlers (with Beautiful Soup or parsel) and a Playwright crawler behind one API, with request queues, sessions and proxy rotation.
Browser tools: Playwright, Selenium, SeleniumBase
Use a browser only for pages that need one: it costs far more CPU, memory and traffic per page than an HTTP request.
- Playwright for Python is the one to start a new project with: it installs its own Chromium, Firefox and WebKit builds (
playwright install), has sync and async APIs, and its locators wait for elements by themselves. Tutorial: Playwright web scraping. - Selenium drives browsers through WebDriver and is everywhere in QA teams; choose it if your team already has Selenium code. Selenium Manager downloads drivers by itself since 4.6. Tutorial: web scraping with Selenium and Python.
- SeleniumBase adds waits, helpers and a test runner on top of Selenium.
Pyppeteer, a Python port of Puppeteer, is unmaintained: its README says "This repo is unmaintained" and asks users to "consider playwright-python as an alternative" (github.com/pyppeteer/pyppeteer). Scraping dynamic pages step by step: Python dynamic web scraping.
Recommended stacks
| Job | Stack |
|---|---|
| First scraper, one site, static pages | Requests + Beautiful Soup |
| Hundreds of pages, fast | httpx (async) or aiohttp + selectolax or lxml |
| Daily crawl of a large site | Scrapy (+ scrapy-playwright for the pages that need a browser) |
| JavaScript-heavy site | Playwright, or the site's JSON requests with httpx |
| XPath-heavy or XML work | Requests or httpx + lxml |
| Forms and simple logins to your own account | MechanicalSoup or a Requests session |
| Mixed HTTP and browser pages in one crawler | Crawlee for Python |
A step-by-step tutorial with the first stack: Python web scraping. Tools beyond Python, from no-code apps to scraping APIs: best web scraping tools.
Proxies for Python scrapers
Every library above sends requests from your server's IP unless you set a proxy, and sites limit how many requests one IP may send. With a rotating gateway, one proxy URL in the library's setting gives each request a new IP; <login>-country-us-city-new_york in the username asks for exit IPs in New York. Region, session and session time go in the same username: -region-<region>, -session-<id> and -ttl-<seconds> (30 to 129,600 seconds, 20 minutes by default), as in <login>-country-us-region-new_york-city-new_york-session-he9f2qga-ttl-600; every part after the login is optional.
ProxyField residential proxies give you a new IP per request or a sticky session from 30 s to 36 h, over HTTP, HTTPS or SOCKS5 on gate.proxyfield.io:7000, with unlimited concurrent threads and country, region and city targeting. They cost $2.95/GB for 1 GB and fall to $1.95/GB from 1,000 GB, with no subscription; traffic never expires, and all purchases are final. HTML fetched by an HTTP client is small, so a GB covers many pages; browser pages cost more. Start with residential proxies and the Easy Trial: 1 GB for $2.95.
Python Libraries for Web Scraping FAQ
What are the best Python libraries for web scraping, and which ones should a beginner start with?
ProxyField residential proxies, from $1.95/GB, plug into every fetching library here; the libraries themselves are Requests and httpx for fetching, Beautiful Soup, lxml and selectolax for parsing, Scrapy for large crawls and Playwright for JavaScript pages. A beginner should start with Requests + Beautiful Soup (pip install requests beautifulsoup4), add Playwright when the data appears only after JavaScript runs, and move to Scrapy at thousands of pages.
Which Python scraping libraries are fastest: Requests or httpx for fetching, and lxml or selectolax for parsing?
For fetching one page at a time, Requests and httpx tie when you reuse a Session or Client (0.53 s and 0.43 s per 1,000 local requests in our test, 2026-10-27); for many pages, async wins: httpx's AsyncClient fetched 200 pages at 100 ms latency in 0.63 s and aiohttp in 0.24 s, against 20.3 s one by one. For parsing, selectolax was fastest (0.64 ms per 55.7 KB page), lxml close (0.80 ms with XPath) and Beautiful Soup 20 to 28 ms. Never call httpx.get() in a loop: it was 37 times slower than one Client.
What is a Python web scraping library?
A package that does one part of scraping: an HTTP client that fetches pages, a parser that extracts data from HTML, a framework that runs whole crawls, or a browser tool that renders JavaScript. A typical scraper combines one client with one parser, and each fetching library takes a proxy in one setting.
Why use Python libraries for web scraping?
They handle the hard parts for you: HTTP details, broken HTML, selectors, concurrency, retries and exports. Python also has the widest choice of scraping libraries and data tools, so the scraped rows go straight into pandas, a database or a notebook.
What are the best Scrapy alternatives?
ProxyField proxies plug into each of them through one proxy URL; the usual alternatives are Crawlee for Python, if you want HTTP and Playwright crawlers behind one API, or a plain httpx plus parsel script for smaller jobs. For pages that need a browser, Playwright with your own queue is the usual choice.
Which Python library is used for web scraping?
Usually two together: Requests or httpx to fetch the page, and Beautiful Soup or lxml to parse it. Scrapy is the usual choice for large, recurring crawls, and Playwright or Selenium for pages that build their content with JavaScript.
What is the Python Requests library?
Requests is the standard HTTP client for simple Python scripts: one call such as requests.get(url) fetches a page, and a session keeps cookies, headers and open connections between requests. It is synchronous, has no default timeout, and neither parses HTML nor runs JavaScript, so scrapers pair it with a parser such as Beautiful Soup. A proxy goes into its proxies argument.
What is Scrapling?
A BSD-licensed Python toolkit, version 0.5 and still marked beta, that combines HTTP and browser fetchers with a parser whose selectors can find elements again after a layout change. It is younger than Scrapy or Beautiful Soup, so test it before you build on it.
Should I use Requests or httpx for scraping?
Requests for simple synchronous scripts, httpx when you need async fetching or HTTP/2 with a nearly identical API; one by one, they were equally fast in our test. Both take a proxy URL in one argument, so switching later is a small change.
What is lxml used for in web scraping?
Parsing HTML and XML fast and querying it with XPath or CSS selectors: in our test it built a 55.7 KB page's tree in 0.69 ms, against 17 to 25 ms for Beautiful Soup. Many tools use it underneath, including Beautiful Soup when you pass the "lxml" parser and Scrapy's selectors through parsel.
Show 1 more questionShow fewer questions
What is curl_cffi, and when do I need it?
A Python HTTP client built on libcurl (0.17.0, October 2026, still beta) that can make its TLS handshake look like a chosen browser's. It helps when a site answers a browser but treats Python's default client differently.
Related pages
Easy Trial: 1 GB for $2.95.
Try for the price of 1 GB, no subscription, traffic never expires.