去年帮朋友找房时,我深刻体会到租房市场的信息不对称问题。同一地段相似房源价格能差出30%,不同朝向的采光差异直接影响居住体验,而这些关键信息往往分散在各个平台。这促使我开发了这套全国租房数据分析系统,用技术手段解决租房决策中的信息盲区。
这套系统基于Python+Django构建,核心价值在于:
技术选型上,我放弃了Scrapy而选择requests+BeautifulSoup组合。虽然Scrapy框架更完善,但对于租房这种反爬不严的垂直站点,轻量级的requests更便于快速迭代。实际测试中,单机每日可稳定采集5万+条房源数据,完全满足分析需求。
后端架构:
前端方案:
技术选型心得:
曾尝试用Flask+SQLAlchemy方案,但Django自带的Admin模块节省了60%后台开发时间。ECharts的配置项需特别注意resize事件监听,否则响应式布局下会出现图表错位。
系统数据处理流程分为四个关键阶段:
数据采集层:
data-compass属性的朝向数据数据存储层:
python复制# 房源核心字段设计
class House(models.Model):
title = models.CharField(max_length=200) # 标题含户型信息
price = models.CharField(max_length=50) # 价格可能是区间
area = models.FloatField() # 面积(平方米)
orientation = models.CharField(max_length=10) # 朝向
floor = models.CharField(max_length=20) # 楼层信息
address = models.TextField() # 详细地址
community = models.CharField(max_length=50) # 小区名称
pattern = models.CharField(max_length=20) # 户型(如2室1厅)
分析计算层:
re.match(r'(\d+)层', floor_str)pd.cut(prices, bins=[0,1000,2000,3000,4000,99999])可视化展示层:
贝壳租房的反爬策略相对温和,核心在于模拟正常用户行为:
python复制def fetch_house_list(city='bj', page=1):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36...',
'Referer': f'https://{city}.zu.ke.com/zufang/'
}
url = f'https://{city}.zu.ke.com/zufang/pg{page}'
try:
resp = requests.get(url, headers=headers, timeout=10)
soup = BeautifulSoup(resp.text, 'html.parser')
# 解析房源列表
houses = soup.select('div[class^="content__list--item"]')
for house in houses:
title = house.select_one('p.content__list--item--title').text.strip()
price = house.select_one('span.content__list--item-price').text
# 其他字段解析...
yield {
'title': title,
'price': re.sub(r'\D', '', price), # 提取纯数字
# 其他字段...
}
except Exception as e:
logging.error(f"爬取失败: {url} - {str(e)}")
反爬应对技巧:
fake_useragent库动态生成User-Agent原始数据存在三大典型问题需要处理:
价格标准化:
python复制def normalize_price(price_str):
if '-' in price_str: # 处理"2000-3000"形式
low, high = map(int, price_str.split('-'))
return (low + high) / 2
return int(re.search(r'\d+', price_str).group())
面积单位统一:
python复制def clean_area(area_str):
try:
return float(area_str.replace('㎡', ''))
except:
return None # 标记为缺失值
楼层信息提取:
python复制def parse_floor(floor_str):
match = re.search(r'(\d+)层', floor_str)
return int(match.group(1)) if match else None
ECharts配置示例(价格分布柱状图):
javascript复制function initPriceChart(data) {
const chart = echarts.init(document.getElementById('price-chart'));
const option = {
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: ['<1000', '1000-2000', '2000-3000', '3000-4000', '>4000']
},
yAxis: { type: 'value' },
series: [{
data: data.counts,
type: 'bar',
itemStyle: {
color: function(params) {
return ['#5470c6','#91cc75','#fac858','#ee6666','#73c0de'][params.dataIndex];
}
}
}]
};
chart.setOption(option);
window.addEventListener('resize', function() {
chart.resize();
});
}
词云生成优化:
python复制def generate_wordcloud(text):
# 加载停用词
stopwords = set(open('stopwords.txt').read().splitlines())
# 添加租房领域特定停用词
stopwords.update(['出租', '房源', '平米', '装修'])
wc = WordCloud(
font_path='simhei.ttf',
background_color='white',
max_words=200,
stopwords=stopwords,
collocations=False # 避免重复统计词组
)
seg_list = jieba.cut(text)
wc.generate(' '.join(seg_list))
wc.to_file('wordcloud.png')
通过pearson相关系数计算楼层与价格的相关性:
python复制def floor_price_correlation():
data = House.objects.all().values('floor', 'price')
df = pd.DataFrame(list(data))
df['floor_num'] = df['floor'].apply(parse_floor)
df['price_num'] = df['price'].apply(normalize_price)
# 计算相关系数
corr = df['floor_num'].corr(df['price_num'], method='pearson')
# 按楼层分组统计
bins = [0, 10, 20, 99] # 低层/中层/高层
labels = ['低楼层', '中楼层', '高楼层']
df['level'] = pd.cut(df['floor_num'], bins=bins, labels=labels)
grouped = df.groupby('level')['price_num'].mean()
return {
'correlation': round(corr, 2),
'level_prices': grouped.to_dict()
}
计算各户型的每平米价格,找出性价比最优的户型:
python复制def best_value_patterns(top_n=5):
queryset = House.objects.exclude(area__isnull=True).exclude(price__isnull=True)
df = pd.DataFrame(list(queryset.values('pattern', 'price', 'area')))
df['price_num'] = df['price'].apply(normalize_price)
df['area_num'] = df['area']
df = df[df['area_num'] > 0] # 过滤异常数据
# 计算每平米均价
df['price_per_sqm'] = df['price_num'] / df['area_num']
pattern_stats = df.groupby('pattern').agg({
'price_per_sqm': 'mean',
'price_num': ['count', 'mean']
})
# 筛选至少有20套房源的户型
pattern_stats = pattern_stats[pattern_stats[('price_num','count')] >= 20]
return pattern_stats.nsmallest(top_n, ('price_per_sqm','mean'))
推荐使用Docker Compose编排服务:
dockerfile复制# docker-compose.yml
version: '3'
services:
web:
build: .
command: gunicorn --bind :8000 --workers 4 project.wsgi
volumes:
- .:/code
ports:
- "8000:8000"
depends_on:
- redis
- db
redis:
image: redis:alpine
db:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: yourpassword
MYSQL_DATABASE: housing
volumes:
- db_data:/var/lib/mysql
volumes:
db_data:
性能优化要点:
django-debug-toolbar找出慢查询对于百万级数据集,采用以下优化策略:
数据库层面:
python复制# 添加复合索引
class Meta:
indexes = [
models.Index(fields=['city', 'price']),
models.Index(fields=['area', 'price']),
]
pandas优化:
python复制# 使用category类型节省内存
df['orientation'] = df['orientation'].astype('category')
# 使用eval加速计算
df.eval('price_per_sqm = price_num / area_num', inplace=True)
异步任务处理:
python复制# Celery任务示例
@shared_task
def async_analysis_task(city):
queryset = House.objects.filter(city=city)
df = pd.DataFrame(list(queryset.values()))
# 执行耗时分析...
return result.to_dict()
现象:连续请求后返回403状态码
解决方案:
Referer和Cookie信息pyautogui.moveTo(x, y, duration=0.5)现象:同时渲染多个图表时页面卡顿
优化方案:
Intersection Observer API实现懒加载javascript复制const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadChart(entry.target);
observer.unobserve(entry.target);
}
});
});
document.querySelectorAll('.chart-container').forEach(el => {
observer.observe(el);
});
现象:部分地址无法正确解析为经纬度
处理流程:
python复制def geocode_address(address):
try:
params = {'address': address, 'key': 'your_amap_key'}
resp = requests.get('https://restapi.amap.com/v3/geocode/geo', params=params)
loc = resp.json()['geocodes'][0]['location']
return tuple(map(float, loc.split(',')))
except:
# 提取主干道名称重试
main_road = re.search(r'.*?(大道|路|街)', address)
if main_road:
return geocode_address(main_road.group())
return (None, None)
这套系统在实际运行中积累了30万+条房源数据,通过分析发现了一些有趣现象:比如北京朝阳区高层住宅的价格溢价达到12%,而南北朝向的房源平均比东西朝向贵8%。这些洞察不仅对租房者有直接参考价值,也为房产投资提供了数据支撑。