网站: 长沙二手房房源_长沙二手房出售|买卖|交易信息-长沙链家
温馨提示: 本案例仅供学习交流使用

首先 明确爬取的数据
- 标题
- 地址
- 房屋信息
- 关注人数 发布时间
- 价格 每平方米的价格
步骤:
- 简单分析界面 对前端的html结构有个了解
- 构建请求 模拟浏览器向服务器发送请求
- 解析数据 提取我们所需要的数据
- 保存数据 对数据进行持久化保存 如csv excel mysql
一.发送请求
右键查看源代码 查看是否为静态数据
Ctrl+F 打开搜索框 查看是否包含我们爬取的数据

发现可以搜到我们想要爬取的数据 因此为静态数据
接着我们构建请求体 复制浏览器中的Url
python
# 导包
import requests
import parsel
import pprint
# 网址
url = 'https://cs.lianjia.com/ershoufang/'
# 请求体
headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0',
'referer':
'https://cs.lianjia.com/',
'cookie':
'select_city=430100; lianjia_uuid=40cfad10-6028-4fa5-98e7-f64cc30f5e1f; Hm_lvt_46bf127ac9b856df503ec2dbf942b67e=1761456276; HMACCOUNT=2EDE6B4FF192C403; _jzqc=1; _jzqckmp=1; sajssdk_2015_cross_new_user=1; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2219a1ef9c78e6c6-07959c473245b58-4c657b58-3686400-19a1ef9c78fc3f%22%2C%22%24device_id%22%3A%2219a1ef9c78e6c6-07959c473245b58-4c657b58-3686400-19a1ef9c78fc3f%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24latest_referrer_host%22%3A%22%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%7D%7D; _qzjc=1; _ga=GA1.2.232101018.1761456287; _gid=GA1.2.1226634737.1761456287; crosSdkDT2019DeviceId=-rpv6dr--190h9s-lgasbfwtoyze63k-gir0i2fuf; _jzqa=1.172784459768417000.1761456277.1761456277.1761459496.2; Hm_lpvt_46bf127ac9b856df503ec2dbf942b67e=1761459550; _qzja=1.2127940051.1761456277714.1761456277714.1761459496327.1761459544138.1761459550314.0.0.0.6.2; _qzjto=6.2.0; srcid=eyJ0Ijoie1wiZGF0YVwiOlwiZTE4YzBjN2Q2NWEwMTM1MzU5ODMzYjM4OTlmODM5NGRiZWRiOTUyODBlNmM2Zjg3MjU2YzNkMTQ4MjJmOWFlZTQ4M2I1MzljODVmMGJjYzYyZTM5NmUwYzc1YzQ4OTExYTAwNjFjZjAyZmM1YzgyODcwMjkyNzBkOWM1Zjg0MGNlM2NjZGIxOTU1NjBiY2RhNzhkMDBlZjE2OGZiNTg4NmJkMzExZTBmZTE0NmJmMDY4MjRlZGJhMjJmYzlkZDBmMzU2OTkzYjliZTE5Y2IwZTJkZGU1MjBmYzY1NGFiNTVlZThjZmViYWIyNTZkM2U0NGNjMDAyNTMxYWFlOTAxOFwiLFwia2V5X2lkXCI6XCIxXCIsXCJzaWduXCI6XCJhNWQ4Zjk5YVwifSIsInIiOiJodHRwczovL2NzLmxpYW5qaWEuY29tL2Vyc2hvdWZhbmcvIiwib3MiOiJ3ZWIiLCJ2IjoiMC4xIn0=; _ga_4JBJY7Y7MX=GS2.2.s1761459508$o2$g1$t1761459562$j6$l0$h0; lianjia_ssid=53f9dd1f-4942-34e4-1128-73bc6a6017cd'
}
# 返回的响应
resp = requests.get(url, headers=headers)
打开页面 F12 or 右击检查 打开开发者工具
点击左上角的这个像鼠标的 然后去页面中去选要爬取的数据 查看分析节点

通过分析可得 每个房屋的所有信息都在class属性为sellListContent Ul标签中的li标签中
OK 简单地分析了结构之后 我们开始写代码
首先复制 浏览器中的地址 接着构建请求体 UserAgent(包含浏览器的基本信息 载荷 浏览器类型等) Referen(防盗链 简单来说就是你目前这个页面是从哪里跳转过来的) Cookie(包含了用户的一些登陆基本信息 ) 在浏览器复制就可以了
二. 解析数据 提取数据
数据解析模块是用的parsel 需要pip 安装 pip install parsel
这里我们选择使用css取提取数据 也可以使用 re xpath 看大家的习惯
python
# 将浏览器返回的文本 转换成Selector对象
select = parsel.Selector(resp.text)
lis = select.css('.sellListContent li')
# 找到包含所有房屋信息的跟标签 循环遍历
接着继续提取爬取的数据
点击开发者工具左上角的箭头 选择爬取的数据 分析页面结构
标题:

其它也类似 这里就不多演示了
温馨提示: 如果标签中有多个 class属性 中间用.隔开 如提取价格的时候
如果要提取表中的属性 使用attr(attract的缩写 提取对应的属性) 如提取图片的时候
python
for li in lis:
title = li.css('.title a::text').get()
address = li.css('.positionInfo a::text').get()
house_info = li.css('.houseInfo ::text').get()
follow = li.css('.followInfo ::text').get()
price = li.css('.totalPrice.totalPrice2 span::text').get()
square_price = li.css('.unitPrice span::text').get()
img = li.css('.lj-lazy ::attr(src)').get()
以上的数据提取完毕 我们可以在控制台打印看看 是否满足我们想要的格式效果

可以看到house_info中的信息有点冗余了 我们给它做个细分
以 ' | '分割 会返回一个列表 通过列表取值 得到详细的划分 然后重新赋值字段名
这里有个问题 就是 存在获取到的数据内容为None的情况 此时需要做个判断 如果获取到的内容是空的就跳过本次循环

取好变量名
注意到 不是每个房屋信息里面都有这个 年份的信息 这个时候可以使用三元运算符 做个判断
如果年在这个信息里面 就取 反之 返回 None
python
for li in lis:
title = li.css('.title a::text').get()
address = li.css('.positionInfo a::text').get()
if li.css('.houseInfo ::text'):
house_info = li.css('.houseInfo ::text').get().split('|')
rooms = house_info[0]
area = house_info[1]
orient = house_info[2]
house_type = house_info[3]
floor = house_info[4]
year = house_info[-2] if '年' in house_info[-2] else 'None'
room_architecture = house_info[-1]
else:
continue
follow = li.css('.followInfo ::text').get().split('/')[0]
issue_time = li.css('.followInfo ::text').get().split('/')[-1]
price = li.css('.totalPrice.totalPrice2 span::text').get()
square_price = li.css('.unitPrice span::text').get()
img = li.css('.lj-lazy ::attr(data-original)').get()
接着我们需要保存数据
保存为Excel文件
首先存储到字典中 之后定义一个空列表 将字典添加到列表中 Like this
此时我们可以使用 美化打印模块 输出打印看看我们所需要的数据
python
dit = {
'title': title,
'address': address,
'rooms': rooms,
'area': area,
'orient': orient,
'house_type': house_type,
'floor': floor,
'year': year,
'room_type': room_architecture,
'follow': follow,
'issue_time': issue_time,
'price': price,
'square_price': square_price,
'img': img,
}
pprint.pprint(dit)
这样就清新很多

