RSS-Feeds mit Python und Scrapy erzeugen

von

Mit Scrapy Daten erfassen und mit Yattag als RSS-Feed ausgeben

Excerpt

I want to present a quick way to generate an RSS file with Python in this blogpost. For this, I am using the tiny Python library Yattag. Like a swiss-knife, it has a bunch of methods which makes it a really powerful tool.

 

Problemstellung

Aus Daten, die in einer Webseite (z.B. eine Enterprise Anwendung, die nicht weiterentwickelt werden soll) generiert und angezeigt werden, soll ein RSS-Feed erstellt werden, der die Daten zur weiteren Verarbeitung in anderen Anwendungen verfügbar macht. Für das sog. Web Scraping gibt es eine große Anzahl an Tools. Hier im Blog haben wir bisher Kofax Kapow [1] und Scrapy [2] mit ScrapingHub [3] vorgestellt. Die Besonderheit im vorliegenden Fall liegt darin, in bestimmten Fällen bestimmte XML-Elemente von RSS-Items mit Character Data [4] (CDATA) anzugeben.

Sucht man im Scrapy-Umfeld nach dem Stichwort "RSS", findet man relativ schnell die beiden Python-Projekte scrapy-rss-exporter [5] und scrapy-rss [6]. Beim ersteren Projekt handelt es sich um einen Item Exporter, während es sich bei letzterem um eine Item Pipeline handelt. Im Endeffekt erzeugen beide Lösungen RSS-Dateien, jedoch ist der Zeitpunkt der Generierung im Scraping-Prozess ein anderer. scrapy-rss ist sehr mächtig und bietet eine große Funktionsvielfalt. So kann z.B. ein eigenes RssItem definiert und eigene Namespaces verwendet werden. Wenn es aber speziell um Character Data geht, sind seine Fähigkeiten limitiert.

Lösungsansatz und Umsetzung

Da es sich einerseits um ein kleines Projekt handelt und andererseits die Lösung schnell umgesetzt werden soll, habe ich mich zwar für Scrapy als Scraping-Lösung zum Erfassen der Daten entschieden, aber von der Verwendung der zuvor vorgestellten RSS-Projekte abgesehen. Stattdessen habe ich mir das Python-Projekt Yattag [7][8] genauer angeschaut.

"Yattag is a Python library for generating HTML or XML in a pythonic way."

Das Projekt, dessen Software unter der LGPL 2.1 zur Verfügung steht, sieht sich eher im Umfeld der automatischen Generierung von HTML-Formularen. Andererseits kann es ziemlich gut, sehr unkompliziert und auf gut lesbare Weise ("pythonic") XML erstellen. Für die schnelle Lösung habe ich mir eine eigene Item Pipeline geschrieben, MyRssPipeline. Mit einer einfachen if-Bedingungen, siehe Zeile 87 ff, kann ich dabei auf Character Data mit dem Key-String "<![CDATA[" reagieren.

MyRssPipeline

# -*- coding: utf-8 -*-

import yattag
from datetime import datetime
from scrapy.exceptions import DropItem

class MyRssPipeline(object):
    def process_item(self, item, spider):
        if item['title'] == '' or item['title'] is None:
            raise DropItem("Missing title in %s" % item)
        if item['link'] == '' or item['link'] is None:
            raise DropItem("Missing link in %s" % item)
        if item['description'] == '' or item['description'] is None:
            raise DropItem("Missing description in %s" % item)
        self.items.append(item)
        return item
    
    def open_spider(self, spider):
        filename = spider.settings.get('FEED_FILE')
        self.file = open(filename, 'w', encoding='utf-8')
        self.items = []
    
    def close_spider(self, spider):
        filename = spider.settings.get('FEED_FILE')
        title = spider.settings.get('FEED_TITLE')
        link = spider.settings.get('FEED_LINK')
        description = spider.settings.get('FEED_DESCRIPTION')
        image = spider.settings.get('FEED_IMAGE')
        self.file.write('<?xml version="1.0" encoding="utf-8"?>\n')
        doc, tag, text = yattag.Doc().tagtext()
        with tag('rss',('version', '2.0')):
            with tag('channel'):
                with tag('title'):
                    text(title)
                with tag('link'):
                    text(link)
                with tag('description'):
                    text(description)
                if image != '':
                    with tag('image'):
                        with tag('link'):
                            text(link)
                        with tag('width'):
                            text('32')
                        with tag('url'):
                            text(image)
                        with tag('height'):
                            text('32')
                        with tag('title'):
                            text(title)
                with tag('lastBuildDate'):
                    text(datetime.now().strftime("%Y-%m-%dT%H:%M:%S+02:00"))
                with tag('language'):
                    text('de-DE')
                with tag('generator'):
                    text('scrapy+yattag')
                for item in self.items:
                    with tag('item'):
                        _gi = item['guid']['guid']
                        _pl = item['guid']['isPermaLink']
                        _lnk = item['link']
                        _pd = item['pubDate']
                        _ca = item['category']
                        _de = item['description']
                        _ti = item['title']
                        _a = item['author']
                        _enT = item['enclosure']['type']
                        _enU = item['enclosure']['url']
                        _enL = item['enclosure']['length']
                        _co = item['content']
                        with tag('guid', ('isPermaLink', '{}'.format(_pl))):
                            text('{}'.format(_gi))
                        with tag('category'):
                            text(_ca)
                        with tag('link'):
                            text('{}'.format(_lnk))
                        with tag('pubDate'):
                            text('{}'.format(_pd))
                        with tag('description'):
                            doc.asis('{}'.format(_de))
                        with tag('title'):
                            if _ti.startswith('<![CDATA['):
                                doc.asis('{}'.format(_ti))
                            else:
                                text('{}'.format(_ti))
                        with tag('author'):
                            text('{}'.format(_a))
                        if _enU != '':
                            with tag('enclosure', ('type', '{}'.format(_enT)), ('length', '{}'.format(_enL)), ('url', '{}'.format(_enU))):
                                pass
                        with tag('content:encoded'):
                            doc.asis('{}'.format(_co))
        self.file.write(yattag.indent(doc.getvalue()))
        self.file.close()

Fazit

Mit der vorgestellten Lösung wird auf unkomplizierte Weise und sehr "pythonic" eine RSS-Datei aus Daten erzeugt, die zuvor mittels Web-Scraping erfasst wurden. Die Umsetzung als Scrapy Item Pipeline mit dem Python-Projekt Yattag sorgt für relativ große Flexibilität bei der Erstellung der RSS-Datei. Insbesondere erlaubt sie die optionale Verwendung von Character Data bei bestimmten XML-Elementen.

Kategorien: Python

Zurück