用Python+Flask+随机森林打造二手房房价预测神器(附完整链家爬虫代码)

每次看到朋友为买房焦头烂额,在各大平台反复对比价格,却总觉得心里没底时,我就想,能不能用技术手段让这个过程更清晰、更理性一些?房价的影响因素错综复杂,地段、面积、楼层、装修,甚至小区的绿化率都可能成为价格的变量。单纯依靠经验判断,难免会有偏差。作为一名开发者,我习惯用数据和模型来解决问题。于是,我决定动手构建一个属于自己的房价预测工具,它不仅能从公开渠道抓取真实的二手房数据,还能通过机器学习模型学习这些数据背后的规律,最终提供一个直观的预测界面。这不仅仅是一个技术项目,更像是一个将数据思维应用于现实生活决策的实践。

整个系统涉及多个环节:数据从哪里来、怎么存、如何分析、怎样呈现结果。我选择了Python作为主力语言,因为它拥有极其丰富的生态库,能一站式解决从爬虫到机器学习再到Web服务的所有需求。Flask框架的轻量灵活,非常适合快速搭建一个功能完备的后端API;而随机森林算法,以其出色的抗过拟合能力和对复杂非线性关系的捕捉能力,成为处理房价这类多因素回归问题的理想选择。下面,我就把自己从零搭建这个系统的完整思路、关键代码和踩过的坑,毫无保留地分享给你。

1. 数据基石:高效、稳定的链家爬虫实战

任何预测系统的根基都是数据。没有高质量、足量的数据,再精妙的模型也是空中楼阁。我们选择链家作为数据源,因为它覆盖广、信息相对规范。但编写一个健壮的爬虫,远不止发送一个HTTP请求那么简单。

1.1 爬虫策略与反爬应对

直接对链家列表页进行暴力抓取,很快就会触发反爬机制,导致IP被封。我们必须模拟得更像一个真实的用户。核心策略包括:

  • 请求头伪装:务必设置完整的User-AgentReferer等头部信息。
  • 请求间隔:在请求之间加入随机延时,避免请求频率过高。
  • 代理IP池:对于大规模抓取,准备一个可靠的代理IP池是必要的,但作为个人项目或小规模抓取,通过控制请求频率和模拟浏览器行为通常已足够。
  • 页面解析:链家的页面结构可能会变动,所以解析代码需要有一定的容错性,不能依赖于绝对固定的CSS选择器路径。

一个基础的请求会话设置可以这样写:

import requests
import time
import random
from fake_useragent import UserAgent

class LianjiaSpider:
    def __init__(self):
        self.session = requests.Session()
        self.ua = UserAgent()
        # 初始化会话,设置一些公共头部
        self.session.headers.update({
            'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
            'Accept-Language': 'zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2',
            'Connection': 'keep-alive',
        })

    def get_random_delay(self):
        """生成一个随机的请求延迟,通常在1到3秒之间"""
        return random.uniform(1, 3)

    def fetch_page(self, url):
        """获取单个页面HTML"""
        headers = {'User-Agent': self.ua.random}
        time.sleep(self.get_random_delay()) # 关键:每次请求前等待
        try:
            resp = self.session.get(url, headers=headers, timeout=10)
            resp.raise_for_status() # 检查HTTP错误
            # 可以在这里检查响应内容是否包含反爬提示,如“访问过于频繁”
            if "验证" in resp.text or "频繁" in resp.text:
                print(f"可能触发了反爬机制: {url}")
                return None
            return resp.text
        except requests.exceptions.RequestException as e:
            print(f"请求失败 {url}: {e}")
            return None

提示:在实际部署爬虫时,请务必遵守目标网站的robots.txt协议,并尊重网站的服务条款。将抓取频率控制在合理范围,避免对目标网站服务器造成不必要的压力。

1.2 数据解析与结构化存储

获取到HTML后,下一步是从中提取出我们关心的结构化信息。这里使用BeautifulSouplxml解析器,效率很高。我们需要提取的字段通常包括:

  • 房屋标题
  • 所在小区
  • 行政区/位置
  • 总价与单价
  • 房屋户型(几室几厅)
  • 建筑面积
  • 房屋朝向
  • 装修情况(精装、简装等)
  • 楼层信息(高/中/低层)
  • 建造年代

