作为一名长期从事数据采集工作的开发者,我经常遇到新手朋友对网络爬虫既好奇又畏惧的情况。今天我们就以豆瓣读书Top250这个经典案例,手把手带你掌握Python数据采集的核心要领。这个项目看似简单,却涵盖了请求构造、反爬应对、参数处理等爬虫工程师的必备技能。
豆瓣图书榜单是许多读书爱好者和数据分析师经常参考的数据源,但直接手动复制效率极低。通过Python的requests库,我们可以用不到20行代码实现自动化采集。不过在实际操作中,你会发现简单的请求背后藏着不少门道——从请求头设置到分页参数处理,每个细节都可能影响最终的数据获取效果。
首先确保你的Python环境版本在3.6以上,这是目前大多数爬虫库稳定支持的最低版本。推荐使用虚拟环境管理项目依赖:
bash复制python -m venv douban_scraper
source douban_scraper/bin/activate # Linux/Mac
douban_scraper\Scripts\activate # Windows
安装核心依赖库requests:
bash复制pip install requests
提示:虽然Python自带urllib库,但requests提供了更人性化的API接口,特别适合爬虫新手使用。它的会话管理、自动编码转换等功能能大幅降低开发复杂度。
让我们从最基础的GET请求开始。豆瓣Top250的URL结构非常简单:
python复制import requests
base_url = "https://book.douban.com/top250"
response = requests.get(base_url)
print(response.status_code) # 大概率会得到418或403
如果你直接运行这段代码,很可能会收到403 Forbidden响应。这是因为现代网站都会检测请求头信息,阻止明显的爬虫访问。
服务器主要通过User-Agent识别客户端类型。我们需模拟浏览器请求:
python复制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",
"Accept-Language": "zh-CN,zh;q=0.9",
"Referer": "https://book.douban.com/"
}
完整的请求头应包含这些关键字段:
豆瓣采用start参数控制分页,每页显示25条数据。要获取全部250条数据,需要循环10次:
python复制for start in range(0, 250, 25):
params = {"start": start}
response = requests.get(base_url, headers=headers, params=params)
print(f"正在获取第{start//25 +1}页,状态码:{response.status_code}")
注意:实际项目中应该添加延时,避免高频请求导致IP被封。建议在每次请求后添加:
python复制import time time.sleep(3) # 3秒间隔
获取到响应内容后,我们需要从HTML中提取有效数据。以单本书为例,其HTML结构通常包含:
html复制<tr class="item">
<td width="100">...</td>
<td class="pl2">
<a href="https://book.douban.com/subject/1234567/" title="书名">
书名
</a>
<p class="pl">作者 / 出版社 / 出版年 / 定价</p>
<div class="star clearfix">
<span class="rating_nums">9.0</span>
<span class="pl">(1000人评价)</span>
</div>
</td>
</tr>
安装解析库:
bash复制pip install beautifulsoup4
提取核心数据的示例代码:
python复制from bs4 import BeautifulSoup
soup = BeautifulSoup(response.text, 'html.parser')
for item in soup.find_all('tr', class_='item'):
title = item.find('a')['title']
author_info = item.find('p', class_='pl').get_text()
rating = item.find('span', class_='rating_nums').get_text()
print(f"书名:{title} | 评分:{rating}")
将数据保存为结构化格式:
python复制import csv
with open('douban_top250.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['排名', '书名', '作者', '评分', '评价人数'])
for idx, item in enumerate(items, 1):
writer.writerow([idx, item['title'], item['author'], item['rating'], item['votes']])
对于更复杂的项目,可以使用SQLite或MySQL:
python复制import sqlite3
conn = sqlite3.connect('douban.db')
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS books
(id INTEGER PRIMARY KEY, title TEXT, author TEXT, rating REAL)''')
应对IP封锁的有效方案:
python复制proxies = {
'http': 'http://10.10.1.10:3128',
'https': 'http://10.10.1.10:1080',
}
response = requests.get(url, headers=headers, proxies=proxies)
使用requests的Session对象实现自动重试:
python复制from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
session = requests.Session()
retries = Retry(total=3, backoff_factor=1)
session.mount('https://', HTTPAdapter(max_retries=retries))
python复制import requests
from bs4 import BeautifulSoup
import time
import csv
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)...",
"Accept-Language": "zh-CN,zh;q=0.9"
}
def get_book_info(start):
url = "https://book.douban.com/top250"
params = {"start": start}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
return parse_html(response.text)
except Exception as e:
print(f"获取数据失败:{e}")
return []
def parse_html(html):
soup = BeautifulSoup(html, 'html.parser')
books = []
for item in soup.find_all('tr', class_='item'):
title = item.find('a')['title']
info = item.find('p', class_='pl').get_text().split('/')
author = info[0].strip()
rating = item.find('span', class_='rating_nums').get_text()
books.append({'title': title, 'author': author, 'rating': rating})
return books
def main():
all_books = []
for start in range(0, 250, 25):
print(f"正在抓取第{start//25 +1}页...")
all_books.extend(get_book_info(start))
time.sleep(3)
with open('douban_top250.csv', 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f)
writer.writerow(['排名', '书名', '作者', '评分'])
for idx, book in enumerate(all_books, 1):
writer.writerow([idx, book['title'], book['author'], book['rating']])
if __name__ == '__main__':
main()
可能原因及解决:
检查点:
中文字符处理建议:
python复制response.encoding = 'utf-8' # 显式设置编码
content = response.text.encode('utf-8').decode('unicode_escape')
在实际项目中,我通常会先快速验证可行性,再逐步添加这些高级功能。对于刚开始学习爬虫的朋友,建议先把基础流程跑通,再考虑优化和扩展。豆瓣的页面结构相对稳定,但也要注意随时可能发生的改版。当发现数据抓取异常时,第一件事应该是手动访问页面,确认结构是否发生了变化。