原因
上一个项目,性能太差。如果把翻译的算力交给插件,比如:Chrome/Edge extension: Immersive Translate , 只需要图转文字就行
目标
采用多线程来提升性能,支持多种语言的图片PDF文件去转换成文字。
安装与配置
环境准备:
- 安装 python:3.12.3-slim
- 安装必要的库和工具:
- Flask(Web 框架)
- pytesseract(OCR 库)
- pdf2image(将 PDF 页面转换为图像)
- reportlab(生成 PDF)
- PyPDF2(处理 PDF)
- tesseract-ocr(需要系统安装)
- ocrmypdf (对 PDF 文件进行 OCR)
先在Windows 11上实现,再实现Linux Docker,以下是在Windows 11上的操作。
因为涉及多个安装包,要从不同地方下载,在代码执行时总缺少工具,这一文章用流水方式记录这段过程,
pytesseract(OCR 库)
项目:https://github.com/UB-Mannheim/tesseract/wiki
Windows 11有安装包下载(我写这篇时的最新版本):https://github.com/UB-Mannheim/tesseract/releases/download/v5.4.0.20240606/tesseract-ocr-w64-setup-5.4.0.20240606.exe
如果想得到更准确识别,还有训练过的语言包下载:GitHub - tesseract-ocr/tessdata: Trained models with fast variant of the "best" LSTM models + legacy models
我下载了简体中文包:chi_sim.traineddata,这个比安装文件自带的大多了41MB,我又下载了english 包。
下载后,文件放到:C:\Program Files\Tesseract-OCR\tessdata
(如果你安装它在上面目录)
Linux可以用sudo来安装,如:
sudo apt-get update
sudo apt-get install tesseract-ocr
sudo apt-get install tesseract-ocr-chi-sim
添加路径
pngquant
在执行时有报错:”ocrmypdf.exceptions.MissingDependencyError: Could not find program 'pngquant' on the PATH
“,发现 ocrmypdf 库,会调用 pngquant (pngquant
is a command-line utility and a library for lossy compression of PNG images. pngquant — lossy PNG compressor)
网上找到,还要摸索... pngquant
是一个用于压缩 PNG 图像的工具,ocrmypdf
在执行优化步骤时需要使用它。 下载bin文件,并放到有PATH覆盖的地方。我就把它放到,上面提及的安装文件的目录:C:\Program Files\Tesseract-OCR\
Ghostscript
在执行时有报错:ocrmypdf.exceptions.MissingDependencyError: Could not find program 'gswin64c' on the PATH
我也不知道它是什么,以下是查到的内容:
gswin64c 是什么?gswin64c 是 Ghostscript 的一个命令行版本的可执行文件。Ghostscript 是一个解释器,用于 PostScript 和 PDF 文件。它广泛用于 PDF 文件的查看、转换和打印等操作。
非商用这个:Postscript and PDF interpreter/renderer: GNU license for win64
Ghostscripthttps://www.ghostscript.com/releases/gsdnld.html
当前下载安装文件:
https://github.com/ArtifexSoftware/ghostpdl-downloads/releases/download/gs10040/gs10040w64.exe
验证命令
Microsoft Windows [Version 10.0.22631.4249]
(c) Microsoft Corporation. All rights reserved.
C:\Users\dave>gswin64c --version
10.04.0
C:\Users\dave>
gs.bat
这个很重要!!! 因为gswin64c.exe 替代了9.0的gs.exe需要手动制作一个执行文件
编辑gs.bat在 gswin64c.exe的目录(我的pc上默认安装在:”C:\Program Files\gs\gs10.04.0\bin“),并加入以下两行:
@echo off
"C:\Program Files\gs\gs10.04.0\bin\gswin64c.exe" %*
这样ocrmypdf
在Windows上寻找的是gs
命令,而实际上Ghostscript的可执行文件是gswin64c.exe
,通过一个批处理实现映射。
将来要放Docker里,Dockerfile也要同时更新依赖
# 基础镜像
FROM python:3.12.3-slim
# 安装必要的系统依赖
RUN apt-get update && apt-get install -y \
tesseract-ocr \
tesseract-ocr-chi-sim \
ghostscript \
pngquant \
libglib2.0-0 \
libsm6 \
libxrender1 \
libxext6 \
poppler-utils \
&& rm -rf /var/lib/apt/lists/*
小结:
对于Pngquant与Ghostscript 另一个解决办法是:禁用 ocrmypdf
的优化功能,只是文件大点。
目录结构
ipdf2tx/
│
├── app.py # Flask 应用程序主文件
├── ocr_util.py # OCR 功能模块
├── requirements.txt # Python 依赖列表
├── Dockerfile # Docker 配置文件
├── templates/
│ └── upload.html # 上传页面模板
├── uploads/ # 上传的 PDF 文件(自动创建)
└── outputs/ # 输出的 PDF 文件(自动创建)
- app.py:主 Flask 应用程序,负责处理 HTTP 请求、管理任务和与前端交互。
- ocr_util.py:负责 PDF 文件的 OCR 转换,包括分页、文本识别和 PDF 合并。
- templates/upload.html:前端上传界面,提供文件上传和语言选择功能。
- uploads/:用于存储用户上传的 PDF 文件。
- outputs/:用于存储转换后的可搜索 PDF 文件。
功能说明
ipdf2tx 用Python开发的跨平台Web应用程序,将基于图像的PDF文件(如扫描书籍)转换为带有可搜索文本层的PDF,同时保留原始图像,通过OCR功能实现。
主要功能
- PDF上传:用户可通过简洁友好的网页界面上传图像PDF文件。
- 多语言OCR支持:支持英语、简体中文、繁体中文、日文
- 多语言同时选择:用户可以在一次转换中选择多种语言,适用于包含多语言内容的PDF文件。
- 高效多进程处理:利用多核CPU的优势,加快大文件或多文件的转换速度。
- OCR转换:利用OCR技术,为每一页PDF添加文本层,实现文字的搜索和复制。
- 实时进度显示:转换过程中,前端界面动态显示转换进度,提升用户体验。
- PDF预览:转换完成后,用户可直接在浏览器中打开并阅读生成的可搜索PDF。
- 多平台支持:兼容Windows和Linux操作系统,满足不同用户的需求。
- Docker化部署:提供Docker容器化方案,简化部署流程,确保环境一致性。
- 端口:9005
技术使用
- Python 3.12.3 + Flask
- ocrmypdf:用于在PDF中添加OCR文本层的工具。
- PyPDF2:用于读取和操作PDF文件的Python库。
- 多进程处理:利用Python的
multiprocessing
模块,实现多页并行处理,大副提升转换速度。 - Docker:提供容器化部署方案,确保跨平台一致性和简化部署流程。
- 依赖工具:
- Tesseract OCR:开源OCR引擎,负责文本识别。
- Ghostscript:用于处理PDF文件的解释器。
- pngquant:用于PNG图像优化,减小PDF文件大小。 (测试PDF 2.63MB,转换后 0.97MB)
安装与配置
Windows 部署
参考上面的环境准备
Docker化部署
Dockerfile
# 使用官方的Python 3.12轻量级镜像作为基础镜像
FROM python:3.12.3-slim
# 设置环境变量,避免生成.pyc文件,并确保输出实时刷新
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
tesseract-ocr \
tesseract-ocr-chi-sim \
ghostscript \
pngquant \
poppler-utils \
&& rm -rf /var/lib/apt/lists/*
# 设置工作目录为/app
WORKDIR /app
# 复制并安装Python依赖
COPY requirements.txt /app/
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt
# 复制应用程序代码到容器中
COPY . /app/
# 创建必要的目录
RUN mkdir -p uploads outputs
# 暴露应用程序运行的端口
EXPOSE 9005
# 设置环境变量以指定Flask运行的主机和端口
ENV FLASK_RUN_HOST=0.0.0.0
ENV FLASK_RUN_PORT=9005
# 启动应用程序
CMD ["python", "app.py"]
requirements.txt
Flask
ocrmypdf
PyPDF2
创建与运行Docker镜像
创建Docker镜像
#在项目文件目录下:
docker build -t ipdf2tx .
运行Docker容器
docker run -d -p 9005:9005 --name ipdf2tx_container ipdf2tx
#如Docker所在主机支持多线程
docker run -d -p 9005:9005 --name ipdf2tx_container --cpus="4" ipdf2tx
打开浏览器,访问:
http://Docker-Container-IP:9005
完整代码
主程序 app.py
# app.py
from flask import Flask, request, send_file, render_template, Response, jsonify
import os
import uuid
from threading import Thread
from werkzeug.utils import secure_filename
from ocr_util import ocr_pdf
from collections import defaultdict
app = Flask(__name__)
app.config['UPLOAD_FOLDER'] = 'uploads'
# 确保上传文件夹存在
os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
# 确保输出文件夹存在
os.makedirs('outputs', exist_ok=True)
# 存储进度的全局变量
progress = defaultdict(int) # 使用defaultdict初始化为0
@app.route('/', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
file = request.files.get('file')
# 获取用户选择的语言,默认为英语
selected_langs = request.form.getlist('language') # 使用getlist以获取多选
if not selected_langs:
selected_langs = ['eng'] # 默认语言
lang = '+'.join(selected_langs) # 例如 'eng+chi_sim'
if file and file.filename.lower().endswith('.pdf'):
filename = secure_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath)
# 使用 UUID 生成唯一的任务 ID
task_id = str(uuid.uuid4())
progress[task_id] = 0 # 初始化进度为 0
# 定义进度回调函数
def progress_callback(p):
progress[task_id] = p
# 开始转换
def convert():
ocr_pdf(filepath, task_id, progress_callback, lang=lang)
# 启动一个新线程来进行转换
t = Thread(target=convert)
t.start()
return jsonify({'task_id': task_id}), 202 # 返回任务 ID
else:
return "请上传 PDF 文件。", 400
return render_template('upload.html')
@app.route('/progress/<task_id>')
def progress_stream(task_id):
def generate():
while True:
status = progress.get(task_id, 0)
yield f"data: {status}\n\n"
if status >= 100 or status == -1:
break
return Response(generate(), mimetype='text/event-stream')
@app.route('/view/<task_id>')
def view_pdf(task_id):
merged_pdf_path = os.path.join('outputs', f'{task_id}.pdf')
if os.path.exists(merged_pdf_path):
return send_file(merged_pdf_path)
else:
return "转换后的PDF不存在。", 404
if __name__ == '__main__':
# app.run(debug=True, port=9005) 如果不指定host, 默认是127.0.0.1这是个loop 0,不能与外面通信 modified at 1231am on 7oct.2024
app.run(host='0.0.0.0',debug=True, port=9005)
必要库
- os:与操作系统有关的,文件路径之类
- uuid:生成唯一的任务 ID。
- Thread:创建新线程,异步处理 OCR 任务。
- secure_filename:确保上传文件名的安全性
- ocr_pdf:从
ocr_util.py
导入的函数,负责 PDF 的 OCR 转换。 - defaultdict:用于存储任务进度,默认值为 0,进度条使用。
代码解释:
文件上传路由 (/)
- GET 请求:
- 渲染上传页面
upload.html
,展示文件上传表单。
- 渲染上传页面
- POST 请求:
- 文件获取:通过
request.files.get('file')
获取上传的文件。 - 语言获取:使用
request.form.getlist('language')
获取用户选择的所有语言复选框的值,返回一个列表。如果未选择任何语言,则默认使用英语 (['eng']
)。 - 语言参数格式:将选中的语言列表用
+
连接,如eng+chi_sim
,符合ocrmypdf
的多语言参数格式。 - 文件验证:检查上传的文件是否存在且为 PDF 格式。
- 文件保存:使用
secure_filename
确保文件名安全,然后保存到uploads/
目录。 - 任务 ID 生成:使用
uuid.uuid4()
生成唯一的task_id
,并初始化进度为 0。 - 进度回调函数:定义一个回调函数
progress_callback(p)
,用于更新当前任务的进度。 - 转换任务启动:定义一个函数
convert()
,调用ocr_pdf
进行 OCR 转换,并在新线程中启动该函数,避免阻塞主线程。 - 返回响应:通过
jsonify
返回任务 ID,并设置 HTTP 状态码为202 Accepted
,表示请求已被接受并正在处理中。 - 错误处理:如果上传的文件不符合要求,返回
400 Bad Request
错误。
- 文件获取:通过
进度查询路由 (/progress/<task_id>
)
- 定义一个生成器函数
generate()
,不断检查当前任务的进度,并通过yield
将进度值发送给前端。 - 当进度达到
100%
或转换失败(进度为-1
)时,停止推送。
查看转换后 PDF 路由 (/view/<task_id>
)
- 根据 task_id 查找并返回转换后的 PDF 文件。
- 如果对应的 PDF 文件存在,则通过 send_file 发送给客户端。
- 如果文件不存在,返回4 04 错误.
- 当前端接收到转换进度达到 100% ,用户点击“打开转换后的 PDF”按钮,前端会通过 /view/<task_id> 找到文件。
OCR 调ocr_util.py
负责 PDF 文件的 OCR 转换,包括分页、文本识别和 PDF 合并
# ocr_util.py
import os
import io
import PyPDF2
import ocrmypdf
from concurrent.futures import ProcessPoolExecutor, as_completed
from threading import Lock
import logging
import shutil
# 配置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
# 创建一个全局锁,用于同步进度更新
progress_lock = Lock()
def process_page(args):
"""
处理单页PDF,进行OCR转换。
Args:
args: Tuple containing (page_num, pdf_bytes, output_dir, lang)
Returns:
Tuple (page_num, success, error_message)
"""
page_num, pdf_bytes, output_dir, lang = args
try:
logging.info(f"开始处理第 {page_num + 1} 页")
page_input = os.path.join(output_dir, f'page_{page_num}.pdf')
page_output = os.path.join(output_dir, f'page_{page_num}_ocr.pdf')
# 提取单页 PDF
pdf_reader = PyPDF2.PdfReader(io.BytesIO(pdf_bytes))
pdf_writer = PyPDF2.PdfWriter()
pdf_writer.add_page(pdf_reader.pages[0])
with open(page_input, 'wb') as f_out:
pdf_writer.write(f_out)
# 对单页 PDF 进行 OCR 处理
ocrmypdf.ocr(
page_input,
page_output,
lang=lang, # 使用指定语言包
deskew=True,
force_ocr=True,
optimize=0,
progress_bar=False
)
logging.info(f"完成处理第 {page_num + 1} 页")
return (page_num, True, None)
except Exception as e:
logging.error(f"处理第 {page_num + 1} 页时出错: {e}")
return (page_num, False, str(e))
def ocr_pdf(filepath, task_id, progress_callback, lang='eng'):
"""
OCR处理PDF文件,添加进度回调。
Args:
filepath: Path to the input PDF
task_id: Unique task ID
progress_callback: Function to call with progress updates
lang: Language code(s) for OCR (e.g., 'eng', 'chi_sim', 'jpn')
"""
output_dir = os.path.join('outputs', task_id)
os.makedirs(output_dir, exist_ok=True)
try:
logging.info(f"开始处理任务 {task_id}")
# 读取PDF文件
with open(filepath, 'rb') as f:
pdf_reader = PyPDF2.PdfReader(f)
total_pages = len(pdf_reader.pages)
logging.info(f"总页数: {total_pages}")
# 准备所有页面的任务
tasks = []
for page_num in range(total_pages):
pdf_writer = PyPDF2.PdfWriter()
pdf_writer.add_page(pdf_reader.pages[page_num])
pdf_bytes_io = io.BytesIO()
pdf_writer.write(pdf_bytes_io)
pdf_bytes = pdf_bytes_io.getvalue()
tasks.append((page_num, pdf_bytes, output_dir, lang))
# 使用ProcessPoolExecutor进行多进程处理
with ProcessPoolExecutor(max_workers=os.cpu_count()) as executor:
future_to_page = {executor.submit(process_page, task): task[0] for task in tasks}
processed_pages = 0
for future in as_completed(future_to_page):
page_num = future_to_page[future]
try:
_, success, error = future.result()
if success:
with progress_lock:
processed_pages += 1
progress = int((processed_pages / total_pages) * 100)
progress_callback(progress)
logging.info(f"页面 {page_num + 1} 处理成功,当前进度: {progress}%")
else:
with progress_lock:
progress_callback(-1)
logging.error(f"页面 {page_num + 1} 处理失败,错误: {error}")
except Exception as exc:
with progress_lock:
progress_callback(-1)
logging.error(f"页面 {page_num + 1} 处理时发生异常: {exc}")
# 检查是否所有页面都成功处理
if processed_pages != total_pages:
raise Exception("部分页面处理失败,请检查日志。")
# 合并所有处理后的单页 PDF
logging.info(f"开始合并PDF文件为 {task_id}.pdf")
merged_pdf_path = os.path.join('outputs', f'{task_id}.pdf')
pdf_merger = PyPDF2.PdfWriter()
for page_num in range(total_pages):
page_output = os.path.join(output_dir, f'page_{page_num}_ocr.pdf')
with open(page_output, 'rb') as f_in:
page_reader = PyPDF2.PdfReader(f_in)
pdf_merger.add_page(page_reader.pages[0])
with open(merged_pdf_path, 'wb') as f_out:
pdf_merger.write(f_out)
logging.info(f"成功合并PDF文件为 {merged_pdf_path}")
progress_callback(100)
except Exception as e:
# 更新进度为-1表示失败
progress_callback(-1)
logging.error(f"任务 {task_id} 处理出错: {e}")
finally:
# 清理临时文件
logging.info(f"清理临时文件夹 {output_dir}")
shutil.rmtree(output_dir, ignore_errors=True)
logging.info(f"任务 {task_id} 完成")
必要库
-
PyPDF2:用于读取和写入 PDF 文件。
-
ocrmypdf:集成 Tesseract OCR 和 Ghostscript,实现 PDF 的 OCR 转换。
-
ProcessPoolExecutor、as_completed:用于多进程并行处理。
- Lock:线程锁,确保进度更新的线程安全。
代码解释
日志使用“全局锁”,在多进程环境下避免竞争
处理单页 PDF 的函数 (process_page
)
提取单页 PDF:
- 使用
PyPDF2.PdfReader
读取页面内容。 - 使用
PyPDF2.PdfWriter
创建一个新的 PDF,仅包含当前页面。 - 将单页内容写入
page_input
文件。
OCR 处理:
使用 ocrmypdf.ocr
对单页 PDF 进行 OCR 转换。
返回结果,包含:页编号、是否成功等
OCR 转换主函数 (ocr_pdf
)
参数:
filepath
:PDF 文件路径。task_id
:唯一的任务 ID,用于标识和跟踪。progress_callback
:回调函数,用于更新任务进度。lang
:OCR 识别的语言代码,支持多语言(如eng
,chi_sim
,jpn
)。
C:\Program Files\Tesseract-OCR\tessdata 用来存放“语言训练数据文件“, 可以在github上找到训练丰富的数据文件。 上面在pytenseract有提到。因为pdf可能含有多种语言文字,程序与支持多选,所在 lang = '+'.join(selected_langs) 可以多选语言。- 使用
ProcessPoolExecutor
并行处理每一页的 OCR 转换,利用CPU 多核提升效率。
我的NAS CPU:Intel(R) Celeron(R) J4125 CPU @ 2.00GHz 4物理核,没有超线程技术,即:4个物理核 = 4个逻辑cores
“ProcessPoolExecutor(max_workers=os.cpu_count())” 代码里没有限定CPU core数量,默认为全部。如果想保留2个,定义变量 reserved_cores=2, 让一个变量存系统的cpu个数:cpu_count = os.cpu_count(), 再让 max_worker=cpu_count-reserved_cores 。如果保留超出实际CPU数量,要有判断代码。 - 使用
PyPDF2.PdfWriter
将所有经过 OCR 转换的单页 PDF 文件合并为一个完整的可搜索 PDF 文件。
上传页面 upload.html
<!-- templates/upload.html -->
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>ipdf2tx - 上传 PDF 文件</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 40px;
}
#progress, #result, #error {
margin-top: 20px;
}
#progressValue {
font-weight: bold;
}
#viewButton {
padding: 10px 20px;
font-size: 16px;
}
.error {
color: red;
font-weight: bold;
}
</style>
</head>
<body>
<h1>ipdf2tx - 上传 PDF 文件</h1>
<form method="post" enctype="multipart/form-data" id="uploadForm">
<label for="fileInput">选择 PDF 文件:</label>
<input type="file" name="file" accept=".pdf" id="fileInput" required>
<br><br>
<label for="languageSelect">选择语言:</label>
<br>
<input type="checkbox" name="language" value="eng" id="lang_eng" checked>
<label for="lang_eng">英语 (English)</label><br>
<input type="checkbox" name="language" value="chi_sim" id="lang_chi_sim">
<label for="lang_chi_sim">简体中文 (Simplified Chinese)</label><br>
<input type="checkbox" name="language" value="chi_tra" id="lang_chi_tra">
<label for="lang_chi_tra">繁体中文 (Traditional Chinese)</label><br>
<input type="checkbox" name="language" value="jpn" id="lang_jpn">
<label for="lang_jpn">日语 (Japanese)</label><br>
<!-- 如果需要支持更多语言,可以继续添加复选框 -->
<br>
<input type="submit" value="上传并转换">
</form>
<div id="progress" style="display:none;">
<p>转换进度:<span id="progressValue">0%</span></p>
</div>
<div id="result" style="display:none;">
<button id="viewButton">打开转换后的 PDF</button>
</div>
<div id="error" class="error" style="display:none;">
转换失败,请重试。
</div>
<script>
const form = document.getElementById('uploadForm');
const progressDiv = document.getElementById('progress');
const progressValue = document.getElementById('progressValue');
const resultDiv = document.getElementById('result');
const viewButton = document.getElementById('viewButton');
const errorDiv = document.getElementById('error');
form.addEventListener('submit', function(e) {
e.preventDefault();
const fileInput = document.getElementById('fileInput');
const file = fileInput.files[0];
const languageCheckboxes = document.querySelectorAll('input[name="language"]:checked');
const selectedLangs = Array.from(languageCheckboxes).map(cb => cb.value);
let selectedLang = 'eng'; // 默认语言
if (selectedLangs.length > 0) {
selectedLang = selectedLangs.join('+'); // 例如 'eng+chi_sim'
}
if (!file) {
alert('请选择一个 PDF 文件。');
return;
}
const formData = new FormData();
formData.append('file', file);
formData.append('language', selectedLang); // 添加语言参数
// 发送上传请求
fetch('/', {
method: 'POST',
body: formData
})
.then(response => {
if (response.status === 202) {
return response.json();
} else {
throw new Error('上传失败');
}
})
.then(data => {
const taskId = data.task_id;
// 显示进度条
progressDiv.style.display = 'block';
errorDiv.style.display = 'none';
resultDiv.style.display = 'none';
progressValue.innerText = '0%';
// 开始监听进度
const progressSource = new EventSource(`/progress/${taskId}`);
progressSource.onmessage = function(event) {
const status = event.data;
if (status === '-1') {
progressValue.innerText = '转换失败,请重试。';
progressSource.close();
progressDiv.style.display = 'none';
errorDiv.style.display = 'block';
} else {
progressValue.innerText = `${status}%`;
if (status >= 100) {
progressSource.close();
resultDiv.style.display = 'block';
}
}
};
// 设置查看按钮的链接
viewButton.onclick = function() {
window.open(`/view/${taskId}`, '_blank');
};
})
.catch(error => {
console.error('发生错误:', error);
alert('上传或转换过程中发生错误,请重试。');
});
});
</script>
</body>
</html>
- 使用多个
<input type="checkbox">
元素,允许用户选择多种语言进行 OCR 处理。 - 默认选中英语 (
checked
)。 - 上传文件只接受pdf accept=".pdf
运行实例
1. 浏览器: http://127.0.0.1:9005
2. 选择pdf
3.点击"上传按钮"
因为上面还有一个VM在占用系统,乎略8cores是满的。 这里只是显示是多任务。
4.任务结束,可以点击"打开转换后的 PDF" 查看
5. 结果与对比
上图为转换后,文字是可以选择的。
下图是转换前,每页是图片形式
由于使用多进程,对系统的内存与cpu要求多一些。