20232313 实验四《Python程序设计》实验报告

20232313 2025-2026-2 《Python程序设计》实验4报告

课程:《Python程序设计》
班级: 2323
姓名: 吴至远
学号:20232313
实验教师:王志强
实验日期:2026年5月18日
必修/选修: 公选课

1.实验内容

(一)实验内容
Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
注:在Windows/Linux系统上使用VIM、PDB、IDLE、Pycharm等工具编程实现。

程序能运行,功能丰富(至少5个功能),先说说成品功能吧:
①可文本输入也可OCR输入;
②联合URLScan网站进行具体分析,可以方便地查看具体分析内容;
③调用DeepSeek API进行分析;
④具备简单的本地知识库;
⑤生成美观的pdf报告;
⑥支持一件学习扫描过的经典案例;
⑦支持一件添加删除新的知识库案例。

课上老师说过Python很方便调用大模型(因为有相应的、成熟的库),所以我想做一个相对简单的工具来测试一下。
同时,看看能不能再本地部署一个小模型来用一下(用于学习已经分析过的案例,即本地知识库的概念)。同时试用一下课上提到过的OCR技术(应该有Lite版本的,刚好试用一下)。
所以我此次实验大体的思路是做一个功能简单的小工具,但是我可以在Python中尝试调用比较丰富的功能。

综上所述,我让豆包推荐了一下有什么小项目能够满足我的要求,选择了做一个“恶意URL检测”的小工具。它能从文本或者图片里识别URL并且给出对其是否是恶意URL。
到后面做了一个锦上添花的功能,即能够把对具体URL的信息写入一个PDF文件中并保存。其还具备本地知识库功能,能把以前的正确结果优先作为依据,从而增强效率。本地知识库可以提前导入重要依据或者以前的判断案例。

2. 实验过程及结果

开发了一个简易的邮件内容分析工具,该工具的主要功能涵盖:基于OCR的图片钓鱼检测、利用外部网站工具来分析恶意URL、以及自动化生成PDF的简略报告。

这次就不一步一步说了。
任务要求我们要体现实验分析、设计、实现过程、结果等信息,那么我就把这部分信息一一说明如下。

1. 实验分析

大致的思路是:
工具能把粘贴的邮件或短信内容(可能是中文钓鱼文本)先提取出其中的网址,然后再用安全网站扫描该网址的风险,
再把原文和扫描结果一起发给DeepSeek AI进行分析,结果生成一份的PDF报告。
大致这样。
那么大概需要干什么呢?

首先需要能把钓鱼邮件文本获取了。 这里可以用对话框让用户直接输入,也可以允许上传图片然后对图片运用OCR技术提取出对应的文本。
然后需要能把文本中的URl过滤出来,同时让DeepSeek从语义的角度分析一下这个有没有钓鱼邮件的风格倾向。在写报告和判定的时候,这两个是最关键的因素。
然后应该让DeepSeek综合分析结果给出评价内容,同时根据我们设定的标准给出分数,随后判定出这个内容是否安全,把这些整合进报告里就可以了。

另外额外的一个功能是本地知识库功能,能把以前的语义和结果对照起来,从而达到学习的效果,增强效率。本地知识库应该可以提前导入一些本地的重要文件之类的(目前想到的是txt文本)。

2. 实验设计

肯定是要使用tkinter组件的。先思考大致使用的逻辑。tk一般分成两层设计,那么在这里只需要考虑两层都应该干什么,然后想办法写出来就行了。

  • 表现层 :负责与用户交互,接收可疑文本输入,并展示最终的检测结果。
  • 逻辑层 :核心控制中枢。应该由一个独立的类主导,负责文本处理、和服务器逻辑操作等的具体实现方法。
    在python中,tk可以采用新建一个类的方式将逻辑和表现层分开。表现层大多是UI+交互选项,而交互选项可以通过将方法绑定到特定按钮的方式来解决。
    使用这种交互方式时,tk推荐另开一个异步子线程去完成特定操作,而主线程始终是UI界面,这样可以保证用户流程的体验。

用户使用起来的流程大致是这样的:
image

大致架构就是这样,接着就是把具体的方法和类写出来就可以了。

3. 实验过程

