Chat2Excel:接口自动化测试

为了系统练习AI项目的接口自动化测试,找了一个真实的前后端分离项目 Chat2Excel 当作被测对象,基于 pytest + requests + Allure 搭了一套完整框架,覆盖用户 / 文件 / AI 三个服务,直连 MySQL 做数据一致性校验,并针对越权、鉴权、SQL 注入做了安全测试。


一、Chat2Excel系统

Chat2Excel 是一个"对话式 Excel 数据分析"平台:用户上传 Excel,后端用 POI 解析,把每个 sheet 建成一张 MySQL 表,然后用户用自然语言提问,AI 生成 SQL 查询并流式返回结果。

架构是 Spring Cloud 微服务:网关 + 用户服务 + 文件服务 + AI 服务,中间件用了 MySQL、Redis、Nacos、Nginx,前端 Vue3。

这个系统有三个特点,直接决定了测试方案的难点:

1)接口不是"一问一答"。 AI 对话接口返回的是 text/event-stream(SSE),是持续推送的事件流,不是一次性 JSON。如果按普通接口那样 requests.post() 拿 resp.json(),要么拿不到东西,要么一直阻塞。

2)接口成功 ≠ 业务成功。 后端有一个 GlobalExceptionHandler 统一兜异常,业务出错时返回的是:

python 复制代码
HTTP 200
{"code": 400, "message": "只能处理excel文件", "data": null}

也就是说 HTTP 状态码永远是 200 ,只有 body 里的 code 才代表业务结果。断言只看 status_code == 200,等于什么都没测。

3)真正的结果在数据库里。 上传一个 Excel,接口只返回一个 fileId 和几KB的元信息。但后端实际做了四件事:文件传 OSS、files 表插记录、每个 sheet 建一张 MySQL 表、再写 file_table_mappings 和 field_mappings 两张映射表。只看接口返回,根本不知道数据到底有没有正确落库。


二、框架设计:三层分离

我理解的"框架"和"脚本"的区别只有一个:用例里应不应该出现 URL。

  • 脚本:requests.post("http://xxx/api/v1/files/upload/single", files=..., headers={"Authorization": ...})
  • 框架:file_api.upload(path)

所以整个框架分成三层:

python 复制代码
用例层(testcases)  只写业务断言:上传后数据库里应该有什么
    ↓
接口层(api)        每个接口一个方法,URL 和参数拼装收敛在这里
    ↓
工具层(common)     HTTP 封装 / SSE 解析 / 数据库 / Redis / 断言

目录结构:

python 复制代码
autotest/
├── config/env.yaml          唯一需要改的配置:地址、账号、数据库
├── conftest.py              全局 fixture:登录、上传文件、连库
├── common/                  工具层
│   ├── http_client.py       HTTP 封装(自动带 token、打日志、文件上传、SSE)
│   ├── sse_client.py        SSE 事件流解析器
│   ├── db_client.py         MySQL 直连(数据一致性断言用)
│   ├── redis_client.py      Redis(读验证码、校验 token)
│   ├── assert_util.py       断言封装
│   └── logger.py            统一日志
├── api/                     接口层
│   ├── user_api.py / file_api.py / ai_api.py / llm_api.py
├── testcases/               用例层
│   ├── test_00_smoke.py     冒烟
│   ├── test_01_user.py      用户服务
│   ├── test_02_file.py      文件服务 + 数据库一致性
│   ├── test_03_ai.py        AI 服务(SSE)
│   ├── test_04_security.py  安全测试
│   └── test_05_defects.py   缺陷复现(xfail)
└── run.py                   一键运行 + 出报告

还有一条原则:配置与代码分离 。环境地址、数据库连接、测试账号全部集中在 config/env.yaml,换环境只改这一个文件,代码一行不动。

python 复制代码
base_url: "http://<your-server>:8080/api/v1"
user:   { username: "autotest_main",   password: "AutoTest@123" }
user_b: { username: "autotest_second", password: "AutoTest@123" }   # 越权测试专用
mysql:  { host: "<your-server>", user: "<db-user>", password: "<db-pass>" }

测试用例总数 52 条:冒烟 5、用户 10、文件 15、AI 8、安全 8、缺陷 6。


三、核心设计一:断言必须看 body.code

因为上面说的 GlobalExceptionHandler,业务失败也是 HTTP 200,所以我把断言分成了三类,覆盖两种"拒绝"方式:

python 复制代码
def assert_success(resp, expect_message=None):
    """断言业务成功:body.code == 200,返回 data"""
    body = _safe_json(resp)
    assert body is not None, f"响应不是合法 JSON: {resp.text[:300]}"
    assert body.get("code") == 200, (
        f"期望业务码 200,实际 code={body.get('code')},"
        f"message={body.get('message')},HTTP={resp.status_code}"
    )
    return body.get("data")


def assert_business_code(resp, expect_code, message_contains=None):
    """断言业务返回码,例如参数错误期望 400"""


def assert_rejected(resp, allow_status=(401, 403)):
    """
    断言请求被"拒绝"。
    兼容两种拒绝方式:
      1. 网关拦截:HTTP 401/403 + 纯文本"令牌无效"
      2. 业务层拒绝:HTTP 200 但 body.code != 200
    """
    body = _safe_json(resp)
    if resp.status_code in allow_status:
        return True
    if body is not None and body.get("code") != 200:
        return True
    raise AssertionError(f"期望请求被拒绝,但它成功了!HTTP={resp.status_code}")

assert_rejected 这个封装解决的是一类很常见的坑:同样是"未授权",网关拦和业务层拦返回的结构完全不同。不封装的话,每个安全用例都得写一遍 if-else。

经验:断言报错信息一定要带上"期望值 + 实际值 + 完整响应",否则失败时只能靠加 print 重新跑一遍。


四、核心设计二:一个 client = 一个独立用户会话

自动化测试里最容易翻车的是用例互相污染。比如"登出后 token 失效"这条用例,如果直接用主账号跑,主账号的 token 就被销毁了,后面所有用例全部失败。

我的解法:ApiClient 内部维护自己的 token,一个实例 = 一个用户会话。

python 复制代码
class ApiClient:
    def __init__(self, base_url, timeout=30, name="api"):
        self.session = requests.Session()
        self.token = None

    def _headers(self, extra=None):
        headers = {}
        if self.token:
            headers["Authorization"] = f"Bearer {self.token}"
        return {**headers, **(extra or {})}

然后在 conftest.py 里按账号开出不同的 client fixture:

python 复制代码
@pytest.fixture(scope="session")
def main_client(cfg):
    """主账号,已登录。session 级:只登录一次,后面所有用例共用 token"""
    client = ApiClient(cfg.base_url, name="main")
    UserApi(client).login(cfg.user["username"], cfg.user["password"])
    return client

@pytest.fixture(scope="session")
def client_b(cfg):
    """第二账号,专用于越权测试"""

@pytest.fixture
def anonymous_client(cfg):
    """未登录客户端,专用于鉴权测试"""

@pytest.fixture
def user_api(main_client):
    return UserApi(main_client)

对应的,env.yaml 里准备了 4 个专用账号:主账号、越权账号、登出账号、改密账号,互不干扰。

改密用例还会用 finally 把密码改回去,保证用例可重复执行:

python 复制代码
try:
    resp = api.change_password("totally-wrong-old-password", NEW, NEW)
    ...
finally:
    api.change_password(NEW, cfg.user_pwd["password"], cfg.user_pwd["password"])

fixture 作用域的选择也是有讲究的:

  • session 级 ------ 配置、登录 token、数据库连接(只做一次,省时间)
  • function 级 ------ 有副作用的资源,比如"上传一个文件"

五、核心设计三:SSE 流式接口怎么自动化

问题

AI 对话接口 POST /ai/chat/stream 返回 text/event-stream。后端推送的格式长这样:

python 复制代码
event: progress
data: {"stage":"INIT","progress":0}

event: progress
data: {"stage":"QUERY_SQL","progress":60}

event: complete
data: {"completed":true,"result":{"requestId":123,...}}

用 requests 默认行为是等响应体全部接收完才返回,而 SSE 是持续推送、连接长时间不关闭的 ------ 直接调用会一直卡住。

解法

第一步,发请求时用 stream=True,并且显式声明 Accept:

python 复制代码
def stream_post(self, path, json_body, timeout=120):
    headers = self._headers({"Accept": "text/event-stream"})
    return self.session.post(url, headers=headers, json=json_body,
                             stream=True, timeout=timeout)

第二步,写一个 SSE 解析器,把文本流解析成事件列表:

python 复制代码
def parse_sse(resp, max_seconds=None) -> List[Tuple[str, dict]]:
    """解析 SSE 响应,返回 [(事件名, data字典), ...]"""
    events = []
    current_event = "message"
    start = time.time()

    for raw_line in resp.iter_lines(decode_unicode=True):
        # 防止后端异常不关连接导致用例卡死
        if max_seconds and (time.time() - start) > max_seconds:
            break
        line = raw_line.strip()

        if line == "":
            current_event = "message"      # 空行 = 一个事件结束
            continue
        if line.startswith("event:"):
            current_event = line[len("event:"):].strip()
            continue
        if line.startswith("data:"):
            payload = line[len("data:"):].strip()
            events.append((current_event, _try_json(payload)))
            continue
    return events

第三步,用例只断言流程和结构:

python 复制代码
@pytest.mark.ai
def test_chat_stream_query_flow(uploaded_file, ai_api):
    resp = ai_api.chat_stream(uploaded_file["fileId"], "这个文件一共有多少条数据?")

    # 1) 先确认这确实是流式接口
    assert "text/event-stream" in resp.headers.get("Content-Type", "")

    events = parse_sse(resp, max_seconds=110)
    names = event_names(events)

    # 2) 必须有 INIT 初始化事件(证明后端链路启动)
    assert find_stage(events, "INIT") is not None, "缺少 INIT 初始化事件"

    # 3) 必须以 complete 收尾,且带结构化结果
    complete = find_event(events, "complete")
    assert complete is not None, f"未收到 complete 事件,实际事件: {names}"
    assert complete.get("completed") is True
    assert complete["result"].get("requestId")

一个关键的取舍:为什么不断言 AI 说的话

大模型输出天然有不确定性。同一句"这个文件有多少条数据",两次回答措辞可能完全不同。断言具体文本 = 制造随机失败。

所以我把 AI 用例的断言收敛成"结构契约":

  • 事件的类型对不对(有 progress、有 complete)
  • 事件的顺序对不对(INIT 必须在前、complete 必须在最后)
  • 事件的结构对不对(complete 里必须有 result.requestId)

至于"AI 回答得对不对",那是模型评测的范畴,交给评测集或人工,不该由稳定性要求很高的回归用例来承担。这是业界对流式 LLM 接口做自动化的通用做法。

max_seconds 这个参数也值得一提:它防的是"后端异常导致连接永不关闭"------ 没有它,一条用例能挂到天荒地老。而后面会讲到,这个保护恰恰帮我发现了一个真实缺陷。


六、核心设计四:数据库一致性校验

只看接口返回,测不出真问题

上传 Excel 接口返回 {fileId: 123, fileName: "国家GDP.xlsx", fileSize: 10240, uploadStatus: 1}。断言它 code == 200、fileId > 0,看起来很美好。

但如果后端只写了 files 表、忘了建动态表 ,或者建表了但行数少了一半,接口返回完全一样 ------ 因为你查的是元信息,不是数据。

所以我把 pymysql 接进来,直接断言数据库:

python 复制代码
@pytest.mark.file
def test_db_consistency_after_upload(uploaded_file, db, file_api):
    """上传 Excel 后,校验数据库三张表 + 动态表都正确落库"""
    file_id = uploaded_file["fileId"]

    # 1) files 表有记录
    file_row = db.get_file(file_id)
    assert file_row is not None, f"files 表中不存在 fileId={file_id} 的记录"
    assert file_row["file_name"] == uploaded_file["fileName"]

    # 2) 文件 <-> MySQL表 的映射
    table_names = db.get_table_names_by_file(file_id)
    assert table_names, "file_table_mappings 未记录表映射"

    # 3) Excel 表头 <-> 字段 的映射
    field_mappings = db.get_field_mappings(file_id)
    assert field_mappings, "field_mappings 未记录字段映射"

    # 4) 动态表存在,且【数据库行数 == 接口返回行数】
    dynamic_table = table_names[0]
    assert db.table_exists(dynamic_table)

    db_count = db.count_table_rows(dynamic_table)
    preview = assert_success(file_api.preview(file_id))
    api_count = preview["paginationInfo"]["totalRecords"]
    assert db_count == api_count, (
        f"数据库表行数 {db_count} 与接口返回 {api_count} 不一致"
    )

第 4 步是全篇我最喜欢的一行断言:数据库和接口,是两个独立的信息源,让它们互相验证。接口说 100 行、数据库说 100 行,才敢说数据是对的。

