news 2026/9/5 2:03:50

Python+Selenium实战:12306抢票脚本从零到一的工程化构建

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python+Selenium实战:12306抢票脚本从零到一的工程化构建

1. 从零开始:为什么我们需要一个“工程化”的抢票脚本?

大家好,我是老张,一个在自动化领域摸爬滚打了十来年的老码农。相信很多朋友都有过在12306上抢票的“痛苦”经历,手速再快也快不过脚本,对吧?所以,自己动手写一个抢票脚本,就成了很多Python学习者的一个经典练手项目。网上随便一搜,就能找到一大堆“几十行代码搞定12306抢票”的教程。我刚开始也是照着这些教程写,确实能跑起来,也能登录、查票。

但问题很快就来了。脚本在我自己电脑上跑得好好的,换台机器就各种报错;抢票过程中网络一波动,脚本就直接崩溃,还得手动重启;想改个出发站或者乘车人,得在一大坨代码里翻来覆去地找,生怕改错了地方。更别提多人协作或者想定时运行了,简直一团乱麻。这让我意识到,一个能“跑起来”的脚本,和一个健壮、可维护、能真正交付使用的“软件工程”项目,中间隔着十万八千里。

我们今天要聊的,就是后面这条路。我们不满足于仅仅实现功能,我们要构建一个工程化的抢票系统。这意味着什么?意味着你的代码要有清晰的结构,像搭积木一样模块分明;意味着配置要和代码分离,改个日期不用去动核心逻辑;意味着要有完善的错误处理和日志,脚本挂了也能知道死在哪里;还意味着要方便部署和协作,你自己能跑,你同事也能一键跑起来。

这听起来好像很复杂,但其实每一步我们都可以用很“Pythonic”的方式,优雅地解决。跟着我的思路走,你会发现,把脚本升级为工程,不仅能让抢票更稳定,更是你编程能力的一次重要跃升。你会开始用软件工程的思维去解决问题,这才是最有价值的收获。好了,闲话不多说,我们直接进入实战。

2. 打好地基:项目初始化与模块化设计

写代码和盖房子一样,地基没打好,后面全是麻烦。我们第一步要做的,就是建立一个清晰、规范的项目结构。别再把所有代码都塞进一个叫12306.py的文件里了,那样只会让你自己都头疼。

2.1 创建标准的项目目录

打开你的命令行或终端,我们从头开始。我习惯为每个项目创建一个独立的目录,并用虚拟环境隔离依赖。

# 创建项目根目录 mkdir 12306_ticket_engine cd 12306_ticket_engine # 创建Python虚拟环境(强烈推荐,避免包冲突) python -m venv venv # 激活虚拟环境 # Windows: venv\Scripts\activate # macOS/Linux: source venv/bin/activate

激活后,你的命令行前面会出现(venv)的标识。接下来,我们创建核心的目录结构。这个结构是我在多个项目中总结出来的,兼顾了清晰度和灵活性。

12306_ticket_engine/ ├── config/ # 存放所有配置文件 │ ├── settings.yaml # 主配置文件(如账号、车次信息) │ └── paths.yaml # 路径配置文件(如驱动位置) ├── core/ # 核心业务逻辑模块 │ ├── __init__.py │ ├── login.py # 登录模块 │ ├── query.py # 查票模块 │ ├── order.py # 下单模块 │ └── driver_manager.py # 浏览器驱动管理模块 ├── utils/ # 工具函数模块 │ ├── __init__.py │ ├── logger.py # 日志工具 │ ├── notifier.py # 通知工具(如邮件、微信) │ └── captcha_solver.py # 验证码处理工具(可选) ├── tests/ # 测试代码 ├── logs/ # 日志文件目录(程序自动生成) ├── drivers/ # 存放浏览器驱动(如chromedriver) ├── requirements.txt # 项目依赖清单 └── main.py # 程序主入口

你可以用mkdirtouch命令逐一创建这些文件和文件夹。这个结构的好处一目了然:功能分离。登录的代码不会和查票的代码搅在一起,工具函数独立存放,配置文件集中管理。以后你想优化登录逻辑,直接去core/login.py里改就行,不会影响到其他部分。

