☰
智能体Skills:生产级能力单元的定义、构建与运行契约
2026/10/28 12:14:15 网站建设 项目流程

1. “agent-skills”不是插件,是智能体的肌肉记忆系统

你第一次在终端里敲下codex skills list,看到满屏滚动的web-search,file-read,git-diff,json-parse—— 这些不是传统意义的CLI工具,也不是npm包,更不是浏览器扩展。它们是智能体(Agent)在执行任务时调用的原子级能力单元,是把“我要查昨天GitHub上那个PR的变更”这种模糊指令,拆解成可调度、可验证、可审计、可组合的确定性动作的最小执行单位。

我去年带团队重构内部研发助手时,踩过最深的坑,就是把它当成“功能菜单”来设计。我们花两周做了个漂亮的Web界面,支持拖拽组合skills,结果上线第一天,用户反馈:“点完‘分析日志’按钮,它卡了47秒,然后报错说‘找不到log-parser-skill’——可我在界面上明明看到它亮着绿灯。” 后来翻日志才发现:那个skill依赖的Python库版本和主进程不兼容,但健康检查只校验了文件存在,没跑实际函数。这暴露了一个本质问题:skills不是静态资源,而是运行时动态加载、带上下文约束、有生命周期管理的执行契约。

关键词里反复出现的CLI,slash commands,API,其实指向同一套分层机制:

  • CLI层(如zcode cli run --skill web-search --query "LLM context window limits")是人类可读、可调试、可脚本化的入口;
  • Slash commands层(如/summarize /doc.pdf)是面向协作场景的语义快捷键,背后绑定的是skill ID + 参数schema;
  • API层(如POST /v1/skills/execute)是服务化封装,供其他Agent或前端调用,承担鉴权、限流、审计日志等职责。

而所有这些入口,最终都指向同一个核心:Skill Definition File—— 一个声明式YAML文件,它定义了这个skill能做什么(description)、需要什么输入(parameters)、输出什么结构(output_schema)、在什么环境里运行(runtime: python3.11 / node18 / docker:alpine)、依赖哪些外部服务(requires: [http, filesystem, git]),以及最关键的——失败时如何降级(fallback: { skill: "text-extract", params: { method: "regex" } })。

这不是“功能列表”,这是智能体的肌肉记忆训练手册。你教一个新人写SQL,不会只给他一张函数表,而是带他练“从订单表查出上周退货率最高的3个SKU”这种具体任务。skills同理:sql-queryskill的价值,不在它能连数据库,而在于它内置了防SQL注入的参数绑定、超时熔断、结果集截断、错误分类(语法错/权限错/锁表错)等生产级细节。你调用它,就默认继承了这些经验。

所以当你看到热搜词里混着deepseek api如何调用和agent skills测试,别被误导。前者是调用大模型的“原材料”,后者是调用经过工程化封装的“预制菜”。一个问“怎么把面粉揉成面团”,一个问“怎么用现成的饺子皮包三鲜馅”。本文要讲的,就是后者——如何让智能体真正“会做事”,而不是只会“瞎聊天”。

2. Skill不是代码,是带约束的执行契约:从定义到部署的完整链路

很多人以为写个skill就是写个Python函数,比如:

def web_search(query: str) -> List[Dict]: return requests.get(f"https://api.example.com/search?q={query}").json()

这离生产级skill差了至少五道关卡。真正的skill定义,是一份强制约束的执行契约,必须通过编译器校验、沙箱运行、安全扫描、性能压测四重门。下面以一个真实可用的github-pr-diffskill为例,拆解从定义到上线的全链路。

2.1 定义层:YAML契约的每一行都是生产红线

这是它的skill.yaml文件(已脱敏):

