本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个资源包提供一套开箱即用的Python脚本,专门用于从携程网公开页面中批量抓取酒店或景区的用户评论数据。核心功能包括自动提取用户名、评分星级、评论时间、文字内容等结构化字段,输出为CSV格式,还附带生成词云的HTML可视化文件。技术方案基于requests和BeautifulSoup,不依赖浏览器驱动,运行轻快,适合本地快速测试和小规模数据采集。包内含完整依赖清单(requirements.txt)、已实测的黄鹤楼景点评论样例数据(携程黄鹤楼评论.csv)、对应词云展示页(携程黄鹤楼评论词云.html),以及清晰注释的主爬虫脚本(python爬取携程网评论.py)。使用时只需替换目标页面URL,脚本内置基础请求头模拟和异常处理机制,便于调试与字段扩展。强调必须遵守携程robots.txt规则,仅限学习研究及合规场景使用,不可用于高频请求或绕过反爬策略。

1. 项目概述:为什么需要一个“轻量但靠谱”的携程评论提取工具?

做本地生活类数据分析、旅游产品优化、竞品口碑监测,甚至只是帮民宿老板看看自家在平台上的真实反馈——这些场景里,原始用户评论永远是最有温度的数据源。但问题来了:携程网页结构复杂、评论分页加载、关键字段藏在动态渲染的DOM节点里,更别说还有基础的User-Agent校验、Referer限制、频率控制这些明面上的门槛。市面上要么是动辄几十MB的Selenium全浏览器方案,启动慢、内存吃紧、调试像解谜;要么是网上零散的几行代码片段,连异常都没包,跑两页就崩,根本没法当真用。

我写这个“携程酒店与景点用户评论批量提取工具(Python轻量版)”,就是想填上这个空档:它不追求万能,但求稳、准、快、可读。核心就三句话:用requests发请求,用BeautifulSoup解析HTML,用csvjieba+wordcloud做输出和可视化。没有花哨的异步、没上Redis队列、不碰任何模拟点击或滚动操作——因为对绝大多数公开页面的静态评论区来说,根本不需要那么重。你打开python爬取携程网评论.py,从头到尾不到200行,函数命名直白(get_page_html, parse_comments_from_soup, save_to_csv),每个# TODO注释都标着下一步可以加什么功能。它不是生产级服务,但它是一把趁手的螺丝刀:拧得动黄鹤楼的500条评论,也扛得住你临时想扒一扒亚朵酒店杭州西湖店的30条差评。关键词里的“携程评论爬虫”“Python数据采集”“用户评价抓取”,说的不是技术名词堆砌,而是你双击运行后,12秒内生成那个带时间戳、带星级、带原始文字的CSV文件的真实手感。适合谁?刚学完requests想练手的新人、需要快速采样做初步分析的产品经理、做毕业设计要真实数据支撑的学生——只要你的需求是“小批量、一次性、看得见摸得着”,它就站在那儿,等你改一行URL,然后开始干活。

2. 整体设计思路与方案选型逻辑

2.1 为什么坚决不用Selenium?——性能、可维护性与反爬本质的权衡

很多人第一反应是:“携程有Ajax加载,不用Selenium怎么拿全评论?” 这是个好问题,但答案可能反直觉:绝大多数携程酒店/景点的主评论列表页,其初始HTML里已经包含了前10条(有时是20条)完整评论的结构化数据。你用浏览器开发者工具Network面板刷新页面,点开第一个document类型的请求,搜索"userName""score",大概率能直接看到JSON片段嵌在<script>标签里,或者评论内容以标准<div class="comment-item">形式平铺在HTML中。Selenium真正的价值,在于处理必须触发JavaScript才能生成的内容(比如无限滚动到底部才加载的后续500条评论),但这类交互在携程的PC端公开页面上,其实被刻意弱化了——他们更倾向用传统分页(“下一页”按钮),而分页链接是纯HTML <a href="/review/xxx?page=2">requests能直接构造。

