Archiv der Kategorie: Datenimport

Haushaltsbuch für Minimalisten

Ich lieeebe einfache Lösungen! Da ich umständehalber meine Haushaltsausgaben besser überwachen möchte, hab ich mir eine wirklich simple Lösung einfallen lassen. Man braucht dazu ein Spreadsheet und eine Datenbank, ich bin dazu Windows-rückfällig geworden und arbeite mit Excel und meinem heißgeliebten alten Access.

Kleiner Meckerpott zwischendurch:

ich fahre ja eigentlich auf meinem Hauptrechner sehr zufrieden mit Linux, aber was ich beim Versuch mit Base (dem LibreOffice Datenbankprogramm) meine Auswertungen zu fahren erlebt habe, war grottenmäßig. Ich hab stundenlang an einem Datenimport ASCII oder auch Excel/Calc hingeastet und nichts hat funktioniert. Keine Umlaute, kein Währungsformat, Zahl beharrlich als Text importiert und nachträglich nicht zu ändern,statt Daten zu importieren werden sie in Calc geöffnet… nee Leute, Base hat mich nicht überzeugt. Ich hab mein Spreadsheet gepackt (als xlsx gespeichert) und bin auf meinen alten Windows-Laptop umgezogen. Ah, ich sags euch, herrlich! Ein Paradies für eine alte Statistikerin wie mich!

Nachtrag: ich leiste Abbitte, es geht doch, und zwar nach dieser Anleitung:
https://help.libreoffice.org/latest/de/text/shared/guide/data_im_export.html
Ist zwar nicht ganz fehlerfrei, so werden Zahlen- und Datumsfelder nicht automatisch erkannt, und das Währungsformat fehlt gänzlich, das ist nicht schön. Es geht mit einem Umweg über Calc und die Zwischenablage, dann kann man mit einem Assistenten ähnlich wie in Access arbeiten und die zu importierenden Felder manuell auswählen. OK, wollen wir mal nicht so kleinlich sein und die €-Beträge als Number mit 2 Dezimalstellen führen. Für die Diagramme in Calc gibts dann wieder €.

Fangen wir vorne an: Die Datenerfassung

Aber ich schweife ab. Lasst uns ganz vorne abfangen. Die Grundlage jeder Statistik ist die Datenerfassung, da beißt die Maus kein Faden ab. Heißt in der Praxis: überall Kassenzettel mitgeben lassen und täglich eintippen. Manchen Läden bieten auch einen sog. e-Bon an (Kassenzettel elektronisch übermittelt) aber das hab ich noch nicht ausprobiert. Also reinhacken, und zwar in einer für die Auswertung sinnvollen Form. Dazu nimmt man gern eine Tabellenkalkulation wie Calc oder Excel, weil die angenehm zu bedienen sind. Meine Haushaltsbuch-Erfassungs-Tabelle sieht so aus:

flatfile

Das Flatfile

Ich höre schon den Aufschrei aller alten Datenbankler: das ist doch redundant und nicht normalisiert! Das, liebe Freunde der Statistik, ist ein klassisches Flatfile und hat mit Datenbankkonventionen  erstmal nix zu tun. da steht halt -zig mal edeka oder Nahkauf, und auch im Sortiment und Datum gibt es vielfache Einträge und basta. ShiSho (shit in shit out) sagten meine amerikanischen Kollegen damals, wenn du die richtigen Daten nicht reingibst, kommt auch nur Mist raus. Das Sortiment ist übrigens biologisch gewachsen, das habe ich mir beim Eintippen der Reihe nach einfallen lassen. Man sollte es recht konsequent verwenden und nicht ständig neue Sortimentseinträge erfinden. Zum Tippen gehts einfach, weil das Spreadsheet eine Auto-Vervollständigen-Funktion hat und es reicht wenn man nur die Anfangsbuchstaben eintippt. Das Datum gibt man pro Kassenzettel nur einmal ein und ziehts dann mit Copy and Paste runter.Beim Artikel sollte man ein wenig Disziplin walten lassen und die Namen für gleiche Artikel gleich schreiben. Andererseits ist eine Auswertung auf Artikelbasis schon ziemlich esoterisch, das reicht mir wenn ichs im Flatfile sehen kann.

Also, wir haben folgende Felder:

Artikel Preis Laden Datum Sortiment

Dann tippen wir brav unsere Kassenzettel ein. Wenn man das gleich jeden Tag erledigt, ist es in ein paar Minuten fertig. Und eins ist wichtig: ehrlich sein, nix unterschlagen, auch nicht das Eis am Sonntagnachmittag oder den Kaffee und Kuchen in der Konditorei.

Kleine Erklärung zwischendurch

Sie vermissen die „grossen“ Ausgaben wir Miete, Strom, Heizung, Telefon etc? Die sehe ich auf meinem Kontoauszug, und ich kann sie beim besten Willen nicht ändern. Natürlich kann man versuchen Strom zu sparen und statt die Heizung anzudrehen einen warmen Pullover anzuziehen, aber wir wollens mal nicht übertreiben. Ein wichtiger Posten allerdings fehlt bei mir, weil ich kein Fahrzeug habe: die Benzinkosten. Die treiben in vielen Haushalten die Kosten in die Höhe, und man sollte schon ein Auge darauf haben.

Erfassung fertig – und erste Schritte im Spreadsheet

Es lohnt sich schon nach etwa zwei Wochen, eine erste Auswertung zu fahren. Als erstes setzt man mal eine Summe unter die Spalte mit dem Preis, das sind logo die gesamten Ausgaben.

summepreis

Nützliches Werkzeug: der AutoFilter

Dann spielt man ein bisschen mit einem Auto-Filter und schaut sich mal an, wieviel man in einem bestimmten Laden ausgegeben hat, und zum Beispiel noch was man alles für einzelne Sortimente gekauft that. Leider ließ sich mein Screenshot-Utility nicht dazu überreden, das Autofilter-Fenster aufzunehmen, aber es ist eigentlich selbsterklärend. Man kriegt z.B. eine Liste alle vorhandenen Läden angezeigt, und kann einen oder mehrere auswählen. Ich nehme mal nur den  nahkauf: Und sehe sofort, dass ich da spätestens jeden zweiten Tag einkaufe.

nahkauf

Oder ich schau mir mal an, was ich so an Fleisch und Wurst eingekauft habe:

fleischwurst

Mhm, das ist recht viel. Man sieht hier auch rote Zahlen, die werden vom EKPreis abgezogen, das ist der sogenannte Frischerabatt wenn das MHD naht und die Ware heruntergesetzt ist. Ich schlag da ganz gern zu, das ist ja noch einwandfrei, man muss es nur zeitnah verbrauchen.

