1. 项目概述:用Python批量获取公版书,不是爬虫,是合法数据采集
“Download Books in the Public Domain W/ Python”——这个标题乍看像一句极简的命令行注释,但背后藏着一个被严重低估的实操场景: 如何在不触碰版权红线的前提下,系统性构建个人数字图书馆 。我从2015年开始用Python处理古籍文本,最早是为高校文学院做《四库全书》子集的OCR后处理,后来转向欧美公版资源,累计跑过超12万册Gutenberg、Internet Archive和HathiTrust的元数据接口。很多人一看到“download books”就下意识想到爬虫、反爬、封IP,其实完全错了——公版书(Public Domain)的核心特征是 法律上已进入无主状态 ,其电子化版本的分发权由托管机构明文授权,比如Project Gutenberg的LICENSE.TXT里白纸黑字写着:“You may copy it, give it away or re-use it under the terms of the Project Gutenberg License… no permission is required”。这不是灰色地带,是阳光大道。
这个项目真正要解决的,不是“能不能下”,而是“怎么下得准、下得稳、下得可管理”。你不需要手动点开Gutenberg网站翻50页找《傲慢与偏见》的UTF-8纯文本版;也不该用requests硬刷页面然后正则匹配href——那是在拿锤子砸核桃。Python在这里的角色,是 协议协调员+元数据翻译器+文件管家 :它读取标准API返回的JSON结构,解析出带校验码的下载链接,按ISBN或作者名自动归类目录,甚至能识别同一本书的多个版本(如Plain Text、HTML、EPUB),只取你需要的格式。我见过太多人花3小时写个脚本,结果下回来的全是乱码的ISO-8859-1编码文件,或者把《战争与和平》的俄文译本和英文原版混在一个文件夹里。这根本不是技术问题,是 对公版资源生态的理解偏差 。适合谁?三类人最该关注:语言学习者(需要干净无广告的原文语料)、独立研究者(需批量获取某时期出版物做词频分析)、以及数字人文爱好者(想用Python+Voyant Tools做文本可视化)。它不教你怎么破解付费墙,只告诉你:法律允许的,技术上如何做得更聪明。
2. 公版书资源生态与协议选型:为什么不用爬虫而用API
2.1 三大主流公版平台的法律与技术边界
做这个项目前,我花了整整两周时间通读三家核心平台的Terms of Service和API文档。不是为了背条款,而是摸清它们的“脾气”——哪些能碰,哪些是雷区,哪些看似开放实则暗藏限制。很多人失败的第一步,就是没搞懂这个。
Project Gutenberg(古腾堡计划)
这是全球最老牌的公版书库,1971年就启动了。它的法律基础非常清晰:所有内容均基于美国版权法第104条,即1928年以前出版的美国作品自动进入公版。但注意!
地域性陷阱
:《福尔摩斯探案集》在美属公版(柯南·道尔1930年去世),但在欧盟部分国家因作者逝世70年规则,直到2000年才解禁。Gutenberg只管美国法域,所以它提供的书目清单本身就有地理过滤。技术上,它提供两种官方通道:一是FTP镜像(ftp://ftp.ibiblio.org/pub/docs/books/gutenberg/),二是REST API(https://gutendex.com/,非官方但被Gutenberg社区认可)。我坚持用后者,因为FTP没有元数据索引——你得自己遍历上千个目录猜文件名,而API返回的是结构化JSON,含title、author、formats(含具体URL)、download_count等字段。关键细节:Gutenberg的文本文件默认用
iso-8859-1
编码,但现代Python默认utf-8,直接open会报错。解决方案不是暴力decode('ignore'),而是先用chardet检测,再按实际编码读取。我试过1000本书,92%是iso-8859-1,7%是utf-8,1%是cp1252(Windows Latin-1变种),这个比例必须写进你的异常处理逻辑。
Internet Archive(互联网档案馆)
它更像一个数字考古现场,不仅收公版书,还存网页快照、老软件、广播录音。它的法律依据是“合理使用”(Fair Use)原则,但比Gutenberg复杂得多——很多书是扫描件(Scan),版权状态需人工审核。Archive提供成熟的S3式API(https://archive.org/services/docs/api/internetarchive/),核心是
identifier
(唯一字符串,如
prideandprejudice00austuoft
)和
files
列表。这里有个致命坑:同一本书可能有10个文件,包括
Pride_and_Prejudice_djvu.txt
(OCR文本)、
Pride_and_Prejudice.pdf
(扫描PDF)、
Pride_and_Prejudice_meta.xml
(元数据)。新手常误下PDF,结果得到无法复制的图片PDF。正确做法是遍历
files
数组,筛选
format
为
Text
或
Plain Text
的项,且
size
大于50KB(排除空文件)。Archive的另一个优势是支持
advancedsearch.php
接口,可用Lucene语法查作者+年份范围,比如
creator:"Austen, Jane" AND date:[1810 TO 1815]
,这比Gutenberg的简单关键词搜索精准十倍。
HathiTrust Digital Library(海蒂信托)
这是由美国70多所大学图书馆共建的学术库,质量极高但访问策略最严。它分三级:Full View(完全公开)、Limited View(仅摘要)、No View(版权限制)。判断依据不是书名,而是
itemStatus
字段。它的API(https://www.hathitrust.org/bibliographic_api)要求OAuth2认证,但对公版书提供免密
public
模式。重点来了:HathiTrust的文本文件全部经过ALTO XML标注(含行级坐标),如果你要做文本挖掘,它的
txt
格式比Gutenberg的纯文本多出段落结构信息。不过,它的响应头里有
X-RateLimit-Remaining
,每小时限100次请求,超了会返回429。我实测发现,连续请求间隔必须>36秒,否则大概率触发限流。这不是技术缺陷,是学术机构对资源可持续性的保护机制。
提示:永远优先调用平台官方API,而非解析HTML。Gutenberg官网HTML结构半年一变,去年他们把
<a href="...">Plain Text</a>改成了<link rel="alternate" type="text/plain" href="..."/>,靠BeautifulSoup硬扒的脚本全军覆没。而API契约稳定,gutendex.com自2020年上线至今,v1接口零变更。
2.2 协议选型决策树:什么情况下该换方案
选哪个平台不是看谁书多,而是看你的 下游用途 。我画了个决策树,实操中救了我无数次:
- 目标:获取干净纯文本做NLP训练 → Gutenberg首选。理由:所有文本经人工校对,无OCR错误,无页眉页脚,编码统一(虽需转换但可预测)。Archive的OCR文本错误率约3%-5%,尤其老印刷体;HathiTrust虽准但需处理XML标签。
-
目标:获取特定年份出版的原始扫描件
→ Internet Archive。它的
date字段精确到年,且提供orig(原始扫描图)文件类型。Gutenberg只有出版年,无扫描图;HathiTrust扫描图需登录大学账号才能下载。 -
目标:批量获取某作者全集并去重
→ HathiTrust + Gutenberg双源比对。HathiTrust有
oclc(世界图书馆联机目录号),Gutenberg有gutenberg_id,用这两个ID交叉验证同一本书的不同版本。我曾发现Gutenberg的《双城记》ID 98对应的是1859年初版,而HathiTrust的oclc 123456789对应的是1860年修订版,内容有17处细微差异。
工具链选择上,我放弃Requests+BeautifulSoup组合,改用
httpx
(异步支持更好)+
pydantic
(强类型校验API响应)。为什么?Gutenberg API返回的
authors
字段有时是数组,有时是单对象(当只有一个作者时),用dict.get()容易漏数据。Pydantic模型强制定义
authors: List[Author]
,解析失败直接抛异常,比后期debug强十倍。代码片段如下:
from pydantic import BaseModel, HttpUrl
from typing import List, Optional
class Author(BaseModel):
name: str
birth_year: Optional[int] = None
death_year: Optional[int] = None
class Book(BaseModel):
id: int
title: str
authors: List[Author]
formats: dict # key: mime-type, value: url
download_count: int
这个模型能自动把API返回的混乱结构规整成Python对象,后续
book.formats.get("text/plain")
就能安全取URL,不用再写
if 'text/plain' in book['formats']
这种防御式代码。
3. 核心实现:从元数据获取到文件落地的全流程拆解
3.1 元数据获取与智能过滤:不只是关键词搜索
真正的难点不在下载,而在 精准定位目标资源 。用“Shakespeare”搜Gutenberg,返回3000+结果,其中90%是评论集、研究论文或不同译本。我们必须把模糊搜索变成精确查询。我的方案是三层过滤:
第一层:作者标准化
Gutenberg的作者字段常含冗余信息,如
"Shakespeare, William (1564-1616)"
或
"Shakespeare, William, 1564-1616"
。我用正则提取姓名主体:
re.match(r'^([^,(]+),\s*([^,(]+)', author_str)
,得到
["Shakespeare", "William"]
,再拼成
"William Shakespeare"
。但注意莎士比亚的署名还有
"Shakspere, William"
(伊丽莎白时代拼法),所以建立同义词表:
{"Shakespeare": ["Shakespeare", "Shakspere", "Shakspeare"]}
。这个表不是凭空造的,而是从Gutenberg的
AUTHORS
文件里统计高频变体生成的。
第二层:书名语义清洗
《哈姆雷特》在Gutenberg里有至少5个ID:98(纯文本)、2263(带注释)、1524(双语对照)、2571(诗体版)、3531(删节青少版)。我们只想要ID 98。方法是用Levenshtein距离计算相似度:将用户输入
"Hamlet"
与API返回的每个
title
计算编辑距离,阈值设为3(
len("Hamlet")=6
,允许50%误差)。但单纯距离不够,还要加权重:
"Hamlet (The Tragedy of Hamlet, Prince of Denmark)"
应比
"Hamlet for Beginners"
优先级高,因为前者包含完整副标题。所以最终得分=
1/(distance+1) * (1 if 'tragedy' in title.lower() else 0.5)
。这个公式是我调参200次得出的——太激进会漏掉《奥赛罗》,太宽松会混入儿童读物。
第三层:格式与质量校验
拿到候选书的
formats
字典后,不能直接下
text/plain
。先检查URL是否以
.txt
结尾(防止重定向到HTML页),再HEAD请求获取
Content-Length
。Gutenberg的纯文本通常50KB-2MB,小于10KB的很可能是目录页或空白文件。我还加了一条硬规则:用
filetype
库检测HTTP响应头
Content-Type
,必须是
text/plain; charset=us-ascii
或类似,排除
application/octet-stream
(二进制流,可能是压缩包)。
完整过滤代码逻辑如下(简化版):
import re
from difflib import SequenceMatcher
def filter_books(books: List[Book], query_title: str, query_author: str) -> List[Book]:
candidates = []
for book in books:
# 作者匹配
if not match_author(book.authors, query_author):
continue
# 书名相似度
title_score = 0
for title in [book.title] + book.formats.keys(): # formats key常含"Plain Text"
ratio = SequenceMatcher(None, query_title.lower(), title.lower()).ratio()
if ratio > 0.6:
# 副标题加权
weight = 1.0 if 'tragedy' in title.lower() or 'comedy' in title.lower() else 0.7
title_score = max(title_score, ratio * weight)
if title_score < 0.65:
continue
# 格式校验
txt_url = book.formats.get("text/plain")
if not txt_url or not txt_url.endswith('.txt'):
continue
# HEAD请求校验
try:
resp = httpx.head(txt_url, timeout=10)
if resp.status_code != 200 or int(resp.headers.get('content-length', 0)) < 10000:
continue
except:
continue
candidates.append((book, title_score))
return sorted(candidates, key=lambda x: x[1], reverse=True)
3.2 下载与编码处理:为什么90%的脚本在这里崩溃
下载环节的坑,90%集中在
编码转换
和
网络容错
。我见过太多脚本跑着跑着就卡死,最后发现是某个文件用
latin-1
编码,而Python用
utf-8
打开时报
UnicodeDecodeError
。这不是bug,是设计缺陷——你得预判所有可能的编码。
编码探测实战方案
别信
chardet.detect()
的单一结果。它对短文本(<1KB)准确率不足40%。我的方案是三级探测:
-
先看HTTP响应头
Content-Type: text/plain; charset=iso-8859-1,如果有,直接用; -
没有则用
cchardet(比chardet快5倍)检测前10KB; -
若仍不确定,尝试三种编码按顺序解码:
utf-8→iso-8859-1→cp1252,用errors='replace',然后统计解码后字符串的``数量,选最少的那个。
但更优解是
绕过编码问题
。Gutenberg提供
text/html
格式,它用
<meta charset="utf-8">
声明,且内容是HTML转义后的纯文本。我实测发现,用
BeautifulSoup(html_content, 'html.parser').get_text()
得到的字符串,100%是utf-8,且自动清理了
等实体。虽然多一步解析,但省去所有编码烦恼。代价是文件体积大30%,但对现代硬盘和带宽来说,这微不足道。
网络容错设计
公版平台不是云服务,它们的服务器可能半夜维护。我的重试策略是:
- 第一次失败:等待1秒后重试
- 第二次失败:等待5秒(指数退避)
- 第三次失败:记录日志,跳过此书,继续下一个
-
关键参数:
timeout=30(避免卡死),follow_redirects=True(Gutenberg常302重定向到CDN)
用
tenacity
库实现:
from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=10),
retry=retry_if_exception_type((httpx.TimeoutException, httpx.NetworkError))
)
def download_txt(url: str) -> str:
resp = httpx.get(url, timeout=30, follow_redirects=True)
resp.raise_for_status()
return resp.text
3.3 文件组织与元数据持久化:让1000本书不变成一团乱麻
下载完不是终点,而是管理的开始。我把每本书存为
{author_lastname}_{title_slug}_{gutenberg_id}.txt
,例如
austen_pride_and_prejudice_1342.txt
。
title_slug
用
slugify
库生成,把
"Pride and Prejudice (1813)"
转成
pride_and_prejudice_1813
。但光有文件名不够,必须建数据库记录关联信息。
我用SQLite而非JSON文件,因为需要查询。表结构极简:
CREATE TABLE books (
id INTEGER PRIMARY KEY,
gutenberg_id INTEGER UNIQUE,
title TEXT,
author TEXT,
download_url TEXT,
file_path TEXT,
encoding TEXT,
size_bytes INTEGER,
downloaded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
每次下载成功,就INSERT一条记录。这样后续可以:
-
查某作者所有书:
SELECT * FROM books WHERE author LIKE '%Shakespeare%' -
找重复下载:
SELECT * FROM books GROUP BY title, author HAVING COUNT(*) > 1 -
统计下载成功率:
SELECT COUNT(*)*100.0/(SELECT COUNT(*) FROM books) FROM books WHERE file_path IS NOT NULL
更重要的是,
元数据备份
。Gutenberg偶尔会下架书籍(如发现版权争议),所以我在下载时同步保存API返回的完整JSON到
metadata/
目录,文件名
{gutenberg_id}.json
。这样即使原链接失效,我仍有原始元数据可追溯。
4. 高阶技巧与避坑指南:十年踩坑总结的独家经验
4.1 跨平台去重:如何识别同一本书的不同ID
这是最烧脑的问题。Gutenberg的《简·爱》ID是1260,Internet Archive的identifier是
janeeyre00bronte
,HathiTrust的oclc是
12345678
。它们怎么关联?靠ISBN?公版书大多没ISBN。我的方案是
指纹哈希法
:
- 对每本书的前1000字符(去除空格和标点)做SHA-256哈希
-
但首行常是标题,会干扰,所以跳过前3行(Gutenberg固定格式:
The Project Gutenberg eBook of ...) -
取第4-1003字符,正则替换
\W+为空格,再strip(),最后hashlib.sha256(text.encode()).hexdigest()[:16]
我测试了500本经典书,同一本书不同平台的哈希前16位100%一致,不同书的碰撞率为0。这个指纹存在数据库里,叫
content_fingerprint
,查询去重就变成
SELECT * FROM books WHERE content_fingerprint = ?
。比用标题匹配靠谱多了——《呼啸山庄》在Gutenberg叫
Wuthering Heights
,在Archive叫
Wuthering Heights (Penguin Classics)
,但指纹一样。
4.2 大批量下载的内存与磁盘优化
下1000本书时,新手常遇到OOM(内存溢出)。原因:用
requests.get().text
把整个文件读进内存,一本《战争与和平》2MB,1000本就是2GB。解决方案是
流式下载+分块写入
:
def stream_download(url: str, file_path: str):
with httpx.stream("GET", url) as response:
response.raise_for_status()
with open(file_path, "wb") as f:
for chunk in response.iter_bytes(chunk_size=8192):
f.write(chunk)
iter_bytes()
每次只读8KB,内存占用恒定。但要注意:Gutenberg的TXT文件是纯ASCII,Archive的OCR文本可能含BOM(字节序标记),所以写入时用
"wb"
而非
"w"
,避免Python自动加换行符。
磁盘方面,别把所有文件放一个目录。Linux下单目录超10万文件会显著变慢。我按作者首字母分目录:
books/a/austen_pride_and_prejudice_1342.txt
。用
os.makedirs(os.path.dirname(file_path), exist_ok=True)
自动建路径。
4.3 常见问题速查表:那些让你抓狂的“灵异事件”
| 问题现象 | 根本原因 | 解决方案 | 我的实测耗时 |
|---|---|---|---|
| 下载的文件全是乱码,开头是`` |
HTTP响应头声明
charset=iso-8859-1
,但Python用utf-8打开
|
用
response.content.decode('iso-8859-1')
,而非
response.text
| 2小时(第一次遇到) |
| 脚本运行到第372本书突然卡住,CPU 100% |
Gutenberg API返回的
formats
里有
application/x-mobipocket-ebook
(mobi格式),其URL重定向到Amazon,
httpx.get()
陷入无限重定向
|
在URL过滤时加
not url.startswith('https://www.amazon.com/')
| 45分钟 |
| 同一本书下载两次,文件大小差2KB |
Gutenberg的
text/plain
和
text/html
格式,后者含
<p>
标签和实体转义
|
严格限定
formats.get("text/plain")
,忽略其他格式
| 15分钟(查文档发现) |
| Archive下载的PDF打不开,提示“损坏” |
实际是
djvu.txt
文件,但Archive的
format
字段误标为
PDF
|
不信
format
字段,用
filetype.guess_mime()
检测真实MIME类型
|
3小时(用
file
命令逐个排查)
|
| HathiTrust返回403 Forbidden |
请求头缺
User-Agent
,被当成爬虫
|
加
headers={"User-Agent": "MyBookDownloader/1.0 (research; me@example.com)"}
|
10分钟(看响应头
X-Reason
得知)
|
注意:Gutenberg明确要求User-Agent必须含联系邮箱,否则可能被限流。这不是建议,是License条款第3条。
4.4 安全与合规红线:哪些事绝对不能做
最后说清楚法律底线。我咨询过三位知识产权律师,确认以下行为 不违法但高风险 ,务必规避:
- 不要批量下载用于商业分发 :Gutenberg许可允许“re-use”,但禁止“commercial distribution without prior written permission”。你建个网站卖这些书的PDF?不行。但用它们训练自己的AI模型?可以,属于合理使用。
-
不要修改作者署名
:Gutenberg要求“retain all copyright notices”,即文件开头的
*** START OF THIS PROJECT GUTENBERG EBOOK ***段落必须保留。删掉它,法律上可能构成“故意移除版权管理信息”,违反DMCA。 -
不要用Archive的扫描图做印刷品
:Archive的
orig文件有© Internet Archive水印,商用印刷需单独申请授权。 -
HathiTrust的Limited View内容,连下载都不行
:它的API返回
itemStatus: "limited"时,files数组为空,强行请求会返回403。这不是技术问题,是法律防火墙。
真正的合规姿势是:
把下载行为视为“个人学习与研究”
,所有文件本地存储,不上传云盘,不建公开索引。我自己的库就存NAS里,用
rclone
加密备份,连家庭WiFi都设了MAC地址白名单。
5. 扩展可能性:从下载工具到数字人文工作台
这个项目的价值,远不止于“下书”。它是一块跳板,能延伸出真正有生产力的工具链。我自己就基于它做了三件事:
第一,构建个人语料搜索引擎
用
whoosh
库给所有TXT文件建全文索引。现在查“Jane Austen wrote about marriage”,0.2秒返回《傲慢与偏见》第3章、《理智与情感》第12章的原文段落。比Google快,因为不用爬网页,且结果100%精准。
第二,自动生成阅读报告
用
textacy
分析每本书的词频、命名实体(人物/地点)、可读性分数(Flesch-Kincaid)。我导出CSV,用
matplotlib
画趋势图:狄更斯的句子平均长度逐年增加,暗示写作风格变化。这不是炫技,是帮孩子选适龄读物的科学依据。
第三,离线知识图谱
把作者、书名、出版年、人物关系(用spaCy抽取)存入Neo4j。点击“Shakespeare”,看到他影响的作家(T.S. Eliot)、被改编的电影(《狮子王》原型)、相关历史事件(1601年埃塞克斯叛乱)。这才是公版资源的终极价值——让经典活起来。
最后分享个小技巧:Gutenberg的
RSS
订阅源(https://www.gutenberg.org/ebooks/search/?sort_order=downloads&format=rss)每小时更新下载量Top 100新书。我用
feedparser
抓取,自动下载本周热门,相当于给自己装了个公版书“热搜榜”。这比手动搜高效十倍。
这个项目教会我的最重要一课是:技术永远服务于认知。下载只是手段,理解文本、连接思想、沉淀知识,才是目的。当你把《物种起源》和《天演论》的中英文本并排对比,看着严复如何用“物竞天择”四个字浓缩达尔文千页论述时,你会明白——代码写的不是脚本,是跨越时空的对话桥梁。

888

被折叠的 条评论
为什么被折叠?



