Java实战:高效提取PDF文件指定坐标的文本内容

前言

临时接到一个紧急需要处理的事项。业务侧一个同事有几千个PDF文件需要整理:需要从文件中的指定位置获取对应的编号和地址。

要的急,工作量大。所以就问到技术部有没有好的解决方案。

问技术的话就只能写个demo跑下了。

解决办法

1. 研究下PDF文档,找出解决方案

PDF的文档看起来比较简单,因为只是需要读取两个坐标位置的文本内容,而且位置相对固定。所以就直接用java的第三方库pdfbox来操作PDF文档。

2. 找个能操作PDF的第三方库pdfbox。

  1. 先下载pdfbox的jar包。
    官网介绍
  2. pdfbox能干啥:
    • pdfbox是Apache软件基金会的一个开源项目,它提供API和工具来处理PDF文档。

    • pdfbox是Apache PDFBox的Java版本,它提供了一个类库,用于读取,写入,转换和创建PDF文档。

    • pdfbox支持处理各种PDF特性,如文本,字体,图像,表单字段,注释,书签,页面布局等。

    • pdfbox还提供了对加密和数字签名PDF文档的支持,以及对PDF文档的提取和合并。

    • pdfbox还提供了对PDF文档的验证,签名验证,加密验证和数字签名的支持。

    • PDFBox是一个用于处理PDF文档的Java库。它提供了一组功能强大的API,可以用于创建、修改和提取PDF文档的内容。PDFBox可以用于各种用途,包括生成PDF文档、提取文本和图像、合并和拆分PDF文件、添加水印和书签等。

    • PDFBox支持处理各种PDF特性,如文本、字体、图像、表单字段、注释、书签、页面布局等。它还提供了对加密和数字签名PDF文档的支持,以及对PDF文档的高级操作,如提取文本位置信息、提取图像和字体等。

3. maven加载包

