darktrinity
Goto Top

Automatisiertes Exportieren von Webseiten als PDF

Hallo liebe Community,

Ich habe folgendes Problem bzw Ziel:
Es sollen mehrere Webseiten automatisiert als PDF Datei exportiert werden. Es handelt sich hier um Stundenzettel von Kollegen. Die URLS haben alle eine fortlaufende ID, zB "https://URL.tld?id=1?weitereDatendieImmerGleichSind=blabla"
Nach Möglichkeit sollte das mit "einfachen Bordmitteln" von Windows 10 realisiert werden, zB Powershell.

Ich habe hier ein wenig experimentiert und verschiedene Ansätze ausporbiert, ich kriege es aber nicht geregelt face-sad Von dem Tool wkhtmltopdf habe ich mir da viel versprochen, doch es läuft leider auf Fehler und generiert leere PDF Dokumente face-sad

Vielleicht hat sich hier ja bereits jemand mit sowas beschäftigt oder hat einen Tipp für mich.

Lieben Dank

Content-ID: 666118

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

Ausgedruckt am: 22.11.2024 um 01:11 Uhr

MrCount
MrCount 26.04.2021 um 15:18:38 Uhr
Goto Top
Servus,

da könnte dir

Invoke-Webrequest

und diese Function helfen

function ConvertTo-PDF {
	param(
		$TextDocumentPath
	)
	
	Add-Type -AssemblyName System.Drawing
	$doc = New-Object System.Drawing.Printing.PrintDocument
	$doc.DocumentName = $TextDocumentPath
	$doc.PrinterSettings = new-Object System.Drawing.Printing.PrinterSettings
	$doc.PrinterSettings.PrinterName = 'Microsoft Print to PDF'  
	$doc.PrinterSettings.PrintToFile = $true
	$file=[io.fileinfo]$TextDocumentPath
	$pdf= [io.path]::Combine($file.DirectoryName, $file.BaseName) + '.pdf'  
	$doc.PrinterSettings.PrintFileName = $pdf
	$doc.Print()
	$doc.Dispose()
}
colinardo
Lösung colinardo 26.04.2021, aktualisiert am 01.10.2022 um 15:57:35 Uhr
Goto Top
Servus @DarkTrinity,
ein Beispiel von diversen möglichen, mit einem einfachen CMDLet das ich vor einiger Zeit mal zusammen geschrieben hatte ...
# function to convert website to pdf
function Convert-WebsiteToPDF {
    [cmdletbinding()]
    param(
        [parameter(mandatory=$true)][string]$URL,
        [parameter(mandatory=$true)][string]$path
    )

    function Load-MultiFileNugetAssembly {
        [CmdletBinding()]
        param(
            [string]$url,
            [string]$name,
            [hashtable]$files
        )
        if($psscriptroot -ne ''){  
            $localpath = $psscriptroot
        }else{
            $localpath = $env:TEMP
        }
        $tmp = "$env:TEMP\$([IO.Path]::GetRandomFileName())"      
        $zip = $null
        try{
            Add-Type -A System.IO.Compression.FileSystem
            # download if needed
            if ($files.Keys | ?{!(Test-Path (join-path $localpath $_))}){
                write-host "Downloading and extracting required library '$name' ... " -F Green -NoNewline  
                (New-Object System.Net.WebClient).DownloadFile($url, $tmp)
                write-host "OK" -F Green  
                $zip = [System.IO.Compression.ZipFile]::OpenRead($tmp)
            }

            foreach($file in $files.GetEnumerator()){
                $outfile = join-path $localpath $file.Name
                $zip.Entries | ?{$_.Fullname -eq $file.Value.InternalPath} | %{
                    [System.IO.Compression.ZipFileExtensions]::ExtractToFile($_, $outfile, $true)
                    Unblock-File -Path $outfile
                }
                if ($file.Value.LoadAssembly){
                    Add-Type -Path $outfile -EA Stop
                }
            }
        }catch{
            throw "Error: $($_.Exception.Message)"  
        }finally{
            if ($zip){$zip.Dispose()}
            if(Test-Path $tmp){del $tmp -Force}
        }  
    }

    # Load Spire.PDF Assemblies
    Load-MultiFileNugetAssembly 'https://www.nuget.org/api/v2/package/FreeSpire.PDF/7.2.0' -name 'Spire.Pdf' -files @{'Spire.Pdf.dll'=@{InternalPath='lib/net40/Spire.Pdf.dll';LoadAssembly=$true};'Spire.License.dll'=@{InternalPath='lib/net40/Spire.License.dll';LoadAssembly=$true}} -EA Stop  

    # create pdf document
    $doc = new-object Spire.Pdf.PDFDocument
    # create conversion settings objects
    $html_format = New-Object Spire.Pdf.HtmlConverter.PdfHtmlLayoutFormat -Property @{
        Layout = [Spire.Pdf.Graphics.PdfLayoutType]::OnePage
    }
    # pdf page settings
    $settings = New-Object Spire.Pdf.PdfPageSettings -Property @{
        Margins = New-Object Spire.Pdf.Graphics.PdfMargins 10
        Size = [Spire.Pdf.PdfPageSize]::A4
        Orientation = 'Portrait'  
    }
    # load html Parameter: URL, execute javascript, convert links, detect page breaks, settings, format
    $doc.LoadFromHTML($URL,$false,$false,$false,$settings,$html_format)
    # save pdf object to file
    $doc.SaveToFile($path)
    # close document
    $doc.Close()
}

