上周整理书房,面对满架的书突然萌生个想法:这些年到底买了哪些书?哪些买了还没读?能不能按出版社、作者快速检索?
翻了一圈现有的图书管理软件,要么收费,要么云端同步慢得令人发指。我这个人有点强迫症,数据必须掌握在自己手里。于是把目光投向了那个常去的图书信息网站——图书大百科(book.qciss.net)。
为什么是它 为什么是它?
图书大百科的数据质量确实不错,ISBN、封面、简介、作者信息都很全,分类也细。但每次查书都要开浏览器、输网址、敲ISBN,查完还得手动复制粘贴到Excel,这操作太反人类了。
作为一个写过几年Python的人,我第一反应是:写个爬虫,把常用数据扒下来存本地。
技术选型
既然要写爬虫,requests+BeautifulSoup是标配。但这次情况特殊:
- 网站结构不算复杂,但有一些基本的反爬措施
- 数据量不小,单线程爬太慢
- 需要长期维护更新
所以最终方案:
requests:发送HTTP请求
BeautifulSoup4:解析HTML
SQLite3:本地数据库,轻量够用
fakeuseragent:随机UserAgent,避免被ban
time模块:控制爬取速度,做个体面人
核心代码解析 核心代码解析
先看看最基础的爬虫骨架:
import requests
from bs4 import BeautifulSoup
from fake_useragent import UserAgent
import time
import sqlite3class 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能绕过大部分基础防护。

爬虫的绅士礼仪
写爬虫不是打仗,要给对方服务器留点喘息空间。我在代码里加了两个小细节:
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 Nonereturn response.textelif response.status_code == 404:print(f"ISBN {isbn} 不存在")return Noneelse:print(f"请求失败: {response.status_code}")return Noneexcept Exception as e:print(f"请求异常: {e}")return None
这里有两个关键点:
- 随机延时:固定间隔容易被识别为爬虫
- 响应长度判断:很多反爬页面跳转到验证码时,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') orsoup.select_one('div.desc'))if summary_elem:有些简介有"查看更多"按钮,需要展开more_btn = summary_elem.find('button', text=re.compile(r'更多|展开'))if more_btn:这里可以模拟点击,但静态爬取只能拿现有文本passbook_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。我从几个渠道获取:
- 豆瓣读书的公开榜单(注意爬取频率)
- 图书馆公开API(有些高校图书馆提供OPAC接口)
- 手动导入:写个简单的命令行工具,支持从CSV批量导入ISBN
def import_isbn_from_csv(self, csv_file):"""从CSV导入ISBN列表"""import csvisbn_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 sqlite3app = 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=余华,所有作品一目了然。
踩坑记录
-
编码问题:有些老书的简介里有特殊字符,requests默认编码可能解析错误,需要手动指定
response.encoding = 'utf8' -
连接池耗尽:爬取几千本书后,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) -
数据去重:同一个ISBN可能对应多个版本的图书,我加了版本字段来区分
最后
这个项目让我意识到,很多看似简单的网站背后,数据价值其实很大。图书大百科的数据质量不错,如果能合理利用,完全可以打造一个私人图书馆管理系统。
代码已经整理好放在GitHub上,有需要的朋友自取。如果你也有图书管理的需求,不妨动手试试,过程本身就是一种乐趣。
(PS:爬虫要遵守robots.txt,我看了book.qciss.net/robots.txt,没有禁止爬取,但还是加了延时,做个体面人。)
图书大百科book.qciss.net