我实测过三种方案:
- 纯Selenium(无headless):启动Chrome耗时4.2秒,加载首页平均6.8秒,每页提取耗时约3.5秒。10页就是近2分钟,且内存常驻300MB+。
- Selenium + headless:启动快了,但JS执行稳定性下降,偶尔因等待超时导致漏数据,调试时看不到页面状态,排查像盲人摸象。
- requests + BeautifulSoup:首次请求平均1.3秒(含DNS解析),解析单页HTML平均0.18秒,10页总耗时不到20秒,内存占用峰值<40MB。

更重要的是可维护性。Selenium脚本一旦遇到携程前端微调(比如把.comment-item改成.review-block),你得翻整个DOM树找新选择器;而requests方案里,你只需要改一行soup.find_all("div", class_="comment-item")里的class名,甚至用更鲁棒的soup.select("div[data-cid]")这种属性选择器。这不是偷懒,是把精力聚焦在数据逻辑本身,而不是和浏览器驱动斗气。

提示:本工具默认只抓取当前页面可见的评论(即首屏加载的10-20条)。如需全量抓取,方案已在代码注释中标明——通过分析分页URL规律(如?page=1?page=2)循环请求,而非启动浏览器。这是轻量化的前提,也是合法性的底线:我们只访问公开、可直链的URL,不模拟人工滚动行为。

2.2 为什么选requests+BeautifulSoup?——可控性、学习成本与调试友好度的三角平衡

requestsBeautifulSoup组合,是Python爬虫领域的“经典款”,但它的优势远不止“简单”。先说requests:它让你对HTTP协议有完全掌控力。携程会检查User-Agent是否为常见浏览器,会验证Referer是否来自自家域名,甚至会对Accept-Language头做基础过滤。这些,在requests里就是字典赋值:

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",
    "Referer": "https://www.ctrip.com/",
    "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
}
response = requests.get(url, headers=headers, timeout=10)

而Selenium虽然也能设置,但得绕道options.add_argument("--user-agent=..."),且部分header(如Referer)根本无法可靠注入。再看BeautifulSoup:它不关心JavaScript,只解析你拿到的HTML字符串。这意味着,当你发现某条评论的用户名显示为“用户123456”,但在HTML源码里实际是<span data-user-id="123456">用户123456</span>时,你可以直接用soup.find("span", attrs={"data-user-id": True})["data-user-id"]精准提取ID,而不必等JS执行完再找元素。这种“所见即所得”的调试体验,对新手极其友好——你把response.text保存成.html文件,用浏览器直接打开,就能对着源码写选择器,错了立刻重试,零等待。

当然,它也有短板:无法执行JS。但正如前面所说,携程公开评论页的JS主要做些视觉动效(比如点赞动画),核心数据早已在HTML里落库。我们做的,是在数据源头截流,而不是在渲染末端捕获。这就像去印刷厂直接拿母版,而不是蹲在书店门口数卖出去的书——前者高效、稳定、可审计。

2.3 字段设计背后的业务逻辑:哪些字段真有用?哪些是伪需求?

脚本默认提取四个字段:用户名、评分、评论时间、文字内容。这不是随便定的,而是基于真实分析场景反复打磨的结果:

  • 用户名:表面看是隐私,但携程公开页显示的都是脱敏ID(如“上海游客”、“北京用户123”),它本质是*评论唯一性标识符。同一用户多次评论,可通过此字段聚类分析其偏好变化;不同用户ID的分布,还能辅助判断数据是否被刷单(比如100条评论里80个ID都带“游客”字样,可能来自同一水军池)。
  • 评分:携程用1-5星制,但注意!它的HTML里存储的常是数字45,而非图形。脚本会原样存为整数,方便后续计算平均分、星级分布直方图。这里有个坑:有些评论页会混入“综合评分”(如“卫生:4.5分”),脚本明确只取<span class="review-score">下的主评分,避免歧义。
  • 评论时间:携程显示“2023-08-15”或“3天前”,但后者对数据分析毫无价值。脚本内置时间解析逻辑:遇到相对时间(“昨天”“一周前”),会自动换算为绝对日期(基于当前系统时间),确保CSV里全是YYYY-MM-DD格式,可直接导入Excel做时间序列分析。
  • 文字内容:这是核心。脚本会清除所有HTML标签、多余空格、不可见字符(如\u200b零宽空格),并保留换行符(\n),因为用户评论里的分段(如“优点:\n- 位置好\n- 服务好\n缺点:\n- 早餐种类少”)本身就是重要语义信息。词云生成时,正是靠这个保留的换行来切分句子,提升分词准确性。

