feat: 重构图书墙交互及多源高阶信息抓取注入系统

1. UI 层:
- 抛弃网格卡片结构,改版为沉浸式的“图书墙”(1:1.43 原比例包含模式)。
- 新增悬浮深色遮罩系统(hover-details),解绑评分点击阻塞。
- 过滤系统默认进入“在读”状态页,彻底移除冗余无效的全选按钮。
2. 抓取与刮削层 (scrape.py):
- 抛弃对 ISBN 查询 Google Books 的严重强依赖。
- 重构抓取链路,首选 Goodreads 与特供的 Books.com.tw(博客来) 高精爬取港台原版资料。
- 建立 Amazon/Goodreads 图片去码净化正则,告别低分辨率与畸形图片。
- 修复 bs4 (.select_first) 解析选择器在旧版环境的兼容性异常。
3. Agent AI 数据管线设计 (bookshelf.py):
- 开放基于 --json 与 --cover 的强力直接注入功能。
- 升级 add 命令作为最优先级的录入手段,免去繁琐的 enrich 阶段,直接连带高清图和满载元数据强插。
- 建立入库反重名智能拦截 (title + author 交叉校验),形成完美的防呆与自动化壁垒。
This commit is contained in:
kai
2026-03-26 11:42:43 +08:00
parent 69c34d4faf
commit 0801f7c7df
9 changed files with 684 additions and 133 deletions
+92 -18
View File
@@ -211,6 +211,8 @@ def _parse_douban_html(html):
result["pub_date"] = value
elif "ISBN" in label.upper():
result["isbn"] = value
elif "原作名" in label:
result["title_en"] = value
# 方法2:兜底 — 如果 span.pl 没有拿到,用纯文本
if not result.get("author"):
@@ -262,6 +264,8 @@ def fetch_douban(identifier: str):
return None
data = _parse_douban_html(html)
if data:
data["douban_url"] = url.split("?")[0]
return data
@@ -352,6 +356,8 @@ def _parse_goodreads_html(html):
if cover_tag:
src = cover_tag.get("src", "")
if src and "nophoto" not in src:
# 移除亚马逊 CDN 图片的尺寸限制,如 ._SY475_ 等以获取原图
src = re.sub(r'\._S[YX]\d+_?\.', '.', src)
result["cover_url"] = src
return result if result.get("title") else None
@@ -368,9 +374,9 @@ def fetch_goodreads(identifier: str):
if "goodreads.com/book/show/" in identifier:
url = identifier.split("?")[0].split("&")[0]
elif _is_isbn(identifier):
# 先通过搜索页找到书籍链接
search_url = GOODREADS_SEARCH_URL.format(query=_clean_isbn(identifier))
else:
# 当作搜索关键词(支持 ISBN 或 书名)
search_url = GOODREADS_SEARCH_URL.format(query=_requests.utils.quote(identifier) if _HAS_REQUESTS else urllib.parse.quote(identifier))
_sleep()
search_html = _get(search_url)
if not search_html:
@@ -380,23 +386,21 @@ def fetch_goodreads(identifier: str):
if not match:
return None
url = f"https://www.goodreads.com/book/show/{match.group(1)}"
else:
return None
_sleep()
html = _get(url)
if not html or "goodreads" not in html.lower():
return None
return _parse_goodreads_html(html)
data = _parse_goodreads_html(html)
if data:
data["goodreads_url"] = url.split("?")[0]
return data
def fetch_goodreads_score(isbn: str):
"""只获取 Goodreads 评分(用于补全其他来源的数据)。"""
data = fetch_goodreads(isbn)
if data and data.get("goodreads_score"):
return data["goodreads_score"]
return None
def fetch_goodreads_data_only_score(isbn: str):
"""(已弃用,外层应直接调用 fetch_goodreads 拿全部信息)"""
pass
# ── Google Books API ──────────────────────────────────────────────────
@@ -496,22 +500,92 @@ def fetch_open_library(isbn: str):
return result if result.get("title") else None
# ── 博客来 (Books.com.tw) ──────────────────────────────────────────────
def fetch_books_tw(keyword: str):
"""
搜索博客来并获取第一条书籍信息
URL: https://search.books.com.tw/search/query/key/{keyword}/cat/BKA
"""
import urllib.parse
search_url = f"https://search.books.com.tw/search/query/key/{urllib.parse.quote(keyword)}/cat/BKA"
html = _get(search_url)
if not html: return None
from bs4 import BeautifulSoup
soup = BeautifulSoup(html, "html.parser")
# 找到第一本书
items = soup.select('.table-searchbox .box')
if not items:
return None
first_item = items[0]
result = {}
title_els = first_item.select('.msg h3 a')
if title_els:
result["title"] = title_els[0].get_text(strip=True)
# 封面
img_els = first_item.select('.box_1 img')
if img_els:
img_el = img_els[0]
src = img_el.get("data-original") or img_el.get("src")
if src:
# 博客来图片通常有 &w= 缩放参数,去掉或改大
import re
src = re.sub(r'&w=\d+', '&w=800', src)
src = re.sub(r'&h=\d+', '&h=800', src)
result["cover_url"] = src.replace("https://im1.book.com.tw/", "https://im2.book.com.tw/")
# 作者、出版社、日期信息
info_boxes = first_item.select('.info')
if info_boxes:
info_box = info_boxes[0]
links = info_box.find_all('a')
authors = []
publisher = ""
for a in links:
href = a.get("href", "")
if "adv_author" in href:
authors.append(a.get_text(strip=True))
elif "adv_pub" in href:
publisher = a.get_text(strip=True)
if authors: result["author"] = " / ".join(authors)
if publisher: result["publisher"] = publisher
info_text = info_box.get_text()
import re
m = re.search(r'出版日期[:]\s*(\d{4}-\d{2}-\d{2}|\d{4}-\d{2}|\d{4}/\d{2}/\d{2})', info_text)
if m:
result["pub_date"] = m.group(1).replace("/", "-")
return result
# ── 主入口 ────────────────────────────────────────────────────────────
def fetch_by_isbn(isbn: str):
"""
根据 ISBN 自动选择数据源。
中文 ISBN → 豆瓣 + Goodreads 补 GR 评分。
中文 ISBN → 豆瓣 + Goodreads 补 GR 评分与链接
英文 ISBN → Goodreads 为主力 → Google Books 兜底。
"""
if _is_chinese_isbn(isbn):
result = fetch_douban(isbn)
if result:
# 尝试补全 Goodreads 评分
if not result.get("goodreads_score"):
gr_score = fetch_goodreads_score(isbn)
if gr_score:
result["goodreads_score"] = gr_score
# 尝试补全 Goodreads 评分和链接
gr_data = fetch_goodreads(isbn)
if gr_data:
if gr_data.get("goodreads_score"):
result["goodreads_score"] = gr_data["goodreads_score"]
if gr_data.get("goodreads_url"):
result["goodreads_url"] = gr_data["goodreads_url"]
if not result.get("title_en") and gr_data.get("title"):
result["title_en"] = gr_data["title"]
if not result.get("author_en") and gr_data.get("author"):
result["author_en"] = gr_data["author"]
return result, "豆瓣"
result = fetch_open_library(isbn)
if result: