PyCharm插件开发:DeepSeek-OCR-2代码文档工具
PyCharm插件开发:DeepSeek-OCR-2代码文档工具
1. 引言
你有没有遇到过这样的情况?接手一个老项目,代码文档要么缺失,要么就是几年前的扫描版PDF,想找个函数说明都得翻半天纸质文档。或者客户发来一份技术规格书,全是扫描图片,里面的表格、公式、代码片段都得手动敲进电脑。
我之前就经常被这种事情困扰,直到发现了DeepSeek-OCR-2这个神器。它不仅能识别文字,还能理解文档结构,把扫描件直接转成可编辑的Markdown。但每次都要打开网页或者命令行,总觉得不够顺手。
后来我想,要是能在PyCharm里直接处理这些文档,那该多方便啊。上传个扫描件,自动转成代码注释;打开个技术文档,直接解析出API接口;甚至能把整个项目的文档自动整理成规范的格式。
这就是我今天要分享的——一个集成DeepSeek-OCR-2的PyCharm插件,让你在IDE里就能搞定所有文档处理工作。
2. 为什么要在PyCharm里集成OCR工具?
2.1 开发者的文档处理痛点
做开发这么多年,我发现文档处理有几个特别烦人的地方:
第一是格式转换的麻烦。客户发来的技术文档经常是PDF扫描件,里面的代码示例、API接口说明都得手动复制。有时候表格跨页了,复制粘贴后格式全乱,还得花时间重新调整。
第二是代码注释的维护。项目文档更新了,但代码里的注释还是老版本。特别是接手别人的代码,注释和实际功能对不上,调试起来特别费劲。
第三是效率问题。在IDE和文档处理工具之间来回切换,打断编码思路。查个API文档要开浏览器,看个技术规格要开PDF阅读器,窗口切来切去,时间都浪费在切换上了。
2.2 DeepSeek-OCR-2的优势
DeepSeek-OCR-2跟传统OCR工具不太一样,它有几个特别适合开发者的特点:
语义理解能力强。传统的OCR就是机械扫描,从左到右、从上到下识别文字。但DeepSeek-OCR-2能理解文档的语义结构,知道哪个是标题、哪个是正文、表格怎么排布、代码块在哪里。这对于技术文档特别重要,因为技术文档的结构通常很复杂。
支持多种格式输出。不仅能转成纯文本,还能保持Markdown格式,表格、列表、代码块都能保留原样。这对于开发者来说太实用了,直接就能用。
处理复杂文档能力强。技术文档里经常有数学公式、化学式、图表,这些传统OCR基本搞不定。DeepSeek-OCR-2专门优化了这些场景,识别准确率很高。
开源且易集成。Apache-2.0协议,商业友好,而且提供了完整的Python API,集成到PyCharm插件里特别方便。
2.3 插件化的价值
把DeepSeek-OCR-2做成PyCharm插件,最大的好处就是工作流一体化。你不用离开开发环境,就能完成文档处理的所有操作。
想象一下这样的场景:你正在写一个函数,需要参考设计文档里的某个接口说明。传统做法是:最小化PyCharm → 打开PDF阅读器 → 找到对应页面 → 截图 → 打开OCR工具 → 识别文字 → 复制到代码里。现在只需要:在PyCharm里右键点击文档 → 选择“提取文本” → 直接粘贴到代码注释里。
效率提升不是一点半点。
3. 插件功能设计
3.1 核心功能模块
这个插件我设计了三个主要功能模块,每个都针对开发者常见的文档处理需求。
文档扫描转注释是最常用的功能。你可能有纸质版的代码规范、API文档,或者客户发来的扫描版需求文档。用这个功能,上传文档图片,选择要添加注释的代码文件,插件会自动识别文档内容,并按照你设定的格式插入到代码中。
比如你有个函数需要添加文档字符串,但设计文档是扫描的PDF。传统做法是手动打字,现在只需要:
# 在PyCharm中右键点击函数定义
# 选择“从文档添加注释”
# 上传扫描的文档图片
# 插件自动识别相关内容并生成docstring
def process_user_data(user_id: int, data: dict) -> bool:
"""
处理用户数据
参数:
user_id: 用户ID,必须是已注册的有效用户
data: 用户数据字典,包含以下字段:
- name: 用户名,字符串类型
- email: 邮箱地址,必须符合邮箱格式
- preferences: 用户偏好设置字典
返回:
bool: 处理成功返回True,失败返回False
异常:
ValueError: 当user_id无效或data格式错误时抛出
DatabaseError: 数据库操作失败时抛出
"""
# 函数实现...
技术规格书解析这个功能特别适合做接口开发。技术规格书里通常有详细的API说明、参数表格、返回格式定义。手动整理这些信息很耗时,还容易出错。
插件能自动识别规格书里的表格,提取API端点、HTTP方法、参数说明、返回格式,然后生成对应的代码框架或者API文档。比如从规格书里识别出这样的表格:
| 端点 | 方法 | 参数 | 说明 |
|------|------|------|------|
| /api/users | POST | {name, email} | 创建新用户 |
| /api/users/{id} | GET | id | 获取用户信息 |
然后自动生成FastAPI的路由代码:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI()
class UserCreate(BaseModel):
name: str
email: str
class UserResponse(BaseModel):
id: int
name: str
email: str
@app.post("/api/users")
async def create_user(user: UserCreate):
"""创建新用户"""
# 实现代码...
return {"id": 1, "name": user.name, "email": user.email}
@app.get("/api/users/{user_id}")
async def get_user(user_id: int):
"""获取用户信息"""
# 实现代码...
return {"id": user_id, "name": "张三", "email": "zhangsan@example.com"}
API文档自动生成是另一个实用功能。很多项目有代码但没文档,或者文档过时了。这个功能可以扫描整个项目的代码文件,提取函数定义、类结构、参数类型,然后生成统一的API文档。
更厉害的是,它还能识别代码中的TODO、FIXME注释,自动生成待办事项列表。对于维护大型项目特别有帮助。
3.2 用户界面设计
插件的UI设计我遵循了PyCharm的原生风格,让用户感觉像是IDE自带的功能,而不是第三方插件。
主界面放在PyCharm的侧边栏,有三个主要面板:
文档上传区在最上面,支持拖拽上传,能处理图片、PDF、Word文档。上传后会有预览,让你确认选对了文件。
处理选项区在中间,这里可以设置各种参数。比如输出格式选Markdown还是纯文本,是否保留表格结构,代码注释的模板用什么样式。还有高级选项,比如设置OCR的语言、识别精度等。
结果展示区在下面,分两个标签页。一个显示原始识别结果,一个显示格式化后的内容。识别结果可以直接编辑,修正一些识别错误,然后一键插入到代码中。
右键菜单也集成了快捷功能。在代码编辑器里选中一段文字,右键就能看到“从文档添加注释”、“生成API文档”等选项。在项目文件上右键,可以选择“扫描项目文档”。
3.3 工作流程
整个插件的工作流程设计得很顺畅:
- 上传文档:拖拽或者选择文件,支持批量上传
- 预处理:自动检测文档类型,如果是图片会做角度校正、去噪处理
- OCR识别:调用DeepSeek-OCR-2接口,识别文档内容
- 结构分析:分析文档结构,识别标题、段落、表格、代码块
- 格式转换:按照设置转换成目标格式
- 结果展示:在编辑器中预览,可以手动调整
- 应用结果:插入到代码、生成文档文件、或者导出
整个过程都有进度提示,长时间操作可以取消。识别结果会缓存,同样的文档第二次处理几乎瞬间完成。
4. 开发环境搭建
4.1 PyCharm插件开发基础
如果你没做过PyCharm插件开发,可能会觉得有点复杂,其实掌握了基本套路就很简单。
首先需要安装IntelliJ IDEA(社区版就行),因为PyCharm插件是用Java开发的,虽然我们的功能主要用Python实现,但插件框架还是需要Java环境。
然后配置PyCharm插件SDK。在IntelliJ IDEA里新建项目,选择“IntelliJ Platform Plugin”,SDK选PyCharm的安装目录。这里有个小技巧:最好用和平时开发一样的PyCharm版本,避免兼容性问题。
项目结构大概长这样:
DeepSeekOCRPlugin/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── deepseekocr/
│ │ │ ├── actions/ # 动作处理器
│ │ │ ├── ui/ # 用户界面
│ │ │ └── utils/ # 工具类
│ │ └── resources/
│ │ ├── META-INF/
│ │ │ └── plugin.xml # 插件配置文件
│ │ └── icons/ # 图标资源
├── lib/ # 依赖库
└── build.gradle # 构建配置
plugin.xml是插件的核心配置文件,这里定义了插件的基本信息、依赖、扩展点等。
4.2 DeepSeek-OCR-2环境配置
DeepSeek-OCR-2的配置相对简单,因为它提供了完整的Python包。
首先创建Python虚拟环境,我推荐用Python 3.9或3.10,兼容性比较好:
# 创建虚拟环境
python -m venv venv
# 激活虚拟环境
# Windows
venv\Scripts\activate
# macOS/Linux
source venv/bin/activate
# 安装DeepSeek-OCR-2
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
pip install transformers
pip install flash-attn --no-build-isolation
如果你的机器没有GPU,或者显存不够,可以用CPU版本,不过速度会慢一些:
# CPU版本安装
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cpu
DeepSeek-OCR-2模型比较大,大概7-8GB,第一次运行时会自动下载。如果网络不好,可以手动下载后指定本地路径:
from transformers import AutoModel, AutoTokenizer
# 指定本地模型路径
model_path = "/path/to/local/deepseek-ocr-2"
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
model = AutoModel.from_pretrained(
model_path,
_attn_implementation='flash_attention_2',
trust_remote_code=True,
use_safetensors=True
)
4.3 插件项目初始化
用Gradle来管理项目依赖比较方便,build.gradle文件这样配置:
plugins {
id 'java'
id 'org.jetbrains.intellij' version '1.13.3'
}
group 'com.deepseekocr'
version '1.0.0'
repositories {
mavenCentral()
}
dependencies {
implementation 'com.fasterxml.jackson.core:jackson-databind:2.15.2'
implementation 'org.apache.httpcomponents:httpclient:4.5.14'
// Python集成相关
implementation 'org.python:jython-standalone:2.7.3'
testImplementation 'junit:junit:4.13.2'
}
intellij {
version = '2023.2.5' // PyCharm版本
type = 'PY' // PyCharm类型
plugins = ['python'] // 依赖的插件
}
patchPluginXml {
sinceBuild = '232'
untilBuild = '242.*'
}
plugin.xml里要声明我们的扩展点:
<idea-plugin>
<id>com.deepseekocr.plugin</id>
<name>DeepSeek OCR Tools</name>
<version>1.0.0</version>
<vendor>Your Name</vendor>
<description>Integrate DeepSeek-OCR-2 into PyCharm for document processing</description>
<depends>com.intellij.modules.platform</depends>
<depends>com.intellij.modules.python</depends>
<extensions defaultExtensionNs="com.intellij">
<!-- 工具窗口 -->
<toolWindow id="DeepSeek OCR"
anchor="right"
factoryClass="com.deepseekocr.ui.OCRToolWindowFactory"/>
<!-- 编辑器右键菜单 -->
<action id="DeepSeekOCR.AddCommentFromDoc"
class="com.deepseekocr.actions.AddCommentAction"
text="Add Comment from Document"
description="Add code comment from scanned document">
<add-to-group group-id="EditorPopupMenu" anchor="first"/>
</action>
</extensions>
</idea-plugin>
5. 核心功能实现
5.1 文档扫描与OCR集成
OCR功能是插件的核心,我把它封装成了一个独立的服务类,这样其他地方调用起来方便,也便于测试。
# ocr_service.py
import os
import tempfile
from typing import Optional, Dict, Any
from PIL import Image
import fitz # PyMuPDF
from transformers import AutoModel, AutoTokenizer
import torch
class OCRService:
def __init__(self, model_path: Optional[str] = None):
"""初始化OCR服务"""
self.device = "cuda" if torch.cuda.is_available() else "cpu"
self.model_path = model_path or "deepseek-ai/DeepSeek-OCR-2"
# 加载模型和tokenizer
self.tokenizer = AutoTokenizer.from_pretrained(
self.model_path,
trust_remote_code=True
)
self.model = AutoModel.from_pretrained(
self.model_path,
_attn_implementation='flash_attention_2',
trust_remote_code=True,
use_safetensors=True,
device_map="auto"
)
self.model.eval()
def process_image(self, image_path: str, prompt: str = None) -> str:
"""处理单张图片"""
if prompt is None:
prompt = "<image>\n<|grounding|>Convert the document to markdown."
try:
# 执行OCR识别
result = self.model.infer(
self.tokenizer,
prompt=prompt,
image_file=image_path,
output_path=None, # 不保存文件,直接返回结果
base_size=1024,
image_size=768,
crop_mode=True,
save_results=False
)
# 提取文本内容
if hasattr(result, 'text'):
return result.text
elif isinstance(result, dict) and 'text' in result:
return result['text']
else:
return str(result)
except Exception as e:
raise Exception(f"OCR处理失败: {str(e)}")
def process_pdf(self, pdf_path: str, max_pages: int = 10) -> Dict[int, str]:
"""处理PDF文档,返回每页的识别结果"""
results = {}
try:
# 打开PDF文件
doc = fitz.open(pdf_path)
# 限制处理页数,避免内存溢出
total_pages = min(len(doc), max_pages)
for page_num in range(total_pages):
page = doc[page_num]
# 将PDF页面转为图片
pix = page.get_pixmap(matrix=fitz.Matrix(2, 2)) # 2倍分辨率
img_data = pix.tobytes("png")
# 保存临时图片文件
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as tmp:
tmp.write(img_data)
tmp_path = tmp.name
try:
# 处理当前页
text = self.process_image(tmp_path)
results[page_num + 1] = text
finally:
# 清理临时文件
os.unlink(tmp_path)
doc.close()
return results
except Exception as e:
raise Exception(f"PDF处理失败: {str(e)}")
def process_document(self, file_path: str, **kwargs) -> Dict[str, Any]:
"""通用文档处理入口"""
file_ext = os.path.splitext(file_path)[1].lower()
if file_ext in ['.png', '.jpg', '.jpeg', '.bmp', '.tiff']:
# 图片文件
text = self.process_image(file_path, kwargs.get('prompt'))
return {
'type': 'image',
'content': text,
'pages': 1
}
elif file_ext == '.pdf':
# PDF文件
max_pages = kwargs.get('max_pages', 10)
pages = self.process_pdf(file_path, max_pages)
return {
'type': 'pdf',
'content': pages,
'pages': len(pages)
}
else:
raise ValueError(f"不支持的文件格式: {file_ext}")
这个服务类做了几件事:自动检测文件类型、处理图片和PDF、管理临时文件、错误处理。实际使用中,还可以根据需要添加更多功能,比如批量处理、进度回调等。
5.2 代码注释自动生成
从OCR结果生成代码注释,关键是理解代码结构和文档内容的对应关系。我设计了一个注释生成器:
# comment_generator.py
import re
from typing import List, Dict, Optional
import ast
class CommentGenerator:
def __init__(self, language: str = "python"):
self.language = language
self.templates = self._load_templates()
def _load_templates(self) -> Dict[str, str]:
"""加载注释模板"""
return {
"python_function": '''\"\"\"
{description}
参数:
{params}
返回:
{returns}
异常:
{exceptions}
\"\"\"''',
"python_class": '''\"\"\"
{class_description}
属性:
{attributes}
方法:
{methods}
\"\"\"''',
"java_method": '''/**
* {description}
*
* @param {params}
* @return {returns}
* @throws {exceptions}
*/''',
"typescript_interface": '''/**
* {description}
*/
interface {name} {{
{properties}
}}'''
}
def extract_code_info(self, code: str) -> Dict[str, Any]:
"""从代码中提取信息"""
if self.language == "python":
return self._extract_python_info(code)
elif self.language == "java":
return self._extract_java_info(code)
# 其他语言...
def _extract_python_info(self, code: str) -> Dict[str, Any]:
"""提取Python代码信息"""
try:
tree = ast.parse(code)
for node in ast.walk(tree):
if isinstance(node, ast.FunctionDef):
# 提取函数信息
func_name = node.name
args = []
returns = None
# 提取参数
for arg in node.args.args:
args.append({
'name': arg.arg,
'type': self._get_annotation(arg.annotation) if arg.annotation else 'Any'
})
# 提取返回类型
if node.returns:
returns = self._get_annotation(node.returns)
# 提取装饰器
decorators = []
for decorator in node.decorator_list:
if isinstance(decorator, ast.Name):
decorators.append(decorator.id)
return {
'type': 'function',
'name': func_name,
'args': args,
'returns': returns,
'decorators': decorators
}
elif isinstance(node, ast.ClassDef):
# 提取类信息
return {
'type': 'class',
'name': node.name,
'methods': [n.name for n in node.body if isinstance(n, ast.FunctionDef)]
}
except SyntaxError:
pass
return {'type': 'unknown'}
def generate_comment(self, code_info: Dict, doc_content: str) -> str:
"""根据代码信息和文档内容生成注释"""
if code_info['type'] == 'function':
return self._generate_function_comment(code_info, doc_content)
elif code_info['type'] == 'class':
return self._generate_class_comment(code_info, doc_content)
else:
return self._generate_general_comment(doc_content)
def _generate_function_comment(self, func_info: Dict, doc: str) -> str:
"""生成函数注释"""
# 从文档中提取相关信息
params_desc = self._extract_params_from_doc(doc, func_info['args'])
returns_desc = self._extract_returns_from_doc(doc)
exceptions_desc = self._extract_exceptions_from_doc(doc)
# 构建参数部分
params_text = ""
for arg in func_info['args']:
desc = params_desc.get(arg['name'], "参数说明")
params_text += f" {arg['name']} ({arg['type']}): {desc}\n"
# 使用模板生成注释
template = self.templates["python_function"]
return template.format(
description=self._extract_description(doc),
params=params_text.rstrip(),
returns=f"{func_info['returns'] or 'Any'}: {returns_desc}",
exceptions=exceptions_desc or "无"
)
def _extract_params_from_doc(self, doc: str, args: List[Dict]) -> Dict[str, str]:
"""从文档中提取参数说明"""
params = {}
# 查找参数说明部分
param_section = re.search(r'参数[::]\s*\n(.*?)(?=\n\s*\n|\Z)', doc, re.DOTALL)
if param_section:
param_text = param_section.group(1)
# 提取每个参数的说明
for line in param_text.strip().split('\n'):
match = re.match(r'^\s*(\w+)[::]\s*(.+)$', line)
if match:
param_name, desc = match.groups()
params[param_name] = desc.strip()
return params
def _extract_description(self, doc: str) -> str:
"""提取函数或类的描述"""
# 提取第一段非空文本作为描述
lines = doc.strip().split('\n')
description = []
for line in lines:
line = line.strip()
if line and not line.startswith(('参数', '返回', '异常', '属性', '方法')):
description.append(line)
elif description:
break
return ' '.join(description) if description else "功能描述"
这个生成器能智能地从文档中提取相关信息,然后按照代码的结构生成规范的注释。支持多种语言,可以根据需要扩展。
5.3 API文档解析与生成
技术文档里的API说明通常有固定格式,利用这个特点可以自动解析:
# api_parser.py
import re
from typing import List, Dict, Optional
from dataclasses import dataclass
@dataclass
class APIParam:
name: str
type: str
required: bool
description: str
example: Optional[str] = None
@dataclass
class APIEndpoint:
path: str
method: str
summary: str
description: str
parameters: List[APIParam]
responses: Dict[str, str]
tags: List[str]
class APIParser:
def __init__(self):
self.patterns = {
'endpoint': re.compile(r'^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(\S+)'),
'param': re.compile(r'^\s*(\w+)\s*\((\w+)\)\s*[::]\s*(.+?)(?:\s*示例[::]\s*(.+))?$'),
'response': re.compile(r'^\s*(\d{3})\s*[::]\s*(.+)$'),
'tag': re.compile(r'^\[(\w+)\]'),
}
def parse_from_markdown(self, markdown_text: str) -> List[APIEndpoint]:
"""从Markdown文本解析API文档"""
endpoints = []
lines = markdown_text.split('\n')
i = 0
while i < len(lines):
line = lines[i].strip()
# 查找API端点
endpoint_match = self.patterns['endpoint'].match(line)
if endpoint_match:
method, path = endpoint_match.groups()
endpoint = APIEndpoint(
path=path,
method=method,
summary="",
description="",
parameters=[],
responses={},
tags=[]
)
# 提取摘要和描述
i += 1
while i < len(lines) and lines[i].strip() and not self.patterns['endpoint'].match(lines[i].strip()):
desc_line = lines[i].strip()
if not endpoint.summary:
endpoint.summary = desc_line
else:
endpoint.description += desc_line + "\n"
i += 1
# 提取参数
while i < len(lines) and lines[i].strip().startswith(('参数', '请求参数')):
i += 1 # 跳过标题行
while i < len(lines) and lines[i].strip() and not lines[i].strip().startswith(('返回', '响应', '示例')):
param_match = self.patterns['param'].match(lines[i])
if param_match:
name, type_str, desc, example = param_match.groups()
endpoint.parameters.append(APIParam(
name=name,
type=type_str,
required='必选' in desc or 'required' in desc.lower(),
description=desc,
example=example
))
i += 1
# 提取响应
while i < len(lines) and lines[i].strip().startswith(('返回', '响应')):
i += 1
while i < len(lines) and lines[i].strip() and not lines[i].strip().startswith(('示例', '代码')):
resp_match = self.patterns['response'].match(lines[i])
if resp_match:
code, desc = resp_match.groups()
endpoint.responses[code] = desc
i += 1
endpoints.append(endpoint)
else:
i += 1
return endpoints
def generate_fastapi_code(self, endpoint: APIEndpoint) -> str:
"""生成FastAPI代码"""
imports = [
"from fastapi import FastAPI, HTTPException",
"from pydantic import BaseModel",
"from typing import Optional, List, Dict",
"",
"app = FastAPI()",
""
]
# 生成数据模型
models = []
for param in endpoint.parameters:
if param.type in ['object', 'array', 'RequestBody']:
model_name = f"{endpoint.path.split('/')[-1].title()}{param.name.title()}Model"
models.append(f"class {model_name}(BaseModel):")
models.append(f' """{param.description}"""')
models.append(" pass # TODO: 定义具体字段")
models.append("")
# 生成路由函数
route_func = []
route_func.append(f'@app.{endpoint.method.lower()}("{endpoint.path}")')
route_func.append(f'async def {self._generate_function_name(endpoint.path, endpoint.method)}():')
route_func.append(f' """{endpoint.summary}"""')
route_func.append(f' # TODO: 实现业务逻辑')
route_func.append(f' return {{"message": "Not implemented"}}')
route_func.append("")
return "\n".join(imports + models + route_func)
def _generate_function_name(self, path: str, method: str) -> str:
"""根据路径和方法生成函数名"""
# 清理路径中的特殊字符
clean_path = re.sub(r'[{}<>]', '', path)
parts = [p for p in clean_path.split('/') if p and not p.startswith('{')]
if parts:
base_name = '_'.join(parts)
else:
base_name = 'root'
# 添加方法前缀
method_prefix = {
'GET': 'get',
'POST': 'create',
'PUT': 'update',
'DELETE': 'delete',
'PATCH': 'patch'
}.get(method, 'handle')
return f"{method_prefix}_{base_name}"
这个解析器能自动从技术文档中提取API信息,然后生成对应的FastAPI代码框架。对于前后端分离的项目特别有用,能节省大量手动编写接口代码的时间。
6. PyCharm插件集成
6.1 工具窗口实现
工具窗口是插件的主界面,我用Swing来实现,保持和PyCharm一致的视觉风格。
// OCRToolWindow.java
package com.deepseekocr.ui;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
import java.awt.*;
public class OCRToolWindowFactory implements ToolWindowFactory {
@Override
public void createToolWindowContent(@NotNull Project project,
@NotNull ToolWindow toolWindow) {
// 创建主面板
OCRToolWindowPanel panel = new OCRToolWindowPanel(project);
// 创建内容
ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
Content content = contentFactory.createContent(panel, "", false);
// 添加到工具窗口
toolWindow.getContentManager().addContent(content);
}
static class OCRToolWindowPanel extends JPanel {
private final Project project;
private JTabbedPane tabbedPane;
public OCRToolWindowPanel(Project project) {
this.project = project;
initUI();
}
private void initUI() {
setLayout(new BorderLayout());
// 创建标签页
tabbedPane = new JTabbedPane();
// 文档转注释标签页
tabbedPane.addTab("文档转注释", createDocumentToCommentPanel());
// API文档生成标签页
tabbedPane.addTab("API生成", createAPIGenerationPanel());
// 设置标签页
tabbedPane.addTab("设置", createSettingsPanel());
add(tabbedPane, BorderLayout.CENTER);
// 状态栏
JPanel statusPanel = new JPanel(new BorderLayout());
JLabel statusLabel = new JLabel("就绪");
statusPanel.add(statusLabel, BorderLayout.WEST);
add(statusPanel, BorderLayout.SOUTH);
}
private JPanel createDocumentToCommentPanel() {
JPanel panel = new JPanel(new BorderLayout(10, 10));
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
// 文件选择区域
JPanel filePanel = new JPanel(new BorderLayout(5, 5));
filePanel.add(new JLabel("选择文档:"), BorderLayout.WEST);
JTextField fileField = new JTextField();
JButton browseButton = new JButton("浏览...");
JPanel fileInputPanel = new JPanel(new BorderLayout(5, 5));
fileInputPanel.add(fileField, BorderLayout.CENTER);
fileInputPanel.add(browseButton, BorderLayout.EAST);
filePanel.add(fileInputPanel, BorderLayout.CENTER);
// 代码文件选择
JPanel codePanel = new JPanel(new BorderLayout(5, 5));
codePanel.add(new JLabel("目标代码文件:"), BorderLayout.WEST);
JTextField codeField = new JTextField();
JButton codeBrowseButton = new JButton("浏览...");
JPanel codeInputPanel = new JPanel(new BorderLayout(5, 5));
codeInputPanel.add(codeField, BorderLayout.CENTER);
codeInputPanel.add(codeBrowseButton, BorderLayout.EAST);
codePanel.add(codeInputPanel, BorderLayout.CENTER);
// 选项区域
JPanel optionsPanel = new JPanel(new GridLayout(0, 2, 5, 5));
optionsPanel.add(new JLabel("输出格式:"));
JComboBox<String> formatCombo = new JComboBox<>(
new String[]{"Markdown", "纯文本", "reStructuredText"}
);
optionsPanel.add(formatCombo);
optionsPanel.add(new JLabel("语言:"));
JComboBox<String> langCombo = new JComboBox<>(
new String[]{"自动检测", "中文", "英文", "中英混合"}
);
optionsPanel.add(langCombo);
// 处理按钮
JButton processButton = new JButton("开始处理");
processButton.setPreferredSize(new Dimension(100, 30));
// 结果展示区域
JTextArea resultArea = new JTextArea(20, 60);
resultArea.setEditable(true);
JScrollPane scrollPane = new JScrollPane(resultArea);
// 布局
JPanel northPanel = new JPanel(new GridLayout(3, 1, 5, 5));
northPanel.add(filePanel);
northPanel.add(codePanel);
northPanel.add(optionsPanel);
panel.add(northPanel, BorderLayout.NORTH);
panel.add(scrollPane, BorderLayout.CENTER);
JPanel southPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT));
southPanel.add(processButton);
panel.add(southPanel, BorderLayout.SOUTH);
return panel;
}
// 其他面板创建方法类似...
}
}
工具窗口用了标签页设计,把不同功能分开,界面清晰。文件选择、参数设置、结果展示都在一个界面里完成,操作流程很顺畅。
6.2 编辑器集成
为了让插件用起来更顺手,我把它集成到了编辑器的右键菜单里。这样在写代码的时候,随时都能调用OCR功能。
// AddCommentAction.java
package com.deepseekocr.actions;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.vfs.VirtualFile;
import com.deepseekocr.services.OCRService;
import com.deepseekocr.services.CommentGenerator;
import com.deepseekocr.ui.DocumentSelectionDialog;
import javax.swing.*;
import java.io.File;
public class AddCommentAction extends AnAction {
@Override
public void actionPerformed(AnActionEvent e) {
Project project = e.getProject();
Editor editor = e.getData(CommonDataKeys.EDITOR);
if (project == null || editor == null) {
Messages.showErrorDialog("请在编辑器中执行此操作", "错误");
return;
}
// 获取当前文件
VirtualFile virtualFile = e.getData(CommonDataKeys.VIRTUAL_FILE);
if (virtualFile == null) {
Messages.showErrorDialog("无法获取当前文件", "错误");
return;
}
// 显示文档选择对话框
DocumentSelectionDialog dialog = new DocumentSelectionDialog(project);
if (dialog.showAndGet()) {
File selectedFile = dialog.getSelectedFile();
String language = dialog.getSelectedLanguage();
// 在后台执行OCR处理
new Thread(() -> {
try {
// 调用OCR服务
OCRService ocrService = new OCRService();
String ocrResult = ocrService.processDocument(
selectedFile.getAbsolutePath()
);
// 生成注释
CommentGenerator generator = new CommentGenerator(language);
String code = editor.getDocument().getText();
String comment = generator.generateComment(code, ocrResult);
// 在UI线程中更新编辑器
SwingUtilities.invokeLater(() -> {
int offset = editor.getCaretModel().getOffset();
editor.getDocument().insertString(offset, "\n" + comment + "\n");
Messages.showInfoDialog("注释添加成功", "完成");
});
} catch (Exception ex) {
SwingUtilities.invokeLater(() -> {
Messages.showErrorDialog("处理失败: " + ex.getMessage(), "错误");
});
}
}).start();
}
}
@Override
public void update(AnActionEvent e) {
// 只在有编辑器且是代码文件时启用
Project project = e.getProject();
Editor editor = e.getData(CommonDataKeys.EDITOR);
VirtualFile file = e.getData(CommonDataKeys.VIRTUAL_FILE);
boolean enabled = project != null &&
editor != null &&
file != null &&
isCodeFile(file);
e.getPresentation().setEnabled(enabled);
}
private boolean isCodeFile(VirtualFile file) {
String extension = file.getExtension();
return extension != null && (
extension.equals("py") ||
extension.equals("java") ||
extension.equals("js") ||
extension.equals("ts") ||
extension.equals("cpp") ||
extension.equals("h") ||
extension.equals("go")
);
}
}
这个动作类处理从文档添加注释的整个流程:显示文件选择对话框、调用OCR服务、生成注释、插入到代码中。所有耗时操作都在后台线程执行,避免界面卡顿。
6.3 配置管理
插件的配置用PyCharm的持久化组件来管理,这样设置能保存下来,下次打开还在。
// PluginSettings.java
package com.deepseekocr.settings;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.util.xmlb.XmlSerializerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@State(
name = "DeepSeekOCRSettings",
storages = @Storage("DeepSeekOCR.xml")
)
public class PluginSettings implements PersistentStateComponent<PluginSettings> {
private String modelPath = "deepseek-ai/DeepSeek-OCR-2";
private String outputFormat = "markdown";
private String defaultLanguage = "auto";
private boolean enableCache = true;
private int cacheSize = 100;
private int maxPages = 20;
private boolean autoInsert = true;
private String commentStyle = "google";
// 模型路径配置
public String getModelPath() {
return modelPath;
}
public void setModelPath(String modelPath) {
this.modelPath = modelPath;
}
// 输出格式配置
public String getOutputFormat() {
return outputFormat;
}
public void setOutputFormat(String outputFormat) {
this.outputFormat = outputFormat;
}
// 默认语言配置
public String getDefaultLanguage() {
return defaultLanguage;
}
public void setDefaultLanguage(String defaultLanguage) {
this.defaultLanguage = defaultLanguage;
}
// 缓存配置
public boolean isEnableCache() {
return enableCache;
}
public void setEnableCache(boolean enableCache) {
this.enableCache = enableCache;
}
public int getCacheSize() {
return cacheSize;
}
public void setCacheSize(int cacheSize) {
this.cacheSize = cacheSize;
}
// 最大页数配置
public int getMaxPages() {
return maxPages;
}
public void setMaxPages(int maxPages) {
this.maxPages = maxPages;
}
// 自动插入配置
public boolean isAutoInsert() {
return autoInsert;
}
public void setAutoInsert(boolean autoInsert) {
this.autoInsert = autoInsert;
}
// 注释样式配置
public String getCommentStyle() {
return commentStyle;
}
public void setCommentStyle(String commentStyle) {
this.commentStyle = commentStyle;
}
@Nullable
@Override
public PluginSettings getState() {
return this;
}
@Override
public void loadState(@NotNull PluginSettings state) {
XmlSerializerUtil.copyBean(state, this);
}
public static PluginSettings getInstance() {
return ApplicationManager.getApplication().getService(PluginSettings.class);
}
}
配置类定义了插件的各种设置项,比如模型路径、输出格式、缓存大小等。这些配置会自动保存到XML文件里,下次启动时自动加载。
7. 实际应用案例
7.1 扫描文档转代码注释
我最近在维护一个老项目,代码是五年前写的,文档只有纸质版的设计说明书。用这个插件,我把设计说明书扫描成PDF,然后直接转成了代码注释。
具体操作很简单:在PyCharm里打开项目,找到需要添加注释的类文件,右键选择“从文档添加注释”,选择扫描的PDF文件。插件会自动识别PDF里的相关内容,生成规范的注释。
比如原来这样的代码:
def calculate_price(quantity, unit_price, discount):
total = quantity * unit_price
if discount:
total = total * (1 - discount)
return total
处理后就变成了:
def calculate_price(quantity: int, unit_price: float, discount: float = 0.0) -> float:
"""
计算商品总价格
根据商品数量、单价和折扣率计算最终价格。
如果提供折扣率,会在总价基础上应用折扣。
参数:
quantity: 商品数量,必须为正整数
unit_price: 商品单价,单位为元,支持小数
discount: 折扣率,0-1之间的小数,如0.1表示9折
返回:
float: 计算后的总价格,保留两位小数
示例:
>>> calculate_price(10, 100.0, 0.1)
900.0
>>> calculate_price(5, 50.0)
250.0
"""
total = quantity * unit_price
if discount:
total = total * (1 - discount)
return round(total, 2)
注释不仅包含了参数说明、返回值说明,还有使用示例。对于团队协作特别有帮助,新同事接手代码时能快速理解函数用途。
7.2 技术规格书解析
另一个实用的场景是解析技术规格书。我们公司最近接了个第三方系统对接的项目,对方给了一份50页的API接口文档,全是扫描件。
用这个插件,我先把文档转成Markdown,然后解析出所有的API接口。插件能自动识别文档里的表格,提取出接口路径、请求方法、参数说明、返回格式。
解析出来的数据可以直接导入到Postman里测试,或者生成对应的客户端代码。对于前端开发来说,还能自动生成TypeScript的类型定义:
// 自动生成的接口定义
interface User {
id: number;
name: string;
email: string;
createdAt: string;
updatedAt: string;
}
interface CreateUserRequest {
name: string;
email: string;
password: string;
role?: string;
}
interface ApiResponse<T> {
code: number;
message: string;
data: T;
timestamp: string;
}
// 自动生成的API客户端
class ApiClient {
async createUser(request: CreateUserRequest): Promise<ApiResponse<User>> {
const response = await fetch('/api/users', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(request)
});
return response.json();
}
async getUser(id: number): Promise<ApiResponse<User>> {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
// 其他接口...
}
这样前后端开发就能基于同一份文档工作,减少沟通成本,也避免了手动编写接口定义时的错误。
7.3 项目文档自动化
对于大型项目,文档维护是个头疼的问题。代码更新了,文档没更新;文档更新了,代码又对不上。用这个插件可以建立代码和文档的关联。
我设置了一个定时任务,每周自动扫描项目里的代码变更,然后更新对应的文档。插件会分析代码的改动,比如新增了函数、修改了参数,然后在文档里相应位置更新说明。
还可以生成项目的API文档网站,类似Read the Docs那种。插件能提取所有公开的类、函数、方法,生成统一的文档页面,包括参数说明、返回值、使用示例等。
对于团队内部的知识管理也很有用。新员工入职时,不用再挨个问老同事这个函数是干嘛的、那个接口怎么用,直接看自动生成的文档就行。文档总是最新的,因为是从代码直接生成的。
8. 性能优化与最佳实践
8.1 处理速度优化
DeepSeek-OCR-2模型比较大,处理速度是需要考虑的问题。我做了几个优化:
批量处理。如果有多个文档要处理,不要一个个来,批量处理能显著提升效率。插件支持选择多个文件,一次性处理。
# 批量处理示例
def batch_process_files(file_paths: List[str], batch_size: int = 5):
"""批量处理文件,控制并发数量"""
results = []
for i in range(0, len(file_paths), batch_size):
batch = file_paths[i:i+batch_size]
batch_results = []
# 使用线程池并发处理
with ThreadPoolExecutor(max_workers=batch_size) as executor:
futures = []
for file_path in batch:
future = executor.submit(process_single_file, file_path)
futures.append(future)
for future in as_completed(futures):
try:
result = future.result(timeout=300) # 5分钟超时
batch_results.append(result)
except TimeoutError:
print(f"处理超时: {file_path}")
except Exception as e:
print(f"处理失败: {e}")
results.extend(batch_results)
return results
缓存机制。同样的文档处理第二次时,直接从缓存读取,不用再跑OCR。缓存键用文件内容的MD5,这样即使文件名变了,只要内容没变就能命中缓存。
import hashlib
import pickle
from pathlib import Path
class OCROache:
def __init__(self, cache_dir: str = ".ocr_cache"):
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(exist_ok=True)
def get_cache_key(self, file_path: str, options: dict) -> str:
"""生成缓存键"""
# 计算文件内容的哈希
with open(file_path, 'rb') as f:
file_hash = hashlib.md5(f.read()).hexdigest()
# 加上处理选项
options_str = str(sorted(options.items()))
key = f"{file_hash}_{hashlib.md5(options_str.encode()).hexdigest()}"
return key
def get(self, key: str):
"""从缓存获取结果"""
cache_file = self.cache_dir / f"{key}.pkl"
if cache_file.exists():
try:
with open(cache_file, 'rb') as f:
return pickle.load(f)
except:
return None
return None
def set(self, key: str, result):
"""保存结果到缓存"""
cache_file = self.cache_dir / f"{key}.pkl"
with open(cache_file, 'wb') as f:
pickle.dump(result, f)
增量处理。对于大型文档,没必要一次性处理完。可以分页处理,处理一页保存一页,即使中途中断了,也能从断点继续。
8.2 内存管理
OCR处理比较耗内存,特别是大文档。我做了几个内存优化的措施:
分块加载。处理大PDF时,不要一次性把所有页面都加载到内存,一页一页处理。
def process_large_pdf_safely(pdf_path: str, max_memory_mb: int = 500):
"""安全处理大型PDF,控制内存使用"""
import psutil
import gc
doc = fitz.open(pdf_path)
results = []
for page_num in range(len(doc)):
# 检查内存使用
memory_info = psutil.virtual_memory()
if memory_info.percent > 90: # 内存使用超过90%
print("内存使用过高,暂停处理")
time.sleep(5) # 等待内存释放
gc.collect() # 强制垃圾回收
# 处理当前页
page = doc[page_num]
result = process_page(page)
results.append(result)
# 及时释放资源
del page
if page_num % 10 == 0: # 每10页清理一次
gc.collect()
doc.close()
return results
图片压缩。OCR识别不需要太高分辨率的图片,适当压缩可以大幅减少内存占用。
def compress_image_for_ocr(image_path: str, max_size: tuple = (1024, 1024)):
"""压缩图片以节省内存"""
from PIL import Image
img = Image.open(image_path)
# 计算缩放比例
width, height = img.size
if width > max_size[0] or height > max_size[1]:
ratio = min(max_size[0] / width, max_size[1] / height)
new_size = (int(width * ratio), int(height * ratio))
img = img.resize(new_size, Image.Resampling.LANCZOS)
# 转换为灰度图(OCR通常不需要颜色信息)
if img.mode != 'L':
img = img.convert('L')
# 保存临时文件
temp_path = f"{image_path}_compressed.png"
img.save(temp_path, optimize=True, quality=85)
return temp_path
及时清理。处理完每个文档后,及时释放模型占用的内存。
class MemoryAwareOCRService:
def __init__(self):
self.model = None
self.tokenizer = None
def process_with_memory_control(self, image_path: str):
"""带内存控制的处理"""
import torch
# 按需加载模型
if self.model is None:
self._load_model()
try:
result = self.process_image(image_path)
return result
finally:
# 清理GPU缓存
if torch.cuda.is_available():
torch.cuda.empty_cache()
# 定期完全卸载模型
self._maybe_unload_model()
def _maybe_unload_model(self):
"""如果内存紧张,卸载模型"""
import psutil
memory_info = psutil.virtual_memory()
if memory_info.percent > 85: # 内存使用超过85%
print("内存紧张,卸载模型")
del self.model
del self.tokenizer
self.model = None
self.tokenizer = None
import gc
gc.collect()
if torch.cuda.is_available():
torch.cuda.empty_cache()
8.3 错误处理与用户体验
插件用了这么多年,我总结了一些提升用户体验的经验:
友好的错误提示。OCR可能失败的原因很多:文件格式不支持、图片质量太差、网络问题等。要给用户明确的错误提示,而不是一堆技术栈信息。
def user_friendly_error(error: Exception) -> str:
"""将技术错误转换为用户友好的提示"""
error_msg = str(error).lower()
if "unsupported format" in error_msg:
return "不支持的文件格式,请上传图片或PDF文件"
elif "network" in error_msg or "connection" in error_msg:
return "网络连接失败,请检查网络后重试"
elif "memory" in error_msg:
return "内存不足,请尝试处理较小的文件或关闭其他程序"
elif "cuda" in error_msg:
return "GPU错误,请尝试使用CPU模式"
elif "timeout" in error_msg:
return "处理超时,请尝试压缩图片或减少页数"
else:
return f"处理失败: {error_msg[:100]}..."
进度反馈。长时间操作一定要有进度提示,让用户知道还在运行,而不是卡死了。
// ProgressDialog.java
public class ProgressDialog extends JDialog {
private JProgressBar progressBar;
private JLabel statusLabel;
private JButton cancelButton;
private volatile boolean cancelled = false;
public ProgressDialog(Frame parent, String title) {
super(parent, title, true);
initUI();
}
private void initUI() {
setLayout(new BorderLayout(10, 10));
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
// 进度条
progressBar = new JProgressBar();
progressBar.setIndeterminate(true); // 不确定进度时用动画
// 状态标签
statusLabel = new JLabel("正在处理...");
statusLabel.setHorizontalAlignment(SwingConstants.CENTER);
// 取消按钮
cancelButton = new JButton("取消");
cancelButton.addActionListener(e -> {
cancelled = true;
setVisible(false);
});
JPanel contentPanel = new JPanel(new BorderLayout(10, 10));
contentPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
contentPanel.add(statusLabel, BorderLayout.NORTH);
contentPanel.add(progressBar, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
buttonPanel.add(cancelButton);
add(contentPanel, BorderLayout.CENTER);
add(buttonPanel, BorderLayout.SOUTH);
pack();
setLocationRelativeTo(getParent());
}
public void updateProgress(int progress, String status) {
SwingUtilities.invokeLater(() -> {
if (progress >= 0) {
progressBar.setIndeterminate(false);
progressBar.setValue(progress);
}
statusLabel.setText(status);
});
}
public boolean isCancelled() {
return cancelled;
}
}
撤销支持。自动生成的注释可能不完美,要支持一键撤销。
// 在编辑器中插入注释时,记录操作以便撤销
public void insertCommentWithUndo(Editor editor, String comment) {
Document document = editor.getDocument();
Project project = editor.getProject();
// 创建可撤销的操作
CommandProcessor.getInstance().executeCommand(project, () -> {
// 开始批量操作
document.startGuardedBlockChecking();
try {
int offset = editor.getCaretModel().getOffset();
// 插入注释
document.insertString(offset, "\n" + comment + "\n");
// 移动光标到合适位置
editor.getCaretModel().moveToOffset(offset + comment.length() + 2);
} finally {
document.stopGuardedBlockChecking();
}
}, "插入注释", "DeepSeekOCR");
}
配置导入导出。用户可能有多台电脑,或者团队要共享配置。支持配置的导入导出很方便。
public void exportSettings(Path exportPath) throws IOException {
PluginSettings settings = PluginSettings.getInstance();
Map<String, Object> config = new HashMap<>();
config.put("modelPath", settings.getModelPath());
config.put("outputFormat", settings.getOutputFormat());
config.put("defaultLanguage", settings.getDefaultLanguage());
config.put("enableCache", settings.isEnableCache());
config.put("cacheSize", settings.getCacheSize());
config.put("maxPages", settings.getMaxPages());
config.put("autoInsert", settings.isAutoInsert());
config.put("commentStyle", settings.getCommentStyle());
// 保存为JSON
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(exportPath.toFile(), config);
}
public void importSettings(Path importPath) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> config = mapper.readValue(importPath.toFile(), Map.class);
PluginSettings settings = PluginSettings.getInstance();
settings.setModelPath((String) config.getOrDefault("modelPath", settings.getModelPath()));
settings.setOutputFormat((String) config.getOrDefault("outputFormat", settings.getOutputFormat()));
// ... 其他设置
}
9. 总结
开发这个PyCharm插件的过程中,我最大的感受是:好的工具应该融入工作流,而不是增加额外步骤。DeepSeek-OCR-2本身已经很强大,但只有集成到开发环境里,才能真正发挥价值。
从实际使用效果来看,这个插件确实能显著提升文档处理效率。以前需要手动录入的文档,现在几分钟就能搞定;以前容易出错的API对接,现在有自动生成的代码框架;以前维护困难的文档,现在能和代码同步更新。
当然,工具不是万能的。OCR识别总有出错的时候,生成的注释也可能不够准确。我的经验是:对于重要的文档,自动生成后一定要人工检查一遍;对于简单的文档,可以直接使用。
技术总是在进步,DeepSeek-OCR-2还在不断更新,插件的功能也可以继续扩展。比如加入更多文档格式的支持,集成更多的代码生成模板,或者加入团队协作功能,让整个团队都能受益。
如果你也在为文档处理烦恼,不妨试试这个思路。不一定非要用我的实现,关键是找到适合自己工作流程的解决方案。好的工具应该让你更专注于创造性的工作,而不是重复性的劳动。
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。
更多推荐

所有评论(0)