这里简单说明一下我设计一些方法的思路吧。函数方法是如何设计的思考是比较重要的,不应该在报告里忽略。
按照事情发展顺序逐步说明。
值得一提的是,我不会给出从头到尾的版本迭代过程,因为太多了。所以我可能会按照用户使用顺序对我的开发过程进行说明,但是实际上的版本迭代过程不是这样的。比如说获取文本,一开始是不会直接上OCR的,肯定是先把基础tk文本框做好。但是在这里我会在开头就直接说明OCR的运用。
首先要做的事情就是获取用户输入以及进行预处理,主要步骤有获得可操作的文本、提取url以及和urlscan进行交互获得信息。
这部分代码框图如下:
image

1.获得可操作的文本

tk怎么用不说了。这里说说怎么使用的OCR。有人开发好了轻便的方法,直接拿来用——引入了Tesseract-OCR引擎。
Tesseract-OCR需要提前安装在我们的主机里,随后在python里直接进行调用即可。
在这里,我通过analyze_image方法实现图片转文字。考虑到中文环境下语言包配置的复杂性,代码设计了自动降级策略,即优先尝试“简体中文+英文”混合识别模式(lang='chi_sim+eng');若因环境缺失中文库导致报错,则自动降级为纯英文模式(lang='eng')。
image

2.url提取规则

第一件困难的事情是,假如我们有文本,我们应该如何提取出其中的URL?这需要我们自己设计一套规则,如下:
对于用户输入的长文本,我们先从用户输入中提取所有可打印的ASCII字符连续序列,将其加入到candidates列表中以待进一步处理:
image
随后,我们对candidates中的可能url进行初步筛选。初步筛选去除了首尾部粘连的中英文符号,过滤了带@符号的、没有“.”、非IP格式的纯数字以及字符长度小于4的文本。额外地,考虑到有些url是纯IP形式,IPv4格式的文本会被保留。
image
最后,对剩余文本进行特征判断,符合url基础特征的将加入clean_url列表,后续传入云端分析进行扫描。(这里选用的网站是Urlscan.io)
特征包括常见网站头、IP、以及裸网站。
image

3.调用DeepSeek分析结果

有了URL,直接给URLSCAN.IO网站扫描即可。选择这个URL扫描网站的好处在于,它可以支持调用API来返回需要的JSON包,这样利好Python编程。
同时Deepseek也提供好用便宜的API,只需要在平台把自己相关的API密钥拿到然后用就行了。
为了生成更好的结果,这里会把URL网站返回的JSON包中一些必要的信息、本地知识库(最后会说明,这里只要知道它会返回有用信息就行)里提供的类似信息一起提供给DeepSeek分析推理,
最后让DeepSeek返回我们需要的有价值的判别信息,并使用fpdf库来把返回的信息整合成一份Pdf。大致这个效果。
在这里先提供一下我撰写的对DeepSeek的提示词:
image
调用交互的代码这里就不放了,有标准的一套打法可以在Deepseek官网查询到。
接着我们来分析AI和RAG的交互设计过程。
我在我的项目中重构了分析流程,在AI分析这部分引入了RAG的辅助,即在AI进行逻辑分析前,会先查阅本地的“记忆库”(即ChromaDB),检索是否存在历史相似案例或白名单。
先说说向量化检索机制。在LocalKnowledgeBase类中,系统维护了一个基于ChromaDB的向量数据库。这里向量化检索机制的作用是当检测开始时,系统能够本地库中召回Top-N个最相关的知识片段。

def search(self, query_text, n_results=4, threshold=0.4):
    # 1. 将当前的查询文本转化为高维向量
    query_embedding = self.encoder.encode([query_text]).tolist()
    
    # 2. 在 ChromaDB 中检索最近邻
    results = self.collection.query(
        query_embeddings=query_embedding,
        n_results=n_results
    )
    
    knowledge_list = []
    if results['documents']:
        for i in range(len(results['documents'][0])):
    
            dist = results['distances'][0][i]
            score = 1 - dist 

            if score > threshold:
                knowledge_list.append({
                    "content": results['documents'][0][i],
                    "category": results['metadatas'][0][i].get('category', 'general'),
                    "score": round(score, 2)
                })
    return knowledge_list

首先计算输入文本的向量特征:代码使用sentence_transformers加载本地离线模型,通过余弦相似度将非结构化的文本转化为计算机可理解的数学向量,这是语义检索的基础。
为了保证召回质量,我还设计了一个关键的阈值过滤机制。因为如果强行引入不相关的历史数据作为上下文,反而会干扰 AI 的判断。在这里取相似度至少大于0.4的文本作为Prompt判断依据。
第二个要说明的是动态上下文融合与权重分配。需要达到的目的是使本地知识高于通用知识的优先级。
具体做法是获取到本地知识后,将其与Urlscan的扫描结果、用户的原始文本进行多源数据融合,待到构建 Prompt时再显式地告诉AI:“如果本地知识库中有匹配……请直接依据本地情报进行判定”。

