28. 实战:基于selenium实现12306自动购票

news2024/11/17 4:48:47

目录

前言

目的

思路

代码实现

1. 进入登录界面,输入账号密码

2. 点击登录按钮,完成滑块验证

3. 在个人中心点击购票,跳转

4. 输入出发地、目的地,从控制台输入得到

5. 文本框输入出发日

6. 若是学生票则切换票型

7. 点击查询

8. 定位预定按钮,点击跳转购票页面

9. 选择学生乘客,并在弹窗中确认购买学生票

10. 提交订单,等待付款

完整代码

运行效果

总结


前言

我们已经学会了selenium的基本操作,并且学会了用它处理验证码、跳转网页、处理内联框架等操作,现在可以进行实战:本节选取12306火车购票作为案例,用自动化测试工具selenium实现自动访问网页并下单等待购票。

2023-01-20更新:完善了全部功能并可以完整运行


目的

手动在控制台输入乘车人(新增), 出发地、目的地、出发日、是否购买学生票,确认后自动跳转12306网站购票。


思路

1. 首先获取登陆页面的URL,随后定位账号密码的输入框,用sendkey接口输入个人信息;

2. 获取登录控件的XPATH地址,点击发现弹窗出现滑块验证,使用drag_and_drop_by_offset接口实现拖拽滑块到终点的操作;

3. 登陆以后默认在个人中心,获取购票按钮XPATH地址,点击访问;

4. 分析购票界面,点击文本框以后可以清空当前文本框,所以动作链应当为:点击 -> 输入 -> 按下回车。因为输入以后会弹出选项,所以我们还得点一下回车,直接切换其他文本框会清空;

5. 出发日文本框点击的时候不会清空,所以用clear接口清空文本框,然后输入正确的日期格式yyyy-mm-dd形式;

6. 如果是学生票,切换学生票;

7. 点击查询,拿到可预定车票列表;

8. 用显示等待定位预定按钮,点击跳转购票页面;

9. 用条件等待选择学生乘客,并在弹窗中确认购买学生票 ; 或直接选择一般乘客购票;

10. 提交订单,等待付款。


代码实现

1. 进入登录界面,输入账号密码

opt = Options()
# option.add_experimental_option('excludeSwitches', ['enable-automation'])
opt.add_argument('--disable-blink-features=AutomationControlled')
opt.add_experimental_option('detach', True)
opt.add_argument('--start-maximized')  # 浏览器窗口最大

web = Chrome(options=opt)

web.get("https://kyfw.12306.cn/otn/resources/login.html")

web.find_element(By.XPATH, '//*[@id="toolbar_Div"]/div[2]/div[2]/ul/li[1]/a').click()

time.sleep(1)

# TODO 输入用户名和密码
web.find_element(By.XPATH, '//*[@id="J-userName"]').send_keys("username")
web.find_element(By.XPATH, '//*[@id="J-password"]').send_keys("password")

2. 点击登录按钮,完成滑块验证

# 点击登录
web.find_element(By.XPATH, '//*[@id="J-login"]').click()

time.sleep(3)

# 拖拽
btn = web.find_element(By.XPATH, '//*[@id="nc_1_n1z"]')
ActionChains(web).drag_and_drop_by_offset(btn, 300, 0).perform()
time.sleep(3)

ActionChains里面有许多动作序列,可以帮助我们完成许多仿人类动作,记得在最后加perform,不然动作序列是不会执行的。

3. 在个人中心点击购票,跳转

# 车票预定
web.find_element(By.XPATH, '//*[@id="link_for_ticket"]').click()

4. 输入出发地、目的地,从控制台输入得到

从控制台获得信息:

更新 : 增加了乘车人,便于定位后续购票人

# 初始化购票信息
print("***欢迎使用自动购票系统***")
print("请依次输入购票信息...")
print("=" * 30)
fromStationText = input("请输入出发地(示例:介休东)...\n")
toStationText = input("请输入目的地(示例:成都东)...\n")
train_date = input("请输入出发日(示例:2023-01-19)...\n")
is_student = input("是否购买学生票?(y/n)\n")
print("=" * 30)
print("处理信息中...\n", "处理完毕,请检查您输入的信息...\n", fromStationText, toStationText, train_date)
confirm = input("是否确认上述信息?(y/n)\n")
if confirm == 'y':
    print("=" * 30)
    print("初始化完毕,开始运行系统...")