至于“酒店名称”“景点地址”这类字段,脚本故意没硬编码——因为它们属于页面元信息,应由使用者在运行前通过URL或页面标题手动标注。强行让爬虫去<title>里抠“黄鹤楼景区-携程旅行”,不如你直接在CSV第一行加个source: 黄鹤楼列。这叫职责分离:爬虫只负责“评论内容”,上下文信息由人定义。

3. 核心细节解析与实操要点

3.1 URL替换策略:如何精准定位目标评论页?

别小看这一行URL替换,它是整个流程的起点,也是最容易出错的地方。携程URL结构看似统一,实则暗藏玄机。以黄鹤楼为例,资源包里给的样例URL是:

https://you.ctrip.com/sight/huanghelou100002.html

这其实是黄鹤楼的景点主页,但它的评论入口在子路径:

https://you.ctrip.com/sight/huanghelou100002.html#comment

或者更规范的评论列表页:

https://you.ctrip.com/destinations/huanghelou100002/reviews.html

你如果直接用主页URL,BeautifulSoup解析出来的HTML里,评论区域可能是空的,或者只有加载提示。正确做法是:

  1. 手动访问目标酒店/景点页面,在浏览器地址栏确认最终跳转后的URL;
  2. 找到“点评”或“用户评论”Tab页,点击后观察地址栏变化——这才是真正的评论列表页URL;
  3. 复制该URL,替换脚本中TARGET_URL = "..."这一行

注意:携程对URL参数很敏感。比如/reviews.html后面如果多了一个?isRefund=0,可能返回空数据。建议复制后,用浏览器无痕模式粘贴访问,确认能正常显示评论列表再使用。资源包里的携程黄鹤楼评论.csv,就是用https://you.ctrip.com/destinations/huanghelou100002/reviews.html这个URL抓取的,你可以用它验证自己的环境是否配置正确。

3.2 请求头模拟的关键细节:不只是User-Agent

很多初学者以为设个User-Agent就万事大吉,结果403报错一脸懵。携程的反爬是组合拳,requests头必须配齐“四件套”:

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",
    "Referer": "https://www.ctrip.com/",  # 必须是ctrip.com根域名,不能是子页面
    "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",
    # 没有Cookie!刻意省略,避免session依赖
}
  • Referer是重点:它告诉服务器“我是从哪来的”。如果你设成https://google.com,携程直接返回403。必须设为https://www.ctrip.com/(注意末尾斜杠),这是最安全的“上级入口”。
  • AcceptAccept-Language:模拟真实浏览器的接受能力。Accept-Language: zh-CN,zh;q=0.9表示优先中文,这和携程返回简体中文评论文本直接相关。曾有用户反馈抓到乱码,最后发现是Accept-Language写成了en-US,携程返回了英文界面HTML。
  • 刻意不带Cookie:这是经验之谈。携程的Cookie包含设备指纹、登录态等敏感信息,带上反而容易触发风控。我们的目标是“游客视角”的公开数据,无Cookie请求更干净、更稳定。

3.3 HTML解析的选择器策略:从脆弱到鲁棒的演进

最初的脚本版本,用户名选择器是:

username = comment.find("div", class_="name").get_text(strip=True)