2.2 使用requirements.txt管理依赖

依赖管理是工程化的第一步。我们创建一个requirements.txt文件,列出项目需要的所有第三方库。

selenium==4.15.0 PyYAML==6.0.1 requests==2.31.0 python-dotenv==1.0.0 schedule==1.2.0

这里除了核心的selenium,我还引入了几个非常重要的库:

  • PyYAML: 用来读写 YAML 格式的配置文件。YAML 比 JSON 更易读,比configparser的 ini 格式更强大,非常适合人类编写配置。
  • requests: 虽然我们主要用 Selenium 操作浏览器,但有时可能需要用 API 接口辅助查询或通知,requests是必备的。
  • python-dotenv: 用于加载环境变量,可以把敏感信息(如密码)从代码中剥离。
  • schedule: 一个轻量级定时任务库,方便我们设置定时抢票。

在项目根目录下,执行pip install -r requirements.txt,就能一键安装所有依赖。记得每次新增库都要更新这个文件。

2.3 设计核心模块的接口

模块化不是简单地把代码分到不同文件,而是要设计好模块之间的“接口”或“契约”。我们以登录模块为例,看看怎么设计。

core/login.py中,我们不应该直接写死账号密码,也不应该把整个浏览器驱动对象driver的创建逻辑都塞进来。它的职责应该很单纯:给定驱动和凭据,执行登录操作,并返回登录成功与否的状态

# core/login.py import time from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from utils.logger import get_logger logger = get_logger(__name__) class LoginModule: def __init__(self, driver): """ 初始化登录模块。 :param driver: 已经创建好的 WebDriver 实例 """ self.driver = driver self.wait = WebDriverWait(driver, 10) def login(self, username, password): """ 执行登录操作。 :param username: 用户名 :param password: 密码 :return: True 登录成功, False 登录失败 """ logger.info(f"开始尝试登录,用户名: {username}") try: # 1. 访问登录页 self.driver.get("https://kyfw.12306.cn/otn/resources/login.html") # 2. 点击“账号登录”选项卡(示例,以实际页面为准) account_login_tab = self.wait.until( EC.element_to_be_clickable((By.CLASS_NAME, "login-hd-account")) ) account_login_tab.click() # 3. 输入用户名密码 user_input = self.wait.until( EC.presence_of_element_located((By.ID, "J-userName")) ) pwd_input = self.driver.find_element(By.ID, "J-password") user_input.send_keys(username) pwd_input.send_keys(password) # 4. 点击登录按钮 login_btn = self.driver.find_element(By.ID, "J-login") login_btn.click() # 5. 等待登录成功后的页面元素出现(例如用户昵称) self.wait.until( EC.presence_of_element_located((By.CLASS_NAME, "my12306")) ) logger.info("登录成功!") return True except Exception as e: logger.error(f"登录过程发生异常: {e}") # 这里可以截图,保存错误现场 self.driver.save_screenshot(f"logs/login_error_{int(time.time())}.png") return False

你看,这个模块只关心“登录”这件事。它需要什么(driver, username, password),产出什么(True/False),非常清晰。验证码处理是一个更复杂的话题,我们可以选择集成第三方打码平台,或者先手动处理。我们可以把验证码处理的逻辑抽象到utils/captcha_solver.py中,然后在登录模块里调用。这样,即使以后换一种验证码解决方案,也只需要改工具模块,登录模块的主体逻辑不变。这就是高内聚、低耦合的设计思想。

3. 让脚本“听话”:配置管理与环境隔离

把账号、车次、日期这些经常变动的信息硬编码在脚本里,是初级开发者常犯的错误。每次修改都要动代码,既危险又麻烦。我们需要一个配置中心

3.1 使用 YAML 管理配置

我强烈推荐使用 YAML 格式来管理配置。它在config/settings.yaml里。