# github-pr-diff.skill.yaml id: github-pr-diff version: 1.3.2 name: GitHub Pull Request Diff Analyzer description: | Fetches and parses the unified diff of a GitHub PR, extracting changed files, line counts, and code language distribution. Handles large diffs via streaming. category: devops tags: [github, pr, diff, code-analysis] # === 执行契约核心 === input_schema: type: object required: [owner, repo, pr_number] properties: owner: type: string description: GitHub organization or username pattern: "^[a-zA-Z0-9][a-zA-Z0-9\\-]{1,38}$" # 严格校验GitHub用户名规则 repo: type: string description: Repository name pattern: "^[a-zA-Z0-9_\\-\\.]+$" pr_number: type: integer minimum: 1 maximum: 999999 max_files: type: integer default: 50 minimum: 1 maximum: 500 output_schema: type: object properties: pr_url: type: string format: uri changed_files: type: array items: type: object properties: filename: type: string additions: type: integer deletions: type: integer language: type: string diff_snippet: type: string maxLength: 2048 # 防止返回超长diff炸内存 summary: type: object properties: total_additions: { type: integer } total_deletions: { type: integer } languages: # 强制要求语言统计,避免空数组 type: object additionalProperties: { type: integer } # === 运行时契约 === runtime: type: python version: "3.11" dependencies: - requests>=2.31.0,<3.0.0 - PyYAML>=6.0.0,<7.0.0 # 指定最小资源,防止OOM resources: memory_mb: 256 cpu_millis: 1000 # 1秒CPU时间上限 # === 安全契约 === permissions: network: ["https://api.github.com"] filesystem: ["read-only"] # 只读,禁止写入 environment: ["GITHUB_TOKEN"] # 显式声明所需环境变量 # === 可靠性契约 === timeout_ms: 15000 # 15秒硬超时 retries: max_attempts: 2 backoff_factor: 2.0 fallback: skill: "text-extract" params: method: "regex" pattern: "diff --git.*?\\n(.*?)(?=diff --git|$)"

注意几个关键设计点:

  • 输入校验不是可选:pattern正则直接复用GitHub官方用户名规则,避免后续API调用因非法字符报400;max_files的maximum: 500是防爆策略——GitHub API单次请求最多返回300个文件,这里留200缓冲,超出直接拒绝,不进沙箱。
  • 输出结构强约束:languages字段明确要求additionalProperties: { type: integer },确保下游解析时不会遇到null或string类型,这是JSON Schema的硬性保障。
  • 资源声明即SLA:memory_mb: 256不是建议值,是沙箱启动时分配的cgroup内存上限,超限直接OOM kill,不给机会泄漏。
  • 网络白名单是安全基线:network: ["https://api.github.com"]意味着代码里哪怕写requests.get("http://evil.com"),也会在syscall层被eBPF拦截,返回ConnectionRefusedError。

提示:很多团队跳过这一步,直接写代码再补schema。后果是——当skill被集成到企业级Agent平台时,平台无法自动校验输入合法性,导致大量400错误涌入监控,而根源只是前端传了个带空格的repo名。契约前置,省去90%的线上排查。

2.2 构建层:Docker镜像不是打包,是执行环境的原子快照

定义完YAML,下一步不是pip install,而是构建一个不可变的、带签名的Docker镜像。我们用自研的skill-builder工具链(开源版见GitHubagent-skills/builder):

# 1. 校验YAML契约 skill-builder validate github-pr-diff.skill.yaml # 2. 生成构建上下文(含依赖解析、安全扫描) skill-builder build --yaml github-pr-diff.skill.yaml \ --output ./dist/github-pr-diff:v1.3.2 # 3. 推送至私有Registry(带SBOM清单) skill-builder push --image ./dist/github-pr-diff:v1.3.2 \ --registry https://reg.internal.company.com \ --sign-key team-devops-2024

这个过程干了什么?

  • 依赖解析:skill-builder会递归解析requirements.txt,并检查requests>=2.31.0,<3.0.0是否满足,同时检测是否有已知CVE的版本(如requests<2.31.0存在 CVE-2023-32681)。不满足则构建失败。
  • SBOM生成:输出sbom.spdx.json,记录所有依赖包、许可证、哈希值,供合规审计。
  • 镜像签名:用公司PKI体系的密钥对镜像manifest签名,运行时Agent平台会校验签名有效性,防止中间人篡改。

构建产物不是tar包,而是一个标准OCI镜像,标签为reg.internal.company.com/agent-skills/github-pr-diff:v1.3.2@sha256:abc123...。这个@sha256后缀才是真实身份,v1.3.2只是便于人类阅读的别名。

2.3 运行层:沙箱不是容器,是带硬件级隔离的执行单元

当Agent调度github-pr-diff时,平台不会直接docker run。而是启动一个轻量级虚拟机(Firecracker microVM),里面只运行这个skill的进程,且:

  • CPU被cgroups限制为0.5核,内存锁定256MB;
  • 网络通过eBPF程序过滤,只放行api.github.com:443;
  • 文件系统是只读挂载,临时目录/tmp是tmpfs内存盘,大小限制10MB;
  • 所有syscall被seccomp-bpf拦截,禁止execve,fork,ptrace等危险调用。