多页爬取的话 在Url中加个参数就可以了 pg=页码
爬虫部分的源代码 供大家学习参考
python
import requests
import parsel
import pprint
import pandas as pd
all_data = []
for i in range(1,11):
url = f'https://cs.lianjia.com/ershoufang/pg{i}/'
# 请求体
headers = {
'user-agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0',
'referer':
'https://cs.lianjia.com/',
'cookie':
'select_city=430100; lianjia_uuid=40cfad10-6028-4fa5-98e7-f64cc30f5e1f; Hm_lvt_46bf127ac9b856df503ec2dbf942b67e=1761456276; HMACCOUNT=2EDE6B4FF192C403; _jzqc=1; _jzqckmp=1; sajssdk_2015_cross_new_user=1; sensorsdata2015jssdkcross=%7B%22distinct_id%22%3A%2219a1ef9c78e6c6-07959c473245b58-4c657b58-3686400-19a1ef9c78fc3f%22%2C%22%24device_id%22%3A%2219a1ef9c78e6c6-07959c473245b58-4c657b58-3686400-19a1ef9c78fc3f%22%2C%22props%22%3A%7B%22%24latest_traffic_source_type%22%3A%22%E7%9B%B4%E6%8E%A5%E6%B5%81%E9%87%8F%22%2C%22%24latest_referrer%22%3A%22%22%2C%22%24latest_referrer_host%22%3A%22%22%2C%22%24latest_search_keyword%22%3A%22%E6%9C%AA%E5%8F%96%E5%88%B0%E5%80%BC_%E7%9B%B4%E6%8E%A5%E6%89%93%E5%BC%80%22%7D%7D; _qzjc=1; _ga=GA1.2.232101018.1761456287; _gid=GA1.2.1226634737.1761456287; crosSdkDT2019DeviceId=-rpv6dr--190h9s-lgasbfwtoyze63k-gir0i2fuf; _jzqa=1.172784459768417000.1761456277.1761456277.1761459496.2; Hm_lpvt_46bf127ac9b856df503ec2dbf942b67e=1761459550; _qzja=1.2127940051.1761456277714.1761456277714.1761459496327.1761459544138.1761459550314.0.0.0.6.2; _qzjto=6.2.0; srcid=eyJ0Ijoie1wiZGF0YVwiOlwiZTE4YzBjN2Q2NWEwMTM1MzU5ODMzYjM4OTlmODM5NGRiZWRiOTUyODBlNmM2Zjg3MjU2YzNkMTQ4MjJmOWFlZTQ4M2I1MzljODVmMGJjYzYyZTM5NmUwYzc1YzQ4OTExYTAwNjFjZjAyZmM1YzgyODcwMjkyNzBkOWM1Zjg0MGNlM2NjZGIxOTU1NjBiY2RhNzhkMDBlZjE2OGZiNTg4NmJkMzExZTBmZTE0NmJmMDY4MjRlZGJhMjJmYzlkZDBmMzU2OTkzYjliZTE5Y2IwZTJkZGU1MjBmYzY1NGFiNTVlZThjZmViYWIyNTZkM2U0NGNjMDAyNTMxYWFlOTAxOFwiLFwia2V5X2lkXCI6XCIxXCIsXCJzaWduXCI6XCJhNWQ4Zjk5YVwifSIsInIiOiJodHRwczovL2NzLmxpYW5qaWEuY29tL2Vyc2hvdWZhbmcvIiwib3MiOiJ3ZWIiLCJ2IjoiMC4xIn0=; _ga_4JBJY7Y7MX=GS2.2.s1761459508$o2$g1$t1761459562$j6$l0$h0; lianjia_ssid=53f9dd1f-4942-34e4-1128-73bc6a6017cd'
}
resp = requests.get(url, headers=headers)
select = parsel.Selector(resp.text)
lis = select.css('.sellListContent li')
for li in lis:
title = li.css('.title a::text').get()
address = li.css('.positionInfo a::text').get()
if li.css('.houseInfo ::text'):
house_info = li.css('.houseInfo ::text').get().split('|')
rooms = house_info[0]
area = house_info[1]
orient = house_info[2]
house_type = house_info[3]
floor = house_info[4]
year = house_info[-2] if '年' in house_info[-2] else 'None'
room_architecture = house_info[-1]
else:
continue
follow = li.css('.followInfo ::text').get().split('/')[0]
issue_time = li.css('.followInfo ::text').get().split('/')[-1]
price = li.css('.totalPrice.totalPrice2 span::text').get()
square_price = li.css('.unitPrice span::text').get()
img = li.css('.lj-lazy ::attr(data-original)').get()
dit = {
'title': title,
'address': address,
'rooms': rooms,
'area': area,
'orient': orient,
'house_type': house_type,
'floor': floor,
'year': year,
'room_type': room_architecture,
'follow': follow,
'issue_time': issue_time,
'price': price,
'square_price': square_price,
'img': img,
}
all_data.append(dit)
pd.DataFrame(all_data).to_excel('lianjia.xlsx', index=False)
以下是爬取十页的数据