if confirm == 'n':
    print("=" * 30)
    print("请重新运行程序!")
    exit(1)

修改:可以精简sendkeys操作,将它们放入一行:将信息输入文本框:

# 输入信息(出发地、目的地、出发日)

# 出发地
web.find_element(By.XPATH, '//*[@id="fromStationText"]').click()
web.find_element(By.XPATH, '//*[@id="fromStationText"]').send_keys(fromStationText, Keys.ENTER)
time.sleep(1)

# 目的地
web.find_element(By.XPATH, '//*[@id="toStationText"]').click()
web.find_element(By.XPATH, '//*[@id="toStationText"]').send_keys(toStationText, Keys.ENTER)
time.sleep(1)

5. 文本框输入出发日

# 出发日
# date = web.find_element(By.XPATH, '//*[@id="train_date"]')
# ActionChains(web).drag_and_drop_by_offset(date, 175, 0).perform()
web.find_element(By.XPATH, '//*[@id="train_date"]').clear()
web.find_element(By.XPATH, '//*[@id="train_date"]').send_keys(train_date)
time.sleep(1)

6. 若是学生票则切换票型

# 点击查询
if is_student == 'y':
    web.find_element(By.XPATH, '//*[@id="sf2_label"]').click()
    time.sleep(1)

7. 点击查询

# 点击查询
web.find_element(By.XPATH, '//*[@id="query_ticket"]').click()
print("=" * 30)
print("查询完毕...")
time.sleep(1)

8. 定位预定按钮,点击跳转购票页面

定位思路有两种,一个是直接找到控件,另一个是相对查找,这里我选用第二种。由于第一种的控件地址隐藏较深,无法直接在源代码定位,藏在js里面,所以我们直接用标头的最后一项相对查找就行了。

# 点击预定
# TODO 待办:定位预订控件
get_ticket = web.find_element(By.XPATH, '//*[@id="float"]/th[16]')
ActionChains(web).move_to_element_with_offset(get_ticket, 55, 70).click().perform()

但是这样很难精准定位,不能适用所有网页,所以还是实践第一种方法:找到控件:

最终确认方法:显示等待

# 点击预定
WebDriverWait(web, 1000).until(EC.presence_of_element_located((By.XPATH, '//*[@id="queryLeftTable"]/tr')))
tr_list = web.find_elements(By.XPATH, '//*[@id="queryLeftTable"]/tr[not(@datatran)]')  # 每一列列车整行信息列表,列车号元素是tr的子元素
if not tr_list:
    print("=" * 30)
    print(f"很抱歉,按您的查询条件,当前未找到从{fromStationText}到{toStationText}的列车。")
    exit(1)
for tr in tr_list:
    train_num = tr.find_element(By.XPATH, './td[1]/div/div[1]/div/a').text  # 取出元素tr里的列车号
    # 动车二等座余票信息
    text_1 = tr.find_element(By.XPATH, "./td[4]").text
    # 火车二等座余票信息
    text_2 = tr.find_element(By.XPATH, "./td[8]").text
    if (text_1 == "有" or text_1.isdigit()) or (text_2 == "有" or text_2.isdigit()):
        # 点击预订按钮
        order_btn = tr.find_element(By.CLASS_NAME, "btn72")
        order_btn.click()
        # 等待订票页面
        WebDriverWait(web, 1000).until(EC.url_to_be('https://kyfw.12306.cn/otn/confirmPassenger/initDc'))
        print("=" * 30)
        print(train_num, "二等座有票!")
        break
    else:
        print("=" * 30)
        print(train_num, "二等座无票!")
        continue

9. 选择学生乘客,并在弹窗中确认购买学生票

修改:用expected condition(EC)选取处理弹窗事件。

