解析银行对账单PDF

时间:2018-04-07 15:17:22

标签: java pdf pdfbox

我的用户有几张银行对帐单。我试图找出一种解析事务行的方法。我之前使用的是TextBox,TextStripper,但我不知道如何继续使用银行对帐单,因为它们的行数不确定,行可能也可能不是固定大小。

1 个答案:

答案 0 :(得分:1)

我在一个名为Apache Tika的开源项目的帮助下编写了这样一个解析器来解析我们的追逐pdf信用卡声明,以加快纳税准备时间。

只需要在pom.xml依赖项中包含tika和pdf解析器:

        <dependency>
            <groupId>org.apache.tika</groupId>
            <artifactId>tika-core</artifactId>
            <version>1.17</version>
        </dependency>
        <dependency>
            <groupId>org.apache.tika</groupId>
            <artifactId>tika-parsers</artifactId>
            <version>1.17</version>
        </dependency>

PDF提取器也非常简单:

import org.apache.tika.metadata.Metadata;
import org.apache.tika.parser.ParseContext;
import org.apache.tika.parser.pdf.PDFParser;
import org.apache.tika.sax.BodyContentHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.ContentHandler;

import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;


public class PdfExtractor {
    private static Logger logger = LoggerFactory.getLogger(PdfExtractor.class);

    public static void main(String args[]) throws Exception {
        StopWatch sw = new StopWatch();
        List<String> files = new ArrayList<>();
        files.add("C:/Users/m/Downloads/20170115.pdf");
        files.add("C:/Users/m/Downloads/20170215.pdf");
        files.add("C:/Users/m/Downloads/20170315.pdf");
        files.add("C:/Users/m/Downloads/20170415.pdf");
        files.add("C:/Users/m/Downloads/20170515.pdf");
        files.add("C:/Users/m/Downloads/20170615.pdf");
        files.add("C:/Users/m/Downloads/20170715.pdf");
        files.add("C:/Users/m/Downloads/20170815.pdf");
        files.add("C:/Users/m/Downloads/20170915.pdf");
        files.add("C:/Users/m/Downloads/20171015.pdf");
        files.add("C:/Users/m/Downloads/20171115.pdf");
        files.add("C:/Users/m/Downloads/20171215.pdf");
        files.add("C:/Users/m/Downloads/20180115.pdf");
        InputStream is;
        List<ChasePdfParser.ChaseRecord> full = new ArrayList<>();
        for (String fileName : files) {
            logger.info("Now processing " + fileName);
            is = new FileInputStream(fileName);
            ContentHandler contenthandler = new BodyContentHandler();
            Metadata metadata = new Metadata();
            PDFParser pdfparser = new PDFParser();
            pdfparser.parse(is, contenthandler, metadata, new ParseContext());
            String data = contenthandler.toString();
            List<ChasePdfParser.ChaseRecord> chaseRecords = ChasePdfParser.parse(data);
            full.addAll(chaseRecords);
            is.close();
        }
        logger.info("Total processing time: " + PrettyPrinter.toMsSoundsGood(sw.getTime()));
        full.forEach(cr -> System.err.println(cr.date + "|" + cr.desc + "|" + cr.amt));
    }
}

行解析器也相当简单,因为每行都有所有必要的信息,所以很容易解析它:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class ChasePdfParser {
    private static Logger logger = LoggerFactory.getLogger(ChasePdfParser.class);

    private static int FOR_TAX_YEAR = 2017;
    private static String YEAR_EXTENSION = "/" + FOR_TAX_YEAR;
    private static DateTimeFormatter check = DateTimeFormatter.ofPattern("MM/dd/uuuu");
    private static List<String> exclusions = new ArrayList<>(Arrays.asList("Payment Thank You", "AUTOMATIC PAYMENT"));

    public static List<ChaseRecord> parse(String data) {
        List<ChaseRecord> l = new ArrayList<>();
        for (String line : data.split("\n")) {
            if (line.isEmpty()) continue;
            String[] split = line.split("\\s");
            if (split == null || split.length == 0) continue;
            String test = split[0];
            if (!isMMDD(test)) continue;
            if(skip(line)) continue;
            if (split.length < 4) continue;
            ChaseRecord cr = new ChaseRecord();
            cr.date = extractDate(test);
            try {
                String last = split[split.length - 1];
                last = last.replaceAll(",", "");
                cr.amt = Double.parseDouble(last);
            } catch (NumberFormatException e) {
                e.printStackTrace();
            }
            cr.desc = String.join(" ", Arrays.copyOfRange(split, 1, split.length - 1));
            cr.desc = cr.desc.replaceAll("\\s\\s+", " ");
            l.add(cr);
        }
        return l;
    }

    private static boolean skip(String s) {
        if (s == null || s.isEmpty()) {
            return true;
        }
        for (String e : exclusions) {
            if (s.contains(e)) {
                return true;
            }
        }
        return false;
    }

    protected static LocalDate extractDate(String s) {
        if (!isMMDD(s)) {
            return null;
        }
        LocalDate localDate = LocalDate.parse(s + YEAR_EXTENSION, check);
        return localDate;
    }

    public static boolean isMMDD(String s) {
        if (s == null || s.isEmpty() || s.length() != 5) {
            return false;
        }
        try {
            s += YEAR_EXTENSION;
            LocalDate.parse(s, check);
            return true;
        } catch (Exception e) {
            return false;
        }
    }

    public static class ChaseRecord {
        public LocalDate date;
        public String desc;
        public Double amt;

        @Override
        public String toString() {
            return "ChaseRecord{" +
                    "date=" + date +
                    ", desc='" + desc + '\'' +
                    ", amt=" + amt +
                    '}';
        }
    }
}