FAZ to Kindle in Python

Nachdem ich durch eine Werbeaktion an ein einjaehriges E-Paper Abo von der Frankfurter Allgmeinen Zeitung gekommen bin, wollte ich das ganze nun gemuetlich auf meinem Kindle geniessen. Allerdings ist das wohl gar nicht so einfach. Zwar gibt es ueber Amazon direkt ein FAZ Abo, dieses ist aber nicht kompatibel mit der E-Paper-Variante. Letztere besteht eigentlich nur aus der Printversion als PDF. Diese bekommt man aber nicht automatisch auf sein Geraet, sondern muss sich erst online einloggen, das ganze herunterladen und auf den Kindle kopieren.

faz2kindle

Viel bequemer waere es doch, die aktuellste Ausgabe immer direkt auf den Kindle geschickt zu bekommen. Morgens kurz das W-LAN anschalten und schon erscheint die aktuellste Ausgabe wie durch Zauberhand auf dem Geraet.

Im Prinzip muessen also drei Schritte automatisiert werden.

  1. Einloggen auf der FAZ-Aboseite
  2. Herunterladen der neusten Ausgabe(n)
  3. Senden an den Kindle

Da python fuer hack-n-slay bekannt ist und viele Bibliotheken fuer diese Art von Problemen bereits mitbringt, wird es im folgenden die Programmiersprache der Wahl.

Einloggen auf www.faz.net/e-paper

Mit dem requests Package ist es moeglich relativ einfach das Surfen auf Webseiten zu automatisieren. Schauen wir uns also mal an, was man fuer den Einloggvorgang denn automatisieren muss. Links auf der Seite sehen wir das Loginfeld.

<form action="https://www.faz.net/membership/loginNoScript" method="post" class="LoginFrm" id="loginFormFaz">
    <input type="hidden" value="/mein-faz-net/" name="loginUrl" id="loginUrl">
    <input type="hidden" value="/e-paper/" name="redirectUrl" id="redirectUrl">

    <input type="text" value="Benutzername" class="Text" name="loginName">
    <div id="loginEPFaz" style="position: relative;">
        <input type="password" onfocus="$('#loginEPFaz span.pwinput_overlay').hide();this.select();return false;" onblur="if(this.value =='') $('#loginEPFaz span.pwinput_overlay').show(); return false;" style="color: #999; " autocomplete="on" name="password" id="passwordFaz" class="Text">
        <span style="display: inline; " onclick="$(this).hide();$(this).prev().focus();" class="pwinput_overlay">Passwort</span>
    </div>
    <div class="right">
        <input type="submit" value="GO" class="SubmitBtn">
    </div>
    <br /><br /><br />
    <span style="font-size: 12px">Sie sind noch nicht registriert? Jetzt kostenlos und schnell <a href="https://www.faz.net/mein-faz-net/">hier registrieren</a>
</form>

Im Prinzip gibt es nur 4 Felder, die mit Daten befuellt werden muessen. Das requests Package von python erlaubt es Cookies und Session-Informationen automatisch in einer eigenen Session mitzuspeichern, weswegen wir uns darueber keine Gedanken machen muessen. Der Einloggvorgang kann also mit ein paar Zeilen abgehandelt werden. Dazu bauen wir uns eine kleine Klasse.

class FazLoader(object):
    def __init__(self):
        self.s = requests.Session()
        data = {"loginUrl": "/mein-faz-net/",
                  "redirectUrl": "/e-paper/",
                  "loginName" : FAZ_USER,
                  "password" : FAZ_PASS
                  }
        req = self.s.post("https://www.faz.net/membership/loginNoScript", data)
        req = self.s.get("http://www.faz.net/e-paper/")

Wir erstellen also eine neue Session und senden die Eingaben, die wir normalerweise ins Formular schreiben wuerden mit. Nach dem Login surfen wir in der selben Session zur e-paper Seite. im Session-Objekt werden nun die entsprechenden Cookies mit den Anmeldeinformationen vorgehalten und werden automatisch bei weiteren Seitenaufrufen immer mitgesendet.

