【从零构建多智能体 01】Python 快速入门:从接管 AI 代码到实现多格式文件解析器

系列总名称:从零构建多智能体:Harness & Hermes 项目实战系列

本文是系列第 1 篇,目标是帮助零基础读者先掌握智能体项目开发必需的 Python 基础能力。

一、为什么 AI 都能写代码了,我们还要学 Python?

现在很多同学写代码的方式已经变了:不再从空白文件一行一行敲,而是先让 AI 生成一个版本,再根据需求不断修改。

这确实提高了效率,但也带来了一个非常现实的问题:

AI 可以生成代码,但你必须能接管代码。

如果你完全看不懂 Python 语法,后面会遇到几个典型困境:

  1. AI 写了一段代码,你不知道程序从哪里开始执行。
  2. 代码报错了,你只能把报错复制给 AI,但无法判断 AI 的修复是否合理。
  3. 需求变了,你不知道应该改哪一行、哪个函数、哪个模块。
  4. AI 改完后表面能跑,但引入了新的隐藏问题。
  5. 代码无法接入真实项目,因为结构混乱、依赖不清、输入输出不稳定。

所以,学习 Python 的目的不是为了和 AI 比谁写得快,而是为了获得对代码的控制权。

在智能体项目里,Python 常常负责:

  • 调用大模型 API;
  • 处理 Prompt 和上下文;
  • 解析 PDF、Word、图片、音频等资料;
  • 编写 FastAPI 后端接口;
  • 管理数据库、缓存和任务队列;
  • 串联 LangChain、LangGraph、Pydantic 等工程工具。

如果你想做的不只是 Demo,而是能维护、能扩展、能上线的智能体项目,Python 基础就是绕不开的底座。

二、本文要解决什么问题?

本文不是一篇纯语法速查表,而是围绕智能体项目开发,帮助你建立一条实用路线:

  • 知道 Python 在 AI Agent 项目里的作用;
  • 掌握最短学习路径;
  • 搭好虚拟环境和依赖管理方式;
  • 理解变量、条件、循环、函数、模块、包等基础语法;
  • 学会用函数封装可复用逻辑;
  • 掌握文件读写、修改、删除和编码处理;
  • 理解类型注解、推导式、装饰器、OOP、Pydantic;
  • 最后实现一个统一文件解析器:一个函数解析 PDF、Word 和图片。

最终目标是从“能运行一段 Python 脚本”,提升到“能写一个小型工程模块”。

三、跟做前先准备一个练习目录

为了让你能一步一步跟下来,建议先在电脑上新建一个干净目录。后面的所有代码都放在这个目录里。

假设目录叫:

agent_python_start/

你可以手动创建,也可以在终端执行:

mkdir agent_python_start
cd agent_python_start

接下来我们会在这个目录中逐步创建下面这些文件:

agent_python_start/
  01_word_count.py
  02_data_types.py
  03_functions.py
  04_file_tools.py
  easy_parser/

每学完一块知识,就会落到一个可运行的小文件上。这样你不是“看懂了”,而是真的把代码跑起来了。

建议跟做顺序:

  1. 先运行最简单的词频统计,理解 Python 程序的输入、处理、输出。
  2. 再学习数据类型,知道字符串、列表、字典在项目中怎么用。
  3. 然后学习函数封装,把重复逻辑变成可复用函数。
  4. 接着学习文件处理,掌握读、写、改、删。
  5. 最后完成 easy_parser 小项目,把前面的基础串成一个工程化案例。

如果你是纯小白,不要一开始就追求写得很高级。先保证每一步都能运行,再逐步优化结构。

四、Python 学到什么程度才够做智能体项目?

初学者很容易陷入一个误区:想把 Python 全部学完再做项目。

这条路非常慢,也不适合 AI 项目开发。更合理的方式是围绕项目所需能力学习。

对于智能体项目,Python 基础至少要达到下面 4 个标准:

能力具体表现
看懂代码结构能知道入口文件、函数、模块、依赖之间的关系
能改关键逻辑能根据需求修改输入、处理流程和输出格式
能定位报错能根据异常信息判断是路径、依赖、类型还是业务逻辑问题
能封装模块能把重复代码封装成函数、工具层或类,方便复用

如果你能做到这 4 点,AI 写代码反而会变成你的放大器。AI 负责快速生成初稿,你负责审查、修正、封装和验收。

五、Python 最短学习路径

面向智能体项目,Python 学习可以分成 3 个阶段。

阶段 1:能写小脚本

这个阶段的核心目标是跑通最小闭环:

输入 -> 处理 -> 输出

例如:

  • 输入一段文本;
  • 按空格切词;
  • 用字典统计词频;
  • 打印统计结果。

请在 agent_python_start/ 目录下新建文件:

01_word_count.py

写入下面这段代码:

text = "python is good python is easy"

counts = {}
for word in text.split():
    counts[word] = counts.get(word, 0) + 1

print(counts)

然后在终端执行:

python 01_word_count.py

你应该看到类似输出:

{'python': 2, 'is': 2, 'good': 1, 'easy': 1}

这段代码很小,但已经包含了 Python 入门最重要的几个知识点:

  • 变量;
  • 字符串;
  • 列表式遍历;
  • for 循环;
  • 字典;
  • dict.get()
  • 输出结果。

逐行解释一下:

text = "python is good python is easy"

这行代码创建了一个字符串变量 text。你可以把它理解成程序的“输入数据”。真实项目里,这个输入可能来自用户问题、上传文件、接口请求或数据库。

counts = {}

