Webseite inkl. aller Bilder als "Single-HTML-File" speichern - nicht als MHTML

thulium

Aktives Mitglied
Thread Starter
Registriert
12.11.2011
Beiträge
2.069
Moin.

Leider hat sich das Format MHTML nicht durchgesetzt und man kann es weder in Safari für iOS oder macOS öffnen.

Frage also:
Gibt es ein Tool, welches alle verlinkten Bilder einer Website in BASE64 umwandelt und in das zu speichernde HTML-Dokument integriert?

Ziel:
Zum Beispiel die Weitergabe einer vollständigen Webseite als eine einzige HTML-Datei, wenn es sich zum Beispiel um eine Webseite hinter einer Bezahlschranke handelt.
Aber auch die Dokumentation eines Inhaltes in einem spezifischen Zustand - zum Beispiel für eine wissenschaftliche Arbeit.

PDF ist ein für das Lesen am Display absolut ekliges Format.

Falls jemand was kennt: ich freue mich über einen Hinweis.
 

walfreiheit

Aktives Mitglied
Registriert
06.06.2004
Beiträge
33.013
Wie soll eine HTML-Datei Bilder beinhalten? Das mit der Ein-Datei-Lösung wird nicht klappen.
du kannst komplette Seiten aber mit Software wie SiteSucker herunterladen.
 

thulium

Aktives Mitglied
Thread Starter
Registriert
12.11.2011
Beiträge
2.069
@tocotronaut
Ich verwende Safari selber nie, daher weiß ich nicht, ob die Readeransicht speicherbar ist.
Bisher fand ich dafür keine Funktion.

@Maulwurfn
Eine HTML-Version kann Bilder, wie erwähnt, in der Kodierung BASE64, enthalten. Es geht also durchaus.
Siehe: https://wiki.selfhtml.org/wiki/Grafik/Grafiken_mit_Data-URI

Mich interessiert ausschließlich eine "Ein-Datei-Lösung".

Das Speichern einer Website als HTML-Datei plus Ordner für die eingebettenen Inhalt geht ja problemlos ohne Extra-Tool in Firefox oder Chrome.
 

dooyou

Aktives Mitglied
Registriert
03.12.2006
Beiträge
4.825
@thulium
Ja, aber das geht doch nur, wenn z. B. eine SVG-Grafik, also z. B. ein Logo, direkt so in den Code eingebettet wurde. Also nicht hinterher. Mit Fotos hat das dann auch nichts zu tun.

Sofern wir immer noch von HTML und nicht irgendwelchen Sonderlösungen/Formate reden.
 

thulium

Aktives Mitglied
Thread Starter
Registriert
12.11.2011
Beiträge
2.069
@dooyou
Nein, auch JPGs können als Base64 kodiert werden.

Da es darum geht einen Single-File zu erzeugen, kann dort beliebiges hineingeschrieben werden, Du missverstehst also noch, worum es hier geht.
 

little_pixel

Aktives Mitglied
Registriert
06.06.2006
Beiträge
4.634
Nette kleine Idee für eine Safari-Erweiterung.

Vielleicht ein Umweg:

In ein PDF drucken und dann mit einem freien Konverter zu EPUB umbauen lassen.

Das würde sich dann auf jedem Display dynamisch anpassen und die Grafiken wären auch drin.

Viele Grüße
 

thulium

Aktives Mitglied
Thread Starter
Registriert
12.11.2011
Beiträge
2.069
@little_pixel
Danke für die Idee. Klingt ein bißchen nach "von hinten durch's Knie in's Auge".
Ist nicht als Kritik gemeint. Ich behalte das mal als absoluten Notbehelf im Kopf.

Vielleicht findet sich ja noch ein Tool.
 

fox78

Aktives Mitglied
Registriert
02.02.2004
Beiträge
2.721
Safari: Ablage -> Sichern unter -> Webarchive ?

Kann man aber nur mit Safari wieder öffnen/betrachten
 

warnochfrei

Aktives Mitglied
Registriert
02.03.2019
Beiträge
1.589
Ich habe jetzt schnell mal was programmiert:

Code:
package main

import (
    "bufio"
    "encoding/base64"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "os"
    "regexp"
    "strings"
  
    "github.com/mitchellh/go-homedir"
)

func usage() {
    fmt.Fprintf(os.Stderr, "Usage: %s -url [URL] [-target [TARGET]]\n", os.Args[0])
    flag.PrintDefaults()
    os.Exit(1)
}

func ReplaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string {
    result := ""
    lastIndex := 0
    for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
        groups := []string{}
        for i := 0; i < len(v); i += 2 {
            groups = append(groups, str[v[i]:v[i+1]])
        }
        result += str[lastIndex:v[0]] + repl(groups)
        lastIndex = v[1]
    }
    return result + str[lastIndex:]
}

func main() {
    homedir, err := homedir.Dir()
    if err != nil {
        fmt.Printf("Error trying to determine your home directory: %s\n", err)
        os.Exit(1)
    }
  
    var urlToGet string
    var target string
  
    flag.StringVar(&urlToGet, "url", "", "URL to download.")
    flag.StringVar(&target, "target", string(homedir), "Your target folder.")
    flag.Parse()
  
    if urlToGet == "" {
        usage()
    }
  
    // Some parsing:
    u, err := url.Parse(urlToGet)
    if err != nil {
        fmt.Printf("Error parsing '%s': %s\n", urlToGet, err)
        os.Exit(1)
    }
  
    resp, err := http.Get(urlToGet)
    if err != nil {
        fmt.Printf("Error trying to download '%s': %s\n", urlToGet, err)
        os.Exit(1)
    }
  
    defer resp.Body.Close()
  
    respData, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Error trying to read the response data: %s\n", err)
        os.Exit(1)
    }

    respString := string(respData)
  
    targetFileName := fmt.Sprintf("%s/[%s] %s.htm", target, u.Host, strings.Replace(u.Path, "/", "_", -1))

    // 1. Replace images in <respString>.
    reImg := regexp.MustCompile("(<img (.*?)(src=\"([^\"]+)\")(.*?)>)")
    respStringWithNoImages := ReplaceAllStringSubmatchFunc(reImg, respString, func(groups []string) string {
        // groups[1] is an image, groups[3] is the src parameter,
        // groups[4] is the src path.
        // If groups[4] begins with /, it is a relative path to u.Host,
        // if it does not, it is a relative path to u.Path.
        // Replace groups[3] by a data URL parameter anyway.
        isRelative := true
        if (strings.HasPrefix(groups[4], "/")) {
            isRelative = false
        }
      
        var imagePath string
        var imageType string
      
        if (isRelative) {
            imagePath = fmt.Sprintf("%s://%s%s%s", u.Scheme, u.Host, u.Path, groups[4])
        } else {
            imagePath = fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, groups[4])
        }
      
        if (strings.HasSuffix(groups[4], ".png")) {
            imageType = "image/png"
        } else if (strings.HasSuffix(groups[4], ".jpg") || strings.HasSuffix(groups[4], ".jpeg")) {
            imageType = "image/jpeg"
        } else if (strings.HasSuffix(groups[4], ".gif")) {
            imageType = "image/gif"
        } else {
            // Unknown type.
            fmt.Printf("Skipping '%s': unknown file type\n", imagePath)
            return fmt.Sprintf("<em>[MISSING: %s]</em>", imagePath)
        }
      
        // Download and convert:
        img, imgerr := http.Get(imagePath)
        if imgerr != nil {
            // Skip this image.
            fmt.Printf("Skipping '%s': %s\n", imagePath, imgerr)
            return fmt.Sprintf("<em>[MISSING: %s]</em>", imagePath)
        }
        defer img.Body.Close()
      
        reader := bufio.NewReader(img.Body)
        content, _ := ioutil.ReadAll(reader)
        encoded := base64.StdEncoding.EncodeToString(content)

        // Keep the parts before and after "src=" for the result.
        return fmt.Sprintf("<img %s src=\"data:image/%s;base64,%s\" %s />", groups[2], imageType, encoded, groups[5])
    })
  
    // 2. Write <respStringWithNoImages> into <targetFileName>.
    f, err := os.Create(targetFileName)
    if err != nil {
        fmt.Printf("Could not create the target file '%s': %s\n", targetFileName, err)
        os.Exit(1)
    }
    defer f.Close()
  
    createFile, err := f.WriteString(respStringWithNoImages)  
    if err != nil {
        fmt.Printf("Could not write to the target file '%s': %s\n", targetFileName, err)
        os.Exit(1)
    }
  
    fmt.Printf("Done. Wrote %d bytes.\n", createFile)
}