我们实测过:即使skill代码里恶意调用os.system("rm -rf /"),在沙箱内也只会返回Permission denied,因为根文件系统是只读的。这才是真正的“能力隔离”。

注意:很多团队用普通Docker容器跑skills,认为“有namespace就够了”。但Docker容器共享宿主机内核,一个恶意skill可通过unshare()逃逸。Firecracker微VM提供的是硬件级隔离,成本只比容器高15%(启动慢50ms,内存多30MB),但安全等级是质变。

2.4 部署层:注册中心不是数据库,是带版本路由的智能网关

最后,这个镜像要注册到平台的Skill Registry。Registry不是简单的key-value存储,而是一个支持语义化版本路由、灰度发布、AB测试的智能网关。注册命令:

skill-registry register \ --image reg.internal.company.com/agent-skills/github-pr-diff:v1.3.2@sha256:abc123... \ --alias github-pr-diff:stable \ --alias github-pr-diff:canary \ --traffic-split canary=5% \ --metadata '{"team":"devops","owner":"@alice"}'

这意味着:

  • 当Agent调用github-pr-diff时,平台默认路由到:stable别名;
  • 如果请求头带X-Skill-Env: canary,则5%流量打到新版本;
  • 所有调用都会记录skill_id,version,input_hash,duration_ms,status_code,用于自动发现劣化版本(如新版本平均耗时从800ms升到2500ms,自动切回stable)。

这才是完整的skill生命周期:定义即契约 → 构建即验证 → 运行即隔离 → 部署即路由。少一环,就不是production-ready的skills。

3. CLI不是交互壳,是技能调度的调试控制台:从本地开发到线上诊断

当你在终端输入zcode cli run --skill github-pr-diff --owner agent-skills --repo builder --pr-number 42,你以为只是在调用一个命令?不,你正在使用一个全链路可观测的调试控制台。CLI层的设计哲学是:让每一次调用都成为一次可复现、可审计、可回溯的实验。

3.1 本地开发:--dry-run模式下的契约预演

新手常犯的错误,是直接在线上环境试错。正确姿势是先用--dry-run模式,在本地完成契约校验:

zcode cli run \ --skill github-pr-diff \ --owner agent-skills \ --repo builder \ --pr-number 42 \ --dry-run \ --verbose

输出不是执行结果,而是契约执行计划:

[DRY RUN] Skill Execution Plan for github-pr-diff:v1.3.2 ├── Input Validation: PASSED (all fields meet schema) ├── Resource Check: │ ├── Memory: 256MB allocated (OK) │ └── Network: api.github.com:443 allowed (OK) ├── Dependency Check: │ ├── requests==2.31.0 (OK, no CVEs) │ └── PyYAML==6.0.1 (OK) ├── Security Scan: │ ├── No execve syscall detected (OK) │ └── No write to /etc/ (OK) └── Estimated Duration: < 1200ms (based on historical p95)

这个输出告诉你:输入合法、资源够用、依赖安全、行为合规、耗时可控。只有全部PASSED,才值得提交到CI。我们团队规定:--dry-run失败的PR,CI直接拒绝合并。

实操心得:--dry-run会模拟沙箱环境,但不真正发起网络请求。它通过静态分析+本地schema校验完成所有检查。因此,即使你没配GITHUB_TOKEN,也能跑通——因为token校验在运行时才做。

3.2 线上调试:--trace模式下的全链路追踪

当线上调用失败,别急着看日志。用--trace获取端到端追踪:

zcode cli run \ --skill github-pr-diff \ --owner agent-skills \ --repo builder \ --pr-number 42 \ --trace \ --output-format json

输出是一个标准OpenTelemetry JSON trace:

{ "service": "zcode-cli", "trace_id": "0123456789abcdef0123456789abcdef", "spans": [ { "name": "validate-input", "start_time": "2024-05-20T10:30:00.123Z", "end_time": "2024-05-20T10:30:00.125Z", "status": "OK" }, { "name": "sandbox-start", "start_time": "2024-05-20T10:30:00.126Z", "end_time": "2024-05-20T10:30:00.180Z", "status": "OK" }, { "name": "http-request", "start_time": "2024-05-20T10:30:00.181Z", "end_time": "2024-05-20T10:30:01.450Z", "status": "ERROR", "error": { "code": "HTTP_404", "message": "Not Found: https://api.github.com/repos/agent-skills/builder/pulls/42" } } ] }