解析代码需要细致处理缺失值。例如,有些房源可能不公布建造年代,我们的程序应该能处理这种情况,而不是直接崩溃。

from bs4 import BeautifulSoup
import re

def parse_house_detail(html, url):
    """解析单个房源详情页的HTML"""
    if not html:
        return None

    soup = BeautifulSoup(html, 'lxml')
    house_info = {}

    # 提取标题
    title_tag = soup.find('h1', class_='main')
    house_info['title'] = title_tag.get_text(strip=True) if title_tag else ''

    # 提取总价和单价 - 这里的选择器是示例,实际需要根据页面结构调整
    total_price_tag = soup.find('span', class_='total')
    unit_price_tag = soup.find('span', class_='unitPriceValue')
    house_info['total_price'] = float(re.sub(r'[^\d.]', '', total_price_tag.text)) if total_price_tag else 0.0
    house_info['unit_price'] = int(re.sub(r'[^\d.]', '', unit_price_tag.text)) if unit_price_tag else 0

    # 提取关键属性,它们通常在一个属性列表里
    intro_items = soup.find_all('li', class_='row')
    for item in intro_items:
        text = item.get_text(strip=True)
        if '房屋户型' in text:
            house_info['layout'] = text.split(':')[-1]
        elif '建筑面积' in text:
            area_text = text.split(':')[-1]
            house_info['area'] = float(re.search(r'[\d.]+', area_text).group())
        # ... 类似地处理其他属性

    house_info['url'] = url
    return house_info

抓取到的数据需要持久化存储。对于这类结构化数据,关系型数据库是自然的选择。我使用SQLAlchemy这个ORM库来操作数据库,它能让我们的代码更清晰,也便于后续与Flask集成。

from sqlalchemy import create_engine, Column, Integer, String, Float, DateTime
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from datetime import datetime

Base = declarative_base()

class HouseListing(Base):
    __tablename__ = 'house_listings'
    id = Column(Integer, primary_key=True)
    title = Column(String(255))
    district = Column(String(50)) # 行政区
    community = Column(String(100)) # 小区
    total_price = Column(Float) # 总价,单位万元
    unit_price = Column(Float) # 单价,单位元/平米
    layout = Column(String(50)) # 户型,如‘3室2厅’
    area = Column(Float) # 面积,平米
    orientation = Column(String(20)) # 朝向
    renovation = Column(String(20)) # 装修情况
    floor = Column(String(50)) # 楼层
    year_built = Column(Integer) # 建造年代
    crawl_time = Column(DateTime, default=datetime.utcnow)

# 创建数据库连接和表
engine = create_engine('sqlite:///houses.db') # 也可替换为MySQL或PostgreSQL连接字符串
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)

将爬虫、解析器和数据存储模块组合起来,就形成了一个完整的数据采集流水线。你可以设定要抓取的城市、区域和页面数,让爬虫在后台安静工作,逐步构建起你自己的二手房数据集。

2. 数据炼金术:从原始数据到模型特征

原始数据就像未经雕琢的玉石,直接丢给模型效果往往不佳。数据预处理和特征工程是机器学习项目中至关重要,甚至是最耗时的一步。这一步做得好,模型性能提升会非常明显。

2.1 数据清洗与异常值处理

首先,我们需要将爬取的数据加载到Pandas DataFrame中进行审视和清洗。

import pandas as pd
from sqlalchemy import create_engine

# 从数据库加载数据
engine = create_engine('sqlite:///houses.db')
df = pd.read_sql_table('house_listings', engine)

print(f"原始数据形状: {df.shape}")
print(df.info())
print(df.describe())

常见的清洗操作包括:

  • 处理缺失值:对于建造年代这类可能缺失的字段,我们可以用该小区的平均建造年代或中位数来填充,或者如果缺失比例很高,考虑删除该特征。
  • 纠正数据类型:确保单价、总价、面积等都是数值类型。
  • 处理异常值:单价或总价极高或极低的记录可能是录入错误或特殊房源(如别墅、地下室),需要根据业务逻辑进行筛选或修正。一个简单的方法是使用分位数进行截断。
# 删除关键字段缺失的记录
df_clean = df.dropna(subset=['total_price', 'unit_price', 'area', 'district'])

# 处理异常值:假设我们认为单价低于1万或高于20万/平米的记录为异常
Q1 = df_clean['unit_price'].quantile(0.01)
Q3 = df_clean['unit_price'].quantile(0.99)
df_clean = df_clean[(df_clean['unit_price'] >= Q1) & (df_clean['unit_price'] <= Q3)]

# 同样处理面积异常(例如,小于20平米或大于500平米的住宅)
df_clean = df_clean[(df_clean['area'] >= 20) & (df_clean['area'] <= 500)]

2.2 特征工程:挖掘数据深层价值

原始特征需要转换才能被模型更好地理解。这一步是体现数据科学家“手艺”的地方。

  • 数值特征标准化/归一化:像面积、建造年代这类数值特征,量纲不同,直接使用可能影响模型。我们通常进行标准化处理,使其均值为0,方差为1。
  • 类别特征编码:行政区、装修情况、朝向等都是类别特征。对于像行政区这种无序类别,使用独热编码;对于像装修情况(可能隐含“毛坯<简装<精装<豪装”的序关系),可以考虑使用标签编码或独热编码。
  • 特征构造:这是提升模型性能的关键。我们可以从现有特征中衍生出新特征。
    • 房龄:用当前年份减去建造年代。
    • 房间数:从“户型”字符串中提取出卧室数量。
    • 是否为中层:从“楼层”信息中判断,因为中层往往最受欢迎。
    • 单价与区域均价比:计算每个房源单价与其所在行政区平均单价的比值,反映其相对贵贱。
from sklearn.preprocessing import StandardScaler, OneHotEncoder
from sklearn.compose import ColumnTransformer
import numpy as np

# 假设df_clean是我们的清洗后DataFrame
# 构造新特征
current_year = datetime.now().year
df_clean['house_age'] = current_year - df_clean['year_built'].fillna(df_clean['year_built'].median())
df_clean['room_count'] = df_clean['layout'].str.extract(r'(\d+)室').astype(float) # 提取卧室数

# 定义特征列
numeric_features = ['area', 'house_age', 'room_count'] # 数值特征
categorical_features = ['district', 'renovation', 'orientation'] # 类别特征

# 创建预处理管道
preprocessor = ColumnTransformer(
    transformers=[
        ('num', StandardScaler(), numeric_features),
        ('cat', OneHotEncoder(handle_unknown='ignore', sparse_output=False), categorical_features)
    ])

# 应用预处理
X_processed = preprocessor.fit_transform(df_clean)

预处理管道(ColumnTransformer)非常好用,它能确保我们在训练集上拟合的转换器(如标准化器、编码器)被原封不动地应用到未来的新数据(如用户在前端输入的预测数据)上,保证数据尺度的一致性。

3. 模型核心:随机森林回归原理与调优

有了干净、丰富的特征,我们就可以开始训练模型了。为什么选择随机森林?因为它几乎是一个“开箱即用”的优秀算法,对数据预处理的要求相对宽松,不易过拟合,并能给出特征重要性排序。

3.1 随机森林算法精要

随机森林属于集成学习中的Bagging方法。它的核心思想是“三个臭皮匠,顶个诸葛亮”:

  1. 自助采样:从原始训练集中有放回地随机抽取N个样本,生成多个不同的子训练集。
  2. 随机特征:对于每棵决策树的每个节点分裂,不是从所有特征中选择最优特征,而是从一个随机子集中选择。这增加了树的多样性。
  3. 投票/平均:对于分类问题,所有树投票决定最终类别;对于回归问题(如房价预测),所有树的预测结果取平均值作为最终输出。

这种机制带来了两大好处:第一,通过平均多棵树的预测,有效降低了模型的方差,提升了泛化能力;第二,随机特征子集的引入,降低了树之间的相关性,让集成的效果更好。

scikit-learn中,使用随机森林回归器非常简单:

from sklearn.ensemble import RandomForestRegressor
from sklearn.model_selection import train_test_split

# 假设X_processed是处理后的特征矩阵,y是目标变量(比如总价或单价)
y = df_clean['total_price'].values

X_train, X_test, y_train, y_test = train_test_split(X_processed, y, test_size=0.2, random_state=42)

