news 2026/9/26 14:06:04

Python爬虫实战:从租房数据采集到可视化看板

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python爬虫实战:从租房数据采集到可视化看板

接手这个项目的时候,我脑子里第一个念头很简单:能不能用Python把某租房平台的数据扒下来,然后做成一套直观的看板。这个念头落地之后,实际的收获比我预想的大得多——爬虫只是前半场,后半场的数据清洗、存储设计、可视化表达,才是真正拉开差距的地方。如果你正卡在“只会写requests请求、不知道怎么把数据变成价值”的阶段,这篇文章应该能帮你省下不少弯路。

我用的技术栈很常规:Python + requests + BeautifulSoup + SQLAlchemy + ECharts,没有上Scrapy这种重型框架,也没有用复杂的分布式方案。原因后面会细说。这套东西跑下来的最终产物是一个本地的房源数据看板,包含区域均价对比、户型占比、面积与价格散点关系、价格区间分布等图表,全部基于真实抓取的数据,不是demo里那种假数据。

先说结论:这个项目最大的价值不在于“爬了多少条数据”,而在于把“从网页到可视化”这条完整链路打通了。你会在里面接触到请求构造、HTML解析、反爬应对、ORM存储、SQL聚合查询、Flask接口设计、前端图表配置,每一个环节都是实际工作中高频用到的技能点。哪怕你暂时不打算做房源方向,把这条链路跑通一次,后面迁移到其他领域会非常顺。

1. 项目拆解:从零散的房源页到结构化看板

1.1 核心需求解析

表面上看,这个项目的目标就是“抓数据、画图表”。但如果你只做到这一步,做出来的东西大概率是个一次性脚本,换个网站就废了,数据攒多了还会乱。所以我在动手之前,先把需求拆成了四个层次:

  • 数据层:能稳定地从目标网站拿到字段完整、干净可用的房源数据。
  • 存储层:数据落库之后可以增量更新、按条件查询、去重,而不是每次全量抓取覆盖。
  • 分析层:能从多个维度聚合数据,比如按区域求均价、按户型统计占比、看面积和价格的相关性。
  • 展示层:用交互式图表把分析结果呈现出来,非技术背景的人也能一眼看懂。

这四个层次对应了项目里的四个模块:爬虫模块、ORM模型与存储模块、统计查询模块、Flask + ECharts可视化模块。分清楚层次之后,代码结构就会很清晰,后期调试也不用到处找逻辑。

1.2 技术选型:为什么是这套组合

技术选型这块,实话实说,我当时有犹豫过要不要上Scrapy。后来还是放弃了,原因很简单:目标网站的规模不大,反爬强度也一般,Scrapy的异步爬取、中间件体系在这里属于大炮打蚊子。requests加BeautifulSoup的组合,写起来直观,调试方便,还能让你把每个环节的原理吃透。等你用这套“笨办法”把手动流程跑顺了,再上Scrapy会轻松很多。

存储层我选了SQLAlchemy ORM。对于爬虫项目,ORM带来的最大好处是:不用手写一堆CREATE TABLE和INSERT语句,Python类直接映射成表结构,后续加字段、改类型都很方便。SQLAlchemy还天然防SQL注入,对数据校验也更友好。数据库我用的SQLite,因为项目规模小、单机跑足够,而且零配置。如果你要对接MySQL,把连接串改一下就行,代码几乎不用动。

可视化部分选ECharts是没悬念的。它图表类型全、交互流畅,更关键的是对前后端分离的展示方式支持很好。我只需要在Flask里暴露几个返回JSON的接口,前端用fetch获取数据,然后塞进ECharts的option配置里就够了,不需要像传统模板渲染那样把数据揉进HTML。

1.3 整体架构和数据处理流程

整个项目的数据流是这样的:

爬虫抓取网页 → BeautifulSoup解析出结构化字段 → 清洗数据(处理空值、单位换算、去除重复) → SQLAlchemy写入SQLite → 统计模块从数据库聚合查询 → Flask接口输出JSON → ECharts渲染图表