保存到数据库
前提需要建库建表 定义字段 (navicat)

python
# 导包
import pymysql
# 连接信息
connect = pymysql.connect(
host='localhost',
user='root',
password='112233',
database='spider',
)
# 拿到游标
cursor = connect.cursor()
# 准备sql语句
sql = 'insert into lj values (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s)'
cursor.executemany(sql, [(title, address, rooms, area, orient, house_type, floor, year, room_architecture,
follow, issue_time, price, square_price, img)])
# 提交事务
connect.commit()
以下是保存成功的数据

保存为Csv文件
python
import csv
csv_writer = csv.DictWriter(open('lianjia.csv', 'w', encoding='utf-8-sig', newline=''), fieldnames=[
'title',
'address',
'rooms',
'area',
'orient',
'house_type',
'floor',
'year',
'room_type',
'follow',
'issue_time',
'price',
'square_price',
'img'
])
csv_writer.writeheader()
dit = {
'title': title,
'address': address,
'rooms': rooms,
'area': area,
'orient': orient,
'house_type': house_type,
'floor': floor,
'year': year,
'room_type': room_architecture,
'follow': follow,
'issue_time': issue_time,
'price': price,
'square_price': square_price,
'img': img,
}
csv_writer.writerow(dit)
二.数据清洗

第一题:
python
# 导包
import numpy as np
import pandas as pd
# 读取数据
df = pd.read_excel('lianjia.xlsx')
# 第一题 可以采用替换 正则表达式提取
# method_1
# df['area'] = df['area'].replace('平米', '', regex=True)
# method_2
df['area'] = df['area'].str.extract(r'(\d+.\d+)', expand=False)
df['area'] = pd.to_numeric(df['area'], errors='ignore').round(2)
df['follow'] = df['follow'].str.extract(r'(\d+)')
df['follow'] = pd.to_numeric(df['follow'], errors='coerce').round(2)
第二题:
python
# 获取现在的日期
current_time = datetime.datetime.now().date()
# 写个函数
def ts_time(str):
# 如果字符串中存在个月 则将月份提取出来 转换成int类型 然后用现在的日期减去天数
# 其它同理
if '个月' in str:
month = int(str.split('个月')[0])
return current_time - datetime.timedelta(days=month * 30)
if '天' in str:
day = int(str.split('天')[0])
return current_time - datetime.timedelta(days=day)
# 应为数据中有 "一年前发布" 的信息 因此得转换成数字1 无法使用map 暂时只能这样处理了
# 如果有好的方法 可以打在评论区
if '年' in str:
year = 1 if '一' in str else str.split('年')[0]
return current_time - datetime.timedelta(days=year * 365)
df['issue_time'] = df['issue_time'].apply(ts_time)
第三题:

目标格式 中|33
考虑到数据存在这两种格式 因此 我们可以通过特殊特征 分别处理
会发现括号是区别这两者的最好方式
如果字符串中存在( 则以楼字分割字符串 此时会返回一个列表 对列表取值就可以得到 楼层的高度
接着以 "|"拼接隔开 接着以共字分割 取后半部分 此时返回的是33层)再以层分割取前面就可以得到数字了
里面用不了正则提取 不然会方便很多
只含有楼层数量的就不解释了
python
def ts_floor(str):
if ')' in str:
return str.split('楼')[0] + '|' + str.split('共')[-1].split('层')[0]
else:
return str.split('层')[0]
df['floor'] = df['floor'].apply(ts_floor)
接着 floor这列的数据就变成了 中|33 这样格式的 我们需要将此以"|"分开 赋值到两个变量中
python
# expand = True 将结果变成DataFrame的形式
# 数据中存在| 的 就分割成两列
# 不存在的则将新的列名 赋值成原来的
def ts_2_floor(str):
if '|' in str:
df[['current_total', 'total_floor']] = df['floor'].str.split('|', expand=True)
return df[['current_total', 'total_floor']]
else:
df['total_floor'] = df['floor']
return df['total_floor']
df['floor'] = df['floor'].apply(ts_2_floor)
# 然后删除之前的floor列名
df.drop(columns=['floor'], inplace=True)
第四题:
python
# 这里我们不适合像题目那样做 直接将年份为空的数据删除就好了
df.dropna(subset='year', inplace=True)
第五题:
python
# 异常值的处理 iqr
lo_iqr = df['price'].quantile(0.25)
up_iqr = df['price'].quantile(0.75)
iqr = up_iqr - lo_iqr
lower_iqr = lo_iqr - 1.5 * iqr
upper_iqr = up_iqr - 1.5 * iqr
# 将超出最小值的数值 用最小的临界值填充
df['price'] = np.where(
df['price'] < lower_iqr,
lower_iqr,
df['price']
)
# 同理
df['price'] = np.where(
df['price'] > upper_iqr,
upper_iqr,
df['price']
)
第六题:
这里注意空格 原数据中字体前后有空格 可以用以下的方法查看
python
print(df['orient'].unique())
或者将原本的空格通过字符串方法去除 然后在映射
python
orientation_map = {
' 南 ': 'S', ' 北 ': 'N', ' 东 ': 'E', ' 西 ': 'W',
' 东南 南 ': 'ES S', '东北': 'NE', ' 西南 ': 'SW', '西北': 'NW',
' 南 北 ': 'S N', ' 北 南 ': 'N S', ' 东南 ': 'E S'
}
df['orient'] = df['orient'].map(orientation_map)
最后保存数据 将清洗完的数据保存到新的文件
python
df.to_excel('cleaned_lianjia.xlsx', index=False)

三.可视化
探索不同户型(rooms)与装修类型(house_type)组合的平均单价
**分析思路:
- 提取户型中的房间数(如"3室1厅" → 3室)
- 按"房间数"和"装修类型"分组计算平均单价
- 用热力图展示不同组合的价格差异**
python
# 导包 起别名
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib import rcParams
import seaborn as sns
# 设置显示的字体为自带的黑体
rcParams['font.family'] = 'SimHei'
# 读取excel数据
df = pd.read_excel('cleaned_lianjia.xlsx')
# 处理数据 使用字符串中的提取匹配的数据
df['room'] = df['rooms'].str.extract(r'(\d+.*)\d+')
# 将多余的字符去掉 使用字符串中的替换方法
df['square_price'] = df['square_price'].str.replace('元/平', '')
df['square_price'] = df['square_price'].str.replace(',', '').astype(int)
# 分组聚合 按照房间和类型分组 计算平均的单价 然后按照高到低重新排列
room_analysis = df.groupby(['room', 'house_type']).agg({
'square_price': 'mean',
}).rename(columns={'square_price': 'mean_square_price'}).sort_values(by=['mean_square_price'], ascending=False)
# 创建画布
plt.figure(figsize=(12, 8))
# 里面的参数上个文章讲了
sns.heatmap(room_analysis[['mean_square_price']], annot=True, annot_kws={'fontsize': 12, 'weight': 'bold'}, fmt='.0f',
cmap='YlOrRd', linewidths=0.5, cbar_kws={'label': '单价(万)', 'shrink': 0.8})
# 设置标题 字体大小 粗细 间距 x y 标签
plt.title('不同户型与装修类型的平均单价热力图', fontsize=16, fontweight='bold', pad=20)
plt.xlabel('平均单价', fontsize=12)
plt.ylabel('户型', fontsize=12)
# 自动调整布局
plt.tight_layout()
# 展示图表
plt.show()

本次的案例分析就到此结束啦 谢谢大家的观看 你的点赞和关注是我更新的最大动力
如果感兴趣的话可以看看我之前的博客