这行代码创建了一个空字典。字典适合保存“键值对”,这里的键是单词,值是单词出现的次数。

for word in text.split():

text.split() 会把字符串按空格拆成多个单词:

["python", "is", "good", "python", "is", "easy"]

for word in ... 的意思是:每次从这个列表里取出一个单词,放到变量 word 中,然后执行缩进里面的代码。

counts[word] = counts.get(word, 0) + 1

这行是词频统计的核心。counts.get(word, 0) 表示:

  • 如果 word 已经在字典里,就取出原来的次数;
  • 如果 word 不在字典里,就先当作 0 次;
  • 最后再加 1。

例如第一次遇到 python

counts.get("python", 0) -> 0
0 + 1 -> 1

第二次遇到 python

counts.get("python", 0) -> 1
1 + 1 -> 2
print(counts)

这行把最终结果打印到终端。对于新手来说,print() 是最重要的调试工具之一。你可以在不理解某个变量时先打印出来,看程序运行到那里时它到底是什么。

你可以试着把第一行改成:

text = "agent agent python fastapi python"

再运行一次,观察输出结果如何变化。学习编程最有效的方法就是“小改动 + 立即运行 + 看结果”。

很多 AI 项目里的复杂代码,本质上也是这个模型:

用户输入 / 文件 / 接口参数 -> 清洗、解析、调用模型 -> 返回文本、JSON 或写入数据库

阶段 2:能做小项目

当脚本变长后,就要考虑代码组织。

这个阶段你需要掌握:

  • 函数封装;
  • 模块拆分;
  • 包导入;
  • 虚拟环境;
  • 依赖管理;
  • 文件读写;
  • 异常处理;
  • 日志输出。

比如“文件解析器”就不应该全部写在一个 main.py 里,而应该拆成:

easy_parser/
  main.py
  src/
    __init__.py
    core.py
    parsers/
      __init__.py
      base.py
      pdf_parser.py
      docx_parser.py
      image_parser.py

这样代码才有扩展空间。

阶段 3:能写 API 和服务

后续进入智能体项目后,Python 会进一步承担后端服务能力。

你会接触:

  • FastAPI;
  • Pydantic;
  • JWT 鉴权;
  • 数据库建模;
  • Redis 缓存;
  • LangChain 调用链路;
  • Prompt 模板管理;
  • 文件上传和资源管理。

所以这篇 Python 基础不是孤立知识,而是给后面的 FastAPI、文件解析、智能体编排打基础。

六、开发环境:先把项目跑稳

很多 Python 报错不一定是代码错了,而是环境不一致。

常见问题包括:

  • 你电脑装了某个库,别人电脑没装;
  • 你用的是新版库,代码依赖旧版 API;
  • 多个项目共用一个全局 Python 环境,依赖互相污染;
  • pip install 安装到了另一个 Python 解释器下面。

解决方式是:每个项目一个虚拟环境,每个项目一份依赖文件

1. 创建虚拟环境

先进入前面创建的练习目录:

cd agent_python_start

然后确认 Python 能正常使用:

python --version

如果你看到类似下面的输出,说明 Python 已经安装成功:

Python 3.11.8

如果提示 python 不是内部或外部命令,说明 Python 没有安装,或者安装时没有加入环境变量。此时建议重新安装 Python,并勾选 Add Python to PATH

Windows PowerShell:

python -m venv .venv
.\.venv\Scripts\Activate.ps1

Windows CMD:

python -m venv .venv
.\.venv\Scripts\activate.bat

macOS / Linux:

python3 -m venv .venv
source .venv/bin/activate

激活成功后,命令行前面通常会出现:

(.venv)

这表示你现在已经进入当前项目自己的 Python 环境。后面安装的第三方库会优先安装到 .venv 目录中,不会污染全局环境。

退出虚拟环境:

deactivate

2. 安装依赖

例如安装请求库:

pip install requests

这里的 requests 是一个常见的 HTTP 请求库。后面智能体项目调用大模型接口、请求后端服务时,经常会用到类似的网络请求能力。

查看当前环境已安装的包:

pip list

如果安装成功,列表里应该能看到 requests

卸载依赖:

pip uninstall requests

3. 锁定依赖版本

把当前环境中的依赖写入 requirements.txt

pip freeze > requirements.txt

生成后,当前目录下会多出一个 requirements.txt 文件。这个文件的作用是记录项目依赖版本,方便别人复现同样的运行环境。

在另一台机器恢复环境:

pip install -r requirements.txt

可以把它理解为:别人拿到你的项目后,不需要一个一个猜要装什么库,只要执行这一条命令,就能把依赖装齐。

对于本文的 easy_parser 示例,依赖大致包括:

PyMuPDF>=1.23.0
python-docx>=0.8.11
paddlepaddle>=2.6.0
paddleocr>=2.7.0
Pillow>=10.0.0

如果在国内网络环境下安装较慢,可以使用镜像源:

pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

注意:OCR 相关库体积较大,首次运行还可能下载模型文件,安装和初始化时间会更长。

七、Python 基础语法:先掌握项目里最常用的部分

1. 变量和基础数据类型

Python 常见数据类型如下:

类型示例常见用途
str"hello"文本、Prompt、文件内容
int10数量、页码、状态码
float0.85分数、相似度、模型温度
boolTrue开关、判断结果
list[1, 2, 3]有序集合
dict{"name": "Ada"}JSON、接口参数、配置
set{"pdf", "docx"}去重、集合判断
tuple(1, "ok")固定结构数据

示例:

请新建文件:

02_data_types.py

写入:

name = "Ada"
nums = [1, 2, 3]
user = {"id": 1, "name": "Ada"}
tags = {"python", "ai"}
pair = (1, "ok")

print(name.upper())
print(nums[0])
print(user["name"])
print("python" in tags)
print(pair[1])

运行:

python 02_data_types.py

你会看到:

ADA
1 Ada True ok

这段代码里:

  • name.upper() 表示把字符串转成大写;
  • nums[0] 表示取列表第 1 个元素,Python 下标从 0 开始;
  • user["name"] 表示从字典里取出 name 对应的值;
  • "python" in tags 表示判断集合里是否包含某个元素;
  • pair[1] 表示取元组第 2 个元素。

在智能体项目中,dict 尤其重要,因为接口参数、大模型响应、配置项、消息结构通常都可以表示成字典或 JSON。

2. 条件判断

score = 86

if score >= 90:
    level = "A"
elif score >= 80:
    level = "B"
else:
    level = "C"

print(level)

Python 使用缩进表示代码块,不使用 JavaScript 那样的大括号。

JavaScript 写法:

if (score >= 90) {
  level = "A";
}

Python 写法:

if score >= 90:
    level = "A"

这也是很多新手最容易出错的地方:缩进不一致会直接导致语法错误或逻辑错误。

3. 循环

遍历列表:

files = ["a.pdf", "b.docx", "c.png"]

for file in files:
    print(file)

遍历字典:

counts = {"python": 2, "agent": 1}

for word, count in counts.items():
    print(word, count)

在文件解析器中,我们经常需要遍历目录下的文件:

from pathlib import Path

input_dir = Path("assets")
files = [file for file in input_dir.iterdir() if file.is_file()]

for file in files:
    print(file.name)

4. 函数

函数的作用是把一段逻辑封装起来,减少重复代码。

请新建文件:

03_functions.py

写入:

def normalize_text(text: str) -> str:
    return " ".join(text.strip().split()).lower()

print(normalize_text("  Hello   Python  "))

运行:

python 03_functions.py

输出:

hello python

这个函数做了 3 件事:

  • 去掉首尾空格;
  • 把多个连续空格压缩成一个;
  • 转成小写。

代码拆开看:

def normalize_text(text: str) -> str:

这行定义了一个函数:

  • def 表示定义函数;
  • normalize_text 是函数名;
  • text 是参数,也就是调用函数时传进来的数据;
  • text: str 表示参数应该是字符串;
  • -> str 表示这个函数最终会返回字符串。
text.strip()

去掉字符串首尾多余空格。

text.strip().split()

把字符串按空白字符拆成列表。多个连续空格会被自动处理。

" ".join(...)

把列表重新拼成一个字符串,中间用一个空格连接。

.lower()

把结果转成小写。

函数调用时,程序会先执行括号里的内容:

normalize_text("  Hello   Python  ")

函数返回结果后,print() 再把结果打印出来。

函数命名建议采用“动词 + 名词”的结构,例如:

  • read_file
  • parse_pdf
  • validate_user
  • normalize_text
  • build_prompt

这样的函数名更容易表达意图。

八、函数封装:代码从“能跑”到“好维护”

很多初学者写代码时,会把所有逻辑都堆到一个文件里。

例如:

name = input("请输入用户名:")
if len(name) < 3:
    print("用户名太短")

password = input("请输入密码:")
if len(password) < 6:
    print("密码太短")

这段代码可以运行,但维护性不好。因为长度校验逻辑重复出现了。

更好的写法是封装函数:

def check_length(value: str, min_length: int, field_name: str) -> bool:
    if len(value) < min_length:
        print(f"{field_name}长度不能少于 {min_length}")
        return False
    return True

name = input("请输入用户名:")
password = input("请输入密码:")

check_length(name, 3, "用户名")
check_length(password, 6, "密码")

这个函数的参数含义如下:

参数含义示例
value要检查的字符串用户名、密码、标题
min_length最小长度3、6、10
field_name字段中文名,用来提示用户用户名、密码

返回值是 bool

  • 返回 True 表示校验通过;
  • 返回 False 表示校验失败。

如果你希望失败时直接停止程序,也可以改成抛异常:

def require_min_length(value: str, min_length: int, field_name: str) -> None:
    if len(value) < min_length:
        raise ValueError(f"{field_name}长度不能少于 {min_length}")

这类校验逻辑在后端接口里非常常见。例如用户注册时,要检查用户名、密码、手机号、邮箱是否合法。前期先用函数写,后面进入 FastAPI 后可以交给 Pydantic 模型做统一校验。

高质量函数的 4 个原则

原则说明
单一职责一个函数只做一件事
命名清晰函数名要体现行为和目标
输入输出明确参数和返回值要清楚,尽量加类型注解
少副作用不要在函数里偷偷修改外部状态

比如文件读取函数可以这样写:

from pathlib import Path

def read_text_file(file_path: Path) -> str:
    return file_path.read_text(encoding="utf-8")

它只负责读取文件,不负责打印、不负责解析、不负责写数据库。这样后续要测试、复用、替换都更简单。

工具层 utils 怎么设计?

当通用函数越来越多时,不建议全部放在 main.py

可以建立工具包:

project/
  main.py
  utils/
    __init__.py
    string_utils.py
    file_utils.py
    validators.py

示例:

# utils/string_utils.py
def normalize_text(text: str) -> str:
    return " ".join(text.strip().split()).lower()
# utils/__init__.py
from .string_utils import normalize_text

__all__ = ["normalize_text"]

业务代码中就可以这样导入:

from utils import normalize_text

print(normalize_text("  Hello  Python "))