Downloaden der aktuellsten Ausgabe

Jetzt wird es schon etwas komplizierter. Nach erfolgreichem Einloggen zeigt die FAZ-Seite eine Onlineversion der aktuellen Ausgabe sowie Links zum Herunterladen als PDF an. Wenn wir beim Aufbau der Seite die Developer Tools von Chrome mitlaufen lassen, sehen wir im Network-Tab, dass unter anderem ein AJAX-Request an http://www.faz.net/e-paper/epaper/list/FAZ gesendet wird. Von dort erhalten wir eine Liste der aktuell verfuegbaren Ausgaben im JSON-Format.

[{"displayDatum":"01.11.2013",
"ausgaben":
[{"displayDatum":"01.11.2013","typ":"FAZ_RMZ","url":"FAZ_RMZ/2013-11-01","thumbnailUrl":"epaper/thumb/FAZ_RMZ/2013-11-01/1.jpg"},{"displayDatum":"01.11.2013","typ":"FAZ","url":"FAZ/2013-11-01","thumbnailUrl":"epaper/thumb/FAZ/2013-11-01/1.jpg"}],
"thumbnailUrl":"epaper/thumb/FAZ_RMZ/2013-11-01/1.jpg"},

{"displayDatum":"31.10.2013","ausgaben":
[{"displayDatum":"31.10.2013","typ":"FAZ_RMZ","url":"FAZ_RMZ/2013-10-31","thumbnailUrl":"epaper/thumb/FAZ_RMZ/2013-10-31/1.jpg"},{"displayDatum":"31.10.2013","typ":"FAZ","url":"FAZ/2013-10-31","thumbnailUrl":"epaper/thumb/FAZ/2013-10-31/1.jpg"}],
"thumbnailUrl":"epaper/thumb/FAZ_RMZ/2013-10-31/1.jpg"},
...]

In der Liste finden sich Informationen darueber, welche Ausgaben verfuegbar sind und von welchen Tagen. Prinzipiell gibt es die RMZ – die Rhein-Main-Variante und die normale FAZ-Variante. Von dieser Liste können wir uns also das Datum jeder verfuegbaren Ausgabe merken, ueber das wir die Ausgabe dann beim Download identifizieren. Im Code sieht das dann folgendermassen aus. Wir rufen die genannte URL auf, parsen die Infos als JSON-Daten und lesen fuer jede Ausgabe das Datum aus. Unsere Downloadmethode wird spaeter das Datum und ein true-false-Flag bekommen, ob wir die RMZ-Variante wollen.

    def downloadAvailable(self):
        url = "http://www.faz.net/e-paper/epaper/list/FAZ"
        req = self.s.get(url)
        if(req.status_code != 200):
            return False
        json_info = json.loads(req.text)
        for i in range(0, len(json_info)):
            # check all available publications
            entities = json_info[i]["ausgaben"]
            for entity in entities:
                # extract date
                date = entity["displayDatum"]
                day, month, year = date.split(".")
                day = int(day)
                month = int(month)
                year = int(year)
                if entity["typ"] == "FAZ" and DOWNLOAD_FAZ:
                    self.download(year, month, day, False)
                if entity["typ"] == "FAZ_RMZ" and DOWNLOAD_RMZ:
                    self.download(year, month, day, True)

Wenn auf der Seite eine neue Ausgabe angeklickt wird, geht eine Anfrage an http://www.faz.net/e-paper/epaper/overview/FAZ/YYYY-MM-DD bzw. an http://www.faz.net/e-paper/epaper/overview/FAZ_RMZ/YYYY-MM-DD. Dort koennen wir das Datum unserer Ausgabe einsetzen und erhalten wiederum JSON-Informationen ueber die Ausgabe.

{"seiten":[...],"ausgabePdf":"20131101FAZ0033.pdf","formattedSize":"16 MB","rmzAvailable":true,"nonRmzAvailable":true}

