Gianmarco Corradini

Using Regular Expressions in SAP by “Brute Force”

Gianmarco Corradini, 04.08.2026

Python Source Code

The following Python script searches accounting documents exported from SAP. It does not execute regular expressions inside SAP. Instead, it reads a local export and applies regex patterns to the downloaded journal entries.

The script supports both a CSV document list and a raw text journal. It can search references, document-header texts, complete journal documents, document headers, or individual booking sections.

"""
Search SAP accounting exports with Python regular expressions.

The script supports two common export formats:

1. A CSV document list containing:
   - Belegnummer
   - Geschäftsjahr
   - Referenz
   - Belegkopftext

2. A raw text journal in which accounting documents can be separated
   by a company-code marker and divided into a header and booking lines.

The searches are performed locally. No regex is executed inside SAP.
"""

from __future__ import annotations

import re
from dataclasses import dataclass
from pathlib import Path
from typing import Iterable, Iterator

import pandas as pd


# --------------------------------------------------
# Configuration
# --------------------------------------------------

CSV_FILE = Path("Belege_2.csv")
JOURNAL_FILE = Path("Journal_10.txt")

# Marker found at the beginning of each document in the raw journal.
# Replace this value with the relevant company code or document marker.
DOCUMENT_MARKER = "0569"

# The original export separates the document header from its booking
# lines with a value such as A123456.
HEADER_SEPARATOR_PATTERN = re.compile(r"A\d{6}")

CSV_COLUMNS = [
    "Belegnummer",
    "Geschäftsjahr",
    "Referenz",
    "Belegkopftext",
]


# --------------------------------------------------
# Data structure for raw journal documents
# --------------------------------------------------

@dataclass(frozen=True)
class JournalDocument:
    """Represent one accounting document from the raw journal export."""

    raw_text: str
    header: str
    bookings: str


# --------------------------------------------------
# General helper functions
# --------------------------------------------------

def compile_pattern(
    expression: str,
    *,
    ignore_case: bool = False,
    multiline: bool = True,
    dotall: bool = True,
) -> re.Pattern[str]:
    """
    Compile a regular expression with practical defaults for SAP exports.

    DOTALL allows the dot character to match line breaks, which is useful
    when an accounting document spans several lines.
    """

    flags = 0

    if ignore_case:
        flags |= re.IGNORECASE
    if multiline:
        flags |= re.MULTILINE
    if dotall:
        flags |= re.DOTALL

    return re.compile(expression, flags)


def contains_pattern(text: object, pattern: re.Pattern[str]) -> bool:
    """Return True when the compiled regex occurs in the supplied value."""

    return pattern.search(str(text)) is not None


# --------------------------------------------------
# CSV document-list search
# --------------------------------------------------

def load_document_list(file_path: Path) -> pd.DataFrame:
    """
    Load a CSV document list and validate the required SAP columns.

    dtype=str preserves leading zeroes in document numbers and references.
    keep_default_na=False prevents empty cells from becoming the string 'nan'.
    """

    dataframe = pd.read_csv(
        file_path,
        dtype=str,
        keep_default_na=False,
    )

    missing_columns = [
        column
        for column in CSV_COLUMNS
        if column not in dataframe.columns
    ]

    if missing_columns:
        missing = ", ".join(missing_columns)
        raise ValueError(f"Missing required CSV columns: {missing}")

    return dataframe[CSV_COLUMNS].copy()


def search_document_list(
    dataframe: pd.DataFrame,
    *,
    column: str,
    expression: str,
    ignore_case: bool = False,
) -> pd.DataFrame:
    """
    Return rows whose selected column matches a regular expression.

    pandas performs the search over the complete column, avoiding a slow
    row-by-row Python loop.
    """

    if column not in dataframe.columns:
        raise KeyError(f"Unknown search column: {column}")

    matches = dataframe[column].str.contains(
        expression,
        case=not ignore_case,
        regex=True,
        na=False,
    )

    return dataframe.loc[matches].copy()


def print_document_list(results: pd.DataFrame) -> None:
    """Print matching CSV rows in a readable table."""

    if results.empty:
        print("No matching documents found.")
        return

    print(results.to_string(index=False))


# --------------------------------------------------
# Raw journal parsing
# --------------------------------------------------

def load_text_file(file_path: Path) -> str:
    """
    Read a text export.

    utf-8-sig also accepts UTF-8 files that contain a byte-order mark.
    Replace the encoding if the SAP export uses another character set.
    """

    return file_path.read_text(
        encoding="utf-8-sig",
        errors="replace",
    )


def split_journal(
    journal_text: str,
    document_marker: str,
) -> list[str]:
    """
    Split the raw journal into document blocks while preserving the marker.

    re.escape treats the marker literally, even if it contains characters
    that otherwise have a special meaning in regular expressions.
    """

    marker_pattern = re.compile(
        rf"(?={re.escape(document_marker)})"
    )

    return [
        block.strip()
        for block in marker_pattern.split(journal_text)
        if block.strip()
    ]


def parse_journal_document(document_text: str) -> JournalDocument:
    """
    Divide one raw journal document into its header and booking section.

    If no separator is found, the complete document is retained as the
    header and the booking section is left empty.
    """

    parts = HEADER_SEPARATOR_PATTERN.split(
        document_text,
        maxsplit=1,
    )

    header = parts[0].strip()
    bookings = parts[1].strip() if len(parts) == 2 else ""

    return JournalDocument(
        raw_text=document_text,
        header=header,
        bookings=bookings,
    )