这里的关键是:utils 只放通用工具函数,不放具体业务逻辑。比如“解析 PDF”可以是通用能力,“生成某个用户的面试报告”就更偏业务逻辑,不适合随便放进工具层。

九、文件处理:智能体项目必须掌握的基础能力

智能体项目经常需要处理文件:

  • 用户上传简历 PDF;
  • 上传 Word 学习资料;
  • 上传截图或扫描件;
  • 保存聊天记录;
  • 保存模型输出结果;
  • 记录日志;
  • 生成报告。

所以文件读写是 Python 基础里非常关键的一块。

1. 文件操作的底层逻辑

文件操作可以理解成 3 步:

打开文件 -> 读写文件 -> 关闭文件

如果文件打开后没有关闭,就可能造成资源泄露。

不推荐:

file = open("data.txt", "r", encoding="utf-8")
content = file.read()
file.close()

更推荐使用 with

with open("data.txt", "r", encoding="utf-8") as file:
    content = file.read()

with 会在代码块执行完后自动关闭文件。

2. open 的常见模式

模式含义注意事项
r只读文件不存在会报错
w覆盖写文件存在会清空原内容
a追加写在文件末尾追加
rb二进制读读取图片、音频等
wb二进制写写入二进制文件

尤其注意 w 模式,它会直接清空原文件。

3. 优先使用 pathlib

现代 Python 项目里,推荐优先使用 pathlib 处理路径。

from pathlib import Path

file_path = Path(__file__).parent / "data.txt"
content = file_path.read_text(encoding="utf-8")

print(content)

相比字符串拼接路径,Path 对象更安全,也更容易跨平台。

错误示例:

file_path = "data" + "/" + "a.txt"

推荐示例:

file_path = Path("data") / "a.txt"

4. 写入文件

覆盖写:

from pathlib import Path

file_path = Path("output.txt")
file_path.write_text("hello\n", encoding="utf-8")

追加写:

with file_path.open("a", encoding="utf-8") as file:
    file.write("new line\n")

写文件时要自己加换行符 \n,否则多次写入可能会挤在同一行。

Path(_file_).parent / “data.txt” : 当前脚本文件所在目录
Path(“output.txt”) : 运行命令时所在目录

5. 修改文件

文本文件的“修改”通常不是直接改磁盘中的某几个字,而是:

读取原内容 -> 在内存中替换 -> 覆盖写回
from pathlib import Path

def replace_text(file_path: Path, old: str, new: str) -> None:
    file = Path(file_path)
    content = file.read_text(encoding="utf-8")
    content = content.replace(old, new)
    file_path.write_text(content, encoding="utf-8")

6. 删除文件

删除文件使用 unlink()

from pathlib import Path

def delete_file(file_path: Path) -> None:
    file = Path(file_path)
    if file.exists() and file.is_file():
        file.unlink()

注意:unlink() 删除后不会进入回收站,真实项目里要谨慎使用。

7. 完整跟做:封装一个文件工具脚本

请新建文件:

04_file_tools.py

写入下面的完整代码:

from pathlib import Path


def read_file(file_path: Path) -> str:
    """读取文本文件内容。"""
    if not file_path.exists():
        return ""
    return file_path.read_text(encoding="utf-8")


def append_file(file_path: Path, content: str) -> None:
    """向文件末尾追加内容。"""
    with file_path.open("a", encoding="utf-8") as file:
        file.write(content)


def replace_file_text(file_path: Path, old: str, new: str) -> None:
    """替换文件中的文本,并在替换前生成备份文件。"""
    if not file_path.exists():
        raise FileNotFoundError(f"文件不存在:{file_path}")

    backup_path = file_path.with_suffix(file_path.suffix + ".bak")
    backup_path.write_text(file_path.read_text(encoding="utf-8"), encoding="utf-8")

    content = file_path.read_text(encoding="utf-8")
    file_path.write_text(content.replace(old, new), encoding="utf-8")


def delete_file(file_path: Path) -> None:
    """删除文件。真实项目中要谨慎调用。"""
    if file_path.exists() and file_path.is_file():
        file_path.unlink()


if __name__ == "__main__":
    current_dir = Path(__file__).resolve().parent #当前脚本文件所在文件夹的绝对路径
    data_path = current_dir / "demo_data.txt"

    data_path.write_text("Python is useful.\n", encoding="utf-8")
    print("第一次读取:")
    print(read_file(data_path))

    append_file(data_path, "Agent needs Python.\n")
    print("追加后读取:")
    print(read_file(data_path))

    replace_file_text(data_path, "Python", "Python programming")
    print("替换后读取:")
    print(read_file(data_path))

    print(f"原文件路径:{data_path}")
    print(f"备份文件路径:{data_path.with_suffix(data_path.suffix + '.bak')}")

运行:

python 04_file_tools.py

运行后,当前目录会出现两个文件:

demo_data.txt
demo_data.txt.bak

逐块解释:

current_dir = Path(__file__).resolve().parent

这行用于获取当前脚本所在目录。这样无论你从哪里运行脚本,它都能找到和脚本同级的文件。

data_path = current_dir / "demo_data.txt"

这行用 / 拼接路径,比字符串拼接更安全。

data_path.write_text("Python is useful.\n", encoding="utf-8")

这行创建并写入文件。如果文件已经存在,会覆盖原内容,所以真实业务里要谨慎使用。

append_file(data_path, "Agent needs Python.\n")

这行调用我们封装好的追加函数,把新内容写到文件末尾。

replace_file_text(data_path, "Python", "Python programming")