这个trace清晰显示:前两步(输入校验、沙箱启动)都成功了,问题出在第三步HTTP请求——404。你立刻知道该去检查PR编号是否输错,而不是去翻skill代码逻辑。这就是--trace的价值:把分布式系统的故障定位,压缩到单次CLI调用里。

3.3 批量操作:--batch模式下的技能流水线编排

CLI不止于单次调用。--batch支持用YAML定义技能流水线:

# batch.yaml jobs: - id: analyze-prs steps: - skill: github-pr-list params: { owner: "agent-skills", repo: "builder", state: "open" } output_to: prs - skill: github-pr-diff params: { owner: "agent-skills", repo: "builder", pr_number: "{{ prs[0].number }}" } output_to: diff - skill: code-summary params: { diff: "{{ diff }}" } output_to: summary timeout: 300000 # 5分钟总超时

执行:

zcode cli batch --file batch.yaml --output-dir ./results

这会自动:

  • 先调用github-pr-list获取所有open PR;
  • 取第一个PR号,调用github-pr-diff;
  • 将diff内容喂给code-summary;
  • 所有中间结果存入./results/analyze-prs/目录,按step命名;
  • 任一step失败,整个batch中止,并输出失败step的完整trace。

我们用这个模式每天自动分析200+个仓库的PR质量,生成周报。关键是:流水线定义本身也是契约——output_to字段强制要求每个step必须声明输出位置,避免隐式状态传递。

3.4 权限诊断:--whoami模式下的最小权限验证

最后,--whoami是权限审计神器:

zcode cli whoami --skill github-pr-diff

输出:

Identity: user-alice@company.com (OIDC token) Permissions granted for github-pr-diff:v1.3.2: ├── Network: api.github.com:443 (granted) ├── Environment: GITHUB_TOKEN (granted, scope: read:org, public_repo) ├── Filesystem: /tmp (granted, read-write, 10MB limit) └── Runtime: python3.11 (granted, 256MB RAM, 0.5 CPU) Missing permissions (would cause failure): ├── None — all required permissions are granted

这解决了最头疼的问题:为什么在A机器能跑,在B机器报Permission denied?答案往往不是代码问题,而是B机器的OIDC token没申请public_reposcope。--whoami一键暴露权限缺口,不用翻文档、不用猜。

CLI的终极价值,是把skills的复杂性封装成可预测、可调试、可编排的原子操作。它不是玩具,是工程师的手术刀。

4. 技能生态不是市场,是带治理规则的协作网络:从安装到演进的实战法则

看到热搜词里skills下载平台有哪些、skills安装包下载,别被误导。真正的skills生态,不是App Store式的下载-安装-启用,而是一个基于信任链、带版本治理、需协作演进的协作网络。安装只是起点,治理才是核心。

4.1 安装的本质:不是复制文件,是建立信任锚点

zcode cli install github-pr-diff做了什么?不是curl | bash,而是:

  1. 解析skill ID:github-pr-diff→ 查询Registry,找到最新stable别名对应的镜像SHA;
  2. 验证签名:用公司根CA证书验证镜像manifest签名;
  3. 校验SBOM:检查SBOM中requests版本是否在白名单(>=2.31.0,<3.0.0);
  4. 下载镜像层:只下载sha256:abc123...指定的layer,跳过所有中间层;
  5. 本地缓存:存入~/.zcode/skills/github-pr-diff@sha256:abc123...,带完整性哈希。

关键点:安装不依赖网络实时性。一旦镜像被信任并缓存,断网也能运行。我们产研团队在飞机上写代码时,常用zcode cli offline --enable启用离线模式,所有skills从本地缓存加载,毫秒级响应。

踩坑实录:某次安全审计要求禁用所有HTTP源。运维同事把Registry地址从https://reg.internal.com改成http://reg.internal.com,结果所有zcode cli install失败。原因?zcode客户端强制校验Registry TLS证书,http://协议直接被拒绝。解决方案是配置--insecure-registry标志,但需管理员审批——这正是治理的体现:安全策略不能被随意绕过。

4.2 版本治理:@符号背后的语义化升级策略

skills的版本不是v1.3.2,而是github-pr-diff@sha256:abc123...。@符号是治理核心:

  • github-pr-diff:stable→ 动态别名,指向当前稳定版SHA;
  • github-pr-diff@sha256:abc123...→ 不可变锚点,永远指向那个镜像;
  • github-pr-diff^1.3→ 语义化范围,匹配1.3.x所有版本。

