当前位置: 首页 > article >正文

【实用部署教程】olmOCR智能PDF文本提取系统:从安装到可视化界面实现

文章目录

  • 引言
  • 系统要求
  • 1. 环境准备:安装Miniconda
    • 激活环境
  • 2. 配置pip源加速下载
  • 3. 配置学术加速(访问国外资源)
  • 4. 安装系统依赖
  • 5. 安装OLMOCR
  • 6. 运行OLMOCR处理PDF文档
  • 7. 理解OLMOCR输出结果
  • 9. 可视化UI界面
    • 9.1 安装界面依赖
    • 9.2 创建界面应用
    • 9.3 启动界面
  • 10. 常见问题及解决方案
    • 网络连接问题
    • GPU内存不足
  • 字体渲染问题
  • 11. 高级应用与优化
    • 针对不同GPU的优化
  • 总结

引言

OLMOCR是由Allen AI研究所(AI2)开发的一款强大的PDF文档处理工具,它结合了先进的光学字符识别(OCR)技术与大型语言模型能力,能够高效处理各类PDF文档,包括低质量扫描件、复杂格式的学术论文等。本文将详细介绍如何在高性能GPU环境下部署OLMOCR,帮助研究人员和开发者实现高效的文档内容提取与处理。
原图:
原图
提取出来的文本:
提取出来的内容

系统要求

在开始部署前,请确保您的系统满足以下条件:

  • GPU: 最新的NVIDIA GPU,如RTX 4090、L40S、A100或H100
  • 显存: 至少20GB的GPU RAM
  • 存储空间: 至少30GB可用磁盘空间
  • 操作系统: Linux(推荐Ubuntu)

1. 环境准备:安装Miniconda

首先,我们需要安装Miniconda并创建一个独立的Python环境:

# 下载并安装Miniconda
wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh
bash ~/miniconda.sh -b -p $HOME/miniconda
eval "$($HOME/miniconda/bin/conda shell.bash hook)"
echo 'export PATH="$HOME/miniconda/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

# 创建Python 3.11环境
conda create -n olmocr_ai python=3.11 -y

激活环境

conda activate olmocr_ai

如果遇到CondaError: Run 'conda init' before 'conda activate'错误,请执行:

conda init bash
source ~/.bashrc
conda activate olmocr_ai

2. 配置pip源加速下载

对于国内用户,建议配置国内镜像源加速依赖包的下载:

# 配置清华源
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
# 升级pip并清理缓存
python -m pip install --upgrade pip
pip cache purge
# 检查配置是否生效
pip config list

其他可选国内源:

  • 阿里云:http://mirrors.aliyun.com/pypi/simple/
  • 中国科技大学:https://pypi.mirrors.ustc.edu.cn/simple/
  • 华中科技大学:http://pypi.hustunique.com/simple/
  • 上海交通大学:https://mirror.sjtu.edu.cn/pypi/web/simple/

3. 配置学术加速(访问国外资源)

如需访问GitHub等国外资源,可以临时启用学术加速:

# 启用学术加速
source /etc/network_turbo
# 配置Hugging Face镜像
export HF_ENDPOINT=https://hf-mirror.com
# 使用完毕后关闭学术加速
# unset http_proxy && unset https_proxy

4. 安装系统依赖

OLMOCR依赖一些系统库,需要提前安装:

sudo apt-get update
sudo apt-get install poppler-utils ttf-mscorefonts-installer msttcorefonts fonts-crosextra-caladea fonts-crosextra-carlito gsfonts lcdf-typetools

注意:安装过程中如果出现MORE提示,按Enter键继续阅读,然后输入"yes"接受许可条款。

5. 安装OLMOCR

接下来,克隆OLMOCR代码库并安装:

# 克隆代码库
git clone https://github.com/allenai/olmocr.git
cd olmocr
# 安装OLMOCR
pip install -e .
# 安装特定版本依赖
pip install sgl-kernel==0.0.3.post1 --force-reinstall --no-deps
pip install "sglang[all]==0.4.2" --find-links https://flashinfer.ai/whl/cu124/torch2.4/flashinfer/ -i https://pypi.tuna.tsinghua.edu.cn/simple --timeout 1800

6. 运行OLMOCR处理PDF文档

现在,我们可以使用OLMOCR来处理PDF文件:

# 创建工作目录并处理PDF文件
python -m olmocr.pipeline ./localworkspace --pdfs tests/gnarly_pdfs/horribleocr.pdf
# 查看处理结果
cat localworkspace/results/output_*.jsonl

7. 理解OLMOCR输出结果

解析完成后的数据
解析完成后的数据
OLMOCR处理完PDF后,会生成JSONL格式的输出文件。下面是输出字段的详细解释:

{
  "id": "2d8a28b2aa386be36ff8d83135990cae1904257d",
  "text": "Christians behaving themselves like Mahomedans...", 
  "source": "olmocr",
  "added": "2025-03-19",
  "created": "2025-03-19",
  "metadata": {
    "Source-File": "tests/gnarly_pdfs/horribleocr.pdf",
    "olmocr-version": "0.1.60",
    "pdf-total-pages": 1,
    "total-input-tokens": 1809,
    "total-output-tokens": 433,
    "total-fallback-pages": 0
  },
  "attributes": {
    "pdf_page_numbers": [[0, 1643, 1]]
  }
}
  • id: 处理任务的唯一标识符,通常是基于输入文件内容生成的哈希值
  • text: 从PDF中提取的实际文本内容
  • source: 标识数据来源为"olmocr"系统
  • added/created: 记录添加和创建的时间戳
  • metadata:
    • Source-File: 原始PDF文件路径
    • olmocr-version: 使用的OLMOCR版本
    • pdf-total-pages: PDF文件的总页数
    • total-input-tokens: 处理过程中输入的token数量
    • total-output-tokens: 处理过程中输出的token数量
    • total-fallback-pages: 使用备用处理方法的页面数量(当主要方法失败时)
  • attributes:
    • pdf_page_numbers: 包含页码信息的数组,格式为[页码索引,字符偏移量,页数]

9. 可视化UI界面

为了让OLMOCR更加易用,我开发了基于Gradio的直观可视化界面,可以通过Web浏览器轻松上传、处理和分析PDF文档。这个界面不仅简化了操作流程,还提供了多种方式查看和验证处理结果。
可视化界面

9.1 安装界面依赖

首先,确保安装必要的依赖包:

pip install gradio pandas

9.2 创建界面应用

将以下代码保存为app.py文件放到olmocr根目录下:

import os
import json
import gradio as gr
import subprocess
import pandas as pd
from pathlib import Path
import shutil
import time
import re

# 创建工作目录
WORKSPACE_DIR = "olmocr_workspace"
os.makedirs(WORKSPACE_DIR, exist_ok=True)

# 应用主题色
PRIMARY_COLOR = "#2563eb"  # 蓝色主题
SECONDARY_COLOR = "#60a5fa"
BG_COLOR = "#f8fafc"
CARD_COLOR = "#ffffff"

def modify_html_for_better_display(html_content):
    """修改HTML以便在Gradio中更好地显示"""
    if not html_content:
        return html_content
    
    # 增加容器宽度
    html_content = html_content.replace('<div class="container">', 
                                       '<div class="container" style="max-width: 100%; width: 100%;">')
    
    # 增加文本大小和字体
    html_content = html_content.replace('<style>', 
                                       f'''<style>
                                       body {{
                                           font-family: "Source Sans Pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
                                           font-size: 16px;
                                           line-height: 1.6;
                                           color: #333;
                                           background-color: {BG_COLOR};
                                       }}
                                       .text-content {{
                                           font-size: 16px; 
                                           line-height: 1.6;
                                           color: #1e293b;
                                       }}
                                       ''')
    
    # 调整图像和文本部分的大小比例,添加阴影和圆角
    html_content = html_content.replace('<div class="row">', 
                                       '<div class="row" style="display: flex; flex-wrap: wrap; margin: 0 -15px;">')
    html_content = html_content.replace('<div class="col-md-6">', 
                                       '<div class="col-md-6" style="flex: 0 0 50%; max-width: 50%; padding: 15px;">')
    
    # 增加页面之间的间距,添加卡片效果
    html_content = html_content.replace('<div class="page">', 
                                       f'<div class="page" style="margin-bottom: 30px; border-radius: 8px; padding: 20px; background-color: {CARD_COLOR}; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">')
    
    # 增加图像样式
    html_content = re.sub(r'<img([^>]*)style="([^"]*)"', 
                         r'<img\1style="max-width: 100%; height: auto; border-radius: 4px; \2"', 
                         html_content)
    
    # 添加更美观的缩放控制
    zoom_controls = f"""
    <div style="position: fixed; bottom: 20px; right: 20px; background: {CARD_COLOR}; padding: 12px; 
                border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.15); z-index: 1000; display: flex; gap: 8px;">
        <button onclick="document.body.style.zoom = parseFloat(document.body.style.zoom || 1) + 0.1;" 
                style="background: {PRIMARY_COLOR}; color: white; border: none; padding: 8px 12px; border-radius: 4px; 
                       cursor: pointer; font-weight: 600; transition: all 0.2s;">
            <span style="font-size: 16px;">+</span>
        </button>
        <button onclick="document.body.style.zoom = 1;" 
                style="background: #64748b; color: white; border: none; padding: 8px 12px; border-radius: 4px; 
                       cursor: pointer; font-weight: 600; transition: all 0.2s;">
            <span style="font-size: 14px;">100%</span>
        </button>
        <button onclick="document.body.style.zoom = parseFloat(document.body.style.zoom || 1) - 0.1;"
                style="background: {PRIMARY_COLOR}; color: white; border: none; padding: 8px 12px; border-radius: 4px; 
                       cursor: pointer; font-weight: 600; transition: all 0.2s;">
            <span style="font-size: 16px;">-</span>
        </button>
    </div>
    """
    html_content = html_content.replace('</body>', f'{zoom_controls}</body>')
    
    return html_content

def process_pdf(pdf_file):
    """处理PDF文件并返回结果"""
    if pdf_file is None:
        return "请上传PDF文件", "", None, None
    
    # 创建一个唯一的工作目录
    timestamp = int(time.time())
    work_dir = os.path.join(WORKSPACE_DIR, f"job_{timestamp}")
    os.makedirs(work_dir, exist_ok=True)
    
    # 复制PDF文件
    pdf_path = os.path.join(work_dir, "input.pdf")
    shutil.copy(pdf_file, pdf_path)
    
    # 构建命令并执行
    cmd = ["python", "-m", "olmocr.pipeline", work_dir, "--pdfs", pdf_path]
    
    try:
        # 执行命令,等待完成
        process = subprocess.run(
            cmd,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
            check=True
        )
        
        # 命令输出
        log_text = process.stdout
        
        # 检查结果目录
        results_dir = os.path.join(work_dir, "results")
        if not os.path.exists(results_dir):
            return f"处理完成,但未生成结果目录\n\n日志输出:\n{log_text}", "", None, None
        
        # 查找输出文件
        output_files = list(Path(results_dir).glob("output_*.jsonl"))
        if not output_files:
            return f"处理完成,但未找到输出文件\n\n日志输出:\n{log_text}", "", None, None
        
        # 读取JSONL文件
        output_file = output_files[0]
        with open(output_file, "r") as f:
            content = f.read().strip()
            if not content:
                return f"输出文件为空\n\n日志输出:\n{log_text}", "", None, None
            
            # 解析JSON
            result = json.loads(content)
            extracted_text = result.get("text", "未找到文本内容")
            
            # 生成HTML预览
            try:
                preview_cmd = ["python", "-m", "olmocr.viewer.dolmaviewer", str(output_file)]
                subprocess.run(preview_cmd, check=True)
            except Exception as e:
                log_text += f"\n生成HTML预览失败: {str(e)}"
            
            # 查找HTML文件
            html_files = list(Path("dolma_previews").glob("*.html"))
            html_content = ""
            if html_files:
                try:
                    with open(html_files[0], "r", encoding="utf-8") as hf:
                        html_content = hf.read()
                        # 修改HTML以更好地显示
                        html_content = modify_html_for_better_display(html_content)
                except Exception as e:
                    log_text += f"\n读取HTML预览失败: {str(e)}"
            
            # 创建元数据表格
            metadata = result.get("metadata", {})
            meta_rows = []
            for key, value in metadata.items():
                meta_rows.append([key, value])
            
            df = pd.DataFrame(meta_rows, columns=["属性", "值"])
            
            return log_text, extracted_text, html_content, df
        
    except subprocess.CalledProcessError as e:
        return f"命令执行失败: {e.stderr}", "", None, None
    except Exception as e:
        return f"处理过程中发生错误: {str(e)}", "", None, None

