链家二手房爬虫与数据分析:Scrapy+pandas毕业设计实战
简介面向计算机相关专业毕业设计、课程设计及Python爬虫与数据分析实战学习者这是一份完整的链家房屋数据爬虫与分析项目。从爬虫框架搭建、Scrapy配置到MySQL数据存储与可视化覆盖数据采集、清洗、入库、分析全流程适合需要可直接运行的高分项目作为参照的初学者。压缩包共31个文件包含7个Python脚本、6张结果图表、2个SQL建表与查询脚本、1个Excel数据结果、1个ipynb交互式分析笔记本及说明文档等整体仅1.43MB轻量完整。项目配有README与启动脚本目录结构清晰便于对照学习和二次修改。已有96人学习使用代码完整确保可运行评审得分99分可作为大作业或期末项目的高质量起步模板。1. 链家二手房爬虫与数据分析一个能直接跑完的毕业设计模板接手这份毕业设计源码时我最先打开的不是main.ipynb而是house.xlsx里的数据形态。因为爬虫只能解决「有没有」而数据分析质量取决于前期数据规整是否到位。这套项目实际上给你搭好了一条完整链路Scrapy 爬取链家二手房列表页把标题、小区、户型、面积、单价等字段落进 Excel 和 SQLite再用 pandas 和 matplotlib 在 Jupyter 里完成价格分布、区域对比、面积与总价关系等分析。对于正在做 Python 课程设计、期末大作业的同学来说它是一份可以直接复现的完整源码包也是理解「爬虫 数据分析」两个方向如何衔接的样板。项目评审分 99 分从代码结构和数据文件组织上看确实覆盖了从采集到展示的每一个阶段。2. 爬虫层Scrapy 与 requests 的选型以及链家页面怎么解析2.1 为什么用 Scrapy 而不是 requests 裸循环很多教程习惯用requests.get加 BeautifulSoup 写一个for循环翻页抓取。这种写法在几十条数据时没什么问题一旦需要连续抓取上千条房源就要自己处理请求重试、异常捕获、去重和并发控制。Scrapy 把这些能力内置为框架特性你只需要定义Item、Spider和Pipeline就能获得稳定可扩展的抓取流程。链家二手房页面的 DOM 结构非常规整每个房源信息都放在div.info中字段位置固定非常适合用 CSS 选择器提取。一个典型的items.py定义如下# items.py import scrapy class LianjiaItem(scrapy.Item): title scrapy.Field() # 房源标题 community scrapy.Field() # 小区名称 layout scrapy.Field() # 户型如 3室1厅 area scrapy.Field() # 面积原文本为 89.5平米 direction scrapy.Field() # 朝向如 南、南北 decoration scrapy.Field() # 装修情况 floor scrapy.Field() # 楼层位置 year scrapy.Field() # 建造年份 unit_price scrapy.Field() # 单价原文类似 52,746元/平 total_price scrapy.Field() # 总价单位万 district scrapy.Field() # 所属区这里将「小区」和「所属区」拆开定义因为链家列表页的positionInfo中通常包含两个链接第一个是小区名第二个是行政区。字段拆得越细后面做区域分组和户型过滤就越方便这也是这个项目在数据分析阶段能直接出图的原因。2.2 列表页 Spider选择器、正则与翻页链家二手房列表页的 URL 格式是https://bj.lianjia.com/ershoufang/pg{n}/pg后面的数字是页码。第一页可以写成pg1/但实际第一页的 URL 没有pg参数所以从第二页开始翻页更稳妥。下面这段爬虫代码直接从div.info层级解析# spiders/lianjia_spider.py import scrapy from lianjia.items import LianjiaItem class LianjiaSpider(scrapy.Spider): name lianjia start_urls [https://bj.lianjia.com/ershoufang/pg1/] def parse(self, response): for div in response.css(div.info): item LianjiaItem() item[title] div.css(div.title a::text).get() comm div.css(div.flood .positionInfo a::text).getall() item[community] comm[0] if comm else None item[district] comm[-1] if len(comm) 1 else comm[0] house_info div.css(div.address .houseInfo::text).get() item[layout] house_info.split(|)[1].strip() if house_info else None item[area] house_info.split(|)[2].replace(平米, ).strip() if house_info else None item[direction] house_info.split(|)[3].strip() if house_info else None item[floor] house_info.split(|)[5].strip() if house_info else None item[year] house_info.split(|)[6].replace(年, ).strip() if house_info else None item[total_price] div.css(div.totalPrice span::text).get() item[unit_price] div.css(div.unitPrice span::text).get() yield item # 翻页找到下一页链接 next_page response.css(div.page-box a[classnext]::attr(href)).get() if next_page: yield scrapy.Request(response.urljoin(next_page), callbackself.parse)这段代码的核心是house_info.split(|)。链家列表页的houseInfo文本格式类似3室1厅 | 89.5平米 | 南 北 | 高楼层/共6层 | 精装 | 2005年建按竖线切分后各个字段的位置是固定的。相比正则匹配split(|)在字段缺失时更容易通过if判断来防御。response.urljoin可以把相对路径的下一页链接拼成完整 URL避免因站点使用相对地址导致翻页失败。2.3 反爬缓和UA 伪装与下载延时链家对异常请求的识别很敏锐默认的Scrapy/2.xUA 很容易被拒。我在settings.py里做两件事固定一个常见浏览器的 UA并控制请求频率。参考配置如下# settings.py BOT_NAME lianjia SPIDER_MODULES [lianjia.spiders] NEWSPIDER_MODULE lianjia.spiders USER_AGENT Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 DEFAULT_REQUEST_HEADERS { Accept: text/html,application/xhtmlxml,application/xml;q0.9,*/*;q0.8, Accept-Language: zh-CN,zh;q0.8, Referer: https://www.lianjia.com/, } DOWNLOAD_DELAY 0.5 CONCURRENT_REQUESTS 8DOWNLOAD_DELAY是每个请求之间的最小等待秒数0.5 秒表示每秒最多 2 个请求。CONCURRENT_REQUESTS会同时发送 8 个请求如果服务器响应变慢应该优先把并发数降到 4 或 2而不是继续降低延时。链家的验证码通常出现在连续快速请求之后所以毕业设计阶段不建议把并发调得更高。如果你发现爬到一半出现滑块验证重置 UA 往往没有用最直接的办法是拉长延时并重新从上次失败的位置继续。3. 数据持久化从 data.xls 到 house.xlsx 的清洗与入库3.1 字段规整与单位统一爬虫输出的字段是原始字符串比如89.5平米、545万、52,746元/平。这些值不能直接参与数值运算需要全部转成 float。下面这段清洗代码是项目里main.ipynb的常见起点# clean.py import pandas as pd import numpy as np df pd.read_excel(data.xls) df[area] df[area].astype(str).str.extract(r([\d.])).astype(float) df[total_price] df[total_price].astype(str).str.replace(万, ).astype(float) df[unit_price] df[unit_price].astype(str).str.replace(元/平, ).str.replace(,, ).astype(float) df[direction] df[direction].fillna(未知) df[year] pd.to_numeric(df[year], errorscoerce)清洗前后的变化可以参考这个对照表字段原始值示例清洗后处理方式area89.5平米89.5str.extract(r([\d.]))total_price545万545.0去掉「万」后转 floatunit_price52,746元/平52746.0移除逗号与单位year2005年2005.0pd.to_numeric(errorscoerce)这里str.extract(r([\d.]))的作用是提取文本中第一个由数字和点组成的连续片段比replace更鲁棒。errorscoerce会把非数字年份变成NaN后续用dropna处理即可。需要注意unit_price里的千分位逗号必须去掉否则astype(float)会直接抛异常。3.2 去重与缺失值处理链家同一个房源可能在多页中出现或者因为重新爬取造成重复记录。去重不能只依赖标题因为同一小区完全可能有两套同样面积的房子。我一般用「小区 面积 总价」组合去重这样既不会错删同面积但不同总价的真房源又能过滤重复抓取df df.drop_duplicates(subset[community, area, total_price], keepfirst) df df.dropna(subset[district, total_price])去掉不符合条件的数据后house.xlsx的规模会明显收敛。如果community有缺失可以把空值填为未知小区但district缺失时最好直接丢弃因为它会影响后面的区域分组统计。3.3 写入 SQLite 为后续分析打底Excel 适合观察和手工筛选但做条件聚合不如 SQLite 方便。项目里带有sql目录说明设计者预留了数据库方案。用 pandas 的to_sql可以一步完成建表和写入import sqlite3 conn sqlite3.connect(lianjia.db) df.to_sql(house, conn, if_existsreplace, indexFalse) conn.execute(CREATE INDEX idx_district ON house(district)) conn.close()to_sql会把 DataFrame 的列名映射到 SQLite 字段if_existsreplace表示下次运行直接覆盖旧表适合重复实验。建索引的目的是加速GROUP BY district之类的查询虽然几千条数据下效果不明显但能体现对数据库设计的理解。一个常见的坑是to_sql会把NaN写入为NULL所以查询时不要用匹配空值要用IS NULL。4. 数据分析与可视化价格分布、区域对比与影响因素拆解4.1 价格与面积的关系散点图与回归趋势房价和面积的关系是二手房分析中最基础的一环。虽然理论上面积越大总价越高但存在学区房、电梯房等干扰因素。先画散点图看整体分布再叠加一条回归趋势线能快速判断数据质量# analysis.py import pandas as pd import matplotlib.pyplot as plt import numpy as np plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False df pd.read_excel(house.xlsx) df df[(df[area] 20) (df[area] 200)] df df[df[total_price] 50] fig, ax plt.subplots(figsize(8, 5)) ax.scatter(df[area], df[total_price], alpha0.4, s8) z np.polyfit(df[area], df[total_price], 1) p np.poly1d(z) xs np.linspace(df[area].min(), df[area].max(), 100) ax.plot(xs, p(xs), colorred, linewidth2) ax.set_xlabel(面积 (㎡)) ax.set_ylabel(总价 (万)) ax.set_title(面积与总价关系) plt.show()np.polyfit是对面积和总价做一次多项式拟合返回斜率和截距np.poly1d把系数变成可调用的函数便于在图上画直线。过滤条件area 20是为了剔除杂物房和车位total_price 50是为了排除明显错误的数据。如果散点图中大多数点紧密分布在趋势线附近说明数据质量高如果出现很多离群点需要检查是否混入了「车位」或「商铺」等非住宅房源。4.2 区域均价对比条形图与箱线图链家覆盖的区域均价差异很大尤其是学区房集中区域。直接求每个区的单价均值容易受极端值影响所以用中位数更稳健。下面代码把各区按单价中位数排序并绘制水平条形图region_price df.groupby(district)[unit_price].median().sort_values() fig, ax plt.subplots(figsize(10, 6)) region_price.plot.barh(axax, color#4C72B0) ax.set_xlabel(单价 (元/㎡)) ax.set_title(各区单价中位数对比) plt.tight_layout() plt.show()sort_values()让条形图从低到高排列这样最高的区出现在顶部视觉上更容易对比。如果想要看出更多细节可以用seaborn.boxplot展示每个区的单价分布箱线图下面的代码只取了房源量最多的 8 个区避免图太拥挤import seaborn as sns top_regions df[district].value_counts().head(8).index plt.figure(figsize(12, 6)) sns.boxplot(datadf[df[district].isin(top_regions)], xdistrict, yunit_price) plt.xticks(rotation45) plt.show()箱线图中的中位线能反映区域典型价格箱子高度代表四分位距顶部的圆点则是异常房源。如果某个区的箱子特别高说明内部价格分化严重可能既有老旧小户型又有高端改善房。这个分析可以直接写进毕业设计的结论部分。4.3 朝向与楼层对价格的影响朝向是二手房交易中用户很关心的属性。链家数据里常见「南」「南北」「东」「西」四种偶尔还有「东南」「西南」复合朝向。用分组对比中位数df[direction] df[direction].fillna(未知) orientation_price df.groupby(direction)[unit_price].median().sort_values(ascendingFalse) print(orientation_price)统计结果中「南北」通常排在前面但这只是单变量分析没有控制区域和楼层。真正写报告时可以再进一步把区域固定为某一个区观察朝向在这个区内的价格差异。这样回答「朝向是否影响房价」时会更严谨。如果想要更精确可以用pd.pivot_table对district和direction做交叉聚合形成一个透视表用颜色热力图展示。5. 进阶与排错并发设计、增量爬取与常见坑位到了这个阶段毕业设计已经能完整运行但答辩时老师最常问的往往是「你如何处理反爬」「重复数据怎么办」「数据为什么可信」。这里给出三个直接能落地的技巧。5.1 并发参数和下载延时的取舍固定延时DOWNLOAD_DELAY 0.5比较简单但如果网络波动固定延时反而可能拖慢速度或触发保护。Scrapy 的 AutoThrottle 会根据服务器响应时间动态调整延时建议设置AUTOTHROTTLE_ENABLED True AUTOTHROTTLE_START_DELAY 1.0 AUTOTHROTTLE_MAX_DELAY 5.0 AUTOTHROTTLE_TARGET_CONCURRENCY 4.0AUTOTHROTTLE_START_DELAY是初始延时MAX_DELAY是最大限制TARGET_CONCURRENCY是目标并发数。开启后Scrapy 会监控每次请求的响应时间如果响应变慢自动增加延时。这个机制比固定延时更能适应当前网络环境也是简历里可以写的一个亮点。5.2 增量爬取用 URL 主键去重增量爬取的核心是记录已经抓过的房源下次只抓新增部分。在pipelines.py里维护一个已见集合# pipelines.py import sqlite3 import scrapy class LianjiaPipeline: def open_spider(self, spider): self.conn sqlite3.connect(lianjia.db) self.seen set(url[0] for url in self.conn.execute(SELECT url FROM house)) def process_item(self, item, spider): if item[url] in self.seen: raise scrapy.exceptions.DropItem(Duplicate URL) self.conn.execute( INSERT INTO house (url,title,community,total_price) VALUES (?,?,?,?), (item[url], item[title], item[community], item[total_price]) ) self.conn.commit() return item注意这个管道要求 spider 里必须给item[url]赋值一般就是response.url。DropItem会丢弃重复项并且不会影响后续数据处理。在settings.py里启用这个 pipeline 即可。这样第二次运行爬虫时旧数据不会被重复写入效率提升明显。5.3 Excel 与编码相关的坑data.xls和house.xlsx虽然都是 Excel 文件但读取库不同。xlrd1.2.0 支持.xls2.x 之后只支持.xlsx。如果你的环境里只装了新版xlrd读.xls会报错。建议在requirements.txt中同时固定xlrd1.2.0和openpyxl。另外用to_csv导出时使用encodingutf-8-sig这样才能避免 Excel 双击打开出现中文乱码df.to_csv(house.csv, indexFalse, encodingutf-8-sig)最后要提醒的是爬虫部分不要盲目追求抓取全量数据链家页面上已经给出了房源总数你只需要按页码抓取即可。如果在抓取过程中遇到验证码先检查请求频率再看网络状态不要直接换代理策略——课程设计的重点在于完整链路和数据分析结论而不是反爬对抗。项目源码里crawl目录和main.ipynb已经把所有步骤串起来了你只需要照着自己的环境把路径改一下就能得到一份结构清晰、能讲清楚来龙去脉的毕业设计。本文还有配套的精品资源点击获取