这行会把文件里的 Python 替换成 Python programming。替换前会先创建 .bak 备份文件,防止误改后无法恢复。

为什么 delete_file() 没有在主流程里调用?因为删除操作不可逆。对于新手教程,默认不执行删除更稳妥。你确认理解后,可以手动加一行测试:

delete_file(data_path)

但请只在练习文件上测试,不要对真实资料执行删除。

十、进阶语法:让代码更适合工程化

1. 类型注解

类型注解不是强制类型检查,而是给人和编辑器看的“类型说明”。

def add(a: int, b: int) -> int:
    return a + b

容器类型也可以标注:

names: list[str] = ["Ada", "Bob"]
scores: dict[str, int] = {"Ada": 90}
phone: str | None = None

类型注解的价值:

  • 提升可读性;
  • 让编辑器提供更准确的提示;
  • 减少团队协作时的理解成本;
  • 为 Pydantic 参数校验打基础。

2. 推导式

普通写法:

nums = []
for x in range(10):
    if x % 2 == 0:
        nums.append(x * 2)

推导式写法:

nums = [x * 2 for x in range(10) if x % 2 == 0]

推导式适合简单的数据转换。如果逻辑过于复杂,还是应该写成普通循环,否则可读性会下降。

3. 装饰器

装饰器可以在不修改原函数代码的前提下,给函数增加额外能力。
核心逻辑:函数套函数(装饰器 --> 包装函数 --> 调用原函数)

例如给函数加日志:

from functools import wraps  # 从 functools 模块导入 wraps,用来保留被装饰函数的原始信息,比如函数名、注释等

def log_call(func):  # 定义一个装饰器函数,参数 func 表示将来要被增强的原函数
    @wraps(func)  # 用 wraps 包装 wrapper,避免原函数信息被 wrapper 覆盖
    def wrapper(*args, **kwargs):  # 定义内部包装函数,*args 接收位置参数,**kwargs 接收关键字参数
        print(f"[start] {func.__name__}")  # 在原函数执行前打印开始日志,func.__name__ 是原函数名
        result = func(*args, **kwargs)  # 调用原函数,并把传入 wrapper 的参数原样转交给原函数
        print(f"[end] {func.__name__}")  # 在原函数执行后打印结束日志
        return result  # 返回原函数的执行结果,保证装饰后函数的返回值不变
    return wrapper  # 返回包装函数,后续真正被调用的是 wrapper

@log_call  # 使用装饰器,等价于 parse_pdf = log_call(parse_pdf)
def parse_pdf(file_path: str) -> str:  # 定义一个解析 PDF 的函数,参数 file_path 是字符串,返回值也是字符串
    return "pdf content"  # 模拟解析 PDF 后返回文本内容

parse_pdf("demo.pdf")  # 调用被装饰后的 parse_pdf,实际会先进入 wrapper,再由 wrapper 调用原函数

执行顺序:

  • @log_call 装饰 parse_pdf
  • 调用 parse_pdf(“demo.pdf”)
  • 进入 wrapper
  • 打印 [start] parse_pdf
  • 执行原来的 parse_pdf
  • 打印 [end] parse_pdf
  • 返回 “pdf content”

在后端和智能体项目里,装饰器常用于:

  • 日志;
  • 计时;
  • 权限校验;
  • 缓存;
  • 重试;
  • 统一异常处理。

4. 面向对象 OOP

当数据和行为需要绑定在一起时,可以使用类。

class Student:
    def __init__(self, name: str, score: float = 60):
        self.name = name
        self.score = score

    def update_score(self, new_score: float) -> None:
        if not 0 <= new_score <= 100:
            raise ValueError("score must be between 0 and 100")
        self.score = new_score

student = Student("Ada")
student.update_score(95)

在本文的统一文件解析器里,PDF、Word、图片解析器都适合用类封装,因为它们拥有相同的行为:接收文件路径,返回解析文本。

5. Pydantic 参数校验

类型注解只提示,不强制校验。Pydantic 可以在运行时校验数据。

安装:

pip install pydantic

示例:

from pydantic import BaseModel, Field, ValidationError

class Query(BaseModel):
    text: str = Field(min_length=1)
    top_k: int = Field(default=3, ge=1, le=20)

try:
    query = Query(text="Python Agent", top_k=5)
    print(query)
except ValidationError as exc:
    print(exc)

Pydantic 在 FastAPI 里非常常见。后面写接口时,请求体、查询参数、响应结构都可以用 Pydantic 模型约束。

十一、实战项目:统一文件解析器

这一节是本文最接近智能体项目的部分。

需求是:

不管用户上传的是 PDF、Word 还是图片,都通过同一个函数解析出文本。

理想调用方式:

from src import parse_file

content = parse_file("assets/sample.pdf")
content = parse_file("assets/sample.docx")
content = parse_file("assets/sample.png")

用户不需要关心文件类型,也不需要关心底层用了哪个库。

1. 项目结构

先在 agent_python_start/ 目录下创建一个新项目:

easy_parser/
  assets/
    sample.pdf
    sample.docx
    sample.png
  outputs/
  src/
    __init__.py
    core.py
    parsers/
      __init__.py
      base.py
      pdf_parser.py
      docx_parser.py
      image_parser.py
  main.py
  requirements.txt

如果你不熟悉命令行,可以直接在编辑器里手动创建这些文件夹和文件。对新手来说,先把结构建对,比纠结命令更重要。

每个目录的职责:

路径职责
assets/存放待解析文件
outputs/保存解析结果
src/core.py统一调度中心
src/parsers/base.py定义解析器抽象协议
src/parsers/pdf_parser.py解析 PDF
src/parsers/docx_parser.py解析 Word
src/parsers/image_parser.pyOCR 解析图片
main.py扫描文件、调用解析器、保存结果

2. 第一步:编写 requirements.txt

easy_parser/requirements.txt 中写入:

PyMuPDF>=1.23.0
python-docx>=0.8.11
paddlepaddle>=2.6.0
paddleocr>=2.7.0
Pillow>=10.0.0

然后在 easy_parser/ 目录下执行:

pip install -r requirements.txt

如果网络慢,可以使用镜像源:

pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/

每个依赖的作用:

依赖作用
PyMuPDF读取 PDF 文件内容,代码中通过 import fitz 使用
python-docx读取 Word .docx 文件中的段落和表格
paddlepaddlePaddleOCR 运行所需的底层框架
paddleocr对图片做 OCR 文字识别
Pillow图片处理基础库,很多 OCR 场景会间接用到

3. 第二步:定义抽象基类 base.py

easy_parser/src/parsers/base.py 中写入:

from abc import ABC, abstractmethod
from pathlib import Path


class BaseParser(ABC):
    @abstractmethod
    def parse(self, file_path: Path) -> str:
        pass

这段代码的意义是建立统一协议:

  • PDF 解析器必须有 parse()
  • Word 解析器必须有 parse()
  • 图片解析器也必须有 parse()
  • 调度器只需要面向 BaseParser 的协议编程。

这样后面新增 TxtParserExcelParserAudioParser 时,也能保持同一套结构。

逐行解释:

from abc import ABC, abstractmethod

abc 是 Python 标准库,专门用来定义抽象类。抽象类可以理解成“接口规范”。

class BaseParser(ABC):

这表示 BaseParser 是一个抽象基类,不是给你直接使用的,而是给其他解析器继承的。

@abstractmethod
def parse(self, file_path: Path) -> str:

@abstractmethod 表示子类必须实现这个方法。也就是说,只要某个类继承了 BaseParser,它就必须写自己的 parse()

这样做的好处是:不同文件类型虽然解析细节不同,但对外都统一成一个动作:传入文件路径,返回文本内容。

4. 第三步:编写 PDF 解析器

easy_parser/src/parsers/pdf_parser.py 中写入:

import fitz
from pathlib import Path
from .base import BaseParser


class PdfParser(BaseParser):
    def parse(self, file_path: Path) -> str:
        text_content = []

        with fitz.open(file_path) as doc:
            for page_num, page in enumerate(doc, start=1):
                text = page.get_text()
                text_content.append(f"--- Page {page_num} ---\n{text}")

        return "\n".join(text_content)

这段代码的运行逻辑:

  1. fitz.open(file_path) 打开 PDF 文件;
  2. for page_num, page in enumerate(doc, start=1) 一页一页遍历;
  3. page.get_text() 提取当前页文本;
  4. text_content.append(...) 把每一页文本放进列表;
  5. "\n".join(text_content) 把所有页的文本合并成一个大字符串。

这里用 with fitz.open() 是为了确保 PDF 文件资源能被正确关闭。打开 PDF 和打开普通文本文件一样,都属于占用系统资源的操作,处理完后要释放。

为什么要加 --- Page 1 --- 这种页码标记?因为真实文档可能有很多页,后续你把解析结果交给大模型时,保留页码有助于定位内容来源。

5. 第四步:编写 Word 解析器

easy_parser/src/parsers/docx_parser.py 中写入:

import docx
from pathlib import Path
from .base import BaseParser


class DocxParser(BaseParser):
    def parse(self, file_path: Path) -> str:
        document = docx.Document(str(file_path))
        content = []

        for paragraph in document.paragraphs:
            if paragraph.text.strip():
                content.append(paragraph.text)

        for table in document.tables:
            for row in table.rows:
                row_text = [
                    cell.text.strip()
                    for cell in row.cells
                    if cell.text.strip()
                ]
                if row_text:
                    content.append(" | ".join(row_text))

        return "\n".join(content)

这段代码分两部分读取 Word:

第一部分读取段落:

for paragraph in document.paragraphs:
    if paragraph.text.strip():
        content.append(paragraph.text)

paragraph.text.strip() 的作用是去掉空白。如果某个段落是空行,就不加入结果。

第二部分读取表格:

for table in document.tables:
    for row in table.rows:
        row_text = [
            cell.text.strip()
            for cell in row.cells
            if cell.text.strip()
        ]

很多 Word 文档里不只有段落,还有表格。如果只读取段落,会漏掉表格内容。所以这里额外遍历 document.tables,把每一行的单元格内容用 | 拼起来。

例如 Word 表格中一行是:

姓名 | 年龄 | 技能

解析后也会尽量保留类似结构,方便后续阅读或交给大模型处理。

6. 第五步:编写图片解析器

easy_parser/src/parsers/image_parser.py 中写入:

from paddleocr import PaddleOCR
from pathlib import Path
from .base import BaseParser


class ImageParser(BaseParser):
    def __init__(self):
        self.ocr = PaddleOCR(
            use_angle_cls=True,
            lang="ch",
            ocr_version="PP-OCRv3",
            enable_mkldnn=False,
        )

    def parse(self, file_path: Path) -> str:
        result = self.ocr.ocr(str(file_path))
        text_content = []

        if result:
            for page_result in result:
                if not page_result:
                    continue

                for line in page_result:
                    if isinstance(line, list) and len(line) >= 2:
                        text_info = line[1]
                        if isinstance(text_info, (list, tuple)) and text_info:
                            text_content.append(str(text_info[0]))

        return "\n".join(text_content)