# 跳转页面提交订单
# 选定乘车人
web.find_element(By.XPATH, f'//*[@id="normal_passenger_id"]/li/label[contains(text(),"{passenger}")]').click()
# 如果乘客是学生,对提示点击确定
if EC.presence_of_element_located((By.XPATH, '//div[@id="dialog_xsertcj"]')):
    web.find_element(By.ID, 'dialog_xsertcj_ok').click()
    # 提交订单
    web.find_element(By.ID, 'submitOrder_id').click()
    time.sleep(2)
else:
    # 提交订单
    web.find_element(By.ID, 'submitOrder_id').click()
    time.sleep(2)

10. 提交订单,等待付款

修改:更新了座位ID无法找到的问题,向上定位XPATH再往后确认

# 选座
print("=" * 30)
seat = input("请尽快进行选座操作([窗]A/B/C/[过道]/D/F[窗])\n")
if seat == 'A':
    web.find_element(By.XPATH, '//*[@id="erdeng1"]/ul[1]/li[1]/a').click()
if seat == 'B':
    web.find_element(By.XPATH, '//*[@id="erdeng1"]/ul[1]/li[2]/a').click()
if seat == 'C':
    web.find_element(By.XPATH, '//*[@id="erdeng1"]/ul[1]/li[3]/a').click()
if seat == 'D':
    web.find_element(By.XPATH, '//*[@id="erdeng1"]/ul[2]/li[1]/a').click()
if seat == 'F':
    web.find_element(By.XPATH, '//*[@id="erdeng1"]/ul[2]/li[2]/a').click()

# 最终确认
web.find_element(By.XPATH, '//*[@id="qr_submit_id"]').click()

print("=" * 30)
print("*"*40, "\n---***<|订单创建完成,请于10分钟内付款|>***---")
print("*"*40)


完整代码

from selenium.webdriver import Chrome
from selenium.webdriver.common.action_chains import ActionChains
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.wait import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

import time

# 初始化购票信息
print("***欢迎使用自动购票系统***")
print("请依次输入购票信息...")
print("=" * 30)
passenger = input("请输入乘车人(示例:蔡徐坤)...\n")
fromStationText = input("请输入出发地(示例:介休东)...\n")
toStationText = input("请输入目的地(示例:成都东)...\n")
train_date = input("请输入出发日(示例:2023-01-19)...\n")
is_student = input("是否购买学生票?(y/n)\n")
print("=" * 30)
print("处理信息中...\n", "处理完毕,请检查您输入的信息...\n", fromStationText, toStationText, train_date)
confirm = input("是否确认上述信息?(y/n)\n")
if confirm == 'y':
    print("=" * 30)
    print("初始化完毕,开始运行系统...")
if confirm == 'n':
    print("=" * 30)
    print("请重新运行程序!")
    exit(1)

# 2.chrome的版本大于等于88
opt = Options()
# option.add_experimental_option('excludeSwitches', ['enable-automation'])
opt.add_argument('--disable-blink-features=AutomationControlled')
opt.add_experimental_option('detach', True)
opt.add_argument('--start-maximized')  # 浏览器窗口最大

web = Chrome(options=opt)

web.get("https://kyfw.12306.cn/otn/resources/login.html")

web.find_element(By.XPATH, '//*[@id="toolbar_Div"]/div[2]/div[2]/ul/li[1]/a').click()

time.sleep(1)

# TODO 输入用户名和密码
web.find_element(By.XPATH, '//*[@id="J-userName"]').send_keys("18306825490")
web.find_element(By.XPATH, '//*[@id="J-password"]').send_keys("lk020511")

# 点击登录
web.find_element(By.XPATH, '//*[@id="J-login"]').click()

time.sleep(3)

# 拖拽
btn = web.find_element(By.XPATH, '//*[@id="nc_1_n1z"]')
ActionChains(web).drag_and_drop_by_offset(btn, 300, 0).perform()
time.sleep(3)

# 车票预定
web.find_element(By.XPATH, '//*[@id="link_for_ticket"]').click()

# 输入信息(出发地、目的地、出发日)

# 出发地
web.find_element(By.XPATH, '//*[@id="fromStationText"]').click()
web.find_element(By.XPATH, '//*[@id="fromStationText"]').send_keys(fromStationText, Keys.ENTER)
time.sleep(1)