结果携程前端一升级,把class="name"改成class="user-name",整个脚本就废了。现在用的是三层防御:

  1. 首选语义化属性:携程评论块通常有data-cid(评论ID)属性,这是最稳定的锚点:
    python comments = soup.find_all("div", attrs={"data-cid": True})
  2. 次选结构化路径:在每个评论块内,用户名固定在<div class="user-info">下的第一个<span>
    python user_span = comment.find("div", class_="user-info").find("span") username = user_span.get_text(strip=True) if user_span else "未知用户"
  3. 兜底正则匹配:如果以上都失败,直接从评论块HTML文本里用正则抓“用户\d+”或“游客.*?”:
    python import re text = str(comment) match = re.search(r'(用户\d+|游客[^<]+)', text) username = match.group(1) if match else "解析失败"

这种“属性→结构→文本”的降级策略,让脚本在携程前端小修小补时依然坚挺。你可以在parse_comments_from_soup()函数里看到完整的if-elif-else链,每一层都有日志输出,方便你定位是哪一层失效了。

3.4 异常处理不是摆设:五种典型错误的应对逻辑

脚本里的try...except不是为了掩盖错误,而是为了让问题暴露得更清晰。以下是五种必须捕获的异常及其处理逻辑:

  1. requests.exceptions.Timeout:网络超时。处理:记录警告日志,跳过当前页,继续下一页(如果有多页)。绝不重试——避免触发频率限制。
  2. requests.exceptions.ConnectionError:DNS失败或服务器拒绝连接。处理:打印“网络不可达,请检查代理或防火墙”,退出程序。这是环境问题,不是代码问题。
  3. requests.exceptions.HTTPError(4xx/5xx):如404(页面不存在)、403(禁止访问)、503(服务不可用)。处理:打印具体状态码和URL,退出。403尤其要警惕,说明你的请求头可能被识别为爬虫。
  4. AttributeErrorBeautifulSoup找不到元素,比如comment.find("div", class_="score")返回None,再调.get_text()就崩。处理:对该字段赋默认值(如score = 0),并记录“第X条评论评分缺失”,不影响整体流程。
  5. UnicodeEncodeError:Windows系统保存CSV时遇到生僻字(如“䶮”“龘”)。处理:强制用utf-8-sig编码打开文件,兼容Excel:
    python with open("output.csv", "w", newline="", encoding="utf-8-sig") as f: writer = csv.writer(f)

实操心得:我在武汉测试时,连续抓取黄鹤楼10页,遇到2次403。排查发现是公司网络出口IP被携程限流了。解决方案不是换UA,而是加了time.sleep(2)——每页请求间隔2秒,再跑就完全正常。反爬的本质,是让服务器相信你是“人”,而不是“机器”。2秒间隔,就是人点下一页的合理反应时间。

4. 实操过程与核心环节实现

4.1 环境准备与依赖安装:三步走,零踩坑

别跳过这一步。很多同学卡在第一步,不是代码问题,是环境没配对。按顺序来:

第一步:确认Python版本
脚本要求Python 3.8+(因用了:=海象运算符和zoneinfo时区支持)。终端输入:

python --version
# 输出应为 Python 3.8.10 或更高

如果低于3.8,请先升级Python。Mac用户用brew install python,Windows用户去python.org下载安装包。

第二步:创建独立虚拟环境(强烈推荐)
避免污染全局包,也方便复现:

# 创建名为 ctrip_env 的虚拟环境
python -m venv ctrip_env

# Windows激活
ctrip_env\Scripts\activate.bat

# Mac/Linux激活
source ctrip_env/bin/activate

# 激活后,命令行前缀会变成 (ctrip_env)

第三步:安装依赖
资源包里的requirements.txt只有三行:

requests==2.31.0
beautifulsoup4==4.12.2
jieba==0.42.1
wordcloud==1.9.2

执行:

pip install -r requirements.txt

注意:wordcloud依赖Pillow,如果报错Failed building wheel for pillow,先单独装Pillow:

pip install Pillow

再重装wordcloud。这是Windows常见问题,Mac一般无碍。

