1. 项目背景与核心需求
最近在整理个人阅读清单时,发现Goodreads的Listopia功能提供了大量优质书单资源。但手动记录每本书的书名、作者和评分实在太费时间。作为Python开发者,我决定写一个爬虫来自动化这个过程,并实现数据清洗和持久化存储。
这个爬虫需要解决几个关键问题:
- 从Listopia页面准确提取书名和作者信息(经常混杂在HTML标签中)
- 处理评分数据的各种显示格式(星级、文字描述等)
- 清洗数据中的异常值和格式不一致问题
- 提供CSV导出和SQLite存储两种持久化方案
2. 技术选型与工具准备
2.1 核心工具链
import requests from bs4 import BeautifulSoup import pandas as pd import sqlite3 import re选择这些库的原因:
requests:比urllib更人性化的HTTP库,适合快速开发BeautifulSoup:HTML解析的瑞士军刀,对标签处理特别友好pandas:数据清洗和CSV导出的最佳选择sqlite3:Python内置,无需额外安装,适合小型项目持久化
2.2 开发环境配置
建议使用虚拟环境避免依赖冲突:
python -m venv goodreads-scraper source goodreads-scraper/bin/activate # Linux/Mac goodreads-scraper\Scripts\activate # Windows pip install requests beautifulsoup4 pandas3. 页面分析与爬取策略
3.1 Goodreads页面结构解析
以"Best Books of 2022"书单为例:
https://www.goodreads.com/list/show/175269.Best_Books_of_2022关键DOM结构:
<tr itemscope itemtype="http://schema.org/Book"> <td class="number">1</td> <td class="cover"> <a href="/book/show/12345"><img...></a> </td> <td class="main"> <div class="title"> <a href="/book/show/12345" itemprop="url"> <span itemprop="name">Book Title</span> </a> by <span itemprop="author">Author Name</span> </div> <div class="rating">4.12 avg rating</div> </td> </tr>3.2 分页处理逻辑
Goodreads使用传统的页码分页:
base_url = "https://www.goodreads.com/list/show/175269" for page in range(1, total_pages+1): url = f"{base_url}?page={page}" response = requests.get(url, headers=headers) # 解析逻辑...注意:务必设置合理的请求间隔(建议3-5秒),避免触发反爬机制
4. 核心数据提取实现
4.1 书名与作者提取
def extract_book_info(book_element): title = book_element.find("span", itemprop="name").text.strip() author = book_element.find("span", itemprop="author").text.strip() # 处理作者名中的冗余信息 author = re.sub(r",.*$", "", author) # 去除",..."部分 author = re.sub(r"\s+", " ", author) # 合并多余空格 return title, author4.2 评分数据解析
评分可能以多种形式出现:
- "4.12 avg rating" → 提取4.12
- "really liked it 4.00" → 提取4.00
- "★★★★☆" → 转换为4.0
处理代码:
def parse_rating(rating_text): # 匹配数字评分 num_match = re.search(r"(\d+\.\d{2})", rating_text) if num_match: return float(num_match.group(1)) # 处理星级评分 star_count = rating_text.count("★") half_star = 0.5 if "½" in rating_text else 0 return star_count + half_star5. 数据清洗与标准化
5.1 常见数据问题处理
def clean_data(df): # 处理缺失值 df["rating"] = df["rating"].fillna(0) # 统一书名格式 df["title"] = df["title"].str.title() # 作者名标准化 df["author"] = df["author"].apply( lambda x: re.sub(r"^(by|and)\s+", "", x, flags=re.IGNORECASE) ) return df5.2 数据验证
添加合理性检查:
def validate_row(row): if not row["title"] or len(row["title"]) < 2: return False if row["rating"] < 0 or row["rating"] > 5: return False return True df = df[df.apply(validate_row, axis=1)]6. 持久化存储实现
6.1 CSV导出
def save_to_csv(df, filename): df.to_csv(filename, index=False, encoding="utf-8-sig") # 添加BOM头解决Excel中文乱码问题 with open(filename, "r+", encoding="utf-8-sig") as f: content = f.read() f.seek(0) f.write("\ufeff" + content)6.2 SQLite存储
def save_to_sqlite(df, db_name="books.db"): conn = sqlite3.connect(db_name) df.to_sql("books", conn, if_exists="replace", index=False) conn.close()优化建议:
- 添加索引加速查询
- 使用事务批量插入
- 添加created_at时间戳
7. 反爬策略应对
7.1 请求头设置
headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Accept-Language": "en-US,en;q=0.9", "Referer": "https://www.goodreads.com/", }7.2 代理与重试机制
from time import sleep from random import uniform def safe_request(url, max_retries=3): for _ in range(max_retries): try: sleep(uniform(1, 3)) # 随机延迟 response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200: return response except Exception as e: print(f"Request failed: {e}") return None8. 完整代码结构
# goodreads_scraper.py class GoodreadsScraper: def __init__(self): self.base_url = None self.session = requests.Session() self.session.headers.update(headers) def scrape_list(self, list_url, pages=1): books = [] for page in range(1, pages+1): url = f"{list_url}?page={page}" response = self.safe_request(url) if response: books.extend(self.parse_page(response.text)) return pd.DataFrame(books) def parse_page(self, html): soup = BeautifulSoup(html, "html.parser") books = [] for book in soup.select("tr[itemtype='http://schema.org/Book']"): title, author = self.extract_book_info(book) rating = self.parse_rating(book.select_one(".rating").text) books.append({"title": title, "author": author, "rating": rating}) return books # 包含之前定义的所有方法...9. 实际应用示例
9.1 运行爬虫
scraper = GoodreadsScraper() df = scraper.scrape_list( "https://www.goodreads.com/list/show/175269", pages=3 ) # 数据清洗 df = clean_data(df) # 持久化 save_to_csv(df, "best_books_2022.csv") save_to_sqlite(df, "books.db")9.2 数据分析示例
# 评分最高的10本书 top_books = df.sort_values("rating", ascending=False).head(10) # 作者作品数量统计 author_counts = df["author"].value_counts().head(10)10. 常见问题与解决方案
10.1 数据提取不完整
可能原因:
- 页面结构变化 → 更新CSS选择器
- 动态加载内容 → 考虑使用Selenium
解决方案:
# 添加更健壮的选择器 title = book.find("span", {"itemprop": "name"}) or book.find("a", {"class": "bookTitle"})10.2 编码问题
处理方案:
response.encoding = response.apparent_encoding # 自动检测编码10.3 数据库优化
# 创建索引 conn = sqlite3.connect("books.db") conn.execute("CREATE INDEX IF NOT EXISTS idx_author ON books(author)") conn.close()11. 项目扩展思路
- 增加图书详情抓取:通过图书URL获取更多元数据(ISBN、页数等)
- 集成到阅读管理系统:自动同步到Notion或Readwise
- 可视化分析:使用Matplotlib生成阅读趋势图
- 定时任务:每周自动更新书单数据
这个爬虫项目最实用的部分是它的数据清洗逻辑,特别是处理Goodreads上不规则的作者名字格式。在实际使用中,我发现正则表达式的精细调整可以显著提高数据质量。比如处理"J.K. Rowling, Mary GrandPré (Illustrator)"这种情况,需要特别设计模式匹配。