# beginn with id
$start_id = 1
# stop with id
$stop_id = 10
# itterate over ids
$start_id..$stop_id | %{
    # Use converter function with URL and output path
    Convert-WebsiteToPDF -URL "https://URL.tld?id=$_&weitereDatendieImmerGleichSind=blabla" -path "E:\Ausgabe\stundenzettel_id_$_.pdf"  
}

Grüße Uwe
DarkTrinity
DarkTrinity 27.04.2021 aktualisiert um 08:43:16 Uhr
Goto Top
Hallo @uwe ,

das Script ist toll - erstmal ganz lieben Dank face-smile Aber irgendwie funktioniert es nicht wirklich...

Wenn ich es über die Powershell ausführe via powershell Web2PDF.ps1 erzeugt es die folgende Fehlermeldung;

Web2PDF.ps1:15 char:55
+     [System.Windows.Forms.SendKeys]::SendWait("{ENTER}")  
+                                                       ~~
The string is missing the terminator: ".  
    + CategoryInfo          : ParserError: (:) , ParentContainsErrorRecordException
    + FullyQualifiedErrorId : TerminatorExpectedAtEndOfString

Auch wenn ich die Powershell mit Administrator- Rechten ausführe oder den Parameter -ExecutionPolicy Bypass verwende erscheint diese Meldung....

Allerdings funktioniert es über die IDE MS Visual Studio Code etwas besser - es kommt keine Fehlermeldung. Allerdings erzeugt es eine leere PDF Datei mit dem Namen stundenzettel_id_0.pdf.

Ich habe es dann in Visual Studio Code etwas verändert - Zeile 81 - 84:
$startid..$stopid | %{
    # Use converter function with URL and output path
    Convert-WebsiteToPDF -URL "https://URL.tld?id=128&weitereDatendieImmerGleichSind=blabla" -path "E:\Ausgabe\stundenzettel_id_128_.pdf"  
}
So erzeugt es den Stundenzettel wie gewünscht - aber auch nur dann wenn ich es über die IDE ausführe. In der Powershell- Konsole wird nach wie vor diese Meldung erzeugt....
colinardo
colinardo 27.04.2021 aktualisiert um 09:46:13 Uhr
Goto Top
Zitat von @DarkTrinity:
Hallo @uwe ,
Servus zurück.
das Script ist toll - erstmal ganz lieben Dank face-smile Aber irgendwie funktioniert es nicht wirklich...
Wurde hier natürlich wie immer vorher einwandfrei getestet face-wink. Ich poste keinen Code den ich nicht vorher teste.

Wenn ich es über die Powershell ausführe via powershell Web2PDF.ps1 erzeugt es die folgende Fehlermeldung;

