上周整理书房,面对满架的书突然萌生个想法:这些年到底买了哪些书?哪些买了还没读?能不能按出版社、作者快速检索?

翻了一圈现有的图书管理软件,要么收费,要么云端同步慢得令人发指。我这个人有点强迫症,数据必须掌握在自己手里。于是把目光投向了那个常去的图书信息网站——图书大百科(book.qciss.net)。

为什么是它 为什么是它?

图书大百科的数据质量确实不错,ISBN、封面、简介、作者信息都很全,分类也细。但每次查书都要开浏览器、输网址、敲ISBN,查完还得手动复制粘贴到Excel,这操作太反人类了。

作为一个写过几年Python的人,我第一反应是:写个爬虫,把常用数据扒下来存本地。

技术选型

既然要写爬虫,requests+BeautifulSoup是标配。但这次情况特殊:

  1. 网站结构不算复杂,但有一些基本的反爬措施
  2. 数据量不小,单线程爬太慢
  3. 需要长期维护更新

所以最终方案:
requests:发送HTTP请求
BeautifulSoup4:解析HTML
SQLite3:本地数据库,轻量够用
fakeuseragent:随机UserAgent,避免被ban
time模块:控制爬取速度,做个体面人

核心代码解析 核心代码解析

先看看最基础的爬虫骨架:

import requests
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
import time
import sqlite3