这里有几个参数需要解释:

use_angle_cls=True

表示开启文字方向分类。比如图片里的文字有一点倾斜,OCR 会尝试自动识别方向。

lang="ch"

表示主要识别中文,也能兼容一部分英文。

ocr_version="PP-OCRv3"

表示使用指定版本的 OCR 模型。

enable_mkldnn=False

在部分 Windows CPU 环境下,关闭 mkldnn 可以减少兼容性问题。

为什么 ImageParser 要写 __init__()?因为 OCR 模型初始化比较慢。如果每解析一张图片都重新初始化一次,程序会很慢。把 OCR 对象放到 __init__() 中,只在创建解析器时初始化一次,后续可以复用。

真实项目里,OCR 返回结构可能因为版本差异有所不同,所以这里对 result 做了多层判断,避免某些图片识别失败时程序直接崩掉。

7. 第六步:导出 parsers 包

easy_parser/src/parsers/__init__.py 中写入:

from .pdf_parser import PdfParser
from .docx_parser import DocxParser
from .image_parser import ImageParser

__all__ = ["PdfParser", "DocxParser", "ImageParser"]

这个文件的作用是统一导出解析器类。这样在 core.py 里可以写:

from .parsers import PdfParser, DocxParser, ImageParser

而不用分别写三个导入语句。

8. 第七步:编写核心调度器 core.py

easy_parser/src/core.py 中写入:

from pathlib import Path
from .parsers import PdfParser, DocxParser, ImageParser


class UnifiedParser:
    def __init__(self):
        self.pdf_parser = PdfParser()
        self.docx_parser = DocxParser()
        self.image_parser = ImageParser()

        self._parser_map = {
            ".pdf": self.pdf_parser,
            ".docx": self.docx_parser,
            ".jpg": self.image_parser,
            ".jpeg": self.image_parser,
            ".png": self.image_parser,
            ".bmp": self.image_parser,
        }

    def parse(self, file_path: str | Path) -> str:
        path = Path(file_path).resolve()

        if not path.exists():
            return f"Error: File not found: {path}"

        parser = self._parser_map.get(path.suffix.lower())
        if parser is None:
            return f"Error: Unsupported file format: {path.suffix}"

        return parser.parse(path)

_parser_instance = UnifiedParser()

def parse_file(file_path: str | Path) -> str:
    return _parser_instance.parse(file_path)

这里有几个工程化亮点:

设计作用
字典映射用后缀匹配解析器,避免大量 if/elif
统一入口外部只调用 parse_file()
单例实例避免每次调用都重复初始化 OCR
Path.resolve()统一路径处理
返回字符串对上层调用者保持简单

再拆开看核心逻辑:

self._parser_map = {
    ".pdf": self.pdf_parser,
    ".docx": self.docx_parser,
    ".jpg": self.image_parser,
    ".jpeg": self.image_parser,
    ".png": self.image_parser,
    ".bmp": self.image_parser,
}

这就是“策略映射”。程序不需要写一堆 if file.endswith(".pdf"),而是直接通过后缀找到对应解析器。

parser = self._parser_map.get(path.suffix.lower())

path.suffix 取文件后缀,.lower() 转成小写,避免 .PDF.Pdf 这种大小写问题。

return parser.parse(path)

调度器并不关心 PDF、Word、图片具体怎么解析。它只负责把任务转交给对应解析器。

这就是工程化里很重要的思想:调度层负责分发,具体实现层负责干活。

9. 第八步:导出统一入口 src/init.py

easy_parser/src/__init__.py 中写入:

from .core import parse_file

__all__ = ["parse_file"]

这样外部就可以直接写:

from src import parse_file

而不用知道 parse_file 实际定义在 src/core.py 里。

这也是很多 Python 包常用的写法:内部结构可以复杂,但对外暴露的入口要简单。

10. 第九步:编写主程序 main.py

easy_parser/main.py 中写入:

from pathlib import Path
from src import parse_file

project_root = Path(__file__).resolve().parent
input_dir = project_root / "assets"
output_dir = project_root / "outputs"
output_dir.mkdir(parents=True, exist_ok=True)

files = [file for file in input_dir.iterdir() if file.is_file()]

for file_path in files:
    content = parse_file(file_path)
    output_path = output_dir / f"{file_path.name}_result.txt"
    output_path.write_text(content, encoding="utf-8")
    print(f"saved: {output_path}")

运行前,请把测试文件放入:

easy_parser/assets/

例如:

easy_parser/assets/sample.pdf
easy_parser/assets/sample.docx
easy_parser/assets/sample.png

然后在 easy_parser/ 目录执行:

python main.py

运行后,解析结果会保存到:

easy_parser/outputs/

这段代码体现了一个完整的批处理流程:

扫描输入目录 -> 逐个解析文件 -> 保存结果 -> 打印进度

这就是智能体项目里文件上传、资料入库、知识库构建的基础形态。

主程序也拆开解释一下:

project_root = Path(__file__).resolve().parent

获取当前项目根目录,也就是 main.py 所在目录。

input_dir = project_root / "assets"
output_dir = project_root / "outputs"

定义输入目录和输出目录。assets 放原始文件,outputs 放解析后的文本结果。

output_dir.mkdir(parents=True, exist_ok=True)

如果 outputs 不存在,就自动创建。exist_ok=True 表示目录已经存在时不要报错。

files = [file for file in input_dir.iterdir() if file.is_file()]

遍历 assets 目录,只保留文件,排除子目录。

content = parse_file(file_path)

