Xây dựng công cụ crawl truyện bằng Python và Asyncio

Việc thu thập dữ liệu từ các trang web đọc truyện yêu cầu sự kết hợp giữa kỹ thuật xử lý HTTP request, phân tích mã nguồn HTML và tối ưu hóa tốc độ tải xuống. Trong hướng dẫn này, chúng ta sẽ xây dựng một công cụ bằng Python sử dụng thư viện aiohttp để tải truyện bất đồng bộ, giúp tăng hiệu suất đáng kể so với phương pháp tuần tự thông thường.

1. Khởi tạo phiên đăng nhập

Để truy cập vào các nội dung hạn chế hoặc để đồng bộ cài đặt cá nhân, bước đầu tiên là thực hiện xác thực với hệ thống. Chúng ta sẽ sử dụng requests để gửi dữ liệu đăng nhập và nhận JSON phản hồi.

import requests

def perform_login(user, pwd):
    login_url = "https://passport.17k.com/ck/user/login"
    payload = {
        "loginName": user,
        "password": pwd
    }
    response = requests.post(login_url, data=payload).json()
    
    if response.get('data', {}).get('nickname'):
        print(f"Xác thực thành công: {response['data']['nickname']}")
        return True
    return False

2. Lọc danh sách truyện theo điều kiện

Tiếp theo, chúng ta cần tìm kiếm các tác phẩm phù hợp (ví dụ: đã hoàn thành và miễn phí). Việc gửi các tham số filter giúp server trả về đúng danh mục mong muốn trước khi chúng ta bóc tách URL.

import re

def fetch_novel_list():
    base_url = 'https://www.17k.com/all/book/2_0_0_0_3_0_1_0_1.html'
    html_content = requests.get(base_url).text
    novel_map = {}
    
    # Sử dụng Regex để trích xuất ID và tên truyện
    patterns = re.findall('<a target="_blank" href="//www.17k.com/book/(.*?)</a>', html_content)
    for item in patterns:
        parts = item.split('" >')
        if len(parts) == 2:
            novel_id, title = parts[0], parts[1]
            novel_map[title] = novel_id
            
    return novel_map

def apply_search_filters():
    filter_configs = {
        "Completed": "https://static.17k.com/js/mod/WXShare.js?v=0911",
        "Free": "https://static.17k.com/js/mod/WXShare.js?v=0911"
    }
    for label, api_url in filter_configs.items():
        resp = requests.get(api_url, params={"v": "0911"}).text
        if "成功" in resp:
            print(f"Kích hoạt bộ lọc {label} thành công.")
    return fetch_novel_list()

3. Trích xuất danh sách chương

Sau khi có ID truyện, chúng ta cần truy cập vào trang chi tiết để lấy link của tất cả các chương hiện có.

def get_chapter_manifest(book_id, headers):
    index_url = f"https://www.17k.com/list/{book_id}.html"
    res = requests.get(index_url, headers=headers)
    res.encoding = 'utf-8'
    
    titles = re.findall('title="(.*?)&', res.text)
    links = re.findall('href="/chapter(.*?)"', res.text)
    
    return dict(zip(titles, links))

4. Tải dữ liệu bất đồng bộ với Asyncio

Để tối ưu tốc độ, thay vì tải từng chương một, chúng ta sử dụng aiohttp để thực hiện nhiều request cùng lúc.

import asyncio
    import aiohttp
    import os

async def save_chapter(session, chapter_name, path_suffix, storage_path, headers):
    target_url = f"https://www.17k.com/chapter{path_suffix}"
    try:
        async with session.get(target_url, headers=headers) as response:
            content = await response.text()
            # Bóc tách nội dung chính trong thẻ div
            body_match = re.findall(r'<div class="p">(.*?)</div>', content, re.DOTALL)
            
            if body_match:
                clean_text = "\n".join(re.findall('<p>(.*?)</p>', body_match[0].strip()))
                file_name = os.path.join(storage_path, f"{chapter_name}.txt")
                
                with open(file_name, "w", encoding="utf-8") as f:
                    chapter_title = re.findall('<h1>(.*?)</h1>', content)[0]
                    f.write(chapter_title + "\n\n")
                    f.write(clean_text)
    except Exception as e:
        print(f"Lỗi khi tải chương {chapter_name}: {e}")

async def batch_download(chapters_data, storage_path, headers):
    async with aiohttp.ClientSession() as session:
        queue = []
        for name, link in chapters_data.items():
            task = asyncio.create_task(save_chapter(session, name, link, storage_path, headers))
            queue.append(task)
        await asyncio.gather(*queue)

5. Quy trình điều phối chính

Hàm điều phối sẽ kết nối tất cả các thành phần: từ tạo thư mục, đăng nhập đến việc khởi chạy vòng lặp sự kiện của Asyncio.

def run_crawler():
    base_dir = "Crawler_Output"
    if not os.path.exists(base_dir):
        os.makedirs(base_dir)

    headers = {
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
    }

    if perform_login("username_example", "password_example"):
        books = apply_search_filters()
        for title, b_id in books.items():
            book_path = os.path.join(base_dir, title)
            if not os.path.exists(book_path):
                os.makedirs(book_path)
            
            # Lấy danh sách chương
            chapters = get_chapter_manifest(b_id, headers)
            if chapters:
                print(f"Bắt đầu tải: {title}")
                asyncio.run(batch_download(chapters, book_path, headers))

if __name__ == '__main__':
    run_crawler()

Thẻ: python web-scraping AsyncIO aiohttp regex

Đăng vào ngày 9 tháng 11 lúc 09:45