如何在上传的PDF文档中高亮匹配的清洗后引用字符串?
如何在上传的PDF文档中高亮匹配的清洗后引用字符串?
看起来你已经搞定了脏引用数据的清洗和拆分工作,接下来要做的就是在上传的PDF里定位这些干净的引用并高亮它们对吧?我来给你梳理下在Next.js项目里实现这个功能的具体步骤,结合你现有的代码逻辑来落地:
第一步:选对PDF处理工具,适配Next.js环境
前端处理PDF最常用的就是pdfjs-dist库,不过因为Next.js有服务端渲染的特性,咱们得把PDF相关的逻辑放在客户端组件里(用'use client'指令标记),避免服务端报错。先安装依赖:
npm install pdfjs-dist
第二步:加载PDF并提取带位置信息的文本
要实现高亮,光拿PDF的纯文本不够,得知道每个文本片段在PDF页面上的坐标(x、y、宽度、高度),这样才能准确叠加高亮层。咱们可以用pdfjs-dist的getTextContent()方法,它会返回每个文本块的详细位置信息:
'use client'; import { useEffect, useRef, useState } from 'react'; import * as pdfjsLib from 'pdfjs-dist'; import 'pdfjs-dist/build/pdf.worker.entry'; // 配置pdfjs的worker路径 pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdf.worker.entry.js'; // 直接复用你已有的清洗函数!保证PDF文本和引用的清洗规则一致,大幅提升匹配精度 const cleanCitationText = (text: string): string => { return text .replace(/\\n|\\r|\\t|\n|\r|\t/g, " ") .replace(/\\\\/g, " ") .replace(/\\/g, " ") .replace(/:-/g, "") .replace(/\s+/g, " ") .trim(); }; export default function CitationHighlighter({ citations }: { citations: string[] }) { const pdfContainerRef = useRef<HTMLDivElement>(null); const [pdfPages, setPdfPages] = useState<Array<{ canvas: HTMLCanvasElement; highlights: Array<{x: number, y: number, width: number, height: number}> }>>([]); // 处理用户上传的PDF文件 const handlePdfUpload = async (file: File) => { if (!pdfContainerRef.current) return; const arrayBuffer = await file.arrayBuffer(); const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise; const pagesData: typeof pdfPages = []; // 遍历PDF的每一页 for (let pageNum = 1; pageNum <= pdf.numPages; pageNum++) { const page = await pdf.getPage(pageNum); const viewport = page.getViewport({ scale: 1.5 }); // 缩放比例,兼顾清晰度和渲染速度 // 创建canvas用于渲染PDF页面 const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); if (!context) continue; canvas.width = viewport.width; canvas.height = viewport.height; await page.render({ canvasContext: context, viewport }).promise; // 提取当前页面的所有文本块(带位置信息) const textContent = await page.getTextContent(); const pageHighlights: Array<{x: number, y: number, width: number, height: number}> = []; // 把PDF文本块按你的规则清洗,同时保留原位置信息 const pageTextBlocks = textContent.items.map(item => ({ cleanedText: cleanCitationText(item.str), originalText: item.str, x: item.transform[4], y: viewport.height - item.transform[5], // 转换PDF坐标系(PDF默认y轴向上,和网页坐标系相反) width: item.width * viewport.scale, height: item.height * viewport.scale })); // 匹配咱们的清洗后引用 citations.forEach(targetCitation => { const cleanedTarget = cleanCitationText(targetCitation); // 这里用包含匹配+忽略大小写,你也可以根据需求改成正则精准匹配 pageTextBlocks.forEach(block => { if (block.cleanedText.toLowerCase().includes(cleanedTarget.toLowerCase())) { pageHighlights.push({ x: block.x, y: block.y, width: block.width, height: block.height }); } // 进阶优化:如果引用跨多个文本块,需要把相邻的匹配块合并成一个完整的高亮区域 }); }); pagesData.push({ canvas, highlights: pageHighlights }); } setPdfPages(pagesData); }; return ( <div> <input type="file" accept=".pdf" onChange={(e) => e.target.files && handlePdfUpload(e.target.files[0])} /> <div ref={pdfContainerRef} className="pdf-container"> {pdfPages.map((page, idx) => ( <div key={idx} style={{ position: 'relative', marginBottom: '20px' }}> <canvas style={{ border: '1px solid #eee' }} ref={el => { if (el) { el.width = page.canvas.width; el.height = page.canvas.height; el.getContext('2d')?.drawImage(page.canvas, 0, 0); } }} /> {/* 叠加高亮层,用半透明黄色标记匹配区域 */} {page.highlights.map((highlight, hIdx) => ( <div key={hIdx} style={{ position: 'absolute', left: `${highlight.x}px`, top: `${highlight.y}px`, width: `${highlight.width}px`, height: `${highlight.height}px`, backgroundColor: 'rgba(255, 255, 0, 0.3)', pointerEvents: 'none' // 不干扰PDF的点击、缩放等交互 }} /> ))} </div> ))} </div> </div> ); }
第三步:优化匹配逻辑,提升准确率
实际场景中可能遇到一些小问题,咱们可以针对性优化:
- 跨文本块的引用:如果一个完整的引用被PDF拆成了多个零散文本块,可以先把页面的文本块按顺序拼接成段落,匹配后再反向定位对应的文本块位置。
- 模糊匹配容错:比如用正则忽略多余空格、大小写差异:
new RegExp(cleanedTarget.replace(/\s+/g, '\\s+'), 'i')。 - 过滤无效匹配:复用你
parseCitationArray里的过滤规则,避免匹配到短文本或纯数字内容。
第四步:处理边界情况
- 大文件PDF加载慢:记得加loading状态提示用户。
- 跨页引用:在高亮时标注对应的页码,方便用户定位。
- 匹配精度问题:如果PDF里的引用和清洗后的文本差异较大,可以在匹配前对双方做更彻底的归一化(比如去掉所有标点、统一全半角)。
内容来源于stack exchange




