PDFs automatisch in txt umwandeln und auswerten

Nachdem ich vor einiger Zeit mal um Hilfe für die Sprachenauswahl zur COM-Programmierung gesucht habe, einige Tage später endlich ein paar Lösungsansätze gefunden habe, ist der Quälcode nun fertig.

Geschrieben habe ich es in VB.NET. Die Programmierer streiten sich sehr ausgelassen darüber, ob nun C# oder VB.NET die bessere Lösung ist.
Für meinen Teil haben beide Sprache ihre Berechtigung, wobei VB.NET nochmal schwieriger zu verstehen ist, wenn man schon C gelernt hatte.

Doch genug gequatscht, hier mal der Code:

Imports System.IO                           'Ein / Ausgabe
Imports System.Text.RegularExpressions      'Regex
Imports Microsoft.Office.Interop            'Excel

Module Module1

    Dim objDateiLeser As StreamReader

    'Übergabeparameter: Pfad zur PDF und zur Ausgabe.txt
    Sub Pdf2Txt(ByVal pdfFile As String, ByVal txtFile As String)
        Dim oProcess As Process
        Dim arguments As String = "-layout" & " " & pdfFile & " " & txtFile
        oProcess = System.Diagnostics.Process.Start("T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\apps\pdf\pdftotext.exe", arguments)
        oProcess.WaitForExit()      'Warten bis pdf2text fertig. :D 
    End Sub

    Sub Main()

        '==============================================================================
        'ALLE PDF DATEIEN IN EINEM bestimmten VERZEICHNIS AUSLESEN UND IN TXT UMWANDELN
        Dim sPath As String = "T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\"              'Pfadangabe
        Dim oDir As New DirectoryInfo(sPath)                'Directory Object von System.IO
        Dim oFiles As FileInfo() = oDir.GetFiles("*.pdf")   'Nur PDFs suchen
        Dim oFile As FileInfo                               'Um später das Array oFiles auszelesen
        Dim pdf As String
        Dim i As Integer = 0 'Zähler für alle PDFs


        'pdf2text aufrufen und Textdatei erzeugen
        For Each oFile In oFiles
            pdf = oFile.ToString
            Dim pdfverz As String = Chr(34) & sPath & pdf & Chr(34) 'Chr(34) für das Anführungszeichen
            Dim ausverz As String = sPath & "ausgabe" & i & ".txt"  'txt verzeichnis
            Call Pdf2Txt(pdfverz, ausverz)
            i = i + 1
        Next

        Console.WriteLine("PDFs wurden in TXT umgewandelt. Nun folgt der BPNexT Export.")

        Dim j As Integer = i        'wird später noch gebraucht beim TXT AUSLESEN

        i = 0

        'PDF ins Archiv verschieben
        For Each oFile In oFiles
            pdf = oFile.ToString
            File.Move(sPath & pdf, sPath & "\Archiv\" & pdf)
            i = i + 1
        Next

        Console.WriteLine("PDFs wurden ins Archiv verschoben.")

        'PDF ENDE
        '========

        '==========================================
        'bpStarter aufrufen und export.xls erzeugen
        Dim oProcess As Process
        Dim arguments As String = "-once"
        oProcess = System.Diagnostics.Process.Start("T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\apps\bpstarter\bpStarter.exe", arguments)
        oProcess.WaitForExit()
        Console.WriteLine("export.xls wurde aus BüroPlus gezogen.")
        Console.WriteLine("REGEX Prozedur wird eingeleitet")
        'bpStarter ENDE
        '==============

        '=================================================================================================
        'EXCEL DEKLARIEREN, NOTWENDIGE ZÄHLER FÜR DIE EINZELNEN SPALTEN UND SPALTENÜBERSCHRIFTEN SCHREIBEN
        'Zähler für Zeilen
        Dim zählerb As Integer = 2

        Dim exWB As Excel.Workbook      'Datei
        Dim exWS As Excel.Worksheet     'Arbeitsblatt
        Dim exAPP As Excel.Application  'Excel Anwendung

        exAPP = New Excel.Application
        exAPP.Visible = False           'Excel unsichtbar / sichtbar
        exWB = exAPP.Workbooks.Add()    'Neue Datei erstellen
        exWS = exWB.ActiveSheet
        exAPP.DisplayAlerts = False     'Hinweise, dass die Datei bereits existiert unterdrücken und automatisch überschreiben

        'Überschriften in Datei schreiben
        exWS.Range("A1").Value = "BL-Nummer"
        exWS.Range("B1").Value = "Auftragsnummer"
        exWS.Range("C1").Value = "Wieland-Nummer"
        exWS.Range("D1").Value = "h-team-Nr"
        exWS.Range("E1").Value = "PE"
        exWS.Range("F1").Value = "EK-Preis"
        exWS.Range("G1").Value = "Lieferdatum"
        'ENDE EXCEL
        '==========

        '==================
        'REGEX DEFINITIONEN

        'Allgemeiner Matchstring
        Dim re1 As String = ".*?"           'Non-greedy match on filler

        'BL-Nummer Beispiel: BL0413412
        Dim re2 As String = "(BL[0-9]{7})"    'Word 1
        Dim r As Regex = New Regex(re1 + re2, RegexOptions.IgnoreCase Or RegexOptions.Singleline)

        'Auftragsnummer
        Dim re5 As String = "(Nummer:      [0-9]{7,})"    'Word 4
        Dim r3 As Regex = New Regex(re1 + re5, RegexOptions.IgnoreCase Or RegexOptions.Singleline)

        'Wieland Artikelnummer Beispiel: 56.702.7053.0

        Dim re3 As String = "((\w{1}\d{1}|[0-9]{2})\.[0-9]{3}\.[0-9]{4}\.[0-9]{1})"    'Word 2
        Dim r1 As Regex = New Regex(re1 + re3, RegexOptions.IgnoreCase Or RegexOptions.Singleline)

        'h-team Nummer
        Dim re4 As String = "(Ihre Artikelnummer: [0-9]{5,})"    'Word 3
        Dim r2 As Regex = New Regex(re1 + re4, RegexOptions.IgnoreCase Or RegexOptions.Singleline)

        'PE
        Dim re8 As String = "(?< =Positionsnetto.+ EUR.+) [0-9]{1,3}" 'word 7
        Dim r6 As Regex = New Regex(re8, RegexOptions.IgnoreCase Or RegexOptions.Singleline)

        'Preis
        Dim re7 As String = "(?<=Positionsnetto.+ ST\s+)([0-9]{1,3}.)*[0-9]{1,3},[0-9]{2}"   'word 5
        Dim r4 As Regex = New Regex(re7, RegexOptions.IgnoreCase Or RegexOptions.Singleline)

        'Liefertermin
        Dim re6 As String = "(?<=Liefertermin )(\(Tag\): \d{2}\.\d{2}\.\d{4})|(unbest){1}"   'word 6
        Dim r5 As Regex = New Regex(re6, RegexOptions.IgnoreCase Or RegexOptions.Singleline)


        'ENDE REGEX DEFINITIONEN
        '=======================

        '==================================================================================
        'SCHLEIFE UM ALLE ERZEUGTEN TXT DATEIEN EINZULESEN UND REGEX DRÜBERLAUFEN ZU LASSEN

        For i = 0 To j - 1      '

            Dim txt As String
            'erzeugte Datei öffnen
            objDateiLeser = New StreamReader(sPath & "ausgabe" & i & ".txt")

            '===================================================================
            'SCHLEIFE UM VERGLEICHEN MIT REGEX AUF MUSTER UND SCHREIBEN IN EXCEL

            Do
                txt = objDateiLeser.ReadLine

                Dim m As Match = r.Match(txt)       'BL-Nummer
                Dim m3 As Match = r3.Match(txt)     'Auftragsnummer
                Dim m1 As Match = r1.Match(txt)     'Wieland Nummer
                Dim m2 As Match = r2.Match(txt)     'h-team Nummer
                Dim m6 As Match = r6.Match(txt)     'PE
                Dim m4 As Match = r4.Match(txt)     'Preis
                Dim m5 As Match = r5.Match(txt)     'Liefdatum

                Dim BL As String
                Dim Auftragsnr As String

                If (m.Success) Then                 'BL-Nummer
                    Dim word1 As Group = m.Groups(1)
                    'Console.WriteLine(word1.ToString)
                    exWS.Range("A" & zählerb).Value = word1.ToString
                    BL = word1.ToString
                End If

                If (m3.Success) Then                'Auftragsnummer
                    Dim word4 As Group = m3.Groups(1)
                    'erste dreizehn Zeichen sprich "Nummer:     " abschneiden
                    Dim aufnr As String = word4.ToString.Remove(0, 13)
                    exWS.Range("B" & zählerb).Value = aufnr
                    Auftragsnr = aufnr
                End If

                If (m1.Success) Then                'Wieland Nummer
                    Dim word2 As Group = m1.Groups(1)
                    exWS.Range("C" & zählerb).Value = word2.ToString
                    exWS.Range("A" & zählerb).Value = BL 'BL-Nummer in Spalte A kopieren
                    exWS.Range("B" & zählerb).Value = Auftragsnr 'BL-Nummer in Spalte A kopieren
                    zählerb = zählerb + 1
                End If

                If (m2.Success) Then                'h-team Nummer
                    Dim word3 As Group = m2.Groups(1)
                    'erste zwanzig Zeichen sprich "Ihre Artikelnummer: " abschneiden
                    Dim hteam As String = word3.ToString.Remove(0, 20)
                    exWS.Range("D" & zählerb - 1).Value = hteam
                End If

                If (m6.Success) Then                'PE
                    Dim word7 As Group = m6.Groups(0)
                    Dim hteam As String = word7.ToString
                    exWS.Range("E" & zählerb - 1).Value = hteam
                End If

                If (m4.Success) Then                'Preis
                    Dim word5 As Group = m4.Groups(0)
                    Dim hteam As String = word5.ToString
                    exWS.Range("F" & zählerb - 1).Value = hteam
                End If

                If (m5.Success) Then                'Liefertermin
                    Dim word6 As Group = m5.Groups(0)
                    Dim treffer As String = word6.ToString
                    If treffer = "unbest" Then      ' bis zum ä
                        treffer = "unbestätigt"
                    Else
                        treffer = treffer.Replace("(Tag): ", "") ' den Vorspann vor dem Datum entfernen
                    End If
                    exWS.Range("G" & zählerb - 1).Value = treffer
                End If

            Loop Until objDateiLeser.EndOfStream
            'REGEXSCHLEIFE UND EXCELSCHREIBSCHLEIFE VORBEI
            '=============================================

            objDateiLeser.Close()
            objDateiLeser = Nothing

        Next i
        'HAUPTSCHLEIFE ZUR DATEI AUSWAHL VORBEI
        '======================================

        Console.WriteLine("Excel wird nach: " & sPath & " gespeichert. Bitte kontrollieren!")
        exAPP.ActiveWorkbook.SaveAs(sPath & "VB-Excel.xls")
        exAPP.ActiveWorkbook.Close()

        exWB = exAPP.Workbooks.Open(sPath & "apps\makro.xls")
        exAPP.Run("Datei_zusammen")
        exAPP.ActiveWorkbook.SaveAs(sPath & "Import.xls")
        exAPP.ActiveWorkbook.Close()

        Console.WriteLine("Ordner wird von Arbeitsdateien befreit")

        'ORDNER AUFRÄUMEN
        '=======================================================
        File.Delete(sPath & "VB-Excel.xls")
        File.Delete(sPath & "export.xls")

        For i = 0 To j - 1
            File.Delete(sPath & "ausgabe" & i & ".txt")
        Next
        'ORDNER AUFGERÄUMT
        '=======================================================

        Console.WriteLine("Dateien werden nach BüroPlus übertragen. Dies kann einige Minuten dauern!")

        Dim COMInstanz As Process
        COMInstanz = System.Diagnostics.Process.Start("T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\apps\COM\Bestelleingang.Import.exe")
        COMInstanz.WaitForExit()

        Dim ordner As Process = System.Diagnostics.Process.Start("explorer", "T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\")


        Console.WriteLine("Nun ist alles fertig, du musst nur noch eine Taste drücken!")
        'Console.ReadKey() 'Programm bis zum Drücken einer Taste anhalten
        'MsgBox("Programm fertig. Bitte in Verzeichnis: " & sPath & "nachsehen")
    End Sub

End Module

Natürlich ist viel einfach aus Beispielen zusammengeschustert und manches vielleicht auch überladen. Aber es tut und für meine Zwecke brauche ich auch keine grafische Oberfläche, die verwirrt bloß den Anwender.

Was das Programm macht, wäre noch erwähnenswert.

1. Alle *.pdf Dateien im Ordner suchen
2. Gefundene *.pdf Dateien durch den pdftotext Automaten schicken (freies und tolles Tool)
3. Export einer aktuellen Liste aus BüroPlus mit bpStarter (auch ein fremdes Programm, aber es tut wunderbar)
4. Eine neue Exceldatei öffnen und Spaltennamen vorbereiten
5. Erzeugte *.txt Dateien aus Punkt 2 einzeln einlesen und mit REGEX Konstrukten nach den gewünschten Daten suchen.
6. Gewünschte Daten in die Exceldatei schreiben
7. Die exportierte Excel mit der erzeugten Excel zusammenführen und eine neue Excel daraus erzeugen mittels folgendem Makro

Sub Datei_zusammen()
'
' Makro2 Makro
' Makro am 09.03.2010 von Testumgebung aufgezeichnet
'

'
    Cells.Select
    Selection.ClearContents
    
    Workbooks.Open Filename:="T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\export.xls"
    Workbooks.Open Filename:="T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\VB-Excel.xls"
    Windows("VB-Excel.xls").Activate
    Sheets("Tabelle1").Select
    Sheets("Tabelle1").Copy After:=Workbooks("export.xls").Sheets(1)
    ActiveWorkbook.Save
    ActiveWindow.Close
    ActiveWindow.Close
    
    With ActiveSheet.QueryTables.Add(Connection:= _
        "ODBC;DSN=Excel-Dateien;DBQ=T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\export.xls;DefaultDir=T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\;DriverId=790;MaxBufferSize=2048;PageTimeout=5;" _
        , Destination:=Range("A1"))
        .CommandText = Array( _
        "SELECT `Sheet1$`.`BL-Nr`, `Sheet1$`.`BL-Pos`, `Tabelle1$`.Auftr" _
        , _
        "agsnummer, `Tabelle1$`.`Wieland-Nummer`, `Tabelle1$`.`h-team-Nr`, `Tabelle1$`.Lieferdatum" & Chr(13) & "" & Chr(10) & "FROM `T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\export`.`Sheet1$` `Sheet1$`, `T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\expor" _
        , _
        "t`.`Tabelle1$` `Tabelle1$`" & Chr(13) & "" & Chr(10) & "WHERE `Sheet1$`.Kubez1 = `Tabelle1$`.`Wieland-Nummer` AND `Sheet1$`.`BL-Nr` = `Tabelle1$`.`BL-Nummer`" _
        )
        .Name = "Abfrage von Excel-Dateien"
        .FieldNames = True
        .RowNumbers = False
        .FillAdjacentFormulas = False
        .PreserveFormatting = True
        .RefreshOnFileOpen = False
        .BackgroundQuery = True
        .RefreshStyle = xlInsertDeleteCells
        .SavePassword = False
        .SaveData = True
        .AdjustColumnWidth = True
        .RefreshPeriod = 0
        .PreserveColumnInfo = True
        .Refresh BackgroundQuery:=False
    End With
    
    Columns("F:F").Select
    Selection.NumberFormat = "m/d/yyyy"
    
End Sub

8. Das C# Programm aufrufen und den COM-Import durchführen. Den Code habe ich freundlicherweise von jemanden zugemailt bekommen, da die API-Aufrufe doch recht umständlich sind. Nach ein paar Anpassungen an meine Bedürfnisse (Excelaufruf und Zeilenweise auslesen in ein Array), dann Vergleich ob der Datensatz existiert und ein paar break; ging es doch recht gut.

9. Nun läuft alles, muss es nur noch meinem Vorgesetzten zeigen und das OK einholen. Dann vielleicht ein paar kleine Anpassung machen. :D

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using Excel = Microsoft.Office.Interop.Excel;
using System.Reflection;  

namespace Bestelleingang.Import
{
    class Program
    {
        static void Main(string[] args)
        {

            int x, y, zeile =100, spalte = 7;

            Excel.Application oXL;
            Excel._Workbook oWB;
            Excel._Worksheet oSheet;
            Excel.Range CurRange;

            oXL = new Excel.Application();
            //oXL.Visible = true;

            oWB = (Excel._Workbook)(oXL.Workbooks.Open(@"T:\LIEFERANTEN\Wieland\Auftragsbestätigungen\Import.xls", Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value, Missing.Value));
            oSheet = (Excel._Worksheet)oWB.ActiveSheet;
            string[,] L = new string[zeile, spalte];
            
            for(x=2; x<zeile ; x++)
            {
                for(y=1; y<spalte; y++)
                {
                        
                        CurRange = (Excel.Range)oSheet.Cells[x, y];
                        L[x, y] = (String)CurRange.Text;
                        if (L[x, 1] == "")
                        { zeile = x; break; }
                }
            }

            CurRange = null;
            oSheet = null;

            oWB.Close(false, null, null);
            oWB = null;

            oXL.Quit();
            oXL = null;
                       

            Debug.Listeners.Add(new ConsoleTraceListener());

            using (var con = new BpnConnection("h-team industrievertretungen", "1", "aichele", ""))
            {
                //Debug.WriteLine("Connected to BpNext Server!");
                BpnBestelleingang bestelleingang = BpnBestelleingang.CreateInstance(con);

                for (x = 2; x < zeile; x++)
                {
                    if (L[x, 1] == "")
                    { break; }

                        var rangeArgs = new Dictionary<String, Object>()
                        {
                             { "BelegNr", L[x, 1] },
                             { "BelegPosNr", L[x, 2] }
                        };


                        int recordCount = bestelleingang.ApplyRange("BelegNr", rangeArgs);
                        //Debug.WriteLine(String.Format("Selected {0} records", recordCount));

                        if (recordCount > 0)
                        {
                            // An existing row can be locked
                            if (bestelleingang.BeginEdit())
                            {
                                bestelleingang.AuftrNr = L[x, 3];
                                if (L[x, 6] != "")
                                {
                                    bestelleingang.LiefDat = DateTime.Parse(L[x, 6]);
                                }
                                bestelleingang.EndEdit();
                            }
                            //Debug.WriteLine("Row Edited");
                        }
                        else
                        {
                            bestelleingang.BeginAppend();
                            // set fields here
                            bestelleingang.EndAppend();
                        }
                }
            }
            Console.Write("So viele Datensaetze wurden geändert: ");
            Console.Write(x - 2);
            Console.WriteLine("Please press ANY KEY. Bitte nicht sagen: Ich kann die ANY KEY Taste nicht finden!");
            //Console.ReadLine();
        }
    }
}
</zeile>

Die 3 Klassen für die API-Aufrufe sind nicht von mir, deshalb lass ich diese hier mal außen vor. Natürlich sind diese für das C# Programm auch wichtig!

Als Anmerkung: Das Programm ist nicht dynamisch, sprich der Pfad muss immer derselbe sein. Wenn man dieses Programm nun mit einer Installationsroutine ausstatten würde, wäre vielleicht ein Config-Datei nützlich um den Programmpfad auszulesen.

[Update]Lösungsansätze für Lesen von Muster aus einer Textdatei

Nachdem ich vor kurzem mal auf mein Problem mit der COM-Programmierung hingewiesen habe und ein paar Anregungen bekommen habe, bin ich nun einen Schritt weiter.

1. Ich habe eine Möglichkeit gefunden den Text aus einer PDF mit dem Tool pdftotext in eine Textdatei umzuwandeln. Bei meiner Testpdf (die übrigens auch als Referenz für zukünftige Auftragsbestätigungen genommen werden kann) hat das super hingehauen.
Dazu genügt es einfach folgenden Code zu verwenden:

pdftotext -layout datei1.pdf ausgabe1.txt

2. Nun sind die ABs zwar in einer grafischen Logik dargestellt, aber mit dem Computer nur mühsam zu verarbeiten. Doch für was gibt es denn Reguläre Ausdrücke? Richtig um Muster zu finden. Die von mir benötigen Daten folgen glücklicherweise immer einem bestimmten Muster. Zum Beispiel ein Datum ist immer gleich aufgebaut und die Artikelnummern. Doch leider sind RegEx sehr schwer zu kapieren. Durch eine Googlesuche nach „Regex Generator“ bin ich auf diese tolle Seite gestoßen: txt2re
Sie mag auf den ersten Blick sehr verwirrend sein, doch auf den zweiten Blick ist es einfach nur genial. Man gibt oben eine Zeile ein und kann unten per Mausklick auswählen, welche Muster man verarbeiten möchte und das Ganze dann auch noch in einer von vielen verbreiteten Sprachen.

Den zweiten Punkt werde ich morgen noch ausführlich testen in VB.NET und dann hoffentlich endlich ein Ergebnis bekommen. Wenn ich dann diese Datei habe, wird ein Export nicht mehr allzu schwer werden.

3. Ich muss nun noch nach der Syntax zum Lesen einer Datei in VB.NET googlen und das schreiben in der Datei. In C und PHP wüsste ich es noch einigermaßen, aber grafisch aufbereitet ist das ja immer etwas anders.

//Update:
Nun hab ich es endlich auch geschafft, was zu coden:

Imports System.IO
Imports System.Text.RegularExpressions

Module Module1

    Dim objDateiMacher As StreamWriter
    Dim objDateiLeser As StreamReader
    Sub Pdf2Txt(ByVal pdfFile As String, ByVal txtFile As String)
        Dim arguments As String = "-layout" & " " & pdfFile & " " & txtFile
        'make sure to provide the path with the pdfFile and the txtFile
        System.Diagnostics.Process.Start("c:\xpdf\pdftotext.exe", arguments)
    End Sub
    Sub Main()
        Call Pdf2Txt("C:\xpdf\datei\eingabe.pdf", "C:\xpdf\datei\ausgabe.txt" )
        Dim txt As String
        objDateiLeser = New StreamReader("C:\xpdf\datei\ausgabe.txt" )

        Dim re1 As String = ".*?"           'Non-greedy match on filler
        Dim re2 As String = "(BL[0-9]{7})"    'Word 1
        Dim r As Regex = New Regex(re1 + re2, RegexOptions.IgnoreCase Or RegexOptions.Singleline)

        Do
            txt = objDateiLeser.ReadLine

            Dim m As Match = r.Match(txt)

            If (m.Success) Then
                Dim word1 As Group = m.Groups(1)

                Debug.WriteLine(word1.ToString)
            End If

        Loop Until objDateiLeser.EndOfStream

        objDateiLeser.Close()
        objDateiLeser = Nothing
        Console.ReadKey()
    End Sub

End Module

Nach ein paar Änderungen am Code dank diesem Forum hier, tut das Programm auch und der unten abgebildete Fehler ist endlich weg.

Doch nun hapert es noch ein bisschen an den Regulären Ausdrücken, doch Übung macht der Meister. Probieren geht über Studieren.