第一次个人编程作业
| 这个作业属于哪个课程 | 计科24级78班-软件工程 |
|---|---|
| 这个作业要求在哪里 | 个人项目 |
| 这个作业的目标 | 本次作业用 Python 实现 SimHash 论文查重,读取文件输出相似度,完成 PSP 工时记录、Git 提交、单元测试和异常处理,分为博客和代码两部分。 |
一、PSP 个人开发流程
本次项目采用 PSP2.1 个人软件开发流程,在编码开始前预估各阶段开发耗时,项目完成后记录实际耗时,以此评估个人开发效率。
| PSP2.1 个人开发流程 | 预估耗时(分钟) | 实际耗时(分钟) |
|---|---|---|
| Planning 计划 | 30 | 40 |
| · Estimate · 估计这个任务需要多少时间 | 30 | 40 |
| Development 开发 | 260 | 290 |
| · Analysis · 需求分析(包括学习新技术) | 60 | 60 |
| · Design Spec · 生成设计文档 | 30 | 40 |
| · Design Review · 设计复审 | 20 | 30 |
| · Coding Standard · 代码规范 | 20 | 20 |
| · Design · 具体设计 | 40 | 50 |
| · Coding · 编码 | 60 | 60 |
| · Code Review · 代码复审 | 20 | 20 |
| · Test · 测试(自我测试,修改代码,提交修改) | 30 | 30 |
| Reporting 报告 | 60 | 100 |
| · Test Report · 测试报告 | 30 | 30 |
| · Size Measurement · 计算工作量 | 10 | 20 |
| · Postmortem & Process Improvement Plan · 事后总结,并提出过程改进计划 | 30 | 30 |
| 合计 | 360 | 410 |
二、计算模块接口的设计与实现过程
2.1 代码组织
项目单文件 main.py,共 7 个函数:
| 函数 | 职责 |
|---|---|
clean_text |
文本清洗,只保留中文汉字 |
stable_hash |
稳定哈希,用 md5 代替内置 hash |
get_simhash |
生成 64 位 SimHash 指纹 |
hamming_distance |
计算两个指纹的海明距离 |
calculate_similarity |
对外统一入口,返回 0~1 相似度 |
read_text_file |
读取文件,统一处理异常 |
main |
命令行参数校验、调度、写文件 |
调用关系:
main
├─ read_text_file × 2
└─ calculate_similarity
├─ clean_text × 2
├─ get_simhash × 2
│ └─ stable_hash × N
└─ hamming_distance × 1
主流程图:
2.2 算法关键
核心:SimHash + 海明距离。
- 文本清洗,只保留汉字
- 提取特征(初版 jieba 分词,后改 3-gram)
- 每个特征用 md5 生成稳定哈希
- 对 64 位每一位加权投票
- 得到 64 位指纹
- 两段文本指纹做异或,统计 1 的个数(海明距离)
相似度 = (64 - 距离) / 64
独到之处:
- 用
md5替代 Python 内置hash(),保证跨进程、跨机器可复现 - 用
lru_cache缓存指纹,批量查重时避免重复分词 - 特征从“词”改为“3-gram 字符”,对字符级篡改更敏感
三、计算模块接口部分的性能改进
3.1 性能分析工具选择
本项目使用 Python 实现,作业指定的 VS 2017 与 JProfiler 分别面向 C++ 和 Java,故选用 Python 生态对应的性能分析工具:
| 工具 | 作用 | 来源 |
|---|---|---|
cProfile |
Python 内置,统计函数调用次数与耗时 | 标准库 |
line_profiler |
第三方库,逐行统计耗时 | pip 安装 |
snakeviz |
将 cProfile 结果可视化 |
pip 安装 |
三者功能等价于 VS 2017 / JProfiler:定位耗时最大的函数,并生成可视化图。
3.2 分析对象
对 calculate_similarity 入口进行性能分析。
测试输入:
t1:约 50 倍重复的长文本(模拟原文)t2:约 50 倍重复的长文本(模拟抄袭文)
运行命令:
python -m cProfile -o profile_result main.py 01.txt 02.txt ans.txt
snakeviz profile_result
3.3 分析结果
3.3.1 cProfile 结果
按累计耗时(cumulative time)排序前 10:
ncalls tottime percall cumtime percall filename:lineno(function)
1 0.000 0.000 0.542 0.542 main.py:56(calculate_similarity)
2 0.009 0.005 0.540 0.270 main.py:23(get_simhash)
2 0.000 0.000 0.529 0.264 jieba/__init__.py:356(lcut)
1652 0.000 0.000 0.528 0.000 jieba/__init__.py:289(cut)
1652 0.001 0.000 0.528 0.000 jieba/__init__.py:249(__cut_DAG)
2 0.003 0.001 0.516 0.258 jieba/__init__.py:180(get_DAG)
2 0.000 0.000 0.513 0.257 jieba/__init__.py:168(check_initialized)
1 0.000 0.000 0.513 0.513 jieba/__init__.py:93(initialize)
读数:
calculate_similarity总耗时 0.542sget_simhash累计 0.540s,占 99.6%jieba.lcut累计 0.529s,占 97.6%- 其中
jieba.initialize占 0.513s