Seht ihr wie der Hase läuft? Mit dem Flatfile im Spreadsheet kann man sich schon ein bisschen mit den Daten vetraut machen, ehe es ernst wird und die Datenbank zum Einsatz kommt. Nicht verbasteln, die grossen Auswertungen mit Summen und Gruppierungen machen wir in der Datenbank, da gehts viel einfacher als im Speadsheet! Finde ich jedenfalls.

Auf gehts zum Datenbanken

Wenn man mit Calc gearbeitet hat, speichert man die Datei als xlsx ab, mit Excel erübrigt sich das natürlich. Dann, täterätää, gehts los: Access aufmachen und eine neue leere Datenbank anlegen.

(Anmerkung am Rande: wenn jemand herausfindet, wie man das Flatfile korrekt nach Base importieren kann, bitte Bescheid sagen! Ich habs aufgegeben und bleib bei Access.)

Zuerst müssen wir unser Flatfile laden. Das geht folgendermassen:

Unter externe Daten auf das Excel-Icon klicken, mit durchsuchen zum Excel-File navigieren, dden ersten Punkt „Importieren in eine neue Tabelle“ auswählen. Dann wird ein Auszug des ersten Arbeitsblattes angezeigt. Der Excel-Import in Access importiert immer nur ein ausgewähltes Arbeitsblatt, das ist eigentlich logisch, weil ja nur in eine einzelne Tabelle importiert wird. Man vergewissere sich, dass das richtige Arbeitsblatt angezeigt wird, und klickt auf Weiter- Erste Zeile enthält Spaltennamen auswählen, kurz drüberschauen ob alles paßt, weiter. Jetzt kommt ein eminent wichtiger Schritt, nämlich die Vergabe der richtigen Datentypen für die einzelnen Felder. Es ist eigentlich selbsterklärend:

Artikel Text, Preis Währung, Laden Text; Datum Datum/Uhrzeit, Sortiment Text. Und Schuss 🙂

Primärschlüssel soll von Access hinzugefügt werden, Weiter, Tabellenname vergeben, ich wähle „Rohdaten“, Fertig stellen. Wer möchte kann sich die Importschritte für die Zukunft speichern.

Ich öffne die Tabelle, und Halleluja, es kann losgehen!

tabelleinacc

Die erste Abfrage: wofür gebe ich mein Haushaltsgeld eigentlich aus?

Dafür werten wir den Preis pro Sortiment aus, da kommt dann raus was ich für Fleisch, Molkereiprodukte, Porto usw. tatsächlich ausgegeben habe. Ich mach sowas gern im Assistenten, die ganz harten Freaks tippen das SQL ein. Aber schön der Reihe nach, damit auch jeder mitkommt.

Das ist jetzt echt Access für blutige Anfänger, aber einmal exerzieren wir es durch, damit sich auch Leute zurechtfinden die noch nicht viel mit Access und Datenbanken überhaupt gearbeitet haben. Wir rufen den Abfrage-Assistenten auf, der steckt unter Erstellen/Abfrage-Assistent Icon, und wählen Auswahlabfrage-Assistent.  Als zugrundeliegende Tabellen müsste eigentlich unsere Rohdaten drinstehen, wir haben ja nur die eine Tabelle. Wir wählen die Felder Preis und Sortiment, und weiter. Detail sollte angewählt sein, weiter. Abfrageentwurf anwählen, fertig stellen. Das sieht so aus:

auswahleditor

Unten sind unsere gewählten Felder eingeblendet. Eminent wichtig ist oben in der Menüleiste das Summenzeichen, damit blenden wir nämlich die Aggregatfunktionen ein, die wir für diese Auswertung brauchen. Der Preis wird summiert und absteigend sortiert, das Sortiment wird gruppiert, so soll es aussehen:

summegruppierung

Wenn wir jetzt die Abfrage ausführen, erhalten wir folgendes prächtige Ergebnis:

abfrage_ergebnis

Na bitte, juckt es sie da nicht in allen Excel-oder Calc-Fingern, das schreit doch nach einem Diagramm-Törtchen! Calc möchte übrigens die Daten genau andersherum, also in der ersten Spalte die Beschriftung, in der zweiten Spalte die Summe von Preis. Sonst wirds kompliziert…

Ich hab wieder Linux zum Zug kommen lassen und machs mit Calc.

preisprosortimenttorte

Na, das war doch jetzt gar nicht schlimm, oder? Für Perfektionisten liefere ich noch das SQL der Abfrage nach:

SELECT Sum(Rohdaten.[Preis]) AS SummevonPreis, Rohdaten.[Sortiment]
FROM Rohdaten
GROUP BY Rohdaten.[Sortiment]
ORDER BY Sum(Rohdaten.[Preis]) DESC;

Weitere interessante Abfragen

Ich machs mir leicht und zeige hier nur das SQL, im Assistenten kanns sichs jeder selber zusammenpfriemeln. Da wäre als nächstes interessant, wieviel Geld ich in welchen Laden getragen habe, also die Summe über den Preis gruppiert nach Laden… hach ich liebe SQL, das ist doch sehr gut verständlich und einleuchtend:

SELECT Sum(Rohdaten.[Preis]) AS SummevonPreis, Rohdaten.[Laden]
FROM Rohdaten
GROUP BY Rohdaten.[Laden]
ORDER BY Sum(Rohdaten.[Preis]) DESC;

preis_pro_laden

Als letztes machen wir noch die Ausgaben pro Tag, für die ist ein Balkendiagramm sinnvoll. Erst das SQL:

SELECT Sum(Rohdaten.Preis) AS SummevonPreis, Rohdaten.Datum
FROM Rohdaten
GROUP BY Rohdaten.Datum
ORDER BY Rohdaten.Datum;

pro_tag_balken

Na, das ist doch mal aussagekräftig… jetzt kann man hingehen und in den Rohdaten nachschauen, was bei den Spitzenwerten an einem bestimmten Tag ausgegeben wurde, aber damit lasse ich euch jetzt alleine, da kann jeder selber spielen. Mir jedenfalls hat es Spaß gemacht, und ich liebe es, mein Haushaltsbuch auszuwerten. Da macht sogar das Kassenzettel erfassen Spaß!

Es geht voran mit dem Stichwortregister-Plugin: aus drei mach zwei