# 自定义CSS
custom_css = f"""
:root {{
  --body-background-fill: {BG_COLOR};
  --background-fill-primary: {CARD_COLOR};
  --border-color-primary: #e2e8f0;
  --color-text-input: #334155;
  --block-background-fill: {CARD_COLOR};
  --block-label-background-fill: #f1f5f9;
  --block-label-text-color: #475569;
  --block-title-text-color: #1e293b;
}}

.contain {{
  max-width: 1200px;
  margin: 0 auto;
  padding: 2rem;
}}

/* 头部区域样式 */
.header-container {{
  background: linear-gradient(135deg, {PRIMARY_COLOR} 0%, {SECONDARY_COLOR} 100%);
  color: white;
  border-radius: 10px;
  padding: 1.5rem;
  margin-bottom: 2rem;
  box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
}}

/* 上传卡片样式 */
.upload-container {{
  background-color: {CARD_COLOR};
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
  padding: 1.5rem;
  border: 1px solid #e2e8f0;
}}

/* 标签页样式 */
.tabs {{
  background-color: {CARD_COLOR};
  border-radius: 8px !important;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
  overflow: hidden;
}}

/* 按钮样式 */
.primary-button {{
  background-color: {PRIMARY_COLOR} !important;
  border: none !important;
  box-shadow: 0 4px 6px rgba(37, 99, 235, 0.2) !important;
}}

.primary-button:hover {{
  background-color: #1d4ed8 !important;
  box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3) !important;
}}

/* 表格样式 */
table {{
  border-collapse: collapse;
  width: 100%;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
}}

th, td {{
  padding: 12px 15px;
  text-align: left;
  border-bottom: 1px solid #e2e8f0;
}}

th {{
  background-color: #f8fafc;
  font-weight: 600;
  color: #475569;
}}

tr:nth-child(even) {{
  background-color: #f8fafc;
}}

tr:hover {{
  background-color: #f1f5f9;
}}

/* HTML预览容器 */
#html_preview_container {{
  height: 800px;
  width: 100%; 
  overflow: auto;
  border: 1px solid #e2e8f0;
  border-radius: 8px;
  background-color: white;
}}

#html_preview_container iframe {{
  width: 100%;
  height: 100%;
  border: none;
}}

/* 说明卡片样式 */
.instruction-card {{
  background-color: {CARD_COLOR};
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
  padding: 1.5rem;
  margin-top: 2rem;
  border: 1px solid #e2e8f0;
}}

/* 特性卡片 */
.feature-card {{
  background-color: {CARD_COLOR};
  border-radius: 8px;
  padding: 1.5rem;
  margin: 1rem 0;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
  border-left: 4px solid {PRIMARY_COLOR};
}}

/* 提示和警告样式 */
.alert-info {{
  background-color: #eff6ff;
  border-left: 4px solid {PRIMARY_COLOR};
  padding: 1rem;
  border-radius: 4px;
  margin: 1rem 0;
}}

.alert-warning {{
  background-color: #fff7ed;
  border-left: 4px solid #f97316;
  padding: 1rem;
  border-radius: 4px;
  margin: 1rem 0;
}}
"""