这套流程里有一个经常被人忽略的关键点:爬虫输出的数据结构,和数据库表的字段结构,以及前端图表需要的JSON结构,是三个不同的层次,不能混在一起写。爬虫模块里解析数据时,我只做字段提取和基本清洗;统计查询时,我再用pandas或SQL做聚合;到了API层,我根据图表需要重新组织JSON。每一层保持单一职责,后面迭代任何一个环节都不会牵连到其他部分。

2. 爬虫核心模块:请求构造、解析策略与反爬应对

2.1 请求怎么构造才不容易被拒

第一步不是写代码,而是先用浏览器开发者工具看目标网站的结构。打开目标网站、按F12、切到Network面板,刷新页面,找到承载房源列表的请求。很多时候网站是异步加载的,列表数据藏在XHR请求返回的JSON里,反而比HTML好解析。

构造请求时,有几个细节能明显降低被拒的概率:

  • User-Agent要完整,不能只在网上随便抄一个,最好是和你当前浏览器版本一致的字符串。有的网站会校验User-Agent和浏览器指纹的一致性,不一致就拒绝。
  • Referer要带上,特别是翻页请求,Referer往往是前一页的URL。缺失Referer或者Referer对不上,很容易触发反爬。
  • Cookie要手动带上。首次访问网站的Cookie里通常有反爬用的指纹信息,建议先从浏览器里复制出来,再放到请求头里。

请求代码大概是这个风格:

import requests import time import random HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36", "Referer": "https://www.xxx.com/zufang/", "Accept": "application/json, text/plain, */*", "Cookie": "你的浏览器Cookie" } def fetch_page(url): for attempt in range(3): try: resp = requests.get(url, headers=HEADERS, timeout=10) if resp.status_code == 200: return resp.text elif resp.status_code in (403, 429): time.sleep(10 + random.random() * 5) continue except requests.RequestException as e: print(f"第{attempt + 1}次请求失败: {e}") time.sleep(2) return None

这里我加了重试机制和退避策略,实测下来能顶住大部分瞬时的网络波动。另外,千万不要快于每5秒一次请求,除非你确定目标网站的robots.txt允许高频抓取。频率控制不仅是礼貌问题,更是防止IP被封的保命手段。

2.2 解析策略:BeautifulSoup还是正则

拿到的响应可能是HTML,也可能是JSON。如果是JSON,用Python自带的json库解析就完事,极其省心。如果是HTML,我习惯先用requests拿文本,再交给BeautifulSoup按结构解析。

BeautifulSoup解析的时候,最容易踩的坑是“定位不准”。别一上来就写复杂的嵌套选择器,先小步验证:

先看一眼页面里房源列表项长什么样。如果列表项都在某个特定的class里,就直接用select方法按CSS选择器提取,稳定性和可读性都更好。比如:

from bs4 import BeautifulSoup soup = BeautifulSoup(html, "lxml") items = soup.select("div.house-lst li") # 示例选择器,按实际页面结构调整 for item in items: title = item.select_one(".title a") price = item.select_one(".price") area = item.select_one(".area") ...

提取字段的时候要养成一个习惯:每个字段都判空。真实网页总有缺字段的情况,不做判断的话,解析到一半就抛异常,整个抓取流程就断了。所以我一般这么写:

def safe_text(element): return element.get_text().strip() if element else ""

解析这块,网络上的坑比较多,很多人的做法是写一个千行级的正则去匹配。我的建议是正则能不用就不用,除非目标数据嵌在script标签的JavaScript变量里、BeautifulSoup拿不到。那种场景再用正则提取JSON片段,然后交给json解析。

2.3 反爬应对:把请求伪装得接近真人

反爬这事,我把它分成两类。一类是“机器检测”,比如IP频率、User-Agent异常、请求间隔过于规律;另一类是“行为检测”,比如鼠标轨迹、点击行为、页面停留时间。

