在二手交易场景中,闲鱼商品定价是核心痛点——卖家难以把握合理市场价(定价过高无人问津,过低亏损),买家缺乏价格参考(担心买贵)。本文将实现“闲鱼商品自动化抓取→多维度数据清洗→特征工程→价格预测模型训练→可视化定价参考”的完整流程,技术栈兼顾实用性与易落地性(Selenium+Pandas+XGBoost+Matplotlib),支持自定义商品关键词(如“iPhone 13”“Switch游戏机”),最终输出精准的市场价格预测结果和定价建议。
一、核心亮点(没时间看全文可直接拿)
自动化全流程:从商品抓取到价格预测一键运行,无需手动干预,支持批量抓取同类商品数据;多维度特征覆盖:抓取商品标题、成色、原价、二手价、销量、浏览量、卖家信用、商品描述等10+核心特征,提升预测精度;高精度预测模型:基于XGBoost算法(比线性回归误差降低30%),支持自定义商品参数(如成色、内存)预测个性化价格;反爬适配:模拟真实用户操作(滑动验证、随机停留、UA轮换),突破闲鱼反爬限制,稳定抓取海量商品;可视化定价参考:生成特征重要性图表(如“内存”对手机价格影响最大)、价格分布直方图,提供直观定价建议;模型可复用:训练后的模型保存为文件,下次无需重新爬取数据,直接输入商品参数即可预测。
二、技术方案:爬虫+预测模型架构设计
2.1 核心流程拆解
整个系统分为5大模块,从数据获取到价格输出形成闭环:
graph TD
A[关键词输入(如“iPhone 13”)] -->|Selenium模拟浏览器| B[闲鱼商品抓取模块]
B -->|提取10+特征| C[数据清洗与预处理模块]
C -->|特征编码/文本提取| D[价格预测模型模块(XGBoost)]
D -->|模型训练/加载| E[定价参考输出模块(可视化+预测结果)]
爬虫模块:用Selenium模拟浏览器,处理登录、滑动验证,动态加载商品列表,提取核心特征;数据清洗模块:处理缺失值、异常值(如“9.9元iPhone”)、格式标准化(如价格去“¥”符号);特征工程模块:文本特征提取(标题/描述关键词)、分类特征编码(成色/卖家信用)、数值特征归一化;预测模型模块:以XGBoost为核心模型,线性回归为基准,对比提升预测精度;输出模块:生成预测价格、定价区间、特征重要性图表、商品价格分布可视化。
2.2 技术栈选型(易落地+高适配)
| 模块 | 工具/库 | 核心作用 | 选型理由 |
|---|---|---|---|
| 动态爬虫 | Selenium 4.15+ | 模拟浏览器操作,突破闲鱼反爬,抓取动态商品 | 闲鱼为React动态渲染,requests无法获取异步数据,Selenium模拟真实用户更稳定 |
| 验证码处理 | ddddocr 1.5+ | 识别滑动验证缺口,自动完成验证 | 轻量开源,无需付费API,识别准确率达90%+ |
| 数据处理 | Pandas 2.2+ | 数据清洗、特征整理、格式转换 | 结构化数据处理利器,适配后续模型输入 |
| 文本特征提取 | Scikit-learn 1.3+ | TF-IDF提取标题/描述关键词特征 | 成熟稳定,快速将文本转换为模型可识别的数值 |
| 预测模型 | XGBoost 2.0+ | 核心价格预测模型(非线性拟合能力强) | 比线性回归、随机森林更适合二手商品价格预测,抗过拟合 |
| 可视化 | Matplotlib/Seaborn | 特征重要性、价格分布、预测结果可视化 | 图表定制性强,直观展示模型效果和定价逻辑 |
| 模型保存 | Joblib 1.3+ | 保存训练后的模型,避免重复训练 | 轻量高效,支持快速加载模型 |
2.3 核心特征设计(影响二手价格的关键因素)
| 特征类型 | 具体特征 | 特征说明 | 处理方式 |
|---|---|---|---|
| 数值特征 | 原价、浏览量、收藏量、发布天数 | 直接影响价格的量化指标 | 归一化处理(避免量纲差异影响模型) |
| 分类特征 | 成色(全新/九成新/八成新) | 二手商品核心定价因素 | 标签编码(如全新=5,九成新=4…) |
| 分类特征 | 卖家信用(极好/良好/一般) | 影响商品可信度,间接影响定价 | 标签编码(极好=3,良好=2,一般=1) |
| 文本特征 | 标题关键词(如“国行”“128G”) | 提取商品核心属性(型号、内存、功能) | TF-IDF转换为数值特征 |
| 布尔特征 | 是否支持包邮、是否走验货宝 | 增值服务对价格的影响 | 0/1编码(支持=1,不支持=0) |
三、全流程实操:爬虫+预测模型落地
3.1 第一步:环境搭建(5分钟搞定)
推荐Python 3.9+,执行命令安装核心依赖:
# 核心依赖:爬虫+数据处理+模型+可视化
pip install selenium==4.15.2 pandas==2.2.2 scikit-learn==1.3.2 xgboost==2.0.3 matplotlib==3.8.4 seaborn==0.13.2 joblib==1.3.2 ddddocr==1.5.0 python-dotenv==1.0.1
额外准备:
下载Chrome浏览器对应版本的ChromeDriver(下载地址),放入项目目录;闲鱼账号(用于登录,建议用小号,避免主号风控);在项目目录创建 文件,写入账号密码(避免硬编码):
.env
XIANYU_USERNAME=你的闲鱼账号(手机号)
XIANYU_PASSWORD=你的闲鱼密码
3.2 第二步:核心代码实现(可直接复制运行)
创建 文件,包含爬虫、清洗、模型全流程代码:
xianyu_price_predict.py
import os
import time
import random
import re
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from dotenv import load_dotenv
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
import ddddocr
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from xgboost import XGBRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
import joblib
# -------------------------- 1. 配置参数(可按需修改)--------------------------
load_dotenv()
# 闲鱼账号密码
USERNAME = os.getenv("XIANYU_USERNAME")
PASSWORD = os.getenv("XIANYU_PASSWORD")
# 目标商品关键词(可自定义,如“Switch 日版”“华为Mate40”)
TARGET_KEYWORD = "iPhone 13"
# 抓取商品数量(建议≥500,数据量越大模型越准)
CRAWL_COUNT = 600
# 保存路径
RAW_DATA_PATH = "闲鱼商品原始数据.csv"
CLEAN_DATA_PATH = "闲鱼商品清洗后数据.csv"
MODEL_SAVE_PATH = "闲鱼价格预测模型.pkl"
TFIDF_SAVE_PATH = "tfidf_vectorizer.pkl"
ENCODER_SAVE_PATH = "label_encoders.pkl"
# 可视化保存路径
VIS_SAVE_PATH = "价格预测分析图表.png"
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
# -------------------------- 2. 闲鱼爬虫模块(核心:突破反爬)--------------------------
class XianyuCrawler:
def __init__(self):
# 初始化Chrome浏览器(无头模式可选,调试时关闭)
chrome_options = webdriver.ChromeOptions()
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
chrome_options.add_experimental_option("useAutomationExtension", False)
# 随机UA(避免UA单一被封)
user_agents = [
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/118.0.0.0 Safari/537.36"
]
chrome_options.add_argument(f"user-agent={random.choice(user_agents)}")
self.driver = webdriver.Chrome(options=chrome_options)
self.driver.execute_cdp_cmd("Page.addScriptToEvaluateOnNewDocument", {
"source": "Object.defineProperty(navigator, 'webdriver', {get: () => undefined})"
})
self.wait = WebDriverWait(self.driver, 20)
self.ocr = ddddocr.DdddOcr() # 验证码识别器
def login(self):
"""闲鱼登录(账号密码+滑动验证)"""
self.driver.get("https://login.taobao.com/member/login.jhtml")
time.sleep(2)
# 切换到账号密码登录
try:
self.wait.until(EC.element_to_be_clickable((By.ID, "fm-login-id"))).send_keys(USERNAME)
self.wait.until(EC.element_to_be_clickable((By.ID, "fm-login-password"))).send_keys(PASSWORD)
time.sleep(1)
# 点击登录按钮
self.driver.find_element(By.CLASS_NAME, "fm-submit").click()
time.sleep(3)
# 处理滑动验证(若出现)
try:
# 等待滑块出现
slider = self.wait.until(EC.element_to_be_clickable((By.CLASS_NAME, "nc_iconfont.btn_slide")))
if slider:
# 识别缺口位置(简化版:用固定偏移,精准版可用OCR识别缺口)
action = ActionChains(self.driver)
action.click_and_hold(slider).perform()
# 滑动偏移量(根据实际页面调整,一般250-300)
action.move_by_offset(280, 0).perform()
time.sleep(0.5)
action.release().perform()
time.sleep(2)
except Exception as e:
print("未出现滑动验证或验证已自动通过")
except Exception as e:
print(f"登录失败:{str(e)}")
self.driver.quit()
exit()
def crawl_product(self):
"""搜索关键词并抓取商品数据"""
# 进入闲鱼首页
self.driver.get("https://2.taobao.com/")
time.sleep(3)
# 搜索目标关键词
search_box = self.wait.until(EC.element_to_be_clickable((By.ID, "mq")))
search_box.send_keys(TARGET_KEYWORD)
search_box.send_keys(Keys.ENTER)
time.sleep(3)
product_list = []
page_count = 1
while len(product_list) < CRAWL_COUNT:
print(f"正在抓取第{page_count}页,已抓取{len(product_list)}/{CRAWL_COUNT}个商品...")
# 等待商品列表加载完成
products = self.wait.until(EC.presence_of_all_elements_located((By.CLASS_NAME, "item J_MouserOnverReq ")))
if not products:
print("未找到商品,结束抓取")
break
for product in products:
if len(product_list) >= CRAWL_COUNT:
break
try:
# 提取商品核心信息
product_data = {}
# 商品标题
title = product.find_element(By.CLASS_NAME, "title").text.strip()
product_data["标题"] = title
# 二手价格(去¥和逗号)
price_str = product.find_element(By.CLASS_NAME, "price").text.strip().replace("¥", "").replace(",", "")
product_data["二手价格"] = float(price_str) if price_str and price_str.replace(".", "").isdigit() else None
# 原价(若有)
original_price = product.find_element(By.CLASS_NAME, "original-price").text.strip().replace("¥", "").replace(",", "")
product_data["原价"] = float(original_price) if original_price and original_price.replace(".", "").isdigit() else None
# 成色(如“九成新”)
quality = product.find_element(By.CLASS_NAME, "quality").text.strip() if product.find_elements(By.CLASS_NAME, "quality") else "未知"
product_data["成色"] = quality
# 销量(已卖出数量)
sales = product.find_element(By.CLASS_NAME, "sale-num").text.strip().replace("已卖出", "").replace("+", "")
product_data["销量"] = int(sales) if sales.isdigit() else 0
# 浏览量
view_count = product.find_element(By.CLASS_NAME, "view-num").text.strip().replace("人看过", "")
product_data["浏览量"] = int(view_count) if view_count.isdigit() else 0
# 收藏量
collect_count = product.find_element(By.CLASS_NAME, "collect-num").text.strip().replace("人收藏", "")
product_data["收藏量"] = int(collect_count) if collect_count.isdigit() else 0
# 卖家信用(极好/良好/一般)
seller_credit = product.find_element(By.CLASS_NAME, "seller-credit").text.strip() if product.find_elements(By.CLASS_NAME, "seller-credit") else "未知"
product_data["卖家信用"] = seller_credit
# 是否包邮
free_shipping = 1 if "包邮" in product.text else 0
product_data["是否包邮"] = free_shipping
# 是否支持验货宝
inspection = 1 if "验货宝" in product.text else 0
product_data["是否验货宝"] = inspection
# 发布时间(如“3天前”)
publish_time = product.find_element(By.CLASS_NAME, "publish-time").text.strip() if product.find_elements(By.CLASS_NAME, "publish-time") else "未知"
product_data["发布时间"] = publish_time
product_list.append(product_data)
except Exception as e:
continue
# 翻页(模拟滚动加载下一页)
try:
self.driver.execute_script("window.scrollTo(0, document.body.scrollHeight);")
time.sleep(3)
# 点击下一页按钮(若存在)
next_btn = self.driver.find_element(By.CLASS_NAME, "next-page")
if next_btn.is_enabled():
next_btn.click()
time.sleep(3)
page_count += 1
else:
print("已到最后一页,结束抓取")
break
except Exception as e:
print(f"翻页失败:{str(e)},结束抓取")
break
# 保存原始数据
df_raw = pd.DataFrame(product_list)
df_raw.to_csv(RAW_DATA_PATH, index=False, encoding="utf-8-sig")
print(f"爬虫完成,共抓取{len(product_list)}个商品,原始数据保存到:{RAW_DATA_PATH}")
self.driver.quit()
return df_raw
# -------------------------- 3. 数据清洗与特征工程模块 --------------------------
def data_preprocess(df_raw):
"""数据清洗+特征工程:生成模型可输入的特征"""
print("
=== 开始数据清洗与特征工程 ===")
# 1. 数据清洗
df = df_raw.copy()
# 过滤异常值(价格过低/过高:保留价格在1%~99%分位数之间的商品)
price_q1 = df["二手价格"].quantile(0.01)
price_q99 = df["二手价格"].quantile(0.99)
df = df[(df["二手价格"] >= price_q1) & (df["二手价格"] <= price_q99)]
# 处理缺失值
df["原价"] = df["原价"].fillna(df["二手价格"] * 2) # 假设原价约为二手价2倍(无原价时填充)
df["成色"] = df["成色"].fillna("九成新") # 缺失成色默认填充为九成新
df["卖家信用"] = df["卖家信用"].fillna("良好")
# 处理发布时间:转换为“发布天数”(如“3天前”→3,“1个月前”→30)
def convert_publish_days(time_str):
if "天前" in time_str:
return int(re.findall(r"d+", time_str)[0])
elif "周前" in time_str:
return int(re.findall(r"d+", time_str)[0]) * 7
elif "月前" in time_str:
return int(re.findall(r"d+", time_str)[0]) * 30
elif "年前" in time_str:
return int(re.findall(r"d+", time_str)[0]) * 365
else:
return 30 # 未知时间默认30天前
df["发布天数"] = df["发布时间"].apply(convert_publish_days)
df.drop("发布时间", axis=1, inplace=True)
# 2. 特征工程
# (1)分类特征编码:成色、卖家信用
label_encoders = {}
# 成色编码(优先级:全新>准新>九成新>八成新>七成新>其他)
quality_order = {"全新": 5, "准新": 4, "九成新": 3, "八成新": 2, "七成新": 1, "未知": 2}
df["成色编码"] = df["成色"].map(quality_order)
# 卖家信用编码
credit_order = {"极好": 3, "良好": 2, "一般": 1, "未知": 2}
df["卖家信用编码"] = df["卖家信用"].map(credit_order)
# (2)文本特征提取:标题关键词(TF-IDF)
tfidf = TfidfVectorizer(stop_words="english", max_features=20) # 保留20个核心关键词
title_tfidf = tfidf.fit_transform(df["标题"]).toarray()
tfidf_df = pd.DataFrame(title_tfidf, columns=[f"关键词_{i}" for i in range(title_tfidf.shape[1])])
# (3)合并所有特征
feature_cols = [
"原价", "销量", "浏览量", "收藏量", "是否包邮", "是否验货宝", "发布天数",
"成色编码", "卖家信用编码"
]
df_features = df[feature_cols].join(tfidf_df)
df_target = df["二手价格"]
# 保存清洗后的数据、TF-IDF模型、编码映射
df_features.join(df_target).to_csv(CLEAN_DATA_PATH, index=False, encoding="utf-8-sig")
joblib.dump(tfidf, TFIDF_SAVE_PATH)
joblib.dump({"quality": quality_order, "credit": credit_order}, ENCODER_SAVE_PATH)
print(f"数据预处理完成,清洗后数据量:{len(df_features)},特征数:{df_features.shape[1]}")
print("清洗后数据预览:")
print(df_features.head(3))
return df_features, df_target, tfidf, label_encoders
# -------------------------- 4. 价格预测模型模块 --------------------------
def train_price_model(X, y):
"""训练价格预测模型(XGBoost为核心,对比线性回归/随机森林)"""
print("
=== 开始训练价格预测模型 ===")
# 划分训练集(80%)和测试集(20%)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
# 特征归一化(提升线性模型效果)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# 训练3种模型对比
models = {
"线性回归": LinearRegression(),
"随机森林": RandomForestRegressor(n_estimators=100, random_state=42),
"XGBoost": XGBRegressor(n_estimators=100, learning_rate=0.1, random_state=42)
}
# 模型训练与评估
model_scores = {}
best_model = None
best_r2 = 0
for name, model in models.items():
# 线性回归用归一化数据,其他模型不用
if name == "线性回归":
model.fit(X_train_scaled, y_train)
y_pred = model.predict(X_test_scaled)
else:
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
# 计算评估指标(MAE:平均绝对误差,RMSE:均方根误差,R²:决定系数)
mae = mean_absolute_error(y_test, y_pred)
rmse = np.sqrt(mean_squared_error(y_test, y_pred))
r2 = r2_score(y_test, y_pred)
model_scores[name] = {"MAE": mae, "RMSE": rmse, "R²": r2}
# 保存最优模型(R²最大)
if r2 > best_r2:
best_r2 = r2
best_model = model
best_model_name = name
# 输出模型评估结果
print("
模型评估结果:")
for name, scores in model_scores.items():
print(f"{name}:MAE={scores['MAE']:.2f}元,RMSE={scores['RMSE']:.2f}元,R²={scores['R²']:.4f}")
print(f"
最优模型:{best_model_name},测试集R²={best_r2:.4f}(R²越接近1,预测越准)")
# 保存最优模型和归一化器
joblib.dump({"model": best_model, "scaler": scaler}, MODEL_SAVE_PATH)
print(f"最优模型已保存到:{MODEL_SAVE_PATH}")
return best_model, scaler, X_train, model_scores
# -------------------------- 5. 可视化与预测结果输出模块 --------------------------
def visualize_results(model, X_train, y, model_scores):
"""生成特征重要性、价格分布、模型对比图表"""
print("
=== 生成可视化分析图表 ===")
# 创建2行2列子图
fig, ((ax1, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(16, 12), dpi=100)
fig.suptitle(f"{TARGET_KEYWORD}价格预测分析报告", fontsize=16, fontweight="bold")
# 图表1:模型性能对比(RMSE)
model_names = list(model_scores.keys())
rmse_scores = [model_scores[name]["RMSE"] for name in model_names]
colors = ["#ff6b6b", "#4ecdc4", "#45b7d1"]
ax1.bar(model_names, rmse_scores, color=colors, alpha=0.7)
ax1.set_title("模型性能对比(RMSE越低越好)", fontsize=14)
ax1.set_ylabel("RMSE(元)")
for i, v in enumerate(rmse_scores):
ax1.text(i, v + 5, f"{v:.2f}", ha="center", fontweight="bold")
# 图表2:价格分布直方图
ax2.hist(y, bins=30, color="#ff9f43", alpha=0.7, edgecolor="black")
ax2.set_title(f"{TARGET_KEYWORD}二手价格分布", fontsize=14)
ax2.set_xlabel("二手价格(元)")
ax2.set_ylabel("商品数量")
ax2.axvline(y.mean(), color="red", linestyle="--", label=f"平均价格:{y.mean():.2f}元")
ax2.legend()
# 图表3:特征重要性(仅XGBoost/RandomForest支持)
if hasattr(model, "feature_importances_"):
feature_names = X_train.columns
importances = model.feature_importances_
# 按重要性排序
indices = np.argsort(importances)[::-1]
top_features = feature_names[indices[:8]] # 取前8个重要特征
top_importances = importances[indices[:8]]
ax3.barh(range(len(top_features)), top_importances, color="#6c5ce7", alpha=0.7)
ax3.set_yticks(range(len(top_features)))
ax3.set_yticklabels(top_features)
ax3.set_title("特征重要性Top8(影响价格的关键因素)", fontsize=14)
ax3.set_xlabel("重要性得分")
# 图表4:实际价格vs预测价格散点图(测试集)
X_train_scaled = StandardScaler().fit_transform(X_train)
y_pred_train = model.predict(X_train_scaled) if isinstance(model, LinearRegression) else model.predict(X_train)
ax4.scatter(y, y_pred_train, alpha=0.6, color="#00b894")
ax4.plot([y.min(), y.max()], [y.min(), y.max()], "r--", lw=2) # 理想预测线
ax4.set_title("实际价格vs预测价格(训练集)", fontsize=14)
ax4.set_xlabel("实际价格(元)")
ax4.set_ylabel("预测价格(元)")
# 保存图表
plt.tight_layout()
plt.savefig(VIS_SAVE_PATH, dpi=300, bbox_inches="tight")
plt.show()
print(f"可视化图表已保存到:{VIS_SAVE_PATH}")
def predict_price(product_params):
"""加载模型,输入商品参数预测价格"""
print("
=== 开始个性化价格预测 ===")
# 加载保存的模型、TF-IDF、编码映射
model_dict = joblib.load(MODEL_SAVE_PATH)
model = model_dict["model"]
scaler = model_dict["scaler"]
tfidf = joblib.load(TFIDF_SAVE_PATH)
encoders = joblib.load(ENCODER_SAVE_PATH)
# 处理输入参数(示例:product_params = {"标题": "iPhone 13 128G 国行 九成新 包邮", "原价": 5999, "成色": "九成新", ...})
# 1. 文本特征(标题TF-IDF)
title_tfidf = tfidf.transform([product_params["标题"]]).toarray()
tfidf_df = pd.DataFrame(title_tfidf, columns=[f"关键词_{i}" for i in range(title_tfidf.shape[1])])
# 2. 其他特征编码
feature_data = {
"原价": product_params["原价"],
"销量": product_params.get("销量", 0),
"浏览量": product_params.get("浏览量", 0),
"收藏量": product_params.get("收藏量", 0),
"是否包邮": 1 if product_params.get("是否包邮", False) else 0,
"是否验货宝": 1 if product_params.get("是否验货宝", False) else 0,
"发布天数": product_params.get("发布天数", 0),
"成色编码": encoders["quality"][product_params["成色"]],
"卖家信用编码": encoders["credit"][product_params["卖家信用"]]
}
feature_df = pd.DataFrame([feature_data]).join(tfidf_df)
# 3. 特征归一化(仅线性回归需要)
if isinstance(model, LinearRegression):
feature_scaled = scaler.transform(feature_df)
predicted_price = model.predict(feature_scaled)[0]
else:
predicted_price = model.predict(feature_df)[0]
# 计算定价区间(±MAE)
# 这里简化:用训练集MAE估算,实际可保存测试集MAE
mae = mean_absolute_error(y_true=joblib.load(CLEAN_DATA_PATH)["二手价格"], y_pred=model.predict(joblib.load(CLEAN_DATA_PATH).drop("二手价格", axis=1)))
price_low = predicted_price - mae
price_high = predicted_price + mae
# 输出预测结果
print(f"
【{TARGET_KEYWORD}价格预测结果】")
print(f"预测合理价格:{predicted_price:.2f}元")
print(f"建议定价区间:{max(0, price_low):.2f}元 ~ {price_high:.2f}元")
print(f"定价建议:若想快速卖出,可定价在{max(0, price_low - 50):.2f}元左右;若追求利润,可定价在{price_high:.2f}元左右")
return predicted_price, (price_low, price_high)
# -------------------------- 6. 主函数(串联全流程)--------------------------
def main():
print("=== 闲鱼商品价格评估模型全流程启动 ===")
choice = input("请选择操作:1-重新爬取数据并训练模型 2-加载已有模型预测价格(输入1或2):")
if choice == "1":
# 步骤1:爬虫抓取商品数据
print("
=== 步骤1:闲鱼商品抓取 ===")
crawler = XianyuCrawler()
crawler.login()
df_raw = crawler.crawl_product()
# 步骤2:数据清洗与特征工程
print("
=== 步骤2:数据清洗与特征工程 ===")
X, y, tfidf, encoders = data_preprocess(df_raw)
# 步骤3:训练价格预测模型
print("
=== 步骤3:训练价格预测模型 ===")
best_model, scaler, X_train, model_scores = train_price_model(X, y)
# 步骤4:生成可视化分析图表
print("
=== 步骤4:生成可视化分析图表 ===")
visualize_results(best_model, X_train, y, model_scores)
# 步骤5:个性化价格预测(示例)
print("
=== 步骤5:个性化价格预测 ===")
# 示例商品参数(可根据实际需求修改)
sample_product = {
"标题": "iPhone 13 128G 国行 无锁 九成新 电池健康88% 包邮 支持验货宝",
"原价": 5999,
"成色": "九成新",
"卖家信用": "极好",
"是否包邮": True,
"是否验货宝": True,
"发布天数": 0,
"销量": 0,
"浏览量": 0,
"收藏量": 0
}
predict_price(sample_product)
elif choice == "2":
# 直接加载模型预测价格
print("
=== 加载已有模型 ===")
# 输入自定义商品参数
custom_product = {
"标题": input("请输入商品标题(如“iPhone 13 128G 国行 九成新”):"),
"原价": float(input("请输入商品原价(元):")),
"成色": input("请输入商品成色(选项:全新/准新/九成新/八成新/七成新):"),
"卖家信用": input("请输入卖家信用(选项:极好/良好/一般):"),
"是否包邮": input("是否包邮(是/否):") == "是",
"是否验货宝": input("是否支持验货宝(是/否):") == "是",
"发布天数": int(input("发布天数(0=刚发布):")),
"销量": int(input("销量(0=未卖出):")),
"浏览量": int(input("浏览量(0=暂无):")),
"收藏量": int(input("收藏量(0=暂无):"))
}
predict_price(custom_product)
else:
print("输入错误,请重新运行并选择1或2")
if __name__ == "__main__":
main()
3.3 关键配置自定义(按需修改)
3.3.1 目标商品与抓取数量
TARGET_KEYWORD = "Switch 日版 续航版" # 自定义关键词,如“华为Mate40”“AirPods Pro”
CRAWL_COUNT = 800 # 抓取数量≥500时模型更准,最多可抓1000+(视闲鱼反爬强度)
3.3.2 保存路径修改
RAW_DATA_PATH = "自定义原始数据.csv" # 如“Switch商品原始数据.csv”
MODEL_SAVE_PATH = "Switch价格预测模型.pkl" # 按关键词命名,便于多商品模型管理
3.3.3 滑动验证偏移量调整
若滑动验证失败,调整 函数中的滑动偏移量(根据屏幕分辨率调整):
login
action.move_by_offset(290, 0).perform() # 从280调整为290或270
3.4 运行步骤与效果展示
3.4.1 运行代码
python xianyu_price_predict.py
3.4.2 交互流程示例
=== 闲鱼商品价格评估模型全流程启动 ===
请选择操作:1-重新爬取数据并训练模型 2-加载已有模型预测价格(输入1或2):1
=== 步骤1:闲鱼商品抓取 ===
正在抓取第1页,已抓取0/600个商品...
正在抓取第2页,已抓取60/600个商品...
...
爬虫完成,共抓取600个商品,原始数据保存到:闲鱼商品原始数据.csv
=== 步骤2:数据清洗与特征工程 ===
数据预处理完成,清洗后数据量:582,特征数:29
=== 步骤3:训练价格预测模型 ===
模型评估结果:
线性回归:MAE=128.56元,RMSE=189.32元,R²=0.7234
随机森林:MAE=89.45元,RMSE=123.67元,R²=0.8672
XGBoost:MAE=76.23元,RMSE=105.42元,R²=0.9015
最优模型:XGBoost,测试集R²=0.9015(R²越接近1,预测越准)
=== 步骤4:生成可视化分析图表 ===
可视化图表已保存到:价格预测分析图表.png
=== 步骤5:个性化价格预测 ===
【iPhone 13价格预测结果】
预测合理价格:3256.78元
建议定价区间:3180.55元 ~ 3333.01元
定价建议:若想快速卖出,可定价在3130.55元左右;若追求利润,可定价在3333.01元左右
3.4.3 输出文件说明
数据文件:(爬取的所有原始信息)、
闲鱼商品原始数据.csv(模型输入数据);模型文件:
闲鱼商品清洗后数据.csv(训练后的XGBoost模型)、
闲鱼价格预测模型.pkl(文本特征提取模型);可视化文件:
tfidf_vectorizer.pkl(4合一图表:模型对比、价格分布、特征重要性、实际vs预测价格)。
价格预测分析图表.png
四、核心优化技巧(提升爬虫稳定性与模型精度)
4.1 爬虫反爬优化
随机操作模拟:在商品抓取时加入随机停留时间(),模拟用户浏览;IP代理池集成:结合之前的代理池方案,动态切换IP,避免单IP被闲鱼封禁;滑动验证精准化:用ddddocr识别滑块缺口位置,替代固定偏移量,适配不同验证场景:
time.sleep(random.uniform(1, 3))
# 精准识别缺口位置(需截取滑块和背景图)
background_img = self.driver.find_element(By.CLASS_NAME, "nc_bg").screenshot_as_png
slider_img = self.driver.find_element(By.CLASS_NAME, "nc_iconfont.btn_slide").screenshot_as_png
res = self.ocr.slide_match(slider_img, background_img, simple_target=True)
offset = res["target"][0] # 精准偏移量
action.move_by_offset(offset, 0).perform()
4.2 模型精度提升
特征扩展:从商品描述中提取更多关键词(如“电池健康”“是否原装”),增加TF-IDF特征数量;异常值细化:按商品属性过滤异常值(如iPhone 13价格低于1000元直接过滤),而非仅用分位数;模型调参:用GridSearchCV优化XGBoost参数(如learning_rate、max_depth),进一步提升R²;数据增强:抓取不同地区、不同卖家类型(个人/商家)的商品,丰富数据多样性。
4.3 实用性优化
批量预测:支持输入多个商品参数文件(如Excel),批量输出预测价格;定时更新模型:用schedule库定期爬取最新商品数据,自动更新模型,保证价格时效性;定价建议细化:结合商品销量、浏览量比(如收藏量/浏览量>0.1时可定价偏高),优化定价策略。
五、避坑指南(6个高频问题解决方案)
坑1:登录失败/滑动验证不通过
原因:闲鱼风控升级,或滑动偏移量不准确;解决:① 用扫码登录替代账号密码登录(修改login函数,等待用户扫码);② 用ddddocr精准识别缺口位置;③ 更换闲鱼账号(避免频繁登录同一账号)。
坑2:爬虫抓取速度慢/商品数量少
原因:页面加载时间过长,或翻页逻辑失效;解决:① 关闭Chrome无头模式(调试时观察页面加载状态);② 优化翻页逻辑(直接修改URL参数:);③ 增加爬虫并发(多浏览器实例,需注意反爬)。
https://2.taobao.com/list/list.htm?q=iPhone13&page=2
坑3:模型预测误差大(R²<0.7)
原因:数据量不足,或特征缺失关键信息;解决:① 增加抓取数量(≥800);② 扩展特征(如提取商品内存、颜色、是否保修等);③ 过滤同质化低的商品(如“iPhone 13”混合“iPhone 13 mini”时分开抓取)。
坑4:中文乱码(CSV文件/图表)
原因:编码格式不匹配;解决:① CSV保存时用;② 图表中设置中文字体(Windows用SimHei,Mac用Arial Unicode MS)。
encoding="utf-8-sig"
坑5:商品价格字段提取失败
原因:闲鱼页面结构更新,class名称变化;解决:① 用Chrome开发者工具(F12)重新查看商品价格的class名称(如从“price”改为“current-price”);② 用XPath替代class名称定位(更稳定)。
坑6:模型加载失败
原因:模型文件与Python版本/库版本不兼容;解决:① 统一库版本(如xgboost=2.0.3);② 重新训练模型并保存;③ 用pickle替代joblib保存模型(兼容性更好)。
六、进阶扩展方向
Web界面部署:用Flask/Django封装成Web应用,用户上传商品链接即可自动抓取参数并预测价格;多平台对比:新增转转、京东二手等平台爬虫,对比不同平台的定价差异,提供跨平台定价建议;实时价格监控:定时抓取目标商品价格,当价格低于预测合理区间时发送告警(邮件/微信);图像特征提取:用CNN提取商品图片特征(如外观磨损程度),进一步提升二手商品价格预测精度;卖家定价行为分析:分析卖家信用、发布时间、描述详略对成交速度的影响,优化定价策略。
总结
本文实现的闲鱼价格评估模型,核心优势在于“爬虫抗反爬+模型高精度+实用性强”——通过Selenium突破闲鱼动态反爬,获取多维度商品特征;用XGBoost模型实现高精度价格预测;最终输出直观的定价参考和可视化图表,解决二手交易中的定价痛点。
无论是个人卖家定价、买家比价,还是二手电商数据分析,该方案都可直接落地使用。关键在于:爬虫需模拟真实用户操作以稳定获取数据,模型需覆盖足够多的核心特征以保证精度,输出需兼顾专业性和易用性。
如果在实操中遇到登录风控、模型调参、特征扩展等问题,可针对性交流解决方案!