db_client 里顺便封装了一批业务化查询方法(get_file / get_table_names_by_file / count_table_rows / table_exists),用例里就不用裸写 SQL 了。

附带收获:接口间一致性

同样的思路可以用来测"接口之间对不对得上"。/files/excel/info/{id} 和 /files/excel/preview/{id} 读的是同一张表,总行数必须一致:

python 复制代码
assert info["totalRows"] == preview["paginationInfo"]["totalRecords"], \
    "excel_info 与 preview 返回的总行数不一致"

"一键复原":一个破坏性测试

平台有个"一键复原"接口,作用是用户把数据改坏后,用原始 Excel 覆盖数据库表还原。这个怎么测?

先破坏,再复原,比对行数:

python 复制代码
def test_restore_file_data(uploaded_file, db, file_api):
    table = db.get_table_names_by_file(uploaded_file["fileId"])[0]
    original_count = db.count_table_rows(table)

    # 破坏数据:直接清空
    db.execute(f"DELETE FROM `{table}`")
    assert db.count_table_rows(table) == 0

    # 调复原接口
    assert_success(file_api.restore(uploaded_file["fileId"]))

    # 数据应被完整还原
    assert db.count_table_rows(table) == original_count

这比"调一下接口看返回 200"强太多 ------ 后者测的是接口活着,前者测的是功能真的有用。

数据准备与清理

数据库校验的前提是有一个已上传的文件。这个前置动作我用 fixture 管理,用例结束后自动清理:

python 复制代码
@pytest.fixture
def uploaded_file(main_client, cfg, tiny_excel_path):
    """上传一个 Excel,用例结束后自动删除,保证用例可重复执行"""
    api = FileApi(main_client)
    info = api.upload(tiny_excel_path).json()["data"]

    yield {**info, "localPath": tiny_excel_path}   # ← 交给用例

    # 清理:删除文件(同时删除衍生表、映射记录、OSS 文件)
    try:
        api.delete([info["fileId"]])
    except Exception as exc:
        log.warning(f"清理文件失败(不影响用例结果): {exc}")

注意清理失败只打 warning 不抛异常 ------ 清理出错不该让用例结果变红。


七、顺带把 Redis 也用上了

除了 MySQL,框架还直连了 Redis,解决两个具体问题:

1)自动化没法收邮件,但需要验证码。

后端的验证码存在 Redis 里,key 就是 code:<6位验证码>。所以我可以直接扫出来用:

python 复制代码
def get_latest_verification_code(self, pattern="code:*"):
    keys = list(self.client.scan_iter(match=pattern, count=100))
    if not keys:
        return None
    # 按 TTL 剩余最长 = 刚生成的那个
    keys.sort(key=lambda k: self.client.ttl(k), reverse=True)
    return keys[0].split("code:", 1)[-1]

这是测试环境的一个小技巧:绕过外部依赖(邮箱),从数据层拿到你需要的东西。

2)验证"登出"是不是真的生效了。

登出接口返回 200 不代表 token 真的失效了。用例不只看返回,还重新用旧 token 请求一次,确认被拒绝:

python 复制代码
def test_logout_then_token_becomes_invalid(cfg):
    api.login(cfg.user_logout["username"], cfg.user_logout["password"])  # 专用账号
    token = client.token

    assert_success(api.get_user_info())   # 登出前:能查到
    assert_success(api.logout())
    assert_rejected(api.get_user_info())  # 登出后:同一 token 被拒绝

八、安全测试:越权、鉴权、注入、绕过网关

接口自动化很适合做安全测试,因为这些都是纯接口层面的问题,跟 UI 无关。我测了四类(8 条用例,正常情况下应该全部通过):

1. 水平越权(IDOR)

用 B 账号上传一个文件,再用 A 账号的 token 去操作它:

python 复制代码
def test_download_other_users_file_forbidden(uploaded_file_b, main_client):
    """下载别人的文件 -> 403"""
    resp = FileApi(main_client).download(uploaded_file_b["fileId"])
    assert resp.status_code == 403

def test_delete_other_users_file_has_no_effect(uploaded_file_b, file_api, db):
    """删除别人的文件 -> 文件必须仍然存在"""
    file_api.delete([uploaded_file_b["fileId"]])
    assert db.get_file(uploaded_file_b["fileId"]) is not None, "严重越权:A 删除了 B 的文件!"

