1. 前言

大家好!今天带来一篇适合新手的Python爬虫实战教程,我们将从零开始,爬取携程网的酒店评论数据,并将结果存入MySQL数据库。通过这个项目,你可以掌握:

  • 如何分析网页接口并构造请求
  • 如何使用requests库发送POST请求
  • 如何处理JSON数据
  • 如何将数据保存到MySQL数据库
  • 简单的反爬措施(随机延时、异常处理)

注意:本教程仅用于学习和研究,请勿对目标网站进行大规模请求,尊重网站版权和用户隐私。爬取前建议查看网站的robots.txt文件。

2. 环境准备

2.1 安装Python

确保你已经安装了Python 3.6+版本,如果没有安装,可以去官网下载安装。

2.2 安装第三方库

我们需要用到以下几个库:

  • pymysql:操作MySQL数据库
  • requests:发送HTTP请求
  • json:处理JSON数据(Python自带)
  • timerandom:用于延时和随机数

打开命令行(终端),执行以下命令安装:

pip install pymysql requests

2.3 安装MySQL数据库

你需要安装MySQL数据库,并创建一个用户(例如root)并记住密码。本教程使用本地数据库,你也可以使用云数据库。

3. 爬虫分析

在写代码之前,我们先来分析携程酒店评论的接口。

