React

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript类库 > React > React PDF文本提取

React结合WASM实现PDF文本提取功能

作者:Eiceblue

本文将教你用Spire.PDF for JavaScript(WASM)在React中实现纯前端PDF文本提取,文件无需上传服务器,直接本地解析并导出txt文件,保护隐私又减轻后端压力,附完整代码和部署方法,有需要的小伙伴可以参考下

PDF 文本提取是文档自动化场景里非常常见的需求。传统方案大多是后端服务读取 PDF 再返回文本,当面对大量用户、大体积 PDF 文件时,会带来服务器压力、文件上传带宽消耗等问题。

借助 WASM 技术,我们可以把 PDF 解析能力放到浏览器前端完成:文件不需要上传到任何服务器,所有解析逻辑在本地浏览器运行,隐私性更好,也减轻后端负担。

本文将使用 Spire.PDF for JavaScript(WASM),结合 React 实现一个本地 PDF 文本提取工具。用户只需要选择本地 PDF,前端直接解析并导出提取后的文本为 .txt 文件。

一、技术选型与运行原理

1.1 库的基本信息

Spire.PDF for JavaScript 是一个运行在浏览器端的 PDF 操作库,核心通过 WebAssembly 编译,提供 JavaScript API 用于加载、解析和操作 PDF 文档。它支持从 PDF 中提取文本、图片、表单数据等,本文仅使用其文本提取功能。

1.2 工作流程

用户选择 PDF 文件
    ↓
浏览器读取为 ArrayBuffer
    ↓
WASM 模块加载 PDF 文档
    ↓
逐页提取文本内容
    ↓
生成 TXT 文件并下载

整个过程在客户端完成,无需上传服务器。

二、项目搭建与文件准备

2.1 依赖文件

执行 npm i spire.office 获取所需资源,然后将以下文件放置在 React 项目的 public/ 目录下:

2.2 React 环境

使用 Create React App 或 Vite 创建的项目均可,本文示例基于 React 函数组件和 Hooks。

三、核心代码实现

3.1 模块加载(WASM 初始化)

由于 WASM 文件需异步加载,我们在 useEffect 中动态导入,并存储模块实例到 state。

useEffect(() => {
  (async () => {
    try {
      const publicUrl = process.env.PUBLIC_URL || '';
      // 动态导入 JS 胶水代码,webpackIgnore 避免打包处理
      const spireModule = await import(/* webpackIgnore: true */ `${publicUrl}/spire.pdf.js`);
      const rawModule = spireModule.default || spireModule;
      // 加载 WASM,指定 locateFile 以找到 .wasm 文件
      const loadedModule = typeof rawModule === 'function'
        ? await rawModule({ locateFile: p => p.endsWith('.wasm') ? `${publicUrl}/${p}` : p })
        : rawModule;
      setPdfModule(loadedModule);
    } catch (error) {
      console.error('加载 spire.pdf.js 失败:', error);
    }
  })();
}, []);

关键点:

3.2 文件选择与状态管理

使用 useRef 隐藏原生 <input>,通过自定义按钮触发选择,提升 UI 一致性。

const fileInputRef = useRef(null);

const handleFileChange = (e) => {
  const file = e.target.files[0];
  if (file && file.type === 'application/pdf') {
    setSelectedFile(file);
  } else {
    alert('请选择一个有效的 PDF 文件');
    e.target.value = '';
  }
};

const handleSelectFile = () => {
  fileInputRef.current.click();
};

3.3 文本提取函数

核心提取逻辑封装在 ExtractPDFText 中,流程如下:

const ExtractPDFText = async () => {
  if (!selectedFile) return;
  const spirepdf = pdfModule?.spirepdf;
  if (!spirepdf) {
    alert('模块尚未加载完成');
    return;
  }

  setIsExtracting(true);
  try {
    // 1. 读取文件为字节数组
    const arrayBuffer = await selectedFile.arrayBuffer();
    const uint8Array = new Uint8Array(arrayBuffer);

    // 2. 创建 PDF 文档对象并加载
    const pdf = new spirepdf.PdfDocument();
    pdf.LoadFromBytes(uint8Array);

    // 3. 配置提取选项(默认即可)
    const extractOptions = new spirepdf.PdfTextExtractOptions();
    let text = '';

    // 4. 遍历所有页面
    for (let i = 0; i < pdf.Pages.Count; i++) {
      const page = pdf.Pages.get_Item(i);
      const textExtractor = new spirepdf.PdfTextExtractor(page);
      text += textExtractor.ExtractText(extractOptions);
    }

    // 5. 生成 TXT 文件并触发下载
    const outputFileName = selectedFile.name.replace(/\.pdf$/i, '_提取结果.txt');
    const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);

  } catch (error) {
    console.error('提取失败:', error);
    alert('提取过程中发生错误,请查看控制台');
  } finally {
    setIsExtracting(false);
  }
};

注意:

四、UI 交互优化

为了更好的用户体验,我们添加了加载状态、按钮禁用、文件已选提示等。

4.1 状态定义

4.2 按钮状态控制

const isButtonDisabled = !selectedFile || isExtracting || !pdfModule;

4.3 样式设计(内联样式)

采用卡片居中布局,按钮使用主色 #1a73e8,并提供悬停和禁用样式。

完整样式代码见下方完整组件。

五、完整组件代码

以下为整合后的 App.js 文件,可直接复制使用(需提前放置好 spire.pdf.js 和 .wasm 文件在 public/ 目录)。

import React, { useState, useEffect, useRef } from 'react';