Dort interessiert uns eigentlich nur der Name der PDF-Datei um den fertigen Downloadlink zu generieren. Dieser hat die Form http://www.faz.net/e-paper/epaper/pdf/FAZ/YYYY-MM-DD/PDFNAME.pdf bzw. http://www.faz.net/e-paper/epaper/pdf/FAZ_RMZ/YYYY-MM-DD/PDFNAME.pdf.

    def getDownloadLink(self, year, month, day, rmz = False):
        s_year = str(year)
        s_month = "%02d" % (month)
        s_day = "%02d" % (day)
        if(rmz):
            url_base = "http://www.faz.net/e-paper/epaper/pdf/FAZ_RMZ/"
            url = "http://www.faz.net/e-paper/epaper/overview/FAZ_RMZ/%s-%s-%s" % (s_year, s_month, s_day)
        else:
            url_base = "http://www.faz.net/e-paper/epaper/pdf/FAZ/"
            url = "http://www.faz.net/e-paper/epaper/overview/FAZ/%s-%s-%s" % (s_year, s_month, s_day)
        req = self.s.get(url)

        # if status not ok, return false
        if(req.status_code != 200):
            return False

        json_info = json.loads(req.text)
        pdf_name = json_info["ausgabePdf"]

        dl_url = "%s%s-%s-%s/%s"  % (url_base, s_year, s_month, s_day, pdf_name)
        return dl_url

Ab diesem Punkt koennen wir uns einloggen, eine Liste der verfuegbaren Ausgaben abrufen und einen Downloadlink anhand des Datums der Ausgabe generieren. Im naechsten Schritt muss die PDF-Datei lediglich heruntergeladen werden.

    def download(self, year, month, day, rmz = False):
        url = self.getDownloadLink( year, month, day, rmz)

        # if link could not be extracted
        if(not url):
            print "No download link could be found for %d-%02d-%02d" % (year, month, day)
            return False

        # get file name from url
        filename = self.basepath + url.split('/', )[-1]
        if os.path.isfile(filename):
            print "File " + filename +" already exists"
            return False

        print "Downloading to " + filename
        req = self.s.get(url,stream=True)
        f = open(filename, "wb")
        for chunk in req.iter_content(chunk_size=20*1024):
            if chunk: # filter out keep-alive new chunks
                f.write(chunk)
                f.flush()
        f.close()
        #TODO: Send downloaded file to Kindle

Unsere Klasse kann jetzt bereits alle verfuegbaren Ausgaben herunterladen und ueberspringt bereits herunter geladene Ausgaben, sodass bei jedem Aufruf der downloadAvailable() Methode ein inkrementelles Update durchgefuehrt wird. Im Prinzip fehlt lediglich das Senden der neu herunter geladenen Ausgaben an den Kindle.

Ausgabe an den Kindle senden

Amazon erlaubt das Senden von Emails mit angehaengten Dokumenten an eine von Amazon bereitgestellte Emailadresse. Diese kann man unter Persoenliche Dokumente in der Kindleverwaltung festlegen. Zusaetzlich muss man die Emailadresse, von der man erwartet, Dokumente zu bekommen auch angeben um moeglichen Spam zu verhindern. Man sendet also von meine@privatemail.de ein Dokument an meinkindle@kindle.com und das angehaengte Dokument wird dann von Amazon an den Kindle gesendet. Sobald selbiger dann im Wifi ist, wird das Dokument automatisch heruntergeladen. Wir erweitern also die Klasse um eine Methode, die eine Mail mit unserem Dokument an die Kindleadresse schickt. Dazu benutze ich einen Gmailaccount, weil der ausreichend grosse Anhaenge erlaubt. GMAIL_USER ist dabei die Gmailadresse in der Form bla@gmail.com

    def mail(self, to, attach_file):
        msg = MIMEMultipart()

        msg['From'] = GMAIL_USER
        msg['To'] = to
        msg['Subject'] = "UPDATED FAZ"

        fname = os.path.basename(attach_file)
        text = "New kindle file " + fname
        msg.attach(MIMEText(text))

        # read and encode file for email use
        part = MIMEBase('application', 'octet-stream')
        f = open(attach_file, 'rb')
        data = f.read()
        f.close()
        part.set_payload(data)
        Encoders.encode_base64(part)
        part.add_header('Content-Disposition',
                'attachment; filename="%s"' % fname)
        msg.attach(part)

        #connect to gmail server
        mailserv = smtplib.SMTP("smtp.gmail.com", 587)
        mailserv.ehlo()
        # start encryption
        mailserv.starttls()
        mailserv.ehlo()
        # send login data
        mailserv.login(GMAIL_USER, GMAIL_PASS)
        # send email
        mailserv.sendmail(GMAIL_USER, to, msg.as_string())
        mailserv.close()