# 目的地
web.find_element(By.XPATH, '//*[@id="toStationText"]').click()
web.find_element(By.XPATH, '//*[@id="toStationText"]').send_keys(toStationText, Keys.ENTER)
time.sleep(1)

# 出发日
web.find_element(By.XPATH, '//*[@id="train_date"]').clear()
web.find_element(By.XPATH, '//*[@id="train_date"]').send_keys(train_date)
time.sleep(1)

# 点击查询
if is_student == 'y':
    web.find_element(By.XPATH, '//*[@id="sf2_label"]').click()
    time.sleep(1)
web.find_element(By.XPATH, '//*[@id="query_ticket"]').click()
print("=" * 30)
print("查询完毕...")
time.sleep(1)

# 点击预定
WebDriverWait(web, 1000).until(EC.presence_of_element_located((By.XPATH, '//*[@id="queryLeftTable"]/tr')))
tr_list = web.find_elements(By.XPATH, '//*[@id="queryLeftTable"]/tr[not(@datatran)]')  # 每一列列车整行信息列表,列车号元素是tr的子元素
if not tr_list:
    print("=" * 30)
    print(f"很抱歉,按您的查询条件,当前未找到从{fromStationText}到{toStationText}的列车。")
    exit(1)
for tr in tr_list:
    train_num = tr.find_element(By.XPATH, './td[1]/div/div[1]/div/a').text  # 取出元素tr里的列车号
    # 动车二等座余票信息
    text_1 = tr.find_element(By.XPATH, "./td[4]").text
    # 火车二等座余票信息
    text_2 = tr.find_element(By.XPATH, "./td[8]").text
    if (text_1 == "有" or text_1.isdigit()) or (text_2 == "有" or text_2.isdigit()):
        # 点击预订按钮
        order_btn = tr.find_element(By.CLASS_NAME, "btn72")
        order_btn.click()
        # 等待订票页面
        WebDriverWait(web, 1000).until(EC.url_to_be('https://kyfw.12306.cn/otn/confirmPassenger/initDc'))
        print("=" * 30)
        print(train_num, "二等座有票!")
        break
    else:
        print("=" * 30)
        print(train_num, "二等座无票!")
        continue


# 跳转页面提交订单
# 选定乘车人
web.find_element(By.XPATH, f'//*[@id="normal_passenger_id"]/li/label[contains(text(),"{passenger}")]').click()
# 如果乘客是学生,对提示点击确定
if EC.presence_of_element_located((By.XPATH, '//div[@id="dialog_xsertcj"]')):
    web.find_element(By.ID, 'dialog_xsertcj_ok').click()
    # 提交订单
    web.find_element(By.ID, 'submitOrder_id').click()
    time.sleep(2)
else:
    # 提交订单
    web.find_element(By.ID, 'submitOrder_id').click()
    time.sleep(2)

# 选座
print("=" * 30)
seat = input("请尽快进行选座操作([窗]A/B/C/[过道]/D/F[窗])\n")
if seat == 'A':
    web.find_element(By.XPATH, '//*[@id="erdeng1"]/ul[1]/li[1]/a').click()
if seat == 'B':
    web.find_element(By.XPATH, '//*[@id="erdeng1"]/ul[1]/li[2]/a').click()
if seat == 'C':
    web.find_element(By.XPATH, '//*[@id="erdeng1"]/ul[1]/li[3]/a').click()
if seat == 'D':
    web.find_element(By.XPATH, '//*[@id="erdeng1"]/ul[2]/li[1]/a').click()
if seat == 'F':
    web.find_element(By.XPATH, '//*[@id="erdeng1"]/ul[2]/li[2]/a').click()

# 最终确认
web.find_element(By.XPATH, '//*[@id="qr_submit_id"]').click()

print("=" * 30)
print("*"*40, "\n---***<|订单创建完成,请于10分钟内付款|>***---")
print("*"*40)


运行效果

 

 

 

 

 


总结

本节是基于selenium的浏览器自动化操作的实例,较为综合,涉及的知识点也比较多,仅供小伙伴们参考学习,请勿用于其他用途!

不太明白的小伙伴可以移步我之前发布过的selenium基础和简单的验证码实战

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/173353.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