# 创建Gradio界面
with gr.Blocks(title="OLMOCR PDF提取工具", css=custom_css) as app:
    # 头部区域
    with gr.Row(elem_classes="header-container"):
        with gr.Column(scale=3):
            gr.Markdown("# 📄 OLMOCR PDF文本智能提取工具")
            gr.Markdown("### 基于AI2最新OCR技术,高精度提取和分析PDF文档内容")
        with gr.Column(scale=1):
            gr.Markdown("![AI2 Logo](https://allenai.org/icons/ai2-logo.svg)")
    
    with gr.Row():
        # 左侧上传和控制区域
        with gr.Column(scale=1, elem_classes="upload-container"):
            gr.Markdown("## 📤 上传文档")
            pdf_input = gr.File(label="选择PDF文件", file_types=[".pdf"], elem_id="pdf_upload")
            
            with gr.Row():
                process_btn = gr.Button("🚀 开始处理", variant="primary", elem_classes="primary-button")
            
            # 添加功能说明
            with gr.Accordion("🔍 功能说明", open=False):
                gr.Markdown("""
                * 支持扫描PDF和数字PDF文件
                * 处理多语言文本内容
                * 保留文档原始布局和结构
                * 高精度识别表格和图表文字
                * 支持低质量扫描件处理
                """)
            
            # 添加处理状态指示
            gr.Markdown("### 📊 处理状态", elem_id="process_status")
            status_html = gr.HTML("""
            <div class="alert-info">
                <p><b>就绪</b> - 请上传PDF文件并点击处理按钮</p>
            </div>
            """)
            
        # 右侧结果显示区域
        with gr.Column(scale=2):
            # 使用更好看的标签页
            tabs = gr.Tabs(elem_classes="tabs")
            with tabs:
                with gr.TabItem("📝 提取文本", elem_id="text_tab"):
                    with gr.Column():  # 使用Column替代Box
                        text_output = gr.Textbox(
                            label="提取的文本内容", 
                            lines=20, 
                            interactive=True,
                            # 移除不兼容的show_copy_button
                        )
                
                with gr.TabItem("👁️ 视觉对比预览", elem_id="html_preview_tab"):
                    gr.Markdown("#### 原始PDF与提取文本对照预览")
                    # 使用更大的HTML组件
                    html_output = gr.HTML(elem_id="html_preview_container")
                
                with gr.TabItem("📋 文档元数据", elem_id="metadata_tab"):
                    with gr.Column():  # 使用Column替代Box
                        gr.Markdown("#### 文档处理元数据")
                        meta_output = gr.DataFrame()
                
                with gr.TabItem("🔧 处理日志", elem_id="log_tab"):
                    with gr.Column():  # 使用Column替代Box
                        log_output = gr.Textbox(
                            label="处理日志", 
                            lines=15, 
                            interactive=False,
                            # 移除不兼容的show_copy_button
                        )
    
    # 底部说明区域
    with gr.Row(elem_classes="instruction-card"):
        gr.Markdown("""
        ## 📚 使用指南
        
        <div class="feature-card">
        <h3>🔷 基本步骤</h3>
        <ol>
            <li>点击"选择PDF文件"上传您需要处理的PDF文档</li>
            <li>点击"开始处理"按钮启动OCR流程</li>
            <li>等待处理完成(根据文件大小可能需要几分钟)</li>
            <li>在标签页中查看提取的文本、视觉预览和元数据</li>
        </ol>
        </div>
        
        <div class="feature-card">
        <h3>🔷 关于视觉预览</h3>
        <ul>
            <li>视觉预览标签页展示原始PDF页面和提取的文本对照</li>
            <li>可以清晰查看OCR过程的识别精度</li>
            <li>使用右下角的缩放按钮调整预览大小</li>
        </ul>
        </div>
        
        <div class="alert-warning">
        <h4>⚠️ 注意事项</h4>
        <ul>
            <li>首次运行时会自动下载模型(约7GB),请确保网络畅通</li>
            <li>处理大型PDF文件时可能需要较长时间,请耐心等待</li>
            <li>对于复杂表格和特殊格式,可能需要额外调整提取的文本</li>
        </ul>
        </div>
        """)
    
    # 自定义JavaScript用于更新状态和界面交互
    gr.HTML("""
    <script>
    // 处理按钮点击时的状态更新
    document.addEventListener('DOMContentLoaded', function() {
        const processBtn = document.querySelector('.primary-button');
        const statusHTML = document.getElementById('process_status').nextElementSibling;
        
        if (processBtn) {
            processBtn.addEventListener('click', function() {
                statusHTML.innerHTML = `
                <div class="alert-info">
                    <p><b>处理中...</b> - 正在分析PDF文档,请耐心等待</p>
                </div>
                `;
            });
        }
    });
    </script>
    """)
    
    # 绑定按钮事件 - 使用阻塞模式
    def update_status():
        return """
        <div class="alert-info">
            <p><b>处理完成</b> - 请在右侧标签页查看结果</p>
        </div>
        """
    
    process_btn.click(
        fn=process_pdf,
        inputs=pdf_input,
        outputs=[log_output, text_output, html_output, meta_output],
        api_name="process"
    ).then(
        fn=update_status,
        outputs=[status_html]
    )