打开浏览器,访问携程酒店详情页(例如:https://hotels.ctrip.com/hotel/xxx),按F12打开开发者工具,切换到Network(网络)标签,刷新页面,找到评论相关的请求。通常会有一个getHotelCommentList的POST请求。

我们通过抓包发现,评论接口地址是:

https://m.ctrip.com/restapi/soa2/33278/getHotelCommentList

在这里插入图片描述

请求方式为POST,请求头(headers)中包含很多参数,如cookieuser-agent等,请求体中是一个JSON对象,包含hotelIdpageIndex等参数。

4. 代码实现

我们按照面向对象的方式编写爬虫,将所有功能封装在Spider类中。

4.1 导入所需模块

import pymysql
import requests
import time
import random
import json
from requests.exceptions import RequestException

4.2 定义Spider类及初始化

class Spider:
    def __init__(self):
        # 接口地址
        self.url = 'https://m.ctrip.com/restapi/soa2/33278/getHotelCommentList'
        # 伪装头
        self.headers = {
            'accept': 'application/json',
            'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
            'cache-control': 'no-cache',
            'content-type': 'application/json',
             # 替换成自己的cookie
            'cookie': '',
            'origin': 'https://hotels.ctrip.com',
            'pragma': 'no-cache',
            'priority': 'u=1, i',
            'referer': 'https://hotels.ctrip.com/',
            'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Microsoft Edge";v="144"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"Windows"',
            'sec-fetch-dest': 'empty',
            'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-site',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0',
            'x-ctx-country': 'CN',
            'x-ctx-currency': 'CNY',
            'x-ctx-locale': 'zh-CN',
            'x-ctx-ubt-pageid': '10650171194',
            'x-ctx-ubt-pvid': '5',
            'x-ctx-ubt-sid': '15',
            'x-ctx-ubt-vid': '1766813870908.08e9zbNAFVFP',
            'x-ctx-wclient-req': 'da863c9181a16db6375013e79f3520dc'
        }
        # 参数(URL中的查询参数)
        self.params = {
            '_fxpcqlniredt': '09031109216141753464',
            'x-traceID': '09031109216141753464-1768914287515-4391436'
        }
        # 临时存储所有页的评论响应
        self.commentList = []
        # 初始化数据库连接(先连接到MySQL服务器,不指定数据库)
        self.connect = pymysql.connect(
            host='localhost',
            user='root',
            password='your_password',   # 修改为你的密码
            charset='utf8mb4'           # 使用utf8mb4支持表情
        )
        self.cursor = self.connect.cursor()
        # 数据库名称
        self.db_name = 'hotel'
        # 表名称
        self.table_name = 'hotel_comments'

关键点说明

  • self.headers中的cookie需要从浏览器中复制,因为携程需要登录状态或某些认证信息。你可以登录携程后,在开发者工具中找到任意请求的cookie,复制粘贴到这里。
  • self.params是URL后面的查询参数,可能包含追踪ID,一般可以保留原样。
  • 数据库连接时,先不指定数据库名,因为后面需要创建数据库。

4.3 获取酒店评论(多页)

    def getHotelCommentFile(self, Id):
        """
        获取酒店评论文件
        :param Id: 酒店的id
        :return: 酒店评论列表(每页响应数据)
        """
        for i in range(1, 101):   # 假设最多100页,可自行调整
            data_dict = {
                "hotelId": Id,
                "pageIndex": i,
                "pageSize": 10,
                "repeatComment": 1,
                "needStaticInfo": False,
                "functionOptions": ["integratedTopComment", "ctripIntegratedExpediaTaList"],
                "head": {
                    "cid": "1766813870908.08e9zbNAFVFP",
                    "ctok": "",
                    "cver": "0",
                    "lang": "01",
                    "sid": "",
                    "syscode": "09",
                    "auth": "",
                    "xsid": "",
                    "extension": [],
                    "platform": "PC",
                    "bu": "HBU",
                    "group": "ctrip",
                    "aid": "",
                    "ouid": "",
                    "locale": "zh-CN",
                    "timezone": "8",
                    "currency": "CNY",
                    "pageId": "102003",
                    "vid": "1766813870908.08e9zbNAFVFP",
                    "guid": "",
                    "isSSR": False
                }
            }
            # 将字典转为JSON字符串
            data = json.dumps(data_dict, ensure_ascii=False)
            try:
                # 随机延时,避免请求过快被反爬
                time.sleep(random.uniform(2, 3))
                # 发送POST请求
                response = requests.post(self.url, headers=self.headers, params=self.params, data=data)
                if response.status_code == 200:
                    self.commentList.append(response.json())
                    print(f'第{i}页的评论数据获取完毕!!!')
                else:
                    print(f'状态码为: {response.status_code}')
                    print(f'网页内容为: {response.text}')
            except RequestException as request_error:
                print(f'网络异常: {request_error}')
                time.sleep(random.uniform(2.5, 3.5))
            except Exception as e:
                print(f'其他异常: {e}')
                time.sleep(random.uniform(3, 4))
        return self.commentList

注意

  • 我们假设评论最多100页,实际可根据返回的总页数动态控制,这里为了简化直接循环。
  • data_dict是请求体,模仿了浏览器发送的数据结构。
  • time.sleep(random.uniform(2, 3))用于模拟人类访问,降低被封IP的风险。

4.4 解析评论内容

    def parseComment(self, Comments):
        """
        解析内容 - 获取评论内容
        :param Comments: 评论列表(每页响应数据)
        :return: 评论内容列表
        """
        commentContents = []
        for Comment in Comments:
            # 获取当前页的评论列表(注意:Comment['data']里可能包含commentList字段)
            commentList = Comment['data']['commentList']
            for comment in commentList:
                # 提取评论内容
                commentContents.append(comment['content'])
        return commentContents

响应数据结构大致如下(简化):
在这里插入图片描述

我们只需提取content字段。

4.5 创建数据库和表

    def createMysqlDbTable(self):
        """
        创建数据库和数据表
        """
        # 1. 创建数据库(如果不存在)
        create_db_sql = f"CREATE DATABASE IF NOT EXISTS {self.db_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
        try:
            self.cursor.execute(create_db_sql)
            print(f"数据库 {self.db_name} 已存在或创建成功。")
        except Exception as e:
            print(f"创建数据库失败: {e}")
            raise

        # 2. 切换到目标数据库
        self.cursor.execute(f"USE {self.db_name}")

        # 3. 创建数据表(如果不存在)
        create_table_sql = f"""
        CREATE TABLE IF NOT EXISTS {self.table_name} (
            id INT AUTO_INCREMENT PRIMARY KEY,
            hotel_id INT NOT NULL,
            comment TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
        """
        try:
            self.cursor.execute(create_table_sql)
            print(f"数据表 {self.table_name} 已存在或创建成功。")
        except Exception as e:
            print(f"创建数据表失败: {e}")
            raise

表字段说明

  • id:自增主键
  • hotel_id:酒店ID(整数)
  • comment:评论内容(TEXT类型,支持长文本)
  • created_at:插入时间戳,自动生成

使用utf8mb4字符集是为了支持emoji表情等特殊字符。

4.6 保存评论到MySQL

    def saveToMysql(self, hotel_id, contents):
        """
        将评论保存到数据库中
        :param hotel_id: 酒店ID
        :param contents: 评论内容列表
        """
        insert_sql = f"INSERT INTO {self.table_name} (hotel_id, comment) VALUES (%s, %s);"
        try:
            # 准备批量插入的数据(元组列表)
            data_to_insert = [(hotel_id, content) for content in contents]
            # 批量插入
            self.cursor.executemany(insert_sql, data_to_insert)
            self.connect.commit()
            print(f"成功插入 {len(contents)} 条记录到数据库。")
        except Exception as e:
            self.connect.rollback()
            print(f"批量插入失败: {e}")

使用executemany可以一次性插入多条记录,提高效率。

4.7 主流程方法

    def start(self, hotel_id):
        """
        主程序 - 集合所有功能模块的函数
        :param hotel_id: 酒店的id
        """
        # 1. 创建数据库和表
        self.createMysqlDbTable()
        # 2. 获取评论数据(返回的是完整的响应列表,包含分页)
        comment_responses = self.getHotelCommentFile(hotel_id)
        if comment_responses:
            # 3. 解析出评论内容
            comment_contents = self.parseComment(comment_responses)
            # 4. 存入数据库
            self.saveToMysql(hotel_id, comment_contents)

4.8 关闭数据库连接

    def close(self):
        """关闭数据库连接"""
        self.cursor.close()
        self.connect.close()

4.9 程序入口

if __name__ == '__main__':
    hotelId = int(input('请输入酒店id: '))
    hotelComment = Spider()
    try:
        hotelComment.start(hotelId)
    except Exception as e:
        print(f"程序运行出错: {e}")
    finally:
        hotelComment.close()
    print('所有评论信息已经插入到数据表中!!!')

5. 运行效果

运行程序后,输入酒店ID(例如从携程酒店详情页URL中获取的数字),程序开始爬取评论并存入数据库。控制台会输出每页的获取情况以及插入成功的记录数。

使用MySQL客户端或工具查看数据:

USE hotel;
SELECT * FROM hotel_comments;

可以看到评论内容已被保存。

6. 注意事项与常见问题

6.1 Cookie失效

携程的Cookie有时效性,如果遇到401403错误,请重新登录携程,在开发者工具中复制新的cookie替换代码中的self.headers

6.2 请求频率过高

代码中已经加入了随机延时,但如果还是被封IP,可以增加延时时间,或者使用代理IP。

6.3 数据库连接错误

  • 确保MySQL服务已启动。
  • 检查用户名、密码、主机是否正确。
  • 如果MySQL安装在非默认端口,需要在pymysql.connect中添加port参数。

6.4 编码问题

使用utf8mb4字符集可以避免某些特殊字符导致的插入错误。

7. 完整代码整合

为了方便大家复制,以下是整合后的完整代码(注意替换cookie和数据库密码):

import pymysql
import requests
import time
import random
import json
from requests.exceptions import RequestException

class Spider:
    def __init__(self):
        self.url = 'https://m.ctrip.com/restapi/soa2/33278/getHotelCommentList'
        self.headers = {
            'accept': 'application/json',
            'accept-language': 'zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6',
            'cache-control': 'no-cache',
            'content-type': 'application/json',
             # 替换成自己的cookie
            'cookie': '',
            'origin': 'https://hotels.ctrip.com',
            'pragma': 'no-cache',
            'priority': 'u=1, i',
            'referer': 'https://hotels.ctrip.com/',
            'sec-ch-ua': '"Not(A:Brand";v="8", "Chromium";v="144", "Microsoft Edge";v="144"',
            'sec-ch-ua-mobile': '?0',
            'sec-ch-ua-platform': '"Windows"',
            'sec-fetch-dest': 'empty',
            'sec-fetch-mode': 'cors',
            'sec-fetch-site': 'same-site',
            'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36 Edg/144.0.0.0',
            'x-ctx-country': 'CN',
            'x-ctx-currency': 'CNY',
            'x-ctx-locale': 'zh-CN',
            'x-ctx-ubt-pageid': '10650171194',
            'x-ctx-ubt-pvid': '5',
            'x-ctx-ubt-sid': '15',
            'x-ctx-ubt-vid': '1766813870908.08e9zbNAFVFP',
            'x-ctx-wclient-req': 'da863c9181a16db6375013e79f3520dc'
        }
        self.params = {
            '_fxpcqlniredt': '09031109216141753464',
            'x-traceID': '09031109216141753464-1768914287515-4391436'
        }
        self.commentList = []
        self.connect = pymysql.connect(
            host='localhost',
            user='root',
            password='your_password',  # 修改为你的数据库密码
            charset='utf8mb4'
        )
        self.cursor = self.connect.cursor()
        self.db_name = 'hotel'
        self.table_name = 'hotel_comments'

    def getHotelCommentFile(self, Id):
        for i in range(1, 101):
            data_dict = {
                "hotelId": Id,
                "pageIndex": i,
                "pageSize": 10,
                "repeatComment": 1,
                "needStaticInfo": False,
                "functionOptions": ["integratedTopComment", "ctripIntegratedExpediaTaList"],
                "head": {
                    "cid": "1766813870908.08e9zbNAFVFP",
                    "ctok": "",
                    "cver": "0",
                    "lang": "01",
                    "sid": "",
                    "syscode": "09",
                    "auth": "",
                    "xsid": "",
                    "extension": [],
                    "platform": "PC",
                    "bu": "HBU",
                    "group": "ctrip",
                    "aid": "",
                    "ouid": "",
                    "locale": "zh-CN",
                    "timezone": "8",
                    "currency": "CNY",
                    "pageId": "102003",
                    "vid": "1766813870908.08e9zbNAFVFP",
                    "guid": "",
                    "isSSR": False
                }
            }
            data = json.dumps(data_dict, ensure_ascii=False)
            try:
                time.sleep(random.uniform(2, 3))
                response = requests.post(self.url, headers=self.headers, params=self.params, data=data)
                if response.status_code == 200:
                    self.commentList.append(response.json())
                    print(f'第{i}页的评论数据获取完毕!!!')
                else:
                    print(f'状态码为: {response.status_code}')
                    print(f'网页内容为: {response.text}')
            except RequestException as request_error:
                print(f'网络异常: {request_error}')
                time.sleep(random.uniform(2.5, 3.5))
            except Exception as e:
                print(f'其他异常: {e}')
                time.sleep(random.uniform(3, 4))
        return self.commentList

    def parseComment(self, Comments):
        commentContents = []
        for Comment in Comments:
            commentList = Comment['data']['commentList']
            for comment in commentList:
                commentContents.append(comment['content'])
        return commentContents

    def createMysqlDbTable(self):
        create_db_sql = f"CREATE DATABASE IF NOT EXISTS {self.db_name} CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
        try:
            self.cursor.execute(create_db_sql)
            print(f"数据库 {self.db_name} 已存在或创建成功。")
        except Exception as e:
            print(f"创建数据库失败: {e}")
            raise
        self.cursor.execute(f"USE {self.db_name}")
        create_table_sql = f"""
        CREATE TABLE IF NOT EXISTS {self.table_name} (
            id INT AUTO_INCREMENT PRIMARY KEY,
            hotel_id INT NOT NULL,
            comment TEXT NOT NULL,
            created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
        """
        try:
            self.cursor.execute(create_table_sql)
            print(f"数据表 {self.table_name} 已存在或创建成功。")
        except Exception as e:
            print(f"创建数据表失败: {e}")
            raise

    def saveToMysql(self, hotel_id, contents):
        insert_sql = f"INSERT INTO {self.table_name} (hotel_id, comment) VALUES (%s, %s);"
        try:
            data_to_insert = [(hotel_id, content) for content in contents]
            self.cursor.executemany(insert_sql, data_to_insert)
            self.connect.commit()
            print(f"成功插入 {len(contents)} 条记录到数据库。")
        except Exception as e:
            self.connect.rollback()
            print(f"批量插入失败: {e}")

    def start(self, hotel_id):
        self.createMysqlDbTable()
        comment_responses = self.getHotelCommentFile(hotel_id)
        if comment_responses:
            comment_contents = self.parseComment(comment_responses)
            self.saveToMysql(hotel_id, comment_contents)

    def close(self):
        self.cursor.close()
        self.connect.close()

if __name__ == '__main__':
    hotelId = int(input('请输入酒店id: '))
    hotelComment = Spider()
    try:
        hotelComment.start(hotelId)
    except Exception as e:
        print(f"程序运行出错: {e}")
    finally:
        hotelComment.close()
    print('所有评论信息已经插入到数据表中!!!')

在这里插入图片描述

在这里插入图片描述

8. 总结

通过这个项目,我们完整地实现了一个爬虫程序,从分析接口、构造请求、处理响应,到最终将数据持久化到数据库。希望这个教程能帮助你理解爬虫的基本流程和数据库操作。

如果在运行过程中遇到任何问题,欢迎在评论区留言交流!

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