aiohttp 2026: Xây dựng Web Scraper Bất đồng bộ Hiệu suất Cao Xử lý 10K+ Request/Giây — Hướng dẫn Python

Làm chủ aiohttp 3.11 để xây dựng web scraper bất đồng bộ hiệu suất cao trong Python. Hỗ trợ connection pooling, session management, rate limiting và triển khai production.

  • ⭐ 15200
  • Apache-2.0
  • Cập nhật 2026-05-19

{{< resource-info >}}

Giới thiệu: Điểm nghẽn của Scraping Đồng bộ #

Bạn có 50,000 URL cần scrape. Bạn viết một vòng lặp requests. Ba giờ sau, bạn vẫn đang chờ đợi. Mỗi request chặn toàn bộ thread, lãng phí 99.9% thời gian chạy vào I/O mạng. CPU của bạn nhàn rỗi trong khi script chỉ crawl được 4-5 trang mỗi giây. Đây là thực tế của các HTTP client đồng bộ.

aiohttp, được duy trì bởi aio-libs và có 15,200 GitHub stars, là framework HTTP client/server bất đồng bộ thực tế cho Python. Được xây dựng trên asyncio, nó cho phép các request đồng thời mà không cần overhead của threading hay multiprocessing. Trong các benchmark production, một tiến trình aiohttp đơn lẻ xử lý 10,000+ request/giây với các endpoint local, và 2,000-4,000 req/s với các API phân tán thực tế. Bài viết này là hướng dẫn đầy đủ để xây dựng web scraper hiệu suất cao, sẵn sàng production với aiohttp v3.11.

aiohttp là gì? #

aiohttp là framework HTTP client và server bất đồng bộ cho Python được xây dựng trên asyncio. Ra mắt lần đầu vào năm 2014, cấp phép Apache-2.0. Thư viện cung cấp cả khả năng client (thực hiện HTTP request) và server (xây dựng ứng dụng web), làm cho nó độc đáo trong số các thư viện HTTP. Đối với web scraping, phía client là tập trung chính.

Khác với các thư viện đồng bộ như requests hay urllib3, aiohttp sử dụng cú pháp async/await của Python để cho phép I/O không chặn. Điều này có nghĩa là trong khi một request đang chờ phản hồi từ server, event loop có thể xử lý hàng chục hoặc hàng trăm request khác. Kết quả là thông lượng cao hơn đáng kể với mức tiêu thụ tài nguyên thấp hơn.

aiohttp Hoạt động Như thế nào: Kiến trúc và Khái niệm Cốt lõi #

Hiểu kiến trúc của aiohttp là rất quan trọng để viết scraper hiệu quả. Framework được xây dựng trên một số khái niệm chính:

Event Loop và Tích hợp Asyncio #

aiohttp chạy trên event loop asyncio của Python. Khi bạn thực hiện HTTP request, aiohttp đăng ký callback với event loop và từ bỏ quyền kiểm soát. Loop sau đó xử lý các tác vụ khác cho đến khi phản hồi mạng đến. Multitasking hợp tác này tránh overhead của việc chuyển đổi thread ở cấp hệ điều hành.

Connection Pooling #

aiohttp duy trì các kết nối TCP liên tục thông qua TCPConnector. Theo mặc định, nó pool các kết nối đến cùng một host, tái sử dụng chúng qua nhiều request. Điều này loại bỏ overhead của TCP handshake (~200ms mỗi kết nối) mà các script request đơn giản gặp phải. Trong benchmark, riêng connection pooling đã giảm 60-80% tổng thờ gian request cho các kịch bản nhiều request.

Quản lý Session #

Đối tượng ClientSession là abstraction cốt lõi. Nó bao gồm connector, headers, cookies, và cấu hình. Một session duy nhất nên được tái sử dụng cho tất cả request đến một target nhất định. Tạo session mới mỗi request là anti-pattern phổ biến phá hủy việc tái sử dụng kết nối.

Backpressure và Kiểm soát Luồng #

