为什么要做自动回复?
很多二手个人卖家白天要上班、晚上要休息,无法及时回复买家的咨询。而平台的消息回复率直接影响搜索权重,且长时间不回复会流失订单。本文实现一个7×24 小时自动回复助手,它能:
-
自动登录平台(或复用已有登录状态)
-
监听新收到的咨询消息
-
根据买家提问内容(关键词/正则)自动匹配预设答案
-
支持回复后标记"已处理",避免重复回复
-
附带随机延时、每日回复封顶、黑名单等防骚扰机制
核心设计
| 模块 | 实现方式 |
|---|---|
| 页面监控 | Selenium 定时刷新"消息中心"页面,定位未读消息元素 |
| 消息去重 | 将已回复的消息 ID(或内容 hash)存入 SQLite/JSON,避免重复回复 |
| 意图匹配 | 基于规则(正则 + 关键词权重),优先级从高到低,如"最低价"→"议价回复" |
| 自动输入回复 | Selenium 定位输入框,逐字输入(模拟人机),点击发送按钮 |
| 安全控制 | 每次回复前随机 sleep 2~6 秒,每日总回复量上限(如 50 条) |
| 日志与通知 | 记录每条回复内容、时间、对象,异常时发送邮件或钉钉通知 |
环境与依赖
bash
pip install selenium schedule python-telegram-bot==13.15 # 如需 Telegram 通知
# 另需:json, re, time, random, sqlite3, logging
同时准备 Chrome 浏览器与对应版本的 ChromeDriver。
完整代码(带详细注释)
1. 配置文件 config.yaml(敏感信息与规则)
yaml
# 账号配置
account:
login_url: "https://example.com/login" # 实际登录页
username: "your_phone_or_email"
password: "your_password"
# 也可通过 cookies 文件复用登录态,避免重复登录
# 回复规则(优先级按列表顺序,匹配到第一个即停止)
reply_rules:
- keywords: ["最低价", "能便宜", "砍价", "刀吗", "优惠"]
template: "亲,已经是底价了哦,因为东西成色很好。您可以再看看说明,爽快的话送个小礼物~"
max_daily: 5 # 此类回复每日最多触发5次
- keywords: ["还在吗", "有没有", "有货吗", "出不"]
template: "在的,东西还在,直接拍下即可,当天发货。有疑问随时问我哈。"
max_daily: 20
- keywords: ["包邮", "邮费", "快递"]
template: "您好,本商品默认不包邮,但若您拍下后联系我可以给您改邮费为 5 元(偏远地区除外)。"
max_daily: 10
- keywords: ["实拍", "图片", "看看图"]
template: "实拍图商品描述里有的,如果您需要更多角度,加我 VX: example(备注闲鱼)发送给您。"
max_daily: 3
- keywords: ["发票", "保修", "盒子", "配件"]
template: "配件齐全,有原盒,无发票(个人卖家)。保修问题请查看商品描述。"
max_daily: 8
- default: true
template: "感谢您的关注!商品详情已写清楚,如有具体问题请直接描述,我会尽快回复。"
max_daily: 30
# 全局限制
global:
daily_reply_limit: 50 # 一天总回复上限
message_check_interval_sec: 30 # 检查新消息的频率(秒)
headless: false # 是否无头模式(调试时可false)
data_dir: "./bot_data" # 存sqlite/日志的目录
2. 主程序 reply_bot.py
python
import os
import re
import time
import json
import random
import sqlite3
import logging
import yaml
from datetime import datetime
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
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.common.exceptions import TimeoutException, NoSuchElementException
# 加载配置
with open("config.yaml", "r", encoding="utf-8") as f:
config = yaml.safe_load(f)
# 日志配置
if not os.path.exists(config["global"]["data_dir"]):
os.makedirs(config["global"]["data_dir"])
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(os.path.join(config["global"]["data_dir"], "bot.log"), encoding="utf-8"),
logging.StreamHandler()
]
)
logger = logging.getLogger("ReplyBot")
# ---------- 数据库管理(存储已回复消息与每日计数) ----------
class DBManager:
def __init__(self, db_path):
self.conn = sqlite3.connect(db_path, check_same_thread=False)
self.cursor = self.conn.cursor()
self._create_tables()
def _create_tables(self):
self.cursor.execute('''CREATE TABLE IF NOT EXISTS replied_messages
(msg_id TEXT PRIMARY KEY, reply_time TEXT, buyer_name TEXT)''')
self.cursor.execute('''CREATE TABLE IF NOT EXISTS daily_stats
(date TEXT PRIMARY KEY, reply_count INTEGER)''')
self.conn.commit()
def is_replied(self, msg_id):
self.cursor.execute("SELECT 1 FROM replied_messages WHERE msg_id=?", (msg_id,))
return self.cursor.fetchone() is not None
def mark_replied(self, msg_id, buyer_name):
now = datetime.now().isoformat()
self.cursor.execute("INSERT INTO replied_messages VALUES (?, ?, ?)", (msg_id, now, buyer_name))
self.conn.commit()
def get_today_reply_count(self):
today = datetime.now().strftime("%Y-%m-%d")
self.cursor.execute("SELECT reply_count FROM daily_stats WHERE date=?", (today,))
row = self.cursor.fetchone()
return row[0] if row else 0
def increment_today_reply_count(self):
today = datetime.now().strftime("%Y-%m-%d")
self.cursor.execute("INSERT INTO daily_stats (date, reply_count) VALUES (?, 1) "
"ON CONFLICT(date) DO UPDATE SET reply_count = reply_count + 1", (today,))
self.conn.commit()
def close(self):
self.conn.close()
# ---------- Selenium 浏览器封装 ----------
class BrowserController:
def __init__(self, headless=False):
chrome_options = Options()
if headless:
chrome_options.add_argument("--headless")
chrome_options.add_argument("--disable-gpu")
chrome_options.add_argument("--no-sandbox")
chrome_options.add_argument("--window-size=1920,1080")
chrome_options.add_experimental_option("excludeSwitches", ["enable-automation"])
self.driver = webdriver.Chrome(options=chrome_options)
self.wait = WebDriverWait(self.driver, 10)
self.logged_in = False
def login(self, login_url, username, password):
"""使用账号密码登录,实际中可能需要处理滑块验证,此处简化。建议预先手动登录一次,复用cookies"""
self.driver.get(login_url)
try:
name_input = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "input[name='username']")))
pwd_input = self.driver.find_element(By.CSS_SELECTOR, "input[name='password']")
name_input.send_keys(username)
pwd_input.send_keys(password)
pwd_input.send_keys(Keys.RETURN)
time.sleep(3)
# 检查登录是否成功(例如判断是否跳转到个人中心)
if "个人中心" in self.driver.title or "message" in self.driver.current_url:
self.logged_in = True
logger.info("登录成功")
return True
else:
logger.error("登录失败,请检查账号或验证码")
return False
except Exception as e:
logger.exception("登录过程异常")
return False
def load_cookies(self, cookie_file):
"""从文件加载 cookies 并添加到 driver(用于免密登录)"""
if os.path.exists(cookie_file):
self.driver.get(config["account"]["login_url"]) # 先访问域名才能加cookie
with open(cookie_file, "r") as f:
cookies = json.load(f)
for cookie in cookies:
self.driver.add_cookie(cookie)
self.driver.refresh()
time.sleep(2)
self.logged_in = True
logger.info("使用Cookies登录成功")
return True
return False
def save_cookies(self, cookie_file):
"""手动登录后保存cookies"""
cookies = self.driver.get_cookies()
with open(cookie_file, "w") as f:
json.dump(cookies, f)
logger.info("Cookies已保存")
def goto_message_page(self, url):
"""进入消息列表页(具体URL根据平台修改)"""
self.driver.get(url)
time.sleep(random.uniform(1, 3))
def get_unread_messages(self):
"""
定位所有未读消息的聊天框或消息条目。
这里需要根据实际页面结构编写选择器,示例结构:
每条消息容器为 .message-item,未读类名为 .unread,包含买家昵称和最后一条消息内容
返回列表,每个元素为 dict { "msg_id": "会话id", "buyer": "张三", "last_msg": "能便宜吗", "element": WebElement }
"""
unread_list = []
try:
# 定位所有未读消息容器(修改为实际网站的CSS选择器)
containers = self.driver.find_elements(By.CSS_SELECTOR, ".message-list .item.unread")
for cont in containers:
# 获取会话唯一标识(例如 data-id 或 链接中的数字)
msg_id = cont.get_attribute("data-id") or cont.find_element(By.TAG_NAME, "a").get_attribute("href")
buyer = cont.find_element(By.CSS_SELECTOR, ".nick").text
# 获取最后一条消息(可能是买家发的)
last_msg_elem = cont.find_element(By.CSS_SELECTOR, ".last-msg")
last_msg = last_msg_elem.text.strip()
unread_list.append({
"msg_id": msg_id,
"buyer": buyer,
"last_msg": last_msg,
"element": cont
})
except Exception as e:
logger.warning(f"获取未读消息出错: {e}")
return unread_list
def click_chat_and_reply(self, chat_element, reply_text):
"""点击聊天进入会话窗口,输入回复内容并发送"""
try:
# 点击未读条目,打开聊天窗口
chat_element.click()
time.sleep(random.uniform(2, 4))
# 定位输入框(不同的平台可能是 textarea 或 div[contenteditable])
input_box = self.wait.until(EC.presence_of_element_located((By.CSS_SELECTOR, "textarea.input, div[contenteditable='true']")))
# 模拟逐字输入(更像真人)
for char in reply_text:
input_box.send_keys(char)
time.sleep(random.uniform(0.05, 0.15))
time.sleep(random.uniform(0.5, 1))
# 点击发送按钮
send_btn = self.driver.find_element(By.CSS_SELECTOR, "button.send-btn")
send_btn.click()
logger.info(f"已回复买家 {chat_element.get_attribute('data-id')} : {reply_text[:30]}...")
# 发送后等待发送完成并关闭/返回列表
time.sleep(2)
# 寻找返回按钮或关闭窗口(按实际设计)
back_btn = self.driver.find_elements(By.CSS_SELECTOR, ".back-button")
if back_btn:
back_btn[0].click()
time.sleep(1)
return True
except Exception as e:
logger.exception(f"回复失败: {e}")
return False
def quit(self):
self.driver.quit()
# ---------- 规则匹配引擎 ----------
class ReplyEngine:
def __init__(self, rules_config):
self.rules = rules_config["reply_rules"]
self.default_rule = None
for rule in self.rules:
if rule.get("default"):
self.default_rule = rule
break
def match_rule(self, message_text):
"""返回匹配的规则(第一个命中)"""
message_lower = message_text.lower()
for rule in self.rules:
if "default" in rule:
continue
keywords = rule.get("keywords", [])
for kw in keywords:
if kw.lower() in message_lower:
return rule
return self.default_rule
# ---------- 主控制逻辑 ----------
class AutoReplyBot:
def __init__(self):
self.db = DBManager(os.path.join(config["global"]["data_dir"], "bot.db"))
self.browser = BrowserController(headless=config["global"]["headless"])
self.engine = ReplyEngine(config)
self.running = False
self.today_replied = 0
def init_login(self):
# 优先尝试cookies登录
cookie_file = os.path.join(config["global"]["data_dir"], "cookies.json")
if self.browser.load_cookies(cookie_file):
return True
# 否则使用账号密码登录(第一次可能需要手动处理验证码)
if self.browser.login(config["account"]["login_url"], config["account"]["username"], config["account"]["password"]):
self.browser.save_cookies(cookie_file)
return True
logger.error("初始化登录失败,请检查网络或账号信息")
return False
def run_once(self):
"""单次检查并处理未读消息"""
global_limit = config["global"]["daily_reply_limit"]
today_count = self.db.get_today_reply_count()
if today_count >= global_limit:
logger.info("今日已达回复上限,停止服务")
self.running = False
return
# 进入消息页面(URL需要替换成实际的消息列表页)
msg_page_url = "https://example.com/messages" # 替换为实际
self.browser.goto_message_page(msg_page_url)
unreads = self.browser.get_unread_messages()
if not unreads:
logger.info("没有未读消息")
return
for msg in unreads:
# 防止触发每日上限
if self.db.get_today_reply_count() >= global_limit:
break
if self.db.is_replied(msg["msg_id"]):
logger.info(f"消息 {msg['msg_id']} 已回复过,跳过")
continue
# 匹配回复内容
rule = self.engine.match_rule(msg["last_msg"])
reply_template = rule["template"]
# 检查该规则的每日次数限制(可选)
rule_limit = rule.get("max_daily", 999)
# 此处简单实现:可以用单独的表记录每个规则今日触发次数,为简化,跳过细节
# 实际生产环境中可以加一个计数器
# 直接回复
success = self.browser.click_chat_and_reply(msg["element"], reply_template)
if success:
self.db.mark_replied(msg["msg_id"], msg["buyer"])
self.db.increment_today_reply_count()
# 随机延时,避免连续操作被风控
time.sleep(random.uniform(3, 8))
else:
logger.error(f"回复失败,跳过 {msg['msg_id']}")
def start_loop(self):
if not self.init_login():
return
self.running = True
logger.info("自动回复机器人启动")
interval = config["global"]["message_check_interval_sec"]
while self.running:
try:
self.run_once()
except Exception as e:
logger.exception("主循环异常")
# 等待下一次检查
for _ in range(interval):
if not self.running:
break
time.sleep(1)
# 每日重置问题:如果日期变化,数据库会读取新的一天计数,无需额外重置
def stop(self):
self.running = False
self.browser.quit()
self.db.close()
logger.info("机器人已停止")
if __name__ == "__main__":
bot = AutoReplyBot()
try:
bot.start_loop()
except KeyboardInterrupt:
bot.stop()
部署与运行注意事项
-
替换选择器
你需要手动打开目标平台的"消息"页面,使用 Chrome 开发者工具(F12)定位:
-
未读消息容器:
.message-list .item.unread -
买家昵称:
.nick -
最后一条消息内容:
.last-msg -
输入框:
textarea.input或div[contenteditable='true'] -
发送按钮:
button.send-btn将代码中的 CSS 选择器全部换成实际值。
-
-
登录方式
-
推荐第一次运行前手动登录一次,然后使用
BrowserController的save_cookies单独生成cookies.json,之后脚本只需加载 cookies,避免重复登录或验证码。 -
如果必须自动登录,可能需要接打码平台或手动输入验证码,脚本已预留接口。
-
-
安全建议
-
回复间隔延迟 3~8 秒,每次检查消息后也随机等待几秒。
-
总回复上限设低一些(如每天 30~50),避免被平台认定为营销机器人。
-
不要回复带微信/QQ等外链的模板,容易被封号。本文示例中的"加VX"仅作演示,实际使用时应改为平台允许的说法,如"发您闲鱼私信"。
-
建议配合
schedule库将运行时段设在白天(9:00-22:00),夜间停止,更符合真人习惯。
-
-
进阶优化
-
接入简单的意图识别(例如使用
sklearn的余弦相似度 + text2vec),减少关键词死板匹配。 -
对于重复问同一问题的用户,只回复一次;加入用户黑名单功能(如用户 ID 前缀)。
-
将回复模板外置数据库,允许热更新。
-
异常时通过企业微信/Telegram 发送报警。
-
测试效果
运行脚本后,在买家端发送"能便宜吗?",几秒内会自动收到预设的议价回复。并且当天第二次问"包邮吗"也会自动回复。所有已回复的会话都会被标记,不会二次打扰。
写在最后
本脚本仅用于个人学习自动化技术与 Selenium 操练,严禁用于骚扰、批量营销等违反二手平台用户协议的行为。合理利用自动化可以解放你的时间,但务必遵守平台的发送频率限制,维持良好的交易环境。代码中所有选择器与示例平台名称均为虚构,请替换为你实际使用的网站结构。
如果你对规则引擎不满意,下一篇文章我将分享如何给机器人接入本地大语言模型(如 ChatGLM-6B),真正实现"智能"回复------敬请期待。