爬虫数据清洗可视化链家房源

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

首先 明确爬取的数据

  • 标题
  • 地址
  • 房屋信息
  • 关注人数 发布时间
  • 价格 每平方米的价格

步骤:

  1. 简单分析界面 对前端的html结构有个了解
  2. 构建请求 模拟浏览器向服务器发送请求
  3. 解析数据 提取我们所需要的数据
  4. 保存数据 对数据进行持久化保存 如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)组合的平均单价

**分析思路:

  1. 提取户型中的房间数(如"3室1厅" → 3室)
  2. 按"房间数"和"装修类型"分组计算平均单价
  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()

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

相关推荐
行走在电子领域的工匠6 小时前
2.2 常用控件
开发语言·python
天才测试猿6 小时前
Selenium三大等待详解
自动化测试·软件测试·python·selenium·测试工具·职场和发展·测试用例
husterlichf7 小时前
pandas___get_dummies详解
pandas
梨轻巧7 小时前
pyside6安装:下载python、配置环境变量、vscode安装和测试pyside6、可能遇到的错误、pycharm 安装pyside6
python
wu_jing_sheng07 小时前
电商销售数据分析实战:从数据挖掘到业务增长
python
voice6707 小时前
西电现代密码学实验一
开发语言·python·密码学
FriendshipT7 小时前
图像生成:PyTorch从零开始实现一个简单的扩散模型
人工智能·pytorch·python
初学小白...8 小时前
反射概述and获得反射对象
开发语言·python
后藤十八里8 小时前
2025python学习笔记Part2
开发语言·python