3.3.2 line_profiler 结果
逐行分析 calculate_similarity:
Timer unit: 1e-07 s
Total time: 0.626875 s
Function: calculate_similarity at line 56
Line # % Time Line Contents
61 3.1 c1 = clean_text(text1)
62 0.0 c2 = clean_text(text2)
63 0.0 if not c1 or not c2:
66 89.9 f1 = get_simhash(c1)
67 7.0 f2 = get_simhash(c2)
68 0.0 dist = hamming_distance(f1, f2)
69 0.0 similarity = (64 - dist) / 64
读数:
- 第 66 行
f1 = get_simhash(c1)占 89.9% - 第 67 行
f2 = get_simhash(c2)占 7.0% - 其余行加起来不到 4%

3.3.3 两个工具互相印证
| 工具 | 结论 |
|---|---|
| cProfile | get_simhash 累计 0.540s,占 99.6% |
| line_profiler | f1 = get_simhash(c1) 占 89.9% |
结论一致:瓶颈在 get_simhash。
3.3.4 性能分析图

snakeviz 可视化结果已生成。图中可以看到调用树/旭日图中,get_simhash 及其子节点 jieba.lcut 占据最大面积。
3.4 瓶颈细分
get_simhash 内部做三件事:
words = jieba.lcut(cleaned_text) # 1. 分词
words = [w for w in words if w not in STOP_WORDS] # 2. 过滤停用词
for word in words: # 3. 64 位投票
h = stable_hash(word)
...
对应耗时:
| 步骤 | 耗时 | 占比 |
|---|---|---|
jieba 首次加载词典(jieba.initialize) |
0.513s | 约 95% |
真正的分词计算(__cut_DAG - initialize) |
约 0.015s | 约 3% |
| 停用词过滤 + 64 位投票 | 可忽略 | 约 0% |
两个结论:
- 首次运行慢,主要是 jieba 加载词典的一次性开销,这是库本身的固有成本,无法避免。
- 真正的算法计算很快,0.015s 量级。
这说明瓶颈不在算法本身,而在“重复调用”上。
3.5 第一轮改进:lru_cache
3.5.1 问题定位
calculate_similarity 每次调用都会:
f1 = get_simhash(c1) # 分词一次
f2 = get_simhash(c2) # 分词一次
如果同一段文本被多次传入,每次都要重新分词、重新算指纹。
实际使用中这种情况很常见:
- 原文固定,抄袭文有多个:原文会被反复分词。
- 测试用例反复用同一段文本:现有测试里
t1在多个用例中重复出现。 - 批量查重场景:一篇原文对多篇论文查重,原文会被反复处理。
3.5.2 现有实现的浪费
get_simhash 是纯函数:
- 输入相同 → 输出必然相同
- 没有副作用
- 参数是
str,可哈希
这三点说明它非常适合做缓存。 但当前代码没有任何缓存,相同的输入被反复计算。
3.5.3 改进方案
给 get_simhash 加 functools.lru_cache 装饰器:
from functools import lru_cache
@lru_cache(maxsize=128)
def get_simhash(cleaned_text: str) -> int:
...
为什么选 lru_cache:
| 理由 | 说明 |
|---|---|
| 零侵入 | 只加一行装饰器,不改函数体 |
| 语义清晰 | 缓存“清洗后文本 → 指纹”的映射 |
| 命中率高 | 实际场景中原文重复率高 |
| 自动淘汰 | maxsize=128,超过后淘汰最久未用的 |
| 线程安全 | 内部有锁,多线程下安全 |
3.5.4 实测说明
对优化后的版本再次运行 profile_test.py 与 profile_line.py:
| 指标 | 优化前 | 优化后 |
|---|---|---|
| cProfile 总耗时 | 0.542s | 0.562s |
get_simhash cumtime |
0.540s | 0.560s |
| line_profiler 第 68 行 % Time | 89.9% | 90.3% |