def load_journal_documents(
    file_path: Path,
    document_marker: str,
) -> list[JournalDocument]:
    """Load and parse all accounting documents in a raw journal export."""

    journal_text = load_text_file(file_path)
    blocks = split_journal(journal_text, document_marker)

    return [
        parse_journal_document(block)
        for block in blocks
    ]


# --------------------------------------------------
# Raw journal search
# --------------------------------------------------

def search_journal(
    documents: Iterable[JournalDocument],
    *,
    expressions: Iterable[str],
    area: str = "all",
    require_all: bool = True,
    ignore_case: bool = False,
) -> Iterator[JournalDocument]:
    """
    Yield journal documents that satisfy one or more regex conditions.

    area:
        "header"   searches only the document header;
        "bookings" searches only the booking lines;
        "all"      searches the complete document.

    require_all:
        True  requires every expression to match;
        False requires at least one expression to match.
    """

    valid_areas = {"header", "bookings", "all"}

    if area not in valid_areas:
        raise ValueError(
            f"area must be one of: {', '.join(sorted(valid_areas))}"
        )

    patterns = [
        compile_pattern(
            expression,
            ignore_case=ignore_case,
        )
        for expression in expressions
    ]

    if not patterns:
        raise ValueError("At least one regular expression is required.")

    for document in documents:
        searchable_text = {
            "header": document.header,
            "bookings": document.bookings,
            "all": document.raw_text,
        }[area]

        pattern_results = [
            contains_pattern(searchable_text, pattern)
            for pattern in patterns
        ]

        is_match = (
            all(pattern_results)
            if require_all
            else any(pattern_results)
        )

        if is_match:
            yield document


def print_journal_documents(
    documents: Iterable[JournalDocument],
) -> None:
    """Print matching journal documents with clear separators."""

    found = False

    for number, document in enumerate(documents, start=1):
        found = True

        print("=" * 80)
        print(f"MATCH {number}")
        print("=" * 80)
        print(document.raw_text)
        print()

    if not found:
        print("No matching journal documents found.")


# --------------------------------------------------
# Examples
# --------------------------------------------------

def run_csv_examples() -> None:
    """Run sample searches on the CSV document list."""

    documents = load_document_list(CSV_FILE)

    print("\nReferences containing exactly five digits and beginning with 2")
    print("-" * 70)

    reference_results = search_document_list(
        documents,
        column="Referenz",
        expression=r"^2\d{4}$",
    )

    print_document_list(reference_results)

    print("\nHeader texts containing the complete word 'Anwalt'")
    print("-" * 70)

    header_results = search_document_list(
        documents,
        column="Belegkopftext",
        expression=r"\bAnwalt\b",
        ignore_case=True,
    )

    print_document_list(header_results)


def run_journal_examples() -> None:
    """Run sample searches on the raw journal text export."""

    documents = load_journal_documents(
        JOURNAL_FILE,
        DOCUMENT_MARKER,
    )

    print("\nDocuments containing both account numbers")
    print("-" * 70)

    account_results = search_journal(
        documents,
        expressions=[
            r"\b69610035\b",
            r"\b69619999\b",
        ],
        area="all",
        require_all=True,
    )

    print_journal_documents(account_results)

    print("\nDocuments whose header contains 'TK'")
    print("-" * 70)

    header_results = search_journal(
        documents,
        expressions=[r"\bTK\b"],
        area="header",
        require_all=True,
        ignore_case=True,
    )

    print_journal_documents(header_results)

    print("\nDocuments whose booking lines contain 'TK'")
    print("-" * 70)

    booking_results = search_journal(
        documents,
        expressions=[r"\bTK\b"],
        area="bookings",
        require_all=True,
        ignore_case=True,
    )

    print_journal_documents(booking_results)


# --------------------------------------------------
# Program entry point
# --------------------------------------------------

def main() -> None:
    """
    Run the examples for files that are present.

    Missing example files are reported separately so that the user can
    use either the CSV search, the raw journal search, or both.
    """

    if CSV_FILE.exists():
        run_csv_examples()
    else:
        print(f"CSV file not found: {CSV_FILE.resolve()}")

    if JOURNAL_FILE.exists():
        run_journal_examples()
    else:
        print(f"Journal file not found: {JOURNAL_FILE.resolve()}")


if __name__ == "__main__":
    main()

Configuration

Place the exported files in the same directory as the Python script, or replace CSV_FILE and JOURNAL_FILE with their actual paths. The value assigned to DOCUMENT_MARKER must correspond to the company-code or document marker appearing at the beginning of each document in the raw journal export.

The CSV example expects the German SAP column names Belegnummer, Geschäftsjahr, Referenz, and Belegkopftext. These names can be changed in CSV_COLUMNS when a different export layout is used.

Search Examples

The first CSV example searches for references containing exactly five digits and beginning with 2:

expression=r"^2\d{4}$"

The second CSV example searches the document-header text for the complete word Anwalt, without distinguishing between uppercase and lowercase letters.

The raw-journal example returns only documents containing both account numbers 69610035 and 69619999. Additional examples show how to restrict a search to the document header or to the booking lines.

Main Improvements

The revised version separates file loading, parsing, searching, and printing into independent functions. CSV searches are vectorized with pandas instead of iterating through every row manually. Regular expressions are compiled once and reused, and the raw journal is represented by a small data class containing the complete document, its header, and its booking section.

The code also validates the CSV structure, preserves leading zeroes, handles missing files explicitly, avoids unrestricted try and except statements, and correctly searches each booking block individually.

Requirements

pip install pandas

The remaining modules used by the script are included in the Python standard library.