离线增量文章画像计算

2.5 离线增量文章画像计算 学习目标 目标 了解增量更新代码过程应用 无 2.5.1 离线文章画像更新需求 文章画像&#xff0c;就是给每篇文章定义一些词。 关键词&#xff1a;TEXTRANK IDF共同的词 主题词&#xff1a;TEXTRANK ITFDF共同的词 更新文章时间&#xff1a; 1、…

10.1002.1:VectorDraw Web /VectorDraw Developer Crack

VectorDraw 网络库 VectorDraw Web Library 是一个矢量图形库&#xff0c;旨在不仅可以打开 CAD 绘图&#xff0c;还可以在任何支持 HTML 5 标准的平台&#xff08;例如 Windows、Android、IOS 和 Linux&#xff09;上显示通用矢量对象。它可以在支持使用 canvas 和 Javascript…

DW动手学数据分析Task3:数据重构)

目录1 数据的合并1.1合并方法一&#xff1a;用concat函数1.2 合并方法二&#xff1a;使用DataFrame自带的方法join方法和append1.3 合并方法三&#xff1a;使用Panads的merge方法和DataFrame的append方法2 换一种角度看数据3 数据聚合与运算3.1 groupby机制3.2 数据运算1 数据的…

论文浅尝 | 利用常识知识图增强零样本和少样本立场检测

笔记整理&#xff1a;张嘉芮&#xff0c;天津大学硕士链接&#xff1a;https://aclanthology.org/2021.findings-acl.278.pd动机传统的数据驱动方法不适用于零样本和少样本的场景。对于人类来说&#xff0c;常识知识是理解和推理的关键因素。在没有标注数据和用户立场的隐晦表达…

2022最新MySQL高频面试题汇总

本文已经收录到Github仓库&#xff0c;该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点&#xff0c;欢迎star~ Github地址&#xff1a;https://github.com/…

【C进阶】通讯录1.0(文末附原码)

⭐博客主页&#xff1a;️CS semi主页 ⭐欢迎关注&#xff1a;点赞收藏留言 ⭐系列专栏&#xff1a;C语言进阶 ⭐代码仓库&#xff1a;C Advanced 家人们更新不易&#xff0c;你们的点赞和关注对我而言十分重要&#xff0c;友友们麻烦多多点赞&#xff0b;关注&#xff0c;你们…

数据结构进阶 unordered_set unordered_map的使用

作者&#xff1a;小萌新 专栏&#xff1a;数据结构进阶 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;介绍高阶数据结构 unorder_set unorder_map的使用 unorder_set unorder_mapunordered系列关联式容器unordered_set介绍unordere…

微信小程序流量主提升ecpm的一些方法

本篇文章主要讲解:微信小程序流量主提升ecpm的一些方法 日期:2023年1月19日 作者:任聪聪 一、对ecpm的疑问和科普 什么是ecpm ecpm就是千次广告曝光收入,是一个预估的价格,而不是额定的,他是随着曝光度,用户点击度,页面访问数来决定的。 ecpm为什么会低? 微信官方…

KaiwuDB 数据服务平台 1.0 产品详解

大家好&#xff0c;今天我分享的是 KaiwuDB 数据服务平台&#xff08;KDP&#xff09;&#xff0c;一款由我们独立自主研发&#xff0c;以 KaiwuDB 为核心的数据服务产品。KDP 产品建设目标是实现数据的云边端的一体化治理&#xff0c;提供一套完整的全生命周期服务。接下来我将…

2.5、线程概念和多线程模型

整体框架 1、什么是线程&#xff0c;为什么要引入线程&#xff1f; 还没有引入进程之前&#xff0c;各个程序只能串行执行 进程是程序的一次执行过程&#xff0c;但这些功能显然不可能是由一个程序顺序处理就能实现的 有的进程可能需要 “同时” 做很多事&#xff0c;而传统的…

伯俊ERP与金蝶云星空对接集成连通应收单新增