注意最后一条:删完再用数据库确认它还在,而不是只看接口返回。越权漏洞最危险的地方就是"接口报错但数据已经被改了"。

2. 鉴权:伪造 JWT

这个项目的 JWT 密钥是硬编码的(源码里写死),理论上攻击者可以自己签发 token。于是我构造一个伪造的 JWT 试进去:

python 复制代码
fake_payload = {"sub": "10000001", "username": "hacker", "exp": ...}
forged = pyjwt.encode(fake_payload, "<硬编码的密钥>", algorithm="HS256")

anonymous_client.set_token(forged)
assert_rejected(anonymous_client.get("/users/info"))

结果是被拒绝的 ------ 原因是这个项目的鉴权实际以 Redis 为准 (网关查 token:<token> 是否存在),伪造的 JWT 在 Redis 里查不到,所以依然进不来。这属于纵深防御生效:主防线有缺陷,但兜底防线挡住了。

3. SQL 注入

在文件名的模糊查询参数里塞注入串:

python 复制代码
def test_sql_injection_in_file_name_filter(file_api):
    resp = file_api.list_files(file_name="' OR '1'='1")
    data = assert_success(resp)
    # 注入成功会查出所有文件;参数化查询下它只是个普通字符串
    assert data["total"] == 0

4. 绕过网关直连微服务

网关做了一层鉴权、微服务端口又做了一层 GatewayTokenFilter。我直接跳过网关,拿合法 token 去请求微服务的裸端口,期望被 403 拦掉:

python 复制代码
url = f"{cfg.host}:{cfg.service_ports['file']}/files/list"
resp = requests.get(url, headers={"Authorization": f"Bearer {main_client.token}"})
assert resp.status_code == 403
assert "禁止访问" in resp.text

如果服务端口没对外暴露,用例会 pytest.skip() 而不是失败 ------ 环境不具备就跳过,不要制造假红。


九、缺陷管理:为什么用 xfail

跑出来 5 个确认的缺陷,我把它们写成了**断言"正确行为"**的用例,然后用 xfail 标记:

python 复制代码
@pytest.mark.defect
@pytest.mark.xfail(
    reason="已知缺陷D1:/files/excel/info 未校验文件归属,任意登录用户可查看他人文件信息",
    strict=False,
)
def test_defect_excel_info_idor(uploaded_file_b, file_api):
    """
    正常逻辑:excel_info 和 preview 一样,都应该校验文件归属。
    实测现象:接口直接按 fileId 查库返回,未校验 user_id -> 越权读取成功。
    """
    resp = file_api.excel_info(uploaded_file_b["fileId"])
    body = resp.json()
    assert body.get("code") != 200, f"越权成功:用户A 读取到了用户B 的文件信息"

这样做的好处很实在:

  • 回归报告里显示 XFAIL(预期失败),而不是 FAILED ------ 留下缺陷证据,又不会让整个回归一片红
  • 缺陷一旦被开发修复,用例自动变成 XPASS ------ 天然就是"修复验证"
  • 缺陷的复现步骤、根因、修复建议,都固化在用例里了,不会随着时间遗忘

举个有代表性的:D1 水平越权

项 内容
接口 GET /api/v1/files/excel/info/{fileId}
预期 拒绝(文件不存在或用户无权限)
实际 200,并把 B 的文件名、大小、行列数返回给 A
根因 该方法用 filesMapper.selectById(fileId),只按 fileId 查,没校验 user_id 。而同模块的 previewExcel / restoreFileData / deleteFiles 用的都是 selectByUserIdAndFileId(userId, fileId) ------ 显然是漏改
修复建议 换成 selectByUserIdAndFileId,查不到就抛"文件不存在或用户无权限"
风险 遍历 fileId 即可拿到全站用户的文件元信息

再看一个"一个字符"的缺陷

D5:下载不存在的文件时返回 500 而不是 404。根因是判断写错了变量:

python 复制代码
FilesEntity filesEntity = filesMapper.selectById(fileId);
if (fileId == null) {          // ← 应该是 filesEntity == null
    throw new ...
}
// 继续执行到 filesEntity.getUserId() → NullPointerException

这类缺陷不写用例是发现不了的 ------ 手工点页面永远点不出"下载一个不存在的文件"。


十、工程化:让框架能真的用起来

一个只有用例的框架活不了多久,还得有:

1)pytest marker 分层