Ich hab mir heute nochmal die Erstellung von Admin-Menüs in WordPress zu Gemüte geführt, und beschlossen aus den drei Plugins zur Erstellung eines Stichwortregisters doch zwei zu machen.  Es sind ja auch getrennte Funktionalitäten, einmal wird die Stichwortliste als CSV aus der wp_posts erstellt, eine Ausgabeseite angelegt und in die wp_options eingetragen, und der Shortcode erzeugt, und zum zweiten wird die Negativliste gepflegt. Ausserdem bräuchte ich dann für die Negativliste eine Menütiefe von 3, und da wirds tricky, das kann WordPress nicht so ohne weiteres.

Man könnte die CSV-Datei, die Ausgabeseite und die Tabelle für die Negativliste auch bei Aktivierung des entsprechenden Plugins automatisch erzeugen, aber irgendwie ist es mir sympathischer, das manuell anzustossen. Muss man halt eine recht ausführliche Anleitung mitliefern, aber das krieg ich auch noch gebacken 😉

Jetzt erst mal weiter mit dem Plugin-Zusammenfassen. Läuft ganz gut, ich hab wohl recht sauber programmiert, Morgen mehr!

Update: Das war ja einfach. Ich habe ein schönes Tutorial bei Honar Systems für die Erstellung von WordPress Admin Pages mit Sub Pages gefunden, damit war die Hauptarbeit schon erledigt. Es gibt jetzt einen Menüeintrag „Stichwortregister“ mit zwei Untermenüs „Konfiguration“ und „CSV Datei“. Es gibt des weiteren einen eigenen Menüpunkt „Negativliste bearbeiten“. Hier mal nur ein kurzes Snippet für die Erzeugung der Menüs:

 function stichwortregister_admin_menu() {
add_menu_page(
__( 'Stichwortregister', 'de_DE' ),
__( 'Stichwortregister', 'de_DE' ),
'manage_options',
'stichwort-page',
'stichwortregister_admin_page_contents',
'dashicons-plugins-checked',
3
);
add_submenu_page( 'stichwort-page',
__( 'Stichwortregister', 'de_DE' ),
__( 'CSV-Datei', 'de_DE' ),
'manage_options',
'stichwort-page-sub-menu-csv',
'stichwortregister_sub_menu_admin_page_csv_contents');

add_submenu_page( 'stichwort-page',
__( 'Stichwortregister', 'de_DE' ),
__( 'Konfiguration', 'de_DE' ),
'manage_options',
'stichwort-page-sub-menu',
'stichwortregister_sub_menu_admin_page_contents');


}
add_action( 'admin_menu', 'stichwortregister_admin_menu' );

Die Funktionalitäten haben sich nicht geändert, ich hab sie nur ein bisschen anders einsortiert. Lief problemlos, ich hab da echt sauber gearbeitet. So, jetzt muss es aber gut sein. Zwei Plugins für ein Stichwortregister, damit leben wir einfach 😉

Perfektionisten würden jetzt noch die Stichwortliste in eine MySQL Tabelle packen, aber mir gefällt das mit dem CSV eigentlich ganz gut, vielleicht will man die Liste auch mal extern bearbeiten oder ergänzen. Ich lass es jetzt mal so.

Nachtrag: die Negativliste im praktischen Einsatz

Ich war noch die praktische Anwendung der Negativliste für das Stichwortverzeichnis schuldig, die liefere ich jetzt nach. Wir haben ja unsere Negativ-Wörter in der Tabelle negativliste stehen und können sie per Plugin pflegen.

Jetzt kommt ihr Einsatz: ich mach das an der Stelle, wo die Stichwortliste aus der CSV-Datei eingelesen und in ein Array weggeschrieben wird. Also, wir gehen in die Function csv_einlesen(). Und zwar an die Stelle, wo die CSV mit einer While not EOF-Schleife eingelesen wird. In jeder eingelesenen Zeile wird der Flag für die Negativliste erstmal auf 0 gesetzt. Dann geht man mit einem Select in die Tabelle negativliste und prüft, ob das aktuelle Wort enthalten ist, wenn ja wird der Flag auf 1 gesetzt. Dann geht man hin und schreibt das aktuelle Wort nur in das Array, wenn der Flag == 0 ist. Das wars!

$negativ_flag = 0;

//Solange nicht EOF erreicht, 1000 ist die maximale Zeilenlänge
while (($csv_array = fgetcsv ($handle, 1000)) !== FALSE ) {

foreach ($csv_array as $index) {

$negativ_flag = 0;
//mit Negativliste abgleichen
$db_item = $wpdb->get_results(
"SELECT * FROM negativliste WHERE wort LIKE '$index'");

if (count($db_item) > 0){
$negativ_flag=1;
}

//Nur ausgeben wenn das Wort nicht in der Negativliste enthalten ist
if ($negativ_flag == 0){
//Hier kommt der Knackpunkt: Neues Stichwort in Array schreiben
//***********************************
array_push($aktListe, $index);
//***********************************

} //Ende von if negativ_flag == false
}
}

Ich muss sagen, ich bin mit der Funktionalität sehr zufrieden, so kriegt man mit relativ wenig Aufwand doch recht saubere Stichwörter und kann die kleinen Füllwörtchen prima ausblenden. Mein Progrämmchen ist lernfähig, das grenzt schon ein bisschen an KI 🙂

Stichwortverzeichnis: die Negativ-Liste

Was soll die können? Nun, ganz einfach. Man soll da Wörter eintragen können, die in dieser Schreibweise nicht im Stichwortregister auftauchen sollen.

Hintergrund: die Stichwortliste wird ja programmgesteuert erzeugt und nimmt prinzipiell alle Wörter auf, die großgeschrieben sind. Die werden dann noch von etlichen Sonderzeichen bereinigt, aber es kann natürlich nicht geprüft werden, ob es sich wirklich um Substantive handelt oder ob nur ein Wort am Satzanfang großgeschrieben wurde.  Da rutschen dann schon mal Wörter durch wie Es, Da, Was, Das, So… klar was ich meine? Sind halt keine sinnvollen Stichwörter. Ich hab in der Access-Version mal versuchsweise alle Wörter ausgeblendet, die weniger als 3 Buchstaben haben, aber so ganz das Gelbe vom Ei ist das auch nicht. Auf dem Programmierblog hier würde dann z.B. alle rausfallen was SQL oder PHP oder AI  heißt – nicht gut!