# config/settings.yaml user: username: "your_12306_account" password: "your_password" # 强烈建议首次运行后,改用环境变量 ticket: from_station: "北京" to_station: "上海" date: "2024-01-20" train_numbers: ["G1", "G3", "G5"] # 优先车次,按顺序尝试 passenger_names: ["张三"] seat_type: "二等座" schedule: enable: true query_interval: 5 # 查询间隔,单位秒,不宜过短 start_time: "08:00" end_time: "23:00" notification: enable: true type: "email" # 支持 email, bark, serverchan 等 email: smtp_server: "smtp.qq.com" smtp_port: 465 sender: "your_email@qq.com" password: "" # 建议用环境变量或授权码 receiver: "target_email@example.com"

YAML 的层次结构非常清晰,一眼就能看懂每个配置项的意义。在代码中,我们这样读取它:

# utils/config_loader.py import yaml import os from pathlib import Path def load_config(): config_path = Path(__file__).parent.parent / 'config' / 'settings.yaml' with open(config_path, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) # 可以在这里做一些配置的校验和默认值填充 if not config['schedule'].get('query_interval'): config['schedule']['query_interval'] = 10 return config

3.2 用环境变量保护敏感信息

把密码直接写在配置文件里,一旦把代码上传到GitHub,就等于公开了。我们需要用环境变量来保护它们。我们可以使用python-dotenv库。

首先,在项目根目录创建.env文件(切记要把这个文件加入.gitignore,不要提交!):

# .env _12306_PASSWORD=your_real_password_here EMAIL_PASSWORD=your_email_auth_code_here

然后,在代码中读取:

# 在主程序或配置加载器中 from dotenv import load_dotenv import os load_dotenv() # 加载 .env 文件中的环境变量 password = os.getenv('_12306_PASSWORD') if not password: raise ValueError("请在 .env 文件中设置 _12306_PASSWORD 环境变量") # 更新配置字典 config['user']['password'] = password

这样,你的敏感信息就只存在于本地的.env文件中,代码和公开的配置文件里都是安全的。

3.3 路径配置与驱动管理

浏览器的驱动(如chromedriver)路径也是个问题。有人放C盘,有人放项目里。我们可以用一个单独的配置文件config/paths.yaml来管理。

# config/paths.yaml drivers: chrome: "drivers/chromedriver" # 相对项目根目录的路径 # chrome: "/usr/local/bin/chromedriver" # 也可以是绝对路径

在驱动管理模块里读取这个配置:

# core/driver_manager.py from selenium import webdriver from selenium.webdriver.chrome.options import Options import yaml import os def create_driver(): # 加载路径配置 with open('config/paths.yaml', 'r') as f: paths = yaml.safe_load(f) driver_path = paths['drivers']['chrome'] # 配置浏览器选项(无头模式、禁用图片等,可以加速) chrome_options = Options() # chrome_options.add_argument('--headless') # 无头模式,调试时可关闭 chrome_options.add_argument('--disable-gpu') chrome_options.add_argument('--no-sandbox') chrome_options.add_argument('--disable-dev-shm-usage') prefs = {"profile.managed_default_content_settings.images": 2} # 不加载图片 chrome_options.add_experimental_option("prefs", prefs) # 创建驱动实例 driver = webdriver.Chrome(executable_path=driver_path, options=chrome_options) driver.set_window_size(1400, 900) # 设置一个合适的窗口大小 return driver

通过这种配置化的管理,当你把项目分享给朋友时,他只需要修改config/目录下的几个文件和.env文件,就能让脚本在他的电脑上跑起来,完全不需要动你的核心代码。

4. 程序的“黑匣子”:健壮的错误处理与日志系统

一个动不动就崩溃的脚本是没用的。我们需要它足够健壮,遇到问题能自己处理,或者至少清楚地告诉我们它“死”在哪里。这就是错误处理和日志系统的价值。

4.1 构建全局异常捕获与重试机制

网络波动、元素加载慢、验证码识别失败……这些在抢票过程中太常见了。我们不能让一次失败就终止整个程序。我们需要一个**装饰器(Decorator)**来实现智能重试。