# 初始化一个随机森林回归模型
rf_model = RandomForestRegressor(n_estimators=100, # 森林中树的数量
                                  max_depth=None, # 树的最大深度,None表示不限制
                                  min_samples_split=2,
                                  min_samples_leaf=1,
                                  random_state=42,
                                  n_jobs=-1) # 使用所有CPU核心并行训练

# 训练模型
rf_model.fit(X_train, y_train)

# 在测试集上评估
train_score = rf_model.score(X_train, y_train)
test_score = rf_model.score(X_test, y_test)
print(f"训练集R²分数: {train_score:.4f}")
print(f"测试集R²分数: {test_score:.4f}")

R²分数越接近1,说明模型对数据的解释能力越强。我们更关心测试集的分数,它反映了模型对未知数据的预测能力。

3.2 模型调优与特征重要性分析

默认参数不一定是最优的。我们可以使用网格搜索随机搜索来寻找最佳超参数组合。常用的调优参数包括:

  • n_estimators:树的数量,越多通常效果越好,但计算成本也越高。
  • max_depth:树的最大深度,控制模型的复杂度,防止过拟合。
  • min_samples_split:内部节点再划分所需最小样本数。
  • min_samples_leaf:叶子节点最少样本数。
from sklearn.model_selection import GridSearchCV