Woher soll mein Plugin aber wissen, welche Wörter es ausblenden soll? Eben! Hier kommt die Negativliste ins Spiel. Ich versuche es mal mit einer Tabelle, obwohl ein CSV sicher auch sinnvoll wäre, das könnte man auch extern bearbeiten. Wie auch immer, es muss ein Admin-Menüpunkt her „Negativliste bearbeiten“. Und die Liste muss an geeigneter Stelle überprüft werden, das mach ich am besten an der Stelle, wo das CSV mit den Stichwörtern in ein Array eingelesen wird. Wenn ein Wort in der Negativliste gefunden wird, soll es nicht ins Array aufgenommen werden. Sollte so oder ähnlich funken, ich fang mal an und berichte später.

Wenns so einfach wäre: die Tücken der lokalen WordPress-Installation

Ich gebs zu, ich hatte es mir einfacher vorgestellt, eine lokale Kopie dieses Blogs anzulegen. Dateien per SFTP runterladen… hat fast eine Stunde gedauert, war aber noch OK. Datenbankbackup einspielen… das ging recht flott, so nach 10 Minuten war MySQL damit fertig. wp_config anpassen (Datenbank Name, User Pwd etc.). Pfade in der wp_options anpassen. Dann liefs, aber mein Plugin lief nicht. Doch es lief so prinzipiell schon, die Erstellung der CSV-Datei aus den Beitragstiteln lief ohne Fehler durch. Aber später bei der Anzeige der Linkliste gabs Probleme. Die guid aus der wp_posts stand natürlich überall noch mit dem Serverpfad drin, da machte ein Klick die Life-Installation auf und zeigte brav den passenden Eintrag an. Menno, ich will aber die lokalen Beiträge haben…

In diesem gewohnt informativen Beitrag bei elmastudio wird vorgeschlagen, die Pfade in der Datenbank Backup Datei mit Hilfe von Notepad++ Suchen&Ersetzen anzupasssen und die Tabellen erst dann zu importieren. War ich wieder zu schnell.. also, Tabellen nochmal droppen und neuer Versuch. Ich melde mich dann wieder.

Also, nach mehreren vergeblichen Versuchen eine Webseite manuell umzuziehen ist es mir jetzt zu dumm geworden, und ich hab mich an das Plugin Duplicator erinnert, das besonders für kleinere Webseiten 1a geeignet ist. Hier bei Weptimizer findet man eine ausführliche Anleitung. Ich hab jetzt meinen Praxis Dr. Inselfisch Blog lokal umgezogen und geh mal mein Plugin testen.

Nachtrag: der Duplicator ist schon Klasse, er hat mir diesen Blog hier klaglos auf meinen lokalen Webserver umgezogen, auch wenn das Einspielen von 94 MB gezipptem Archiv schon ein Stück gedauert hat. Tolles Plugin, wirklich!

Ein ehrgeiziges Projekt: Stichwortverzeichnis

Meine beiden mit Abstand am häufigsten gebrauchten Kochbücher sind „Das Bayrische Kochbuch“ und „Joy of Cooking“. Und bei beiden ist das am Meisten genutzte Feature das Stichwortverzeichnis, da findet man alles! Es eignet sich auch hervorragend zum kreuz- und querschmökern, man findet dabei Rezepte die man sonst nie gesehen hätte. Langer Rede kurzer Sinn, ein Stichwortverzeichnis (Volltext-Index) wäre ein schickes Feature für mein Inselfisch-Kochbuch. Und dazu möchte ich nicht das Stichwort-Feature von WordPress nutzen, das ist mir viel zu umständlich zu bedienen und zu schlecht auszuwerten.

Dazu waren einige Vorüberlegungen nötig. Zunächst muss ich mal festlegen, woher ich die Stichwörter nehme. Ich habe in meinem Inselfisch-Kochbuch knappe 400 Rezepte, in denen relevanter Text in zwei Feldern steht, einmal in post_title und einmal in post_content. Der Titel ist meist zwischen 5 und 10 Wörter lang, der Content kann wesentlich länger sein, bis zu einer ganzen DIN A 4 Seite und mehr, also auf jeden Fall mehr als 100 Wörter. Ich möchte meine Stichwortbasis natürlich maschinell erzeugen, dazu muss ich irgendwie einzelne Wörter aus meiner Datenbasis extrahieren. Dazu muss ich meinen Quelltext aufsplitten (ob mit PHP oder VBA wird sich noch zeigen) und in eine Tabelle schreiben. Diese Tabelle (ich nenne sie mal Rohdaten) soll am Ende nur alle Stichwörter und eine ID (Autowert) enthalten. Man sieht schon: mit dem Content, das wird uferlos, das können leicht mehrere Zehntausend Wörter werden. Ich beschränke mich also auf die Wörter aus dem post_title, das ist besser zu handeln. Wir machen das mal als erste Annäherung und schauen uns dann an, was wir mit den extrahierten potentiellen Stichworten anfangen können.

Wie ich das in MS Access angehe: ich importiere mir die gesamte Tabelle wp_posts aus dem Original-Inselfischkochbuch über CSV-Export aus phpmyadmin. Dann schreibe ich mir eine Abfrage,, die nur den post_title enthält und als where-Klausel post_Type = post und post_status = publish bekommt, damit ich nur die echt veröffentlichten Rezepte in der Datenbasis habe. Die Abfrage heißt quelle und sieht so aus:

Screenshot quelle

265 Datensätze a drei bis zehn Wörter, damit kann man arbeiten. Ich lege ausserdem fest, dass ich nur Großgeschriebene Wörter im Stichwortverzeichnis haben möchte. Das ist willkürlich, aber doch recht sinnvoll, weil damit die ganzen kleinen Füllwörter rausfallen.

Dann gehts rund: Ich lege mir eine Tabelle namens ziel an, die nur zwei Felder hat: ID(AutoWert) und Wort(Text). Sie bleibt zunächst leer. Dann bastle ich mir ein VBA-Modul. Hier lege ich zwei Recordsets an, rstquelle das ist die Abfrage Quelle, und rstziel, das ist die Tabelle Ziel.

Jetzt gehe ich zum ersten Datensatz in rstquelle und lese mir den Inhalt des Feldes post_title ein. Dann verwende ich die VBA-Funktion split(), die zerlegt das Feld in einzelne Wörter, die in ein Array geschrieben werden.  Ich laufe durch dieses Array und prüfe zunächst, ob das Wort Groß oder klein geschrieben ist:

Asc(Left(liste(i), 1) >= 65) And (Asc(Left(liste(i), 1)) <= 90)

Mit dieser Funktion bin ich nicht so recht glücklich, weil sie unerklärlicherweise manche Wörter mit Kleinbuchstaben doch durchrutschen läßt, aber ich hab noch nichts besseres gefunden.