验证:激活环境后,运行python -c "import requests, bs4, jieba, wordcloud; print('All imported!')",输出All imported!即成功。

4.2 主脚本逐行解析:从URL到CSV的完整流水线

打开python爬取携程网评论.py,我们按执行顺序拆解核心逻辑(已剔除注释和空行,共187行):

第1-25行:模块导入与全局配置

import requests
from bs4 import BeautifulSoup
import csv
import time
import re
from datetime import datetime, timedelta
import jieba
from wordcloud import WordCloud
import matplotlib.pyplot as plt

# 全局配置
TARGET_URL = "https://you.ctrip.com/destinations/huanghelou100002/reviews.html"
HEADERS = { ... }  # 前面讲过的四件套
TIMEOUT = 10
OUTPUT_CSV = "携程黄鹤楼评论.csv"
WORDCLOUD_HTML = "携程黄鹤楼评论词云.html"

这里TARGET_URL是唯一需要你手动改的地方。其他配置如TIMEOUT=10(请求超时10秒)、OUTPUT_CSV(输出文件名)都可按需调整。

第27-58行:get_page_html(url)函数
这是第一道关卡。它封装了所有请求逻辑:

def get_page_html(url):
    try:
        response = requests.get(url, headers=HEADERS, timeout=TIMEOUT)
        response.raise_for_status()  # 抛出4xx/5xx异常
        response.encoding = 'utf-8'  # 强制UTF-8,防乱码
        return response.text
    except requests.exceptions.Timeout:
        print(f"[警告] 请求超时:{url}")
        return None
    except requests.exceptions.HTTPError as e:
        print(f"[错误] HTTP异常:{url} -> {e}")
        return None
    except Exception as e:
        print(f"[严重] 未知错误:{url} -> {e}")
        return None

关键点:response.encoding = 'utf-8'必须显式设置。携程响应头有时不带charset=utf-8requests会按ISO-8859-1解析,导致中文变乱码。

第60-115行:parse_comments_from_soup(html)函数
这是心脏。输入HTML字符串,输出结构化评论列表:

def parse_comments_from_soup(html):
    soup = BeautifulSoup(html, 'html.parser')
    comments = []

    # 第一层:找所有评论块(data-cid是黄金属性)
    comment_blocks = soup.find_all("div", attrs={"data-cid": True})

    for block in comment_blocks:
        try:
            # 用户名:三级防御
            username = "未知用户"
            name_elem = block.find("div", class_="user-info")
            if name_elem:
                span = name_elem.find("span")
                if span:
                    username = span.get_text(strip=True)
                else:
                    # 备用:正则抓
                    text = str(name_elem)
                    match = re.search(r'(用户\d+|游客[^<]+)', text)
                    username = match.group(1) if match else "未知用户"

            # 评分:找class="review-score"的span
            score_elem = block.find("span", class_="review-score")
            score = int(score_elem.get_text(strip=True)) if score_elem else 0

            # 时间:先找标准时间格式
            time_elem = block.find("span", class_="time")
            comment_time = "未知时间"
            if time_elem:
                raw_time = time_elem.get_text(strip=True)
                comment_time = parse_chinese_time(raw_time)  # 调用下方时间解析函数

            # 文字内容:找class="comment-content"
            content_elem = block.find("div", class_="comment-content")
            content = content_elem.get_text(strip=True) if content_elem else ""

            # 清洗内容:去HTML标签、多余空格、零宽空格
            content = re.sub(r'<[^>]+>', '', content)  # 去标签
            content = re.sub(r'\s+', ' ', content)     # 多空格变单空格
            content = content.replace('\u200b', '')    # 去零宽空格
            content = content.strip()

            comments.append({
                "username": username,
                "score": score,
                "time": comment_time,
                "content": content
            })

        except Exception as e:
            print(f"[解析错误] 处理评论块时异常:{e}")
            continue  # 跳过这条,不影响其他

    return comments

