资讯动态

Python爬虫实战:安居客租房数据抓取、清洗与分析全链路

发布时间:2026/10/26 16:59:49 来源:尧图企业网站定制
简介这份资源围绕安居客武汉地区出租房信息展开完整覆盖从数据爬取、清洗到分析可视化的全流程适合正在学习Python爬虫、需要完成课程设计或毕业设计的学生以及想了解真实数据采集项目如何落地的人群。压缩包共14个文件包含4个Python脚本、4张可视化结果图、2份CSV数据文件以及配置文件、说明文档和缓存文件整体约3.68MB脚本与数据、图表相互对应便于对照理解。目前已有588人学习下载。资源最大特点是注释极为详细几乎逐行解释代码逻辑并首次引入matplotlib完成四个不同层面的可视化图表对爬虫入门和数据分析初学者都较友好。读者可借此掌握代理池配置、页面解析、数据清洗与图表呈现的完整思路也能参考项目结构组织自己的采集分析任务。1. 从安居客租房列表到一份能看的数据这条链路到底值不值得走很多人第一次做租房数据项目都是被一个很具体的需求逼出来的想比较几个小区的租金水平或者想看看某个地铁站周边一居室到底什么价位手动翻几十页列表翻到眼花于是想到用爬虫把安居客上的出租房信息抓下来再做数据清洗和数据分析。这个标题讲的正是这条完整链路——爬取、清洗、分析三段式。它解决的不是能不能抓到这一个问题而是抓完之后数据能不能用、能不能得出可信结论。适合有 Python 基础、想拿一个真实场景练手 pandas 数据清洗和处理、又不想停留在玩具数据集上的从业者。我自己的血泪经验是抓取只占三成工作量剩下七成全花在字段对齐、异常值处理和口径统一上翻车基本都翻在清洗环节。2. 抓取层安居客租房列表页的字段拆解与请求构造2.1 先想清楚要抓哪些字段再决定怎么抓动手写代码之前先把目标字段列成一张表这决定了你后面解析逻辑的复杂度。安居客租房列表页每条房源通常能拿到这些信息字段名含义来源位置清洗难度title房源标题列表卡片标题低community小区名标题或副标题中district区域/商圈面包屑或标签中rent月租金价格区块低layout户型面积户型行中area建筑面积面积户型行中floor楼层楼层信息行高orientation朝向标签行中tags标签集合标签区高detail_url详情页链接卡片链接低字段一旦定下来抓取策略就清晰了列表页负责拿大部分结构化字段详情页只在需要补充信息比如具体地址、发布时间时才二次请求。新手常犯的错是一上来就抓详情页请求量翻十倍被封的风险也翻十倍。常见做法是列表页为主、详情页按需补。2.2 请求构造分页参数、请求头与节流安居客的租房列表是典型的 URL 分页结构页码通常体现在路径里。下面是一个最小可跑的请求骨架重点看请求头和节流部分import requests import time import random HEADERS { User-Agent: ( Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 ), Accept-Language: zh-CN,zh;q0.9, Referer: https://www.anjuke.com/, } def fetch_page(city: str, page: int, timeout: int 10) - str: 抓取指定城市租房列表的第 page 页返回 HTML 文本 url fhttps://{city}.anjuke.com/rent/p{page}/ try: resp requests.get(url, headersHEADERS, timeouttimeout) resp.raise_for_status() resp.encoding resp.apparent_encoding # 防止中文乱码 return resp.text except requests.RequestException as e: print(f[WARN] page {page} failed: {e}) return def polite_sleep(base: float 2.0, jitter: float 1.5): 随机休眠降低被识别为自动化请求的概率 time.sleep(base random.uniform(0, jitter))逻辑说明fetch_page把城市和页码拼进 URLapparent_encoding是处理中文页面乱码最省事的办法比硬编码 utf-8 稳。polite_sleep用基础间隔加随机抖动比固定 sleep 更接近真人节奏。参数上timeout建议 10 秒太短会误判超时太长会拖慢整体base我一般设 2 秒起步页数多的时候不要低于 1.5 秒。提示请求头里的Referer和Accept-Language不是可有可无的装饰缺失时部分页面会返回精简版或直接拒绝这是很多人本地能跑、换台机器就空的原因。2.3 解析用选择器把 HTML 变成结构化记录解析层我倾向用 BeautifulSoup 配 lxml 解析器容错好、调试直观。核心是把每张房源卡片定位出来再在卡片内部逐字段提取from bs4 import BeautifulSoup from urllib.parse import urljoin def parse_list(html: str, base_url: str) - list[dict]: 从列表页 HTML 中解析出房源记录列表 soup BeautifulSoup(html, lxml) records [] for card in soup.select(div.property-content): title_el card.select_one(h3.property-content-title-name) price_el card.select_one(span.property-price-total-num) info_els card.select(p.property-content-info-attribute) link_el card.select_one(a.property-content-title-name) record { title: title_el.get_text(stripTrue) if title_el else None, rent: price_el.get_text(stripTrue) if price_el else None, layout: info_els[0].get_text(stripTrue) if len(info_els) 0 else None, area: info_els[1].get_text(stripTrue) if len(info_els) 1 else None, detail_url: urljoin(base_url, link_el[href]) if link_el else None, } records.append(record) return records逻辑说明select返回列表select_one返回单个元素用if ... else None兜底是为了防止某个字段缺失导致整条记录崩掉。urljoin把相对链接补成绝对链接方便后续去重和回访。参数上选择器字符串是最容易随页面改版失效的部分建议把它抽成常量集中管理改版时只改一处。2.4 落盘先存原始数据再谈清洗抓取阶段我强烈建议原样落盘不要边抓边清洗。存成 JSON Lines 或 CSV 都行关键是保留原始文本给后面留后悔药import json def save_raw(records: list[dict], path: str): 追加写入原始记录一行一条 JSON with open(path, a, encodingutf-8) as f: for r in records: f.write(json.dumps(r, ensure_asciiFalse) \n)逻辑说明JSON Lines 的好处是追加写、单行损坏不影响整体、读取时按行解析即可。ensure_asciiFalse保证中文正常显示。参数上文件按城市或日期分片比如raw_shanghai_20240101.jsonl避免单文件过大。3. 清洗层把脏字符串变成能算数的列3.1 清洗的四个动作抽取、统一、补全、去重抓下来的字段几乎全是带单位的字符串比如租金是4500元/月面积是89.5㎡户型是2室1厅1卫。清洗的目标是把它们变成数值列和规范类别列。四个动作按顺序做抽取用正则把数字从字符串里抠出来。统一把元/月元每月统一成同一口径把㎡平米统一。补全缺失值按规则填充或标记不要直接删。去重按 detail_url 或标题小区租金组合去重。下面是一段可直接复用的清洗代码import re import pandas as pd def extract_number(text): 从字符串中抽取第一个数字支持小数 if pd.isna(text): return None m re.search(r(\d(?:\.\d)?), str(text)) return float(m.group(1)) if m else None def clean_rent(df: pd.DataFrame) - pd.DataFrame: df df.copy() # 租金抽取数字 df[rent_num] df[rent].apply(extract_number) # 面积抽取数字 df[area_num] df[area].apply(extract_number) # 户型拆成室、厅 df[rooms] df[layout].str.extract(r(\d)室).astype(float) df[halls] df[layout].str.extract(r(\d)厅).astype(float) # 去重 df df.drop_duplicates(subset[detail_url]) # 异常值过滤租金 500 到 100000 之间面积 5 到 1000 之间 df df[(df[rent_num].between(500, 100000)) (df[area_num].between(5, 1000))] return df逻辑说明extract_number用非贪婪正则抓第一个数字兼容整数和小数。str.extract直接产出新列比 apply 快。异常值过滤用between一行搞定边界值 500 和 100000 是根据一线城市实际租金分布定的二三线城市可以下调。参数上rooms和halls转 float 是为了后续能参与计算缺失的会变成 NaN。3.2 单价才是可比口径别拿总租金直接比不同房源面积差一倍直接比月租金没有意义。清洗完必须算一个派生列——每平米月租金df[rent_per_sqm] df[rent_num] / df[area_num]逻辑说明这个派生列是后面所有分析的基石。参数上如果面积缺失rent_per_sqm会是 NaN分析时要显式排除不要用 0 填充否则会拉低均值。注意清洗阶段最容易被忽略的是同一套房源多次出现。安居客列表里同一房源可能因为置顶、推荐位重复出现去重必须放在清洗早期否则后面所有统计都会偏高。3.3 缺失值处理删、填、标记三选一缺失值处理没有标准答案但有判断顺序。先看缺失比例超过 40% 的列直接放弃不要硬填。低于 5% 的可以删行。中间地带用规则填充或标记def handle_missing(df: pd.DataFrame) - pd.DataFrame: df df.copy() # 朝向缺失填未知保留记录 df[orientation] df[orientation].fillna(未知) # 楼层缺失标记出来分析时单独看 df[floor_missing] df[floor].isna() # 关键字段缺失租金、面积直接删 df df.dropna(subset[rent_num, area_num]) return df逻辑说明把缺失本身当成一个信息维度保留下来比无脑填充更诚实。参数上floor_missing这种布尔标记列在分组分析时特别有用能帮你判断缺失是否集中在某类房源。4. 分析层从租金分布到区域对比的落地路径4.1 先做描述统计别急着上模型拿到清洗后的数据第一步永远是描述统计。用 pandas 几行就能看清数据长什么样import pandas as pd df pd.read_csv(clean_rent.csv) # 整体租金分布 print(df[rent_num].describe()) print(df[rent_per_sqm].describe()) # 按区域聚合 district_stat df.groupby(district).agg( count(rent_num, size), avg_rent(rent_num, mean), median_rent(rent_num, median), avg_unit(rent_per_sqm, mean), ).sort_values(avg_unit, ascendingFalse) print(district_stat.head(10))逻辑说明describe给出均值、中位数、四分位能快速发现极端值。groupby加agg是区域对比的标准写法同时看均值和本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