# utils/retry.py import time from functools import wraps from utils.logger import get_logger logger = get_logger(__name__) def retry_on_failure(max_retries=3, delay=2, exceptions=(Exception,)): """ 一个重试装饰器。 :param max_retries: 最大重试次数 :param delay: 每次重试间隔(秒) :param exceptions: 触发重试的异常类型 """ def decorator(func): @wraps(func) def wrapper(*args, **kwargs): retries = 0 while retries < max_retries: try: return func(*args, **kwargs) except exceptions as e: retries += 1 logger.warning(f"函数 {func.__name__} 执行失败,第 {retries} 次重试。错误: {e}") if retries >= max_retries: logger.error(f"函数 {func.__name__} 重试 {max_retries} 次后仍失败。") raise # 重试耗尽,抛出异常 time.sleep(delay) return None return wrapper return decorator

这个装饰器可以灵活地用在任何可能失败的函数上。比如,查询余票的请求很容易因网络问题失败,我们就可以给它加上重试:

# core/query.py from utils.retry import retry_on_failure from selenium.common.exceptions import TimeoutException, NoSuchElementException class QueryModule: # ... 其他代码 ... @retry_on_failure(max_retries=2, delay=3, exceptions=(TimeoutException, NoSuchElementException)) def query_tickets(self, from_station, to_station, date): logger.info(f"查询 {date} {from_station} -> {to_station} 的车票") # ... 具体的查询逻辑 ... # 如果这里因为元素没找到或超时抛出异常,装饰器会自动重试2次

4.2 搭建分层级的日志系统

print()语句是调试的利器,但用于生产环境就太弱了。我们需要一个能记录不同级别信息(调试、信息、警告、错误)、能输出到文件、能按日期滚动的日志系统。Python自带的logging模块就非常强大。

# utils/logger.py import logging import sys from pathlib import Path from logging.handlers import TimedRotatingFileHandler def get_logger(name, log_level=logging.INFO): """获取一个配置好的日志记录器""" logger = logging.getLogger(name) if logger.handlers: # 防止重复添加handler return logger logger.setLevel(log_level) # 格式 formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) # 控制台处理器 console_handler = logging.StreamHandler(sys.stdout) console_handler.setFormatter(formatter) logger.addHandler(console_handler) # 文件处理器(按天滚动) log_dir = Path("logs") log_dir.mkdir(exist_ok=True) file_handler = TimedRotatingFileHandler( filename=log_dir / 'ticket_engine.log', when='midnight', # 每天午夜滚动 interval=1, backupCount=7, # 保留最近7天的日志 encoding='utf-8' ) file_handler.setFormatter(formatter) logger.addHandler(file_handler) return logger

在每个模块的开头,我们这样获取自己的日志记录器:

# 在 core/login.py 等模块顶部 from utils.logger import get_logger logger = get_logger(__name__) # __name__ 会自动变成 'core.login'

然后,在代码中根据情况记录:

logger.debug("这是一个调试信息,通常记录变量值。") # 默认不显示,需要时开启 logger.info("用户登录成功。") logger.warning("查询响应较慢,可能网络拥堵。") logger.error("找不到‘提交订单’按钮,页面结构可能已变更!")

运行程序后,你会在logs/目录下看到ticket_engine.log文件,里面按时间记录了所有操作。当脚本出错时,第一时间去翻日志文件,比盲目猜测高效一百倍。

4.3 关键操作截图与状态上报

对于像点击“提交订单”这种最关键的操作,除了记录日志,我们还可以让它自动截图,保存出错时的页面状态。同时,结合通知模块,在成功抢到票或发生严重错误时,立即给你发邮件或微信消息。

# core/order.py class OrderModule: def __init__(self, driver, notifier): self.driver = driver self.notifier = notifier # 通知器实例 def submit_order(self): try: submit_btn = self.wait.until(EC.element_to_be_clickable((By.ID, "submitOrder_id"))) submit_btn.click() logger.info("已点击提交订单按钮。") # 检查是否出现支付页面 self.wait.until(EC.title_contains("支付")) logger.critical("!!!抢票成功,请及时支付!!!") self.notifier.send("抢票成功通知", "您预订的车票已成功锁定,请立即登录12306支付!") return True except Exception as e: error_img_path = f"logs/order_error_{int(time.time())}.png" self.driver.save_screenshot(error_img_path) logger.error(f"提交订单失败,已截图: {error_img_path}, 错误: {e}") self.notifier.send("抢票脚本异常", f"提交订单环节出错:{e},请查看日志和截图。") return False