伯俊ERP与金蝶云星空对接集成表头表体组合查询连通应收单新增(应收单-标准应收单&#xff08;KD应收单销售退)数据源系统:伯俊ERP未来&#xff0c;伯俊科技也会砥砺前行&#xff0c;不断为品牌提供更全面的零售终端致胜利器。伯俊科技始终坚持创新发展&#xff0c;探索大零售行…

【IDEA】自动部署SpringBoot Jar包到远程服务器并通过脚本启动jar

【IDEA】自动部署SpringBoot Jar包到远程服务器并通过脚本启动jar 文章目录 前言一.Deployment使用二.ssh使用三.启动脚本 前言 IDEA版本: 2019.3 一.Deployment使用 IDRA原生的Deployment不需要离开idea就可以直接将项目部署到远程运行&#xff0c;同时拥有远程视图窗口…

高并发系统设计 --计数服务抽离

传统计数 模糊计数 Cache DB。写Cache&#xff0c;批量刷新DB。 有一个写请求&#xff0c;我们就写cache&#xff0c;写一个在cache中1&#xff0c;buffer记一个&#xff0c;差不多&#xff08;buffer满了&#xff0c;时间到了&#xff09;写一次DB&#xff0c;丢数据也就丢…

JVM快速入门学习笔记(二)

临近过年&#xff0c;事太多&#xff0c;学习效率也好低&#xff0c;最近已经好久没搞学习了&#xff0c;发篇简单的学习笔记意思下吧 5. 沙箱安全机制 Java安全模型的核心就是Java沙箱&#xff08;sandbox&#xff09;&#xff0c;什么是沙箱&#xff1f;沙箱是一个限制程序运…

Oracle 12c多租户特性详解:从Schema到PDB的变化与隔离

CDB和PDB的职责分离一些数据库管理员管理整个CDB&#xff0c;而另一些管理员管理单个的pdb。.管理整个CDB的dba作为普通用户连接到CDB&#xff0c;管理整个CDB和根的属性&#xff0c;以及pdb的一些属性。例如&#xff0c;这些dba可以创建、拔出、插入和删除pdb。它们还可以为根…

【c语言】文件操作详解

主页&#xff1a;114514的代码大冒险 qq:2188956112&#xff08;欢迎小伙伴呀hi✿(。◕ᴗ◕。)✿ &#xff09; Gitee&#xff1a;庄嘉豪 (zhuang-jiahaoxxx) - Gitee.com 目录 前言 一、文件是什么 二、文件的打开和关闭 1.文件指针 2.文件的打开和关闭 三&#xff0c;文件的顺…

《计算机体系结构量化研究方法》 B.4 虚拟存储器 笔记

B.4 虚拟存储器 一、基本概念 1、虚拟存储器把物理存储器划分成块以后分配给不同的进程&#xff1b;采用一种保护机制来限制各个进程&#xff0c;使其仅能访问属于自己的块。 2、重定位机制允许同一程序在物理存储器中的任意位置运行。 3、页和段用于块&#xff0c;缺页错误…

SSM框架整合入门案例

文章目录SSM整合案例1&#xff0c;SSM整合1.1 流程分析1.2 整合配置步骤1&#xff1a;创建Maven的web项目步骤2:添加依赖步骤3:创建项目包结构步骤4:创建SpringConfig配置类步骤5:创建JdbcConfig配置类步骤6:创建MybatisConfig配置类步骤7:创建jdbc.properties步骤8:创建Spring…

《Buildozer打包实战指南》第七节 常见的打包问题

目录 无法访问xxx网址&#xff0c;连接超时 目标路径xxx已经存在&#xff0c;并且不是一个空目录 每次打包时间都要很久 待更新 在打包过程中难免会碰到一些问题&#xff0c;在本节&#xff0c;笔者会把自己碰到的一些问题的解决方案写出来&#xff0c;好让读者节省时间。 …

KaiwuDB CTO 魏可伟:1.0 时序数据库技术解读

大家好&#xff0c;首先非常感谢大家参与本次 KaiwuDB 1.0 系列产品发布会。作为国内数据库新生品牌力量&#xff0c;KaiwuDB 是浪潮集团控股的数据库企业&#xff0c;我们聚焦在工业物联网、数字能源、交通车联网、智慧产业等快速发展的重要领域&#xff0c;希望为各大行业客户…