基于Django的爬虫与大数据二手房信息采集系统实战解析

📅 发布时间:2026/10/21 2:50:02
基于Django的爬虫与大数据二手房信息采集系统实战解析
做毕设最怕什么不是不会写代码而是代码写完了老师问你“你这个系统架构是怎么设计的”“数据怎么保证不重复”“可视化图表背后的接口是怎么组织的”你支支吾吾答不上来。今天聊的这个项目——基于Django的爬虫与大数据二手房信息采集系统正好是这类问题的典型样本。你光看标题可能会觉得这不就是个爬虫吗其实拆开来看它是一条完整的“采集-清洗-存储-分析-展示”数据流水线涉及的技术点很杂但都不深非常适合作为毕设选题也适合想系统练一遍Web开发数据采集全流程的同学参考。这个项目能解决的问题很明确把分散在房产信息平台上的二手房屋数据通过爬虫批量抓下来经过清洗和结构化处理后存入数据库再通过Django后端提供查询接口最后用可视化图表把价格分布、区域对比、户型占比等信息展示出来。适合谁适合那些已经学过Python基础、想做一个“有数据、有界面、有分析”的完整系统的同学也适合想转型做数据采集或数据可视化方向的人用来练手。下面我按实际开发顺序把这个项目的关键设计、实现细节、踩坑记录和答辩要点完整拆给你。1. 先搞清楚这个系统到底要做什么1.1 标题拆解三个关键词背后的真实需求我拿到这类题目第一件事不是打开IDE写代码而是把标题里的关键词一个个拆开搞清楚每个词对应什么工作量。先看“Django”。这意味着整个系统要有一个Web后端要能处理请求、管理数据、渲染页面或提供API。Django自带ORM、Admin后台、模板引擎做这类管理系统非常合适。很多同学会把Django当成一个简单的模板渲染工具但如果你的系统里既有数据采集又有数据展示那Django的角色应该是“业务逻辑中枢”——接收爬虫写入的数据对外提供查询和统计接口同时承载前端页面。再看“网络爬虫”。这是整个系统的数据来源也是很多人最感兴趣的部分。爬虫不是简单地拿requests请求一个URL然后把HTML存下来就完了你需要考虑解析策略、字段提取、请求频率控制、异常重试、断点续爬。房源数据有特殊性列表页和详情页信息不对等列表页能拿到标题、总价、区域这些基础字段但朝向、楼层、建筑年代这类信息通常在详情页里。所以爬虫模块要设计两级页面抓取先爬列表页拿到URL列表再进详情页补齐字段。最后是“大数据技术”。很多同学一听到“大数据”就发怵觉得是不是得上Hadoop、Spark。其实毕设层面的大数据指的是对海量数据的采集、清洗、分析和可视化能力。你这套房源数据可能就几万条用不着分布式计算但你可以在架构中引入Redis做缓存用Pandas做数据清洗和统计分析用ECharts做可视化再配合定时任务实现数据自动更新——这些技术组合在一起就已经是完整的大数据应用链路了。1.2 这个选题最常见的误区把“爬虫”当全部我见过不少同学做类似题目花了两周时间写爬虫把房源数据爬下来存进Excel然后开始写论文最后发现系统根本没有“系统”的样子。这不是一个爬虫脚本而是一个采集系统。从用户视角看它需要有查询界面从数据视角看它需要有稳定的存储结构从运维视角看它需要有定时更新机制从论文视角看它需要有分析结论。说得直白一点爬虫只是这个系统的输入环节你花在Django页面、数据库设计、可视化图表上的功夫某种程度上比爬虫本身更重要。答辩的时候老师不会只看你能不能抓到数据而是会问你的数据是怎么清洗的你的系统是怎么设计的你的分析结论是什么如果你的系统就是爬虫加一张表格这几个问题一个都接不住。2. 技术架构与整体设计思路2.1 系统的三层结构采集层、服务层、展示层我在设计这个项目的时候把整个系统分成了三层每一层职责单一层与层之间通过明确的接口交互。这样做的最大好处是你论文里的架构图好画代码也好维护哪一层出问题直接去对应目录排查就行。第一层是采集层由爬虫模块和定时任务组成。爬虫负责抓取列表页和详情页数据把原始数据转换成统一的字典结构写入待处理队列。定时任务用APScheduler或系统crontab实现每天固定时间触发一次增量采集避免数据过时。第二层是服务层也就是Django应用本体。它接收采集层写入的房源数据执行去重、清洗、字段补全等操作然后持久化到MySQL数据库。对外服务层提供两类接口一类是页面渲染接口返回HTML页面另一类是数据API接口返回JSON数据供前端图表调用。第三层是展示层基于Bootstrap和ECharts实现。页面包括房源列表页支持筛选和分页、区域价格分析页地图或柱状图、户型分布页饼图、价格趋势页折线图。安客居房采集系统 ├── crawler/ # 采集层 │ ├── spiders/ # 爬虫核心 │ ├── cleaners/ # 数据清洗 │ └── tasks.py # 定时任务 ├── houseweb/ # 服务层 │ ├── models.py # ORM模型 │ ├── views/ # 视图函数 │ ├── apis/ # JSON接口 │ └── urls.py ├── templates/ # 展示层 ├── static/ # 静态资源 └── data_analysis/ # 大数据分析脚本2.2 技术选型的几点思考和取舍技术选型是毕设答辩时老师必问的内容你要能说出“为什么选它”而不是“大家都在用”。Django和Flask怎么选Flask轻量灵活适合做小接口但既然题目明确要求Django就用Django。退一步讲Django的ORM、Admin、认证体系都是现成的做后台管理系统能省下大量重复造轮子的时间。Admin后台还能直接可视化查看采集到的房源数据演示的时候很加分。数据存MySQL还是MongoDB房源数据结构相对规整字段基本固定用MySQL完全没问题而且MySQL在毕设环境里最通用、最不容易被质疑。MongoDB适合字段经常变化的场景但房源字段是稳定的所以选MySQL。为了提高查询性能我对热门小区字段加了索引并用Redis缓存区域统计结果避免每次打开页面都去数据库跑一遍聚合查询。大数据组件用哪些我实际用的是一套轻量组合Pandas负责数据分析Redis做缓存和简单队列ECharts做可视化输出。如果你论文里需要体现“大数据技术”可以再加一个Hadoop生态的轻量替代比如用ClickHouse做OLAP查询。但要注意一个基本原则没有真实数据规模支撑的技术栈都是空中楼阁。几万条数据根本不需要上Hadoop你上了反而会被老师追问集群怎么搭的、数据量多大、跑一个查询要多久。用Pandas加MySQL的组合能自圆其说就够了。3. 核心模块设计与实现细节3.1 房源爬虫模块两级页面抓取与字段映射爬虫模块是这个系统最先要解决的问题。房源数据的字段主要分两类列表页基础字段和详情页补充字段。我列了一份字段映射表设计数据库字段的时候直接对照着来保证每个字段都有来源。字段名来源页面类型说明title列表页varchar房源标题community列表页varchar小区名称district列表页varchar所属区域layout列表页varchar户型如3室2厅area列表页float建筑面积单位㎡total_price列表页float总价单位万元unit_price详情页float单价单位元/㎡toward详情页varchar朝向floor_level详情页varchar楼层描述listing_time详情页date挂牌时间source_url详情页varchar房源原始链接唯一标识列表页解析相对简单只需要提取每个房源卡片中的标题、总价、面积、区域等字段然后取出详情页链接放进待爬队列。详情页解析要复杂一些因为页面结构里字段名不是统一的比如“房屋朝向”这行文字可能直接是“朝南”也可能带一个图标标签。我的做法是先用BeautifulSoup定位到包含“朝向”字样的父节点再通过兄弟节点提取值比直接写死CSS选择器更稳。请求控制上我在爬虫里加了两个核心参数请求间隔和失败重试次数。请求间隔设的是2到4秒随机值避免请求频率过高触发对方接口的风控机制。失败重试次数设的是3次连续失败就跳过这条记录并写入日志不影响整体采集进度。采集过程中还做了断点续爬每次启动前从数据库里读取已有房源URL集合爬虫遇到重复URL直接跳过这样定时任务每天执行的时候只需要增量抓取新增房源压力小很多。3.2 数据清洗模块脏数据在这里被干掉爬虫抓下来的数据是不能直接入库的原因很简单页面上的数据五花八门有“暂无数据”的占位符有“价格面议”的特殊值有带“万”“㎡”这些单位的字符串还有同一个小区在不同记录里名称大小写不一致的情况。如果直接入库后续做分析的时候所有统计都是错的。清洗规则我整理了一下核心就四条。第一空值处理总价、面积、户型三个关键字段为空的记录直接剔除因为这些字段做分析时缺一不可。第二单位统一总价字段去掉“万”字面积字段去掉“㎡”单价字段不加单位全部转换成浮点数。第三文本规范化朝向字段里的“南北”和“南 北”统一去除空格楼层描述统一按“低楼层/中楼层/高楼层”三档归类。第四去重以source_url为唯一键做去重同一套房源多次采集只保留最新一条。这些清洗逻辑我用Pandas实现写成独立的清洗函数。好处是清洗和采集解耦爬虫只管抓数据清洗脚本可以随时调整规则重新跑一遍不影响原始数据。实际项目里清洗这一步消耗的时间不比写爬虫少但很多同学会忽略我建议在论文里单独用一节来描述清洗规则答辩时能体现你的工程思维。3.3 Django后端与数据可视化从数据到图表的最后一公里数据入MySQL之后Django的工作就是把这些数据“用起来”。我在设计阶段定了两个原则页面和数据接口分离接口只返回JSON具体图表渲染交给前端ECharts去完成。这样前端页面只关心图表怎么画后端只关心数据怎么查改图表样式不用动后端代码加统计维度也不用动前端页面。可视化这一块我选了四类图表来回答四个问题区域均价用柱状图对比不同城区的价格水平总价分布用直方图观察房源总价集中在哪个区间户型占比用饼图看看市场主力户型是什么价格趋势用折线图展示近几个月挂牌价的走势。这四张图组合起来基本就能覆盖老师对“大数据分析”的期待。图表背后的数据全部由Django接口实时查询数据库生成而不是提前算好写死在页面上这样演示的时候可以动态筛选条件效果比静态图好得多。4. 实操过程与核心代码实现4.1 环境准备与初始化开发环境是Python 3.9Django 3.2MySQL 5.7Redis 6.0全部在虚拟环境里跑避免多个项目互相干扰依赖版本。新建一个名为house_env的虚拟环境然后安装依赖包。依赖清单里除了Django还有requests、BeautifulSoup4、pandas、django-redis、apscheduler、mysqlclient这几个核心包。python -m venv house_env source house_env/bin/activate # Windows: house_env\Scripts\activate pip install django3.2 requests beautifulsoup4 pandas django-redis apscheduler mysqlclient django-admin startproject house_project cd house_project python manage.py startapp houseapp python manage.py startapp crawler_app数据库这步有个坑新建数据库时一定要指定utf8mb4字符集否则后面存中文会乱码。建库命令是CREATE DATABASE house_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;创建完数据库我在Django的settings.py里配置好MySQL连接信息然后执行迁移命令生成初始表。开发阶段我建议打开Django的DEBUG模式这样报错时能看到完整的调用栈排查问题效率高很多。部署到正式环境再关掉DEBUG也不迟。4.2 房源数据模型设计数据模型这块我建了两个核心表HouseInfo存房源基础信息CrawlLog存每次采集任务的执行日志。HouseInfo表的设计我在前面的字段映射表里已经列出了这里补充说明几个设计要点。source_url字段标了uniqueTrue商品平台上同一套房的链接是唯一的拿它做唯一索引天然合适。crawl_time字段记录采集时间每次入库时自动写入当前时间用于后面做增量判断。status字段用来标记房源状态1代表正常展示0代表已下架或失效。这个字段在做趋势分析时会用到——某段时间房源数量下降可能是真实市场变化也可能是大量房源下架导致的。Django模型的写法如下from django.db import models class HouseInfo(models.Model): source_url models.CharField(max_length512, uniqueTrue, verbose_name房源链接) title models.CharField(max_length255, verbose_name标题) community models.CharField(max_length128, db_indexTrue, verbose_name小区) district models.CharField(max_length64, db_indexTrue, verbose_name区域) layout models.CharField(max_length32, verbose_name户型) area models.FloatField(verbose_name面积(㎡)) total_price models.FloatField(verbose_name总价(万)) unit_price models.FloatField(verbose_name单价(元/㎡)) toward models.CharField(max_length16, verbose_name朝向) floor_level models.CharField(max_length16, verbose_name楼层) listing_time models.DateField(nullTrue, blankTrue, verbose_name挂牌时间) status models.IntegerField(default1, verbose_name状态) crawl_time models.DateTimeField(auto_now_addTrue, verbose_name采集时间) class Meta: db_table house_info verbose_name 房源信息字段设计有几个细节值得注意面积和总价直接存储浮点数不给二维表格里的合计字段留位置所有统计都通过SQL的聚合函数动态计算保证数据一致性。小区和区域两个字段加了索引因为后面所有的区域对比、小区筛选都会用这两个字段做WHERE条件。4.3 爬虫核心逻辑列表页解析与详情页补全爬虫部分我用了requests加BeautifulSoup没用Scrapy。原因有两点一是这个项目的数据量在毕设层面不算大多线程加请求间隔就能在一个小时内完成一轮全量采集不需要Scrapy这种重型框架来调度二是requests写的代码逻辑非常直观答辩的时候讲起来方便每行代码都能讲清楚在干什么。列表页的解析逻辑核心是提取每个房源卡片的信息和详情页链接。我写了一个fetch_list_page函数输入页面URL输出房源基础字段的列表import requests import json import time import random from bs4 import BeautifulSoup HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36, Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.9,en;q0.8, } def fetch_list_page(page_url): resp requests.get(page_url, headersHEADERS, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) items [] for card in soup.select(.house-lst .list-item): try: title_el card.select_one(.title a) item { title: title_el.get_text(stripTrue), url: https: title_el.get(href), total_price: float(card.select_one(.total-price span).get_text(stripTrue)), community: card.select_one(.community).get_text(stripTrue), district: card.select_one(.region).get_text(stripTrue), layout: card.select_one(.layout).get_text(stripTrue), area: float(card.select_one(.area).get_text(stripTrue).replace(㎡, )) } items.append(item) except AttributeError: continue return items这段代码有两个我实测踩过的坑。第一个链接是这个平台的反向代理地址直接解析出来的href是相对路径必须以“https:”开头拼成完整URL否则requests会报无host异常。第二个页面结构里总价外面嵌套了一层span标签选择器要匹配到内层span才能取到纯数字如果直接取外层会带冗余文本导致float转换报错。列表页获取到房源基础数据后我启动了一个多线程任务池来抓详情页。详情页的核心提取逻辑用了正则模式匹配因为详情页参数是动态渲染的有些部分的字段数据直接内嵌在JavaScript变量里def fetch_detail_page(url): resp requests.get(url, headersHEADERS, timeout10) resp.encoding utf-8 soup BeautifulSoup(resp.text, html.parser) detail {} for label in [房屋朝向, 楼层, 挂牌时间]: try: label_el soup.find(span, stringlabel) value_el label_el.find_next(span) key_map {房屋朝向: toward, 楼层: floor_level, 挂牌时间: listing_time} detail[key_map[label]] value_el.get_text(stripTrue) except AttributeError: continue return detail这里有个通用技巧不要尝试用“第几行的第几个元素”来定位而是用字段名称文字来定位。页面改版时往往只改样式不改文案这个方法能最大程度抵抗页面结构变化的影响。另外提醒一下所有爬虫脚本都必须遵守目标网站的抓取规则并且控制合理频率。在课程设计与研究场景下页面数据仅作学习验证使用练习时优先采用模拟数据环境。4.4 数据入库与Django接口实现采集到的数据经过清洗后统一通过Django的ORM写入数据库。写入库逻辑时我做了三件事先按source_url判断记录是否存在存在就更新字段不存在就新建然后对关键字段再校验一遍避免空值入库最后把这条URL加入本轮采集集合用于后续进度追踪。from houseapp.models import HouseInfo def save_house_data(house_list): for item in house_list: HouseInfo.objects.update_or_create( source_urlitem[source_url], defaults{ title: item[title], community: item[community], district: item[district], layout: item[layout], area: item[area], total_price: item[total_price], unit_price: item.get(unit_price, 0), toward: item.get(toward, ), floor_level: item.get(floor_level, ), listing_time: item.get(listing_time) or None, } )Django接口这部分我给前端页面提供了三个API房源列表接口返回分页数据支持区域、户型、价格区间筛选统计接口返回区域均价、户型占比等聚合数据趋势接口返回近6个月挂牌量变化。每个接口都返回JSON格式前端用fetch请求后直接喂给ECharts。统计接口的SQL逻辑是典型的聚合查询Django的ORM表达为from django.db.models import Avg, Count def district_stats(request): stats HouseInfo.objects.filter(status1) \ .values(district) \ .annotate(avg_priceAvg(unit_price), house_countCount(id)) \ .order_by(-avg_price) return JsonResponse({data: list(stats)})这个接口的逻辑很直观但有一个性能细节需要注意如果房源表数据量大每次请求都要全表聚合会很慢。我的优化策略是先用Redis缓存聚合结果缓存时间为5分钟第一次请求时从数据库查出结果写入Redis后续请求直接读缓存。5分钟是权衡的结果——用户看到的数据不至于太旧同时也给数据库减少了大量重复计算。前端图表调用接口的示例fetch(/api/district/stats/) .then(response response.json()) .then(res { const chart echarts.init(document.getElementById(districtChart)); chart.setOption({ xAxis: { data: res.data.map(i i.district) }, yAxis: { type: value, name: 均价(元/㎡) }, series: [{ type: bar, data: res.data.map(i i.avg_price) }] }); });ECharts初始化前一定要确认DOM元素已经渲染完成不然图表容器宽度计算为0出来一张空图。比较简单可靠的方案是在window.onload事件里执行图表初始化或者用setTimeout延迟100毫秒。5. 常见问题排查与避坑心得5.1 采集环节三个最容易翻车的点爬虫跑起来容易稳定运行难。我在整个开发过程里踩了无数坑挑三个最有代表性的说。第一个是请求头缺失导致的页面反爬拦截。最开始写爬虫时只设置了User-Agent没设置Accept和Accept-Language页面很快返回了验证页面。解决方案是补全请求头信息模拟浏览器的完整请求特征。这个问题的排查思路是打印响应状态码和返回内容的前500个字符如果返回内容明显不是你需要的HTML结构基本就是被拦了。第二个是页面结构改版导致的选择器失效。头天晚上跑得好好的第二天早上启动爬虫所有字段都解析不到数据。排查发现页面元素的class名字变了。解决办法是前面提到的字段文字定位法把对CSS类名的依赖降到最低。另外还加了一个自动告警机制如果连续50个页面解析结果为空脚本终止运行并发送日志通知。第三个是抓取频率过高导致IP临时被限制。某次调参时我把请求间隔改成了0.5秒跑了10分钟之后所有请求都超时。这个问题没有特别好的办法只能严格控制抓取频率并且把失败请求的日志记录完整等限制解除后重新抓取失败记录。建议用指数退避策略连续失败时把等待时间逐步拉长而不是执行固定的重试间隔。5.2 数据与展示问题乱码、重复和无数据数据处理阶段的坑比爬虫还要多因为很多东西不是一眼能看出来的。乱码问题在数据入库后最容易暴露。正常情况下页面编码是utf-8但偶尔有房源页返回的是gbk编码解析出来全是乱码。我给请求函数加了一个编码自适应逻辑先用requests的apparent_encoding属性检测实际编码再决定用哪种编码解码。重复数据其实是定时采集最容易出现的问题。方案前面已经说了用update_or_create方法以source_url为唯一键做更新或创建。这个方案跑了一个月重复率稳定在0。需要提醒的是MySQL对URL字段做唯一索引时如果URL长度超过255字符会报错我实际测试中最长的URL有180字符所以字段长度至少要到512才安全。页面显示无数据通常是Django模板搞错了变量名或者接口返回的JSON字段名和前端JS里引用的字段名不一致。排查方法是打开浏览器开发者工具先看Network里接口返回的原始数据再对比前端代码里取的字段名是否一致。我发现这个问题的效率比调试Django日志高得多。5.3 答辩现场高频问题清单做这个项目的过程中我把老师可能会问到的问题提前整理了一份清单一边做一边准备答案。下面这些是最高频的几个为什么选择Django而不是Flask答题目要求加上Django的ORM和Admin后台能提高开发效率适合业务逻辑管理类系统。爬虫遇到反爬怎么处理答合理设置请求头、控制访问频率、随机化User-Agent更重要的是遵循平台规则在合规前提下用模拟数据做课程设计验证。数据清洗的规则是什么答空值剔除、单位统一、文本规范化、URL去重四类规则并用Pandas实现。大数据技术体现在哪里答定时增量采集、Redis缓存、Pandas统计分析、ECharts多维可视化。系统的扩展性如何答采集层、服务层、展示层三层分离新增数据源只需要写一个新的爬虫模块新增分析维度只需要扩展API和图表。这些问题你现在看着觉得简单但如果不提前准备现场很容易卡壳。我的建议是做完项目后自己对着上面的问题口头讲一遍讲不顺的地方回头看代码。6. 远程调试、项目讲解与二次开发方向6.1 远程调试怎么聊才高效很多人都遇到过这种情况项目在别人电脑上跑不起来让你远程帮忙看一聊就是两小时最后发现只是依赖没装全。远程调试的关键不是“快速给答案”而是“规范排查流程”。我自己调试过多个类似项目之后总结出一套固定的排查顺序先看Python版本是否匹配再看pip freeze的依赖清单是否完整然后看MySQL和Redis服务是否启动最后才看业务代码。这套顺序能解决80%的“跑不起来”问题。你在帮别人调项目时不要直接动手改代码而是让对方把报错信息完整截图发过来。很多新手只发一行“报错了”实际上Django的报错信息里带着完整的堆栈追踪从下往上看最后一行通常是根因。找对了根因修复往往是一行代码的事。6.2 给没写过代码的人讲清楚这个项目不管是毕设答辩还是项目展示你一定会面对“没写过代码”的听众。这时候不要讲技术细节要讲“这个故事”。我的讲法是先展示系统界面让听众直观感受这个系统能干什么再说数据从哪里来讲解采集过程然后展示数据库里几万条真实房源数据说明数据规模紧接着用图表页面展示分析结论比如“某区域均价最高”“两居室是市场主流户型”最后总结系统价值能帮用户快速了解市场行情、辅助置业决策。这套讲解逻辑从“现象”到“数据”再到“结论”普通听众完全跟得上。比上来就讲“我用Scrapy框架写了爬虫中间件”要好一百倍。6.3 这个项目后续还能往哪儿扩展如果这个项目要作为毕业设计做到当前程度已经足够了。但如果想继续完善有两条路值得走。第一条是往“实时化”方向扩展接入消息队列让新发布的房源在几分钟内自动入库并在前端页面上线第二条是往“智能化”方向扩展用机器学习对房源价格做预测给用户提供“这个价格是否合理”的参考提示。这两条路都能单独作为研究课题不过那就是另一个项目了。我个人在实际操作中的体会是一个能完整跑通的系统比十个半成品有价值得多。你现在看到的这些设计思路和代码细节全部都是我在实际开发中一步步走出来的。很多方案看着简单但你没踩过坑就不知道为什么要这么设计。做这类系统的过程里最有收获的不是学会requests怎么用、ECharts怎么画图而是学会怎么把一个大目标拆成一个个可以验证的小步骤再一步步验证推进。把这个能力练好了以后不管做什么系统你都能游刃有余。