单次运行看不出加速,原因是缓存命中率为 0%。
calculate_similarity 单次调用中,c1 与 c2 各出现一次,lru_cache 缓存中没有对应条目,因此仍按正常路径计算。这是预期行为:lru_cache 优化的是“相同输入的重复调用”,而非单次运行。
在下列场景中,缓存会显著生效:
- 一篇原文对多篇抄袭文查重:原文指纹只计算一次,后续直接命中缓存;
- 单元测试中同一段文本被多个用例复用;
- 批量查重任务。
因此,本次优化不改变单次运行耗时,但为批量查重场景提供了显著的性能冗余。
3.5.5 为什么不是其他优化
| 备选方案 | 为什么不选 |
|---|---|
| 优化 jieba 分词 | jieba 本身已足够快,瓶颈是重复调用 |
| 减少 64 位循环 | 循环不是瓶颈,省不了多少 |
| 换更快的分词库 | 改变算法行为,可能影响准确度 |
| 手动缓存 | lru_cache 已足够,无需重复造轮子 |
结论:lru_cache 是最合适的方案——零侵入、语义清晰、效果明显。
3.6 第二轮改进:引入测试文件后发现的问题
3.6.1 引入测试文件
老师提供 6 个测试文件:
orig.txt 原文
orig_0.8_add.txt 逐字插入错字
orig_0.8_del.txt 逐字删除
orig_0.8_dis_1.txt 相邻字对调
orig_0.8_dis_10.txt 句内中度打乱
orig_0.8_dis_15.txt 句内重度打乱
跑测试命令:
python main.py orig.txt orig_0.8_add.txt ans_add.txt
python main.py orig.txt orig_0.8_del.txt ans_del.txt
python main.py orig.txt orig_0.8_dis_1.txt ans_dis1.txt
python main.py orig.txt orig_0.8_dis_10.txt ans_dis10.txt
python main.py orig.txt orig_0.8_dis_15.txt ans_dis15.txt
python main.py orig.txt orig.txt ans_same.txt
3.6.2 第一轮结果
ans_same.txt 100.00
ans_add.txt 93.75
ans_del.txt 92.19
ans_dis1.txt 93.75
ans_dis10.txt 87.50
ans_dis15.txt 87.50
问题:
dis_10和dis_15分数完全相同,无法区分打乱程度- 所有抄袭版相似度都偏高(87~94),区分度低
3.6.3 问题定位
原实现使用 jieba 分词作为 SimHash 特征,SimHash 基于“词集合”,不关心词的顺序。
dis 系列文件是字符级打乱,词集合变化很小,导致指纹几乎不变,无法区分打乱程度。
举例:
原文:我吃饭
打乱:饭吃我
jieba 切出来都是 我、吃、饭,词集合相同,SimHash 指纹相同。
3.6.4 改进方案
将特征从“jieba 分词”改为 3-gram 字符切分,特征单元为连续 3 个汉字,对字符顺序变化更敏感。
改动位置:get_simhash 内部特征提取那一句。
# 改动前
words = jieba.lcut(cleaned_text)
words = [w for w in words if w not in STOP_WORDS]
# 改动后
n = 3
words = [cleaned_text[i:i + n] for i in range(len(cleaned_text) - n + 1)]
3.6.5 改进后结果
ans_same.txt 100.00
ans_dis1.txt 85.94
ans_add.txt 75.00
ans_dis10.txt 75.00
ans_del.txt 68.75
ans_dis15.txt 60.94
趋势:
same > dis_1 > add ≈ dis_10 > del > dis_15
结论:
dis_1 > dis_10 > dis_15递减趋势成立,算法能有效区分打乱程度。add与dis_10分数相近,说明“逐字插入错字”与“中度打乱”对 3-gram 特征的破坏程度相当。- 重度打乱
dis_15得分最低,符合预期。
改进成功。
四、计算模块部分单元测试展示
4.1 测试文件与运行方式
测试文件 test_simhash.py,与 main.py 同目录,使用 pytest 运行:
pytest -v
共 19 个测试用例,覆盖 6 个函数:
| 被测函数 | 测试数量 |
|---|---|
clean_text |
3 |
get_simhash |
3 |
hamming_distance |
2 |
calculate_similarity |
4 |
read_text_file |
4 |
main |
3 |
4.2 测试函数与构造思路
4.2.1 clean_text(3 个)
def test_clean_text_normal():
"""正常句子,含标点、数字、英文,应只保留中文"""
assert clean_text("今天是星期天,天气晴!123abc") == "今天是星期天天气晴"
def test_clean_text_pure_symbol():
"""纯标点符号,清洗后应为空"""
assert clean_text("!!!$$$ ,,") == ""
def test_clean_text_empty():
"""空字符串,清洗后仍为空"""
assert clean_text("") == ""
构造思路:
| 测试 | 输入 | 期望 | 覆盖场景 |
|---|---|---|---|
| 正常句子 | 中文+标点+数字+英文 | 只剩中文 | 常规路径 |
| 纯符号 | 只有符号 | 空串 | 边界 |
| 空串 | "" |
空串 | 边界 |
4.2.2 get_simhash(3 个)
def test_get_simhash_same_text():
"""相同文本,指纹必须一致"""
assert get_simhash("今天是星期天天气晴") == get_simhash("今天是星期天天气晴")
def test_get_simhash_diff_text():
"""不同文本,指纹应该不同"""
assert get_simhash("今天是星期天天气晴") != get_simhash("苹果香蕉梨子很好吃")
def test_get_simhash_empty():
"""空文本,返回 0"""
assert get_simhash("") == 0
构造思路:
| 测试 | 验证点 |
|---|---|
| 同文本指纹一致 | 确定性 |
| 不同文本指纹不同 | 区分度 |
| 空文本返回 0 | 边界 |
4.2.3 hamming_distance(2 个)
def test_hamming_same():
"""相同指纹,距离为 0"""
assert hamming_distance(123, 123) == 0
def test_hamming_opposite():
"""完全相反的 64 位指纹,距离为 64"""
full_one = (1 << 64) - 1
assert hamming_distance(0, full_one) == 64
构造思路:
- 相同 → 距离 0
- 全反(0 vs 2⁶⁴-1)→ 距离 64
4.2.4 calculate_similarity(4 个)
def test_sim_identical():
t = "今天是星期天,天气晴,今天晚上我要去看电影。"
assert calculate_similarity(t, t) == 1.0
def test_sim_empty_one_side():
t = "今天是星期天,天气晴,今天晚上我要去看电影。"
assert calculate_similarity(t, "") == 0.0
def test_sim_unrelated_less_than_related():
t1 = "今天是星期天,天气晴朗,我打算晚上和朋友一起去看电影,然后吃顿好的。"
t2 = "苹果香蕉梨子橘子都是水果,富含维生素,多吃对身体有好处,尤其是夏天。"
t3 = "今天是周天,天气晴朗,我晚上要和朋友去看电影,然后吃顿好的。"
res_unrelated = calculate_similarity(t1, t2)
res_related = calculate_similarity(t1, t3)
assert res_unrelated < res_related
def test_sim_sample():
t1 = "今天是星期天,天气晴,今天晚上我要去看电影。"
t2 = "今天是周天,天气晴朗,我晚上要去看电影。"
res = calculate_similarity(t1, t2)
assert 0.5 < res < 1.0
构造思路:
| 测试 | 验证点 |
|---|---|
| 自己比自己 | 100% |
| 一边为空 | 0% |
| 无关 < 相关 | 相对关系(避免绝对值脆弱) |
| 作业样例 | 落在合理区间 |
4.2.5 read_text_file(4 个)
def test_read_file_not_exist():
with pytest.raises(SystemExit):
read_text_file("这个文件肯定不存在_12345.txt")
def test_read_path_is_dir(tmp_path):
with pytest.raises(SystemExit):
read_text_file(str(tmp_path))
def test_read_file_wrong_encoding(tmp_path):
p = tmp_path / "gbk.txt"
p.write_bytes("中文内容".encode("gbk"))
with pytest.raises(SystemExit):
read_text_file(str(p))
def test_read_file_success(tmp_path):
p = tmp_path / "sample.txt"
p.write_text("今天是星期天,天气晴。", encoding="utf-8")
content = read_text_file(str(p))
assert content == "今天是星期天,天气晴。"
构造思路:用 tmp_path 临时目录构造各种文件:
- 不存在 → 退出
- 目录 → 退出
- GBK 编码 → 退出
- 正常 UTF-8 → 返回内容
4.2.6 main(3 个)
def test_main_wrong_argc(monkeypatch, capsys):
monkeypatch.setattr(sys, "argv", ["main.py", "only_one_arg"])
with pytest.raises(SystemExit):
main()
captured = capsys.readouterr()
assert "用法" in captured.out
def test_main_normal_run(tmp_path, monkeypatch):
orig = tmp_path / "orig.txt"
add = tmp_path / "add.txt"
out = tmp_path / "ans.txt"
orig.write_text("今天是星期天,天气晴,今天晚上我要去看电影。", encoding="utf-8")
add.write_text("今天是周天,天气晴朗,我晚上要去看电影。", encoding="utf-8")
monkeypatch.setattr(
sys, "argv",
["main.py", str(orig), str(add), str(out)]
)
main()
assert out.exists()
content = out.read_text(encoding="utf-8")
float(content)
assert len(content.split(".")[-1]) == 2
def test_main_output_permission_error(tmp_path, monkeypatch, mocker):
orig = tmp_path / "orig.txt"
add = tmp_path / "add.txt"
out = tmp_path / "ans.txt"
orig.write_text("今天是星期天,天气晴。", encoding="utf-8")
add.write_text("今天是周天,天气晴朗。", encoding="utf-8")
monkeypatch.setattr(
sys, "argv",
["main.py", str(orig), str(add), str(out)]
)
real_open = open
def fake_open(path, mode="r", *args, **kwargs):
if "w" in mode:
raise PermissionError
return real_open(path, mode, *args, **kwargs)
mocker.patch("builtins.open", side_effect=fake_open)
with pytest.raises(SystemExit):
main()
构造思路:
monkeypatch模拟命令行参数capsys抓取打印输出tmp_path构造临时文件mocker模拟open抛异常
4.3 覆盖率
使用 pytest-cov 生成报告:
pytest --cov=main --cov-report=html
最终覆盖率:99%。
未覆盖部分:main.py 的 if __name__ == "__main__": 入口分支。原因是 pytest 以 import 方式加载 main.py,此时 __name__ == "main" 而非 "__main__",该分支不会执行。这是 pytest 的固有行为,属正常现象。