Web2PDF.ps1:15 char:55
> +     [System.Windows.Forms.SendKeys]::SendWait("{ENTER}")  
> +                                                       ~~
> The string is missing the terminator: ".  
>     + CategoryInfo          : ParserError: (:) , ParentContainsErrorRecordException
>     + FullyQualifiedErrorId : TerminatorExpectedAtEndOfString

Das ist z.B. eine Zeile die in meinem Code überhaupt nicht vorkommt! Ergo muss diese entweder von dir selbst hinzugefügt worden sein oder in deinem Powershell-Profil schon der Syntax-Fehler eingebaut sein!
Wenn du selbstständig Teile veränderst kann ich natürlich nichts dran machen face-smile.

Auch wenn ich die Powershell mit Administrator- Rechten ausführe oder den Parameter -ExecutionPolicy Bypass verwende erscheint diese Meldung....
Admin-Rechte sind nicht nötig für das Ausführen des Skripts. Nur Schreibrechte auf den Skript bzw. Temp-Ordner für das Herunterladen von 2 DLLs aus dem Nuget Repo.
Ich habe es dann in Visual Studio Code etwas verändert - Zeile 81 - 84:
> $startid..$stopid | %{
>     # Use converter function with URL and output path
>     Convert-WebsiteToPDF -URL "https://URL.tld?id=128&weitereDatendieImmerGleichSind=blabla" -path "E:\Ausgabe\stundenzettel_id_128_.pdf"  
> }
> 
Das ist erst mal Blödsinn x mal den selben Output zu erzeugen, wenn dann verändert man die Parameter $start_id und $stop_id auf den gewünschten Wert so dass die Schleife nur mit den entsprechenden IDs ausgeführt wird.
So erzeugt es den Stundenzettel wie gewünscht - aber auch nur dann wenn ich es über die IDE ausführe. In der Powershell- Konsole wird nach wie vor diese Meldung erzeugt....
Dann liegt der Fehler sehr wahrscheinlich in deinem Powershell Profil, denn obige Zeile ist wie gesagt nicht in meinem Skript enthalten und hier funktioniert das reine Skript auf sämtlichen Windows 10 oder auch 7 Maschinen problemlos, es muss also auf deiner Seite liegen.
Starte das Skript mal ohne das Laden des Powershell-Profils des Users, mit dem Parameter -NoProfile.
Und schau mal in dein Powershell-Profil-Skript, was da so alles drin steht. Dazu in einer Konsole eingeben:
start $profile
Das sollte dein Profil-Skript öffnen sofern vorhanden.

Grüße Uwe
DarkTrinity
DarkTrinity 27.04.2021 aktualisiert um 09:31:50 Uhr
Goto Top
Zitat von @colinardo:
Das ist erst mal Blödsinn x mal den selben Output zu erzeugen, wenn dann verändert man die Parameter $startid und $stopid auf den gewünschten Wert

In dem Fall wird aber ein leere Datei erzeugt und die Variable (die ID) hat den Wert 0. Daher habe ich das zu Testzwecken mal mit statisch probiert


DIe Fehlermeldung in der Konsole hatte mich auch gewundert, da ich diesen Snip nämlich ebenfalls nicht in dem Code gefunden habe Hinzugefügt habe ich diese Zeile jedenfalls nicht

Der Einwand mit dem Powershell Profil macht Sinn, es würde auch die unterschiedlichen Ergebnisse bei IDE und Konsole erklären. Ich gebe das bei uns mal an den zuständigen Kollegen weiter.

Wenn das geklärt ist mache ich damit weiter.
colinardo
colinardo 27.04.2021 aktualisiert um 09:45:50 Uhr
Goto Top
Zitat von @DarkTrinity:
In dem Fall wird aber ein leere Datei erzeugt und die Variable (die ID) hat den Wert 0
Dann hast du aber selbst die Variable $startid schon auf 0 geändert ... Diese müssen natürlich auf die Werte gesetzt werden für die IDs deren Seiten du exportieren willst, diese werden einfach hochgezählt. $start_id ist der Anfangswert und $stop_id ist der Endwert. $start_id..$stop_id ist ein einfacher FOR-Loop zählt also immer eins hoch bis $stop_id erreicht ist.
Wenn das geklärt ist mache ich damit weiter.
Alles klar.
DarkTrinity
DarkTrinity 27.04.2021 um 09:36:58 Uhr
Goto Top
Nein, ich habe die Werte von 128 zu 129 begrenzt:

# beginn with id
$start_id = 128
# stop with id
$stop_id = 129
# itterate over ids
colinardo
colinardo 27.04.2021 aktualisiert um 09:42:24 Uhr
Goto Top
Ach, Asche auf mein Haupt, Variablen-Tippfehler in Zeile 81, die Unterstriche fehlten in den Variablennamen, ist korrigiert, my fault sorry ... face-smile.
DarkTrinity
DarkTrinity 27.04.2021 um 09:52:45 Uhr
Goto Top
Urgs, da hätte ich auch selbst drauf kommen können - es sticht Dir ja schon fast ins Auge XD
Never mind face-smile
DarkTrinity
DarkTrinity 27.04.2021 aktualisiert um 11:05:16 Uhr
Goto Top
Also ....

Diese besagten Zeilen, über die sich die Powershell Konsole beschwert, sehe ich nicht in der Visual Studio IDE....

ABER: Mit einem cat web2pdf.ps1 sehe ich sie .... Schon unglaublich ....

Ich habe den in der Vidual Studio IDE für mich sichtbaren Code nun per Copy & Paste in Notepadd++ eingefügt, neu gespeichert und siehe da: Das Script läuft face-smile

Nochmal vielen Dank face-smile
colinardo
colinardo 27.04.2021 aktualisiert um 11:50:27 Uhr
Goto Top
Schuss ins Blaue: Vielleicht versehentlich zwei verschiedene Scripte mit dem selben Namen in unterschiedlichen Verzeichnissen bearbeitet, passiert mir auch ab und zu mal wenn man sich in der Bearbeitungsphase entscheidet die Files schon mal verschieben zu müssen, in der IDE aber noch das alte Ziel hinterlegt ist.
Das Script läuft
Na dann haben wir das Ziel ja erreicht face-smile.
Nochmal vielen Dank
Immer gerne.

Grüße Uwe
DPSERVER
DPSERVER 18.05.2021 um 08:30:41 Uhr
Goto Top
Hallo Uwe, auf der Suche nach einer etwas anderen Lösung bin ich auf deinen Beitrag hier gestoßen. Das funktionier perfekt. Leider habe ich nicht das fachliche Wissen, deinen Code auf meine Wünsche umzuschreiben. Ich möchte daher um deine Hilfe bitten.
Ich würde gern eine feste URL "https://URL.tld" in eine Verzeichnis ablegen. Allerdings mit Datum und Zeitstempel "E:\Ablage\dok1_datum_uhrzeit.pdf"
Das Script würde ich dann gern per Zeitsteuerung regelmäßig auslösen. Vielen Dank Frank
colinardo
colinardo 18.05.2021 um 09:19:18 Uhr
Goto Top
@DPSERVER du hast Post.
DarkTrinity
DarkTrinity 18.05.2021 um 11:45:59 Uhr
Goto Top
Hallo Uwe,

das Script ist toll . ich konnte soweit anpassen, daß es aus dem Onlinedokument daten zieht (Projektnummer, Projektzeitraum, etc) und diese in den Dateinamen der erzeugten PDF einfliessen lässt face-smile

Ein letzter Punkt will mir jedoch nicht gelingen - kann man das Script auch dahingehend anpassen, daß es den Extract auf einer entsprechenden Anzahl an A4 Seiten verteilt ?
colinardo
colinardo 18.05.2021 aktualisiert um 12:37:57 Uhr
Goto Top
Zitat von @DarkTrinity:
Ein letzter Punkt will mir jedoch nicht gelingen - kann man das Script auch dahingehend anpassen, daß es den Extract auf einer entsprechenden Anzahl an A4 Seiten verteilt ?
Kannst du in den Settings der Funktion anpassen
    # create conversion settings objects
    $html_format = New-Object Spire.Pdf.HtmlConverter.PdfHtmlLayoutFormat -Property @{
        Layout = [Spire.Pdf.Graphics.PdfLayoutType]::Paginate
    }
    # pdf page settings
    $settings = New-Object Spire.Pdf.PdfPageSettings -Property @{
        Margins = New-Object Spire.Pdf.Graphics.PdfMargins 10
        Size = [Spire.Pdf.PdfPageSize]::A4
        Orientation = 'Portrait'  
    }   
    # load html Parameter: URL, execute javascript, convert links, detect page breaks, settings, format
    $doc.LoadFromHTML($URL,$false,$false,$true,$settings,$html_format)