这样,你的脚本就从一个脆弱的“玩具”,变成了一个拥有自我诊断和报告能力的“哨兵”。你可以安心地让它运行,而不必一直守在电脑前。

5. 组装与运行:从散装脚本到完整应用

模块、配置、日志都准备好了,现在我们需要一个“指挥官”把它们组装起来,并指挥运行。这就是main.py的职责。

5.1 编写主程序流程

主程序的逻辑应该清晰得像一个流程图:初始化 -> 登录 -> 循环查询 -> 下单 -> 结束。我们使用我们之前设计好的各个模块。

# main.py import time from core.driver_manager import create_driver from core.login import LoginModule from core.query import QueryModule from core.order import OrderModule from utils.config_loader import load_config from utils.logger import get_logger from utils.notifier import EmailNotifier # 假设我们实现了邮件通知器 logger = get_logger(__name__) def main(): logger.info("="*50) logger.info("12306抢票引擎启动") logger.info("="*50) # 1. 加载配置 config = load_config() user_cfg = config['user'] ticket_cfg = config['ticket'] schedule_cfg = config['schedule'] notify_cfg = config['notification'] # 2. 初始化驱动和模块 driver = create_driver() notifier = EmailNotifier(notify_cfg['email']) if notify_cfg['enable'] else None login_module = LoginModule(driver) query_module = QueryModule(driver) order_module = OrderModule(driver, notifier) if notifier else OrderModule(driver) try: # 3. 登录 if not login_module.login(user_cfg['username'], user_cfg['password']): logger.error("登录失败,程序退出。") return # 4. 主循环:查询与抢票 logger.info(f"开始监控 {ticket_cfg['date']} {ticket_cfg['from_station']} -> {ticket_cfg['to_station']} 的车票") while True: try: # 执行一次查询 query_result = query_module.query_tickets( ticket_cfg['from_station'], ticket_cfg['to_station'], ticket_cfg['date'] ) if query_result: # 假设query_result返回有余票的车次信息 logger.info(f"发现符合条件的车次: {query_result}") # 尝试为每个乘客下单 success = order_module.create_order(query_result, ticket_cfg['passenger_names']) if success: logger.critical("抢票流程全部完成!") break # 成功则跳出循环 # 根据配置间隔等待 time.sleep(schedule_cfg['query_interval']) except KeyboardInterrupt: logger.info("用户手动中断程序。") break except Exception as e: logger.error(f"主循环发生未知错误: {e}", exc_info=True) # exc_info会打印堆栈跟踪 time.sleep(10) # 发生未知错误,等待稍长时间后继续 finally: # 5. 清理资源 logger.info("正在关闭浏览器...") driver.quit() logger.info("程序退出。") if __name__ == "__main__": main()

5.2 实现定时与守护运行

我们之前引入了schedule库,可以轻松实现定时任务。比如,我们想让脚本只在每天早上8点到晚上10点之间运行。

# 在主程序循环前,加入定时判断 import schedule from datetime import datetime def job(): """定时任务要执行的函数,其实就是我们上面的主循环体""" # ... 把上面的查询抢票逻辑封装成一个函数放这里 ... # 配置定时规则 if schedule_cfg['enable']: start_time = schedule_cfg['start_time'] end_time = schedule_cfg['end_time'] logger.info(f"定时任务已启用,运行时段: {start_time} - {end_time}") def time_in_range(): now = datetime.now().time() return datetime.strptime(start_time, '%H:%M').time() <= now <= datetime.strptime(end_time, '%H:%M').time() while True: if time_in_range(): job() # 执行抢票任务 time.sleep(schedule_cfg['query_interval']) # 执行后等待 else: current_time = datetime.now().strftime('%H:%M') logger.info(f"当前时间 {current_time} 不在运行时段内,休眠1分钟。") time.sleep(60) # 非运行时段,每分钟检查一次 else: # 非定时模式,直接运行一次job() job()

对于需要长时间在服务器后台运行的场景,你可以使用systemd(Linux) 或nssm(Windows) 将你的 Python 脚本注册为系统服务,实现开机自启和进程守护。这超出了本文的范围,但这是工程化部署的必经之路。

5.3 版本控制与协作建议