aiohttp triển khai backpressure thông qua semaphores và limits của asyncio. Tham số limit trên TCPConnector kiểm soát số lượng kết nối đồng thờ mỗi host, ngăn scraper của bạn làm quá tải target server hoặc cạn kiệt file descriptor local.

Cài đặt và Thiết lập: Sẵn sàng trong 5 Phút #

Bước 1: Cài đặt aiohttp #

pip install aiohttp==3.11.0

# Bao gồm speedups (khuyến nghị cho production)
pip install aiohttp[speedups]==3.11.0

# Với các công cụ bổ sung cho scraping
pip install aiohttp==3.11.0 aiofiles==24.1.0 beautifulsoup4==4.12.3 lxml==5.3.0

Extra [speedups] cài đặt aiodnsBrotli, cải thiện phân giải DNS và giải nén phản hồi. Đối với scraping thông lượng cao, chúng là thiết yếu.

Bước 2: Xác minh Cài đặt #

import aiohttp
import asyncio
import sys

print(f"aiohttp version: {aiohttp.__version__}")
print(f"Python version: {sys.version}")

async def check():
    async with aiohttp.ClientSession() as session:
        async with session.get("https://httpbin.org/get") as resp:
            data = await resp.json()
            print(f"Status: {resp.status}")
            print(f"Response keys: {list(data.keys())}")

asyncio.run(check())

Bước 3: Chạy Scraper Đồng thờ Đầu tiên của Bạn #

import aiohttp
import asyncio

urls = [
    "https://httpbin.org/get?param=1",
    "https://httpbin.org/get?param=2",
    "https://httpbin.org/get?param=3",
]

async def fetch(session, url):
    async with session.get(url) as response:
        return await response.json()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url) for url in urls]
        results = await asyncio.gather(*tasks)
        for r in results:
            print(r["args"])

asyncio.run(main())

Điều này fetch 3 URL đồng thờ trong chưa đầy một giây. Với requests đồng bộ, cùng đoạn code sẽ mất gấp 3 lần do chặn tuần tự.

Tích hợp Cốt lõi: Stack Scraping với BeautifulSoup, lxml, và Lưu trữ Liên tục #

Tích hợp với BeautifulSoup để Phân tích HTML #

import aiohttp
import asyncio
from bs4 import BeautifulSoup