Grüße Uwe
DarkTrinity
DarkTrinity 18.05.2021 um 14:20:03 Uhr
Goto Top
Hi face-smile

vielen lieben Dank für die schnelle Antwort.

Hat wunderbar funktioniert, nachdem ich neben Deinem geposteten Snip noch folgende Zeile angepasst habe:
# load html Parameter: URL, execute javascript, convert links, detect page breaks, settings, format
    $doc.LoadFromHTML($URL,$false,$false,$true,$settings,$html_format)
paulmx
paulmx 01.10.2022, aktualisiert am 03.10.2022 um 11:24:09 Uhr
Goto Top
Hallo Uwe,

ich versuche mich gerade an deinem Skript. Leider läuft das CMD Fenster so schnell durch, dass ich nur im Vorbeifliegen etwas von "...konnte nicht gefunden werden..." erhaschen konnte.
Wie kann ich hier denn ein Protokoll für die Meldung, bzw den Fehler erstellen?

Viele Grüße,
Paul

Nachtrag:

Nach einem kleinen Anfängerfehler habe ich das Skript zum Laufen bringen können und es speichert viele, viele meiner Webseiten als Pdf. Genauer gesagt fast im Sekundentakt Eine. Und das scheinbar unendlich face-smile
Mein neues Problem: obwohl ich mich vorher auf der Webseite eingeloggt habe und dann den Pfad kopiert habe, scheitert das Skript scheinbar am Login und speichert immer nur die Hauptseite, wo sich der Login-Aufruf befindet.

Wie und an welcher Stelle kann ich denn den Login zur Webseite in das Skript einfügen?

Ich bin echt dankbar für jede Anregung und Idee, die mich weiter vorwärts bringt !!!

Viele Grüße,
Paul
colinardo
colinardo 02.10.2022 aktualisiert um 16:09:08 Uhr
Goto Top
Servus Paul.
Zitat von @paulmx:

Genauer gesagt fast im Sekundentakt Eine. Und das scheinbar unendlich face-smile
Der TO wollte eine Reihe von Artikeln herunterladen deswegen die Schleife von 1-10 oben!

Mein neues Problem: obwohl ich mich vorher auf der Webseite eingeloggt habe und dann den Pfad kopiert habe, scheitert das Skript scheinbar am Login und speichert immer nur die Hauptseite, wo sich der Login-Aufruf befindet.
Das ist normal denn was du im Browser machst interessiert die Bibliothek nicht, weil sie eine eigene Browser-Rendering-Engine mit eigener Session-Verwaltung und eigenen Cookies nutzt!


Wie und an welcher Stelle kann ich denn den Login zur Webseite in das Skript einfügen?
Das kommt darauf an welche Methode der Login nutzt. Wenn es eine Basic-Authentification ist, lassen sich die Credentials direkt in die URL einbauen als
https://USERNAME:PASSWORT@domain.tld
Wenn der Login jedoch aus einer Custom Form besteht geht das hiermit nicht.
Indem Fall müsstest du entweder zu einer Webbrowser Automation z.B. mittels Selenium greifen das habe ich hier schon mal gezeigt:

PowerShell: Einführung in die Webbrowser Automation mit Selenium WebDriver

Oder man macht den Login mittels POST/GET Requests manuell. Bedingt aber bei beiden Varianten das man weiß wie der Login genau vollzogen wird, ergo man braucht den HTML-Quellcode und per Browser-Developer Tools untersucht man den Login auf dem Netzwerktab. Ein Universalrezept gibt es hier nicht, das kommt immer auf die Webseite drauf an.

Bitte aber den Thread hier nicht weiter übernehmen, dieser wurde als erledigt gekennzeichnet.

Für mehr Informationen bitte PN oder neuen Beitrag aufmachen. Merci.

Grüße Uwe