对于第一类,常见的应对手段有:

  • 随机User-Agent池:准备十几个主流浏览器的UA,每次随机取一个。
  • 固定延迟加随机抖动:两次请求之间sleep一个随机时间,比如2到4秒,而不是每次都精确地睡3秒。
  • 代理IP:如果目标网站对单个IP的请求频率卡得很死,就需要代理池。这里我不展开说具体的代理服务,只提醒一句:免费代理大多数不稳定,别指望它扛大流量。

对于第二类行为检测,requests是模拟不了的,这种情况才需要上Selenium。但说句掏心窝的话:大部分中级以下的目标网站根本用不到Selenium,requests加合理的请求头就能拿下来。Selenium又慢又耗资源,能不用就不用。只有当你发现数据必须经过JavaScript动态计算才能拿到、且不去逆向接口就拿不到的时候,再考虑它。

import random USER_AGENTS = [ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0 Safari/537.36", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/119.0 Safari/537.36", "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:121.0) Gecko/20100101 Firefox/121.0", ] def get_random_headers(): return { "User-Agent": random.choice(USER_AGENTS), "Referer": "https://www.xxx.com/", }

这里有个经验:很多初学者担心自己UA不够新,其实网站更在意的是“你的请求链是否完整且一致”。缺Referer、Accept异常、Accept-Language缺失,比UA版本旧更容易触发风控。

2.4 数据清洗:字段缺省、单位换算与去重

抓下来的数据不能直接入库。以房源数据为例,常见的脏数据有这几种:

  • 价格字段出现“暂无”、“面议”这类非数值内容,需要转为空值或直接过滤。
  • 面积字段带单位“平米”,但有的是“50㎡”,有的是“50平”,格式不统一,需要格式化成纯数字。
  • 同一个房源在列表页和详情页出现多次,需要根据房源ID去重。
  • 区域字段可能是“朝阳”也可能是“朝阳区”,需要归一化处理。

清洗逻辑写在哪一层?我习惯爬虫解析完就做第一层清洗(去掉明显无意义的字符),入库前做第二层清洗(规范化格式),统计查询时做第三层处理(聚合、分组)。分层清洗的好处是每层的职责清晰,也不会出现一个函数里又做正则又做去重又做格式化的“大杂烩”。

2.5 SQLAlchemy数据入库:ORM模型、会话管理与增量去重

数据清洗完之后,下一步就是入库。我先定义一个ORM模型。以房源表为例:

from sqlalchemy import create_engine, Column, Integer, String, Float from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker Base = declarative_base() class House(Base): __tablename__ = "houses" id = Column(Integer, primary_key=True, autoincrement=True) house_id = Column(String(64), unique=True, index=True) # 房源唯一ID,用于去重 title = Column(String(255)) district = Column(String(64)) # 区域 layout = Column(String(32)) # 户型 area = Column(Float) # 面积(平方米) price = Column(Float) # 租金(元/月) url = Column(String(512))

关键就在house_id这一列,我加了unique约束和索引。每次写入前先查一下这个ID在不在库里,不在才插入。这样增量抓取时就不会产生重复数据。

engine = create_engine("sqlite:///houses.db") Base.metadata.create_all(engine) Session = sessionmaker(bind=engine) session = Session() def insert_house(item): exists = session.query(House).filter_by(house_id=item["house_id"]).first() if exists: return False session.add(House(**item)) session.commit() return True

批量插入的时候,一条条commit效率不高,可以用bulk_save_objects或先批量add、最后集中commit。不过对于千级数据量来说,差异不大,属于锦上添花的优化。

3. 数据可视化:从数据库聚合到ECharts看板

3.1 分析维度怎么定

数据入库只是手段,可视化才是目的。但可视化不是把数据随便一画,而是要先想清楚:你拿着这批房源数据,到底想回答什么问题?