Dann werden noch evtl vorhandene Sonderzeichen entfernt, dazu gibts eine Funktion die Folgende Zeichenkette durchläuft:

Const strSonderzeichen As String = „.,:;#+’*?=)(/%$§!~\}][{“

Dann schreibe jeweils ein gefundenes, geputztes Wort in die Zieltabelle. Dann gehe ich zum nächsten Datensatz der Quelltabelle und wiederhole den Vorgang. das mache ich, bis EOF der Quelltabelle erreicht ist.

Hurra! 669 Datensätze, beim ersten Drüberschauen sieht es schon mal ganz gut aus. Noch die Dubletten ausblenden, das geht mit einer Abfrage mit Gruppierung ganz easy. Sortieren, man sieht noch ein bisschen Datenschmutz ganz oben, das bereinigt man per Hand.

abfrage quelle

Es bleiben 496 recht manierliche Stichworte übrig. Damit könnte ich jetzt schon nach MySQL und WordPress gehen und die Webseite aufbauen, aber ich halte mich noch ein wenig in Access auf und teste nochmal, mit der „wackeligen“ Asc-Funktion bin ich nicht zufrieden.

preg_replace & Co. im praktischen Einsatz

Ich bin immer noch an den exportierten Posts aus WordPress, die wir im Q&D-Editor schonmal manuell nachbearbeitet haben. Dabei sind sicher etliche Dinge aufgefallen, die man programmatisch korrigieren könnte, und wie man das praktisch angeht, schreib ich hier mal auf.

Wir haben immer noch die Ausgangstabelle rezepte, in der nur die ID, der post_content und der post_title drinstehen. Damit wir unsere Rohdaten nicht verhunzen, legen wir uns erstmal eine leere Kopie dieser Tabelle an, das geht im phpmyadmin unter „Operationen“, kopiere Tabelle, nur Struktur, oder mit dem simplen SQL:

CREATE TABLE rezepte_arbeit LIKE rezepte

Das kopiert die Struktur inklusive evtl. vorhandener Indizes etc. und wird unser Arbeitspferd. (Falls man mal die Daten mitkopieren möchte, CREATE TABLE table2 SELECT * FROM table1, aber das nur als Anmerkung am Rande)

Zur Kontrolle des Ergebnisses klemmt man sich die (noch leere) Arbeitstabelle in den Q&D-Editor. Spätestens jetzt macht es Sinn, den Namen der Tabelle auf eine Variable zu legen und die beiden SQLs entsprechend anzupassen.

