You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用PDFBox解析行数不定、行宽可变的用户银行对账单交易行?

Hey there! I’ve dealt with exactly this kind of bank statement parsing headache before—those variable line widths and unpredictable transaction lines are no joke. Let’s break down how to tackle this with PDFBox effectively:

Core Approach: Move Beyond Basic Text Extraction

The default TextStripper and TextArea tools work great for simple layouts, but bank statements need a more targeted approach that leverages text position metadata (coordinates, font, size) to identify and group transaction lines.


1. First, Map the PDF's Layout Patterns

Before writing code, analyze a few sample statements to spot consistent patterns:

  • Transaction lines often share a similar vertical spacing (Y-coordinate range)
  • Fields like date, description, and amount usually sit in fixed horizontal zones (even if line widths vary)
  • Non-transaction lines (headers, footers, totals) have distinct font sizes or positions

You can use PDFBox's PDFRenderer to render pages as images, or temporarily draw bounding boxes around text positions to visualize these zones (more on that later).


2. Build a Custom TextStripper to Track Transaction Lines

Instead of using the default TextStripper, extend it to track text positions by their Y-coordinates—this lets you group text into lines dynamically, even if line widths are variable. Here’s a working example:

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class TransactionStripper extends PDFTextStripper {
    private List<List<TextPosition>> transactionLines = new ArrayList<>();
    private List<TextPosition> currentLine = new ArrayList<>();
    private float lastY = -1;
    // Adjust this tolerance based on your PDFs (accounts for minor vertical shifts)
    private final float Y_TOLERANCE = 2.5f;

    public TransactionStripper() throws IOException {
        super();
    }

    @Override
    protected void writeString(String text, List<TextPosition> textPositions) throws IOException {
        for (TextPosition tp : textPositions) {
            float currentY = tp.getYDirAdj();
            
            // Check if we've moved to a new line (outside Y tolerance)
            if (lastY != -1 && Math.abs(currentY - lastY) > Y_TOLERANCE) {
                // Only add the line if it looks like a transaction
                if (isPotentialTransaction(currentLine)) {
                    transactionLines.add(new ArrayList<>(currentLine));
                }
                currentLine.clear();
            }
            currentLine.add(tp);
            lastY = currentY;
        }
    }

    // Custom filter to skip non-transaction lines (tweak this for your statements)
    private boolean isPotentialTransaction(List<TextPosition> line) {
        boolean hasNumericAmount = false;
        int totalTextLength = 0;
        
        for (TextPosition tp : line) {
            String text = tp.getUnicode().trim();
            totalTextLength += text.length();
            // Check for numbers (including decimals for amounts)
            if (text.matches(".*\\d+\\.?\\d*.*")) {
                hasNumericAmount = true;
            }
        }
        // Require a minimum length and a numeric value to filter headers/footers
        return hasNumericAmount && totalTextLength > 8;
    }

    public List<List<TextPosition>> getCleanTransactionLines() {
        // Add the final line after processing all text
        if (isPotentialTransaction(currentLine)) {
            transactionLines.add(currentLine);
        }
        return transactionLines;
    }
}

3. Parse Fields Using Horizontal Coordinates

Once you have grouped transaction lines, use each TextPosition's X-coordinate to split the line into fields (date, description, amount). Even if line widths vary, these fields usually stay within consistent horizontal ranges:

public static void parseTransactionFields(List<List<TextPosition>> transactionLines) {
    for (List<TextPosition> line : transactionLines) {
        StringBuilder date = new StringBuilder();
        StringBuilder description = new StringBuilder();
        StringBuilder amount = new StringBuilder();

        for (TextPosition tp : line) {
            float xPos = tp.getXDirAdj();
            String text = tp.getUnicode().trim();
            if (text.isEmpty()) continue;

            // Adjust these X ranges based on your PDF's layout
            if (xPos < 160) {
                date.append(text).append(" ");
            } else if (xPos < 480) {
                description.append(text).append(" ");
            } else {
                amount.append(text);
            }
        }

        // Output parsed data (replace with your storage logic)
        System.out.println("Date: " + date.toString().trim());
        System.out.println("Description: " + description.toString().trim());
        System.out.println("Amount: " + amount.toString().trim());
        System.out.println("------------------------");
    }
}

4. Handle Multi-Line Transactions

Some transactions have wrapped descriptions that span multiple lines. Add logic to merge these lines into a single transaction by checking if a line lacks a date field:

public static List<List<TextPosition>> mergeMultiLineTransactions(List<List<TextPosition>> rawLines) {
    List<List<TextPosition>> cleanedTransactions = new ArrayList<>();
    List<TextPosition> currentTransaction = null;

    for (List<TextPosition> line : rawLines) {
        boolean hasDate = false;
        // Check if the line contains a date pattern (adjust regex for your date format)
        for (TextPosition tp : line) {
            if (tp.getXDirAdj() < 160 && tp.getUnicode().matches("\\d{2}/\\d{2}/\\d{4}")) {
                hasDate = true;
                break;
            }
        }

        if (hasDate) {
            // Save the previous transaction if it exists
            if (currentTransaction != null) {
                cleanedTransactions.add(currentTransaction);
            }
            currentTransaction = new ArrayList<>(line);
        } else {
            // Append to the current transaction's description
            if (currentTransaction != null) {
                currentTransaction.addAll(line);
            }
        }
    }

    // Add the final transaction
    if (currentTransaction != null) {
        cleanedTransactions.add(currentTransaction);
    }
    return cleanedTransactions;
}

5. Quick Tips for Debugging & Adaptation

  • Visualize text positions: Use PDFBox's PDPageContentStream to draw rectangles around each TextPosition—this helps you fine-tune X/Y ranges.
  • Handle scanned PDFs: If your statements are image-based (not text-embedded), you’ll need to combine PDFBox with an OCR tool like Tesseract to extract text first.
  • Check for tagged PDFs: Some banks provide structured tagged PDFs—use PDDocument.isTagged() to check, and extract structured data directly if available.

内容的提问来源于stack exchange,提问作者Amit

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 08:54:36