4.4 测试设计评价
- 共 19 个测试,超过作业要求的 10 个。
- 覆盖了正常路径、边界、异常路径三类。
- 异常测试覆盖 5 种异常,与“异常处理说明”一节一一对应。
- 覆盖率 99%,除入口分支外全部覆盖。
五、计算模块部分异常处理说明
5.1 异常清单与设计目标
本模块共处理 5 种异常,每种的设计目标如下:
| 异常 | 设计目标 | 触发场景 |
|---|---|---|
FileNotFoundError |
输入文件不存在时给出明确提示,而不是抛出堆栈 | 命令行传入的路径无效 |
UnicodeDecodeError |
文件编码非 UTF-8 时提示用户 | 文件为 GBK 等编码 |
PermissionError |
文件无读/写权限时给出提示 | 文件被占用或权限不足 |
IsADirectoryError |
传入的是目录而非文件时提示 | 误传路径为文件夹 |
SystemExit |
命令行参数数量不对时打印用法并退出 | 参数不是 3 个 |
设计原则:
- 不抛裸异常:所有异常都捕获并打印中文提示,避免用户看到 Python 堆栈。
- 统一退出码:所有异常都
sys.exit(1),便于评测脚本判断。 - 错误信息包含路径:方便用户定位是哪个文件出错。
5.2 每种异常的单元测试与场景
5.2.1 FileNotFoundError
触发场景:命令行传入的原文路径不存在。
单元测试:
def test_read_file_not_exist():
"""文件不存在,应退出程序"""
with pytest.raises(SystemExit):
read_text_file("这个文件肯定不存在_12345.txt")
说明:用一个肯定不存在的文件名调用 read_text_file,断言程序退出(SystemExit)。
5.2.2 UnicodeDecodeError
触发场景:文件是 GBK 编码,程序只支持 UTF-8。
单元测试:
def test_read_file_wrong_encoding(tmp_path):
"""非 UTF-8 编码文件,应退出程序"""
p = tmp_path / "gbk.txt"
p.write_bytes("中文内容".encode("gbk"))
with pytest.raises(SystemExit):
read_text_file(str(p))
说明:用 tmp_path 创建一个 GBK 编码的文件,调用 read_text_file,断言退出。
5.2.3 PermissionError(读)
触发场景:传入的路径是目录,不是文件。
单元测试:
def test_read_path_is_dir(tmp_path):
"""传进来是目录,应退出程序"""
with pytest.raises(SystemExit):
read_text_file(str(tmp_path))
说明:tmp_path 本身是一个目录,直接用 open() 打开目录会抛出 IsADirectoryError(PermissionError 的子类),断言退出。
5.2.4 PermissionError(写)
触发场景:输出文件无写入权限。
单元测试:
def test_main_output_permission_error(tmp_path, monkeypatch, mocker):
"""输出文件无写权限,应退出程序"""
orig = tmp_path / "orig.txt"
add = tmp_path / "add.txt"
out = tmp_path / "ans.txt"
orig.write_text("今天是星期天,天气晴。", encoding="utf-8")
add.write_text("今天是周天,天气晴朗。", encoding="utf-8")
monkeypatch.setattr(
sys, "argv",
["main.py", str(orig), str(add), str(out)]
)
real_open = open
def fake_open(path, mode="r", *args, **kwargs):
if "w" in mode:
raise PermissionError
return real_open(path, mode, *args, **kwargs)
mocker.patch("builtins.open", side_effect=fake_open)
with pytest.raises(SystemExit):
main()
说明:
- 用
monkeypatch模拟命令行参数 - 用
tmp_path构造三个临时文件 - 用
mocker替换open,只在写模式抛PermissionError,读模式放行 - 断言
main()退出
为什么不能简单 mocker.patch("builtins.open", side_effect=PermissionError)?
因为那样连读文件也一起抛异常,read_text_file 会先捕获,走不到写文件的 except。所以需要 fake_open 区分模式。
5.2.5 SystemExit(参数数量不对)
触发场景:命令行只传了 1 个参数(应为 3 个)。
单元测试:
def test_main_wrong_argc(monkeypatch, capsys):
"""参数数量不是4个,应打印用法并退出"""
monkeypatch.setattr(sys, "argv", ["main.py", "only_one_arg"])
with pytest.raises(SystemExit):
main()
captured = capsys.readouterr()
assert "用法" in captured.out
说明:
monkeypatch把sys.argv改成只有 2 个元素- 断言
main()退出 capsys抓取标准输出,验证打印了“用法”
5.3 异常处理与测试对应表
| 异常 | 测试函数 | 覆盖率 |
|---|---|---|
FileNotFoundError |
test_read_file_not_exist |
✅ |
UnicodeDecodeError |
test_read_file_wrong_encoding |
✅ |
PermissionError(读) |
test_read_path_is_dir |
✅ |
PermissionError(写) |
test_main_output_permission_error |
✅ |
SystemExit |
test_main_wrong_argc |
✅ |
所有异常分支均被测试覆盖。
六、附:Git 提交记录
| commit message | 说明 |
|---|---|
chore: 新建学号文件夹与基础空文件 |
初始化 |
feat: 实现simhash论文查重基础版本 |
核心算法 |
test: 新增pytest单元测试文件test_si... |
测试框架 |
fix: 用 md5 替换内置 hash 保证指纹跨... |
稳定哈希 |
test: 补充文件读取与 main 流程测试 |
覆盖率提升至 93% |
test: 补充编码异常与写入权限异常测试 |
覆盖率提升至 99% |
perf: 添加性能分析脚本(cProfile 与 l...) |
性能分析 |
fix: 性能分析脚本改用 cProfile.runctx... |
修复 pytest 误收集 |
perf: 为 get_simhash 添加 lru_cache... |
第一轮性能优化 |
fix: 改用3-gram字符特征,提升对字符... |
第二轮算法改进 |

浙公网安备 33010602011771号