我当时定了四个核心问题:

  • 哪个区域的房子最贵?区域之间的价差有多大?——区域均价柱状图。
  • 价格集中在什么区间?整租的租金负担大概在什么水平?——价格直方图。
  • 面积和租金是不是线性关系?有没有性价比特别高的房源?——面积-价格散点图。
  • 户型分布是啥样?一居、两居、三居各占多少?——户型占比饼图。

这四个问题对应了四个图表,每一个都是从数据中能直接获得洞察的维度。做可视化之前,先把问题定义清楚,比一上来就套图表模板重要得多。

3.2 用pandas做聚合查询

聚合这块我用的是pandas。把SQLite的数据全部读进DataFrame,然后按需分组、聚合、输出JSON。pandas的优势是语法直观,groupby一下就能出统计结果,不用一遍遍写SQL。

import pandas as pd df = pd.read_sql("SELECT * FROM houses", engine) # 区域均价 area_price = df.groupby("district")["price"].mean().sort_values(ascending=False) # 价格区间分布 price_bins = pd.cut(df["price"], bins=[0, 2000, 4000, 6000, 8000, 10000, float("inf")]) price_dist = df.groupby(price_bins, observed=False).size() # 面积与价格相关性 scatter_data = df[["area", "price"]].dropna()

用pandas跑聚合最大的好处是省掉了手工拼接SQL的麻烦,而且可以无缝做数据清洗和转换。如果你对SQL更熟,用SQLAlchemy core或原生SQL也完全可以,看个人习惯。

3.3 Flask接口设计:让前端拿数据更优雅

可视化层我用Flask起了个轻量服务,直接读pandas或SQLAlchemy查出来的结果,包装成JSON返回给前端。

我建议把接口按照“一个图表一个接口”的粒度来设计:

from flask import Flask, jsonify app = Flask(__name__) @app.route("/api/area_price") def area_price_api(): data = area_price.reset_index() result = { "categories": data["district"].tolist(), "values": [round(v, 2) for v in data["price"].tolist()] } return jsonify(result)

ECharts的很多图表配置里,x轴数据对应categories,y轴数值对应series数据。所以我直接按这个结构输出,前端拿到就能直接用,不用再做转换。看起来很简单,但这是很实用的小技巧——API的输出结构尽量贴近前端组件的输入结构,能省掉一堆胶水代码。

3.4 ECharts配置与动态渲染

前端部分,我用一个最朴素的HTML页面,引入ECharts,然后通过fetch从接口取数,填入option配置。

以区域均价柱状图为例,核心配置长这样:

fetch('/api/area_price') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('areaChart')); chart.setOption({ title: { text: '各区域平均租金' }, tooltip: {}, xAxis: { type: 'category', data: data.categories }, yAxis: { type: 'value', name: '元/月', axisLabel: { formatter: '{value} 元' } }, series: [{ type: 'bar', data: data.values, itemStyle: { color: '#5470c6' } }] }); });

散点图则是另一种配置方式:

fetch('/api/scatter') .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('scatterChart')); chart.setOption({ xAxis: { type: 'value', name: '面积 (㎡)' }, yAxis: { type: 'value', name: '租金 (元/月)' }, series: [{ type: 'scatter', data: data.points, symbolSize: 8 }] }); });

ECharts官方的文档和示例都很全,遇到不熟悉的图表类型,直接去查配置项就行。这里我想强调一个容易被忽略的点:很多数据可视化的“图”看起来丑,不是因为ECharts不好用,而是因为数据没有处理干净。比如散点图里混进了几个面积300平、租金2000的异常值,整个图的坐标尺就被拉变形了。所以可视化之前,把异常值过滤掉非常关键。

4. 踩坑实录:反爬、编码、存储与性能问题排查

4.1 高频问题速查表

这一路上我踩了不少坑,按出现频率从高到低整理如下:

