1. 项目概述与目标
最近在做一个图书价格监控的小工具,需要定期采集"Books to Scrape"网站的图书数据。这个网站是专门为爬虫练习设计的,包含了完整的图书分类、详情页和价格信息。通过Python爬虫技术,我们可以把这些数据采集下来,构建本地的图书价格情报库。
提示:Books to Scrape网站是一个模拟真实书店的练习平台,非常适合爬虫初学者练手。它包含了完整的图书分类、详情页、价格和库存信息,但不会触发复杂的反爬机制。
2. 技术选型与工具准备
2.1 为什么选择Requests + BeautifulSoup组合
在Python爬虫生态中,Requests和BeautifulSoup是最经典的组合之一。我选择它们主要基于以下几点考虑:
- 轻量级:相比Scrapy等框架,这个组合更加轻量,适合中小规模的爬取任务
- 学习曲线平缓:对新手友好,API设计直观易懂
- 灵活性高:可以自由定制请求头和解析逻辑
- 社区支持好:遇到问题容易找到解决方案
2.2 环境配置与依赖安装
首先确保你已经安装了Python 3.6+版本。然后通过pip安装必要的库:
bash复制pip install requests beautifulsoup4 pandas
- requests:用于发送HTTP请求
- beautifulsoup4:HTML解析库
- pandas:数据处理和CSV导出
3. 爬虫核心实现
3.1 网站结构分析
在开始编码前,我们需要先了解目标网站的结构:
- 首页:http://books.toscrape.com/
- 分类页面:如http://books.toscrape.com/catalogue/category/books/travel_2/index.html
- 图书详情页:如http://books.toscrape.com/catalogue/its-only-the-himalayas_981/index.html
每个图书条目包含以下关键信息:
- 书名
- 价格
- 评分
- 库存状态
- 图书描述
- 产品信息(UPC、类型等)
3.2 基础爬取流程设计
我们的爬虫将按照以下步骤工作:
- 从首页获取所有分类链接
- 遍历每个分类页面,获取图书列表
- 进入每本图书的详情页,提取完整信息
- 将数据保存到CSV文件
3.3 请求层实现
首先实现一个可靠的请求函数,处理网络请求和异常:
python复制import requests
from requests.exceptions import RequestException
def fetch_page(url, headers=None, timeout=10):
"""
发送HTTP请求并返回响应内容
:param url: 目标URL
:param headers: 自定义请求头
:param timeout: 超时时间(秒)
:return: 响应文本或None
"""
default_headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
}
final_headers = {**default_headers, **(headers or {})}
try:
response = requests.get(url, headers=final_headers, timeout=timeout)
response.raise_for_status()
return response.text
except RequestException as e:
print(f"请求失败: {url}, 错误: {e}")
return None
3.4 解析层实现
使用BeautifulSoup解析HTML并提取数据:
python复制from bs4 import BeautifulSoup
def parse_book_list(html):
"""
解析图书列表页,提取图书链接
:param html: 页面HTML内容
:return: 图书链接列表
"""
soup = BeautifulSoup(html, 'html.parser')
book_links = []
for article in soup.select('article.product_pod'):
link = article.h3.a['href']
# 处理相对路径
if not link.startswith('http'):
link = 'http://books.toscrape.com/catalogue/' + link.replace('../', '')
book_links.append(link)
return book_links
def parse_book_detail(html):
"""
解析图书详情页,提取图书信息
:param html: 页面HTML内容
:return: 图书信息字典
"""
soup = BeautifulSoup(html, 'html.parser')
book_info = {
'title': soup.select_one('h1').text.strip(),
'price': soup.select_one('p.price_color').text.strip(),
'rating': soup.select_one('p.star-rating')['class'][1],
'availability': soup.select_one('p.availability').text.strip(),
'description': soup.select_one('#product_description + p').text.strip() if soup.select_one('#product_description') else '',
}
# 提取产品信息表中的数据
for row in soup.select('table tr'):
key = row.th.text.strip().lower().replace(' ', '_')
value = row.td.text.strip()
book_info[key] = value
return book_info
4. 数据存储与导出
4.1 使用Pandas处理数据
我们将使用Pandas来组织和导出数据:
python复制import pandas as pd
def save_to_csv(data, filename='books_data.csv'):
"""
将数据保存到CSV文件
:param data: 数据列表
:param filename: 输出文件名
"""
df = pd.DataFrame(data)
df.to_csv(filename, index=False, encoding='utf-8-sig')
print(f"数据已保存到 {filename}")
4.2 完整爬取流程
现在我们把所有部分组合起来:
python复制import time
def scrape_books_to_scrape():
base_url = 'http://books.toscrape.com/'
all_books = []
# 1. 获取首页分类
print("正在获取分类列表...")
homepage = fetch_page(base_url)
if not homepage:
return
soup = BeautifulSoup(homepage, 'html.parser')
categories = []
for category in soup.select('.side_categories ul li ul li a'):
category_url = base_url + category['href']
categories.append(category_url)
# 2. 遍历每个分类
for category_url in categories[:3]: # 只爬取前3个分类作为演示
print(f"\n正在处理分类: {category_url}")
category_page = fetch_page(category_url)
if not category_page:
continue
# 3. 获取分类下的图书列表
book_links = parse_book_list(category_page)
# 4. 遍历每本图书
for book_link in book_links[:5]: # 每个分类只爬取5本作为演示
print(f" 正在处理图书: {book_link}")
book_page = fetch_page(book_link)
if not book_page:
continue
# 5. 解析图书详情
book_info = parse_book_detail(book_page)
all_books.append(book_info)
# 礼貌性延迟,避免对服务器造成压力
time.sleep(1)
# 6. 保存数据
if all_books:
save_to_csv(all_books)
else:
print("没有获取到任何图书数据")
if __name__ == '__main__':
scrape_books_to_scrape()
5. 常见问题与解决方案
5.1 请求被拒绝或返回空数据
可能原因:
- 网站检测到爬虫行为
- 请求头不够真实
- IP被临时限制
解决方案:
- 完善请求头,添加Referer、Accept等字段
- 添加随机延迟
- 使用代理IP(对于练习网站通常不需要)
5.2 数据解析失败
可能原因:
- 网页结构发生变化
- CSS选择器写得不准确
解决方案:
- 检查网页源代码,确认元素是否存在
- 使用更宽松的选择器,如
article h3 a而不是.product_pod h3 a - 添加异常处理,跳过解析失败的条目
5.3 编码问题
可能原因:
- 网页使用特殊编码
- CSV导出时编码设置不正确
解决方案:
- 检查响应头中的charset
- 导出CSV时使用
utf-8-sig编码
6. 进阶优化建议
6.1 增加并发处理
使用concurrent.futures实现简单并发:
python复制from concurrent.futures import ThreadPoolExecutor
def scrape_book(book_link):
book_page = fetch_page(book_link)
if not book_page:
return None
return parse_book_detail(book_page)
# 在遍历图书列表时替换为:
with ThreadPoolExecutor(max_workers=5) as executor:
book_infos = list(executor.map(scrape_book, book_links))
all_books.extend([info for info in book_infos if info])
6.2 添加断点续爬功能
将已爬取的URL保存到文件,程序重启时跳过这些URL:
python复制import os
def load_scraped_urls(filename='scraped_urls.txt'):
if os.path.exists(filename):
with open(filename, 'r') as f:
return set(f.read().splitlines())
return set()
def save_scraped_url(url, filename='scraped_urls.txt'):
with open(filename, 'a') as f:
f.write(url + '\n')
# 在爬取每个URL前检查
scraped_urls = load_scraped_urls()
for book_link in book_links:
if book_link in scraped_urls:
continue
# ...爬取逻辑...
save_scraped_url(book_link)
6.3 添加日志记录
使用Python的logging模块记录爬取过程:
python复制import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('scraper.log'),
logging.StreamHandler()
]
)
# 替换print语句为logging
logging.info(f"正在处理分类: {category_url}")
logging.warning(f"请求失败: {url}, 错误: {e}")
7. 项目扩展思路
- 价格监控:定期运行爬虫,比较价格变化,发现折扣图书
- 数据可视化:使用Matplotlib或Seaborn分析图书价格分布
- 分类统计:按分类统计平均价格、评分分布等
- 自动化通知:当发现特定条件的图书(如高评分低价书)时发送邮件通知
这个爬虫项目虽然基础,但包含了爬虫开发的核心要素:请求发送、页面解析、数据存储和异常处理。通过扩展和优化,可以构建出更强大的图书数据采集系统。