# 2. 构建包含“负向约束”与“优先级规则”的 Prompt
    prompt = f"""
    请分析以下文本是否存在网络钓鱼风险。
    
    【输入内容】: {text}
    {local_info_str}  <-- RAG 注入点
    【扫描情报】: {scan_summary}
    
    【输出要求】:
    1. 必须输出标准的 JSON 格式。
    2. **重要原则**: 如果【本地知识库】中有匹配的安全策略、白名单或相似案例,
       请直接依据本地情报进行判定,并在 reasoning 中引用来源。
    """
    # ... 发送请求给 DeepSeek ...
4.本地知识库的构建和运用

这里的设计逻辑如下图所示:
image
在本地用一下别人做好的语义模型并不是那么难。根据参考文献的其中一篇文章的步骤来执行就可以获取到一个轻量级的语义比对模型。同时利用ChromeDB来存储。
它大致的效果是将长文本分片,然后运用特定方法计算高位向量并存储,后续通过向量距离比对来观察二者语义是否接近。
首先说说构建原理,这个比较麻烦:
在构建RAG(检索增强生成)系统的过程中,如何让机器“听懂”人话并“记住”知识是最大的挑战。我在LocalKnowledgeBase模块的设计中,并没有盲目追求参数量巨大的模型,而是基于“轻量化桌面应用”的定位,做出了一系列针对性的技术选型。
首先是向量化模型Embedding Model的选取。代码中采用了all-MiniLM-L6-v2模型。这在当前的开源社区中被公认为是一个“性价比”极高的选择。它虽然体积小巧,但在语义相似度任务上的表现却非常出色,并可极大地降低了本地 CPU 的推理负担。
为了适应攻防演练中常见的离线或弱网环境,我在代码初始化阶段设计了优先加载本地路径 ./my_model 的逻辑,只有在本地模型缺失时才会尝试联网下载。

model_path = "./my_model"
if os.path.exists(model_path):
    self.encoder = SentenceTransformer(model_path) # 优先加载离线模型
else:
    self.encoder = SentenceTransformer('all-MiniLM-L6-v2') # 联网回退

其次是文本数据的处理策略。高质量的检索始于高质量的切分,如果简单地按照字符数对文本进行硬切割,很容易将一个完整的攻击案例切得支离破碎,导致AI获取的上下文残缺不全。因此,我自定义了 _semantic_split智能切分算法。该算法摒弃了机械的定长切割,转而采用“段落优先”的策略。代码会先清洗多余的空行,然后以自然段落的双换行符为边界进行逻辑分组。系统会动态计算当前缓冲区的长度,尽量将属于同一语义范畴的文字保留在同一个切片(Chunk)中,只有在累积长度超过 400 字符时才进行截断。这种保留语义完整性的预处理方式,让存入数据库的每一条知识片段都是逻辑通顺的,从而大幅提升了后续检索的准确率。

def _semantic_split(self, text, max_chunk_size=400):
    text = re.sub(r'\n{3,}', '\n\n', text) # 清洗多余空行
    paragraphs = text.split('\n\n')        # 按自然段落粗切
    # ... (动态合并段落逻辑)
    if len(current_chunk) + len(para) < max_chunk_size:
        current_chunk += "\n" + para

最后是向量数据库的架构选择。考虑到项目代码是单机运行的桌面客户端,部署庞大的服务器级数据库(如Milvus或 Elasticsearch)显然过于沉重且维护成本高昂。因此,我选择了ChromaDB作为存储后端。ChromaDB的PersistentClient 模式非常契合本项目的架构,它不需要独立的后台进程,而是直接以文件的形式将数据持久化存储在本地的 ./local_data_store 目录下。这使得无论是用户的历史操作记录还是导入的安全白名单,都能在软件重启后依然完好保存。配合其内置的高效HNSW索引算法和余弦相似度计算能力,系统能够在毫秒级内从成千上万条记录中召回最相关的知识,完美支撑了上层AI的实时分析需求。

self.client = chromadb.PersistentClient(path=db_path)
self.collection = self.client.get_or_create_collection(
    name="security_knowledge",
    metadata={"hnsw:space": "cosine"} # 指定余弦相似度空间
)

