简介:这份源码示例面向需要在 Delphi 桌面应用中集成中文分词能力的开发者,重点演示如何借助 PythonForDelphi 组件调用 Python 脚本与结巴分词库,在 Pascal 工程里完成中文文本切分,适合具备一定 Delphi 基础、希望快速打通两种语言协作链路的中级开发者参考。压缩包共 78 个文件,约 13.59MB,以 pyc、py 等 Python 模块与结巴分词词典数据为主,另含 dll 运行时库、pas 与 dfm 等 Delphi 源码和界面文件,以及 dproj、dpr、conf 等项目配置与可执行文件,完整覆盖从工程构建到运行依赖的各个环节。已有 542 人学习下载。示例将分词调用封装进 Delphi 表单,读者可据此理解 Python 脚本加载、分词模式选择与结果回传的实现思路,并参考项目文件组织方式排查 DLL 依赖与配置问题,为自建中文文本处理工具提供可直接复用的骨架。
1. 一个老 Delphi 项目要接中文分词,为什么我最后选了结巴
前阵子接手一个维护了快十年的桌面端项目,界面全是 Delphi 写的,业务逻辑也跑在客户端里,偏偏新需求要求对用户输入的中文做关键词提取和模糊匹配。团队里没人愿意动老代码,更不想为了一个分词功能把整个工程迁到别的技术栈上。翻了一圈方案,最后落地的路子是:Delphi 负责界面和业务,Python 侧用结巴分词处理文本,两边通过进程调用和标准输入输出对接。这份源码就是那套思路的最小可运行例子,把「Delphi 怎么调 Python」「结巴怎么初始化」「中文结果怎么回传」三件事拆开讲清楚了。它适合两类人:手里有 Delphi 存量项目、又想用现成分词能力的人;以及想搞清楚跨语言调用边界怎么划的开发者。分词本身不玄学,真正容易翻车的是编码和进程通信,这个例子正好把这两块摆到了台面上。
2. 项目结构与技术选型:Delphi 壳 + Python 分词内核
2.1 为什么是「进程调用」而不是 DLL 嵌入
先把这个例子的架构说透。它没有把 Python 解释器嵌进 Delphi 进程,也没有编译成 DLL 让 Delphi 直接加载,而是走的最朴素的路线:Delphi 用CreateProcess起一个 Python 脚本,把待分词的文本通过标准输入或者临时文件传过去,Python 侧跑完结巴,把结果按约定格式写回标准输出,Delphi 再读回来解析。这个选择看着土,但对存量项目最友好。
原因很实际。把 Python 嵌进 Delphi 进程,要么依赖 Python4Delphi 这类桥接组件,要么自己封装 C API,版本一升级、位数一对不上就是一堆链接错误,血泪经验告诉我们这种耦合在老项目里就是定时炸弹。而进程调用把两边彻底隔开:Python 环境坏了不影响 Delphi 启动,分词脚本改了不用重新编译 Delphi,调试时还能单独在命令行跑脚本验证。代价是每次调用有一次进程启动开销,几十毫秒级别,对桌面端的关键词提取这种低频操作完全能接受。
常见做法是把 Python 脚本和 Delphi 可执行文件放在同一目录下的py/子目录里,运行时用相对路径定位,避免写死绝对路径导致换台机器就跑不起来。
2.2 目录结构长什么样
这个例子的文件不多,但职责划分清楚,照着这个骨架扩就行:
project/ ├── DelphiApp/ │ ├── MainForm.pas // 主窗体,输入框 + 按钮 + 结果框 │ ├── MainForm.dfm // 窗体布局 │ └── SegmentClient.pas // 封装进程调用与结果解析 ├── py/ │ ├── segment.py // 结巴分词入口,读 stdin 写 stdout │ └── requirements.txt // jieba 依赖 └── README.mdSegmentClient.pas是核心,它把「起进程、写输入、读输出、关进程」四步封成一个函数,主窗体只管调它。segment.py则保持极简,不做任何业务判断,只负责分词和格式化输出。这种「Delphi 管业务、Python 管算法」的分法,后面要换分词库或者加词性标注,只动 Python 侧就行。
2.3 技术栈与依赖版本
Delphi 侧没有第三方依赖,用的是 RTL 自带的System.SysUtils、Winapi.Windows、Winapi.ShellAPI这些单元,所以老版本 Delphi(比如 7 之后的 Unicode 版本)基本都能编。Python 侧只依赖jieba,装法就一行:
pip install jieba注意 Python 要用 3.x,脚本里统一按 UTF-8 处理文本。Delphi 如果是较老的版本,字符串默认是 ANSI,跨进程传中文时编码转换是必踩的坑,后面避坑章节会专门讲。
3. 核心代码拆解:从 Delphi 发起调用到结巴返回结果
3.1 Python 侧:一个只认标准输入输出的分词脚本
先看 Python 这头,它被设计成「无状态、可独立测试」的形态。核心逻辑就是读一行、分一行、写一行:
# segment.py import sys import jieba def main(): # 强制标准输入输出走 UTF-8,避免 Windows 控制台默认 GBK 导致乱码 sys.stdin.reconfigure(encoding='utf-8') sys.stdout.reconfigure(encoding='utf-8') # 加载自定义词典(可选),路径相对脚本所在目录 try: jieba.load_userdict('userdict.txt') except FileNotFoundError: pass # 没有自定义词典也能跑,不阻断主流程 for line in sys.stdin: text = line.strip() if not text: continue # 精确模式分词,返回 list words = jieba.lcut(text) # 用空格拼接,Delphi 侧按空格切分即可 sys.stdout.write(' '.join(words) + '\n') sys.stdout.flush() # 关键:不 flush 的话 Delphi 读不到数据 if __name__ == '__main__': main()逐段说下逻辑。reconfigure这两行是给 Windows 兜底的,因为默认控制台编码经常是 GBK,不显式指定的话中文进去就成乱码。load_userdict用try/except包住,是因为自定义词典属于增强项,缺了不该让整个调用失败,这个容错思路在跨进程场景里很重要。主循环按行读,意味着 Delphi 可以一次传多行、批量处理,也可以一行一调,灵活度高。lcut是结巴的精确模式,返回列表;如果业务要全模式或者搜索引擎模式,换成jieba.cut(text, cut_all=True)或jieba.cut_for_search(text)即可,接口形状一致。
最后那个flush()是重点。标准输出默认带缓冲,Python 写完不刷,Delphi 那头ReadFile就会一直阻塞等数据,表现出来就是「点了按钮界面卡死」。这个坑我在别的项目里踩过,排查了半天才发现是缓冲问题。
3.2 Delphi 侧:封装一个可复用的分词客户端
Delphi 这头的关键是把进程调用封装干净。下面这段是SegmentClient.pas的核心,用管道重定向标准输入输出:
// SegmentClient.pas(节选) function TSegmentClient.Segment(const AText: string): string; var SA: TSecurityAttributes; ReadPipe, WritePipe: THandle; StartupInfo: TStartupInfo; ProcessInfo: TProcessInformation; BytesWritten, BytesRead: DWORD; Buffer: array[0..4095] of AnsiChar; CmdLine: string; InputUtf8: UTF8String; begin Result := ''; // 1. 创建匿名管道,读端给 Delphi,写端给子进程 SA.nLength := SizeOf(SA); SA.bInheritHandle := True; SA.lpSecurityDescriptor := nil; CreatePipe(ReadPipe, WritePipe, @SA, 0); SetHandleInformation(ReadPipe, HANDLE_FLAG_INHERIT, 0); // 2. 组装命令行,脚本路径用相对路径 CmdLine := 'python "' + ExtractFilePath(ParamStr(0)) + 'py\segment.py"'; // 3. 启动进程,重定向 stdin/stdout ZeroMemory(@StartupInfo, SizeOf(StartupInfo)); StartupInfo.cb := SizeOf(StartupInfo); StartupInfo.dwFlags := STARTF_USESTDHANDLES; StartupInfo.hStdInput := WritePipe; StartupInfo.hStdOutput := WritePipe; StartupInfo.hStdError := WritePipe; if not CreateProcess(nil, PChar(CmdLine), nil, nil, True, CREATE_NO_WINDOW, nil, nil, StartupInfo, ProcessInfo) then Exit; // 4. 写入 UTF-8 文本,注意换行符 InputUtf8 := UTF8String(AText + sLineBreak); WriteFile(WritePipe, InputUtf8[1], Length(InputUtf8), BytesWritten, nil); CloseHandle(WritePipe); // 关闭写端,子进程才知道输入结束 // 5. 读回结果 while ReadFile(ReadPipe, Buffer, SizeOf(Buffer) - 1, BytesRead, nil) and (BytesRead > 0) do begin Buffer[BytesRead] := #0; Result := Result + string(UTF8ToString(Buffer)); end; // 6. 收尾 CloseHandle(ReadPipe); WaitForSingleObject(ProcessInfo.hProcess, 5000); CloseHandle(ProcessInfo.hProcess); CloseHandle(ProcessInfo.hThread); end;参数和步骤说明一下。CreatePipe建的是匿名管道,bInheritHandle := True让子进程能继承句柄,但读端要SetHandleInformation去掉继承,否则句柄泄漏。CREATE_NO_WINDOW防止弹黑框,桌面端体验必需。第 4 步写完立刻CloseHandle(WritePipe)很关键,不关的话 Python 的for line in sys.stdin永远等不到 EOF,进程挂住。第 5 步按块读,UTF8ToString做解码,保证中文正确。最后WaitForSingleObject给 5 秒超时,防止异常情况下界面无限等待。
3.3 结果解析与界面联动
拿到空格分隔的字符串后,主窗体里再切成数组展示:
procedure TMainForm.btnSegmentClick(Sender: TObject); var Client: TSegmentClient; Raw: string; Words: TArray<string>; begin Client := TSegmentClient.Create; try Raw := Client.Segment(edtInput.Text); Words := Raw.Trim.Split([' '], TStringSplitOptions.ExcludeEmpty); mmoResult.Lines.Clear; mmoResult.Lines.Add('共 ' + IntToStr(Length(Words)) + ' 个词'); for var W in Words do mmoResult.Lines.Add(W); finally Client.Free; end; end;Split用空格切,ExcludeEmpty过滤掉连续空格产生的空串。这里把「调用」和「展示」分开,是为了以后换成分词结果直接喂给搜索框或者高亮逻辑时,不用改客户端封装。如果业务要词性,Python 侧换成jieba.posseg,输出格式改成词/词性,Delphi 侧解析规则跟着调一下就行,架构不用动。
4. 跑起来与避坑:编码、路径、进程三个高频翻车点
4.1 从零跑通的完整步骤
先把环境铺好,再验证 Python 侧单独能跑,最后联调 Delphi,这个顺序能省掉大量排查时间。
第一步,确认 Python 可用并装依赖:
python --version pip install jieba第二步,单独测脚本,这一步是黑匣子排查的基准:
echo "今天天气不错,适合写代码" | python py/segment.py正常应该输出空格分隔的词。如果这步就乱码或者报错,别急着开 Delphi,先把 Python 侧修好。
第三步,用 Delphi 打开工程编译,把py目录放到编译产物同级,点按钮测试。如果结果框空白,先看第 4.2 节的排查项。
4.2 常见问题排查:四条踩坑记录
现象一:结果框里全是问号或者方块。原因基本是编码没对齐,Delphi 老版本字符串是 ANSI,写进管道时按本地代码页编码,Python 按 UTF-8 读就错位。解决办法是 Delphi 侧写入前显式转UTF8String,Python 侧reconfigure成 UTF-8,两头都锁死 UTF-8,别依赖系统默认。
现象二:点按钮后界面卡死,几秒后才恢复或者一直不恢复。前面提过,多半是 Python 没flush,或者 Delphi 没关写端导致子进程等 EOF。检查segment.py里有没有sys.stdout.flush(),以及SegmentClient里CloseHandle(WritePipe)是否在读取之前执行。
现象三:换台机器就提示找不到脚本。原因是命令行里写死了绝对路径。统一改成基于ParamStr(0)的相对路径,并且把py目录随安装包一起分发。另外注意路径里有空格时要加引号,CmdLine里已经处理了。
现象四:第一次调用正常,连续点几次后进程越积越多。这是句柄和进程没回收干净。检查CloseHandle是否覆盖了所有分支,尤其是CreateProcess失败提前Exit的那条路径,管道句柄也要关。稳妥做法是用try/finally把清理逻辑包起来,别靠人肉记。
提示:调试跨进程调用时,先把 Python 脚本在命令行跑通,再联调 Delphi。两边同时改,出了问题根本分不清是哪头的锅。
4.3 性能与批量处理的取舍
单次进程启动有固定开销,如果业务要处理成百上千条文本,一条一调就浪费了。这个例子的脚本按行读,天然支持批量:Delphi 侧把多条文本拼成多行一次写入,Python 逐行输出,Delphi 按行解析即可。改造点只在Segment函数,把单字符串入参换成TStringList,写入时逐行写,读取时按sLineBreak切。这样一次进程启动处理一批,开销摊薄,桌面端批量导入关键词的场景就很顺。
5. 二次开发:把例子改成能进生产的组件
5.1 三个优先改造的方向
这个例子是骨架,要进真实项目,我一般按优先级改三处。第一,把SegmentClient改成常驻进程模式:启动时拉起 Python,保持管道不关,后续调用复用同一个进程,彻底消掉启动开销,代价是要处理进程意外退出的重连逻辑。第二,Python 侧加自定义词典热加载,业务词库变了不用重启,监听文件修改时间即可。第三,输出格式从纯文本升级成 JSON,带上词、词性、偏移量,Delphi 侧用现成的 JSON 库解析,扩展性比空格分隔强得多。
改造时有个验证方法很实用:给SegmentClient写一组固定输入的单元测试,把「输入文本 → 期望分词结果」存成基线,每次改完跑一遍对比。跨进程的东西肉眼很难看出回归,靠基线对比最稳。
| 改造项 | 改动位置 | 收益 | 风险 |
|---|---|---|---|
| 常驻进程 | SegmentClient.pas | 去掉启动开销 | 需处理进程崩溃重连 |
| 词典热加载 | segment.py | 词库实时生效 | 文件监听有延迟 |
| JSON 输出 | 两端协议 | 字段可扩展 | 解析代码变多 |
5.2 一个容易忽略的细节:超时与降级
生产环境里 Python 进程可能因为环境问题起不来,这时候不能让 Delphi 界面一直等。我的习惯是给WaitForSingleObject设一个明确超时,超时后直接返回原文或者空结果,并在日志里记一笔,让业务侧有降级路径。分词失败不该拖垮整个功能,这个原则比任何优化都重要。
从那以后我每次做跨语言调用,都强制先把「单侧能独立跑通」和「失败有降级」这两条走一遍,再谈性能和扩展。希望帮到你。
本文还有配套的精品资源,点击获取