# 定义参数网格
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [10, 20, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

# 创建网格搜索对象
grid_search = GridSearchCV(estimator=RandomForestRegressor(random_state=42),
                           param_grid=param_grid,
                           cv=5, # 5折交叉验证
                           scoring='r2',
                           n_jobs=-1,
                           verbose=1)

grid_search.fit(X_train, y_train)

print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳交叉验证分数: {grid_search.best_score_:.4f}")
best_model = grid_search.best_estimator_

训练好的随机森林模型还有一个宝贵产出:特征重要性。它可以告诉我们哪些因素对房价的影响最大。

import pandas as pd
import matplotlib.pyplot as plt

# 获取特征重要性
feature_names = numeric_features + list(preprocessor.named_transformers_['cat'].get_feature_names_out(categorical_features))
importances = best_model.feature_importances_

# 组合成DataFrame并排序
feat_imp_df = pd.DataFrame({
    'feature': feature_names,
    'importance': importances
}).sort_values('importance', ascending=False)

print(feat_imp_df.head(10))

# 可视化
plt.figure(figsize=(10,6))
plt.barh(feat_imp_df['feature'].head(15), feat_imp_df['importance'].head(15))
plt.xlabel('Feature Importance')
plt.gca().invert_yaxis() # 重要性高的在上方
plt.tight_layout()
plt.show()

你可能会发现,“面积”、“行政区_某某区”、“房龄”等特征排在重要性前列,这与我们的常识是吻合的。这个分析结果本身也具有很大的业务洞察价值。

4. 系统集成:用Flask构建可交互的Web应用

模型训练好了,但它还只是一个躺在Jupyter Notebook里的“玩具”。我们需要把它包装成一个用户可以访问和使用的服务。Flask微框架的简洁性在这里大放异彩。

4.1 Flask后端API设计

后端核心是提供两个主要功能:1. 接收前端传来的房屋特征,返回预测价格;2. 提供一些可视化数据。我们使用RESTful风格设计API。

首先,我们需要将训练好的模型和预处理管道保存下来,以便在Web服务中加载。

import joblib

# 保存预处理管道和最佳模型
joblib.dump(preprocessor, 'models/house_price_preprocessor.pkl')
joblib.dump(best_model, 'models/house_price_rf_model.pkl')
print("模型和预处理管道已保存。")

然后,创建Flask应用。项目结构可以这样组织:

house_price_predictor/
├── app.py              # Flask主应用文件
├── models/             # 存放训练好的模型文件
│   ├── house_price_preprocessor.pkl
│   └── house_price_rf_model.pkl
├── static/             # 静态文件(CSS, JS)
│   └── ...
├── templates/          # Jinja2模板
│   └── index.html
└── requirements.txt    # 项目依赖

app.py的核心内容:

from flask import Flask, request, jsonify, render_template
import joblib
import numpy as np
import pandas as pd

app = Flask(__name__)

# 在应用启动时加载模型和预处理管道
try:
    preprocessor = joblib.load('models/house_price_preprocessor.pkl')
    model = joblib.load('models/house_price_rf_model.pkl')
    print("模型加载成功!")
except Exception as e:
    print(f"模型加载失败: {e}")
    preprocessor = None
    model = None

@app.route('/')
def index():
    """渲染主页面"""
    return render_template('index.html')

@app.route('/api/predict', methods=['POST'])
def predict():
    """预测API端点"""
    if preprocessor is None or model is None:
        return jsonify({'error': '模型未就绪'}), 503

    try:
        # 从POST请求的JSON中获取数据
        data = request.get_json()
        # 假设前端传来的数据格式如下:
        # {
        #   "area": 89.5,
        #   "district": "浦东",
        #   "renovation": "精装",
        #   "orientation": "南",
        #   "year_built": 2010,
        #   "layout": "3室2厅"
        # }
        # 我们需要将其构造成与训练时相同的DataFrame格式
        current_year = 2025 # 根据实际情况调整
        input_dict = {
            'area': [float(data.get('area', 0))],
            'district': [data.get('district', '')],
            'renovation': [data.get('renovation', '')],
            'orientation': [data.get('orientation', '')],
            'year_built': [int(data.get('year_built', 2000))],
            'layout': [data.get('layout', '')]
        }
        input_df = pd.DataFrame(input_dict)
        # 构造衍生特征(必须与训练时完全一致)
        input_df['house_age'] = current_year - input_df['year_built']
        input_df['room_count'] = input_df['layout'].str.extract(r'(\d+)室').astype(float).fillna(2) # 默认2室

        # 应用预处理管道
        processed_input = preprocessor.transform(input_df)

        # 进行预测
        prediction = model.predict(processed_input)[0]

        # 可以返回预测值,也可以返回一个区间(例如,基于模型在验证集上的误差估计)
        response = {
            'predicted_total_price': round(float(prediction), 2),
            'predicted_unit_price': round(float(prediction) / input_dict['area'][0], 2) if input_dict['area'][0] > 0 else 0,
            'status': 'success'
        }
        return jsonify(response)

    except Exception as e:
        return jsonify({'error': f'预测过程中发生错误: {str(e)}'}), 400

if __name__ == '__main__':
    # 生产环境应使用Gunicorn等WSGI服务器,而不是Flask自带的开发服务器
    app.run(host='0.0.0.0', port=5000, debug=True)

4.2 前端交互与可视化

前端的目标是提供一个干净、易用的界面,让用户输入房屋信息,并直观地看到预测结果。我们可以用简单的HTML、CSS和JavaScript(配合一点jQuery或Vue.js)来实现。这里使用原生JavaScript和Fetch API为例。

templates/index.html的核心部分:

<!DOCTYPE html>
<html>
<head>
    <title>二手房房价预测系统</title>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
    <style>
        /* 基础样式省略 */
        .input-group { margin-bottom: 15px; }
        .result-box { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 5px; }
    </style>
</head>
<body>
    <div class="container">
        <h1>二手房价格预测工具</h1>
        <p>输入房屋信息,获取基于机器学习模型的房价预测。</p>

        <div class="input-section">
            <div class="input-group">
                <label>建筑面积 (㎡):</label>
                <input type="number" id="area" step="0.1" placeholder="例如:89.5">
            </div>
            <div class="input-group">
                <label>行政区:</label>
                <select id="district">
                    <option value="浦东">浦东新区</option>
                    <option value="闵行">闵行区</option>
                    <!-- 其他区 -->
                </select>
            </div>
            <!-- 更多输入字段:装修、朝向、建造年代、户型等 -->
            <button onclick="predictPrice()">开始预测</button>
        </div>

        <div id="result" class="result-box" style="display:none;">
            <h3>预测结果</h3>
            <p>预测总价: <strong id="predicted-total">--</strong> 万元</p>
            <p>预测单价: <strong id="predicted-unit">--</strong> 元/㎡</p>
        </div>

        <div id="chart" style="width: 100%; height: 400px;"></div>
    </div>

    <script>
        function predictPrice() {
            const inputData = {
                area: document.getElementById('area').value,
                district: document.getElementById('district').value,
                renovation: document.getElementById('renovation').value,
                orientation: document.getElementById('orientation').value,
                year_built: document.getElementById('year_built').value,
                layout: document.getElementById('layout').value
            };

            fetch('/api/predict', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(inputData)
            })
            .then(response => response.json())
            .then(data => {
                if (data.status === 'success') {
                    document.getElementById('predicted-total').textContent = data.predicted_total_price;
                    document.getElementById('predicted-unit').textContent = data.predicted_unit_price;
                    document.getElementById('result').style.display = 'block';
                } else {
                    alert('预测失败: ' + (data.error || '未知错误'));
                }
            })
            .catch(error => {
                console.error('Error:', error);
                alert('网络请求失败');
            });
        }

        // 使用ECharts初始化一个展示各行政区均价的可视化图表
        // 这部分需要后端提供一个/api/avg_price_by_district的接口来提供数据
        // 代码略...
    </script>