最后,为了支撑上层AI的逻辑判断,我在入库环节强化了元数据(Metadata)管理。当调用add_document接口时,系统不仅存储向量,还会同步写入 category分类标签。这使得 RAG 引擎在后续检索时,能够清晰区分出哪些是“用户录入的历史案例(user_case)”,哪些是“企业白名单(whitelist)”,为构建动态权重的 Prompt 提供了底层数据支撑:

metadatas = [{
    "source": source_name,
    "category": category, # 关键分类标签,用于AI区分知识类型
    "date": datetime.now().strftime("%Y-%m-%d")
} for _ in chunks]

说完麻烦的构建原理后,其次说说如何做到学习。
在我的代码中,RAG的自我学习效果逻辑是这样的:
当用户在界面上点击“学习此案例”按钮时,代码首先会从界面文本框中获取经过OCR修正后的最终文本,同时调取AI刚刚生成的判定结论与推理逻辑,将这两部分信息通过字符串格式化拼接成一个标准化的知识块后存入本地知识库中,以备后续查询。
可以把分析好的结果打包成一个案例塞到本地知识库中学习,也可以提前塞一些需要注意的资料进去。
这一机制主要通过前端的learn_current_case函数与后端add_document接口协同完成。当用户在界面上点击“学习此案例”按钮时,代码首先会从界面文本框中获取经过OCR修正后的最终文本,同时调取AI刚刚生成的判定结论与推理逻辑,将这两部分信息通过字符串格式化拼接成一个标准化的知识块后存入本地知识库中,以备后续查询。
image
为了防止误操作导致错误知识污染数据库,代码在此处设计了二次确认逻辑。只有当用户明确点击确认后,系统才会调用本地知识库接口。
代码在入库时会自动为该条数据打上 category='user_case' 的元数据标签。该标签会使得其在参考中参考权重更大。

def learn_current_case(self):
    # 1. 获取“攻击样本”:这是经过 OCR 识别和人工修正后的最终文本
    content = self.text_input.get("1.0", tk.END).strip()
    
    # 2. 获取“专家判例”:这是刚才 AI 分析出的结论(假设已被人工认可)
    verdict = self.last_ai_result.get('verdict', '未知')
    reasoning = self.last_ai_result.get('reasoning')

    # 3. 知识萃取与重组:将样本与结论打包,形成完整的证据链
    knowledge_text = f"【历史案例回顾】\n邮件原文:\n{content}\n\n=== 专家判定 ({verdict}) ===\n分析逻辑:{reasoning}"
    
    # 4. 增量更新:调用后端接口,实时写入向量数据库
    # 注意:这里标记 category="user_case",赋予其高优先级
    try:
        self.logic.kb.add_document(knowledge_text, source_name=case_name, category="user_case")
        messagebox.showinfo("成功", "已录入!工具变得更聪明了。")
    except Exception as e:
        self.log(f"录入失败: {e}")

实际运用的时候会把最匹配的案例拿出来供DeepSeek分析,并将参考优先级设为最高。就像下面的代码:
image
这里的逻辑很好理解。
至于调用的本地小模型,需要自己设置分片之类的参数,按照推荐的参数设计就可以了,效率还是比较高的。

5.pdf生成

调用fpdf库,版式代码让Ai生成就好了,这里十分方便,因为并不涉及太多逻辑技巧,这是AI最擅长的,也是我们可以放心托付的。
这里不提供代码。
pdf的内容是由DeepSeek返回JSON格式的包内容来决定的。DeepSeek返回标准格式的包,可以使得我们的pdf报告内容趋于稳定。
同时要注意的是,中文报告需要自己想办法解决,因为字体需要自己下载sim字体文件。我在这里选用的是系统的宋体。

6.多线程异步交互架构

为了保证GUI界面的流畅性,我采用了多线程架构,将耗时的网络I/O操作与UI渲染分离。
首先是在总体框架MVC下,程序的GUI界面和逻辑处理程序是分开的。所以这里程序会使用Python的threading模块创建后台线程来执行扫描任务,避免阻塞主界面响应。

def start_scan(self):
......
threading.Thread(target=self.run_process, args=(content,), daemon=True).start()

同时用root.after方法安全地更新GUI组件,防止多线程直接操作界面导致的崩溃问题。这种设计让用户能在扫描过程中继续操作窗口,提升使用体验。如下,展示了更新日志和更新按钮时运用root.after的情况:

def update_log(self, msg):
        self.root.after(0, lambda: self._log(msg))  #更新日志
               ......
         self.root.after(0, lambda: self.finish_process(False)) #更新按钮

4. 实验结果

进行了一些测试,效果还是不错的:

①测试场景:
发件人: Microsoft Teams 通知服务 notifications@teams-microsoft-voice-message.com 收件人: 销售部; 市场部 主题:
您有一条来自 "李强 (总经理)" 的未接语音留言 (时长: 2分15秒) 日期: 2026年1月9日
Microsoft Teams
您好,
您在 Microsoft Teams 上收到了一条新的语音留言。
来自: 李强 (General Manager) 接收时间: 2026-01-09 09:30 AM (GMT+8) 时长: 2:15 转录预览:
"关于那个合同的紧急修改意见,请务必在今天中午前......"
由于您的 Teams 客户端离线,该消息已暂存至云端服务器。请点击下方按钮收听完整录音。
[ >> 点击收听语音留言 << ] (URL: https://login.microsoftonline-shared-voice-mail.club/auth/play.php?id=88231)
注意:此链接将在 48 小时后失效。
Microsoft Corporation | One Microsoft Way, Redmond, WA 98052

image

②测试场景:
注册Shadon时,Shadon发送的验证码。
 Hi,
Thank you for creating account on Shodan. Please click on the link below to activate your account.
URL:  https://account.shodan.io/activate/b103e55325a94490b0fde5e5bc60248a

image
image

③测试场景:
输入残缺的URL(如 http://)、邮箱地址或纯数字干扰项。

image
image

5. 代码托管

git可以使用命令行版本的,也可以使用图形化或者平台。git的命令行版本掌握起来较为麻烦,在这里我们使用方便的图形化工具。
VSCODE可以支持便捷的图形化界面来将代码托管到码云(Gitee)。下面是在VSCODE平台上将代码提交到Gitee上的具体过程。
在VSCODE左侧栏选择上往下第三个“源代码管理”(在这里事先配置过),点击“提交”,随后在“更改”部分点击确认,通过更改:
image
发现代码提交到Gitee平台,如下:
image

所有版本和源代码均在仓库中。这里不放出来了。
总体函数逻辑如下图:
image

3. 实验过程中遇到的问题和解决过程

问题一:大模型输出格式不可控导致的解析失败
问题描述:
在早期测试中,尽管Prompt中要求返回JSON格式,但DeepSeek仍偶尔会在返回内容中包含Markdown代码块标记(如 ```json)或在JSON前后添加解释性文字。直接将此类字符串传入 json.loads() 会抛出JSONDecodeError异常,导致程序中断且无法生成报告。
解决方案:
1.在解析前增加了数据清洗的预处理逻辑,使用replace函数移除Markdown标记。
2.通过查找{和}的索引位置,精准提取有效的JSON字符串片段,过滤掉了首尾的冗余文本。最后引入try-except机制解决JSONDecodeError,并在解析失败时记录错误日志并返回None,确保程序不会直接崩溃。

问题二:同步网络请求导致的GUI界面卡住
问题描述:
Urlscan.io的扫描任务通常需要5-20秒才能完成。最初我将扫描逻辑写在主线程中,导致点击“开始侦测”后,整个软件界面进入未响应状态,无法进行任何操作。
解决方案:
为保证GUI界面的流畅性,我将run_process逻辑放入守护线程(Daemon Thread)中后台运行,实现了UI渲染与网络I/O的分离。同时,为遵循Tkinter的线程安全原则,我不直接在子线程操作UI组件,而是通过root.after 方法将日志更新和按钮状态切换的任务调度回主线程执行(这也是Tkinter经典的操作方法),彻底解决了界面卡顿与潜在的并发冲突问题。

全课总结

盖浇饭和蛋炒饭。
还有Git使用、Gitee账号创建和代码托管,以及Socket套接字实现网络通信、pip用法、pyinstaller打包教程和网络爬虫概念,以及序列类型中字典、列表、集合、元组的区别,爬虫的使用等等。 让我知道了Python不是C语言的简化版。

课程感想体会、意见和建议

总体氛围比较轻松愉悦,学得不会那么吃力,上课有助于青少年健康成长,希望保持下去。
单词打卡非常有新意,提问方式很好,能够促进学生主动思考。

参考资料

posted @ 2026-05-18 18:15  20232313-吴至远  阅读(46)  评论(0)    收藏  举报