复制代码
      pdfbox有三个大的版本,每个版本差异较大,这个时候如果要引入的时候,要注意对应的版本了,否则demo就有可能跑不起来。
      ![pdfbox三个版本官方说明](https://img-blog.csdnimg.cn/3a822ec1571f4e088431d58704756781.png)
      pdfbox最新的大版本是3.0。作为新时代的青年,肯定要与时俱进。3.0肯定是要用上的。

3. 先验证下第三方库是否可行

下载jar包后,直接用java代码跑下demo。 demo读取pdf文档内容并输出文本数据到控制台

java 复制代码
    import org.apache.pdfbox.pdmodel.PDDocument;
    import org.apache.pdfbox.text.PDFTextStripper;

    import java.io.File;
    import java.io.IOException;
    public class PDFBoxDemo {
        public static void main(String[] args) throws IOException {
            PDDocument document = PDDocument.load(new File("D:\\pdf\\test.pdf"));
            PDFTextStripper stripper = new PDFTextStripper();
            String text = stripper.getText(document);
            System.out.println(text);
            document.close();
        }
    }

发现demo跑起来后,报错。

原因是因为demo是2.0的版本,而当前的jar包是3.0的版本。PDDocument.load这个修改为Loader.load就OK了。

接下来,就是如何获取到指定坐标位置的文本内容。

4. 确认文本在PDF文档中的坐标位置。

确认PDF文本坐标一般有两种方案。

1. 代码校验(最精准)

先用demo跑下,看下是否可以读取到指定坐标位置的文本内容。

java 复制代码
 /**
    * 获取文档坐标
    * @param  file PDF文件对象
    * @param sourceTex 匹配的字符
    * @return 坐标
    */
   public static Point getPoint(File file,String sourceTex) {
       Point point = new Point();
       //获取文档坐标
      try {
        PDDocument document =  Loader.loadPDF(file);
        PDFTextStripper textStripper = new PDFTextStripper() {
            @Override
            protected void writeString(String text, List<TextPosition> textPositions) throws IOException {
                if (text.contains(targetText)) {
                    TextPosition textPositionStart = textPositions.get(0);
                    TextPosition textPositionEnd = textPositions.get(textPositions.size()-1);
                    point.setX(textPositionStart.getX());
                    point.setY(textPositionStart.getY()); 
                }
            }
        };

        textStripper.setSortByPosition(true);
        textStripper.setStartPage(1);
        textStripper.setEndPage(document.getNumberOfPages());

        textStripper.getText(document);

        document.close();

        } catch (IOException e) {
            e.printStackTrace();
        }
        return point;
    }

跑完demo后,发现可以读取到指定坐标位置的文本内容。

这里会有个小问题,就是返回的坐标点有的会有小数。因为当前返回类型float,所以需要转换成int。

2. 最直接粗暴的方法。

复制代码
  1. 福昕PDF文档工具。
  2. 直接用福昕PDF文档定位工具定位坐标。
  说实话,开发比较少用这种方式,因为感觉有点lower(其实是自己不太会用)

5. 整个demo先验证第三方库是否可行。

拿1个文件试试水

java 复制代码
 public static void main(String[] args) {
        String filePath = "D:\\test\\test.pdf";
         try {
            PDDocument document = Loader.loadPDF(file);
            PDFTextStripperByArea  textStripper = new PDFTextStripperByArea ();
            Rectangle rectangle = new Rectangle(80,120, 250,10);
            String regionName = "regionName";
            textStripper.addRegion(regionName, rectangle);
            PDPage page = document.getPage(0);
            textStripper.extractRegions(page);
            String text = textStripper.getTextForRegion(regionName);
  
            System.out.println(text);
          
            textStripper.setSortByPosition(true);
            textStripper.setStartPage(1);
            textStripper.setEndPage(document.getNumberOfPages());
            textStripper.getText(document);
            document.close();
        }catch (IOException e) {
            e.printStackTrace();
        }
      
    }

结果能够正常输出对应的文本内容。

6. 整活上代码。

奉上全部demo代码

java 复制代码
package com.example.demo;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import com.alibaba.fastjson2.JSON;
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.PDFTextStripperByArea;
import org.apache.pdfbox.text.TextPosition;
import org.springframework.boot.test.autoconfigure.data.cassandra.DataCassandraTest;


import java.awt.*;
import java.awt.geom.Rectangle2D;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.stream.Collectors;

/**
 * Desc: 验证pdfbox的可行性
 *
 * @author admin
 * @date since 2023/8/8 18:44
 */

public class PdfDemo {
	//要匹配的位置内容点
    private  static final String[] target= {"name", "address"};
    public static void main(String[] args) {
       ExcelWriter excelWriter= ExcelUtil.getWriter("D:\\test\\pdf\\test.xls");
       String folderPath = "D:\\test\\pdf";
       File folder = new File(folderPath);
       if (folder.exists() && folder.isDirectory()) {
           List<Map<String,Object>>  mps =  listPdfFiles(folder);
           excelWriter.write(mps, true);
       } else {
           System.out.println("Invalid folder path.");
       }
       excelWriter.close();
    }
	/**
     * 获取pdf文件列表
     *
     * @param folder 文件夹
     * @return {@code List<Map<String,Object>>}
     */
    private static  List<Map<String,Object>>  listPdfFiles(File folder) {
        List<Map<String,Object>> mps = new ArrayList<>();
        File[] files = folder.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.isDirectory()) {
                    listPdfFiles(file); // 递归调用,处理子文件夹
                } else {
                    String fileName = file.getName();
                    if (fileName.toLowerCase().endsWith(".pdf")) {
                        mps.add(getLineData(file));
                    }
                }
            }
        }
        return mps;
    }
    /**
     * 行数据
     *
     * @param file 文件
     * @return {@code Map<String,Object>}
     */
    public static Map<String,Object> getLineData(File file){
        Map<String,Object> lineData = new HashMap<>(target.length+2);
        List<Point> pointList =  getPoint(file);
        String[]  arr=  getPointValue(file, pointList.stream().map(s -> new Rectangle(s.getX(), s.getY(), 260, 10)).toArray(Rectangle[]::new));
        if(arr.length>=target.length) {
        for(int i=0;i<target.length;i++)
        {
            lineData.put(target[i], arr[i]);
        }
            lineData.put("fileName", file.getName().toLowerCase().replace(".pdf", ""));
        }
      return lineData;
    }
 	/**
     * 获得PDF指定坐标点文本值
     *
     * @param file       文件
     * @param rectangles 矩形坐标
     * @return {@code String[]}
     */
    public  static String[] getPointValue( File file,Rectangle... rectangles){
        String[] textArr = new String[rectangles.length];
       // String text="";
        try {
            PDDocument document = Loader.loadPDF(file);
            PDFTextStripperByArea  textStripper = new PDFTextStripperByArea ();

            for(int i = 0; i < rectangles.length;i++   ) {
                Rectangle rectangle =rectangles[i];
                String regionName = "regionName"+rectangle.getX()+rectangle.getY();
                textStripper.addRegion(regionName, rectangle);
                PDPage page = document.getPage(0);
                textStripper.extractRegions(page);
                // 获取区域的text
                String text = textStripper.getTextForRegion(regionName);
                text = text.replace("\u0000","-").replace(" ","");
                System.out.println(">>text"+text);
                textArr[i]=text;
            }

            textStripper.setSortByPosition(true);
            textStripper.setStartPage(1);
            textStripper.setEndPage(document.getNumberOfPages());

            textStripper.getText(document);

            document.close();
        }catch (IOException e) {
            e.printStackTrace();
        }

        return  textArr;
    }

    public  static List<Point> getPoint( File file){
        List<Point> pointList=new ArrayList<>();
        try {
        PDDocument document =  Loader.loadPDF(file);
        PDFTextStripper textStripper = new PDFTextStripper() {
            @Override
            protected void writeString(String text, List<TextPosition> textPositions) throws IOException {
                for(String target:target){
                    if (text.contains(target)) {
                        Point point = new Point();
                        TextPosition textPositionEnd = textPositions.get(textPositions.size() - 1);
                        point.setX((int) textPositionEnd.getEndX());
                        point.setY((int) textPositionEnd.getY());
                        pointList.add(point);
                    }
                }
            }
        };

        textStripper.setSortByPosition(true);
        textStripper.setStartPage(1);
        textStripper.setEndPage(document.getNumberOfPages());
        textStripper.getText(document);
        document.close();

        } catch (IOException e) {
            e.printStackTrace();
        }

        System.out.println(">>>>>pointList" + JSON.toJSONString(pointList));
        return pointList;
    }
} 

7. 验证代码可行性

整理出来的excel,检查里面有些空格没有处理,就让业务自己批量替换一下。

因为代码只是一次性用的,就没有怎么进行封装了。总体来讲业务同事比较满意。

结论

  1. 第三方库pdfbox可以操作PDF文档。3.0版本之后和历史版本相差比较大,最好先阅读下源码。
  2. 坐标定位的话,可以用第三方也可以代码定位
  3. 如果代码后续想复用的话,最好抽离出公共方法
  4. 文件比较多的情况下,建议增加多线程处理。
相关推荐
奋进的芋圆16 分钟前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin33 分钟前
设计模式之桥接模式
java·设计模式·桥接模式
model200533 分钟前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉1 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国1 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882481 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈2 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_992 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc
沛沛老爹2 小时前
Java泛型擦除:原理、实践与应对策略
java·开发语言·人工智能·企业开发·发展趋势·技术原理
专注_每天进步一点点2 小时前
【java开发】写接口文档的札记
java·开发语言