colinardo
Goto Top

Powershell: Text anhand seiner Position aus PDF-Dokumenten auslesen

article-picture

back-to-topEinleitung


Möglichkeiten für das automatische Extrahieren von Text aus PDF-Dokumenten gibt es etliche (PDFtk, iTextSharp,...). Das gezielte Extrahieren der gewünschten Inhalte geschieht dann aber oftmals mittels Regular Expressions, das genügt auch in vielen Fällen, so lange sich der Text ausreichend von anderen Teilen des Dokumentes eindeutig abhebt. Oftmals lässt aber die Struktur des Dokumentes ein effizientes auslesen der Texte nicht mehr zu, und genau hier soll folgende Funktion einspringen, indem man mit Ihr Text anhand seiner absoluten Position innerhalb eines genau definierten Rechtecks auslesen kann. Nützlich ist das bspw. bei immer gleich aufgebauten Formularen bei denen aber keine "echten" PDF-Formularfelder mehr vorhanden sind weil das PDF mit der "flatten" Funktion behandelt wurde. Wer dagegen echte PDF-Formulare bei denen die Formularfelder noch im Original vorhanden sind auslesen möchte findet von mir bereits hier ein entsprechendes Skript.

back-to-topWas macht die Funktion


Das unten aufgeführte CMDLet extrahiert bereits vorhandenen maschinenlesbaren Text aus einem oder mehreren PDF-Dokumenten mittels seiner absoluten Positionen im Dokument. Anhand einer Hashtable definiert man die Koordinaten für Rechtecke innerhalb derer der Text ausgelesen werden soll. Als Ergebnis erhält man ein oder mehrere Objekte mit den Texten als separate Eigenschaften für eine objektorientierte Weiterverarbeitung. Für die Verarbeitung der PDF-Dokumente verwendet das Skript als Basis die iTextSharp-DLL welche automatisch von Nuget in das Skript- oder TEMP-Verzeichnis (bei direkter Ausführung ohne Skriptdatei) heruntergeladen wird sofern sie noch nicht vorhanden ist.

back-to-topPowershell CMDLet: Get-PDFTextFromLocation


