㊙️本期内容已收录至专栏《Python爬虫实战》,持续完善知识体系与项目实战,建议先订阅收藏,后续查阅更方便~持续更新中!
㊗️爬虫难度指数:⭐⭐
🚫声明:本数据&代码仅供学习交流,严禁用于商业用途、倒卖数据或违反目标站点的服务条款等,一切后果皆由使用者本人承担。公开榜单数据一般允许访问,但请务必遵守"君子协议",技术无罪,责任在人。

全文目录:
-
- [🌟 开篇语](#🌟 开篇语)
- [1️⃣ 标题 && 摘要](#1️⃣ 标题 && 摘要)
- [2️⃣ 背景与需求(Why)](#2️⃣ 背景与需求(Why))
- [3️⃣ 合规与注意事项(必读)](#3️⃣ 合规与注意事项(必读))
- [4️⃣ 技术选型与整体流程(What/How)](#4️⃣ 技术选型与整体流程(What/How))
-
- [静态 vs 动态 vs API](#静态 vs 动态 vs API)
- 整体流程
- [为什么选 requests + lxml?](#为什么选 requests + lxml?)
- [5️⃣ 环境准备与依赖安装(可复现)](#5️⃣ 环境准备与依赖安装(可复现))
- [6️⃣ 核心实现:请求层(Fetcher)](#6️⃣ 核心实现:请求层(Fetcher))
- [7️⃣ 核心实现:解析层(Parser)](#7️⃣ 核心实现:解析层(Parser))
- [8️⃣ 数据存储与导出(Storage)](#8️⃣ 数据存储与导出(Storage))
- [9️⃣ 运行方式与结果展示(必写)](#9️⃣ 运行方式与结果展示(必写))
- [🔟 常见问题与排错(强烈建议读)](#🔟 常见问题与排错(强烈建议读))
-
- [Q1: 抓到的是空页面或返回 403](#Q1: 抓到的是空页面或返回 403)
- [Q2: HTML 抓到了但解析不到数据](#Q2: HTML 抓到了但解析不到数据)
- [Q3: 编码乱码问题](#Q3: 编码乱码问题)
- [Q4: 解析报错 `list index out of range`](#Q4: 解析报错
list index out of range)
- [1️⃣1️⃣ 进阶优化(可选但加分)](#1️⃣1️⃣ 进阶优化(可选但加分))
-
- 并发加速:多线程版本
- [断点续跑:记录已抓取的 URL](#断点续跑:记录已抓取的 URL)
- 日志与监控
- 定时任务:每日自动抓取
- [1️⃣2️⃣ 总结与延伸阅读](#1️⃣2️⃣ 总结与延伸阅读)
- [🌟 文末](#🌟 文末)
-
- [📌 专栏持续更新中|建议收藏 + 订阅](#📌 专栏持续更新中|建议收藏 + 订阅)
- [✅ 互动征集](#✅ 互动征集)
🌟 开篇语
哈喽,各位小伙伴们你们好呀~我是【喵手】。
运营社区: C站 / 掘金 / 腾讯云 / 阿里云 / 华为云 / 51CTO
欢迎大家常来逛逛,一起学习,一起进步~🌟
我长期专注 Python 爬虫工程化实战 ,主理专栏 《Python爬虫实战》:从采集策略 到反爬对抗 ,从数据清洗 到分布式调度 ,持续输出可复用的方法论与可落地案例。内容主打一个"能跑、能用、能扩展 ",让数据价值真正做到------抓得到、洗得净、用得上。
📌 专栏食用指南(建议收藏)
- ✅ 入门基础:环境搭建 / 请求与解析 / 数据落库
- ✅ 进阶提升:登录鉴权 / 动态渲染 / 反爬对抗
- ✅ 工程实战:异步并发 / 分布式调度 / 监控与容错
- ✅ 项目落地:数据治理 / 可视化分析 / 场景化应用
📣 专栏推广时间 :如果你想系统学爬虫,而不是碎片化东拼西凑,欢迎订阅/关注专栏👉《Python爬虫实战》👈
💕订阅后更新会优先推送,按目录学习更高效💯~
1️⃣ 标题 && 摘要
一句话概括:使用 Python + requests + lxml 爬取酒店列表页与详情页,提取酒店基础信息和用户评价摘要,最终输出结构化 CSV/SQLite 数据。
你能获得:
- 掌握列表页 → 详情页两层爬取的经典模式
- 学会 XPath/CSS 选择器的实战容错技巧
- 构建可复用的酒店数据采集与清洗流程
2️⃣ 背景与需求(Why)
为什么要爬酒店数据?
做过旅游规划的人都知道,在不同平台对比酒店时,需要反复跳转、截图、记录价格和评分,效率极低。如果能把目标城市的酒店信息批量抓取下来,做成本地数据库,就可以:
- 数据分析:按区域、星级、评分做聚类分析,找出性价比最高的选择
- 价格监控:定期抓取同一批酒店,观察价格波动趋势
- 信息聚合:整合多平台数据,生成自己的酒店推荐榜单
目标站点与字段清单
本次选择的示例站点是 某酒店聚合平台的静态列表页(为避免法律风险,这里用抽象描述,实际代码中会用真实可访问的测试站点)。
列表页字段:
- 酒店名称 (hotel_name)
- 星级 (star_rating)
- 价格区间 (price_range)
- 综合评分 (overall_score)
- 地址 (address)
- 详情页链接 (detail_url)
详情页字段:
- 酒店介绍 (description)
- 设施标签 (facilities)
- 评价总数 (review_count)
- 最新10条评价摘要 (recent_reviews)
3️⃣ 合规与注意事项(必读)
robots.txt 协议
在开始爬取前,务必查看目标站点的 /robots.txt,确认是否允许爬取目标路径。例如:
json
User-agent: *
Disallow: /admin/
Disallow: /api/payment/
Allow: /hotels/
如果明确禁止,请尊重站点规则,或寻找官方 API。
频率控制原则
- 不要短时间内发起大量请求:建议每个请求间隔 1-3 秒,模拟人类浏览行为
- 避免并发轰炸:初学阶段使用单线程顺序抓取,稳定性优先
- 设置合理的 timeout:防止因网络波动导致程序卡死
数据使用边界
- ❌ 不要采集:用户手机号、身份证、支付信息等敏感数据
- ❌ 不要绕过:付费内容墙、登录验证(除非你有合法账号)
- ✅ 可以采集:公开展示的酒店名称、评分、公开评价等非个人信息
- ✅ 用于:个人学习、数据分析、非商业研究
4️⃣ 技术选型与整体流程(What/How)
静态 vs 动态 vs API
本案例属于静态页面爬取:
- 列表页和详情页的 HTML 都是服务端渲染好的,直接 requests 就能拿到完整内容
- 不涉及 JavaScript 动态加载、不需要 Selenium/Playwright
- 数据藏在 HTML 结构里,用 XPath/CSS 解析即可
如果你遇到的酒店站点是动态加载(比如滚动加载更多),那就需要:
- 抓包找到真实的 JSON API 接口(最优)
- 或使用 Playwright 模拟浏览器(备选)
整体流程
json
┌─────────────┐
│ 输入城市 │
│ 关键词 │
└──────┬──────┘
│
▼
┌─────────────────┐
│ 采集列表页 │ (分页循环)
│ 提取酒店链接 │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 逐个访问详情页 │ (带延时)
│ 提取详细字段 │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 数据清洗 │ (去重/格式化)
│ 异常值处理 │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 存储到 CSV │
│ 或 SQLite │
└─────────────────┘
为什么选 requests + lxml?
- requests:轻量级、API 简洁、支持 session 管理
- lxml:解析速度快(基于 C 库)、XPath 支持完善、内存占用低
- vs BeautifulSoup:bs4 更易读但速度略慢,这里追求效率选 lxml
- vs Scrapy:Scrapy 适合大规模分布式爬取,对于几百条数据的需求过重
5️⃣ 环境准备与依赖安装(可复现)
Python 版本
推荐 Python 3.8+(最低 3.7),因为后续会用到 f-string 和类型注解。
依赖安装
bash
pip install requests lxml pandas
- requests: HTTP 请求库
- lxml: HTML/XML 解析器
- pandas: 数据清洗与导出(可选,如果只用 CSV 可以用标准库 csv)
项目结构
json
hotel_scraper/
│
├── scraper/
│ ├── __init__.py
│ ├── fetcher.py # 请求层
│ ├── parser.py # 解析层
│ └── storage.py # 存储层
│
├── data/
│ └── hotels.csv # 输出文件
│
├── logs/
│ └── scraper.log # 日志文件
│
├── config.py # 配置文件
├── main.py # 入口文件
└── requirements.txt # 依赖清单
6️⃣ 核心实现:请求层(Fetcher)
设计要点
一个健壮的 Fetcher 需要处理:
- 伪装 headers:模拟真实浏览器
- 会话保持:使用 session 复用连接
- 异常重试:网络波动时自动重试
- 超时控制:防止长时间等待
代码实现
python
# scraper/fetcher.py
import requests
import time
import random
from typing import Optional
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
class HotelFetcher:
def __init__(self):
self.session = requests.Session()
# 配置重试策略
retry_strategy = Retry(
total=3, # 最多重试3次
backoff_factor=1, # 重试间隔递增
status_forcelist=[429, 500, 502, 503, 504] # 这些状态码触发重试
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
# 设置默认 headers
self.headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
'Accept-Encoding': 'gzip, deflate, br',
'Connection': 'keep-alive',
'Upgrade-Insecure-Requests': '1'
}
def fetch(self, url: str, delay: tuple = (1, 3)) -> Optional[str]:
"""
获取页面内容
Args:
url: 目标 URL
delay: 延时区间(秒),随机选择
Returns:
页面 HTML 文本,失败返回 None
"""
try:
# 随机延时,类行为
time.sleep(random.uniform(*delay))
response = self.session.get(
url,
headers=self.headers,
timeout=10 # 10秒超时
)
# 检查状态码
response.raise_for_status()
# 指定编码(避免乱码)
response.encoding = response.apparent_encoding
return response.text
except requests.exceptions.Timeout:
print(f"⏱️ 请求超时: {url}")
return None
except requests.exceptions.HTTPError as e:
if e.response.status_code == 403:
print(f"🚫 访问被拒绝(403),可能触发反爬: {url}")
elif e.response.status_code == 404:
print(f"❌ 页面不存在(❗ HTTP错误 {e.response.status_code}: {url}")
return None
except requests.exceptions.RequestException as e:
print(f"🔌 网络异常: {url} | {str(e)}")
return None
关键点说明
-
为什么用 Session?
- 自动管理 Cookie
- 连接池复用,减少 TCP 握手次数
- 可以设置全局 headers
-
重试策略的意义
backoff_factor=1表示第1次重试等1秒,第2次等2秒,第3次等4秒- 避免服务器短暂抖动时直接失败
-
延时的必要性
- 太快会被识别为机器人(触发 429 Too Many Requests)
- 1-3秒的随机延时是一个经验值,可根据实际情况调整
7️⃣ 核心实现:解析层(Parser)
列表页解析
假设列表页的 HTML 结构如下:
html
<div class="hotel-list">
<div class="hotel-item">
<h3 class="name">北京饭店</h3>
<span class="star">★★★★★</span>
<span class="price">¥599起</span>
<span class="score">4.8分</span>
<p class="address">东城区东长安街33号</p>
<a href="/hotel/detail/12345" class="detail-link">查看详情</a>
</div>
<!-- 更多酒店... -->
</div>
代码实现
python
# scraper/parser.py
from lxml import etree
from typing import List, Dict, Optional
class HotelParser:
@staticmethod
def parse_list_page(html: str, base_url: str) -> List[Dict]:
"""
解析列表页,提取酒店基础信息
Args:
html: 页面 HTML
base_url: 基础 URL(用于拼接相对链接)
Returns:
酒店信息列表
"""
tree = etree.HTML(html)
hotels = []
# 获取所有酒店卡片
hotel_items = tree.xpath('//div[@class="hotel-item"]')
for item in hotel_items:
try:
# 提取各字段(使用 text() 获取文本)
name = item.xpath('.//h3[@class="name"]/text()')
star = item.xpath('.//span[@class="star"]/text()')
price = item.xpath('.//span[@class="price"]/text()')
score = item.xpath('.//span[@class="score"]/text()')
address = item.xpath('.//p[@class="address"]/text()')
detail_link = item.xpath('.//a[@class="detail-link"]/@href')
# 容错处理:有些字段可能缺失
hotel = {
'name': name[0].strip() if name else '未知',
'star': star[0].count('★') if star else 0,
'price': price[0].replace('¥', '').replace('起', '').strip() if price else '0',
'score': float(score[0].replace('分', '')) if score else 0.0,
'address': address[0].strip() if address else '地址未公开',
'detail_url': base_url + detail_link[0] if detail_link else ''
}
hotels.append(hotel)
except Exception as e:
print(f"⚠️ 解析单个酒店时出错: {str(e)}")
continue # 跳过这个条目,继续处理下一个
return hotels
@staticmethod
def parse_detail_page(html: str) -> Dict:
"""
解析详情页,提取详细信息和评价
Args:
html: 详情页 HTML
Returns:
详情信息字典
"""
tree = etree.HTML(html)
try:
# 提取酒店介绍
description_nodes = tree.xpath('//div[@class="hotel-intro"]//text()')
description = ''.join(description_nodes).strip() if description_nodes else ''
# 提取设施标签
facilities = tree.xpath('//ul[@class="facilities"]/li/text()')
facilities_str = ', '.join([f.strip() for f in facilities]) if facilities else ''
# 提取评价总数
review_count_text = tree.xpath('//span[@class="review-count"]/text()')
review_count = 0
if review_count_text:
# 处理 "1234条评价" 这种格式
import re
match = re.search(r'\d+', review_count_text[0])
review_count = int(match.group()) if match else 0
# 提取最新10条评价摘要
reviews = tree.xpath('//div[@class="review-item"]//p[@class="content"]/text()')
recent_reviews = [r.strip() for r in reviews[:10]] if reviews else []
return {
'description': description[:500], # 截取前500字
'facilities': facilities_str,
'review_count': review_count,
'recent_reviews': '|||'.join(recent_reviews) # 用特殊分隔符连接
}
except Exception as e:
print(f"⚠️ 解析详情页出错: {str(e)}")
return {
'description': '',
'facilities': '',
'review_count': 0,
'recent_reviews': ''
}
解析技巧总结
-
XPath vs CSS 选择器
- XPath 更强大(支持父节点选择、文本操作)
- CSS 选择器更简洁(适合简单场景)
- 这里选 XPath 是因为需要处理复杂的文本提取
-
容错三板斧
if xxx else 默认值:防止列表为空时索引报错try-except:包裹整个解析逻辑continue:跳过错误项,不中断整体流程
-
处理动态变化的选择器
- 优先用
class而不是id(id 可能是动态生成的) - 使用
contains()函数://div[contains(@class, "hotel")] - 多个备选方案:如果主选择器失败,尝试备选路径
- 优先用
8️⃣ 数据存储与导出(Storage)
字段映射表
| 字段名 | 类型 | 示例值 | 说明 |
|---|---|---|---|
| hotel_id | INTEGER | 12345 | 自增主键 |
| name | TEXT | 北京饭店 | 酒店名称 |
| star | INTEGER | 5 | 星级(0-5) |
| price | TEXT | 599 | 价格(保留原始文本) |
| score | REAL | 4.8 | 综合评分 |
| address | TEXT | 东城区东长安街33号 | 地址 |
| description | TEXT | 位于市中心... | 介绍(截取前500字) |
| facilities | TEXT | WiFi, 停车场, 健身房 | 设施列表 |
| review_count | INTEGER | 1234 | 评价总数 |
| recent_reviews | TEXT | 很干净 | |
| detail_url | TEXT | https://... | 详情页链接 |
| crawl_time | TEXT | 2026-01-29 10:30:00 | 抓取时间 |
存储实现(SQLite)
python
# scraper/storage.py
import sqlite3
from datetime import datetime
from typing import List, Dict
class HotelStorage:
def __init__(self, db_path: str = 'data/hotels.db'):
self.db_path = db_path
self._init_db()
def _init_db(self):
"""初始化数据库表"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS hotels (
hotel_id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
star INTEGER DEFAULT 0,
price TEXT,
score REAL DEFAULT 0.0,
address TEXT,
description TEXT,
facilities TEXT,
review_count INTEGER DEFAULT 0,
recent_reviews TEXT,
detail_url TEXT UNIQUE, -- 唯一约束,防止重复
crawl_time TEXT
)
''')
# 创建索引加速查询
cursor.execute('CREATE INDEX IF NOT EXISTS idx_score ON hotels(score)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_star ON hotels(star)')
conn.commit()
conn.close()
def save_hotels(self, hotels: List[Dict]):
"""
批量保存酒店数据
Args:
hotels: 酒店信息列表(需包含所有字段)
"""
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
success_count = 0
duplicate_count = 0
for hotel in hotels:
try:
cursor.execute('''
INSERT INTO hotels (
name, star, price, score, address,
description, facilities, review_count,
recent_reviews, detail_url, crawl_time
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
''', (
hotel.get('name'),
hotel.get('star', 0),
hotel.get('price', '0'),
hotel.get('score', 0.0),
hotel.get('address', ''),
hotel.get('description', ''),
hotel.get('facilities', ''),
hotel.get('review_count', 0),
hotel.get('recent_reviews', ''),
hotel.get('detail_url', ''),
datetime.now().strftime('%Y-%m-%d %H:%M:%S')
))
success_count += 1
except sqlite3.IntegrityError:
# detail_url 重复,跳过
duplicate_count += 1
continue
conn.commit()
conn.close()
print(f"✅ 成功保存 {success_count} 条数据")
if duplicate_count > 0:
print(f"⏭️ 跳过 {duplicate_count} 条重复数据")
def export_to_csv(self, output_path: str = 'data/hotels.csv'):
"""导出为 CSV 文件"""
import pandas as pd
conn = sqlite3.connect(self.db_path)
df = pd.read_sql_query('SELECT * FROM hotels ORDER BY score DESC', conn)
conn.close()
df.to_csv(output_path, index=False, encoding='utf-8-sig') # utf-8-sig 避免 Excel 乱码
print(f"📊 已导出到 {output_path} ({len(df)} 条记录)")
去重策略
这里使用 detail_url UNIQUE 约束实现去重:
- 如果同一个酒店详情页链接已存在,插入时会抛出
IntegrityError - 捕获异常后跳过,避免重复数据
其他去重方案:
- 内容 hash:对
name + address做 MD5,相同则认为重复 - 定期清理:保留最新抓取的数据,删除旧记录
9️⃣ 运行方式与结果展示(必写)
主程序入口
python
# main.py
from scraper.fetcher import HotelFetcher
from scraper.parser import HotelParser
from scraper.storage import HotelStorage
def main():
# 初始化组件
fetcher = HotelFetcher()
parser = HotelParser()
storage = HotelStorage()
# 配置参数
base_url = 'https://example-hotel-site.com' # 替换为实际站点
city = '北京'
max_pages = 3 # 抓取前3页
print(f"🚀 开始抓取 {city} 酒店数据...")
all_hotels = []
# 1. 遍历列表页
for page in range(1, max_pages + 1):
list_url = f'{base_url}/hotels?city={city}&page={page}'
print(f"\n📄 正在抓取第 {page} 页: {list_url}")
html = fetcher.fetch(list_url)
if not html:
print(f"❌ 第 {page} 页抓取失败,跳过")
continue
# 解析列表页
hotels = parser.parse_list_page(html, base_url)
print(f"✅ 解析到 {len(hotels)} 个酒店")
# 2. 逐个抓取详情页
for i, hotel in enumerate(hotels, 1):
detail_url = hotel['detail_url']
if not detail_url:
continue
print(f" └─ [{i}/{len(hotels)}] {hotel['name']} ...")
detail_html = fetcher.fetch(detail_url)
if not detail_html:
continue
# 解析详情页并合并数据
detail_info = parser.parse_detail_page(detail_html)
hotel.update(detail_info)
all_hotels.append(hotel)
print(f"📦 当前页已完成,累计 {len(all_hotels)} 条数据")
# 3. 保存到数据库
if all_hotels:
storage.save_hotels(all_hotels)
storage.export_to_csv()
print(f"\n🎉 全部完成!共采集 {len(all_hotels)} 条酒店数据")
else:
print("\n⚠️ 未采集到任何数据,请检查配置")
if __name__ == '__main__':
main()
启动命令
bash
python main.py
输出示例(控制台)
json
🚀 开始抓取 北京 酒店数据...
📄 正在抓取第 1 页: https://example-hotel-site.com/hotels?city=北京&page=1
✅ 解析到 20 个酒店
└─ [1/20] 北京饭店 ...
└─ [2/20] 北京国际饭店 ...
...
📦 当前页已完成,累计 20 条数据
📄 正在抓取第 2 页: ...
...
✅ 成功保存 60 条数据
⏭️ 跳过 0 条重复数据
📊 已导出到 data/hotels.csv (60 条记录)
🎉 全部完成!共采集 60 条酒店数据
数据库查询结果示例
sql
SELECT name, star, score, address FROM hotels ORDER BY score DESC LIMIT 5;
| name | star | score | address |
|---|---|---|---|
| 北京饭店 | 5 | 4.9 | 东城区东长安街33号 |
| 北京国际饭店 | 5 | 4.8 | 建国门内大街9号 |
| 王府井希尔顿酒店 | 5 | 4.7 | 东城区王府井大街8号 |
| ... | ... | ... | ... |
🔟 常见问题与排错(强烈建议读)
Q1: 抓到的是空页面或返回 403
原因:
- 网站检测到你的 User-Agent 是爬虫
- IP 被临时封禁(请求过快)
- 需要登录或 Cookie 验证
解决方案:
python
# 方案1:更换 User-Agent
headers['User-Agent'] = 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) ...'
# 方案2:增加延时
time.sleep(random.uniform(3, 6)) # 延长到 3-6 秒
# 方案3:使用代理池(需购买或搭建)
proxies = {
'http': 'http://your-proxy-ip:port',
'https': 'https://your-proxy-ip:port'
}
response = session.get(url, proxies=proxies)
Q2: HTML 抓到了但解析不到数据
原因:
- 页面是动态渲染的(XHR 加载数据)
- XPath 选择器写错了
- HTML 结构变化了
排查步骤:
- 打印
html[:1000]看看前1000个字符有没有目标内容 - 用浏览器 DevTools 复制元素的 XPath 对比你写的
- 如果是动态加载,按 F12 → Network → XHR,找到真实的 JSON 接口
示例:抓接口替代抓 HTML
python
# 假设发现真实接口是这个
api_url = 'https://example.com/api/hotels?city=北京'
response = fetcher.session.get(api_url)
data = response.json() # 直接解析 JSON,比 XPath 简单
Q3: 编码乱码问题
表现 :酒店名称 显示为 é...'åº---å��ç§°
原因:
- 网站用的是 GBK/GB2312 编码,但你用 UTF-8 解码了
解决:
python
# 在 fetcher.py 的 fetch 方法中
response.encoding = response.apparent_encoding # 自动检测编码
# 或手动指定
response.encoding = 'gbk'
Q4: 解析报错 list index out of range
原因:
- 某个字段在部分酒店页面不存在
- XPath 没匹配到任何元素
正确写法:
python
# ❌ 错误:直接取 [0]
name = item.xpath('.//h3/text()')[0]
# ✅ 正确:先判断是否为空
name = item.xpath('.//h3/text()')
name = name[0] if name else '未知'
1️⃣1️⃣ 进阶优化(可选但加分)
并发加速:多线程版本
当你需要抓取几千个酒店详情页时,单线程会非常慢。可以用 ThreadPoolExecutor:
python
from concurrent.futures import ThreadPoolExecutor, as_completed
def fetch_and_parse_detail(hotel_info):
"""抓取并解析单个酒店详情"""
detail_html = fetcher.fetch(hotel_info['detail_url'])
if detail_html:
detail = parser.parse_detail_page(detail_html)
hotel_info.update(detail)
return hotel_info
# 在 main.py 中
with ThreadPoolExecutor(max_workers=5) as executor: # 5个线程并发
futures = [executor.submit(fetch_and_parse_detail, hotel) for hotel in hotels]
for future in as_completed(futures):
result = future.result()
all_hotels.append(result)
注意:并发数不要太大(建议 3-10),否则容易触发反爬。
断点续跑:记录已抓取的 URL
python
# 在开始前读取已有数据
conn = sqlite3.connect('data/hotels.db')
cursor = conn.cursor()
fetched_urls = set([row[0] for row in cursor.execute('SELECT detail_url FROM hotels')])
conn.close()
# 抓取时跳过已有的
for hotel in hotels:
if hotel['detail_url'] in fetched_urls:
print(f"⏭️ 已存在,跳过: {hotel['name']}")
continue
# 正常抓取流程...
日志与监控
python
import logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler('logs/scraper.log'),
logging.StreamHandler()
]
)
# 使用
logging.info(f"开始抓取: {url}")
logging.error(f"抓取失败: {url}")
# 统计成功率
success_rate = success_count / total_count * 100
logging.info(f"成功率: {success_rate:.2f}%")
定时任务:每日自动抓取
方案1:Linux crontab
bash
# 每天凌晨2点执行
0 2 * * * cd /path/to/hotel_scraper && python main.py >> logs/cron.log 2>&1
方案2:Python APScheduler
python
from apscheduler.schedulers.blocking import BlockingScheduler
scheduler = BlockingScheduler()
@scheduler.scheduled_job('cron', hour=2)
def scheduled_crawl():
main()
scheduler.start()
1️⃣2️⃣ 总结与延伸阅读
我们完成了什么?
回顾整个流程,我们从零构建了一个生产级酒店数据采集系统:
✅ 请求层 :模拟浏览器、处理异常、自动重试
✅ 解析层 :XPath 精准提取、多重容错、处理缺失字段
✅ 存储层 :SQLite 持久化、自动去重、CSV 导出
✅ 工程化:模块分离、日志记录、可扩展架构
这套代码不仅能抓酒店,稍加修改就能用于:
- 电商商品信息采集
- 新闻文章聚合
- 招聘信息监控
- 二手房源追踪
下一步可以做什么?
1. 进阶到 Scrapy 框架
Scrapy 是工业级爬虫框架,优势:
- 自动去重、断点续爬
- 分布式部署(配合 Redis)
- 内置管道处理、中间件机制
学习路径:
bash
pip install scrapy
scrapy startproject hotel_spider
推荐教程:Scrapy 官方文档 + 《Python 网络爬虫权威指南》
2. 处理复杂反爬场景
如果目标站点有:
- 滑块验证码 → 使用
ddddocr库或打码平台 - 字体反爬(价格用自定义字体显示) → 解析字体文件映射
- JavaScript 混淆 → 用 Playwright 或逆向分析
工具推荐:
- Playwright:比 Selenium 更快更稳定
- mitmproxy:抓包分析加密参数
3. 数据分析与可视化
有了数据后,可以:
python
import matplotlib.pyplot as plt
import pandas as pd
df = pd.read_csv('data/hotels.csv')
# 星级分布
df['star'].value_counts().plot(kind='bar')
plt.title('Hotel Star Rating Distribution')
plt.show()
# 价格与评分的关系
plt.scatter(df['price'], df['score'])
plt.xlabel('Price')
plt.ylabel('Score')
plt.show()
4. 监控价格波动
定时抓取 + 对比历史价格:
python
# 每天记录价格
cursor.execute('''
INSERT INTO price_history (hotel_id, price, date)
VALUES (?, ?, ?)
''', (hotel_id, current_price, today))
# 发现降价时发邮件通知
if current_price < historical_avg * 0.9:
send_email('价格跌了 10%!')
推荐阅读
📚 书籍:
- 《Python 网络爬虫权威指南》(Ryan Mitchell)
- 《精通 Scrapy 网络爬虫》
🔗 在线资源:
- Scrapy 官方文档:https://docs.scrapy.org
- XPath 速查表:https://devhints.io/xpath
- requests 库文档:https://requests.readthedocs.io
⚖️ 法律与伦理:
- 阅读《网络安全法》相关条款
- 遵守 robots.txt 协议
- 数据仅用于个人学习和非商业研究
最后的话
写爬虫就像在数字世界探险,充满了乐趣和挑战。但请记住:
技术是中性的,关键在于如何使用。
希望这篇教程能帮你打开网络数据采集的大门,在合规的前提下,用代码让生活更便捷!
有任何问题,欢迎在评论区交流!💬✨
🌟 文末
好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!
小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥
📌 专栏持续更新中|建议收藏 + 订阅
专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:
✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)
📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集
想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?
评论区留言告诉我你的需求,我会优先安排更新 ✅
⭐️ 若喜欢我,就请关注我叭~(更新不迷路)
⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)
⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)

免责声明:本文仅用于学习与技术研究,请在合法合规、遵守站点规则与 Robots 协议的前提下使用相关技术。严禁将技术用于任何非法用途或侵害他人权益的行为。技术无罪,责任在人!!!