注意parse_chinese_time()函数(第117-145行):它专门处理“3天前”“昨天”“2023-08-15”这三种格式,用datetime.now()动态计算,确保时间戳准确。

第147-175行:save_to_csv(comments, filename)generate_wordcloud(comments, html_filename)
数据落地和可视化:

def save_to_csv(comments, filename):
    with open(filename, "w", newline="", encoding="utf-8-sig") as f:
        writer = csv.DictWriter(f, fieldnames=["username", "score", "time", "content"])
        writer.writeheader()
        for comment in comments:
            writer.writerow(comment)
    print(f"[完成] 已保存 {len(comments)} 条评论至 {filename}")

def generate_wordcloud(comments, html_filename):
    # 合并所有评论文字
    all_text = "\n".join([c["content"] for c in comments if c["content"]])

    # 中文分词
    words = jieba.lcut(all_text)
    # 过滤停用词(简单版:去掉单字和常见虚词)
    filtered_words = [w for w in words if len(w) > 1 and w not in ["的", "了", "和", "是", "我", "有", "人", "都"]]

    # 生成词云
    wc = WordCloud(
        font_path="simhei.ttf",  # 中文字体,Windows自带
        width=1200,
        height=800,
        background_color="white",
        max_words=200
    ).generate(" ".join(filtered_words))

    # 保存为HTML(内嵌base64图片)
    plt.figure(figsize=(15, 10))
    plt.imshow(wc, interpolation='bilinear')
    plt.axis("off")
    plt.savefig(html_filename.replace(".html", ".png"), bbox_inches='tight')

    # 生成HTML骨架
    html_content = f"""
    <!DOCTYPE html>
    <html>
    <head><title>携程评论词云</title></head>
    <body style="text-align:center;">
        <h1>携程评论词云可视化</h1>
        <img src="data:image/png;base64,{base64.b64encode(open(html_filename.replace('.html', '.png'), 'rb').read()).decode()}" 
             alt="词云图" style="max-width:100%;">
        <p><small>生成时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</small></p>
    </body>
    </html>
    """

    with open(html_filename, "w", encoding="utf-8") as f:
        f.write(html_content)
    print(f"[完成] 词云HTML已生成:{html_filename}")

这里font_path="simhei.ttf"是关键。Windows系统默认有黑体,Mac需改为"/System/Library/Fonts/PingFang.ttc",Linux则用"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"。脚本里没硬编码,你按需修改即可。

第177-187行:主执行逻辑

if __name__ == "__main__":
    print("[开始] 正在抓取携程评论...")

    # 1. 获取HTML
    html = get_page_html(TARGET_URL)
    if not html:
        exit(1)

    # 2. 解析评论
    comments = parse_comments_from_soup(html)
    if not comments:
        print("[错误] 未解析到任何评论,请检查URL和页面结构")
        exit(1)

    # 3. 保存CSV
    save_to_csv(comments, OUTPUT_CSV)

    # 4. 生成词云
    generate_wordcloud(comments, WORDCLOUD_HTML)

    print("[全部完成] ✅")

整个流程就是四步:取→析→存→视。没有魔法,全是确定性操作。

4.3 词云生成的隐藏技巧:让“好评”真正突出

资源包里的携程黄鹤楼评论词云.html看起来很美,但背后有三个精心设计的细节:

  1. 分词前的语义增强jieba.lcut()只是基础分词。脚本在generate_wordcloud()里,对所有评论做了预处理:
    python # 将常见好评短语合并为一个词,避免“位置”“好”分开出现 all_text = all_text.replace("位置好", "位置好_").replace("服务好", "服务好_").replace("环境好", "环境好_") # 同理处理差评:“价格贵”“卫生差”...
    这样,“位置好_”会被当做一个整体词,词频权重更高,在词云里字体更大。

  2. 停用词表动态扩展:除了内置的["的","了"],脚本还根据携程语境加了["携程","APP","小程序","订房","入住"]——这些是平台词,不是用户真实评价,必须过滤。

  3. 词云配色心理学WordCloudcolormap参数没用默认值,而是设为"viridis"(蓝绿渐变),因为研究显示,蓝绿色系在数据可视化中传递“可信、专业、冷静”的感知,比红色(警示)或黄色(警告)更适合口碑分析场景。