class BookSpider:
    def __init__(self):
        self.ua = UserAgent()
        self.base_url = "https://book.qciss.net"
        self.headers = {
            'UserAgent': self.ua.random,
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,/;q=0.8',
            'AcceptLanguage': 'zhCN,zh;q=0.8,enUS;q=0.5,en;q=0.3',
            'Connection': 'keepalive',
        }
        self.init_db()
    
    def init_db(self):
        """初始化SQLite数据库"""
        self.conn = sqlite3.connect('books.db')
        self.cursor = self.conn.cursor()
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS books (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                isbn TEXT UNIQUE,
                title TEXT,
                author TEXT,
                publisher TEXT,
                pub_date TEXT,
                summary TEXT,
                cover_url TEXT,
                category TEXT,
                create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
            )
        ''')
        self.conn.commit()

这里的重点是fakeuseragent。很多网站会检查UserAgent,如果是Python默认的urllib头,大概率被拒。随机UA能绕过大部分基础防护。
6low

爬虫的绅士礼仪

写爬虫不是打仗,要给对方服务器留点喘息空间。我在代码里加了两个小细节:

def fetch_book(self, isbn):
    """根据ISBN获取图书详情"""
    time.sleep(random.uniform(1, 3))   随机延时13秒
    
    try:
        url = f"{self.base_url}/book/{isbn}"
        response = requests.get(url, headers=self.headers, timeout=10)
        
        if response.status_code == 200:
             检查是否被重定向到验证页面
            if len(response.text) < 5000:   验证页面通常很小
                print(f"触发反爬机制,暂停60秒")
                time.sleep(60)
                return None
            return response.text
        elif response.status_code == 404:
            print(f"ISBN {isbn} 不存在")
            return None
        else:
            print(f"请求失败: {response.status_code}")
            return None
            
    except Exception as e:
        print(f"请求异常: {e}")
        return None

这里有两个关键点:

  1. 随机延时:固定间隔容易被识别为爬虫
  2. 响应长度判断:很多反爬页面跳转到验证码时,HTML内容会变得很短,通过长度能快速识别

数据解析的艺术

BeautifulSoup的用法很简单,但实际解析时有很多坑:

def parse_book_page(self, html):
    """解析图书详情页"""
    soup = BeautifulSoup(html, 'html.parser')
    
    book_data = {}
    
     获取书名  多种选择器备选
    title_elem = (
        soup.select_one('h1.booktitle') or 
        soup.select_one('.bookinfo h2') or 
        soup.select_one('div.bookname')
    )
    book_data['title'] = title_elem.text.strip() if title_elem else '未知'
    
     获取ISBN  通常藏在某个角落
    isbn_elem = soup.find('span', string=re.compile(r'ISBN'))
    if isbn_elem:
         找到父元素再找相邻元素
        parent = isbn_elem.find_parent('li')
        if parent:
            book_data['isbn'] = parent.text.replace('ISBN', '').strip()
    
     获取图书简介  可能折叠在"更多"里
    summary_elem = (
        soup.select_one('div.booksummary') or 
        soup.select_one('div.intro') or
        soup.select_one('div.desc')
    )
    if summary_elem:
         有些简介有"查看更多"按钮,需要展开
        more_btn = summary_elem.find('button', text=re.compile(r'更多|展开'))
        if more_btn:
             这里可以模拟点击,但静态爬取只能拿现有文本
            pass
        book_data['summary'] = summary_elem.text.strip()
    
    return book_data

重点在于选择器的冗余设计。网站改版是常事,一套选择器失效了,还有备胎。

数据存储的巧思

SQLite用起来简单,但直接插入大量数据容易出问题。我用了事务批量提交和UPSERT:

def save_books_batch(self, books_list):
    """批量保存图书"""
    sql = '''
        INSERT OR REPLACE INTO books 
        (isbn, title, author, publisher, pub_date, summary, cover_url, category)
        VALUES (?, ?, ?, ?, ?, ?, ?, ?)
    '''
    
    data = []
    for book in books_list:
        data.append((
            book.get('isbn'),
            book.get('title'),
            book.get('author'),
            book.get('publisher'),
            book.get('pub_date'),
            book.get('summary'),
            book.get('cover_url'),
            book.get('category')
        ))
    
    try:
        self.cursor.executemany(sql, data)
        self.conn.commit()
        print(f"批量保存 {len(data)} 条记录成功")
    except sqlite3.Error as e:
        print(f"数据库错误: {e}")
        self.conn.rollback()

INSERT OR REPLACE 相当于UPSERT操作,ISBN重复时会自动更新,非常适合定期增量更新。

#### 如何获取ISBN列表

有了爬虫,还需要ISBN。我从几个渠道获取:

  1. 豆瓣读书的公开榜单(注意爬取频率)
  2. 图书馆公开API(有些高校图书馆提供OPAC接口)
  3. 手动导入:写个简单的命令行工具,支持从CSV批量导入ISBN
def import_isbn_from_csv(self, csv_file):
    """从CSV导入ISBN列表"""
    import csv
    
    isbn_list = []
    with open(csv_file, 'r', encoding='utf8') as f:
        reader = csv.DictReader(f)
        for row in reader:
            isbn = row.get('isbn', '').replace('', '')
            if isbn and len(isbn) in [10, 13]:
                isbn_list.append(isbn)
    
    return isbn_list

运行效果

目前我已经爬了大约3000本常用图书的数据,查询速度秒级响应。在本地建了个简单的Flask应用:

from flask import Flask, request, jsonify
import sqlite3

app = Flask(__name__)

@app.route('/search')
def search():
    keyword = request.args.get('q', '')
    
    conn = sqlite3.connect('books.db')
    cursor = conn.cursor()
    
     模糊搜索书名或作者
    cursor.execute('''
        SELECT isbn, title, author, publisher 
        FROM books 
        WHERE title LIKE ? OR author LIKE ?
        LIMIT 20
    ''', (f'%{keyword}%', f'%{keyword}%'))
    
    results = cursor.fetchall()
    conn.close()
    
    return jsonify([{
        'isbn': r[0],
        'title': r[1],
        'author': r[2],
        'publisher': r[3]
    } for r in results])

if __name__ == '__main__':
    app.run(debug=True)

现在找书只需要打开浏览器输入http://localhost:5000/search?q=余华,所有作品一目了然。

踩坑记录

  1. 编码问题:有些老书的简介里有特殊字符,requests默认编码可能解析错误,需要手动指定response.encoding = 'utf8'

  2. 连接池耗尽:爬取几千本书后,requests会报连接错误。解决方案是使用Session并限制最大连接数:

    self.session = requests.Session()
    adapter = requests.adapters.HTTPAdapter(
        pool_connections=10,
        pool_maxsize=20,
        max_retries=3
    )
    self.session.mount('http://', adapter)
    self.session.mount('https://', adapter)
    
  3. 数据去重:同一个ISBN可能对应多个版本的图书,我加了版本字段来区分

最后

这个项目让我意识到,很多看似简单的网站背后,数据价值其实很大。图书大百科的数据质量不错,如果能合理利用,完全可以打造一个私人图书馆管理系统。

代码已经整理好放在GitHub上,有需要的朋友自取。如果你也有图书管理的需求,不妨动手试试,过程本身就是一种乐趣。

(PS:爬虫要遵守robots.txt,我看了book.qciss.net/robots.txt,没有禁止爬取,但还是加了延时,做个体面人。)
图书大百科book.qciss.net

posted on 2026-03-02 14:47  yqqwe  阅读(0)  评论(0)    收藏  举报