我们强制要求:

  • 生产环境只允许用@sha256格式(防意外升级);
  • CI/CD流水线用^1.3(允许小版本热修复);
  • 本地开发用:stable(获取最新特性)。

升级不是apt upgrade,而是显式声明:

# 查看可升级版本 zcode cli upgrade --list github-pr-diff # 升级到特定SHA(需审批) zcode cli upgrade github-pr-diff@sha256:def456... # 回滚(同样需审批) zcode cli rollback github-pr-diff@sha256:abc123...

每次升级/回滚,都会生成一条审计日志,记录操作人、时间、旧SHA、新SHA、审批工单号。这是合规刚需。

4.3 协作演进:--fork模式下的技能定制化实践

当标准github-pr-diff不满足需求(比如要加企业内部代码扫描),不要改原版。用--fork创建分支:

zcode cli fork \ --skill github-pr-diff \ --name internal-pr-diff \ --description "Adds SonarQube scan for internal repos" \ --template ./templates/internal-scan.j2

这会:

  • 克隆原skill的YAML和代码;
  • 替换模板中的{{ sonarqube_url }}为你的企业地址;
  • 生成新的skill IDinternal-pr-diff;
  • 注册到Registry,但标记为private: true(仅限内部可见)。

关键原则:Fork不是复制,是继承+增强。internal-pr-diff的YAML里会自动添加:

inherits_from: "github-pr-diff@sha256:abc123..." # 显式继承基线 patches: - file: "main.py" line: 42 add: "scan_result = sonarqube_scan(diff)"

这样,当上游github-pr-diff发布v1.4.0修复了diff解析bug,你可以一键zcode cli merge-upstream internal-pr-diff,只合并bugfix,不覆盖你的定制逻辑。这是Git式技能演进。

4.4 生态健康:--audit模式下的技能风险扫描

最后,zcode cli audit是生态体检仪:

zcode cli audit --all --report-format html > audit-report.html

报告包含:

  • 安全风险:哪些skills用了有CVE的requests<2.31.0;
  • 性能劣化:哪些skills的p95耗时比上周升了50%;
  • 依赖腐化:哪些skills还在用python3.9(已EOL);
  • 权限过度:哪些skills声明了network: ["*"]却只用api.github.com。

我们每月运行一次,自动生成整改工单。例如:报告指出web-searchskill的network: ["*"]应收紧为["https://api.duckduckgo.com"],工单自动创建,指派给owner,SLA 72小时修复。

技能生态的健康,不在于有多少个skills,而在于每个skills是否持续符合契约、安全、性能、治理四重标准。安装只是开始,治理才是日常。

5. 从零构建你的第一个production-ready skill:手把手实战指南

现在,让我们亲手构建一个真实可用的skill:pdf-text-extract。它要解决一个具体痛点——从PDF中提取纯文本,但要处理扫描件(OCR)、加密PDF、大文件(>100MB)等生产环境常见问题。全程不假定任何框架,只用标准工具链。

5.1 需求拆解:为什么不能直接用pdftotext?

你可能想:pdftotext不是现成的吗?但生产环境会遇到:

  • 扫描PDF:pdftotext返回空字符串,需调用Tesseract OCR;
  • 加密PDF:pdftotext报错Permission denied,需先解密;
  • 大文件:pdftotext加载100MB PDF吃光2GB内存;
  • 中文乱码:默认编码不支持GBK,需显式指定。

所以,我们的skill必须:
✅ 自动检测PDF类型(文本型/扫描型/加密型);
✅ 对扫描件调用OCR(用pytesseract,但需预装tesseract-ocr-chi-sim);
✅ 对加密PDF尝试用空密码解密(企业内网常见);
✅ 流式处理大文件,内存占用<128MB;
✅ 输出UTF-8文本,中文不乱码。

5.2 定义契约:pdf-text-extract.skill.yaml