调用统一入口。这里主程序完全不关心文件是 PDF、Word 还是图片。

output_path = output_dir / f"{file_path.name}_result.txt"

生成输出文件路径。例如:

sample.pdf -> sample.pdf_result.txt

保留原始后缀的好处是避免同名文件互相覆盖。例如 sample.pdfsample.docx 都会有独立结果文件。

十二、如何给解析器新增一种文件格式?

假设我们想支持 .txt 文件。

第一步,新建 txt_parser.py

from pathlib import Path
from .base import BaseParser

class TxtParser(BaseParser):
    def parse(self, file_path: Path) -> str:
        return file_path.read_text(encoding="utf-8")

第二步,在 parsers/__init__.py 中导出:

from .pdf_parser import PdfParser
from .docx_parser import DocxParser
from .image_parser import ImageParser
from .txt_parser import TxtParser

第三步,在 core.py 中注册:

self.txt_parser = TxtParser()

self._parser_map = {
    ".pdf": self.pdf_parser,
    ".docx": self.docx_parser,
    ".png": self.image_parser,
    ".txt": self.txt_parser,
}

这样外部调用方式不变:

content = parse_file("assets/readme.txt")

这就是模块化设计的价值:新增能力时,不需要推翻原来的调用方式。

十三、常见坑和排查方式

1. 虚拟环境没有激活

现象:

ModuleNotFoundError: No module named 'fitz'

排查:

where python
pip list

确认当前终端使用的是 .venv 里的 Python。

2. fitz 安装错误

PyMuPDF 的导入名是 fitz,但安装包名是 PyMuPDF

正确安装:

pip install PyMuPDF

代码中导入:

import fitz

3. 中文乱码

读写文本时尽量明确指定编码:

content = file_path.read_text(encoding="utf-8")
file_path.write_text(content, encoding="utf-8")

不要依赖系统默认编码。

4. 相对路径找不到文件

错误写法:

file_path = Path("assets/sample.pdf")

如果从不同目录运行脚本,路径可能失效。

更稳的写法:

project_root = Path(__file__).resolve().parent
file_path = project_root / "assets" / "sample.pdf"

5. w 模式误清空文件

下面代码会清空原文件:

open("data.txt", "w", encoding="utf-8")

如果只是追加日志,应使用:

open("data.txt", "a", encoding="utf-8")

6. OCR 初始化慢

PaddleOCR 第一次运行可能会下载模型,初始化时间较长。这不是代码卡死,通常等待即可。

真实项目里,不建议每次解析图片都重新创建 OCR 实例。应该像本文示例一样,在解析器初始化时创建一次,然后复用。

十四、本文知识如何连接后续智能体项目?

第一篇看起来是 Python 基础,但它已经在为后面的项目做铺垫。

本文能力后续项目用途
虚拟环境和依赖管理FastAPI、LangChain、OCR 等依赖稳定运行
函数封装Prompt 构造、文件解析、接口逻辑复用
模块和包后端分层架构、服务拆分
文件处理简历、资料、图片、日志、报告处理
类型注解提升大型项目可读性
PydanticFastAPI 请求参数和响应结构校验
OOPAgent、Parser、Service、Repository 等对象建模
统一解析器多模态智能体的资料输入基础

换句话说,后面的智能体项目不是突然变复杂的,而是从这些基础能力逐步搭起来的。

十五、练习任务

如果你想真正掌握本文内容,建议完成下面几个练习。

练习 1:词频统计升级版

要求:

  • data.txt 读取文本;
  • 统一转小写;
  • 去掉常见标点;
  • 统计词频;
  • 按出现次数从高到低输出。

提示:

from pathlib import Path

def count_words(file_path: Path) -> dict[str, int]:
    text = file_path.read_text(encoding="utf-8").lower()
    for char in ",.!?;:":
        text = text.replace(char, " ")

    counts = {}
    for word in text.split():
        counts[word] = counts.get(word, 0) + 1
    return counts

练习 2:封装文件工具函数

实现下面 4 个函数:

def read_file(file_path: Path) -> str:
    ...

def append_file(file_path: Path, content: str) -> None:
    ...

def replace_file_text(file_path: Path, old: str, new: str) -> None:
    ...

def delete_file(file_path: Path) -> None:
    ...

要求:

  • 全部使用 pathlib
  • 明确指定 utf-8
  • 删除前判断文件是否存在;
  • 修改前自动备份。

练习 3:给统一解析器添加 TXT 支持

要求:

  • 新增 TxtParser
  • 继承 BaseParser
  • core.py 注册 .txt
  • 保持外部调用方式不变。

完成后应该可以这样调用:

content = parse_file("assets/demo.txt")

十六、总结

本文的重点不是“背 Python 语法”,而是建立智能体项目开发所需的 Python 基础能力。

你需要重点掌握:

  1. AI 可以写代码,但人必须能接管代码。
  2. Python 学习应围绕项目最小闭环:输入、处理、输出。
  3. 虚拟环境和 requirements.txt 是保证项目可复现的基础。
  4. 函数封装和工具层设计决定代码是否好维护。
  5. 文件处理是智能体处理资料、日志、知识库的前置能力。
  6. 类型注解、装饰器、OOP、Pydantic 是工程化代码的重要基础。
  7. 统一文件解析器展示了从基础语法走向小型工程模块的完整过程。

如果你已经能看懂并改造本文中的统一文件解析器,那么后续学习 FastAPI、数据库建模、LangChain 和多智能体协作时,就不会只是“跟着敲”,而是能理解每一层代码为什么这么设计。

更多推荐