# 启动应用
if __name__ == "__main__":
    app.launch(
        server_name="0.0.0.0",  # 绑定到所有网络接口
        server_port=19851,      # 或其他端口
        share=False             # 禁用共享功能
        # 移除不兼容的favicon_path参数
    )

9.3 启动界面

保存代码后,在终端运行以下命令启动界面:

python app.py

启动成功后,您会看到类似以下输出:

Running on local URL:  http://0.0.0.0:19851

现在您可以通过浏览器访问 http://ip:port 来使用OLMOCR的可视化界面。

10. 常见问题及解决方案

网络连接问题

如果在下载模型或依赖时遇到网络问题,建议:

  • 确认已配置正确的镜像源
  • 尝试使用学术加速
  • 增加下载超时时间:pip install --timeout 1800 [包名]

GPU内存不足

  • 减小批处理大小
  • 处理较小的PDF片段
  • 考虑使用更大内存的GPU

字体渲染问题

如果PDF中的某些字体无法正确渲染,可能需要安装额外的字体包:

sudo apt-get install fonts-noto fonts-noto-cjk

11. 高级应用与优化

针对不同GPU的优化

  • RTX 4090: 适合处理中等规模的批量PDF文件,建议batch_size设置为2-4
  • A100/H100: 可以处理更大规模的PDF批量,可将batch_size提高到8或更高
  • 内存优化: 对于显存较小的GPU,可以使用–low_memory参数降低内存使用

总结

通过本教程,可以完成OLMOCR在高性能GPU环境下的部署和基本使用。OLMOCR作为一款结合OCR和大语言模型的工具,可以显著提高PDF文档处理的效率和准确性,特别适用于处理学术论文、技术文档等复杂格式的资料。

随着AI技术的不断发展,文档智能处理领域将会出现更多创新。OLMOCR提供了灵活的架构,允许研究者和开发者在此基础上进行扩展和定制,以满足特定领域的文档处理需求。

希望本文能帮助您顺利部署OLMOCR,提升文档处理效率!


http://www.kler.cn/a/592211.html

相关文章:

  • 计算机网络——总结
  • 分布式的消息流平台之Pulsar
  • 阿里云平台服务器操作以及发布静态项目
  • VBA常见的知识都有哪些,让AI编写的VBA经常 报错,所以VBA的基础还是要学习的
  • 西门子PLC
  • 88页手册上线 | 企业级本地私有化DeepSeek实战指南
  • 个人学习编程(3-19) leetcode刷题
  • 笔记本运行边缘计算
  • 【Qt】private槽函数可以被其他类中的信号连接
  • 算法岗学习路线
  • 【Python】12、函数-02
  • 告别传统直播单一性,智享三代 AI 无人直播开启定制时代
  • 2025-gazebo配置on vmware,wsl
  • android MutableLiveData setValue 响应速速 postValue 快
  • 网络爬虫【爬虫库urllib】
  • 织梦DedeCMS优化文章模版里的“顶一下”与“踩一下”样式
  • VulnHub-matrix-breakout-2-morpheus通关攻略
  • 启幕数据结构算法雅航新章,穿梭C++梦幻领域的探索之旅——二叉树序列构造探秘——堆的奥义与实现诗篇
  • redis缓存更新策略
  • Superagent 异步请求:如何处理复杂的 HTTP 场景