//Arbeitstabelle festlegen
$arbeitstabelle = "arbeit_rezepte";
echo "<h1>Tabelle: ".$arbeitstabelle."</h1>";
...
$sql = "SELECT * FROM ".$arbeitstabelle." WHERE post_content LIKE '%".$filter."%'";
...
$sql = ('SELECT * FROM '.$arbeitstabelle.' WHERE post_content LIKE "%'.$filter.'%" 
LIMIT '.$limit.', '.$ergebnisse_pro_seite.'');

Wer es ganz genau haben möchte, legt auch noch den Dateinamen des Q&D-Editor Skripts auf eine Variable und paßt die Navigationslinks entsprechend an, aber das ist eigentlich schon Fleißarbeit:

//Dateinamen auf Variable legen
$datei = "arbeitspferd.php";
...
if($limit>0){echo '<a href="'.$datei.'?seite_nr='.($limit-1).'"><button type="button">Voriger Datensatz</button></a>';}
echo '<a href="'.$datei.'?seite_nr='.($limit+2).'"><button type="button">Nächster Datensatz</button></a><br>';
...
 //Nav Links alle Seiten
 for ($i=1; $i<=$row_total; $i++) { 
    echo "<a href='".$datei."?seite_nr=".$i."'>".$i."</a> "; 
};

Da die Tabelle noch leer ist, kriegt man erstmal gar nichts angezeigt:

arbeitstabelle_leer

arbeitstabelle_leer

Aber das ist ja OK so und wird sich gleich ändern.

Das Gerüst für die geplanten Aktionen

Ich bastle mir ein kleines Formular mit einem Button zum Starten des Skripts, und gebe der Übersicht halber mal alle Datensätze tabellarisch aus. Das sieht so aus:

<?php
//file:skript_starten.php
header('Content-Type: text/html; charset=utf-8');
require 'connection.php';

//Quell-und Zieltabelle festlegen
$quelltabelle = "rezepte";
$zieltabelle = "arbeit_rezepte";

echo "<h1>Skript starten</h1>";
echo "<h2> Quelle: ".$quelltabelle." Ziel: ".$zieltabelle."</h2>";

//Formular mit Button für Start
echo "<form action ='#' method = 'post'>";
echo "<input type = 'submit' name = 'absenden' value = 'Skript starten'>";
echo "</form>";

if(isset($_POST['absenden'])){
    
    echo "<script type=\"text/javascript\">alert('Skript gestartet');</script>";
    
    //Hier kommt die Action hin
    
    
    }

$sql = "SELECT * FROM ".$quelltabelle."";
$rs_result = $conn->query($sql);
$row_total = $rs_result->num_rows;

echo "Datensätze gesamt: ".$row_total."<br>";

 //Ausgabe in Tabelle - nur Arbeitsversion, kann man für den Betrieb rausnehmen
echo "<table border='1' cellpadding='4'>";
echo "<tr><th>ID</th><th>Titel</th><th>Content</th></tr>";
 
 while($row = $rs_result->fetch_assoc()) {
            
            echo "<tr><td valign='top'>".$row["ID"]."</td>";
            echo "<td valign='top'>".utf8_encode($row["post_title"])."</td>";
            //HTML Source in Textarea ausgeben
            echo "<td valign='top'><textarea rows='5' cols='80'>".utf8_encode($row["post_content"])."</textarea>";
            echo "</td></tr>";
            
 } //Ende von while row = rs_result
 
 echo "</table>";
 
?>

Ich hole mir mit dem require die Connection, lege Quell-Und Zieltabelle auf Variable und baue mir den Button für das Starten des Skripts. Die Kontrollausgabe der aktuellen Quelltabelle ist nicht unbedingt notwendig, das dient nur der Übersicht.

Jetzt gehts zur Sache

Der Ablauf ist folgender: die Datensätze aus der Quelltabelle werden selektiert und die Felder auf Variablen gelegt. An den Variablen nimmt man dann die nötigen Manipulationen vor, und die fertig bearbeiteten Datenfelder werden in ein Array geschrieben. Aus dem Array füllt man dann mit Insert die Zieltabelle. Das Ergebnis kann man dann gleich mal im Q&D-Editor überprüfen. Ich mach dann gleich noch ein Beispiel, hier zuerst mal der Source:

if(isset($_POST['absenden'])){
    
    echo "<script type=\"text/javascript\">alert('Skript gestartet');</script>";
    
    //Hier kommt die Action hin
    
    $sql = "SELECT * FROM ".$quelltabelle."";
    $rs_result = $conn->query($sql);
    
      
    $i=0;
    $temp = array();
    
    while($row = $rs_result->fetch_assoc()) {
            
            $akt_id =$row["ID"];
            $title = $row["post_title"];
            $content = $row["post_content"];
            
            // Hier kann man beliebige Stringfunktionen auf die Felder anwenden
            $content = strip_tags($content);
            
            //Behandelte Strings in Array einlesen
            $temp[$i][id] = $akt_id;
            $temp[$i][titel] = $title;
            $temp[$i][content]= $content;
            $i = $i+1;
            
    }
            
 }
$len_temp = count($temp);

//Array zeilenweise in Zieltabelle wegschreiben
for ($i = 0; $i < $len_temp; $i++){    
  
          $temp_id = $temp[$i][id];
          $temp_titel = $temp[$i][titel];
          $temp_content = $temp[$i][content];
          
          //addslashes ist nötig weil sonst der Insert bei jedem Hochkomma aussteigt
          $sql_insert = "INSERT INTO ".$zieltabelle." (ID, post_title, post_content) 
          VALUES (".$temp_id." ,'".addslashes($temp_titel)."' , '".addslashes($temp_content)."')";
          
          if (mysqli_query($conn, $sql_insert)) {
                      echo "Insert erfolgreich<br>";
                      
                    }
                      
                    else {
                      echo "Fehler beim Update: " . mysqli_error($conn);
                      
                    }
} // end for

Das wars schon – das Skript braucht ein paar Sekunden, nicht ungeduldig werden. Wichtig ist, dass man die einzufügenden Felder beim Insert noch mit einem addslashes() behandelt, weil sonst bei jedem Anführungszeichen im Text das Skript aussteigt.

Ich habe oben nur eine Ersetzung ausgeführt, nämlich das:

$content = strip_tags($content);

Das führt in der Praxis evtl. zu unerwünschten Effekten, weil es wirklich gnadenlos alle HTML-Tags raushaut. Für meine Zwecke besser geeignet ist der strip_tags mit Ausnahmen:

$content = strip_tags($content,'<h2>, <h3>, <ol>, <ul>, <li>');

Das läßt meine Überschriften und die Listen drin, sieht schon besser aus.

mit_liste

mit_liste

Aber da muss jeder selber tüfteln, was für seine Zwecke am besten geeignet ist. Fröhliches googlen nach praktikablen Stringbehandlungen! Und nicht vergessen vor erneutem Starten des Skripts die Zieltabelle mit Truncate zu leeren, sonst fällt der Insert wegen doppelter IDs auf die Nase.

Nachtrag: den Truncate kann man natürlich auch ins Skript mit einbauen, einfach die Action um folgenden Code ergänzen:

//Zieltabelle leeren
    $sql_zap="TRUNCATE TABLE ".$zieltabelle."";
    if (mysqli_query($conn, $sql_zap)) {
                      echo "Truncate erfolgreich<br>";
                      
                    }
                      
                    else {
                      echo "Fehler beim Truncate: " . mysqli_error($conn);
                      
                    }

Das muss natürlich vor dem Insert rein, am Besten setzt man es ganz an den Anfang innerhalb der if(isset($_POST…)-Bedingung. Dann spart man sich das manuelle Leeren der Zieltabelle.

Quick&Dirty Editor für HTML-Posts mit Vorschaufunktion

Wer viel mit CMS umgeht, kennt das zur Genüge: in der Datenbank landet im Feld für den Inhalt eines beliebigen Posts oder einer Seite Text mit jeder Menge HTML-Tags, schließlich arbeitet man ja mit dem TinyMCE oder einem seiner Verwandten.Das sieht zum Beispiel in WordPress so aus:

<h2>Einleitung</h2>
In München-Haidhausen gibt es einen wunderbaren Laden, den "Liquid", da kann man erlesene Alkoholika auch in kleinen Mengen (ab 0,1 l) kaufen. Daher gibt es bei mir die Scaloppine mal mit Marsala, mal mit Sherry, oder ich bereite sie mit Portwein zu, ganz nach Lust und Laune. Wichtig ist, daß der Wein eher von der lieblichen Sorte sein soll, nur dann bekommt die Sauce den schönen vollmundigen Geschmack.
<h2>Zutaten</h2>
Für 4 Personen:

4 Kalbsschnitzel oder 8 dünn geschnittene Minutensteaks, alle Fettränder und Häutchen sorgfältig abgeschnitten, Mehl zum wenden. Je 1 El Butter und gutes Olivenöl, Salz, Pfeffer.
Zum Ablöschen: 1 kleines Glas lieblicher Südwein.
Noch 1 El. eiskalte Butter.
<h2>Zubereitung</h2>
Siehe <a href="http://evileu.de/inselfisch-kochbuch/2017/01/12/schnitzel-mit-zitrone-scaloppine-al-limone/">Scaloppine Grundrezept</a>, zum Ablöschen ein kleines Glas Südwein (s.oben) nehmen. Man kann die Scaloppine auch noch mit etwas feingehackter Petersilie bestreuen, aber das muß nicht unbedingt sein.

Das ist der post_content eines Rezeptes für Scaloppine. Im Inselfisch-Kochbuch sieht das so aus:

scaloppine

scaloppine

Recht und schön, aber im richtigen Leben kommt es immer wieder mal vor, dass man Content aus einem CMS abzieht und in einem anderen CMS weiterverwenden möchte. Das geht in den seltensten Fällen ohne umfangreiche Nacharbeiten, und nicht alles kann man programmgesteuert erledigen.

Man kann mit PHP natürlich alle HTML-Tags entfernen (siehe strip_tags Doku) und dabei sogar Ausnahmen definieren, welche Tags drinbleiben dürfen, und man kann sich auch mit preg_replace mehr oder weniger geniale Ersetzungen einfallen lassen. Aber das hat auch so seine Nachteile.

In dem Scaloppine-HTML ist zum Beipiel ein Link drin, der auf das Inselfisch-Kochbuch auf evileu.de verweist, das kann natürlich nicht so bleiben. Schmeißt man alle Links programmatisch komplett raus, ergibt der Text keinen Sinn mehr, da fehlt dann für das ganze Rezept die Zubereitung. Hier müßte man, wenn man es ganz richtig machen will, dem Link nachgehen und den entsprechenden Zubereitungstext rauskopieren. Das ist natürlich aufwendig, aber im Zweifelsfall die einzig richtige Lösung.

Tscha, was macht man jetzt, wenn man vor ein paar Hundert Posts sitzt, die manuell überarbeitet werden müssen? Man überlegt sich eine Arbeitserleichterung.

Voraussetzungen

Ich hab mir mal aus der WordPress posts-Tabelle nur die minimalsten Rohdaten abgezogen, nämlich die ID, den Titel und den Content, gefiltert nach post_type = post und post_status=publish, das sind die Kerndaten aller veröffentlichten Kochrezepte. Wenn ich sonst noch was brauchen sollte, den Autor oder das Datum oder sogar die Kategorien oder sonstwas, das kann ich mir später über die ID wieder dazulinken. Das selbe könnte ich auch mit Joomla-Beiträgen oder Drupal-Nodes machen, Hauptsache die ID kommt mit und der Titel und Content passen. Und das Ganze ist eigentlich nur sinnvoll, wenn die Posts nicht allzu lang sind, aber meine Kochrezepte gehen selten über eine Bildschirmseite hinaus.

Wir basteln uns einen Quick&Dirty Editor

Damit man jetzt nicht immer direkt in der Datenbank rumpfuschen muss, wenn man in einem HTML-Post etwas ändern will, habe ich mir eine zwar nur als Arbeitspferd geeignete, aber durchaus zeitsparende Lösung überlegt. Ich habe bei meinen ganzen Snippets zur Pagination gekupfert und mir ein PHP-Script gebaut, in dem pro Seite ein Datensatz dargestellt wird. Es gibt eine rudimentäre Navigation (voriger/nächster Datensatz), das reicht mir für die Arbeit, schließlich gucke ich im Zweifelsfall einen Datensatz nach dem anderen durch. Dann habe ich mit Hilfe einer Tabelle zwei Bildschirmbereiche konstruiert, im ersten wird der HTML-Code dargestellt und kann editiert werden, im zweiten gibt es eine Vorschau des Ergebnisses. Noch ein Knöpfchen zum Speichern der Änderungen, und es kann losgehen.

qe_scaloppine

qe_scaloppine

Für den Fall dass ich zu einem bestimmten Datensatz springen möchte habe ich ganz unten auch noch eine nummerische Navigation eingebaut, das ist bei über 300 Datensätzen ganz nützlich.

seitenpagination

Seitenpagination

Wie man mit dem Editor arbeitet

Wenn man in einem Post etwas entdeckt hat, das man ändern möchte, kann man dies direkt im HTML-Fenster tun. Hier habe ich zum Beispiel ein Bild drin, das wird WordPress-typisch in einen caption-Shortcode eingeklemmt, der sorgt in WordPress für die korrekte Darstellung der Bildbeschriftung.

caption

caption

Ein anderes CMS kennt aber den Shortcode nicht, also muss er raus. Das sieht man auch an der Darstellung im Vorschau-Fenster, der Browser kann natürlich ohne WordPress mit dem Shortcode nix anfangen und stellt ihn als Text dar:

caption_vorschau

caption_vorschau

Um das zu korrigieren, schmeisse ich im HTML-Fenster die caption-Tags raus und platziere noch ein paar <br> an den geeigneten Stellen, mache gleichzeitig noch einige kleine Textkorrekturen:

ohne_caption

ohne_caption

Klick auf den Button „Änderungen speichern“, dann kommt erst mal eine kurze Meldung, ob der Update erfolgreich war:

meldung

meldung

Und dann kriegt man das Ergebnis im Vorschaufenster angezeigt:

vorschau

vorschau

Na bitte, das geht doch ratzfatz!

Ohne Netz und doppelten Boden

Da ich beim Klicken auf den „Änderungen speichern“-Button direkt einen Update auf die Datenbank fahre, sind die Änderungen natürlich nicht mehr rückgängig zu machen, aber das kann ich verschmerzen. Schlimmstenfalls muss ich mir einen Datensatz aus der Originaltabelle wieder herholen, wenn ich ihn total verhunzt habe, aber das ist mir eigentlich noch nicht passiert. Im richtigen Leben wird sowas eh der Praktikant machen, und der kann HTML und weiß was er tut, wenn er Tags rausschmeisst oder korrigiert.

Der Q&D-Editor ist ja auch nur als Arbeitshilfe für Entwickler gedacht, und nicht für den Endanwender. Er soll auch nicht den gezielten Einsatz von programmatischen preg_replace-Aktionen ersetzen, die haben ja durchaus auch einen Sinn. Mir gehts immer so, dass ich beim Arbeiten mit dem Editor meistens ganz schnell merke, welche Elemente programmgesteuert rausgeschmissen werden können, weil ich immer die selben Tags editiere – und dann ist natürlich ein bisschen zusätzliches PHP angesagt. In der Praxis wird man immer eine Kombination aus manuellem Editieren und programmgesteuerten Ersetzungen fahren, damit bin ich eigentlich bis jetzt recht gut klargekommen.

Und jetzt gibts den Sourcecode des Q&D-Editors, in einem neuen Beitrag.

 

Von Joomla nach WordPress umziehen – der Vollständigkeit halber

Ich habe drüben im Drupal-Blog ausführlich beschrieben, wie man Inhalte aus Drupal in WordPress-Beiträge überführt, nachzulesen hier. Das selbe machen wir jetzt noch mit Joomla, und zwar möchte ich die Beiträge aus einer bestimmten Kategorie nach WordPress exportieren. Meine ausgewählte Kategorie sind natürlich die Rezepte, mit der Kategorie-ID 8 (im Zweifelsfall nachschlagen in der Tabelle #__categories).

Wie kommen wir an Titel und Inhalt der Rezepte?

Aus der Tabelle #__content. Die holen wir uns nach Access rüber und schauen sie mal näher an.

content

content

Die id und den title nehmen wir mit, den alias lassen wir aus. Dann brauchen wir noch den introtext und den fulltext (beide!), weil Joomla den Beitragstext in diese beiden Felder aufteilt, sobald man einen Weiterlesen-Tag eingefügt hat. Das sieht man hier im Screenshot oben bei den ids 4 bis 8, so sieht das dann aus.

Den state müssen wir filtern, -2 bedeutet „marked for deletion“, 1 ist veröffentlicht, wir nehmen nur die 1er. Und die catid filtern wir auch, auf die 8 für Rezepte. (Bei mir ist noch ein Haufen Schrott vom Testen mit drin, ich nehm mal noch die Datensätze 9-47 raus). Übrig bleibt die folgende Tabelle:

content_gefiltert

content_gefiltert

Die specken wir noch ab, die Felder introtext und fulltext werden zu einem Feld namens bodytext zusammengefügt, die catid und der state fliegen raus.

Jetzt fehlen noch die Tags für die Kategorien

Kleine Erinnerung: ich hatte die Rezeptkategorien aus WordPress auf Joomla-Tags ohne Schachtelung abgebildet. Die holen wir uns jetzt wieder zurück, und zwar aus der Tabelle #__tags.

tags

tags

Der erste Eintrag für root fliegt raus, vom Rest übernehmen wir id und title. Parent_id ist überall leer, weil nicht geschachtelt, published = 1 passt auch. Um jetzt die Tags den Rezepten (hier in der Tabelle id_title_bodytext) zuzuordnen, verknüpfen wir über die Tabelle contentitem_tag_map. Beim Importieren nach Access darauf achten, dass content_item_id und tag_id als Integer rüberkommen! Das sieht dann so aus:

tags_beziehungen

tags_beziehungen

Darüber basteln wir uns eine Abfrage, in der zunächst mal jedes Rezept so oft vorkommt, wie es Tags zugeordnet hat:

abfrage_tags

abfrage_tags

Kurzer Blick über die Tabelle: ich habe zu keinem Rezept mehr als 2 Tags zugeordnet, aber das passt schon, ist korrekt.

Zuordnen der Tags zu WordPress-Kategorien

Dafür zieht man sich alle gültigen Tags aus der #–tags ab und importiert sie als Kategorien nach WordPress, wie das geht habe ich in diesem Artikel unter Schritt 1: Anlegen der Kategorien in WordPress im Drupal-Blog ausführlich beschrieben.

Dann habe ich mir die entstandene Tabelle wp_terms nach Access rübergeholt:

wp_terms

wp_terms

Hier kann ich jetzt über den name des Terms auf den title des tags verknüpfen, weil die Schreibweise identisch ist:

join_term_name

join_term_name

Damit bekomme ich die WordPress-Kategorie-IDs zu den Rezepten.

term_id

term_id

Das hatten wir so ähnlich schonmal, jedes Rezept taucht jetzt so oft auf, wie es Kategorien/Tags hat. Jetzt Gruppieren wir noch, und nehmen den ersten und letzten Wert der Term-ID. Diese sind bei den Datensätzen gleich, die nur eine Kategorie zugeordnet haben, das ist schon richtig so.

ersterwert_letzterwert

ersterwert_letzterwert

So, das wars. Diese Tabelle kann man nach WordPress importieren, und zwar genau so wie ich es in diesem Artikel beschrieben habe:

DRUPAL NACH WORDPRESS: BEITRÄGE MIT KATEGORIEN ERZEUGEN

Dabei muss man wahrscheinlich noch eine kleine Abfrage einbauen, was passieren soll wenn erster und letzte Wert der term_id gleich sind, aber prinzipiell funktioniert die Sache so einwandfrei, die Beiträge werden in WordPress angelegt und mit Kategorien versehen.

Nachtrag: mehr als 2 Kategorien in der Export-Datei

Ich hab ja ganz lässig behauptet, wenn man mehr als zwei Kategorien pro Rezept  in die Export-Datei schreiben will, braucht man in Access eine Kreuztabelle. Jetzt bin ich gefragt worden, wie man das anstellt, und ich muss zugeben die ist sehr, sehr haarig zu konfigurieren. Ich hab da mal den „kleinen Dienstweg“ über ein Excel-Makro genommen, funktioniert auch und ist wesentlich leichter nachzuvollziehen.

Die Ausgangsbasis

Wir schubsen unsere Ausgangstabelle in Excel rein, die dürfte ja inzwischen bekannt sein. Jeder Datensatz taucht so oft auf, wie er Kategorien hat, und wir haben die Joomla-ID dazugemappt:

excel_startdaten

excel_startdaten

Post_id, post_title, term_id und name kommen aus WordPress, das was hier joomla_map.id heißt ist die Joomla-Tag-ID aus unserer mapping-Tabelle.

So soll es dann aussehen

Das Arbeitsblatt mit den Ausgangsdaten habe ich „start“ genannt, das Output-Arbeitsblatt heisst „ziel“ und sieht in leerem Zustand so aus:

excel_ziel_leer

excel_ziel_leer

Wir übernehmen nur die id und den titel, und verteilen die Tags auf die entsprechenden Felder tag1…tagn. Der Clou dabei ist, dass man unbegrenzt viele Tags pro Datensatz übernehmen und einsortieren kann. Den Content hab ich mir hier gespart, kann man sich später über die id wieder dazumappen.

So wirds gemacht

Der VBA-Code dazu sieht so aus:

Sheets("start").Select
        
    i = 2
    j = 2
    k = 0
    akt_id = Cells(2, 1).Value
        
    While akt_id <> ""
        
                Sheets("start").Select
                akt_id = Cells(i, 1).Value
                akt_titel = Cells(i, 2).Value
                akt_kat = Cells(i, 4).Value
                akt_tag = Cells(i, 5).Value
                    
                    
                Sheets("ziel").Select
                Cells(j, 1).Value = akt_id
                Cells(j, 2).Value = akt_titel
                Cells(j, 3 + k).Value = akt_tag
                
                
                i = i + 1
                If Worksheets("start").Cells(i, 1) <> akt_id Then
                    j = j + 1
                    k = 0
                Else
                k = k + 1
                End If
        Wend

Was hab ich gemacht? Ich steppe mit dem i durch die start-Tabelle und schreibe so lange in die selbe Zeile j der ziel-Tabelle, wie ich in start auf die selbe id treffe. Das k zählt die Spalte für das tag1..n hoch. Treffe ich auf eine neue id in der Zieltabelle, wird j um 1 erhöht und k wieder auf 0 gesetzt, und das ganze so lange bis ich auf das erste leere Feld id in der start-Tabelle treffe.

Das Ergebnis sieht dann so aus:

excel_zieldaten

excel_zieldaten

In der Praxis wird man sicher die Anzahl der zu importierenden tags auf ein handliches Mass  beschränken, das muss sich jeder selber überlegen, was da bei seinen Bestandsdaten Sinn macht. Aber so bekommt man alle Kategorien aus WordPress auf Joomla-Tags gemappt, mit dieser Export-Tabelle kann man weiterarbeiten.