最近整理了一套之前给学员做的课设项目,就是标题里写的这个基于 Python + Django 的去哪儿网数据爬取与分析系统。这个项目核心倒不复杂:用 Python 爬虫把去哪儿网的航班、酒店基础数据抓下来,清洗之后存进 MySQL,再用 Django 搭一个 Web 页面,把数据列表、统计图表都展示出来,附带一份完整的课程设计文档和测试数据。整个项目涉及 Python 爬虫、Django Web 开发、MySQL 数据库设计、ECharts 可视化,非常适合做 Python 课程设计、毕业设计,或者刚学完 Django 想练一个完整项目的读者拿来参考。
我为什么专门挑这个项目来说,因为它几乎把 Python 学习路上几个大块都串起来了:爬虫、数据处理、关系型数据库、Web 后端、前端展示。你单独学 Django 的 ORM 也好,单独学 requests 爬虫也好,都是零散的,真正能把它们串成一个跑起来的系统,才能说对这几块有实际手感。这篇就把我从抓包到入库、再到页面展示的完整过程,以及中间踩过的坑,一次性写清楚。
1. 项目整体设计与思路拆解
1.1 需求拆解:这个系统到底要做什么
先说结论,这个项目不是一个“大而全”的生产级系统,而是典型的课程设计粒度。它要解决的问题是:你有一个数据源(去哪儿网),想积累一批真实数据,然后用一个 Web 系统把这些数据展示出来,并且能做简单的统计分析。
所以我第一件事就是把需求拆成三个独立模块:
- 数据采集模块:定时或手动触发,从去哪儿网抓取航班、酒店信息,解析出结构化字段。
- 数据存储模块:用 MySQL 保存所有采集结果,设计合理的表结构,避免重复数据堆积。
- Web 展示模块:基于 Django 提供列表查询、关键字筛选、统计图表页面。
这三个模块之间是松耦合的。我特意把爬虫和 Django 分开,而不是在 Django 的 view 里直接写爬虫逻辑。原因是爬虫这种耗时操作放在请求线程里非常危险,页面会卡死,而且爬虫失败时不应该影响系统运行。实际开发中,爬虫先跑完,数据落入数据库,Django 只负责查库展示,这样两边出问题都好排查,调试效率也高。
1.2 为什么选 Python + Django + 去哪儿网这个组合
这个组合是我反复权衡过才定下来的,不是随便拿市面上现成框架硬凑。先说抓取端,Python 生态里 requests 库写爬虫可以说是零门槛,比 Java 的 HttpClient 舒服太多,加上 lxml、BeautifulSoup 这些解析库,半个小时就能把基础爬虫跑通。去哪儿网页面结构里,机票和酒店数据是以 JSON 接口形式返回的,这让解析难度比直接解析 HTML 低了一个档次,只需要用 json 模块就能把字段取出来。
再聊 Django,选它的核心原因是“自带全套”。它自带 ORM,不用手写 SQL 就能完成数据库增删改查;自带 Admin 后台,数据管理页面几乎是白送的;自带模板系统,配合 Bootstrap 和 ECharts 就能做出一套不错的界面。对于一个课程设计,Django 可以少写二三百行重复代码。
至于数据库选择 MySQL,是因为它在国内用得最多,也是“数据库课程设计”这个环节里点名率最高的数据库软件。MySQL 的数据导入导出、可视化工具(Navicat、DBeaver 等)都很成熟,最后交付时你直接把 sql 文件丢给老师,他就能在本地还原。如果你的环境装不上 MySQL,用 SQLite 也能完整跑通这个项目,Django 切换数据库的成本极低,改一下 settings 即可。
1.3 系统架构与项目目录规划
系统整体流程是单向数据流:爬虫程序采集数据 -> 清洗字段 -> 写入 MySQL -> Django ORM 读取 -> 视图层渲染页面 -> 前端图表展示。我最终项目的目录结构是这样规划的:
code复制travel_spider/
├── manage.py
├── spiders/
│ ├── qunar_spider.py # 爬虫核心代码
│ └── data_clean.py # 数据清洗工具
├── travel_web/ # Django 项目配置
├── analysis/ # Django app:列表与统计展示
├── templates/
│ ├── base.html
│ ├── flight_list.html
│ └── dashboard.html
├── static/
│ ├── css/
│ └── js/
├── db.sqlite3 或 MySQL 数据
└── requirements.txt
这样设计有个好处:爬虫和 Web 是完全独立的两个入口,你在命令行执行 python spiders/qunar_spider.py 就能采集数据,而 python manage.py runserver 启动的是 Web 系统。后续如果要加定时任务,只需要用 crontab 或系统计划任务定期执行爬虫脚本即可,完全不用动 Web 端代码。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心细节解析与实操要点
2.1 抓包分析:定位去哪儿网的 JSON 数据接口
这个系统能不能跑起来,最关键的环节不是写代码,而是抓包分析。我第一次做的时候直接去解析搜索页面的 HTML,结果发现页面源码里几乎没有航班数据,全是从接口异步加载的,白折腾了一整天。后来老老实实打开浏览器开发者工具,切换到 Network 面板,重新发起一次机票搜索,过滤 XHR 请求,就能看到返回 JSON 的真实接口。
这种接口的典型特征是在请求参数中携带出发地、目的地、日期、乘客人数等信息,返回数据体量不大,但结构层级嵌套很深。我这里以航班搜索为例,简化说明:
python复制import requests
import json
headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
"Referer": "https://flight.qunar.com/",
"Accept": "application/json",
}
params = {
"searchType": "0",
"depCity": "北京",
"arrCity": "上海",
"date": "2025-06-20",
"adultNum": "1",
"childNum": "0",
"infantNum": "0",
}
resp = requests.get("https://flight.qunar.com/twell/domestic/interface/search_result.jsp",
headers=headers, params=params, timeout=10)
data = resp.json()
返回的 JSON 结构里,航班列表一般在 data.flight 这种层级下,每个航班包含 flightNo(航班号)、depTime(出发时间)、arrTime(到达时间)、price(价格)、airCompany(航空公司)、depAirport(出发机场)等字段。不同批次抓包接口字段名可能有变化,所以写解析代码前先在控制台把 JSON 打印出来,看清层级,这一步能省很多调试时间。
注意:任何网站的数据结构和接口都可能发生变化。如果 2025 年之后接口改了路径或字段名,你只需要重新抓包定位新接口、调整一下 URL 和字段映射,整体爬虫架构不用推翻重建。
2.2 爬虫代码要点:请求重试、字段清洗和入库
爬虫部分我一般不写得很复杂,但有两个细节必须处理:一是请求失败重试,二是字段清洗。直接上核心逻辑:
python复制import time
import pymysql
import requests
def fetch_flights(url, params, retry=3):
for i in range(retry):
try:
resp = requests.get(url, params=params, headers=headers, timeout=10)
resp.raise_for_status()
return resp.json()
except Exception as e:
print(f"第{i + 1}次请求失败: {e}")
time.sleep(2)
return None
def clean_price(raw_price):
# 接口返回的可能是带单位的字符串,统一转成数字
if isinstance(raw_price, str):
return float(raw_price.replace("¥", "").replace(",", "").strip())
return float(raw_price)
def save_to_mysql(items):
conn = pymysql.connect(host="localhost", user="root", password="123456",
database="travel_db", charset="utf8mb4")
cursor = conn.cursor()
insert_sql = """
INSERT INTO flight_info (flight_no, dep_city, arr_city, dep_time,
arr_time, price, airline, created_at)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
ON DUPLICATE KEY UPDATE price = VALUES(price);
"""
for item in items:
cursor.execute(insert_sql, (
item["flightNo"], item["depCity"], item["arrCity"],
item["depTime"], item["arrTime"], item["price"],
item["airCompany"], time.strftime("%Y-%m-%d %H:%M:%S")
))
conn.commit()
cursor.close()
conn.close()
我特意在 SQL 里加了 ON DUPLICATE KEY UPDATE,配合表结构中 flight_no + dep_time 的唯一索引,可以做到同一航班同一时间只保留一条记录,重复抓取时自动更新价格。这个设计是从实际需求里长出来的——爬虫不是只跑一次,你可能会跑很多次,没有唯一键约束,数据库很快就被重复数据撑爆了。
字段清洗看起来是小事,实际是爬虫里最容易出 bug 的地方。比如价格字段可能返回 "¥680" 或者 "680.00",时间字段可能是时间戳也可能是格式化的字符串,出发城市可能带“市”字。我建议新建一个 data_clean.py,把字符串转数字、字段缺失补默认值、去除首尾空格这些逻辑集中在一起,别散落在主爬虫文件里。
2.3 数据库表结构设计:别在第一步就留下隐患
数据库设计直接决定后面 Django 代码好不好写。我没用 Django 的 models 直接建表,而是先手工在 MySQL 里创建好库和表,再用 Django 的 inspectdb 反向生成模型,这样更贴近“先有数据库再有业务”的课程设计场景,也方便检查表结构。
航班信息表设计如下:
sql复制CREATE TABLE flight_info (
id INT AUTO_INCREMENT PRIMARY KEY,
flight_no VARCHAR(20) NOT NULL COMMENT '航班号',
dep_city VARCHAR(50) NOT NULL COMMENT '出发城市',
arr_city VARCHAR(50) NOT NULL COMMENT '到达城市',
dep_time DATETIME NOT NULL COMMENT '出发时间',
arr_time DATETIME NOT NULL COMMENT '到达时间',
price DECIMAL(10,2) NOT NULL COMMENT '票价',
airline VARCHAR(50) DEFAULT '' COMMENT '航空公司',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '采集时间',
UNIQUE KEY uk_flight_dep (flight_no, dep_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='航班采集表';
几个关键点值得强调:第一,dep_time 用 DATETIME 而不是 VARCHAR,这样 Django 做日期筛选、按月份统计价格时能直接用 SQL 函数,效率和正确性都更高。第二,价格字段用 DECIMAL(10,2),别用 FLOAT,浮点计算在金融场景下坑很深,后面算均价、环比、同比都会出精度问题。第三,表名和字段名统一用下划线风格,Django 的 ORM 默认约定就是这样,省得反向生成时手动改字段映射。
酒店表的设计思路类似,核心字段包括酒店名称、所在城市、最低价格、评分、评论数、抓取时间。如果你想做的是“酒店价格趋势分析”,那可以再加一个 price_date 字段,同一天抓取的同一酒店价格汇聚在一起,后面做折线图会非常顺手。表结构设计这一步,一定要站在“我将来要查什么”的角度倒推字段,而不是站在“接口返回了什么”的角度照搬。
3. 实操过程与核心环节实现
3.1 环境准备:Python、虚拟环境与依赖安装
开始写代码之前,先把基础环境弄干净。我用的是 Python 3.8+,建议你装 3.10 之前的稳定版本,某些第三方库对新版本适配可能有延迟。安装完 Python 之后,第一步不是急着 pip install django,而是先建虚拟环境。虚拟环境的作用是让当前项目的依赖和系统全局 Python 隔离开,避免多个项目之间的包版本互相打架。
Windows 下创建和激活虚拟环境的命令是这样的:
bash复制python -m venv venv
venv\Scripts\activate
macOS / Linux 下激活命令是 source venv/bin/activate。如果你以后不想要这个虚拟环境了,直接删除整个 venv 文件夹即可,这就是最简单粗暴的“删除虚拟环境”方法。很多人来问虚拟环境怎么删除,其实它就是个文件夹,删掉就没了,不用执行什么卸载命令。
激活后安装依赖:
bash复制pip install django==4.2 requests pymysql pandas
如果你在国外服务器上装包慢,可以临时换成清华源:
bash复制pip install -i https://pypi.tuna.tsinghua.edu.cn/simple django requests pymysql
这里我特意强调 pymysql 而不是 mysqlclient。这两个都是让 Django 连 MySQL 的驱动力库,但 mysqlclient 在 Windows 上经常要编译环境,装起来容易报错。pymysql 是纯 Python 实现,装完即用,对课程设计项目来说完全够用。后面你在 Django 的 __init__.py 里加一句 pymysql.install_as_MySQLdb(),Django 就能把它当成 MySQLdb 来使用。
3.2 Django 项目初始化:创建项目、创建 app
环境配好之后,执行下面这组命令,完成 Django 项目初始化和 app 创建:
bash复制django-admin startproject travel_web
cd travel_web
python manage.py startapp analysis
创建出来的 analysis 这个 app,就是负责展示和分析业务的核心模块。我已经习惯了项目配置和业务模块分家的组织方式:travel_web 这个包里面只放 settings、urls 这些全局配置,所有的业务代码都写在 app 里。后面如果要加一个酒店管理模块,再创建一个新的 app 就行,不用动已有代码,这种拆分在课程设计的答辩环节也很加分,老师会认为你有模块化意识。
创建完成后,别忘了在 travel_web/settings.py 的 INSTALLED_APPS 列表里加上 analysis:
python复制INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"analysis", # 自己添加的 app
]
如果不加这一步,你后面做的模型迁移、模板查找、静态文件收集,全都不会生效。这个低级坑我见过不少同学踩过,写代码 5 分钟,找 bug 半小时。
3.3 数据库连接配置与模型反向生成
连接 MySQL 之前,先在本地数据库软件里建好库,然后修改 settings 里的数据库配置:
python复制DATABASES = {
"default": {
"ENGINE": "django.db.backends.mysql",
"NAME": "travel_db",
"USER": "root",
"PASSWORD": "123456",
"HOST": "127.0.0.1",
"PORT": "3306",
"OPTIONS": {
"charset": "utf8mb4",
},
}
}
同时在 travel_web/__init__.py 里加一行:
python复制import pymysql
pymysql.install_as_MySQLdb()
配置好之后,我不建议手写 models.py,直接让 Django 扫描已经存在的表结构,反向生成模型:
bash复制python manage.py inspectdb > analysis/models.py
生成后检查一下模型里的 db_table 是否指向了正确的表,然后自己补上字段注释和类型校验即可。Django 的 ORM 查询写起来相当顺手,这是它比直接写 SQL 方便的地方。比如我要查均价最低的 10 个航班:
python复制from analysis.models import FlightInfo
cheapest_flights = FlightInfo.objects.all().order_by("price")[:10]
删除某条记录对应的是 FlightInfo.objects.filter(id=xxx).delete(),Django 执行查询和删除对象都有对应的 ORM 方法,你完全不需要手写 SQL,这正是 Django 为开发者省事的主要体现。
3.4 视图与模板:实现列表筛选和统计展示
有了模型之后,接着写视图。我这里设计了两个核心页面:一个是航班列表页,支持按出发城市、到达城市筛选并分页显示;另一个是数据统计面板页,用柱状图和折线图展示各航司航班数量、价格走势。
列表视图代码像这样:
python复制from django.shortcuts import render
from django.core.paginator import Paginator
from analysis.models import FlightInfo
def flight_list(request):
flights = FlightInfo.objects.all()
dep_city = request.GET.get("dep_city", "")
arr_city = request.GET.get("arr_city", "")
if dep_city:
flights = flights.filter(dep_city=dep_city)
if arr_city:
flights = flights.filter(arr_city=arr_city)
paginator = Paginator(flights, 20)
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
return render(request, "flight_list.html", {"page_obj": page_obj})
模板里就用 Django 模板语言循环输出航班数据,分页栏用页面上下文里的 page_obj 控制上一页下一页。这里有一个初学者特别容易踩的坑:分页后,如果当前 URL 里带着 ?dep_city=北京,点击下一页时筛选条件会丢失。解决办法是在模板分页链接里保留已有的 GET 参数,或者干脆把筛选字段放进隐藏 input,用表单 GET 提交。
统计面板的实现,我选择让 Django 视图里直接聚合数据,把结果以 JSON 形式传给前端 ECharts。比如按航空公司统计航班数量的聚合查询:
python复制from django.db.models import Count
def dashboard(request):
airline_stats = (
FlightInfo.objects.values("airline")
.annotate(total=Count("id"))
.order_by("-total")
)
return render(request, "dashboard.html", {"airline_stats": list(airline_stats)})
前端模板里,我通过 Bootstrap 和 CDN 引入 ECharts,把 airline_stats 用 json_script 过滤器传到 JS 变量里,再用图表 API 渲染。这样一个简单的数据可视化页面就出来了,不用写任何前后端分离的复杂逻辑,课程设计完全够用。
4. 常见问题与排查技巧实录
4.1 爬虫常见问题:403、封 IP、数据为空
我在实际开发中遇到的问题,八成集中在爬虫这块,很多问题现在看来是小坑,第一次碰见时却会卡住很久。
第一个高频问题是 403 Forbidden。这个报错几乎都是因为请求头不够完整,服务器识别出这是脚本请求。解决办法是把浏览器开发者工具里看到的 User-Agent、Referer、Accept 等请求头全部复制过来。有时候还需要加上 Cookie,但课程设计里尽量不要依赖硬编码的 Cookie,因为过期后要重新抓取,很麻烦。
第二个是请求频率过高导致 IP 被封。我的处理方案非常简单粗暴:每次请求后强制 time.sleep(3),同时把搜索日期分散开来,一次抓三个月的数据,中间隔五秒以上。这样虽然速度慢,但稳定性高很多。如果做生产级爬虫,还可以用代理池,但课程设计没必要,慢一点不是问题。
第三个是“请求成功但是数据为空”。这个最常见的原因是接口返回的 JSON 里有一个状态码字段,可能因为缺少某个参数而返回空列表,但 HTTP 状态码仍然是 200。所以解析 JSON 后,一定要先检查业务状态码,再解析数据,不能想当然地认为请求成功就等于数据获取成功。
第四个是网站接口变更。我把这个单列出来,是因为这类问题没有彻底的解决办法,只能靠“抓包-定位-调整”这个循环来应对。我的建议是爬虫代码里把所有接口 URL 和字段名抽到配置区,万一接口改了,改配置比改逻辑快得多。
4.2 数据库问题速查:连接、乱码、重复数据
数据库这块,我把常见问题整理成一张表,你碰到对应现象直接查表:
| 现象 | 原因 | 解决方案 |
|---|---|---|
连接报错 Access denied for user |
用户名密码错误或权限不足 | 检查 MySQL 用户授权,执行 GRANT ALL ON travel_db.* TO 'root'@'localhost' |
| 中文乱码 | 表或连接字符集不是 utf8mb4 | 建表时指定 DEFAULT CHARSET=utf8mb4,连接配置加 charset=utf8mb4 |
| 重复数据 | 表缺少唯一索引 | 给业务唯一字段加 UNIQUE KEY,爬虫入库时用 ON DUPLICATE KEY UPDATE |
| 时间字段差 8 小时 | Django 时区设置问题 | settings 里 TIME_ZONE = "Asia/Shanghai",USE_TZ = False |
| 查询特别慢 | 筛选字段没索引 | 对常用的 dep_city、dep_time 字段加普通索引 |
时区问题值得多说一句:Django 默认开启了 UTC 时区,如果你把 USE_TZ 设为 True,那么从数据库读出来的 DATETIME 会转换成系统 UTC 时间显示,导致你明明存的是北京时间 14:00,页面显示却是 06:00。课程设计里我建议直接 USE_TZ = False,存什么就显示什么,省心。如果要做生产系统再考虑统一 UTC 的规范。
4.3 Django 运行与部署问题
Django 本身开发调试很顺畅,但真到了要把项目部署到服务器时,坑就来了。如果你的课设要现场演示,不要求外网访问,那 python manage.py runserver 0.0.0.0:8000 就够了。但如果老师要求你把系统部署起来,我建议别用 runserver,它性能差且不够稳定,换 gunicorn 和 Nginx 的组合是更常规的方案。
我的部署步骤一般是这样的:先在服务器上装 Python 和 MySQL,把项目代码传上去,创建虚拟环境,安装依赖;然后执行 python manage.py migrate 和 python manage.py collectstatic;接着用 gunicorn 启动 Django,命令大致形如 gunicorn travel_web.wsgi:application -b 127.0.0.1:8000;最后在 Nginx 里配置反向代理,把 80 端口转到 8000。
如果你不想折腾 Nginx,也有更省事的办法:很多面板工具支持一键部署 Python 项目,导入项目目录、指定启动命令就能自动完成 Nginx 和进程守护。我在帮学员部署课程设计时用过几次,体验确实方便,适合对 Linux 不熟悉的同学。需要注意的一点是,部署后静态文件路径一定要配置正确,很多人 Django 页面打开了但没样式,就是 STATIC_ROOT 和 Nginx 静态目录没配对。
4.4 常见异常解决思路
做开发时异常信息是最好用的调试线索,但有些人一看到红色报错就蒙了。我总结一套排查思路:先看报错最后三行,确定是哪个文件哪一行代码出问题;再判断属于哪一类——网络请求类、数据库类、还是模板渲染类;网络问题就检查 URL、请求头、超时,数据库问题就检查连接配置、表结构、字段名,模板问题就检查变量名和过滤器语法。
比如最典型的 NoReverseMatch 报错,出现在模板中 {% url %} 标签写错视图别名,或者 urls.py 里没配对 name。对应到操作就是去 urls 文件里核对 name 参数。再比如 TemplateDoesNotExist,虽然提示是模板不存在,但实际原因很可能是你忘了把 app 加进 INSTALLED_APPS,Django 的模板查找器压根不扫描这个 app 的目录。这类问题表面和实质不一致,一旦遇到,别只盯着报错表面,先检查整体配置。
5. 项目扩展与实际应用建议
5.1 从课程设计走向完整项目的扩展方向
这个项目如果只停留在“跑通并答辩”的层面,其实已经完成了课程设计的使命。但如果你有精力,我建议往几个方向再推一步,这些扩展点也是答辩时最容易拿加分的地方。
第一,给爬虫加定时调度。你可以用系统自带的任务计划程序定个每天凌晨两点爬取的任务,这样数据库里会累积出时间序列数据,统计分析就变成了真正的“趋势分析”,评分的含金量完全不一样。第二,把数据采集模块从 requests 升级到 Scrapy 框架,Scrapy 自带并发调度、去重、中间件机制,代码组织更规范,这是爬虫进阶的必经之路。第三,在 Web 端加一个用户登录和爬取任务管理功能,用 Django 自带的 User 模型加上简单的权限控制,系统就更像一个产品了。第四,把数据库和 Web 服务都用 Docker 容器化,交付时给老师一个 docker-compose up 一键启动,这种工程化能力在简历上是明显的加分项。
5.2 我对这个项目的一些实操体会
最后说点个人感受。这个项目最花时间的部分,其实不是 Django 代码,也不是爬虫逻辑,而是抓包分析和数据清洗。抓包要一层层地看请求参数和响应层级,数据清洗要处理各种接口返回的脏数据。很多初学者拿到需求就想直接写代码,结果写一半发现接口字段拿错了,或者存进去的数据没法统计,返工成本极高。
我自己的实践方法是:正式动手之前,先花半天时间把接口返回的 JSON 完整打印出来,一条条核对字段,把需要清洗的字段列成清单,甚至先在 Excel 里模拟一下最终数据分析的表格结构,然后再写代码。这个习惯帮我避开了大量后期改表结构和改解析逻辑的麻烦。
另外一个建议是,爬虫和 Web 开发两部分的代码,分别提交到 Git 仓库的不同分支,或者至少用文件目录天然隔开。这个项目我一开始就坚持爬虫脚本独立于 Django 项目运行,事实证明后面改任何一边都不会影响另一边,整个项目维护起来非常舒服。
如果你准备拿这个项目去做课设或者毕业设计,最后整理文档时,建议把数据库建表语句、爬虫抓包分析截图、系统功能截图这三部分作为重点。文档里先说清楚系统的整体架构,再分模块讲实现思路,每个模块配核心代码和运行截图。这套材料整理下来,答辩基本就稳了。
这个项目后续还能扩展的方向也很多,可以往多平台数据采集走,也可以往数据可视化大屏方向做,核心的数据采集与 Web 展示框架搭好后,换数据源、换图表类型都是顺手的事。如果你在复现过程中卡在抓包或者接口字段变化的问题上,不用慌,按我上面说的方式重新抓一次包、调整一下字段映射就行,骨架逻辑都不会变。