function Get-PDFTextFromLocation {
    <#
    .Synopsis
       Extracts text from PDF documents
    .DESCRIPTION
       Extracts text from defined locations in PDF documents
    .LINK
       © @colinardo (https://administrator.de/tutorial/powershell-text-anhand-seiner-position-aus-pdf-dokumenten-auslesen-32779450015.html)
    .INPUTS
         System.IO.FileSystemInfo[] / You can pipe the output of Get-ChildItem to the function
    .OUTPUTS
         System.Management.Automation.PSCustomObject[]
    .EXAMPLE
        $locations = @{
            1 = [ordered]@{
                'Invoice No.' = 30,123,203,172  
                'Date' = 130,230,150,245  
            }
        }
        Get-ChildItem "E:\data" -File -Filter *.pdf | Get-PDFTextFromLocation -locations $locations  

       This example extracts 2 fields on page 1 from every PDF coming on the pipeline
    .NOTES
       Hashtable-Format for text location definition:
        - first level of hashtable defines the page index from which to extract the field data
        - second level keys define a user defined unique names for the fields
        - the value in the second level defines the rectangle location as an  array with four items:
            - [LowerLeftX],[LowerLeftY],[UpperRightX],[UpperRightY])
            - coordinate origin is the non rotated lower left corner of the page.
        
        Example 1:
        ============
        @{
            1 = [ordered]@{
                'Invoice No.' = 30,123,203,172  
                'Date' = 130,230,150,245  
            }
        }

        Example 2:
        ============
        @{
            1 = [ordered]@{
                'Invoice No.' = 30,123,203,172  
                'Date' = 130,230,150,245  
            }
            2 = [ordered]@{
                'Sum' = 120,80,150,100  
            }
        }
    #>
    param(
        # Filepath
        [Parameter(mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][Alias('Fullname')][string[]]$path,  
        # hashtable with definition of pages and locations
        [Parameter(mandatory=$true)][hashtable]$locations,
        # output source fileinfo
        [Parameter(mandatory=$false)][switch]$passthru
    )

    begin{
        # function to load iTextSharp assembly
        function Load-iTextLibrary {
            if($psscriptroot -ne ''){  
                $localpath = join-path $psscriptroot 'itextsharp.dll'  
            }else{
                $localpath = join-path $env:TEMP 'itextsharp.dll'  
            }
            $zip = $null;$tmp = ''  
            $script:iTextDLL = $localpath
            try{
                if(!(Test-Path $localpath)){
                    Add-Type -A System.IO.Compression.FileSystem
                    $tmp = "$env:TEMP\$([IO.Path]::GetRandomFileName())"  
                    write-host "Downloading and extracting required 'iTextSharp.dll' ... " -F Green -NoNewline  

                    (New-Object System.Net.WebClient).DownloadFile('https://www.nuget.org/api/v2/package/iTextSharp/5.5.13.1', $tmp)  
                    $zip = [System.IO.Compression.ZipFile]::OpenRead($tmp)
                    $zip.Entries | ?{$_.Fullname -eq 'lib/itextsharp.dll'} | %{  
                        [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_,$localpath)
                    }
                    write-host "OK" -F Green   
                }
                if (Get-Item $localpath -Stream zone.identifier -ea SilentlyContinue){
                    Unblock-File -Path $localpath
                }
                Add-Type -Path $localpath
            }catch{
                throw "Error: $($_.Exception.Message)"  
            }finally{
                if ($zip){$zip.Dispose()}
                if ($tmp -ne ''){del $tmp -Force -EA SilentlyContinue}  
            }
        }

        # load iText library
        Load-iTextLibrary

        # also read protected files
        [iTextSharp.text.pdf.PdfReader]::unethicalreading = $true

        if (!("iText.GetTextFromLocationStrategy" -as [type])){  
            # Create GetTextFromLocationStrategy class
            Add-Type -ReferencedAssemblies $script:iTextDLL -TypeDefinition '  
                using System.Text;
                using System.Collections;
                using System.Collections.Generic;
                using System.Collections.Specialized;
                using iTextSharp.text;
                using iTextSharp.text.pdf.parser;
                using iTextSharp.text.pdf;

                namespace iText {
                    public class GetTextFromLocationStrategy : iTextSharp.text.pdf.parser.ITextExtractionStrategy {
                        StringBuilder result = new StringBuilder();
                        OrderedDictionary resultfields = new OrderedDictionary();
                        Vector lastBaseline = new Vector(0, 0, 0);
                        OrderedDictionary rects;

                        public GetTextFromLocationStrategy(OrderedDictionary locations) {
                            rects = locations;
                        }

                        public void RenderText(TextRenderInfo renderinfo) {
                            Vector curBaseline = renderinfo.GetBaseline().GetStartPoint();
                            Vector topRight = renderinfo.GetAscentLine().GetStartPoint();
                            iTextSharp.text.Rectangle rect = new iTextSharp.text.Rectangle(curBaseline[Vector.I1], curBaseline[Vector.I2], topRight[Vector.I1], topRight[Vector.I2]);
                            foreach (DictionaryEntry field in rects) {
                                Rectangle fieldRect = (Rectangle)field.Value;
                                if (!resultfields.Contains(field.Key)) {
                                    resultfields.Add(field.Key, new StringBuilder(""));  
                                }
                                if (rect.Left >= fieldRect.Left && rect.Top <= fieldRect.Top && rect.Bottom >= fieldRect.Bottom && rect.Right <= fieldRect.Right) {
                                    if (lastBaseline[Vector.I2] > curBaseline[Vector.I2] && ((StringBuilder)resultfields[field.Key]).ToString() != "") {  
                                        ((StringBuilder)resultfields[field.Key]).Append("\r\n");  
                                        result.Append("\r\n");  
                                    }
                                    string txt = renderinfo.GetText();
                                    ((StringBuilder)resultfields[field.Key]).Append(txt);
                                    result.Append(txt);
                                }
                            }
                        }

                        public string GetResultantText() {
                            return result.ToString();
                        }
                        public OrderedDictionary GetResultTable() {
                            OrderedDictionary dic = new OrderedDictionary();
                            foreach (DictionaryEntry field in resultfields) {
                                dic.Add(field.Key, field.Value.ToString());
                            }
                            return dic;
                        }
                        public void BeginTextBlock() { }
                        public void EndTextBlock() { }
                        public void RenderImage(ImageRenderInfo renderinfo) { }
                    }
                }
            '  
        }
        
        
        # function to convert mm to points
        function M2P($mm){$mm * 2.83465}

        # convert user supplied location values to rectangles with point values
        foreach($page in $locations.Keys){
            [string[]]$locations.$page.Keys | %{$locations.$page.$_ = [iTextSharp.text.Rectangle]::new((M2P $locations.$page.$_[0]),(M2P $locations.$page.$_[1]),(M2P $locations.$page.$_[2]),(M2P $locations.$page.$_[3]))}
        }
    }

    process{
        # Process files
        foreach($file in $path) {
            $reader = $null
            try {
                write-verbose "Processing file '$($file)' ..."  
                # create pdf reader object
                $reader = New-Object iTextSharp.text.pdf.PdfReader $file
                # parse pages defined
                $data = [ordered]@{}
                foreach($page in $locations.Keys | ?{$_ -in (1..$reader.NumberOfPages)}){
                    # create instance from custom Text-Extraction-Strategy
                    $strategy = New-Object iText.GetTextFromLocationStrategy ([System.Collections.Specialized.OrderedDictionary]$locations.$page)
                    [void][iTextSharp.text.pdf.parser.PdfTextExtractor]::GetTextFromPage($reader,$page,$strategy)
                    # output result object
                    $data += $strategy.GetResultTable()
                }
                # if there is extracted data
                if ($data){
                    if ($passthru.IsPresent){
                        # append file info
                        $data.FileInfo = Get-Item $file
                    }
                    # return custom object
                    [pscustomobject]$data
                }
            }catch{
                write-host $_.Exception.Message -F Red
            }finally{
                if($reader){$reader.Dispose()}
            }
        }
    }
}

back-to-topPraktische Verwendung der Funktion


Gegeben sei als Beispiel das folgende Muster-Dokument aus dem wir die zwei umrandeten Textfelder extrahieren werden:

screenshot

back-to-topWie definiert man die Positionen?


Für die Definition der Positionen dient eine Hashtable die ein bestimmtes Format aufweisen muss. Auf der ersten Ebene der Hashtable wird die Seitenzahl als Key angegeben auf die sich die folgenden Positionen beziehen. Der Wert des Keys ist dann eine weitere Hashtable die als Keys eindeutige Bezeichner haben und als Wert ein Array aus 4 Zahlen hat, welches ein Rechteck über die Position des Textes legt der darin extrahiert werden soll. Das Rechteck bzw. das Array ist wie folgt definiert:
[LOWER-LEFT-X] , [LOWER-LEFT-Y] , [UPPER-RIGHT-X] , [UPPER-RIGHT-Y]
  • Der Koordinaten-Ursprung liegt dabei immer in der linken unteren Ecke der jeweiligen Seite.
  • Als Einheit kommen Millimeter (mm) zum Einsatz

Das ganze sieht dann in der Praxis folgendermaßen aus.

Hier werden zwei Felder auf Seite 1 ausgelesen:
$locations = @{
    1 = [ordered]@{
        'Geschäftspartnernummer' = 167,223,188,228  
        'Empfänger' = 23,211,73,233  
    }
}

Es können sowohl mehrere Seiten als auch pro Seite mehrere Positionen in der Hashtable definiert werden. Die Namen der Felder können beliebig gewählt werden, müssen aber alle eindeutig sein damit später eine eindeutige Zuordnung möglich ist.

Hier werden zwei Felder auf Seite 1 und 1 Feld auf Seite 3 ausgelesen.
$locations = @{
    1 = [ordered]@{
        'Geschäftspartnernummer' = 167,223,188,228  
        'Empfänger' = 23,211,73,233  
    }
    3 = [ordered]@{
        'Bankleitzahl' = 10,20,40,30  
    }
}

Wichtig ist das die Koordinaten mit ausreichend Abstand zu dem Text umgebenden Rahmen definiert werden, ansonsten wird der Text darin nicht extrahiert! Erfasst werden nämlich nur Blöcke deren Koordinaten wirklich vollständig innerhalb des vom Benutzer definierten Rechtecks liegen. Sollte also ein Text einmal nicht vollständig oder überhaupt nicht ausgelesen werden, ist meist das Rechteck zu klein gewählt worden, es hilft dann die Koordinaten des Rechtecks zu vergrößern, dabei macht es nichts aus wenn das Rechteck zum Teil anderen Text ansatzweise umfasst so lange dessen umgebender Kasten nicht durch das Rechteck mit erfasst werden.

Für die Bestimmung der exakten Positionen können als digitale Hilfsmittel in PDF-Readern oder Editoren etwa vorhandene Mess-Tools bzw. Lineal-Funktionen benutzt werden. Ein extra Ausdrucken des Dokumentes und Messen mittels Lineal ist somit überflüssig und sollte auch aus ökologischen Gründen vermieden werden wann immer es geht ­čî▓.

Es folgen nun ein paar praktische Beispiele für die Verwendung des CMDLets.

back-to-topCode-Beispiel 1 - Auslesen eines einzelnen Dokumentes


# Positionen der zu extrahierenden Textfelder definieren
$locations = @{
    1 = [ordered]@{
        'Geschäftspartnernummer' = 167,223,188,228  
        'Empfänger' = 23,211,73,233  
    }
}
# Ein einzelnes PDF-Dokumente auslesen und das Ergebnis auf der Konsole als Liste ausgeben
Get-PDFTextFromLocation -Path "E:\dokument.pdf" -locations $locations | format-list *  

Ergebnis für das o.g. Musterdokument

screenshot

back-to-topCode-Beispiel 2 - Export der Daten mehrerer gleich aufgebauter Dokumente in eine CSV-Datei


# Positionen der zu extrahierenden Textfelder definieren
$locations = @{
    1 = [ordered]@{
        'Geschäftspartnernummer' = 167,223,188,228  
        'Empfänger' = 23,211,73,233  
    }
}
# Alle gleich aufgebauten PDF-Dokumente eines Ordners verarbeiten und das Ergebnis in eine CSV-Datei ausgeben
Get-ChildItem "E:\quelldaten" -File -Filter *.pdf | Get-PDFTextFromLocation -locations $locations | export-csv "E:\output.csv" -Delimiter ";" -NoTypeInformation -Encoding UTF8 -Force  

Ergebnis der CSV-Datei für zwei ähnliche Musterdokumente

"Geschäftspartnernummer";"Empfänger"
"1222334455";"Herrn
Max Mustermann
Musterstr. 99
99999 Musterhausen"
"22342344";"Frau
Monika Musterfrau
Musterweg. 1
88888 Musterdorf"

Eventuell ist das dem ein oder anderen von euch ein nützliches Hilfsmittel.

Wie immer alle Angaben ohne Gewähr auf Leib und Leben face-smile.

Content-Key: 32779450015

Url: https://administrator.de/contentid/32779450015

Printed on: September 21, 2023 at 20:09 o'clock

Member: Kraemer
Kraemer Sep 19, 2023 at 09:45:08 (UTC)
Goto Top
Moin,

zwar noch nicht getestet aber viel Lob von meiner Seite für diese tolle Idee.
Member: colinardo
colinardo Sep 19, 2023 updated at 10:11:26 (UTC)
Goto Top
Danke dir. Dachte das taucht hier ja immer wieder mal auf, und gefunden habe ich, speziell das, bisher bis auf Tabula(für Tabellen) nur in kommerziell verfügbaren Programmen.
Member: Crusher79
Crusher79 Sep 19, 2023 updated at 14:12:39 (UTC)
Goto Top
Hallo,

danke für diesen Ansatz hab vor längeren auch was gebaut. Aber nicht ganz so schön.

Hatte mir als Basis pdftotext ausgesucht- https://www.xpdfreader.com/pdftotext-man.html

Hintergrund war, dass man hier schon Layout übernehmen oder unterdrüken kann. Mit Layout wäre es so, wie wir es von früher kennen: Fixe Abstände zum linken Rand. Zumindest so einigermaßen.

Die Ankerpunkte kann man via Regex auch schön finden. Hier ging es darum, wenn ein Sachbearbeiter nichht gefüllt war, dass dann trotzdem das richtige Feld gefunden wurde.

Muster:
A U F T R A G S B E S T Ä T I G U N G
Ihr Ansprechpartner: Kunden-Nr. Auf.-Nr. Datum Seite
Max Mustermann 22292 864987 /V 11.02.21 1

Die Belge stammen aus Crystal Reports. Man kan natürlich auch damit einen "hidden" String - also weiße Schrift auf weißen Grund o.ä. - nehmen und alles in eine Art Token packen.

Das war damals mein Ansatz. Lief aber relativ robust.

@colinardo ansonsten schwebte mir vor Dokument und Text-Stream erst zu slicen und mit tesseract auszuelesen. Weiter unten ein Bsp. dazu für einen Barcode. Zwar kein OCR, aber so die Richtung.

An pdftotext wird wohl nicht wirklich viel weiterentwickelt. Schaue mir daher dein Script mal genauer an. Mir fällt schon dass ein oder andere Projekt ein.

Wir haben ja X-Rechung, ZUGFeRD etc. aber ohne XML Anteil ist so ein PDF dumm wie Stück Brot. Ich mag immer noch den Ansatz mit der Zerlegung.

Die Trefferquote war recht hoch. Ansonsten hab ich einfach "MANUELLE ERFASSUNG" o.ä. in den Dateinamen geschrieben. Dann konnte jeder einfach nacharbeiten. Kam aber selten vor.

Mit Ausschnitten wie beim Barcode lässt sich das ganze noch verfeinern. Sieht man ja an deinen Beispiel gut. Abbyy oder Omnipage machen ja nichts anderes. Nur das man hier die Masken über die GUI definieren kann.

Nimmt man das noch via Script vor, steht eine solche Lösung den kommerziellen kaum in was nach.


# Snippet
$pdftotext = "C:\ps\pdftotext.exe"  

Function Global:FindBelegNr($PDFToTextFile)
    {
		#PDF in durchsuchbaren Text umwandeln. simple2 und clip zum "normalisieren" 
        Start-Process -NoNewWindow -Wait $pdftotext -ArgumentList "-simple2", "-clip", "-f 1", "-l 1", $PDFToTextFile, $TempTextFile  

        $texttest = Get-Content -Path $TempTextFile -Raw

        #$patternBelegRe = "Rech.-Nr. " 
        #$patternBelegAb = "Auf.-Nr. " 
        [string] $DokBelegAbRe =""  
        [string] $DokBelegAn =""  
        [string] $DokBelegLs =""  
                
        #Angebot Nr in einer Zeile
        $regex_An = "(ANGEBOT )(Kunden-Nr\. )(Angebot-Nr\. )(Datum )(Seite)\r\n([\d]+)\s([\d]+)"                  
        #Auf ODER Rech übermehrere Zeilen
        $regex_AbRe = "(.*)\r\n(Ihr Ansprechpartner: )(Kunden-Nr\. )((Auf|Rech)\.-Nr\. )(Datum )(Seite)\r\n(.*)\s([\d]+)\s([\d]+)"  
        #Nur Lieferschein
        $regex_Ls = "(LIEFERSCHEIN)\r\n(Lieferschein-Nr\. )(Lieferanten-Nr\. )(Datum )(Seite)\r\n([\d]+)\s([\d]+)"  
        #Nur Bestellschein
        $regex_Bs = "(.*)(Lieferanten-Nr. )(Bestell-Nr. )(Datum )(Seite)\r\n([\d]+)\s([\d]+)"  
                
        $texttest | select-string -Pattern $regex_AbRe -AllMatches | % { $DokBelegAbRe = $_.Matches.Groups[1].Value + "_" + $_.Matches.Groups[10].Value }  
        $texttest | select-string -Pattern $regex_An -AllMatches | % { $DokBelegAn = $_.Matches.Groups[1].Value + "_" + $_.Matches.Groups[7].Value }  
        $texttest | select-string -Pattern $regex_Ls -AllMatches | % { $DokBelegLs = $_.Matches.Groups[1].Value + "_" + $_.Matches.Groups[6].Value }  
        $texttest | select-string -Pattern $regex_Bs -AllMatches | % { $DokBelegBs = $_.Matches.Groups[1].Value + "_" + $_.Matches.Groups[7].Value }  

        IF ($DokBelegAbRe.length -gt 0) { $Script:Belegnr = $DokBelegAbRe }
        IF ($DokBelegAn.length -gt 0) { $Script:Belegnr = $DokBelegAn }
        IF ($DokBelegLs.length -gt 0) { $Script:Belegnr = $DokBelegLs }
        IF ($DokBelegBs.length -gt 0) { $Script:Belegnr = $DokBelegBs }        
    }


Barcode ausschneiden und aulsesen:
using namespace System.Drawing;
Add-Type -AssemblyName System.Drawing;

[void] [Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")  

$testBild = "C:\temp\code_ls4.png-1.png"  
$src=[System.Drawing.Image]::FromFile($testBild)

$rect = New-Object System.Drawing.Rectangle(740,1500,400,250) # top, left, width, height of slice
$slice = $src.Clone($rect, $src.PixelFormat);
#$slice.Save("c:\temp\test_slice.png", "png"); 

$src = $slice;


[void] [System.Reflection.Assembly]::LoadFrom("c:\temp\BarcodeImaging.dll");      
$barcodes = @{}
[BarcodeImaging]::FullScanPage([ref] $barcodes, $src, 150)

$barcodes
Member: Crusher79
Crusher79 Sep 19, 2023 at 14:29:12 (UTC)
Goto Top
Zitat von @colinardo:

Danke dir. Dachte das taucht hier ja immer wieder mal auf, und gefunden habe ich, speziell das, bisher bis auf Tabula(für Tabellen) nur in kommerziell verfügbaren Programmen.

Gebe dir vollkommen recht! Andere nennen es DocuXXX und verlangen 60.000 bis 120.000 Euro. Workflows und DSGVO mal außen vor gelassen braucht man meist doch im Alltag nur ein paar Infos.

Wenn man so die Datei umbennennt, reicht dass bei manchen DMS schon für einen Import. Die Möglichkeiten die sich damit auftuen sind vielflältig.

Fehlt der Text hilft tesseract weiter. Auch wenn es nur um Ausschnitte - s.o. geht.

Aus eigener Praxis kann ich auf jedenfall bestätigen, dass @colinardo Lösung sehr robust ist! Arbeitet es doch teils so, wie Menschen es auch händisch umsetzen würden.

Passt es mal nicht, kann man immer noch die Suchmuster verfeienern. Auf fehlen von Nummern reagieren etc. Aber sowas im Detail durch zu spielen sollte jeden klar sein. Dann hat man ein sehr robustes System!
Member: TomTomBon
TomTomBon Sep 19, 2023 at 14:41:41 (UTC)
Goto Top
Ich kenne es auch aus kommerziellen Systemen als ZoneOCR.

Sehr sehr ähnlich aufgemacht ist es bei ScannerVision.
Aber halt closed Source und bis auf das Thema der Position des Bereiches ist dort nicht viel zu machen.
Und natürlich die Definition der dort erhobenen Daten und das ein OCR gleich mitgemacht wird...

Aber Colinardo hat dies vorgemacht wie man es selbst bauen kann.
Genial !
Member: MatthiasBP
MatthiasBP Sep 20, 2023 at 09:26:46 (UTC)
Goto Top
Danke, mir hat es jmd in einen Beitrag eingehaengt Link. Ich lerne gerade ueber Funktionen, habe es in ein Modul kopiert und erhalte einen Fehler, es will einen Namen fuer ein Makro haben. Habe gestern gelernt, dass die Funktion eine Klammer () am Ende haben soll. Liegt es daran?
Eine Frage geht es ueberhaupt fuer meine Zwecke?
Habe knapp 500 pdf's mit bis zu 90 oder mehr Seiten. Bestellungen koennen bei einem Kunden bis zu vier auf einer Seite sein, bei zwei Kunden entweder 1/1 oder 1/2 oder 2/1 Bestellungen, also beide je eine oder der erste zwei und der zweite eine und umgekehrt. Die Felder koennte ich jeweils messen und eingeben mit allen Kombinationen, aber nicht fuer jede Seite oder verstehe ich es falsch?
Member: Crusher79
Crusher79 Sep 20, 2023 at 09:43:19 (UTC)
Goto Top
Zitat von @MatthiasBP:

... verstehe ich es falsch?

Ich hab hier eig. auch zu viel geschrieben. Rubrik: Anleitungen ....

Lass es doch drüben diskutieren! Hab auch viel Sauereien gemacht. Nur das sprengt hier den Rahmen.
Member: colinardo
colinardo Sep 20, 2023 updated at 10:36:42 (UTC)
Goto Top
Servus @MatthiasBP.
habe es in ein Modul kopiert und erhalte einen Fehler, es will einen Namen fuer ein Makro haben.
Ähh nee, das ist Powershell kein VBA!
@MatthiasBP habe dir in deinem Thread dazu geantwortet => Pdf in Excel Bestellnummern auslesen, bitte auch nur dort diesbezüglich antworten. Merci.

Grüße Uwe