Jetzt erweitern wir unsere Downloadfunktion um einen Aufruf der Mailfunktion, die das heruntergeladene Dokument an Amazon schickt. KINDLE_MAIL repraesentiert die Amazonmailadresse fuer persoenliche Dokumente.

    def download(self, year, month, day, rmz = False):
        url = self.getDownloadLink( year, month, day, rmz)

        # if link could not be extracted
        if(not url):
            print "No download link could be found for %d-%02d-%02d" % (year, month, day)
            return False

        # get file name from url
        filename = self.basepath + url.split('/', )[-1]
        if os.path.isfile(filename):
            print "File " + filename +" already exists"
            return False

        print "Downloading to " + filename
        req = self.s.get(url,stream=True)
        f = open(filename, "wb")
        for chunk in req.iter_content(chunk_size=20*1024):
            if chunk: # filter out keep-alive new chunks
                f.write(chunk)
                f.flush()
        f.close()

        if(DOWNLOAD_TO_KINDLE):
            print "Sending " + filename + " to " + KINDLE_MAIL
            self.mail(KINDLE_MAIL, filename)
        return True

Fully Integrated Cloud Service

Um das ganze jetzt fully integrated automatisiert als cloud-Service laufen zu lassen *scnr*, habe ich auf dem Debianserver, auf dem das Skript laufen soll einfach einen cronjob in die /etc/crontab eingetragen. Dieser fuehrt das Skript taeglich 21:30 aus. 21 Uhr wird auf faz.net jeweils die Ausgabe des Folgetages zur Verfuegung gestellt, weshalb so zeitnah die aktuellste Variante an den Kindle geschickt wird.

# automatically download new faz half past 9
30 21   * * *   pete    python /home/pete/fazload/fazload.py

Ich habe das ganze noch ein wenig aufgehuebscht und auf github gepostet. Da das nur ein kleiner Nachmittagshack war, uebernehme ich keine Verantwortung fuer Qualitaet und Korrektheit.

https://github.com/peteh/faz2kindle

7 thoughts on “FAZ to Kindle in Python

  1. Hallo, vielen lieben Dank für das Skript, ein Traum würde für mich als Abonnent des epapers war. Leider kommt beim Download die folgende Fehlermeldung:

    403 Forbidden

    Error 403 Forbidden
    Forbidden
    Guru Meditation:
    XID: 810385140

    Varnish cache server

    Kann man das Skript anpassen? Leider ist Python nicht mein Ding, war schon froh es und einige fehlende Pakete installieren zu können. Ich glaube die FAZ wehrt sich gegen Download Skripte. Irgendwie versuchen die das zu erkennen…

  2. ok wenn man sich beim PW nicht vertippt funktionierts. Hurra! Es fehlt aber scheinbar die Sonntagsausgabe FAS. Oder sitzt das Problem wieder vor dem Rechner?

  3. die Änderung von FAZ in FAS im Python Kode bei den URLs führt zum Erfolg. Nochmals vielen, vielen Dank.

  4. Tolle Sache! Genau so etwas suche ich. Jedoch habe ich keinen Kindle und müsste alles “nur” via FTP in einen Ordner speichern, damit ich es unterwegs bequem abrufen kann. Ist viel verlangt, aber kannst Du mir da weiterhelfen?
    Danke.

  5. Super Sache, genau was ich brauche. Funktioniert alles bis auf die PDF’s selbst: die sind alles nur 253(!) bytes gross….

Leave a Reply to Andreas Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.