id: pdf-text-extract version: 1.0.0 name: PDF Text Extractor with OCR Fallback description: | Extracts plain text from PDF files. Automatically detects scanned PDFs and invokes Tesseract OCR. Handles encrypted PDFs with common passwords. Streams large files to avoid OOM. input_schema: type: object required: [pdf_path] properties: pdf_path: type: string description: Local path to PDF file (must be readable) pattern: "^/tmp/.*\\.pdf$" # 限定/tmp,防路径遍历 ocr_lang: type: string default: "eng" enum: ["eng", "chi_sim", "jpn", "kor"] max_pages: type: integer default: 50 minimum: 1 maximum: 500 password: type: string description: Password for encrypted PDF (empty for common defaults) output_schema: type: object properties: text: type: string description: Extracted plain text (UTF-8) maxLength: 10485760 # 10MB max text output metadata: type: object properties: pages_processed: { type: integer } is_scanned: { type: boolean } ocr_used: { type: boolean } encoding: { type: string } runtime: type: python version: "3.11" dependencies: - PyPDF2>=3.0.0,<4.0.0 - pytesseract>=0.3.10,<0.4.0 - pdf2image>=1.16.0,<2.0.0 resources: memory_mb: 128 cpu_millis: 30000 # 30秒超时 permissions: filesystem: ["read-only"] environment: ["TESSDATA_PREFIX"] timeout_ms: 30000 retries: { max_attempts: 1 } fallback: skill: "text-extract" params: { method: "dummy", text: "PDF extraction failed" }

注意:pattern: "^/tmp/.*\\.pdf$"强制输入路径必须在/tmp,杜绝../../../etc/passwd路径遍历;maxLength: 10485760防止OCR返回超长垃圾文本。

5.3 编写代码:main.py(核心逻辑)

#!/usr/bin/env python3.11 import os import sys import tempfile import logging from pathlib import Path from typing import Dict, Any, Optional # 导入PyPDF2等,略(标准库导入) def extract_text(pdf_path: str, ocr_lang: str = "eng", max_pages: int = 50, password: str = "") -> Dict[str, Any]: """Main entry point. Returns dict matching output_schema.""" result = { "text": "", "metadata": { "pages_processed": 0, "is_scanned": False, "ocr_used": False, "encoding": "utf-8" } } # Step 1: Validate input path pdf_file = Path(pdf_path) if not pdf_file.exists(): raise ValueError(f"PDF file not found: {pdf_path}") if not pdf_file.is_file(): raise ValueError(f"Not a file: {pdf_path}") if not str(pdf_file).startswith("/tmp/"): raise ValueError("PDF path must be under /tmp") # Step 2: Try text extraction first (fast) try: text = _extract_text_pypdf2(pdf_file, password, max_pages) if text.strip() and len(text) > 100: # Heuristic: non-empty meaningful text result["text"] = text result["metadata"]["pages_processed"] = _count_pages_pypdf2(pdf_file) return result except Exception as e: logging.debug(f"PyPDF2 extraction failed: {e}") # Step 3: Fall back to OCR for scanned PDF result["metadata"]["is_scanned"] = True result["metadata"]["ocr_used"] = True # Use pdf2image to convert to images, then tesseract try: images = convert_from_path( str(pdf_file), dpi=200, first_page=1, last_page=max_pages, thread_count=2 ) ocr_text = "" for i, image in enumerate(images): if i >= max_pages: break # Stream each page to avoid loading all into memory with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as tmp: image.save(tmp.name, "PNG") page_text = pytesseract.image_to_string( tmp.name, lang=ocr_lang, config="--psm 6" # Assume single column ) ocr_text += page_text + "\n\n" os.unlink(tmp.name) # Clean up immediately result["text"] = ocr_text[:10485760] # Respect maxLength result["metadata"]["pages_processed"] = len(images) return result except Exception as e: logging.error(f"OCR extraction failed: {e}") raise def _extract_text_pypdf2(pdf_path: Path, password: str, max_pages: int) -> str: """Text extraction using PyPDF2.""" reader = PdfReader(pdf_path) # Try decrypt if reader.is_encrypted: if password: reader.decrypt(password) else: # Try common passwords for pwd in ["", "password", "123456"]: if reader.decrypt(pwd) == 1: break text = "" for i, page in enumerate(reader.pages): if i >= max_pages: break text += page.extract_text() or "" return text if __name__ == "__main__": # CLI entry point (for local testing) import argparse parser = argparse.ArgumentParser() parser.add_argument("--pdf-path", required=True) parser.add_argument("--ocr-lang", default="eng") parser.add_argument("--max-pages", type=int, default=50) parser.add_argument("--password", default="") args = parser.parse_args() result = extract_text( args.pdf_path, args.ocr_lang, args.max_pages, args.password ) print(result["text"])

关键设计:

  • 内存友好:OCR时每页转存为临时PNG,处理完立即删除,不累积;
  • 密码容错:自动尝试空密码、password、123456,覆盖企业内网常见情况;
  • 流式处理:max_pages参数控制处理页数,防大PDF卡死。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询