ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Python实战:用AT指令与语音Modem实现自动接听电话机器人

2026/9/7 9:59:41 拓冰建站 浏览量
Python实战:用AT指令与语音Modem实现自动接听电话机器人 之前刷到一个很火的“给100个野人打电话”整活视频评论区都在玩梗我却在想如果反过来一天之内真的有一百通陌生来电打进来一个人手动接听、记录号码、统计时长不仅累而且大概率会漏掉关键电话。更现实一点很多做业务测试、电话外呼验证、呼叫中心联调的同学经常需要批量验证来电处理逻辑靠人工一台台去点根本不现实。所以这篇文章我准备认真写一期 Python 实战如何用计算机 语音 Modem / 4G 上网卡自动监听来电、识别号码、自动接听、记录通话信息最终做成一个可以 7×24 小时值守的“自动接电话机器人”。虽然标题是“给100个野人接电话的第一天”但实际上是一套完整的来电自动化处理方案。新手可以从零开始复现有经验的开发者也可以直接拿走核心代码改造。1. 背景与核心概念1.1 什么是“自动来电接听系统”自动来电接听系统是指通过硬件设备接收电话线路上的来电信号并按照预设规则自动完成振铃检测、来电号码识别、接听操作、通话时长统计、挂断或转接等一系列动作的软件系统。它可以做到实时监测电话线路是否有来电。从线路信号中解析来电号码。根据白名单/黑名单决定接听还是拒接。自动接听后播放提示音、录音或直接挂断。把每一次来电的号码、时间、时长写入数据库。这类系统的核心价值在于“将重复性、机械性的人工接听动作替换成程序化的自动处理”。尤其适合号码测试、呼叫中心压力联调、漏话回拨提醒等场景。1.2 常见应用场景在实际项目中我总结过几类比较典型的用法场景传统处理方式自动化处理方式用大量号码验证接听线路是否正常人工逐一拨入并确认接通系统自动接听按号码入库省去人工呼叫中心联调时需要快速回拨手动记录号码再回拨程序自动记录按需批量回拨漏接电话提醒看到未接来电后手动回拨系统自动记录并触发短信/邮件提醒通话统计和合规审计人工登记通话记录每次来电自动落库导出报表如果你是做物联网项目或者维护一批插了 SIM 卡的路由器、充电桩、车载设备这类设备的运营号码也会频繁收到来自平台或用户侧的语音呼叫自动接听系统可以用于判断 SIM 卡是否欠费、设备语音通路是否正常。1.3 为什么“自动接听”比“人工接听”更可靠人工处理高频来电时存在几个确定性问题接听效率不稳定高峰时段容易漏接。号码记录手工登记容易出错。通话时长、来电时间等数据无法结构化存储。无法 24 小时持续值守。程序自动接听则不存在上述问题。它不依赖人的状态只要硬件和脚本正常运行就能在每次振铃后稳定完成检测、接听、记录、挂断的闭环。不过有一点需要提前说清楚自动接听只能在“合法授权、自有号码”的范围内使用。如果你要用于测试他人号码或业务环境务必先取得合规审批不要拿这套方案去做骚扰电话或未经授权的通话采集。后续我也会在最佳实践部分展开讲安全边界。2. 环境准备与版本说明2.1 方案选型先聊硬件。要让电脑“接电话”有几种常见路径方案硬件成本难度适用场景语音 Modem 电话线低低模拟固话线路稳定可靠4G USB Dongle AT 指令中中无线 SIM 卡适合移动号码语音网关 SIP 协议高高企业级呼叫中心、批量并发云通信平台 API按量计费低有公网回调能力不依赖本机硬件本文以“USB 语音 Modem 或 4G Dongle Python 串口指令”为主线因为这套方案最直观能让你真实看到“振铃 → 来电显示 → 接听 → 挂断”的完整链路而且代码可以在普通电脑上直接运行。这些硬件的具体型号有差异通信指令大体是统一的所以下面的代码不需要绑定某个品牌。2.2 软件环境本文示例的软件环境如下操作系统Windows 10/11 或 Linux树莓派系统、Ubuntu 均可Python 版本3.9 及以上串口通信库pyserial数据库SQLite 3Python 内置 sqlite3 模块即可串口调试工具PuTTY 或串口助手如果你用的是 Windows需要先确认设备管理器中语音 Modem 或 4G Dongle 被识别为 COM 口如果用的是 Linux通常是/dev/ttyUSB0或/dev/ttyACM0并且需要给当前用户加上串口访问权限。以下命令在 Linux 下可以把当前用户加入dialout组注销后生效sudo usermod -a -G dialout $USER版本需要根据你的项目实际情况调整本文示例以常见环境为例重点演示配置思路和代码骨架。2.3 安装依赖创建项目目录并安装 pyserialmkdir call-robot cd call-robot pip install pyserial如果下载较慢可以切换到国内镜像源pip install pyserial -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后先用一串指令测试串口是否可用。把下面这段保存为port_test.pyimport serial ports [ /dev/ttyUSB0, /dev/ttyACM0, COM3, COM4, ] for port in ports: try: ser serial.Serial(port, 115200, timeout0.5) ser.write(bAT\r) data ser.read(64) print(port, , data) ser.close() except Exception as e: print(port, , e)如果串口返回b\r\nOK\r\n或者出现OK说明设备和电脑已经连通。接下来可以进入核心原理部分。3. 核心原理AT 指令与来电检测3.1 什么是 AT 指令AT 指令集是从早期 Modem 时代流传下来的一套文本命令协议现在绝大多数语音 Modem、4G 模块、GSM 模块等设备依然支持。它的特点是通过串口发送一行以AT开头的文本设备执行后返回结果。几组与来电处理关系最密切的指令指令作用AT测试串口是否连通返回 OKATZ重置模块参数ATE0关闭指令回显ATCLIP1开启来电显示功能ATA接听当前来电ATCHUP挂断当前通话ATD号码;发起语音呼叫分号表示语音呼叫ATCSQ查询信号强度3.2 来电流程拆解当电话打进来时串口会先后收到两类数据第一类是振铃通知RING第二类是来电显示信息CLIP: 13800138000,129,,,,,0RING表示线路有振铃信号CLIP后面跟着的就是主叫号码。不同硬件返回的格式略有差异但基本都能从中解析出号码。整个自动接听流程可以拆成四个步骤循环读取串口数据等待RING出现。读取后续的CLIP消息提取来电号码。根据预设规则决定是否接听如果需要接听则发送ATA。通话保持若干秒后发送ATCHUP挂断把号码和时长写入数据库。这里的关键点在于硬件检测到振铃之后不会自动接听必须由程序主动发送ATA。而挂断动作也需要程序发送ATCHUP否则通话会一直保持。3.3 为什么需要“接听后挂断”的完整闭环有些小伙伴会问既然只是为了记录号码不接听直接挂断不行吗如果只是记录号码确实可以不做接听操作。但很多场景下我们需要的是“验证一条线路能否正常接通”或者“让呼叫方听到指定提示音”这种情况下必须完整执行“接听 → 保持 → 挂断”的动作。另外有些硬件在持续不接听时振铃次数过多会被运营商判定为超时未应答影响后续测试结果。因此在设计脚本时我建议把接听和挂断做成一个完整的闭环而不是只解析号码就结束。4. 完整实战Python 自动接听电话机器人4.1 项目结构我们创建一个完整的项目目录call-robot/ ├── call_monitor.py # 主程序 ├── init_db.py # 初始化数据库 ├── requirements.txt # 依赖列表 └── call_robot.service # Linux systemd 服务文件可选4.2 初始化数据库用 SQLite 存储来电记录零配置、单文件非常适合个人项目和中小规模测试。先写建表脚本init_db.pyimport sqlite3 DB_PATH calls.db def init_db(): conn sqlite3.connect(DB_PATH) conn.execute( CREATE TABLE IF NOT EXISTS calls ( id INTEGER PRIMARY KEY AUTOINCREMENT, phone TEXT NOT NULL, status TEXT, duration INTEGER DEFAULT 0, created_at TEXT ) ) conn.commit() conn.close() print(数据库初始化完成, DB_PATH) if __name__ __main__: init_db()运行python init_db.py字段说明phone来电号码。status来电状态如accepted、completed、rejected。duration通话保持时长单位秒。created_at来电时间ISO 格式字符串。4.3 编写主程序核心代码call_monitor.py如下import serial import sqlite3 import time import re from datetime import datetime # 串口参数请根据实际情况修改 DEVICE /dev/ttyUSB0 # Windows 下改为 COM3 之类 BAUD_RATE 115200 DB_PATH calls.db # 是否启用自动接听 AUTO_ANSWER True # 接听后保持通话秒数 KEEP_SECONDS 3 def save_call(phone, status, duration0): 保存一条来电记录 conn sqlite3.connect(DB_PATH) conn.execute( INSERT INTO calls (phone, status, duration, created_at) VALUES (?, ?, ?, ?), (phone, status, duration, datetime.now().isoformat()) ) conn.commit() conn.close() def send_at(ser, cmd, delay0.3): 发送 AT 指令并清空返回数据 ser.write((cmd \r).encode()) time.sleep(delay) ser.reset_input_buffer() def handle_incoming(ser, phone): 处理一次来电记录 - 接听 - 保持 - 挂断 print(f[来电] 号码: {phone}时间: {datetime.now().isoformat()}) save_call(phone, received) if not AUTO_ANSWER: print([策略] 已关闭自动接听仅记录号码) return # 1. 接听 print([动作] 发送 ATA 接听) ser.write(bATA\r) time.sleep(1) # 2. 保持通话 KEEP_SECONDS 秒 print(f[动作] 通话保持 {KEEP_SECONDS} 秒) time.sleep(KEEP_SECONDS) # 3. 挂断 print([动作] 发送 ATCHUP 挂断) ser.write(bATCHUP\r) time.sleep(0.5) ser.reset_input_buffer() save_call(phone, completed, durationKEEP_SECONDS) print([完成] 本次来电处理结束) print(- * 50) def main(): # 打开串口 ser serial.Serial(DEVICE, BAUD_RATE, timeout0.5) print(f串口 {DEVICE} 打开成功) # 初始化模块 send_at(ser, ATE0) # 关闭回显 send_at(ser, ATCLIP1) # 开启来电显示 print(模块初始化完成等待来电...) try: while True: line ser.readline().decode(errorsignore).strip() if not line: continue if line.startswith(RING): print([振铃] 检测到 RING) time.sleep(0.3) # 在 RING 之后读取来电显示信息 clip_line for _ in range(3): info ser.readline().decode(errorsignore).strip() if info: clip_line info break print([原始数据] , clip_line) # 从 CLIP: 13800138000,129,,,,,0 中解析号码 match re.search(r(\d), clip_line) phone match.group(1) if match else unknown handle_incoming(ser, phone) elif line.startswith(CLIP): # 部分设备会直接返回 CLIP 数据 match re.search(r(\d), line) phone match.group(1) if match else unknown save_call(phone, clip-only) except KeyboardInterrupt: print(\n用户中断程序退出) finally: ser.close() print(串口已关闭) if __name__ __main__: main()4.4 代码关键点解释这一段脚本虽然不长但有几个点很容易踩坑我逐个拆开讲。第一串口读取是异步的。ser.readline()会阻塞等待串口数据。当没有来电时脚本会一直停在readline()上。这里设置timeout0.5表示每 0.5 秒如果没有数据就返回空这样主循环可以继续执行。第二RING 和 CLIP 的到达顺序需要兼容。大多数模块先发RING再发CLIP。但少数模块可能先推送CLIP然后才出现RING。所以脚本里我同时处理了line.startswith(RING)和line.startswith(CLIP)两种情况避免漏掉号码。第三接听后一定要稍作停顿再挂断。发送ATA后模块需要短暂时间完成语音通道建立。如果立刻发ATCHUP有些模块会直接忽略挂断指令导致通话一直保持。所以脚本里time.sleep(1)之后再进入保持阶段。第四串口回显要关闭。ATE0指令的作用是关闭命令回显。如果不关闭串口会把发送过的指令内容原样返回干扰readline()对来电数据的解析。4.5 添加回拨功能除了被动接听我们还可以给系统加上回拨能力。比如需要验证某个号码今天是否已经来电可以基于历史记录批量回拨。下面的代码片段演示了如何拨打电话并获取结果def call_back(ser, phone): 回拨指定号码保持几秒后挂断 print(f[拨号] 正在呼叫 {phone}) ser.write(fATD{phone};\r.encode()) time.sleep(5) print([动作] 挂断) ser.write(bATCHUP\r) time.sleep(0.5) ser.reset_input_buffer()这里要注意ATD号码;后面必须带英文分号分号的作用是告诉模块这是语音呼叫不是数据呼叫。很多新手在这个地方漏掉分号结果模块一直返回NO CARRIER就是这个原因。4.6 运行与验证先启动主程序python call_monitor.py然后拿起另一部手机拨打语音 Modem 或 4G Dongle 所插 SIM 卡的号码。正常情况下控制台会输出类似下面的内容串口 /dev/ttyUSB0 打开成功 模块初始化完成等待来电... [振铃] 检测到 RING [原始数据] CLIP: 13800138000,129,,,,,0 [来电] 号码: 13800138000时间: 2025-06-18T15:30:22.123456 [动作] 发送 ATA 接听 [动作] 通话保持 3 秒 [动作] 发送 ATCHUP 挂断 [完成] 本次来电处理结束查看数据库sqlite3 calls.db select * from calls order by id desc limit 5;预期输出1|13800138000|received|0|2025-06-18T15:30:22.123456 2|13800138000|completed|3|2025-06-18T15:30:22.123456可以看到每次来电会产生两条记录一条是“检测到来电”一条是“处理完成”。如果你不希望记录重复可以在save_call中增加去重逻辑例如根据phone created_at判断是否已存在同一条记录。4.7 生产化用 systemd 托管服务如果你打算把脚本部署到树莓派或云服务器上长期运行可以用 systemd 托管掉线自动重启。创建/etc/systemd/system/call_robot.service[Unit] DescriptionCall Robot Service Afternetwork.target [Service] ExecStart/usr/bin/python3 /opt/call-robot/call_monitor.py WorkingDirectory/opt/call-robot Restartalways RestartSec5 Userroot [Install] WantedBymulti-user.target启动并设置开机自启sudo systemctl daemon-reload sudo systemctl start call_robot sudo systemctl status call_robot sudo systemctl enable call_robot如果你的脚本路径不是/opt/call-robot记得替换ExecStart和WorkingDirectory。5. 常见问题与排查思路我根据自己的使用经验整理了下面几个高频报错和排查方向。问题现象常见原因解决思路串口打开失败设备未识别、权限不足、端口被占用确认设备管理器或ls /dev/ttyUSB*Linux 下把用户加入 dialout 组关闭串口调试工具发送 AT 无响应波特率不匹配、线序问题尝试 9600、57600、115200 等常见波特率检查 USB 转串口线是否完好来电后没有 RING模块未开启语音呼叫或 SIM 卡无语音权限发送ATCLIP1开启来电显示用手机手动拨打确认号码能否通能识别 RING 但解析不到号码来电显示未开通、运营商未传主叫号码发送ATCLIP1联系运营商开通来电显示发送 ATA 后无法挂断语音通道建立时间不足在 ATA 后加time.sleep(1)再发送挂断指令拨号时一直出现 NO CARRIERATD后少了分号或号码格式不对确认命令格式为ATD号码;例如ATD13800138000;脚本运行一段时间后卡死串口缓冲未清理、设备掉线定期ser.reset_input_buffer()开启Restartalways自动重启如果你遇到上面没覆盖的问题推荐用串口助手手动发 AT 指令定位。先排除硬件链路问题再回到脚本排查效率会高很多。5.1 串口通信稳定性优化长时间运行时串口设备偶尔会出现无响应、缓冲堆积等问题。下面的“看门狗”思路可以显著提高稳定性主循环里增加无数据超时判断连续多久没有数据自动重启串口。每次处理完一次来电后主动ser.reset_input_buffer()清空历史缓冲。记录最近一次成功处理来电的时间超过阈值则重启程序。示例last_active time.time() while True: line ser.readline().decode(errorsignore).strip() if line: last_active time.time() elif time.time() - last_active 600: print([警告] 超过 10 分钟无数据尝试重置串口) ser.close() ser serial.Serial(DEVICE, BAUD_RATE, timeout0.5) init_modem() last_active time.time()这种设计在生产环境中非常实用尤其适合无人值守的长周期测试。6. 最佳实践与工程建议6.1 号码合规与最小权限原则这套系统本质上是操作真实电话线路的工具使用前一定要明确边界只对自有号码、授权测试号码运行自动接听或回拨。不要用来批量拨打陌生号码不要做任何形式的骚扰通话。不要私自录制通话语音如果需要录音必须事先告知通话对方并获取同意。如果用于企业呼叫中心自动化拨测需要走工单审批流程。最小权限原则同样适用于系统本身运行脚本的账号应尽量使用普通用户而不是 root数据库文件只允许当前服务账号读写不要在生产环境中随意关闭防火墙或暴露调试端口。6.2 日志与数据结构化只把数据存进 SQLite 是不够的。建议增加文件日志方便排查问题。示例在handle_incoming中追加日志写入def log_event(message): with open(call_robot.log, a, encodingutf-8) as f: f.write(f{datetime.now().isoformat()} {message}\n)然后在关键动作处调用log_event(fcall from {phone}, statusaccepted) log_event(fcall from {phone}, statuscompleted, duration{KEEP_SECONDS})如果想把日志与业务数据分开也可以接入 logging 模块按天滚动切割日志文件。对于个人项目最简单的logging.basicConfig就足够import logging logging.basicConfig( filenamecall_robot.log, levellogging.INFO, format%(asctime)s %(levelname)s %(message)s, )6.3 号码识别策略实际环境中来电可能来自座机、手机、国际号码或隐藏号码。解析策略要注意手机号码一般以86开头去掉前缀后为 11 位。座机号码可能带区号长度不固定。隐藏号码可能返回P或空串此时不要强行解析。示例增强解析逻辑def normalize_phone(raw_phone: str) - str: 清洗号码去掉 86 前缀、空格和中划线 phone re.sub(r[\s\-], , raw_phone) if phone.startswith(86): phone phone[3:] return phone解析时优先匹配CLIP中带引号的号码段匹配不到则返回unknown并在后续统计时把unknown单独归类不影响整体报表。6.4 数据库备份策略SQLite 虽然轻量但长期写数据后需要注意备份与归档建议每天定时把calls.db复制到备份目录。超过一定时间的数据可以导出为 CSV 后清空主表。数据库文件不要放在网络共享盘上避免并发写入锁定。Linux 下可以用 crontab 做定时备份0 2 * * * sqlite3 /opt/call-robot/calls.db .backup /opt/call-robot/backup/calls_$(date \%Y\%m\%d).db6.5 扩展方向这套系统只是打电话自动化的冰山一角继续往下扩展可以做成语音提示音播放在接听后播放预设的 WAV 音频需要模块支持语音播放或额外接语音网关。呼叫转接根据来电号码匹配不同部门转接到第二路线路。短信自动回复结合短信猫模块在漏接后自动回一条短信。Web 管理后台用 Flask 或 FastAPI 展示来电记录、统计图表、黑白名单配置。AI 语音对话接入语音合成与识别接口让机器人完成简单的自助问答。每种扩展都会引入新的技术点但核心的“来电检测 → 自动接听 → 结构化记录”骨架不会变。7. 总结与后续演进这篇文章从一个“给 100 个野人接电话”的整活标题出发把整个来电自动接听流程完整拆解了一遍从硬件选型、AT 指令原理、串口通信到 Python 代码实现、数据库记录、常见问题排查再到生产环境部署与合规边界。整个项目核心代码量不算大但它涉及的知识点非常综合串口通信、硬件指令集、正则解析、数据库操作、进程托管、问题排查。任何一个环节出问题都可能让脚本“听到铃声却不接电话”所以我强烈建议你先用串口助手手动发一遍 AT 指令把硬件链路跑通之后再运行 Python 脚本。如果你之前没接触过串口编程建议在测试时多观察控制台输出对比RING、CLIP、OK等关键字符串的返回节奏。理解硬件设备的“性格”之后再复杂的自动接听规则也只是在这个基础上加逻辑而已。下一步你可以继续研究语音网关方案通过 SIP 协议对接软交换实现并发多路呼叫处理也可以研究如何把来电记录接入企业微信或钉钉机器人告警实现“漏接电话实时通知到手机”。这套自动接听做出来之后至少以后再有陌生号码打来就不用担心错过什么重要测试了。如果这篇文章对你有启发或者你在复现过程中遇到了其他问题可以按文章里的排查思路多试几轮。技术踩坑就是这样把坑填平了后面就顺了。