Python爬虫实战:采集酒店列表页与详情页,提取酒店基础信息和用户评价摘要等(附CSV导出 + SQLite持久化存储)!

㊙️本期内容已收录至专栏《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️⃣ 进阶优化(可选但加分))
    • [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

关键点说明

  1. 为什么用 Session?

    • 自动管理 Cookie
    • 连接池复用,减少 TCP 握手次数
    • 可以设置全局 headers
  2. 重试策略的意义

    • backoff_factor=1 表示第1次重试等1秒,第2次等2秒,第3次等4秒
    • 避免服务器短暂抖动时直接失败
  3. 延时的必要性

    • 太快会被识别为机器人(触发 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': ''
            }

解析技巧总结

  1. XPath vs CSS 选择器

    • XPath 更强大(支持父节点选择、文本操作)
    • CSS 选择器更简洁(适合简单场景)
    • 这里选 XPath 是因为需要处理复杂的文本提取
  2. 容错三板斧

    • if xxx else 默认值:防止列表为空时索引报错
    • try-except:包裹整个解析逻辑
    • continue:跳过错误项,不中断整体流程
  3. 处理动态变化的选择器

    • 优先用 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 结构变化了

排查步骤:

  1. 打印 html[:1000] 看看前1000个字符有没有目标内容
  2. 用浏览器 DevTools 复制元素的 XPath 对比你写的
  3. 如果是动态加载,按 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 网络爬虫》

🔗 在线资源:

⚖️ 法律与伦理:

  • 阅读《网络安全法》相关条款
  • 遵守 robots.txt 协议
  • 数据仅用于个人学习和非商业研究

最后的话

写爬虫就像在数字世界探险,充满了乐趣和挑战。但请记住:

技术是中性的,关键在于如何使用。

希望这篇教程能帮你打开网络数据采集的大门,在合规的前提下,用代码让生活更便捷!

有任何问题,欢迎在评论区交流!💬✨

🌟 文末

好啦~以上就是本期 《Python爬虫实战》的全部内容啦!如果你在实践过程中遇到任何疑问,欢迎在评论区留言交流,我看到都会尽量回复~咱们下期见!

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦~
三连就是对我写作道路上最好的鼓励与支持! ❤️🔥

📌 专栏持续更新中|建议收藏 + 订阅

专栏 👉 《Python爬虫实战》,我会按照"入门 → 进阶 → 工程化 → 项目落地"的路线持续更新,争取让每一篇都做到:

✅ 讲得清楚(原理)|✅ 跑得起来(代码)|✅ 用得上(场景)|✅ 扛得住(工程化)

📣 想系统提升的小伙伴:强烈建议先订阅专栏,再按目录顺序学习,效率会高很多~

✅ 互动征集

想让我把【某站点/某反爬/某验证码/某分布式方案】写成专栏实战?

评论区留言告诉我你的需求,我会优先安排更新 ✅


⭐️ 若喜欢我,就请关注我叭~(更新不迷路)

⭐️ 若对你有用,就请点赞支持一下叭~(给我一点点动力)

⭐️ 若有疑问,就请评论留言告诉我叭~(我会补坑 & 更新迭代)


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

相关推荐
2301_790300968 小时前
Python单元测试(unittest)实战指南
jvm·数据库·python
Data_Journal8 小时前
Scrapy vs. Crawlee —— 哪个更好?!
运维·人工智能·爬虫·媒体·社媒营销
VCR__8 小时前
python第三次作业
开发语言·python
韩立学长8 小时前
【开题答辩实录分享】以《助农信息发布系统设计与实现》为例进行选题答辩实录分享
python·web
深蓝电商API9 小时前
async/await与多进程结合的混合爬虫架构
爬虫·架构
2401_838472519 小时前
使用Scikit-learn构建你的第一个机器学习模型
jvm·数据库·python
u0109272719 小时前
使用Python进行网络设备自动配置
jvm·数据库·python
工程师老罗9 小时前
优化器、反向传播、损失函数之间是什么关系,Pytorch中如何使用和设置?
人工智能·pytorch·python
Fleshy数模9 小时前
我的第一只Python爬虫:从Requests库到爬取整站新书
开发语言·爬虫·python
CoLiuRs9 小时前
Image-to-3D — 让 2D 图片跃然立体*
python·3d·flask