Bauen: go mod init macuser.de/singlefiledownloader ; go build
Verwendung: singlefiledownloader -url "http://irgendwas.de"

Erzeugt 'ne HTML-Datei, in der alle Bilder als base64 eingebunden sind. Nur JavaScripts sind noch extern. Da möge ein anderer rumbasteln. Oder ich, aber nicht mehr heute.

Getestet unter Windows 10, sollte aber auf'm Mac auch nicht meckern.
 

warnochfrei

Aktives Mitglied
Registriert
02.03.2019
Beiträge
1.589
Na gut, hier die vollständige Variante (ich kann's ja doch nicht lassen), die auch JS und CSS umwandelt:

Code:
package main

import (
    "bufio"
    "encoding/base64"
    "flag"
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "os"
    "regexp"
    "strings"
  
    "github.com/mitchellh/go-homedir"
)

func Usage() {
    fmt.Fprintf(os.Stderr, "Usage: %s -url [URL] [-target [TARGET]]\n", os.Args[0])
    flag.PrintDefaults()
    os.Exit(1)
}

func ReplaceAllStringSubmatchFunc(re *regexp.Regexp, str string, repl func([]string) string) string {
    result := ""
    lastIndex := 0
    for _, v := range re.FindAllSubmatchIndex([]byte(str), -1) {
        groups := []string{}
        for i := 0; i < len(v) && v[i+1] > -1; i += 2 {
            groups = append(groups, str[v[i]:v[i+1]])
        }
        result += str[lastIndex:v[0]] + repl(groups)
        lastIndex = v[1]
    }
    return result + str[lastIndex:]
}

func MakeAbsolutePath(u *url.URL, s string, isRelative bool) string {
    if (strings.HasPrefix(s, "http:") || strings.HasPrefix(s, "https:")) {
        // This is already an absolute path.
        return s
    }
    
    if (isRelative) {
        return fmt.Sprintf("%s://%s%s%s", u.Scheme, u.Host, u.Path, s)
    } else {
        return fmt.Sprintf("%s://%s%s", u.Scheme, u.Host, s)
    }
}

func DeleteEmptySlices(s []string) []string {
    var r []string
    for _, str := range s {
        if str != "" {
            r = append(r, str)
        }
    }
    return r
}

func main() {
    homedir, err := homedir.Dir()
    if err != nil {
        fmt.Printf("Error trying to determine your home directory: %s\n", err)
        os.Exit(1)
    }
  
    var urlToGet string
    var target string
  
    flag.StringVar(&urlToGet, "url", "", "URL to download.")
    flag.StringVar(&target, "target", string(homedir), "Your target folder.")
    flag.Parse()
  
    if urlToGet == "" {
        Usage()
    }
  
    // Some parsing:
    u, err := url.Parse(urlToGet)
    if err != nil {
        fmt.Printf("Error parsing '%s': %s\n", urlToGet, err)
        os.Exit(1)
    }
  
    resp, err := http.Get(urlToGet)
    if err != nil {
        fmt.Printf("Error trying to download '%s': %s\n", urlToGet, err)
        os.Exit(1)
    }
  
    defer resp.Body.Close()
  
    respData, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Error trying to read the response data: %s\n", err)
        os.Exit(1)
    }

    respString := string(respData)
    targetFileName := fmt.Sprintf("%s/[%s] %s.htm", target, u.Host, strings.Replace(u.Path, "/", "_", -1))
    
    fmt.Printf("Downloading '%s' to '%s'...\n", urlToGet, targetFileName)

    // 1. Replace images in <respString>.
    fmt.Println("Converting images.")
    reImg := regexp.MustCompile("(<img (.*?)(src=\"([^\"]+)\")(.*?)>)")
    respStringWithNoImages := ReplaceAllStringSubmatchFunc(reImg, respString, func(groups []string) string {
        // groups[1] is an image, groups[3] is the src parameter,
        // groups[4] is the src path.
        // If groups[4] begins with /, it is a relative path to u.Host,
        // if it does not, it is a relative path to u.Path.
        // Replace groups[3] by a data URL parameter anyway.
        isRelative := true
        if (strings.HasPrefix(groups[4], "/")) {
            isRelative = false
        }
      
        var imageType string
        imagePath := MakeAbsolutePath(u, groups[4], isRelative) 
      
        if (strings.HasSuffix(groups[4], ".png")) {
            imageType = "image/png"
        } else if (strings.HasSuffix(groups[4], ".jpg") || strings.HasSuffix(groups[4], ".jpeg")) {
            imageType = "image/jpeg"
        } else if (strings.HasSuffix(groups[4], ".gif")) {
            imageType = "image/gif"
        } else {
            // Unknown type.
            fmt.Printf("Skipping '%s': unknown file type\n", imagePath)
            return fmt.Sprintf("<em>[MISSING: %s]</em>", imagePath)
        }
      
        // Download and convert:
        img, imgerr := http.Get(imagePath)
        if imgerr != nil {
            // Skip this image.
            fmt.Printf("Skipping '%s': %s\n", imagePath, imgerr)
            return fmt.Sprintf("<em>[MISSING: %s]</em>", imagePath)
        }
        defer img.Body.Close()
      
        reader := bufio.NewReader(img.Body)
        content, _ := ioutil.ReadAll(reader)
        encoded := base64.StdEncoding.EncodeToString(content)

        // Keep the parts before and after "src=" for the result.
        return fmt.Sprintf("<img %s src=\"data:image/%s;base64,%s\" %s />", groups[2], imageType, encoded, groups[5])
    })
    
    // 3. Inline CSS and JS in <respStringWithNoImages>.
    fmt.Println("Converting CSS.")
    reImgCss := regexp.MustCompile("(?:<link (.*?rel=\"stylesheet\".*?href=\"([^\"]+)\".*?|.*?href=\"([^\"]+)\".*?rel=\"stylesheet\".*?)>)")
    respStringWithNoCSS := ReplaceAllStringSubmatchFunc(reImgCss, respStringWithNoImages, func(groups []string) string {
        // The last non-empty item in groups[] is the path.
        groups = DeleteEmptySlices(groups)
        lastItem := groups[len(groups)-1]
        
        isRelative := true
        if (strings.HasPrefix(lastItem, "/")) {
            isRelative = false
        }
        
        cssPath := MakeAbsolutePath(u, lastItem, isRelative)
        
        // Download and append:
        css, csserr := http.Get(cssPath)
        if csserr != nil {
            // Skip this script.
            fmt.Printf("Skipping '%s': %s\n", cssPath, csserr)
        }
        defer css.Body.Close()
        
        reader := bufio.NewReader(css.Body)
        content, _ := ioutil.ReadAll(reader)
        
        return fmt.Sprintf("\n<style type='text/css'>%s</style>\n", content)
    })
    
    fmt.Println("Converting JavaScript.")
    reImgJs := regexp.MustCompile("(?:<script [^>]*?src=\"([^\"]+)\")[^>]*?>")
    respStringWithNoExternalResources := ReplaceAllStringSubmatchFunc(reImgJs, respStringWithNoCSS, func(groups []string) string {
        // The last item in groups[] is the path.
        lastItem := groups[len(groups)-1]
        
        isRelative := true
        if (strings.HasPrefix(lastItem, "/")) {
            isRelative = false
        }
        
        jsPath := MakeAbsolutePath(u, lastItem, isRelative)
        
        // Download and append:
        js, jserr := http.Get(jsPath)
        if jserr != nil {
            // Skip this script.
            fmt.Printf("Skipping '%s': %s\n", jsPath, jserr)
        }
        defer js.Body.Close()
        
        reader := bufio.NewReader(js.Body)
        content, _ := ioutil.ReadAll(reader)
        
        return fmt.Sprintf("\n<script language='text/javascript'>%s</script>\n", content)
    })
  
    // 4. Write <respStringWithNoExternalResources> into <targetFileName>.
    f, err := os.Create(targetFileName)
    if err != nil {
        fmt.Printf("Could not create the target file '%s': %s\n", targetFileName, err)
        os.Exit(1)
    }
    defer f.Close()
  
    createFile, err := f.WriteString(respStringWithNoExternalResources)  
    if err != nil {
        fmt.Printf("Could not write to the target file '%s': %s\n", targetFileName, err)
        os.Exit(1)
    }
  
    fmt.Printf("Done. Wrote %d bytes.\n", createFile)
}
 