最后,别忘了使用 Git 进行版本控制。在项目根目录初始化仓库:

git init

创建一个合理的.gitignore文件,忽略不需要提交的内容:

# .gitignore venv/ __pycache__/ *.py[cod] *$py.class *.so .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # 日志和截图 logs/ *.log *.png # 环境变量和本地配置 .env config/local_*.yaml # 浏览器驱动(因为不同系统驱动不同) drivers/* !drivers/.gitkeep # 可以保留一个空文件占位

将代码提交到仓库,并推送到 GitHub 或 Gitee 等远程平台。这样,你的代码就有了备份,也方便团队协作。在README.md中详细写下项目的配置步骤、运行方法和注意事项,让任何一个接手的人都能快速上手。

走到这一步,你的“12306抢票脚本”已经脱胎换骨,成为一个结构清晰、配置灵活、异常健壮、便于协作的自动化工程。这个过程里学到的模块化设计、配置管理、日志处理和工程思维,远比抢到一张票本身更有价值。技术之路,就是不断把简单的事情做规范,把复杂的事情拆解简单。希望这个从零到一的工程化构建过程,能给你带来启发。如果在实践中遇到任何问题,不妨回头看看日志,那里面通常藏着答案。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/5 2:02:08

Janus-Pro-7B效果深度评测:对比YOLOv8的目标检测与描述能力

Janus-Pro-7B效果深度评测&#xff1a;对比YOLOv8的目标检测与描述能力 1. 引言 最近在测试一些多模态模型时&#xff0c;我遇到了一个挺有意思的模型叫Janus-Pro-7B。它号称不仅能像传统视觉模型那样“看见”物体&#xff0c;还能“理解”场景&#xff0c;并用自然语言描述出…

作者头像 李华
网站建设 2026/9/3 5:46:25

重新定义桌面交互:UI-TARS Desktop的智能效率突破

重新定义桌面交互&#xff1a;UI-TARS Desktop的智能效率突破 【免费下载链接】UI-TARS-desktop A GUI Agent application based on UI-TARS(Vision-Lanuage Model) that allows you to control your computer using natural language. 项目地址: https://gitcode.com/GitHub…

作者头像 李华
网站建设 2026/9/4 14:26:34

开源虚拟化工具UTM全解析:跨设备解决方案与性能优化指南

开源虚拟化工具UTM全解析&#xff1a;跨设备解决方案与性能优化指南 【免费下载链接】UTM Virtual machines for iOS and macOS 项目地址: https://gitcode.com/gh_mirrors/ut/UTM 一、核心价值解析&#xff1a;为什么选择UTM虚拟化工具 在多设备协作日益普遍的今天&am…

作者头像 李华
网站建设 2026/9/4 9:27:32

AI写春联新体验:皇城大门春联生成终端实测,输入愿望词秒出对联

AI写春联新体验&#xff1a;皇城大门春联生成终端实测&#xff0c;输入愿望词秒出对联 1. 引言&#xff1a;当传统年俗遇上AI创意 春节贴春联&#xff0c;是刻在我们文化记忆里的仪式感。但每年绞尽脑汁想一副好对联&#xff0c;或者在网上搜索那些千篇一律的模板&#xff0c…

作者头像 李华
网站建设 2026/8/25 18:12:47

Python对象和实例属性

#type() 来判断对象类型 #判断基本数据类型可以直接写int&#xff0c;str等 print( type(123)type(456))#判断一个对象是否是函数怎么办&#xff1f;可以使用types模块中定义的常量&#xff1a; import types type(abs)types.BuiltinFunctionType 使用dir() 如果要获得一个对象…

作者头像 李华
网站建设 2026/8/25 17:57:04

CosyVoice GPU 入门实战:从零搭建高效语音处理流水线

最近在做一个语音处理的实时应用&#xff0c;发现用传统的CPU方案处理音频流时&#xff0c;延迟总是下不来&#xff0c;尤其是在做降噪和特征提取的时候&#xff0c;CPU占用率直接拉满&#xff0c;吞吐量却上不去。这让我开始寻找GPU加速的方案&#xff0c;最终把目光投向了Cos…

作者头像 李华