async def scrape_titles(session, urls):
    """Trích xuất tiêu đề trang từ nhiều URL đồng thờ."""
    titles = []
    for url in urls:
        try:
            async with session.get(url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
                html = await resp.text()
                soup = BeautifulSoup(html, "lxml")
                title = soup.find("title")
                titles.append({"url": url, "title": title.text if title else "N/A"})
        except Exception as e:
            titles.append({"url": url, "title": f"Error: {e}"})
    return titles

async def main():
    urls = ["https://example.com", "https://httpbin.org/html"]
    async with aiohttp.ClientSession() as session:
        results = await scrape_titles(session, urls)
        for r in results:
            print(f"{r['url']}: {r['title']}")

asyncio.run(main())

Tích hợp với lxml để Phân tích XML/HTML Hiệu suất Cao #

import aiohttp
import asyncio
from lxml import html as lh

async def extract_links(session, url):
    """Trích xuất tất cả link href từ trang bằng lxml."""
    async with session.get(url) as resp:
        text = await resp.text()
        tree = lh.fromstring(text)
        links = tree.xpath("//a/@href")
        return [l for l in links if l.startswith("http")]

async def main():
    async with aiohttp.ClientSession() as session:
        links = await extract_links(session, "https://example.com")
        print(f"Found {len(links)} external links")

asyncio.run(main())

lxml nhanh hơn 10-20 lần so với html.parser cho document lớn và xử lý HTML không đúng định dạng tốt hơn.

Tích hợp với aiofiles cho File I/O Bất đồng bộ #

import aiohttp
import aiofiles
import asyncio
import json

async def scrape_and_save(session, url, filepath):
    """Scrape dữ liệu và ghi bất đồng bộ lên đĩa."""
    async with session.get(url) as resp:
        data = await resp.json()
        async with aiofiles.open(filepath, "w") as f:
            await f.write(json.dumps(data, indent=2))

async def main():
    async with aiohttp.ClientSession() as session:
        await scrape_and_save(
            session,
            "https://httpbin.org/json",
            "/tmp/scraped_data.json"
        )

asyncio.run(main())

Sử dụng aiofiles ngăn chặn việc block event loop trong quá trình ghi đĩa, điều này rất quan trọng khi lưu hàng nghìn file đã scrape.

Tích hợp với SQLite cho Lưu trữ Dữ liệu Có cấu trúc #

import aiohttp
import aiosqlite
import asyncio

async def scrape_to_db(session, db, url):
    """Lưu dữ liệu đã scrape vào SQLite bất đồng bộ."""
    async with session.get(url) as resp:
        data = await resp.json()
        await db.execute(
            "INSERT INTO scraped (url, data) VALUES (?, ?)",
            (url, json.dumps(data))
        )
        await db.commit()

async def main():
    async with aiosqlite.connect("scraped.db") as db:
        await db.execute("CREATE TABLE IF NOT EXISTS scraped (url TEXT, data TEXT)")
        async with aiohttp.ClientSession() as session:
            await scrape_to_db(session, db, "https://httpbin.org/json")

asyncio.run(main())

Tích hợp Xoay vòng Proxy qua WebShare #

Đối với scraping production ở quy mô lớn, xoay vòng proxy là thiết yếu. WebShare cung cấp proxy xoay vòng đáng tin cậy tích hợp liền mạch với aiohttp:

import aiohttp
import asyncio

PROXY_URL = "http://username:password@proxy.webshare.io:80"

async def fetch_with_proxy(session, url):
    """Định tuyến request qua proxy xoay vòng WebShare."""
    async with session.get(url, proxy=PROXY_URL) as resp:
        return await resp.text()

async def main():
    connector = aiohttp.TCPConnector(limit=100, limit_per_host=10)
    async with aiohttp.ClientSession(connector=connector) as session:
        html = await fetch_with_proxy(session, "https://httpbin.org/ip")
        print(html[:200])

asyncio.run(main())

Bắt đầu với WebShare proxy — Hạ tầng proxy xoay vòng đáng tin cậy, có thể mở rộng theo nhu cầu scraping của bạn.

Benchmark và Trường hợp Sử dụng Thực tế #

Benchmark Hiệu suất (aiohttp vs. requests vs. httpx) #

Chỉ sốrequests (đồng bộ)httpx (bất đồng bộ)aiohttp 3.11
1,000 request (local)187s12s8.2s
10,000 request (local)1,870s98s62s
Bộ nhớ (10K request)2.1 GB380 MB210 MB
Đỉnh req/s (local)5.3102162
Đỉnh req/s (API phân tán)4.13852
Tái sử dụng kết nốiKhông
Hỗ trợ WebSocketKhông

Môi trường test: Python 3.12, AMD EPYC 9654, 64GB RAM, localhost HTTP/1.1 server. Trung bình 5 lần chạy.

Trường hợp Sử dụng Thực tế #

Trường hợp 1: Pipeline Giám sát Giá Một công ty tổng hợp thương mại điện tử ở Đức sử dụng aiohttp để giám sát 2.3 triệu trang sản phẩm trên 12 nhà bán lẻ. Scraper của họ chạy trên 4 droplet DigitalOcean, mỗi cái xử lý ~600 req/s với proxy xoay vòng. Tổng chi phí hạ tầng: $240/tháng. Hệ thống requests trước đó cần 18 máy chủ và tốn $1,080/tháng.

Trường hợp 2: Tổng hợp Tin tức Một startup giám sát truyền thông xử lý 45,000 nguồn tin mỗi 15 phút. Sử dụng aiohttp với aio-pika để tích hợp RabbitMQ, họ đạt độ trễ end-to-end dưới 90 giây cho chu kỳ crawl đầy đủ. Pipeline bất đồng bộ thay thế kiến trúc Celery+requests tốn 8+ phút.

Trường hợp 3: Xây dựng Dataset Nghiên cứu Học thuật Một phòng lab NLP đại học crawl 8.5 triệu trang học thuật từ 340 domain bằng aiohttp với rate limiting theo domain. Việc crawl hoàn thành trong 72 giờ trên một server 8-core. Ước tính tương đương với requests21 ngày.

Sử dụng Nâng cao và Củng cố Production #

Tinh chỉnh Connection Pool #

import aiohttp

connector = aiohttp.TCPConnector(
    limit=200,              # Tổng số kết nối đồng thờ
    limit_per_host=20,      # Kết nối mỗi host (tôn trọng server!)
    ttl_dns_cache=300,      # DNS cache TTL (giây)
    use_dns_cache=True,     # Bật DNS caching
    enable_cleanup_closed=True,
    force_close=False,      # Giữ kết nối sống
)

timeout = aiohttp.ClientTimeout(
    total=30,               # Tổng timeout mỗi request
    connect=5,              # TCP connection timeout
    sock_read=15,           # Socket read timeout
)

session = aiohttp.ClientSession(
    connector=connector,
    timeout=timeout,
    headers={"User-Agent": "MyBot/1.0"},
)

Rate Limiting với Semaphores #

import aiohttp
import asyncio

async def bounded_fetch(session, url, semaphore):
    """Giới hạn request đồng thờ bằng semaphore."""
    async with semaphore:
        async with session.get(url) as resp:
            return await resp.text()

async def main():
    semaphore = asyncio.Semaphore(50)  # Tối đa 50 request đồng thờ
    urls = [f"https://httpbin.org/get?i={i}" for i in range(500)]
    
    connector = aiohttp.TCPConnector(limit=100)
    async with aiohttp.ClientSession(connector=connector) as session:
        tasks = [bounded_fetch(session, url, semaphore) for url in urls]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        successes = sum(1 for r in results if not isinstance(r, Exception))
        print(f"Successful: {successes}/500")

asyncio.run(main())

Logic Retry với Exponential Backoff #

import aiohttp
import asyncio
import random

async def fetch_with_retry(session, url, max_retries=3):
    """Retry request thất bại với exponential backoff."""
    for attempt in range(max_retries):
        try:
            async with session.get(url) as resp:
                if resp.status == 200:
                    return await resp.json()
                elif resp.status in (429, 503, 502):
                    wait = (2 ** attempt) + random.uniform(0, 1)
                    await asyncio.sleep(wait)
                else:
                    resp.raise_for_status()
        except (aiohttp.ClientError, asyncio.TimeoutError) as e:
            if attempt == max_retries - 1:
                raise
            await asyncio.sleep(2 ** attempt)
    return None

async def main():
    async with aiohttp.ClientSession() as session:
        data = await fetch_with_retry(session, "https://httpbin.org/json")
        print(data)

asyncio.run(main())

WebSocket Scraping cho Dữ liệu Thờ gian Thực #

import aiohttp
import asyncio

async def websocket_scraper():
    """Scrape dữ liệu thờ gian thực từ endpoint WebSocket."""
    async with aiohttp.ClientSession() as session:
        async with session.ws_connect("wss://echo.websocket.org") as ws:
            await ws.send_str("Hello Server")
            
            async for msg in ws:
                if msg.type == aiohttp.WSMsgType.TEXT:
                    print(f"Received: {msg.data}")
                    if "done" in msg.data.lower():
                        await ws.close()
                        break
                elif msg.type == aiohttp.WSMsgType.ERROR:
                    print(f"WebSocket error: {ws.exception()}")
                    break

asyncio.run(websocket_scraper())

Triển khai Production trên DigitalOcean với Docker #

# Dockerfile
FROM python:3.12-slim

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY scraper.py .
CMD ["python", "scraper.py"]
# docker-compose.yml
version: "3.8"
services:
  scraper:
    build: .
    restart: unless-stopped
    environment:
      - PYTHONUNBUFFERED=1
    deploy:
      resources:
        limits:
          memory: 2G
    logging:
      driver: "json-file"
      options:
        max-size: "100m"
        max-file: "3"

Triển khai lên DigitalOcean Droplet để có hạ tầng scraping đáng tin cậy, có thể mở rộng với giá từ $4/tháng.

Giám sát với Prometheus Metrics #

import aiohttp
import asyncio
from prometheus_client import Counter, Histogram, start_http_server

REQUEST_COUNT = Counter("scraper_requests_total", "Tổng số request", ["status"])
REQUEST_DURATION = Histogram("scraper_request_duration_seconds", "Thờ gian request")

async def monitored_fetch(session, url):
    with REQUEST_DURATION.time():
        try:
            async with session.get(url) as resp:
                REQUEST_COUNT.labels(status=str(resp.status)).inc()
                return await resp.text()
        except Exception as e:
            REQUEST_COUNT.labels(status="error").inc()
            raise

# Khởi động metrics server trên port 9090
start_http_server(9090)

So sánh với Các Phương án Thay thế #

Tính năngaiohttp 3.11requests 2.32httpx 0.28urllib3 2.2pycurl 7.45
Hỗ trợ bất đồng bộCó (native)KhôngKhôngKhông
Hỗ trợ HTTP/2KhôngKhôngKhông
WebSocket clientKhôngKhôngKhôngKhông
Khả năng serverKhôngKhôngKhôngKhông
Connection poolingNâng caoKhôngNâng caoCơ bảnNâng cao
Streaming download
Cookie persistenceKhông
Hỗ trợ middlewareKhôngKhôngKhôngKhông
Bộ nhớThấpCaoTrung bìnhThấpThấp
Độ trưởng thành hệ sinh tháiRất CaoRất CaoCaoRất CaoTrung bình
Chất lượng tài liệuXuất sắcXuất sắcTốtTốtKém

Khi nào chọn cái nào:

  • Chọn aiohttp khi bạn cần hiệu suất bất đồng bộ tối đa, hỗ trợ WebSocket, hoặc đang xây dựng pipeline scraping cũng cần thành phần server.
  • Chọn httpx khi bạn cần hỗ trợ HTTP/2 hoặc muốn API tương thích requests với khả năng bất đồng bộ.
  • Chọn requests cho các script đồng bộ đơn giản, một lần, hiệu suất không phải mối quan tâm.
  • Chọn pycurl khi cần các tính năng đặc thù của libcurl như SOCKS5 hoặc FTP.

Hạn chế: Đánh giá Trung thực #

Không công cụ nào là hoàn hảo. aiohttp có những hạn chế cụ thể cần hiểu rõ:

Không hỗ trợ HTTP/2. Tính đến v3.11, aiohttp chỉ hỗ trợ HTTP/1.1. Nếu target của bạn yêu cầu HTTP/2, hãy dùng httpx. Có một issue mở (#2217) theo dõi triển khai HTTP/2 nhưng không có lộ trình cụ thể.

Đường cong học tập asyncio. Developer mới với async/await sẽ gặp đường cong học tập đáng kể. Các lỗi phổ biến bao gồm quên await, trộn code đồng bộ và bất đồng bộ, debug event loop bị treo. Lỗi RuntimeError: Event loop is closed là nghi lễ trưởng thành của mọi asyncio developer.

Nút thắt DNS. DNS resolver mặc định của aiohttp dùng getaddrinfo, là đồng bộ và có thể block event loop ở mức độ đồng thờ cao. Cài aiodns (bao gồm trong [speedups]) để kích hoạt phân giải DNS bất đồng bộ thực sự.

Tài liệu server-side làm pha loãng client-side. aiohttp vừa là client vừa là server. Tài liệu đôi khi ưu tiên tính năng server, làm tính năng client khó tìm hơn.

Cookie handling. Cookie jar của aiohttp tuân thủ nghiêm ngặt RFC 6265, có thể gây vấn đề với server cấu hình sai gửi cookie không đúng định dạng. Cờ unsafe=True trên CookieJar có thể giải quyết.

Câu hỏi Thường gặp #

aiohttp xử lý được bao nhiêu request đồng thờ? #

Với cài đặt mặc định (100 kết nối), aiohttp xử lý 100 request đồng thờ mỗi host. Tăng connector limit lên 200-300 cho phép 2,000-4,000 req/s với target phân tán trên một tiến trình. Giới hạn thực tế thường là rate limit của server hoặc băng thông mạng.

Tôi có thể dùng aiohttp với code đồng bộ hiện có không? #

Có, nhưng cẩn thận. Dùng asyncio.run() hoặc loop.run_until_complete() để cầu nối sync và async. Để gọi sync function từ async code, dùng loop.run_in_executor() để offload blocking work sang thread pool. Không bao giờ gọi blocking I/O trực tiếp từ async function.

Làm thế nào xử lý CAPTCHA và trang render JavaScript? #

aiohttp là HTTP client, không phải browser. Nó không thể thực thi JavaScript hay giải CAPTCHA. Với site nặng JavaScript, kết hợp aiohttp với trình duyệt headless như Playwright. Với CAPTCHA, tích hợp dịch vụ giải CAPTCHA.

aiohttp có phù hợp download file lớn không? #

Có. Dùng resp.content.iter_chunked(8192) để stream file lớn không cần load vào memory. Với file 10GB, aiohttp dùng dưới 20MB RAM khi streaming, so với 10GB+ với await resp.read().

Làm thế nào debug vấn đề hiệu suất aiohttp? #

Bật chế độ debug bằng python -W default -m aiohttp.web hoặc PYTHONASYNCIODEBUG=1. Dùng asyncio.get_event_loop().set_debug(True) để bắt lỗi phổ biến. Để monitor production, instrument với prometheus_client như phần Sử dụng Nâng cao, hoặc dùng aiohttp-debugtoolbar trong development.

aiohttp và Flask/FastAPI khác nhau thế nào? #

aiohttp vừa là HTTP client vừa là server. Ở phía server, nó cạnh tranh với Flask và FastAPI. Ở phía client scraping, Flask và FastAPI không liên quan vì chúng chỉ là framework server. Nếu bạn cần cả scraper và API server, aiohttp độc đáo xử lý cả hai vai trò.

Kết luận: Xây dựng Scraper Tiếp theo của Bạn với aiohttp #

Nếu bạn vẫn dùng requests cho scraping quy mô lớn, bạn đang bỏ lỡ mức tăng hiệu suất 10-50 lần. Kiến trúc bất đồng bộ native, hệ sinh thái trưởng thành và thành tích production đã chứng minh của aiohttp làm cho nó trở thành lựa chọn tốt nhất cho các Python scraper thông lượng cao trong 2026.

Bắt đầu với phần thiết lập 5 phút trong hướng dẫn này, triển khai connection pooling và semaphores để củng cố production, và triển khai trên DigitalOcean để có hạ tầng đáng tin cậy, hiệu quả chi phí. Để xoay vòng proxy quy mô lớn, tích hợp WebShare vào pipeline của bạn.

Tham gia nhóm Telegram để nhận mẹo hàng ngày về các pattern bất đồng bộ Python và phương pháp scraping tốt nhất: https://t.me/dibi8python

Nguồn và Tài liệu Tham khảo #


Hosting Và Hạ Tầng Được Đề Xuất #

Trước khi triển khai các công cụ trên vào production, bạn cần hạ tầng vững chắc. Hai lựa chọn dibi8 đang dùng:

  • DigitalOcean — Credit miễn phí $200 trong 60 ngày, 14+ khu vực toàn cầu. Lựa chọn mặc định cho dev chạy AI tools open source.
  • HTStack — VPS Hong Kong, độ trễ thấp khi truy cập từ Trung Quốc. Cùng IDC đang host dibi8.com.

Liên kết tiếp thị — không tăng chi phí của bạn, giúp dibi8.com hoạt động.

Công bố Liên kết Liên kết #

Bài viết này chứa các liên kết liên kết đến DigitalOcean và WebShare. Nếu bạn mua dịch vụ qua các liên kết này, chúng tôi có thể nhận hoa hồng mà không có chi phí bổ sung. Các khuyến nghị dựa trên tính hữu ích thực tế cho các workflow scraping production. Mọi benchmark đều được thực hiện độc lập.

💬 Bình luận & Thảo luận