thulium

Aktives Mitglied
Thread Starter
Registriert
12.11.2011
Beiträge
2.069
Ganz herzlichen Dank erstmal.

Du schreibst: "go mod init macuser.de/singlefiledownloader ; go build"

Ich habe noch nie per Kommandozeile ein Skript verarbeitet. Auf welche Weise muss das Skript gespeichert werden? Als Textdatei mit dem Namen "singlefiledownloader" ohne Endung?

Zum Ausführen:
Bist Du sicher, dass man damit eine Webseite aufrufen kann, die hinter dem Login einer Bezahlschranke liegt?
 

warnochfrei

Aktives Mitglied
Registriert
02.03.2019
Beiträge
1.589
Auf welche Weise muss das Skript gespeichert werden? Als Textdatei mit dem Namen "singlefiledownloader" ohne Endung?

Irgendwas mit der Endung ".go".

Also:

Code:
$ ls
irgendwas.go

In diesem Ordner:

Code:
$  go mod init macuser.de/singlefiledownloader ; go build

Sollte gehen.

Bist Du sicher, dass man damit eine Webseite aufrufen kann, die hinter dem Login einer Bezahlschranke liegt?

Nein, aber alle anderen.
 

thulium

Aktives Mitglied
Thread Starter
Registriert
12.11.2011
Beiträge
2.069
@Schiffversenker
Mir geht es um die Weitergabe von Inhalten, wo man dem Empfänger keinerlei Erklärung zum Format geben muss.

