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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user