python 复制代码
markers =
    smoke: 冒烟用例,验证环境连通性,每次提交后快速执行
    user / file / ai: 按服务划分
    security: 安全测试(越权/鉴权/注入/上传校验)
    defect: 已知缺陷复现(xfail)
    slow: 慢用例(大文件、并发),默认跳过

AI 用例单独标记是刻意的 ------ 它依赖真实大模型、一条要跑十几秒到几十秒、结果还不确定,不该拖累日常回归:

python 复制代码
python run.py -m "not ai"     # 默认推荐:跑除 AI 外的全部
python run.py -m smoke        # 改完代码先跑冒烟
python run.py -m security     # 只跑安全

2)先冒烟,再回归

冒烟用例只有 5 条,但价值极高 ------ 它回答的是"环境通不通":

  • 网关可达性(带无效 token 能收到拒绝,说明网关活着)
  • 登录链路 + 查用户信息
  • MySQL 可达
  • Redis 可达
  • 未登录请求受保护接口被拒绝

冒烟挂了就别看其他用例的红了 ------ 那大概率是环境问题,不是代码缺陷。

3)一键运行 + 双报告

run.py 把 pytest 命令和报告生成包成一条命令,同时产出两种报告:

  • pytest-html:自包含 HTML,双击就能开,不需要额外环境
  • Allure:更好看,适合演示(需要 allure 命令行 + Java)
python 复制代码
if importlib.util.find_spec("pytest_html") is not None:
    args += [f"--html={HTML_REPORT}", "--self-contained-html"]
if importlib.util.find_spec("allure_pytest") is not None:
    args.append(f"--alluredir={ALLURE_DIR}")

用 find_spec 判断插件是否存在,没装就跳过而不是报错 ------ 降低别人的使用门槛。

4)连不上外部依赖就 skip,不要 fail

python 复制代码
@pytest.fixture(scope="session")
def db(cfg):
    client = DbClient(cfg.mysql)
    try:
        client.connect()
    except Exception as exc:
        pytest.skip(f"MySQL 连接失败,跳过数据一致性用例: {exc}")
    yield client
    client.close()

十一、踩过的坑

1)测试环境连不上,先别怀疑用例。 一开始登录一直 500,排查半天发现是网关白名单配置的问题:源码里白名单写的是 /users/auth,但实际请求路径是 /api/v1/users/auth,前缀对不上。先用 Postman 确认接口通不通,再怀疑用例。

2)Windows 控制台中文乱码。 PowerShell 里跑中文日志全是乱码,chcp 65001 切到 UTF-8 即可。

3)AI 用例的不确定性要从设计上隔离,而不是靠重试。 我的做法是三层:marker 隔离 + 只断言结构契约 + 超时保护(max_seconds)。

4)用例之间的污染往往来自"共享账号"。 登出、改密这类有副作用的用例,一定要用独立账号。

5)断言信息写详细,是给自己省时间。 每条断言都带上期望值、实际值和完整响应体,失败时一眼看出问题,不用重新跑一遍加 print。

相关推荐
ss2731 小时前
AI全栈实战 | 3.3-02 Python 并发:有 GIL 为什么还用多线程,asyncio 和 JS 事件循环同源不同味
开发语言·javascript·python
空心木偶☜1 小时前
Langgraph操作时常见的错误
python·ai·ai编程·langgraph
cpolar技术支持1 小时前
外网测试机的异常传不回内网?自托管 Sentry,用 cpolar 打通错误上报链路
python·nginx·docker·cpolar·sentry
Chengbei111 小时前
拒绝无效洞!蚂蚁Src榜一Clown 2.0 SRC SKill 第二代带状态闭环的白盒 & 黑盒审计 Skill
网络·人工智能·安全·web安全·网络安全·自动化·安全架构
小小龙学IT1 小时前
Python SQLAlchemy 2.0 深度解析:从 Core 到 ORM 的数据访问双引擎
开发语言·数据库·python
2601_962885721 小时前
如何用 Python 计算 ROC 变动率指标做动量分析?
开发语言·python·算法
在世修行1 小时前
干货:文件上传端点解析
python·fastapi·接收上传
ZhangJun953 小时前
Mobike 共享单车分析项目
人工智能·python·算法·kmeans·聚类·knn
XLYcmy3 小时前
PDF 论文处理器 — 优势与功能文档
大数据·python·网络安全·pdf·dify·rag·漏洞检测