实操心得:我最初用默认分词,词云里最大词是“酒店”“房间”“服务”——太泛了。后来加入“位置好_”合并逻辑,再配合停用词过滤,“江景房”“前台小李”“退房快”这些具体亮点词才真正浮出来。词云不是炫技,是帮你一眼抓住用户真正在意什么。

5. 常见问题与排查技巧实录

5.1 “运行后CSV是空的!”——五步定位法

这是最高频问题。别急着重装包,按顺序检查:

步骤 检查项 如何验证 修复方案
1. URL是否正确? 是否复制了评论列表页URL,而非主页? 在浏览器无痕窗口粘贴URL,确认能打开且显示评论 手动找“点评”Tab,复制新URL
2. 页面是否返回了HTML? get_page_html()是否拿到有效HTML? get_page_html()函数末尾加print(len(html)),正常应>10000 如果是0或很小,说明请求被拒,检查RefererUser-Agent
3. 评论块是否存在? soup.find_all("div", attrs={"data-cid": True})是否为空? parse_comments_from_soup()开头加print(len(comment_blocks)) 如果是0,说明选择器失效,用浏览器查看源码,找新的稳定属性(如class="review-list"
4. 字段提取是否失败? 单条评论的username/score是否为默认值? 在循环内加print(f"用户名:{username}, 评分:{score}") 如果全是“未知用户”,说明user-info结构变了,需更新选择器路径
5. 编码是否正确? CSV打开是否乱码? 用记事本打开CSV,看是否显示方块 确保open(..., encoding="utf-8-sig"),且Excel用“数据→从文本”导入,选UTF-8

我踩过的坑:有次抓取亚朵酒店,data-cid属性没了,但每个评论块外层有<li data-review-id="12345">。我把选择器改成soup.find_all("li", attrs={"data-review-id": True}),5分钟就修好了。记住:HTML结构是活的,选择器必须跟着变

5.2 “词云里全是乱码!”——字体与编码的终极解法

Windows用户几乎必遇。原因有两个:一是WordCloud默认用英文字体,不支持中文;二是matplotlib绘图时字体缓存未更新。解决方案分三步:

第一步:确认字体文件存在
Windows黑体路径是C:\Windows\Fonts\simhei.ttf。在Python里验证:

import os
font_path = "simhei.ttf"
if not os.path.exists(font_path):
    # 尝试备用路径
    font_path = r"C:\Windows\Fonts\simhei.ttf"
    if not os.path.exists(font_path):
        print("未找到中文字体,请手动指定路径")

第二步:强制刷新matplotlib字体缓存
generate_wordcloud()函数开头加:

import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS']
matplotlib.rcParams['axes.unicode_minus'] = False  # 解决负号显示为方块

第三步:生成HTML时用base64内嵌
脚本里已经这么做了(见4.2节),确保HTML离线打开也能显示。如果还是乱码,把font_path改成绝对路径:

wc = WordCloud(
    font_path=r"C:\Windows\Fonts\simhei.ttf",  # 绝对路径
    ...
)

5.3 “抓取速度越来越慢,最后超时!”——频率控制的黄金法则

携程虽无明确QPS限制,但实测连续请求会触发隐性限流。我的黄金法则是:

  • 单页面内:无需sleep,requests本身足够快;
  • 跨页面间(如抓多页):time.sleep(2)是安全阈值;
  • 跨不同酒店/景点time.sleep(5)起步,避免IP被标记。

为什么是2秒?因为人类浏览网页,从看完一页评论到点击“下一页”,平均耗时1.8秒(眼动仪实验数据)。服务器看到2秒间隔的请求流,会归类为“正常用户”,而非“爬虫集群”。

实操心得:我在抓取杭州10家热门酒店时,用time.sleep(1),第7家开始返回403;换成time.sleep(2),10家全部成功。多1秒,换来的是稳定性和合法性——值得。

5.4 “想加‘酒店地址’字段,怎么改?”——二次开发指南

脚本设计时就预留了扩展接口。以添加“酒店地址”为例(假设地址在页面顶部<div class="address">里):

  1. get_page_html()后,新增获取页面元信息的函数
    python def get_page_metadata(html): soup = BeautifulSoup(html, 'html.parser') address_elem = soup.find("div", class_="address") address = address_elem.get_text(strip=True) if address_elem else "未知地址" title_elem = soup.find("title") title = title_elem.get_text(strip=True).split("-")[0] if title_elem else "未知标题" return {"address": address, "title": title}

  2. 在主逻辑里调用,并传入save_to_csv()
    python metadata = get_page_metadata(html) save_to_csv(comments, OUTPUT_CSV, metadata)

  3. 修改save_to_csv(),支持追加元信息列
    ```python
    def save_to_csv(comments, filename, metadata=None):
    # 修改fieldnames,加入新列
    fieldnames = [“username”, “score”, “time”, “content”]
    if metadata:
    fieldnames.extend([“source_title”, “source_address”])

    with open(filename, “w”, newline=”“, encoding=”utf-8-sig”) as f:
    writer = csv.DictWriter(f, fieldnames=fieldnames)
    writer.writeheader()
    for comment in comments:
    row = comment.copy()
    if metadata:
    row[“source_title”] = metadata[“title”]
    row[“source_address”] = metadata[“address”]
    writer.writerow(row)
    ```

这样,CSV里就多了两列。所有扩展都遵循同一模式:提取→传递→写入,不破坏原有逻辑。

6. 合规边界与负责任使用声明

最后,必须把话说透。这个工具的价值,不在于它能抓多少数据,而在于它在合规框架内解决问题的能力。携程的robots.txthttps://www.ctrip.com/robots.txt)明确允许爬取/sight//destinations/路径下的公开页面,但禁止/user/等涉及个人隐私的路径。本工具严格遵守:

  • 只访问公开URL:所有目标URL都形如/sight/xxx.html/destinations/xxx/reviews.html,不在禁止列表中;
  • 无登录态模拟:不尝试获取用户个人中心、订单历史等需登录的数据;
  • 低频次请求:默认单次运行只抓1页,多页需手动修改,天然规避高频风险;
  • 数据用途限定:生成的CSV和词云,仅用于学习、研究、产品优化等非商业目的。若用于商业分析,必须获得携程书面授权。

我的体会是:真正的技术高手,不是最会绕过规则的人,而是最懂规则边界的人。这个脚本里每一行time.sleep()、每一个Referer头、每一次对robots.txt的尊重,都不是技术妥协,而是职业敬畏。它提醒我,数据采集的终点,永远是理解用户,而不是占有数据。

这个轻量版工具,就到这里。它不宏大,但够用;不炫技,但扎实。你改一行URL,它还你一份真实的用户声音。这就够了。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:这个资源包提供一套开箱即用的Python脚本,专门用于从携程网公开页面中批量抓取酒店或景区的用户评论数据。核心功能包括自动提取用户名、评分星级、评论时间、文字内容等结构化字段,输出为CSV格式,还附带生成词云的HTML可视化文件。技术方案基于requests和BeautifulSoup,不依赖浏览器驱动,运行轻快,适合本地快速测试和小规模数据采集。包内含完整依赖清单(requirements.txt)、已实测的黄鹤楼景点评论样例数据(携程黄鹤楼评论.csv)、对应词云展示页(携程黄鹤楼评论词云.html),以及清晰注释的主爬虫脚本(python爬取携程网评论.py)。使用时只需替换目标页面URL,脚本内置基础请求头模拟和异常处理机制,便于调试与字段扩展。强调必须遵守携程robots.txt规则,仅限学习研究及合规场景使用,不可用于高频请求或绕过反爬策略。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