function App() {
  const [pdfModule, setPdfModule] = useState(null);
  const [selectedFile, setSelectedFile] = useState(null);
  const [isExtracting, setIsExtracting] = useState(false);
  const fileInputRef = useRef(null);

  // 加载 WASM 模块
  useEffect(() => {
    (async () => {
      try {
        const publicUrl = process.env.PUBLIC_URL || '';
        const spireModule = await import(/* webpackIgnore: true */ `${publicUrl}/spire.pdf.js`);
        const rawModule = spireModule.default || spireModule;
        const loadedModule = typeof rawModule === 'function'
          ? await rawModule({ locateFile: p => p.endsWith('.wasm') ? `${publicUrl}/${p}` : p })
          : rawModule;
        setPdfModule(loadedModule);
      } catch (error) {
        console.error('加载 spire.pdf.js 失败:', error);
        alert('PDF 库加载失败,请刷新重试');
      }
    })();
  }, []);

  const handleSelectFile = () => {
    fileInputRef.current.click();
  };

  const handleFileChange = (e) => {
    const file = e.target.files[0];
    if (file && file.type === 'application/pdf') {
      setSelectedFile(file);
    } else {
      alert('请选择一个有效的 PDF 文件');
      e.target.value = '';
    }
  };

  const ExtractPDFText = async () => {
    if (!selectedFile) {
      alert('请先选择一个 PDF 文件');
      return;
    }

    const spirepdf = pdfModule?.spirepdf;
    if (!spirepdf) {
      alert('WASM 模块尚未加载完成,请稍后重试');
      return;
    }

    setIsExtracting(true);
    try {
      const arrayBuffer = await selectedFile.arrayBuffer();
      const uint8Array = new Uint8Array(arrayBuffer);

      const pdf = new spirepdf.PdfDocument();
      pdf.LoadFromBytes(uint8Array);

      const extractOptions = new spirepdf.PdfTextExtractOptions();
      let text = '';

      for (let i = 0; i < pdf.Pages.Count; i++) {
        const page = pdf.Pages.get_Item(i);
        const textExtractor = new spirepdf.PdfTextExtractor(page);
        text += textExtractor.ExtractText(extractOptions);
      }

      const outputFileName = selectedFile.name.replace(/\.pdf$/i, '_提取结果.txt');
      const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = outputFileName;
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
      URL.revokeObjectURL(url);

    } catch (error) {
      console.error('提取文本失败:', error);
      alert('提取文本时发生错误,请查看控制台');
    } finally {
      setIsExtracting(false);
    }
  };

  // 样式对象
  const styles = {
    container: {
      display: 'flex',
      justifyContent: 'center',
      alignItems: 'center',
      minHeight: '100vh',
      backgroundColor: '#f0f2f5',
      fontFamily: 'Arial, sans-serif',
      padding: '20px',
    },
    card: {
      backgroundColor: '#fff',
      padding: '40px 50px',
      borderRadius: '16px',
      boxShadow: '0 8px 24px rgba(0,0,0,0.12)',
      maxWidth: '600px',
      width: '100%',
      textAlign: 'center',
    },
    title: {
      fontSize: '28px',
      fontWeight: '600',
      color: '#1a1a2e',
      marginBottom: '30px',
    },
    fileArea: {
      marginBottom: '30px',
    },
    customFileButton: {
      display: 'inline-block',
      padding: '10px 24px',
      backgroundColor: '#e8f0fe',
      color: '#1a73e8',
      borderRadius: '30px',
      fontWeight: '500',
      cursor: 'pointer',
      border: '1px solid #d2e3fc',
      transition: 'background 0.2s',
    },
    fileName: {
      marginTop: '12px',
      fontSize: '16px',
      color: '#333',
      wordBreak: 'break-all',
    },
    extractButton: {
      padding: '12px 40px',
      fontSize: '18px',
      fontWeight: '500',
      color: '#fff',
      backgroundColor: '#1a73e8',
      border: 'none',
      borderRadius: '30px',
      cursor: 'pointer',
      transition: 'background 0.3s, transform 0.1s',
      boxShadow: '0 2px 8px rgba(26,115,232,0.3)',
    },
    extractButtonDisabled: {
      backgroundColor: '#b0b0b0',
      cursor: 'not-allowed',
      boxShadow: 'none',
    },
    extractingText: {
      marginTop: '15px',
      fontSize: '16px',
      color: '#1a73e8',
    },
  };

  const isButtonDisabled = !selectedFile || isExtracting || !pdfModule;

  return (
    <div style={styles.container}>
      <div style={styles.card}>
        <h1 style={styles.title}>📄 PDF 文本提取器</h1>

        <div style={styles.fileArea}>
          <input
            type="file"
            accept=".pdf"
            onChange={handleFileChange}
            ref={fileInputRef}
            style={{ display: 'none' }}
          />
          <div
            style={styles.customFileButton}
            onClick={handleSelectFile}
            onMouseEnter={(e) => (e.currentTarget.style.backgroundColor = '#d2e3fc')}
            onMouseLeave={(e) => (e.currentTarget.style.backgroundColor = '#e8f0fe')}
          >
            📂 选择 PDF 文件
          </div>
          {selectedFile && (
            <div style={styles.fileName}>
              ✅ 已选择:<strong>{selectedFile.name}</strong>
            </div>
          )}
        </div>

        <button
          onClick={ExtractPDFText}
          disabled={isButtonDisabled}
          style={{
            ...styles.extractButton,
            ...(isButtonDisabled ? styles.extractButtonDisabled : {}),
          }}
          onMouseEnter={(e) => {
            if (!isButtonDisabled) {
              e.currentTarget.style.backgroundColor = '#1558b0';
            }
          }}
          onMouseLeave={(e) => {
            if (!isButtonDisabled) {
              e.currentTarget.style.backgroundColor = '#1a73e8';
            }
          }}
        >
          {isExtracting ? '⏳ 正在提取...' : '🚀 提取并下载文本'}
        </button>

        {isExtracting && (
          <div style={styles.extractingText}>处理中,请稍候…</div>
        )}
      </div>
    </div>
  );
}

export default App;

六、部署与运行

  1. 使用 npm start 启动开发服务器。
  2. 页面加载后会自动加载 WASM 模块,稍等片刻即可使用。
  3. 选择 PDF 文件,点击按钮提取文本,浏览器会自动下载 _提取结果.txt。

本文提供了一套基于 JavaScript 的 PDF 文本提取方案,涵盖 WASM 加载、文件读取、逐页提取和下载交互。该实现完全运行在浏览器端,不依赖后端服务,适用于文档预览、内容搜索、数据迁移等场景。

开发者可根据实际需求调整提取选项、增加错误处理或扩展为批量处理。代码结构清晰,易于集成到现有 React 项目中。

以上就是React结合WASM实现PDF文本提取功能的详细内容,更多关于React PDF文本提取的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文