ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python爬虫实战:从携程景点数据采集到存储分析全流程解析

Python爬虫实战:从携程景点数据采集到存储分析全流程解析 简介网络数据采集是数据科学和工程实践中的重要基础环节其核心原理是通过模拟HTTP请求与解析响应内容自动化获取互联网上的结构化与非结构化信息。这项技术对于市场分析、学术研究和业务决策具有显著价值广泛应用于舆情监控、价格追踪和用户行为分析等场景。在旅游领域通过爬虫获取景点与评论数据能够为目的地管理、服务优化提供数据支撑。本文以Python为核心工具结合requests库与BeautifulSoup解析器详细拆解了从目标分析、反爬应对到数据清洗存储的完整工程链路并深入探讨了Ajax接口逆向与SQLite数据库应用等关键技术要点为构建稳健、高效的垂直领域数据采集系统提供了可复用的实战方案。1. 项目概述与核心价值最近帮几个学弟学妹看了他们的毕业设计选题发现“基于Python爬取携程景点数据和评论数据”这个方向热度一直不减。这确实是个非常经典的练手项目它几乎涵盖了数据采集、数据处理、数据存储和简单分析的全流程对于计算机、软件工程甚至旅游管理专业的学生来说都是一个能充分展示技术能力和业务理解能力的课题。我自己当年也做过类似的项目深知其中门道。这个项目的核心价值在于它不是一个孤立的“爬虫”而是一个微型的“数据工程”实践。你需要考虑如何稳定、高效地从携程这类大型商业网站获取结构化数据如景点名称、地址、评分、门票价格和非结构化数据用户评论并最终形成一份可供分析的数据集。整个过程会涉及到反爬策略应对、数据清洗、存储方案选型等多个实战环节远比写一个简单的requests脚本要复杂得多。对于初学者这个项目能帮你把Python基础语法、HTTP协议、HTML解析、数据库操作等知识点串联起来。对于有一定基础的同学则可以深入探索并发爬取、数据去重、增量更新、情感分析等进阶话题。最终产出的不仅仅是一份源代码和文档更是一份证明你具备解决实际问题能力的作品。接下来我将以一个“过来人”的身份拆解这个项目的完整实现思路、关键技术细节以及那些容易踩坑的地方希望能为你提供一份可以直接参考的“实战手册”。2. 项目整体设计与思路拆解2.1 需求分析与目标定义做任何项目第一步永远是明确你要什么。对于“爬取携程景点数据和评论数据”我们需要将其拆解为具体、可执行的目标。核心数据目标景点基础数据针对某一个或某几个目标城市如北京、上海获取该城市下所有或指定数量的景点列表。每条景点数据应至少包含景点ID唯一标识通常从URL或页面源码中提取景点名称地理位置省、市、区、详细地址综合评分点评数量门票价格成人票、儿童票等需注意是否有“免费”或“价格区间”官方描述的简介或标签如“适合亲子”、“历史古迹”景点详情页URL景点评论数据针对上一步获取到的每一个景点进一步爬取其用户评论。每条评论数据应至少包含关联的景点ID/名称用户昵称或匿名标识用户评分如“5分”、“4分”评论正文评论时间有用数点赞数评论类型如“全部”、“带图”、“好评”、“差评”非功能性目标稳定性程序需要能应对网络波动、目标网站结构微调、反爬机制如IP封锁、请求频率限制等问题。效率在遵守Robots协议和不对目标网站造成压力的前提下合理设计爬取策略比如使用多线程/异步IO来提升I/O密集型任务的效率。可维护性代码结构清晰配置与逻辑分离便于后续扩展如增加爬取其他城市、其他数据类型或修改如解析规则变更。数据质量爬取的数据应尽可能干净、完整需要设计数据清洗和校验环节。2.2 技术栈选型与考量技术选型没有绝对的对错只有适合与否。下面是我基于常见实践和项目复杂度推荐的技术栈并解释为什么这么选。编程语言Python 3.8。这是毋庸置疑的选择。Python在数据抓取和处理领域的生态极其丰富语法简洁开发效率高。像requests、BeautifulSoup、Scrapy等库几乎成了行业标准。HTTP请求库初级/轻量级选择requestshttpx(异步)。requests是同步请求的王者简单易用对于学习和小规模爬取足够了。但如果要爬取成百上千个页面同步请求的等待时间会成为瓶颈。此时可以引入httpx或aiohttp进行异步请求能大幅提升效率。高级/工程化选择Scrapy框架。如果你的项目规模较大或者你想更深入地学习爬虫框架的设计思想Scrapy是更好的选择。它内置了请求调度、管道处理、中间件等机制能让你更专注于爬取规则的编写而不用操心并发、去重、异常处理等底层细节。对于毕业设计来说使用Scrapy会显得项目更有分量。HTML解析库BeautifulSoup解析HTML/XML的瑞士军刀API友好支持多种解析器如lxml,html.parser。非常适合处理静态页面或从动态渲染后的HTML字符串中提取数据。lxml解析速度比BeautifulSoup默认的解析器快很多XPath表达式功能强大且精确。如果你的页面结构复杂用XPath定位元素会比BeautifulSoup的find/find_all更高效。parselScrapy内置的选择器库融合了XPath和CSS选择器的优点性能也很好。如果你用Scrapy通常就直接用它了。动态页面处理携程的景点列表和评论列表很多都是通过Ajax动态加载的尤其是评论的“加载更多”。直接请求网页URL拿到的HTML是不完整的。首选方案分析Ajax接口。通过浏览器的开发者工具F12 - Network - XHR/JS找到真正返回数据的API接口。直接模拟请求这些接口效率最高数据最干净通常是JSON格式。这是爬虫工程师的必备技能。备选方案Selenium或Playwright。当接口参数加密复杂难以逆向或者页面交互逻辑极其繁琐时可以使用浏览器自动化工具。它们能模拟真人操作浏览器获取渲染后的完整DOM。但代价是速度慢、资源占用高不适合大规模爬取。毕业设计中可以酌情使用以应对难点。数据存储CSV文件最简单快捷适合数据量不大几万条以内的情况。用Python内置的csv模块或pandas的to_csv即可。JSON文件适合存储嵌套结构的数据比如一个景点对应多条评论。可读性好。数据库SQLite/MySQL/MongoDBSQLite单文件数据库无需安装服务器适合本地开发和中小型项目。用sqlite3模块内置或SQLAlchemyORM操作。MySQL关系型数据库适合需要复杂查询或未来可能做数据关联分析的场景。需要安装数据库服务。MongoDB文档型数据库存储JSON格式数据非常自然schema灵活适合评论这类半结构化或结构易变的数据。选择建议对于毕业设计我推荐使用SQLite或MySQL。这能体现你对于结构化数据存储和SQL操作的能力在答辩时也更容易展示数据表的设计。其他工具库pandas数据清洗、处理的利器。爬下来的原始数据往往很脏用pandas进行去重、缺失值处理、格式转换等非常方便。Faker生成模拟的HTTP请求头User-Agent等有助于绕过简单的反爬。redis可选用于实现分布式爬虫的请求队列和去重如果项目涉及大规模爬取可以考虑。注意技术选型不是堆砌炫技。对于毕业设计我建议采用requestsBeautifulSoup/lxml分析Ajax接口MySQL/SQLite这条技术路径。它足够完成项目能全面展示你的技能且复杂度适中易于把控和答辩讲解。Scrapy可以作为加分项但前提是你有足够时间掌握其核心概念。3. 核心细节解析与实操要点3.1 目标网站结构与数据定位分析在写第一行代码之前必须花足够的时间“人肉”分析目标网站。这是决定爬虫成败的关键一步。1. 景点列表页分析打开携程景点频道选择目标城市如“北京”。观察URL规律通常是https://you.ctrip.com/sight/beijing1.html或带有分页参数。打开浏览器开发者工具F12刷新页面。查看网页源码右键“查看网页源代码”搜索一个你看到的景点名。如果能找到说明是静态页面或服务端渲染可以直接用BeautifulSoup解析源码。更常见的情况源码里没有景点列表数据。这时切换到Network网络选项卡筛选XHR或JS类型的请求。在列表页滚动或点击分页时观察哪个请求的响应里包含了景点数据通常是JSON格式。这个请求的URL、请求方法GET/POST、请求头Headers和查询参数Query String Parameters/Payload就是你后续需要模拟的对象。2. 景点详情页与评论接口分析点击进入一个具体景点如“故宫博物院”。基础信息景点名称、地址、评分等通常在详情页的HTML中可以直接解析。评论数据评论绝对是动态加载的。在评论区域不断下拉观察Network面板。你会找到一个返回评论列表的API接口。仔细分析这个接口URL长什么样是否有规律可循请求参数常见的会有poiId景点ID、pageIndex页码、pageSize每页条数、sortType排序方式等。你需要找出哪些参数是必须的以及poiId如何从详情页URL或源码中获取。响应格式一定是JSON。分析其结构找到评论列表如data.comments或list以及总页数或是否有下一页的标识如hasNextPage。3. 反爬机制初探请求头检查上述数据接口的请求头重点关注User-Agent、Referer有时还会有自定义的Cookie或Token。你的爬虫需要携带这些信息。频率限制不要用死循环无间隔地疯狂请求。观察网站手动快速刷新几次看是否会弹出验证码或暂时无法访问。这决定了你后续需要设置的请求延迟时间。参数加密有些网站的接口参数可能是加密的尤其是pageIndex之后的页码。如果发现改变页码但参数是一串无规律的乱码说明可能有简单的加密。这需要一定的JS逆向分析能力是毕业设计中的一个难点和亮点。3.2 爬虫策略设计与伦理边界策略设计广度优先爬取先爬取目标城市的所有景点列表得到一批poiId再根据poiId逐个爬取详情和评论。这是最直观的策略。增量爬取进阶设计一个简单的机制记录上次爬取的最后时间或最大ID下次运行时只爬取新增或更新的数据。这需要数据库支持。分阶段爬取先快速爬下所有景点的ID和基础信息数据量小再在另一个阶段或低峰期慢慢爬取耗时的评论数据。伦理与法律边界务必重视遵守Robots.txt访问https://you.ctrip.com/robots.txt查看携程对爬虫有哪些限制。虽然这不是法律文件但遵守它是行业共识和基本礼仪。控制请求频率在代码中为每个请求添加随机延迟例如time.sleep(random.uniform(1, 3))模拟人类浏览速度避免对服务器造成冲击。这是避免IP被封锁的最有效方法之一。数据用途明确你的数据仅用于毕业设计、学术研究或个人学习。绝对不要将大规模爬取的数据用于商业用途、公开传播或对他人网站进行恶意竞争这很可能构成不正当竞争或侵犯权益。尊重版权用户评论的版权可能属于用户或平台在展示或使用时要注明来源并注意不要侵犯个人隐私虽然公开评论但大规模聚合分析仍需谨慎。实操心得在代码里定义一个全局的REQUEST_DELAY变量来控制延迟方便调整。对于评论接口如果一次返回几十条爬完一个景点的所有评论可能需要几十个请求务必为这些请求也加上延迟。一个稳健的爬虫其“礼貌性”比“快速性”更重要。4. 实操过程与核心环节实现下面我将以“爬取北京市景点列表及前5页评论”为例使用requestsBeautifulSoupAjax接口分析SQLite的技术栈展示核心代码片段和思路。假设我们已经通过分析找到了景点列表和评论的API接口。4.1 环境准备与依赖安装首先创建一个新的项目目录并初始化虚拟环境推荐。# 创建项目目录 mkdir ctrip_spider_project cd ctrip_spider_project # 创建虚拟环境 (Python 3.8) python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # Linux/Mac: source venv/bin/activate # 安装核心库 pip install requests beautifulsoup4 lxml pandas # 如果需要操作MySQL安装pymysql或mysql-connector-python # pip install pymysql # 这里我们使用SQLite它是Python内置的无需安装。4.2 数据库设计我们在项目根目录创建一个database.py文件来定义数据库模型。使用SQLite数据库文件将保存在本地。# database.py import sqlite3 import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) class CtripDatabase: def __init__(self, db_pathctrip_data.db): self.connection sqlite3.connect(db_path) self.cursor self.connection.cursor() self._create_tables() logger.info(f数据库连接已建立: {db_path}) def _create_tables(self): 创建景点表和评论表 # 景点表 self.cursor.execute( CREATE TABLE IF NOT EXISTS sights ( id INTEGER PRIMARY KEY AUTOINCREMENT, sight_id TEXT UNIQUE NOT NULL, -- 来自携程的唯一ID name TEXT NOT NULL, city TEXT, district TEXT, address TEXT, overall_rating REAL, -- 综合评分如4.7 comment_count INTEGER, introduction TEXT, detail_url TEXT, crawled_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) ) # 评论表 self.cursor.execute( CREATE TABLE IF NOT EXISTS comments ( id INTEGER PRIMARY KEY AUTOINCREMENT, sight_id TEXT NOT NULL, -- 关联景点ID user_nickname TEXT, user_rating INTEGER, -- 用户评分如5 content TEXT NOT NULL, comment_time TEXT, -- 原始时间字符串如“2023-10-01” useful_count INTEGER DEFAULT 0, FOREIGN KEY (sight_id) REFERENCES sights (sight_id) ) ) # 创建索引以提高查询速度 self.cursor.execute(CREATE INDEX IF NOT EXISTS idx_sight_id ON comments (sight_id)) self.connection.commit() logger.info(数据表创建完成。) def insert_sight(self, sight_data): 插入或更新一条景点数据 sql INSERT OR REPLACE INTO sights (sight_id, name, city, district, address, overall_rating, comment_count, introduction, detail_url) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) try: self.cursor.execute(sql, ( sight_data.get(sight_id), sight_data.get(name), sight_data.get(city), sight_data.get(district), sight_data.get(address), sight_data.get(overall_rating), sight_data.get(comment_count), sight_data.get(introduction), sight_data.get(detail_url) )) self.connection.commit() return self.cursor.lastrowid except sqlite3.Error as e: logger.error(f插入景点数据失败: {e}, 数据: {sight_data}) return None def insert_comment_batch(self, comments): 批量插入评论数据 if not comments: return sql INSERT OR IGNORE INTO comments (sight_id, user_nickname, user_rating, content, comment_time, useful_count) VALUES (?, ?, ?, ?, ?, ?) # 假设comments是字典列表我们确保顺序对应 data_to_insert [] for comment in comments: # 这里可以根据需要生成一个唯一标识例如sight_idcontent的hash来真正去重。 # 为了简单我们使用 OR IGNORE但可能无法完全避免重复。 data_to_insert.append(( comment.get(sight_id), comment.get(user_nickname), comment.get(user_rating), comment.get(content), comment.get(comment_time), comment.get(useful_count, 0) )) try: self.cursor.executemany(sql, data_to_insert) self.connection.commit() logger.info(f成功批量插入 {self.cursor.rowcount} 条评论。) except sqlite3.Error as e: logger.error(f批量插入评论失败: {e}) def close(self): 关闭数据库连接 self.connection.close() logger.info(数据库连接已关闭。) # 简单测试 if __name__ __main__: db CtripDatabase() # 测试插入 test_sight { sight_id: test_123, name: 测试景点, city: 北京, district: 东城区, address: 测试地址, overall_rating: 4.5, comment_count: 1000, introduction: 这是一个测试景点简介。, detail_url: https://you.ctrip.com/sight/test.html } db.insert_sight(test_sight) db.close()4.3 核心爬虫逻辑实现接下来我们创建主爬虫文件spider.py。这里将模拟找到的API接口进行请求。# spider.py import requests import json import time import random import logging from typing import List, Dict, Optional from database import CtripDatabase logging.basicConfig(levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s) logger logging.getLogger(__name__) class CtripSpider: def __init__(self): # 初始化数据库 self.db CtripDatabase() # 配置请求头模拟浏览器 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, Referer: https://you.ctrip.com/, Accept: application/json, text/plain, */*, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } # 基础URL此处为示例实际需要根据分析结果替换 self.sight_list_api https://you.ctrip.com/api/destinations/api/GetSightList # 示例URL self.comment_list_api https://you.ctrip.com/api/sight/api/GetCommentList # 示例URL self.request_delay (1, 3) # 请求延迟范围秒 def _request_with_retry(self, url, paramsNone, methodGET, max_retries3): 带重试和延迟的请求函数 for i in range(max_retries): try: # 随机延迟避免请求过快 time.sleep(random.uniform(*self.request_delay)) if method.upper() GET: response requests.get(url, headersself.headers, paramsparams, timeout10) else: response requests.post(url, headersself.headers, jsonparams, timeout10) response.raise_for_status() # 检查HTTP状态码 # 尝试解析JSON try: return response.json() except json.JSONDecodeError: logger.warning(f响应不是有效的JSON: {response.text[:200]}) return None except requests.exceptions.RequestException as e: logger.warning(f请求失败 (尝试 {i1}/{max_retries}): {url}, 错误: {e}) if i max_retries - 1: wait_time 2 ** i # 指数退避 logger.info(f等待 {wait_time} 秒后重试...) time.sleep(wait_time) else: logger.error(f请求最终失败: {url}) return None return None def fetch_sight_list(self, city_codebeijing1, max_pages5): 获取景点列表 Args: city_code: 城市代码从URL中分析得出如beijing1 max_pages: 最大爬取页数用于控制范围 all_sights [] for page in range(1, max_pages 1): logger.info(f正在爬取第 {page} 页景点列表...) # 构造请求参数需要根据实际接口分析 params { cityCode: city_code, page: page, pageSize: 20, # 每页数量 sortType: 1, # 排序方式需分析 # 可能还有其他必要参数如_时间戳 } data self._request_with_retry(self.sight_list_api, paramsparams) if not data or data.get(code) ! 0: # 假设接口返回code0表示成功 logger.error(f获取第 {page} 页景点列表失败或数据异常。) break # 解析数据这里需要根据实际接口返回的JSON结构来写 sight_items data.get(data, {}).get(list, []) if not sight_items: logger.info(f第 {page} 页无数据可能已到末页。) break for item in sight_items: sight_info { sight_id: str(item.get(sightId)), # 确保是字符串 name: item.get(sightName, ).strip(), city: 北京, # 根据city_code映射 district: item.get(district, ), address: item.get(address, ), overall_rating: float(item.get(overallRating, 0)), comment_count: int(item.get(commentCount, 0)), introduction: item.get(introduction, )[:500], # 截取部分防止过长 detail_url: fhttps://you.ctrip.com/sight/{city_code}/{item.get(sightId)}.html } all_sights.append(sight_info) # 存入数据库 self.db.insert_sight(sight_info) logger.info(f第 {page} 页爬取完成共 {len(sight_items)} 个景点。) # 简单判断是否还有下一页根据实际接口返回字段 if page data.get(data, {}).get(totalPage, 1): break logger.info(f景点列表爬取结束共获取 {len(all_sights)} 个景点。) return all_sights def fetch_comments_for_sight(self, sight_id, max_comment_pages5): 获取单个景点的评论 Args: sight_id: 景点ID max_comment_pages: 最大爬取评论页数 all_comments [] for page in range(1, max_comment_pages 1): logger.info(f爬取景点 {sight_id} 的第 {page} 页评论...) params { poiId: sight_id, pageId: page, pageSize: 10, # 每页评论数 sortType: 3, # 排序3可能代表“最新” # 可能还需要其他固定参数 } data self._request_with_retry(self.comment_list_api, paramsparams) if not data or data.get(code) ! 0: logger.warning(f获取景点 {sight_id} 第 {page} 页评论失败。) break comment_items data.get(data, {}).get(commentList, []) if not comment_items: logger.info(f景点 {sight_id} 第 {page} 页无评论。) break page_comments [] for item in comment_items: # 清洗评论内容去除多余空白和换行 content item.get(content, ).strip().replace(\r\n, ).replace(\n, ) if not content: # 跳过空评论 continue comment { sight_id: sight_id, user_nickname: item.get(userNickName, 匿名用户), user_rating: int(item.get(score, 0)), # 用户打分 content: content, comment_time: item.get(publishTime, ), # 原始时间字符串 useful_count: int(item.get(usefulCount, 0)) } page_comments.append(comment) all_comments.extend(page_comments) logger.info(f景点 {sight_id} 第 {page} 页爬取到 {len(page_comments)} 条评论。) # 判断是否还有下一页根据实际接口 if not data.get(data, {}).get(hasNextPage, True): break # 批量存入数据库 if all_comments: self.db.insert_comment_batch(all_comments) logger.info(f景点 {sight_id} 评论爬取完成共 {len(all_comments)} 条已入库。) return all_comments def run(self, city_codebeijing1, max_sight_pages3, max_comment_pages_per_sight2): 主运行函数 logger.info(开始爬取携程景点数据...) # 1. 爬取景点列表 sights self.fetch_sight_list(city_codecity_code, max_pagesmax_sight_pages) if not sights: logger.error(未获取到任何景点信息程序终止。) return # 2. 遍历景点爬取评论 total_sights len(sights) for idx, sight in enumerate(sights, 1): sight_id sight[sight_id] logger.info(f正在处理景点 ({idx}/{total_sights}): {sight[name]} (ID: {sight_id})) # 可以根据评论数量决定是否爬取例如只爬取评论数大于10的 if sight.get(comment_count, 0) 10: self.fetch_comments_for_sight(sight_id, max_comment_pagesmax_comment_pages_per_sight) else: logger.info(f景点 {sight[name]} 评论数过少({sight.get(comment_count, 0)})跳过评论爬取。) # 处理完一个景点后可以稍作长时间休息避免对单一景点请求过快 time.sleep(random.uniform(2, 5)) logger.info(所有数据爬取任务完成) self.db.close() if __name__ __main__: spider CtripSpider() # 为了演示和测试控制爬取范围3页景点列表每个景点最多爬2页评论 spider.run(city_codebeijing1, max_sight_pages3, max_comment_pages_per_sight2)4.4 数据清洗与简单分析示例爬取到的数据往往是原始的、杂乱的。我们创建一个data_clean.py文件来进行简单的清洗和导出。# data_clean.py import pandas as pd from database import CtripDatabase import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) def export_and_clean_data(): 从数据库导出数据并进行简单清洗 db CtripDatabase(ctrip_data.db) conn db.connection # 1. 导出景点数据 sights_df pd.read_sql_query(SELECT * FROM sights, conn) logger.info(f导出了 {len(sights_df)} 条景点记录。) # 简单清洗处理缺失值 # 评分缺失的用0填充或根据业务逻辑用平均值这里简单处理 sights_df[overall_rating].fillna(0, inplaceTrue) sights_df[comment_count].fillna(0, inplaceTrue) # 去除名称完全为空的记录理论上不应该有 sights_df sights_df[sights_df[name].notna() (sights_df[name].str.strip() ! )] # 2. 导出评论数据 comments_df pd.read_sql_query(SELECT * FROM comments, conn) logger.info(f导出了 {len(comments_df)} 条评论记录。) # 清洗评论去除内容完全为空或过短的评论比如少于5个字符 if not comments_df.empty: comments_df comments_df[comments_df[content].notna()] comments_df comments_df[comments_df[content].str.len() 5] # 将comment_time转换为datetime类型便于分析 # 注意原始时间字符串格式可能不统一需要根据实际情况解析 # 这里假设格式是2023-10-01或2023-10-01 12:00:00 try: comments_df[comment_time_parsed] pd.to_datetime(comments_df[comment_time], errorscoerce) except Exception as e: logger.warning(f时间解析失败: {e}) # 3. 保存为CSV文件方便用Excel查看或进一步分析 sights_df.to_csv(cleaned_sights.csv, indexFalse, encodingutf-8-sig) comments_df.to_csv(cleaned_comments.csv, indexFalse, encodingutf-8-sig) logger.info(数据已清洗并保存为 cleaned_sights.csv 和 cleaned_comments.csv) # 4. 简单的数据分析示例 if not sights_df.empty: print(\n 景点数据分析 ) print(f景点总数: {len(sights_df)}) print(f平均评分: {sights_df[overall_rating].mean():.2f}) print(f总评论数: {sights_df[comment_count].sum()}) # 找出评分最高的前5个景点 top_sights sights_df.nlargest(5, overall_rating)[[name, overall_rating, comment_count]] print(\n评分最高的5个景点:) print(top_sights.to_string(indexFalse)) if not comments_df.empty and user_rating in comments_df.columns: print(\n 评论数据分析 ) print(f评论总数: {len(comments_df)}) rating_dist comments_df[user_rating].value_counts().sort_index() print(\n用户评分分布:) for rating, count in rating_dist.items(): print(f {rating}分: {count} 条) db.close() if __name__ __main__: export_and_clean_data()5. 常见问题与排查技巧实录在实际操作中你几乎一定会遇到下面这些问题。这里我把自己踩过的坑和解决方法记录下来。5.1 请求被拒绝或返回乱码/错误数据现象requests返回的状态码是403、404或者返回的HTML/JSON不是预期数据。排查步骤检查请求头这是最常见的原因。用浏览器访问目标页面在开发者工具的Network里找到数据请求把它的User-Agent、Referer、Cookie等全部复制到你的爬虫代码的headers字典里。特别是User-Agent很多网站会校验。检查参数对比你的请求参数和浏览器发出的参数是否完全一致。注意是否有动态生成的参数如_时间戳、sign签名等。这些可能需要从网页JS代码里分析生成逻辑。检查Cookies有些接口需要登录态。你可以先用浏览器登录然后把Cookie字符串复制过来。但注意Cookie会过期且毕业设计不建议模拟登录复杂度高且易违规。尝试使用Sessionrequests.Session()可以保持会话自动处理一些Cookie。降低请求频率立刻增加请求间隔时间time.sleep并加入随机性。这是应对反爬最基本的手段。5.2 无法找到数据或解析出错现象能拿到HTML但用BeautifulSoup或 XPath 找不到数据。排查步骤确认数据来源再次确认数据是否真的在当前的HTML页面里。99%的情况下数据是通过Ajax接口加载的。务必在Network的XHR/JS分类下寻找。检查解析器BeautifulSoup使用lxml解析器通常更健壮BeautifulSoup(html, lxml)。确保已安装lxml库 (pip install lxml)。验证选择器在浏览器的开发者工具里使用Copy - Copy selector或Copy - Copy XPath功能可以快速获取元素的路径。但直接复制过来的路径可能很长且脆弱。最好自己分析HTML结构写相对简洁和稳定的选择器。处理动态加载如果数据是滚动加载的你需要模拟滚动事件或者更常见的是找到滚动加载触发的API接口。5.3 数据库操作异常现象插入数据失败报主键冲突、数据类型错误等。排查与解决主键/唯一约束冲突我们在景点表将sight_id设为UNIQUE并使用INSERT OR REPLACE。这意味着如果遇到相同的sight_id会更新原有记录。这适合景点信息可能更新的场景。对于评论我们用了INSERT OR IGNORE但仅靠现有字段可能无法精确去重。更好的做法是生成一个唯一键例如f{sight_id}_{hash(comment_content[:50])}作为业务主键。数据类型不匹配确保插入的数据类型与表定义一致。例如把字符串4.5插入REAL字段是OK的但把暂无评分插入就会出错。在插入前进行数据清洗和类型转换至关重要。连接未关闭长时间运行的爬虫可能会打开很多数据库连接。确保在爬虫结束时调用db.close()或者使用with语句管理连接。5.4 爬虫效率低下现象爬取几千条数据需要几个小时。优化方案使用并发/异步这是最有效的提速方法。可以将requests替换为aiohttp或httpx进行异步请求。或者使用concurrent.futures.ThreadPoolExecutor实现多线程。但务必注意并发请求必须配合严格的速率限制否则极易被封IP。可以结合asyncio.Semaphore或线程池的队列来控制并发数。减少不必要的请求在爬取评论前先判断comment_count如果为0或很少直接跳过。优化数据库写入始终使用executemany进行批量插入而不是在循环里逐条execute。每插入100条或500条提交一次事务而不是每条都提交。分离IO和计算将HTTP请求IO密集型和HTML解析/数据清洗CPU密集型分开考虑。IO部分适合用异步CPU部分如果很重可以考虑多进程但通常爬虫瓶颈在IO。5.5 应对IP封锁对于毕业设计级别的爬取通常不会触发严格的IP封锁但也要有预案。首要策略慢将延迟时间调大比如time.sleep(random.uniform(3, 8))。这是最有效、最道德的方法。使用代理IP池进阶如果确实需要大量数据可以考虑使用付费或免费的代理IP。但免费代理质量极不稳定付费代理是一笔开销。实现逻辑是维护一个IP列表请求失败时自动切换。识别验证码如果遇到验证码项目复杂度会急剧上升。可以考虑使用第三方打码平台如超级鹰、图鉴但这涉及额外成本和接口调用。对于毕业设计建议在代码中检测到验证码页面时记录日志并暂停一段时间或手动处理。终极建议在开始大规模爬取前先用极小的范围如爬2个景点每个景点爬1页评论跑通整个流程。确保数据能正确获取、解析、存储。然后再逐步扩大范围并密切观察日志看是否有错误或封禁迹象。随时准备好调整策略。本文还有配套的精品资源点击获取
返回列表