问题现象可能原因解决思路
请求返回403请求头不完整或频率过高补全headers、降低频率、加随机延迟
解析结果为空列表选择器失效或页面结构改变重新打开页面检查结构,改用更稳定的定位方式
中文乱码页面编码和requests解码不一致用resp.encoding指定编码,或从headers里取charset
数据库里有大量重复数据没有唯一约束或没做增量判断加unique字段,插入前先查重
ECharts图表空白JSON结构不对或series类型错误打开浏览器控制台看报错,打印API返回数据排查
抓取到一半IP被限制请求频率过高降低频率或换代理,重新等段时间再继续

4.2 页面结构变化导致的“隐性失败”

最坑的问题不是404,而是页面改版之后解析器静默失效。你看到解析结果都是空,代码也不报错,就是拿不到数据。这个问题靠debugger很难发现,因为它不抛异常。

排查手段是写一个“断言式”的检查:解析完一批数据后,检查关键字段是否有值、数量和预期是否一致。如果数量骤降,就发个告警或直接停掉,免得抓了一堆无效数据入库。

4.3 性能优化备忘

对千条级别的数据量,这个项目没必要谈性能优化。但如果你想跑几十万条数据,有几点可以提前注意:

  • 请求不要串行爬,可以考虑用concurrent.futures的线程池,但注意线程数控制在5以内,别把目标网站搞崩。
  • 数据库写入用批量方式,别一条条commit。
  • 可视化接口不要每次都从全表计算,可以提前算好结果缓存到本地JSON或Redis,前端直接读取。

性能优化的原则就一条:在哪一层出现了瓶颈,就在哪一层解决,别盲目上分布式。很多人一听到“爬虫性能”就想到分布式爬虫,可对于绝大多数个人项目来说,瓶颈根本不在这。

写在项目之外

最后说点我自己的心得体会。这个项目做完,我最深的感觉是:爬虫只是工具,数据分析和可视化才是让数据产生价值的地方。很多人在爬虫阶段反复纠结选择器怎么写、反爬怎么过,但拿到数据之后却不知道怎么处理,也不知道这些数据能回答什么问题。这其实是本末倒置了。

如果你也想自己动手做一遍,我建议从一个小目标开始,不要追求“全网房源数据”,先锁一个城市、一个平台、几百条数据,把整条链路跑通。跑通之后再考虑扩大数据量、增加分析维度、优化展示效果。这个项目后续能扩展的方向也不少——比如加定时调度实现每日自动抓取、接一个预测模型做租金预测、把图表从本地页面升级成在线看板。每往前推进一步,你的数据工程能力都会跟着提升一截。

如果你在做的过程中遇到问题,先不要急着改代码,去把目标网页的原始HTML打出来看一看,或者去浏览器控制台看看接口返回。绝大多数爬虫问题,最后都是“数据结构和预期不一致”的问题,找到源头比瞎试有效得多。祝你能用这套流程,建立起自己的第一个数据可视化作品。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/26 14:03:20

ax:面向Agentic负载的Kubernetes CLI编排调度入口

1. 从“ax”这个标题说起:一个被低估的Agentic编排入口第一次看到“ax”这个标题,很多人会以为是某个命令行工具的缩写,或者某个内部代号。但把热搜词摊开来看——ax、agentic、orchestrator、Kubernetes、CLI——这几个词凑在一起&#xff0…

作者头像 李华
网站建设 2026/9/26 14:03:04

从收藏到行动:用藏经阁思维搭建个人百度网盘知识库

我在搭“百度网盘免费资源”这个方向的知识库之前,和很多人一样,网盘里存了上千个文件,真要用的时候却找不到东西,甚至忘了自己存过什么。后来我给自己定了一个规矩:所有资源必须按“能不能变成能力”来分类&#xff0…

作者头像 李华
网站建设 2026/9/26 14:03:03

手摸手带你安装OpenClaw并对接飞书:TaoToken统一Key配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华