@warnochfrei
Danke für Deinen Hinweis.
OK, ich dachte mir schon, dass es mit dem Verfahren für Bezahlschranken nicht gehen kann.

Ich guck die nächsten Tage mal, ob es mir gelingt für eine normale Webseite Dein Skript anzuwenden.
Aber letztlich tue ich mit mit der Kommandozeile schwer, weil ich sie ansonsten nie verwende.

Trotzdem Danke für Deine Arbeit :)
 

warnochfrei

Aktives Mitglied
Registriert
02.03.2019
Beiträge
1.589
Ich guck die nächsten Tage mal, ob es mir gelingt für eine normale Webseite Dein Skript anzuwenden.

Hier eine vorkompilierte Version von meinem Catalina:
https://ufile.io/abl95c6x

Aber letztlich tue ich mit mit der Kommandozeile schwer, weil ich sie ansonsten nie verwende.

Ich hatte tatsächlich überlegt, ob ein GUI sinnvoll wäre. Aber für ein Textfeld und einen Knopf erschien mir das etwas unsinnig.
 

thulium

Aktives Mitglied
Thread Starter
Registriert
12.11.2011
Beiträge
2.069
Ich habe deine vorkompilierte version "sfd" nach "downloads/test/" kopiert.

Danach in der Kommandozeile auf Catalina:

cd downloads
cd test
sfd -url "https://macuser.de"

Ergebnis:

command not found
 
Oben