</body>
</html>

为了让应用更完整,你还可以添加数据大屏页面,使用ECharts绘制各行政区房价分布、房价随时间变化趋势、不同户型均价对比等图表。这些图表的数据可以通过额外的Flask路由从数据库查询聚合后,以JSON格式提供给前端。

5. 部署上线与性能考量

开发完成后的系统,最终需要部署到服务器上才能对外提供服务。对于个人项目或小规模应用,有许多云服务商提供免费的容器或应用托管服务。

5.1 使用Docker容器化

Docker能确保应用在任何环境下的运行一致性。编写一个Dockerfile

# 使用官方Python轻量级镜像
FROM python:3.9-slim

# 设置工作目录
WORKDIR /app

# 复制依赖文件并安装
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple

# 复制应用代码
COPY . .

# 暴露端口
EXPOSE 5000

# 定义启动命令
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]

对应的requirements.txt需要列出所有依赖:

Flask==2.3.3
gunicorn==21.2.0
pandas==2.0.3
scikit-learn==1.3.0
joblib==1.3.2
requests==2.31.0
beautifulsoup4==4.12.2
lxml==4.9.3
fake-useragent==1.4.0
SQLAlchemy==2.0.19

然后构建镜像并运行容器:

docker build -t house-price-predictor .
docker run -d -p 5000:5000 --name predictor-app house-price-predictor

5.2 性能优化与监控

当用户量增加,你需要关注性能。

  • 模型预测优化:随机森林预测本身是并行的(n_jobs=-1),但对于Web服务,要确保模型加载一次后常驻内存。Flask应用通常以多进程/多线程方式运行,需要注意模型对象是否支持并发读取(scikit-learn模型一般没问题)。
  • 数据库优化:如果可视化大屏需要频繁查询大量历史数据,考虑对数据库表建立索引,或者使用更快的缓存(如Redis)来存储聚合结果。
  • 异步任务:爬虫任务非常耗时,不应该阻塞Web请求。可以使用Celery + Redis将爬虫任务转为后台异步执行,通过另一个接口查询任务状态和结果。
  • 日志记录:为应用添加完善的日志记录,记录每一次预测请求的参数和结果、错误信息等,便于后期分析和模型迭代。
import logging
from logging.handlers import RotatingFileHandler

# 在Flask应用中配置日志
handler = RotatingFileHandler('app.log', maxBytes=10000, backupCount=3)
handler.setLevel(logging.INFO)
app.logger.addHandler(handler)

@app.route('/api/predict', methods=['POST'])
def predict():
    app.logger.info(f"收到预测请求: {request.get_json()}")
    # ... 预测逻辑
    app.logger.info(f"预测完成,结果: {prediction}")
    return jsonify(response)

整个项目从爬虫到模型再到Web应用,涉及了数据处理、机器学习、软件工程等多个领域的知识。最难的部分往往不是写代码,而是处理真实数据中的各种“脏乱差”,以及将各个模块无缝衔接起来。我建议你在动手时,先聚焦核心链路:爬取少量数据 -> 训练一个简单模型 -> 做出一个能预测的Web页面。把这个最小可行产品跑通后,再逐步迭代,加入数据清洗、特征工程、模型调优、前端美化、异步任务等高级功能。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