毕设代码二手房数据实战:从零构建可扩展的爬取与分析系统
最近在帮学弟学妹看毕设发现好多同学都选了二手房数据分析这个方向。想法很好但一看到代码问题就来了数据爬着爬着就断了字段一会儿是“万/平”一会儿是“元/㎡”代码全写在一个文件里改个参数都得找半天。这别说答辩了自己看着都头疼。所以今天咱们就一起动手从零开始搭建一个结构清晰、稳定可靠、易于扩展的二手房数据爬取与分析系统。这套方案特别适合毕设代码规范逻辑完整答辩时老师一看就明白。1. 先想清楚我们到底要解决什么问题在做技术选型之前得先理清毕设项目的核心痛点数据源不稳定很多同学直接用requests硬怼某个房产网站一旦网站改版或封IP整个项目就瘫痪了。数据格式混乱价格单位不统一万 vs 元面积带中文平米 vs ㎡楼层描述五花八门高/中/低层具体数字导致后续分析根本无法进行。代码像“意大利面条”数据爬取、解析、清洗、存储的代码全部揉在一起想加个去重功能或者换数据源牵一发而动全身。缺乏工程化思维没有错误处理、没有日志、没有配置管理程序跑一次就扔无法复用和迭代。明确了问题我们才能选择合适的技术和架构。2. 技术选型为什么是 Scrapy SQLite对于新手来说选对工具能事半功倍。这里简单对比一下爬虫框架Scrapy vs RequestsBeautifulSoupRequestsBeautifulSoup优点是入门极其简单几行代码就能跑起来适合一次性、小批量的抓取任务。缺点是所有东西请求调度、异常处理、并发控制都要自己手写代码容易变得冗长且难以维护。Scrapy它是一个为爬虫而生的框架。虽然学习曲线稍陡但它提供了完整的爬虫生命周期管理。对于毕设这种需要体现工程能力、代码结构清晰、且可能面临反爬的项目Scrapy是更专业的选择。它内置了异步处理、中间件、管道等机制让我们的代码可以像搭积木一样模块化。数据存储SQLite vs CSV/ExcelCSV/Excel存数据快查看方便。但问题很多无法高效去重、查询复杂数据时需要全部读入内存、多线程写入可能损坏文件、没有数据类型约束。SQLite它是一个轻量级的关系型数据库一个文件就是一个数据库。优势明显结构化存储可以定义严格的字段类型TEXT, INTEGER, REAL保证数据质量。强大的查询能力用SQL可以轻松做数据筛选、聚合、统计为后续分析铺路。并发安全Scrapy多线程爬取时SQLite能很好地处理并发写入。易于集成Python内置sqlite3模块无需安装额外服务。结论对于毕设强烈推荐Scrapy SQLite组合。它能让你的项目代码瞬间提升一个档次从“脚本”升级为“系统”。3. 核心实现细节拆解下面我们分模块来构建这个系统。3.1 创建Scrapy项目与基础结构首先安装Scrapy并创建项目骨架这是良好组织的开端。pip install scrapy scrapy startproject house_spider cd house_spider scrapy genspider lianjia lianjia.com这个命令会生成一个标准的项目结构其中几个关键文件items.py: 定义我们要爬取的数据结构像定义一个数据模型。pipelines.py: 数据清洗和存储的逻辑写在这里。middlewares.py: 处理请求和响应的中间件比如换User-Agent、设置代理。spiders/lianjia.py: 爬虫的核心解析逻辑。3.2 定义清晰的数据模型items.py在items.py中我们先定义好期望的数据字段这相当于一份数据契约。import scrapy class HouseItem(scrapy.Item): # 定义字段像定义数据库表的列 title scrapy.Field() # 房源标题 total_price scrapy.Field() # 总价单位万元 unit_price scrapy.Field() # 单价单位元/平方米 area scrapy.Field() # 面积单位平方米 district scrapy.Field() # 行政区 street scrapy.Field() # 街道/板块 community scrapy.Field() # 小区名 floor scrapy.Field() # 楼层信息清洗后 orientation scrapy.Field() # 朝向 layout scrapy.Field() # 户型如3室2厅 listing_date scrapy.Field() # 挂牌日期 url scrapy.Field() # 详情页链接用于去重和追溯 crawl_time scrapy.Field() # 爬取时间戳3.3 编写健壮的爬虫解析逻辑spiders/lianjia.py这是最核心的部分重点在于选择器的健壮性和错误处理。import scrapy from house_spider.items import HouseItem from urllib.parse import urljoin import logging class LianjiaSpider(scrapy.Spider): name lianjia allowed_domains [lianjia.com] start_urls [https://bj.lianjia.com/ershoufang/pg1/] # 示例起始页 def parse(self, response): 解析列表页提取房源详情页链接并翻页。 # 1. 提取当前页所有房源链接 house_links response.xpath(//div[classinfo clear]/div[classtitle]/a/href).getall() for link in house_links: full_url urljoin(response.url, link) # 将详情页的请求交给 parse_house 方法处理 yield scrapy.Request(urlfull_url, callbackself.parse_house) # 2. 翻页逻辑查找“下一页”按钮 next_page response.xpath(//div[classpage-box house-lst-page-box]//a[classnext]/href).get() if next_page: next_page_url urljoin(response.url, next_page) yield scrapy.Request(urlnext_page_url, callbackself.parse) else: self.logger.info(已爬取到最后一页) def parse_house(self, response): 解析房源详情页提取具体信息。 关键使用多个XPath或CSS选择器提高容错率并做好异常处理。 item HouseItem() item[url] response.url try: # 标题 - 尝试多种选择器 title response.xpath(//h1[classmain]/text()).get() or \ response.css(h1.main::text).get() item[title] title.strip() if title else # 总价字符串如“500万” total_price_text response.xpath(//span[classtotal]/text()).get() # 清洗函数在pipeline里统一实现这里先存储原始文本 item[total_price] total_price_text # 单价字符串如“单价 65432元/平米” unit_price_text response.xpath(//span[classunitPriceValue]/text()).get() item[unit_price] unit_price_text # 面积字符串如“89.3平米” area_text response.xpath(//div[classarea]/div[classmainInfo]/text()).get() item[area] area_text # 其他字段类似提取... # 使用 get() 方法避免索引错误配合 or 提供默认值 item[district] response.xpath(//div[classareaName]/span[classinfo]/a[1]/text()).get(default) item[street] response.xpath(//div[classareaName]/span[classinfo]/a[2]/text()).get(default) item[community] response.xpath(//div[classcommunityName]/a[1]/text()).get(default) # 楼层信息原始字符串如“低楼层/共6层” floor_info response.xpath(//div[classroom]/div[classsubInfo]/text()).get() item[floor] floor_info item[orientation] response.xpath(//div[classtype]/div[classmainInfo]/text()).get(default) item[layout] response.xpath(//div[classroom]/div[classmainInfo]/text()).get(default) yield item # 将初步提取的item交给Pipeline处理 except Exception as e: # 记录解析错误的页面便于后续排查 self.logger.error(f解析页面 {response.url} 时出错: {e}) # 可以选择将错误URL存入文件后续重试3.4 数据清洗与标准化管道pipelines.py原始数据是“脏”的必须在存储前清洗。这是体现数据质量的关键步骤。import sqlite3 from datetime import datetime import re class HouseSpiderPipeline: def __init__(self): # 初始化时创建数据库连接和表 self.conn sqlite3.connect(houses.db) self.cursor self.conn.cursor() self.create_table() def create_table(self): 创建数据表定义字段类型 self.cursor.execute( CREATE TABLE IF NOT EXISTS houses ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, total_price REAL, unit_price INTEGER, area REAL, district TEXT, street TEXT, community TEXT, floor TEXT, orientation TEXT, layout TEXT, listing_date TEXT, url TEXT UNIQUE, -- 唯一约束用于去重 crawl_time TIMESTAMP ) ) self.conn.commit() def process_item(self, item, spider): 对item进行清洗和转换然后存入数据库。 # 1. 清洗总价将“500万”转换为 500.0 (float) item[total_price] self._clean_price(item.get(total_price, )) # 2. 清洗单价将“65432元/平米”转换为 65432 (int) item[unit_price] self._clean_unit_price(item.get(unit_price, )) # 3. 清洗面积将“89.3平米”转换为 89.3 (float) item[area] self._clean_area(item.get(area, )) # 4. 清洗楼层简化描述例如从“低楼层/共6层”提取为“低楼层” item[floor] self._clean_floor(item.get(floor, )) # 5. 添加爬取时间戳 item[crawl_time] datetime.now().strftime(%Y-%m-%d %H:%M:%S) # 6. 存入数据库 (使用 INSERT OR IGNORE 实现基于URL的去重) self.cursor.execute( INSERT OR IGNORE INTO houses (title, total_price, unit_price, area, district, street, community, floor, orientation, layout, listing_date, url, crawl_time) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) , ( item.get(title), item.get(total_price), item.get(unit_price), item.get(area), item.get(district), item.get(street), item.get(community), item.get(floor), item.get(orientation), item.get(layout), item.get(listing_date), item.get(url), item.get(crawl_time) )) self.conn.commit() return item # 以下是具体的清洗函数 def _clean_price(self, price_str): 清洗总价字符串 if not price_str: return None # 匹配数字包括小数并处理“万”单位 match re.search(r([\d\.]), price_str) if match: number float(match.group(1)) if 万 in price_str: return number elif 亿 in price_str: # 极端情况处理 return number * 10000 return None def _clean_unit_price(self, unit_price_str): 清洗单价字符串 if not unit_price_str: return None match re.search(r([\d,]), unit_price_str.replace(,, )) return int(match.group(1)) if match else None def _clean_area(self, area_str): 清洗面积字符串 if not area_str: return None match re.search(r([\d\.]), area_str) return float(match.group(1)) if match else None def _clean_floor(self, floor_str): 清洗楼层字符串提取关键信息 if not floor_str: return # 示例提取“低楼层”或“中楼层/高楼层”中的第一部分 parts floor_str.split(/) return parts[0].strip() if parts else floor_str def close_spider(self, spider): 爬虫关闭时关闭数据库连接 self.conn.close()3.5 配置反爬与请求控制settings.py middlewares.py在settings.py中开启必要的组件并配置参数# settings.py BOT_NAME house_spider SPIDER_MODULES [house_spider.spiders] NEWSPIDER_MODULE house_spider.spiders # 遵守robots协议对于毕设建议遵守 ROBOTSTXT_OBEY True # 配置下载延迟避免请求过快非常重要 DOWNLOAD_DELAY 2 # 每次请求间隔2秒 # 启用我们编写的Pipeline和Middleware ITEM_PIPELINES { house_spider.pipelines.HouseSpiderPipeline: 300, } DOWNLOADER_MIDDLEWARES { house_spider.middlewares.RandomUserAgentMiddleware: 543, # 自定义UserAgent中间件 } # 设置一个常见的User-Agent列表 USER_AGENT_LIST [ Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ..., Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 ..., # ... 可以多准备几个 ]然后在middlewares.py中实现随机User-Agent的逻辑# middlewares.py import random from house_spider.settings import USER_AGENT_LIST class RandomUserAgentMiddleware: 随机更换User-Agent的中间件 def process_request(self, request, spider): ua random.choice(USER_AGENT_LIST) if ua: request.headers[User-Agent] ua return None4. 性能与安全性考量请求频率控制DOWNLOAD_DELAY是最基本的控制。更高级的可以用AutoThrottle扩展让Scrapy自动调整速度。本地存储防注入我们使用SQLite的参数化查询?占位符而不是字符串拼接这从根本上防止了SQL注入。避免个人信息泄露我们的爬虫只抓取公开的房源信息不涉及任何个人电话、身份证等隐私数据。这是法律和道德的底线。5. 生产环境避坑指南来自实战的经验网站结构变更应对这是爬虫最大的敌人。解决方法使用更通用的选择器尽量用class或id少用绝对路径。多用get()和getall()配合默认值。将关键XPath/CSS选择器提取到配置文件中一旦网站改版只需修改配置文件无需动核心代码。编写监控脚本定期运行爬虫检查抓取数量是否骤降及时报警。数据去重我们的方案是在数据库层面为url字段设置UNIQUE约束利用INSERT OR IGNORE语句实现。这是最简单有效的方法。冷启动调试技巧先用scrapy shell url命令在交互环境里测试你的选择器是否正确。将DOWNLOAD_DELAY调大比如5秒慢慢爬几页观察日志和数据库确保一切正常再全速运行。在Pipeline的清洗函数中多打日志确保每个字段的转换都符合预期。6. 如何运行与扩展运行爬虫非常简单cd house_spider scrapy crawl lianjia数据爬取完成后你的houses.db数据库里就有了干净、结构化的数据。这时你的毕设才刚刚开始变得有趣数据分析直接用SQL查询比如“计算各区的平均单价”、“找出性价比最高单价低、面积大的房源”。数据可视化用pandas读取SQLite数据配合matplotlib或pyecharts画图生成房价分布图、趋势图等这是毕设答辩的亮点。价格预测模型以单价或总价为标签以面积、楼层、区位等为特征尝试用scikit-learn跑一个简单的线性回归或决策树模型哪怕结果不精确这个过程也极具学习价值。写在最后这套代码不仅仅是为了完成一次数据抓取。它展示了一个完整的、可维护的数据流水线从目标定义Item、任务调度Spider、数据清洗Pipeline到持久化存储SQLite。每一层职责清晰耦合度低。建议你不要直接复制粘贴。最好的学习方式是用这个框架换一个你熟悉的城市或房产网站注意robots.txt重新实现一遍。尝试添加新的功能比如用Redis实现分布式去重或者将数据推送到MySQL。基于爬取的数据真的去做一些分析和可视化把整个故事讲完整。当你把这一套流程走通并能在答辩中清晰地阐述每个模块的设计意图和实现细节时你的毕设就已经成功了。这不仅是一份代码更是一份解决问题的工程思维。动手试试吧