每年毕业季,都会有一大批计算机专业的同学盯上“租房数据可视化分析平台”这个选题。原因很简单:它把爬虫、Web开发、数据库、可视化全串起来了,工作量足够,技术栈又不像人工智能那么抽象,适合本科毕设,也适合拿来当作品集。我前后带过几个小朋友做这类项目,自己也完整落地过一套,今天就把整条链路拆开讲一遍,从技术选型到爬虫细节,再到Flask后端和Echarts大屏,最后把最容易踩的坑都摆出来。这个项目围绕Python、Flask、Echarts、爬虫、Layui五个核心词展开,看完你至少能自己搭一个能答辩、能演示、能扩展的完整系统。
1. 项目到底做什么:先搞清楚需求再动手
1.1 毕设常见误区:别把“展示”当“系统”
很多同学拿到“租房数据可视化分析平台”这个题目,第一反应是“我要做一个好看的网页,放几张图”。这不是系统,是PPT。毕设评审老师首先看的是你的系统有没有完整的数据流转:数据从哪来、存到哪、后端怎么处理、前端怎么展示。如果只是拿一份现成Excel导入前端画图表,爬虫部分就被架空了,整个项目的核心工作量直接砍掉一半。
我当时给学生的建议是:把题目拆成四个必须闭环的环节——数据采集(爬虫)、数据存储(数据库)、业务逻辑(后端接口)、结果呈现(可视化页面)。每一个环节都要有真实的代码和可运行的结果。比如爬虫抓下来的数据要能入数据库,数据库里的数据要能被API接口查询,前端页面要能通过Ajax拿数据并渲染图表。这条链路打通了,答辩时你演示一遍,老师就知道你是真做了。
接下来要明确“可视化分析”到底分析什么。租房数据常见维度包括:区域分布、价格区间、户型结构、面积范围、租房类型(整租/合租)、朝向、楼层、发布时间等。分析目标一般是“某个城市不同区域的租金水平对比”“热门户型占比”“价格随面积变化的趋势”这类。把分析指标确定下来,再去设计爬虫字段和数据库表结构,就不会做了半天发现字段不够用。
1.2 功能拆解与模块划分
我习惯把系统分为三大模块。第一是数据采集与预处理,负责爬取目标网站的房源列表和详情页信息,做清洗、去重、标准化,最后入库。第二是后端服务,用Flask提供RESTful接口,接收前端查询参数,从数据库聚合统计并返回JSON。第三是前端展示,用Layui搭建页面框架,用Echarts绘制图表,通过Ajax和后端交互。
具体到页面,至少要包含四个视图:总览大屏(展示核心指标和地图分布)、区域分析(柱状图对比各区租金)、户型结构(饼图展示户型占比)、趋势分析(折线图展示价格走势或房源增量)。这些页面数量说多不多,但每个页面都牵扯一个聚合查询接口,后端设计时要预留好参数。我最初只做了三个页面,后来为了凑功能加了“价格区间分布”和“面积-价格散点图”,其实散点图很加分,因为能直观展示租金和面积的关系,代码也不复杂。
还有一个容易被忽略的功能点:数据手动刷新。爬虫跑完之后数据不会自动更新,所以我在前端加了一个“同步数据”按钮,点击后触发后端的爬虫脚本,这样演示时可以说系统支持增量采集。当然真正生产环境会用定时任务,但毕设里一个按钮足够体现设计思路了。
1.3 技术选型的心路历程:为什么Flask+Echarts+Layui
关于后端框架,很多人会纠结Flask和FastAPI。我实际对比过:FastAPI性能更好,自动生成API文档,但它在国内毕设里还不算主流,答辩时老师可能不熟悉;Flask生态成熟、中文资料多、Layui和Echarts的例子一搜一大把,出了问题好解决。而且Flask轻量,适合快速开发,对毕设来说完全够用。如果你后期想加用户登录、权限管理,Flask也有现成扩展;如果想加机器学习预测,Flask也能直接调用模型。所以选Flask不是因为它最强,而是因为它最稳。
前端框架为什么不选Vue/React?因为没有必要。Layui是服务端渲染时代的产物,但它自带表格、表单、导航、弹层等组件,页面风格偏后台管理,特别适合数据可视化类项目。你不需要会webpack,不需要构建工具,直接把layui.js和layui.css引进来就能用,和Flask模板渲染配合起来非常自然。而Echarts是百度的开源可视化库,图表类型丰富,配置项灵活,尤其是地图、饼图、柱状图、折线图的视觉效果好,API文档全中文,上手成本极低。三个技术凑在一起,正好覆盖了“后端接口 + 页面框架 + 数据图表”三个层次。
数据库方面,SQLite适合demo,MySQL是答辩标配。我的建议是直接用MySQL,8.0版本,字符集选utf8mb4。SQLite虽然零配置,但老师可能会问“为什么不用MySQL”,你还得解释,干脆一开始就上MySQL,本地装个MySQL服务也不麻烦。后面部署到服务器也方便迁移。
2. 爬虫模块:房源数据从哪来、怎么抓
2.1 目标分析与爬取策略
租房网站的页面结构大同小异,核心就是列表页和详情页。列表页展示小区名、户型、租金、面积等摘要信息,详情页有更完整的字段,比如朝向、楼层、装修、发布时间、所在楼层、维护时间。爬虫策略我建议分两步:先抓列表页,解析出房源详情页的链接和基础字段;再访问详情页补齐其他字段。如果嫌详情页请求数量太大,可以只抓列表页,但字段会少一些,分析维度不够。为了毕设完整性,建议把详情页也抓了。
这里要强调一个概念:一定要先分析目标网站的robots协议和反爬机制。租房网站的列表页通常用翻页实现,URL可能有规律,也可能由JavaScript动态渲染。如果页面是服务端渲染,直接用requests+BeautifulSoup或requests+XPath就能搞定;如果是Ajax加载,需要找到真实的JSON接口。我用过requests+XPath组合,主要是因为XPath在处理HTML节点时非常直接,尤其是text()函数配合contains()可以精准提取信息。
爬取时要控制请求频率。最简单的做法是在两次请求之间sleep(1)到sleep(3)秒,随机化请求头。更稳妥的做法是设置一个请求重试机制,比如连续失败3次就跳过该页面并记录日志。我当时写了一个fetch_page()函数,内部处理了超时、异常状态码、编码探测,所有页面请求都走这一个入口,这样修改请求头或代理配置时只需要动一处。这也是爬虫代码可维护性的关键。
2.2 基于XPath的信息提取实战
XPath提取文本时最常见的困惑是text()和string()的区别。我拿一个实际字段举例,比如提取房源的“租赁方式”:
from lxml import etree html = etree.HTML(response.text) # 假设页面里有 <span class="des">整租 · 3室1厅 · 85平米</span> item = html.xpath('//span[contains(@class, "des")]//text()')这里用//text()会返回该节点下所有文本节点,包括子元素的文本,结果可能是一个列表,例如['整租', '·', '3室1厅', '·', '85平米']。而如果写string(//span[contains(@class, "des")]),它会返回整个节点内部的纯文本拼接,去掉标签,得到整租 · 3室1厅 · 85平米。所以在提取紧凑文本时,优先用string()或先拿到节点再.xpath('string(.)'),避免拆出碎片。这个细节别轻视,很多同学清洗数据时发现字段乱七八糟,根源就在提取阶段把文本拆碎了。
字段规整是另一个核心。比如面积字段“85平米”,需要提取数字并转成float;价格字段“4500元/月”,提取4500;朝向可能是“朝南”或“南”,也归一化。写爬虫时我习惯在parse阶段就完成清洗,而不是入库后再处理。这样可以减少后面SQL的麻烦。清洗逻辑用正则表达式就够:re.search(r'(\d+\.?\d*)', text)。特别注意有些网站面积可能是“85㎡”,正则改成(\d+(\.\d+)?)\s*[㎡平]?。
还有一个容易忽略的点:发布时间。列表页经常出现“今天更新”“3天前发布”这类相对时间,直接存字符串没法做时间趋势分析。我写了一个parse_time()函数,把“今天”“昨天”“N天前”转换为具体日期,转换逻辑很简单,就是datetime.now() - timedelta(days=n),但要注意爬虫运行日期的时间点,否则会出现未来日期。这个转换后的日期是后续做“房源增量趋势图”的基础,不能省。
2.3 数据清洗与入库前的最后把关
爬虫跑完会得到一批“半成品”数据,里面可能有重复房源、缺失字段、异常值。别急着入库,先过三关。第一关是去重,房源ID是天然的主键,如果目标网站没有ID,就用“小区名+价格+面积+户型”做成指纹,用MD5生成唯一键。我之前遇到过同一个房源在列表页和详情页各出现一次,如果没有去重逻辑,数据库里会多出一倍的记录。
第二关是缺失值处理。有些房源详情页可能缺失朝向或装修信息,字段可以为空,但前端的图表也要能处理。我在后端聚合时用IFNULL或SQLAlchemy的func.coalesce,把空值替换成“未知”。如果是价格缺失,建议直接丢弃这条记录,因为价格是核心字段,没有价格的分析没有意义;面积缺失则可以用同一户型的中位数填充,但这个要说明是估算,不能硬造。
第三关是异常值过滤。比如爬到一个“面积200平方米,租金500元/月”的数据,明显是虚假房源或页面解析错误,这种数据会影响可视化结果。我在清洗阶段设置阈值:租金低于100元且面积大于150平的记录直接过滤,或者用mean ± 3倍标准差做简单的离群点剔除。毕设阶段不需要高深算法,但要有这个意识,答辩时可以提“经过数据清洗保证了统计结果的可靠性”。
清洗完成后写入MySQL。这里推荐使用INSERT IGNORE或ON DUPLICATE KEY UPDATE来避免重复,配合唯一索引。如果你用SQLAlchemy,可以设置unique=True约束,然后数据库层面做兜底。总之,入库环节一定要稳。
3. 数据持久化:建表、入库、查询优化
3.1 表结构设计与字段类型选择
租房信息表我一般命名为house_info,核心字段包括:id(自增主键)、house_id(房源唯一ID,加唯一索引)、title、layout(户型)、rent_type(整租/合租)、area(面积,DECIMAL(6,1))、price(月租金,INT)、district(区域)、block(商圈或小区)、orientation(朝向)、floor(楼层)、decoration(装修)、publish_date(发布时间)、source_url(详情页链接)、crawl_time(爬取时间)、is_valid(是否有效标记)。
字段类型有一个细节:价格一定用INT,因为租金都是整数;面积用DECIMAL而不是FLOAT,避免浮点误差。publish_date用DATE类型,查询BETWEEN区间非常方便。district和block都用VARCHAR(50)就够了,但别忘了加索引,因为后面所有区域分析都是根据district做GROUP BY,没有索引的查询会很慢。
要不要建分区表?我觉得没必要,毕设数据量撑死几万条,加索引后毫秒级返回。真正的优化点在于聚合查询。SQLAlchemy的func函数族,如func.count、func.avg、func.group_concat,能帮你写出优雅的聚合逻辑。还有一点:如果前端同时展示多个图表,每个图表都查一次数据库也是OK的,但如果图表太多,可以考虑在后端做缓存,比如用functools.lru_cache或Redis。毕设阶段,lru_cache就够,加上缓存过期时间,省得每次都打数据库。
3.2 批量入库与去重策略
爬虫采集到的数据通常是几百上千条一次性到达,逐条插入太慢,而且事务开销大。我推荐的方案是用SQLAlchemy Core的insert().values([...])批量插入,或者直接写原生SQL。在代码里的写法是:
from sqlalchemy.dialects.mysql import insert stmt = insert(HouseInfo).values(list_of_dicts) stmt = stmt.on_duplicate_key_update(price=stmt.inserted['price']) db.session.execute(stmt) db.session.commit()这段逻辑借助MySQL的ON DUPLICATE KEY UPDATE实现“存在则更新、不存在则插入”,配合house_id唯一索引,天然去重。如果你用的是纯requests+PyMySQL,也可以写成:
INSERT INTO house_info (house_id, title, price, ...) VALUES (...) ON DUPLICATE KEY UPDATE title=VALUES(title), price=VALUES(price);千万别小看这个批量插入。我第一次写爬虫时用session.add()逐条提交,6000条数据跑了二十多分钟,换成批量插入后几秒钟就完了。这里再提一个经验:爬虫每跑完一个列表页,可以先把解析结果暂存在内存列表里,攒到200条再批量入库,这样既能提高效率,又能在中途出错时快速定位到具体页面。
3.3 SQLAlchemy模型配置与连接池
Flask-SQLAlchemy是标准选择。配置连接池时,注意SQLALCHEMY_POOL_SIZE和SQLALCHEMY_POOL_RECYCLE,尤其是MySQL默认的wait_timeout是8小时,如果你启动Flask后长时间不访问,连接会失效,再次请求时可能抛“MySQL server has gone away”。解决办法是把pool_recycle设为3600秒,让连接池在空闲超时前回收连接。另外一个冷门但重要的配置是SQLALCHEMY_TRACK_MODIFICATIONS设为False,不然会有内存警告。
表模型定义如下:
class HouseInfo(db.Model): __tablename__ = 'house_info' id = db.Column(db.Integer, primary_key=True, autoincrement=True) house_id = db.Column(db.String(32), unique=True, index=True) title = db.Column(db.String(128)) layout = db.Column(db.String(32)) rent_type = db.Column(db.String(8)) area = db.Column(db.Numeric(6, 1)) price = db.Column(db.Integer) district = db.Column(db.String(50), index=True) ...注意district加索引。另外,不要把表名写成house,house在某些SQL方言里是保留字,虽然MySQL不是,但换个词更保险。表名就老老实实用house_info。模型写好后,执行db.create_all()即可建表,但后续加了新索引或新字段,create_all()不会自动更新,你需要在MySQL里手动ALTER TABLE,或者用Flask-Migrate管理。毕设阶段手动改表就行,但记得备份数据。
4. Flask后端:API设计比想象中简单
4.1 路由规划与接口设计
Flask后端最重要的不是渲染页面,而是提供API。我通常把API分成两类:一类是静态资源接口,比如首页、图表页面的模板渲染;另一类是数据接口,统一以/api/开头,返回JSON。这样做的好处是前端和后端职责分明,以后想换成Vue前端,只要保留API就能复用。
我设计的接口大致如下:
GET / 渲染首页 GET /api/rent/overview 返回核心指标:总房源数、平均租金、区域总数、最新更新时间 GET /api/rent/district_stats 返回各区域房源数、平均租金、最高/最低租金 GET /api/rent/layout_pie 返回户型占比 GET /api/rent/price_range 返回价格区间分布 GET /api/rent/trend 返回发布时间/房源数量折线图数据 GET /api/rent/scatter 返回面积与价格散点数据 POST /api/rent/refresh 触发爬虫更新数据每个接口的路由内部逻辑基本一致:接收请求参数(可能是district、page),查询数据库,聚合处理成前端需要的结构,再jsonify返回。比如/api/rent/district_stats的典型实现:
from sqlalchemy import func @app.route('/api/rent/district_stats') def district_stats(): rows = db.session.query( HouseInfo.district, func.count(HouseInfo.id).label('count'), func.avg(HouseInfo.price).label('avg_price'), func.max(HouseInfo.price).label('max_price'), func.min(HouseInfo.price).label('min_price') ).group_by(HouseInfo.district).all() data = [{ 'district': r.district, 'count': r.count, 'avg_price': round(float(r.avg_price), 1), 'max_price': r.max_price, 'min_price': r.min_price } for r in rows] return jsonify(code=0, data=data)注意返回的avg_price做完round后是float,否则Numeric类型序列化时可能报错或变成字符串。这是一个非常典型的坑:SQLAlchemy的Numeric字段取出来是Decimal,jsonify不能直接序列化,所以要么转float,要么在jsonify里设置自定义JSONEncoder。我推荐在所有API出口都统一做类型转换,不要寄希望于全局encoder,因为不直观。
4.2 聚合统计分析怎么做
“可视化分析”的重点是分析,不是把原始数据堆给前端。前端Echarts虽然也能做数据加工,但最好在后端就把聚合结果算好,前端只负责渲染。比如“按价格区间分布”,这个查询逻辑就是按价格分桶统计:
import json buckets = [(0, 1500), (1500, 2500), (2500, 4000), (4000, 6000), (6000, 10000), (10000, 10**9)] labels = ['0-1.5k', '1.5-2.5k', '2.5-4k', '4-6k', '6-10k', '10k+'] # 思路:用case when 给每条记录打上桶标签,再分组统计我这里直接给一段可用的SQLAlchemy查询逻辑,使用case()函数:
from sqlalchemy import case bucket_case = case( [(HouseInfo.price < 1500, '0-1.5k'), (HouseInfo.price < 2500, '1.5-2.5k'), (HouseInfo.price < 4000, '2.5-4k'), (HouseInfo.price < 6000, '4-6k'), (HouseInfo.price < 10000, '6-10k')], else_='10k+' ) rows = db.session.query( bucket_case.label('range'), func.count(HouseInfo.id).label('cnt'), func.avg(HouseInfo.price).label('avg_price') ).group_by('range').all()注意case()的用法:从上到下依次匹配,所以把小的价格区间写在前面,这样第一个条件price < 1500已经把低价格的过滤掉了,后面不需要写price BETWEEN 1500 AND 2500。这种写法看似简单,但很多同学会纠结边界值,用case直接解决。而“按区域平均租金”就更简单,GROUP BY district再avg(price),前面已经演示过。
如果你还想分析“户型/面积的交互关系”,可以用GROUP BY layout, district这样形成交叉表。但毕设阶段不要过度设计,做两三个重点分析维度就够了。核心是让老师看到你有能力把原始数据转化为可交互的图表数据。
4.3 Flask与SQLAlchemy集成要点
集成阶段的坑大多是配置问题。第一,确保app.config['SQLALCHEMY_DATABASE_URI']写对,格式是mysql+pymysql://用户名:密码@localhost/数据库名?charset=utf8mb4。注意pymysql是纯Python驱动,容易安装,不需要编译;第二,如果你的Python版本是3.8,Flask-SQLAlchemy最高版本建议用2.5.1,3.x版本要求Python3.9+,不然安装会失败。第三,所有视图函数最好都放在app.app_context()里才进行数据库操作,如果你写了一个独立的爬虫脚本来调用模型,不要忘记在脚本里with app.app_context():包起来。
很多同学喜欢在爬虫里直接用原生的pymysql连接数据库,在Flask里又用SQLAlchemy,两套配置在一起容易混淆。我建议统一:爬虫脚本也复用Flask的app和db,把爬虫写成一个模块,函数入口为run_spider(),然后在Flask视图里调用,这样就能共享同一个db.session。而且这样还能实现我前面说的“前端点击同步数据按钮触发爬虫”的功能,不用开两个进程。
5. 可视化大屏:Layui搭骨架,Echarts画图表
5.1 Layui页面布局与组件使用
Layui的页面布局特别适合这种数据看板。我一般先用layui-col-md12或layui-row把页面网格化,顶部放标题和核心指标卡片,下方分左右两块,左侧放区域地图和柱状图,右侧放户型饼图和折线图。Layui的layui-card组件自带阴影边框,比手动写CSS省事得多。它还提供了“选项卡”模块,可以把多个图表放在不同tab里,避免页面滚动太长。
一个让我很省心的Layui特性是它的栅格系统自带响应式,你不需要自己写媒体查询,浏览器缩小窗口时模块自动换行。这块对于演示很关键,因为答辩现场可能用的是投影仪,分辨率比较小。如果你自己写float布局,很容易在投影上乱掉。Layui默认的后台风格偏灰白色,如果需要更亮眼的视觉效果,可以去主题文件里改$--global-color变量,或者直接写内联样式覆盖。
页面里引用Layui的方式很简单:
<link rel="stylesheet" href="/static/layui/css/layui.css"> <script src="/static/layui/layui.js"></script>然后初始化常用的模块:
layui.use(['jquery', 'layer', 'element'], function(){ var $ = layui.$; // 后续Ajax请求都基于这个 $ });这里有一个细节:layui内置的jquery是模块化隔离的,你需要在layui.use回调里拿到$,而不是在全局直接用$。不然你写$.ajax会报$ is not defined。我第一次用Layui时就栽在这里,排查了好久。
5.2 Echarts图表配置:柱状图、饼图、折线图、地图
Echarts的配置项核心是option对象,图表类型由series下的type决定。柱状图展示各区域平均租金时,我会开启grid的containLabel属性,否则y轴标签会被截断。另外给柱状图加渐变效果,只需在series.itemStyle.color里写对象:
color: { type: 'linear', x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: '#4A90E2' }, { offset: 1, color: '#63D0FF' } ] }饼图展示户型占比时,最关键的是把tooltip设置成trigger: 'item',并且formatter里可以用{b}和{d}分别代表名称和百分比。如果你觉得默认的提示框太丑,可以用axisPointer自定义。最近我在看Echarts实战案例,发现地理坐标图的视觉引导线(markLine)和富文本提示框非常实用——比如地图上的每个区域,鼠标悬停显示该区域的详细指标,同时旁边有一条折线指向侧边的解释文字,这种配置非常适合展示区域数据,让人觉得专业。
折线图要注意x轴刻度对齐。常规情况没问题,但如果你有两个系列,单位不一致,需要设置yAxis.type为value,并且yAxis.axisLabel.formatter可以加上单位后缀。我做过一个“不同区域房源数量与平均租金”的双轴图,左边是房源数(柱),右边是租金(线),配置两个yAxis就能实现,但记得series里对应yAxisIndex。
另外就是中国地图。如果租房数据是一个城市内的分析,用城市底图会更好。城市地图的GeoJSON需要自己去下载或找现成插件,Echarts官方地图数据已经不再内置。这个坑我在第五章详细说。
5.3 前后端数据交互:Ajax与JSON格式约定
前端所有图表的初始化数据都应该从后端接口拉取,不要在JavaScript里写死。我开发的约定是:接口统一返回{code: 0, data: ...},code=0表示正常,非0表示异常且msg字段携带错误信息。前端拿到后先校验code再渲染图表,这样出错时能第一时间看到提示,而不是图表空白。
用Layui的jQuery发起请求:
$.ajax({ url: '/api/rent/district_stats', type: 'GET', dataType: 'json', success: function(res) { if (res.code === 0) { var districts = res.data.map(item => item.district); var avgPrices = res.data.map(item => item.avg_price); myChart.setOption(option); } else { layer.msg(res.msg); } } });这里要注意:Echarts的实例必须在DOM渲染完成后创建,否则容器宽高为0,图表显示不出来。你可以在layui.use的回调里初始化,因为此时DOM已经就绪。如果页面有多个图表,我习惯用一个函数initCharts()统一初始化,并维护一个全局的charts对象,这样刷新数据时直接调用charts['districtChart'].setOption(newOption, true),第二个参数notMerge=true表示完全替换配置,不然有时setOption会叠加旧数据。
5.4 大屏自适应与主题配色技巧
答辩演示时可能用不同分辨率的屏幕,所以图表必须自适应。Echarts提供了resize()方法,配合监听窗口变化:
window.addEventListener('resize', function() { myChart.resize(); });更稳妥的做法是在每个图表初始化后都添加这个监听,或者用Layui的窗口事件。如果页面还有侧边栏折叠功能,记住在折叠动画结束后也调一次resize(),否则容器尺寸变了图表不会自动调整。
配色方面,我分享一个快速搭出好看配色的方法:从Echarts官网“主题编辑器”下载一个主题,比如“macarons”或“westeros”,然后在初始化时echarts.init(dom, 'macarons')注册主题。如果你不想下载文件,也可以自己定义颜色数组,一般5-6个颜色足够:
color: ['#3E8EED', '#16C79A', '#F6B93B', '#EA4250', '#9B59B6', '#34495E']这块不用过度发挥,保持整体协调就好。我在学生的答辩PPT上看到过各种荧光绿加大红,那真是噩梦。图表配色统一了,系统给人的第一印象就能提升一个档次。
6. 毕设过程中的高频坑:排查实录与解决套路
6.1 爬虫被反爬怎么办
租房网站的反爬策略通常有三种:User-Agent校验、IP频率限制、数据为JavaScript动态渲染。解决第一种,在request headers里伪装成正常浏览器的UA,同时加上Accept-Language,别提有多简单。第二种,控制请求间隔,我推荐用random.uniform(0.5, 1.5),再配合IP代理池;但毕设不建议自己搭代理池,本地几万条数据用不到。如果网站封了你的IP,等一段时间再爬,或者换手机热点,都是一时应急。
第三种比较麻烦:页面数据不在HTML里,而是通过Ajax接口动态渲染。这时候不要傻傻去解析HTML,直接找到接口。打开浏览器开发者工具,切到Network,刷新页面,找到返回JSON数据的XHR请求,直接请求这个接口,解析JSON,效率比解析HTML高得多。这个方法也适用于IKEA、自如这类偏前后端分离的站点。关键是找到接口的规律,比如常见的是/api/list?page=1,直接改page参数即可。
还有一个小技巧:如果你用requests请求出现403,但浏览器打开正常,大概率是缺少Referer头或Cookie。把浏览器里的Cookie复制到请求头里就能解决。不要用大量账号乱试,那种操作为一个毕设项目不值当。
6.2 中文乱码、编码问题
中文乱码是爬虫项目最容易原地爆炸的问题。根源一般有两个:一是网页编码判断错误,二是数据库连接字符集不匹配。前者可以在requests.get后通过resp.apparent_encoding检测,但前提是你已经拿到了resp.content。最简单的处理是:
resp = requests.get(url, headers=headers) resp.encoding = resp.apparent_encoding html = resp.text如果实在不行,就直接用resp.content.decode('gbk', errors='ignore'),因为很多网站用的是GBK编码。但如果你在解析时发现一些字仍然乱码,不要愁,之后把清洗后的文本存到MySQL里,入库前一定要保证数据库连接串带charset=utf8mb4,而且数据库和表本身也要utf8mb4。否则查询出来的数据在页面上显示一堆问号,那种情况很抓狂。
在Layui前端渲染时,如果JSON返回中文字符串没问题,但HTML页面本身乱码,记得在模板头部写<meta charset="utf-8">。这个低级的错误也常发生。
6.3 Echarts地图数据加载失败
使用Echarts绘制中国地图时,经常遇到Map ... not exists错误。原因是Echarts 5不再内置地图数据,需要注册GeoJSON。我用的是广州地图,做法是下载guangdong.json或guangzhou.json后,在HTML里全局注册:
$.getJSON('/static/map/guangzhou.json', function(json) { echarts.registerMap('guangzhou', json); initMapChart(); });建议用fetch或$.getJSON异步加载,加载完成后再初始化图表。如果你发现地图区域显示不出来,先确认GeoJSON文件是否成功加载,直接console.log打印出来看看。另外一个常见问题:地理坐标系geo和series-map都配置了map属性,可能重复绘制边界,配置时二者取其一即可。我更喜欢用geo配合visualMap,因为你还可以设置visualMap的颜色深浅来映射数据大小,效果很漂亮。
6.4 Flask端口占用、重启失败等现场问题
开发Flask应用时,默认端口5000,但在macOS或某些Linux发行版上,AirPlay或系统服务会占用5000端口。解决办法是启动时换端口:
python app.py runserver --host=0.0.0.0 --port=8080如果你用的是if __name__ == '__main__': app.run(debug=True, port=8080),可以在代码里直接指定。还有的人总是遇到修改了Python代码但浏览器刷新页面还是旧数据,这是因为Flask的模板渲染缓存。如果是开启debug模式,Flask自带热重载,但注意如果重载失败,可能是监控文件太多导致的。这时候手动重启一次程序就能解决,别在那儿一直刷新浏览器。
另一个现场的坑:爬虫脚本跑完后,前端同步数据按钮提示成功,但图表数据没变。大概率是接口返回了缓存,要么是浏览器缓存,要么是我上面提到的lru_cache。解决浏览器缓存可以在Ajax请求中加cache: false,或者在后端给API响应设置no-cache头。如果是lru_cache,确认你的爬虫更新数据库后是否调用了清除缓存的函数。这个细节演示时特别重要,不然老师点一下“同步数据”,页面没变化,场面非常尴尬。
7. 后续还能怎么扩展(加分项)
7.1 房价预测模型:线性回归或简单LSTM
毕设答辩加分项里,“机器学习预测”几乎是永恒的亮点。租房数据的核心因素是面积、区域、户型、楼层、朝向等,用线性回归预测租金是一个很好的开始。数据量不多的时候,sklearn.linear_model.LinearRegression就能跑,关键是特征工程。我建议把区域用one-hot编码,价格取对数,面积做标准化。验证方式用train_test_split,输出RMSE和R2,然后在前端增加一个“预测租金”的小工具,用户输入面积和区域,后端返回预测价格。
如果你还想更进一步,标题里提到了“大数据大模型”,那可以上一招LSTM预测时间序列房价走势。但说实话,租房时间序列数据通常不够长、不够规范,强上深度模型容易过拟合。我的建议是:毕设答辩时,用线性回归做基准模型,说明它可解释性强;如果时间充裕,再尝试用Prophet预测未来几周的房源量趋势,效果直观而且容易调。不要为了大模型而大模型,老师问起你为什么用LSTM,你得能说出优势和代价。
7.2 定时采集与增量更新
爬虫只跑一次很难体现平台价值。更完整的设计是用APScheduler给爬虫加定时任务,比如每天早上8点增量采集一次。增量采集的关键是判断哪些房源是新增的,这可以通过house_id去重实现,前面已经提到。定时任务不要直接塞进Flask的app里,建议单独写一个scheduler.py,用BackgroundScheduler启动,或者部署到服务器的cron里。如果你只是本地演示,用threading.Timer也行,但一重启就没了。
另一种方案是每夜用脚本运行爬虫,然后生成一份数据快照导出到CSV。这样即使数据库丢失,也有备份。我在毕设阶段就吃过亏,数据库被自己手贱删了,还好有CSV备份,重新导入后搞定。养成定期备份的习惯,真的很有用。
7.3 部署上线:Gunicorn + Nginx的组合方案
如果你想把项目部署到服务器,方便老师随时在线查看,那就需要正经部署Flask应用。开发环境里的app.run()只适合本机调试,生产环境用Gunicorn作为WSGI服务器,Nginx做反向代理。Nginx负责处理静态文件和请求转发,Gunicorn负责跑Python代码。配置大概是这样:
gunicorn -w 4 -b 127.0.0.1:5000 manage:app然后Nginx配置里把/路径代理到127.0.0.1:5000,具体配置不展开,网上很多。这里提醒一点:如果你用了密钥或数据库密码,不要把敏感信息硬编码在Python文件里,可以写到.env文件然后读取。部署到公网更是要注意基础安全,别开debug模式,数据库账号别用root。
我见过太多人把毕设部署到阿里云,结果几个小时就被入侵的案例。原因无他:端口全开、密码弱、debug开着。哪怕只是毕设,也要有最基本的网络安全意识。这里说个实际的:如果只用局域网演示,其实不需要公网服务器,直接flask run --host=0.0.0.0就能用手机访问。但注意别把电脑暴露在公共网络上,尤其是Windows防火墙关了的情况下,风险不小。
**最后分享一个个人习惯:**每次写完一个模块,我都会随手记录当前遇到的坑和解决办法,放在项目的README.md里。答辩前看一遍,很多细节自己都想起来了。比如“XPath的string()返回字符串类型,text()返回list”“Numeric字段要转float再jsonify”“Echarts在容器隐藏时初始化宽为0”……这些都是别人问不倒你的干货。如果你正在纠结这个题目,我建议直接按这个路子动手。不要怕代码写得不够优雅,能跑通闭环,能讲清楚数据流,你就已经领先80%的同组同学了。