Posts Tagged ‘PHP’

Mittwoch, 23. Oktober 2013

Mit Scripts generierte ICS-Dateien mit dem korrekten Zeichensatz in Google Calendar anzeigen

Folgender kleiner Text (hier: in einem PHP-Script) hilft:

...
header('Content-Type: text/calendar; charset=utf-8');
header('Content-Disposition: inline; filename="calendar.ics"');
...

Gibt man das Charset nicht explizit an, erscheinen in Google Calendar statt den Sonderzeichen schwarze Rhomben mit einem Fragezeichen: �

Via: Character encoding in external public calendars – Why is Danish UTF-8 character not show correctly?

Tags: , ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen

Montag, 7. Oktober 2013

Mit Google Docs Spreadsheet eine Variable von einer Internet-Seite abrufen und als Zellwert verwenden

In Google Docs Spreadsheet ist es äusserst einfach, eigene Zellfunktionen zu definieren und diese danach mit einer JavaScript-basierten Funktion zu hinterlegen.

Ich habe mir diese Funktionalität zu Nutzen gemacht, um via meinen Linux-Server die aktuellen Gold- und Silberpreise in Schweizer Franken von einer Web-Site auszulesen und als „rohe“ Zahl an eine Zelle eines Google Docs Spreadsheet weiterzugeben. Basierend auf diesem Wert werden danach weitere Berechnungen in anderen Zellen angestossen.

Zuerst habe ich hierzu ein Spreadsheet eröffnet und sogleich unter Tools > Script editor… ein Script namens „Code.gs“ erstellt. Dort habe ich folgende (hier anonymisierte) JavaScript-Funktion abgelegt:

function getQuote(item) {
  if(item.length < 3) {
    return '#ERROR: parameter too short';
  }
  
  var response = UrlFetchApp.fetch("http://site.tld/index.php?item=" + item + "&raw=true");
  
  quoteRaw = response.getContentText();
  quote = parseFloat(quoteRaw).toFixed(2);
  
  return quote;
}

Anschliessend habe ich in der Zelle A1 den Wert „Gold“ hinterlegt. In Zelle A2 habe ich notiert „=getQuote(A1)“. Sobald man die Zelle A2 verlässt, versucht Google, die URL aufzurufen und die Antwort in eine Zahl umzuwandeln. Dies setzt natürlich voraus, dass das PHP-Script auf dem entfernten keinen HTML-Code zurückgibt, sondern Plaintext. Sobald Google die Funktion getQuote() aufruft, steht in der Zelle für ein bis zwei Sekunden „Thinking…“, während Google im Hintergrund meinen Server kontaktiert und die hinterlegte URL aufruft. Sobald der Wert empfangen wurde, wird dieser in der Zelle angezeigt.

Nebenbemerkung: Ja, den Umweg über meinen eigenen Server könnte ich mir selbstverständlich sparen. Ich bin mir es aber zu Schade, a) ausgedehntes JavaScript zu programmieren, zumal b) Google keine guten Debug-Werkzeuge im Script-Editor bereitstellt. Stattdessen verlasse ich mich auf PHP, Apache und auf dem Server installierte Linux-Tools mit ihren ausgefeilten Funktionen und einem sauberen Debug Logging.

Tags: , , , , ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen

Montag, 7. Oktober 2013

Ein PHP-Script so geschwätzig wie möglich machen

<?php
	ini_set('error_reporting',E_ALL);
	ini_set('display_errors',1);

	...
?>

Tags: , , , , ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen

Freitag, 27. September 2013

Mit PHPs SimpleXML auf XML-Namespaces zugreifen

Das klappt tatsächlich, auch wenn der Code damit nicht mehr so simpel wird.

XML

<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:georss="http://www.georss.org/georss">
<channel>
<title>foursquare checkin history for Mario A.</title>
<description>foursquare checkin history for Mario A.</description>
<link>https://foursquare.com/emeidi</link>
<atom:link type="application/rss+xml" rel="self" href="https://feeds.foursquare.com/history/.rss" />
<item>
<title>Migros</title>
<description>@ Migros</description>
<link>https://foursquare.com/emeidi/checkin/</link>
<pubDate>Fri, 27 Sep 13 17:22:13 +0000</pubDate>
<guid isPermaLink="false"></guid>
<georss:point>46.9490403456011 7.440057740911321</georss:point>
</item>
<item>
<title>Bahnhof Bern</title>
<description>@ Bahnhof Bern</description>
<link>https://foursquare.com/emeidi/checkin/</link>
<pubDate>Fri, 27 Sep 13 17:21:57 +0000</pubDate>
<guid isPermaLink="false"></guid>
<georss:point>46.94901360281286 7.440102388543716</georss:point>
</item>
<item>
...

PHP

...
foreach($xml->channel->item as $item) {
    $coords = $item->children('http://www.georss.org/georss')->point;
}
...

Via: SimpleXML and namespaces

Tags: , ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen

Samstag, 22. Juni 2013

Auf Twitter API 1.1 wechseln

Am 11. Juni wurden die in meinem Dashboard eingebundenen Twitter-Feeds plötzlich still. Der Grund: Twitter hat an diesem Tag API 1.0 deaktiviert und macht es seither Entwicklern und Script-Kiddies schwer, mit wenigen Zeilen Code auf öffentliche Twitter-Feeds zuzugreifen.

Wer vor dem selben Problem wie ich steht, sei hier eine kurze Anleitung präsentiert, um die Grundfunktionalität wieder online zu schalten:

Ich habe mich für die PHP-Klasse Codebird entschieden, da ich keine Lust hatte, OAuth-Requests und sonstigen Firlefanz auf eigene Faust zu programmieren.

Zuerst eröffnet man über den offiziellen Entwicklungsbereich mit dem persönlichen Twitter-Konto eine neue Twitter-Applikation. Wichtig ist, dass man sich den Consumer Key und das Consumer Secret merkt, denn diese beiden Variablen werden von der Klasse zur Kommunikation mit Twitter benötigt — authentifizieren sie doch die Applikation gegenüber dem Kurznachrichtendienst.

Nachdem man codebird.php sowie — ganz wichtig — cacert.pem in einem Unterverzeichnis des Web-Roots abgelegt hat, integriert man die Klasse in die bestehen Infrastrukutur:

        function __construct() {
            parent::__construct();
            
            require_once(dirname(__FILE__) . '/twitter/codebird.php');
            \Codebird\Codebird::setConsumerKey('AAAAAAAAAAAAAAAAAAAAA', 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAA');
            $this->cb = \Codebird\Codebird::getInstance();
        }

Als nächstes benötigt man einen sog. Bearer Token. Einmal generiert kann dieser für jeden weiteren Request an die Twitter API wiederverwendet werden — sofern man den Token nicht manuell deaktiviert. Alternativ kann man diesen Token auch bei jedem Aufruf des PHP-Scripts neu von Twitters Server abholen, was aber nicht einer guten Entwicklungspraxis (Cacheing!) entspricht.

Den Bearer Token erhält man in etwa folgendermassen:

$reply = $this->cb->oauth2_token();
$bearerToken = $reply->access_token;
\Codebird\Codebird::setBearerToken($bearerToken);

Jetzt also ist man wie mit API 1.0 in der Lage, eine simple Abfrage abzusetzen — im vorliegenden Fall rufe ich die letzten Tweets von John Gruber ab:

$this->cb->setReturnFormat(CODEBIRD_RETURNFORMAT_JSON);
$jsonData = $this->cb->statuses_userTimeline(array('screen_name' => 'daringfireball','count' => 25),true); // 'true' is needed for app only auth

Damit die Kompatibilität von bestehendem Code mit der neuen Klasse gewahrt wird, nenne ich JSON explizit als Antwortformat. Ganz wichtig ist, dass die Codebird-Methode als zweiten Paramenter true übergeben erhält — dies weist die Klasse an, die Anfrage im Kontext einer Applikation und nicht eines bestimmten Benutzers abzusetzen.

Tags: , , , , , ,
Labels: Web

Keine Kommentare | neuen Kommentar verfassen

Mittwoch, 22. Mai 2013

Konkurrenz-Probleme in PHP

In den letzten Wochen habe ich intensiv an meinem Raspberry Pi-Dashboard für unser Apartment geschraubt und gebastelt. Verschiedene Tiles werden mittels AJAX-Requests, welche über jQuery abgesetzt werden, mit Inhalten versorgt — und dies regelmässig alle paar Minuten.

Ein mysteriöser Bug machte mir in den letzten Tagen das Leben schwer: Es konnte vorkommen, dass zwei Tiles mit den Aktienkursen desselben Unternehmens gefüllt wurden, obwohl die Tile-IDs unterschiedliche ISINs trug.

Nach einigen Minuten Debugging schwante mir dann plötzlich, was das Problem war: Der auf dem Server lokal zwischengespeicherte HTML-Cache war zwar in Dateien mit der richtigen ISIN im Dateinamen abgelegt — der Inhalt war aber identisch. Konkret enthielt die HTML-Datei über das Unternehmen AAPL die Daten für das Unternehmen GOOG.

Wie sich heraustellte, hatte ich mein Script nicht dermassen intelligent programmiert, dass es mit mehreren, nur wenige Millisekunden auseinanderliegenden Requests ordnungsgemäss umging. Um nämlich den Aktienkurs einer ISIN abzufragen, muss dieser zuerst über einen HTTP-Request an ein Suchscript meines unfreiwilligen Kursanbieters in eine vom Anbieter intern verwendete NOTATION_ID umgewandelt werden (was wissbegierigen Lesern dieses Blogs einen Hinweis gibt, von welcher Web-Site ich die Kursdaten beziehe). Erst danach kann ich die Informationsseite des auf dem Aktienmarkt gehandelten Unternehmens aufrufen, herunterladen und im lokalen Dateisystem ablegen.

Konkret war mein Fehler, dass ich die Suchergebnisse zur Umwandlung der ISIN in die NOTATION_ID in eine Cache-Datei mit statischem Dateinamen ablegte. Da die Requests mit wenigen Millisekunden Versatz beim Server ankamen, war die Cache-Datei manchmal bereits von einem späteren Request überschrieben worden, als ich den HTML-Code endlich in PHP einlesen wollte (ich denke, dass wir hier von einigen wenigen hundert Millisekunden sprechen). Dies führte dazu, dass ich einer ISIN fälschlicherweise die NOTATION_ID des nachfolgenden Requests zuwies und diese plötzlich für zwei Kursanbieter galt. Da ich dieses Mapping aus Performance-Gründen ebenfalls serialisiert in einer Cache-Datei ablegte, wurden ab diesem Zeitpunkt zwei Tiles mit identischen Inhalten befüllt.

Die erste Lösung, dem Dateinamen einen sich ständig ändernden Wert einzupflegen, scheiterte grandios, weil ich time() verwendete. Da die Requests innerhalb der selben Sekunde auf dem Server eintreffen konnten, konnte die Cache-Datei im schlimmsten Fall weiterhin vom nachfolgenden Request überschrieben werden. Die zweite Lösung war deshalb, durch das Einfügen einer mittels rand(1000,9999) definierten Zufallszahl einen wahrlich zufälligen Wert in den Dateinamen einzuschleusen. Die Chancen, einen identischen Cache-Namen zu erwischen, ist hier 1:10000 — good enough, würde ich sagen.

Natürlich hätte ich auch einfach die ISIN in den Namen der Cache-Datei aufnehmen können. Hier hegte ich aber Zweifel, weil ich nicht wollte, dass bei einem Downloadfehler einfach unbemerkt die bestehenden, veralteten Daten eingelesen würden.

Tags: , , , , , , ,
Labels: Programmierung

1 Kommentar | neuen Kommentar verfassen

Sonntag, 12. Mai 2013

Wetter-Icons als Web-Font in ein Dashboard einbinden

Derzeit arbeite ich mit Hochdruck an einem Dashboard, welches ich in unserer Mietwohnung in Form eines TFT-Bildschirms beim Eingangsbereich platzieren möchte. Das Dashboard basiert softwaretechnisch auf HTML, CSS, JavaScript und PHP. Es integriert gecachte HTML-Dateien, JSON-Abfragen (MEZI, Twitter) und sogar einen iCalendar.

Ein Element (resp. „Tile“) des Dashboards wird die Wettervorhersage für den morgigen Tag sein. Um nicht nur eine qualitative Beschreibung des Wetters anzuzeigen, sondern auch ein Icon, habe ich mich natürlich vom derzeitigen (technologischen wie designtechnischen) Platzhirschen forecast.io inspirieren lassen. Dessen mit HTML5-Canvas animierten Icons waren dann aber doch Overkill für mich.

Stattdessen habe ich auf die Meteocons Web-Font von Alessio Atzeni zurückgegriffen. Die Web-Font ist in der Public Domain und lässt sich somit problemlos in ein HTML5/CSS3-Web-Projekt einbinden.

Leider sind die Icons meines Wissens nicht einem standardisierten Wettertyp zugeordnet, weshalb ich dieses Mapping mittels eines PHP-Arrays von Hand nachholen musste. Das Resultat lässt sich durchaus sehen:

eMeidi.local Dashboard Weather Forecast

Tags: , , , ,
Labels: Web

3 Kommentare | neuen Kommentar verfassen

Mittwoch, 24. April 2013

WordPress verbieten, YouTube-Links automatisch einzubetten

Um zu verhindern, dass YouTube-Links in einem WordPress-Beitrag automatisch als Objekt eingebettet werden, muss man in seinem Theme in den entsprechenden Dateien des Themes (bei mir: single.php) zuoberst im PHP-Block folgenden Befehl einfügen:

...
remove_filter( 'the_content', array( $GLOBALS['wp_embed'], 'autoembed' ), 8 );
...

Tags: , , , , ,
Labels: Web

Keine Kommentare | neuen Kommentar verfassen

Donnerstag, 14. Februar 2013

Data Mining mit Ubuntu, MySQL, PHP und Python

Beruflich setze ich mich derzeit mit der Analyse von Inhalten von Web-Sites auseinander. Nachfolgend habe ich einige Erfahrungen aufgelistet, welche ich dabei gemacht habe.

Wir verarbeiten in diesem Projekt Web-Sites, welche wir mit entsprechenden Tools aus dem Web auf den lokalen Rechner gespiegelt haben. Die Web-Site Assets liegen im Dateisystem. Zur Weiterverarbeitung der Daten wurden die HTML-, JS-, CSS- und XML-Rohdaten in eine MySQL-Tabelle gespitzt (12GB) und anschliessend mit Meta-Daten ergänzt.

Ich persönlich bin nicht sicher, ob die Ablage von HTML-Code in der Datenbank die sinnvollste und performanteste Lösung ist, aber dies war nunmal der Stand des Projektes als ich dazu gestossen bin — und daran liess sich nichts mehr rütteln.

Da wir unter anderem auch Volltextsuchen auf die Grunddaten anwenden, hätte ich mir Apache Solr genauer angeschaut und darauf mittels PHP und JSON zugegriffen.

Kommandozeile

Da der Ubuntu-Server aus mir unerfindlichen Gründen mit LAMPP aufgesetzt wurde, befinden sich die Binaries wie php nicht in den Standardpfaden und werden von bash ohne absolute Pfadangabe nicht gefunden. Da man den Interpreter täglich dutzende, wenn nicht gar hunderte Male aufruft, ist es ratsam, das Verzeichnis sofort in die $PATH-Variable des Shells aufzunehmen.

Hier findet man sich in der Shell Startup File Hell wieder. Je nachdem, ob man lokal arbeitet oder sich per SSH einloggt muss der Befehl an einem anderen Ort stehen. Schlussendlich habe ich die nachfolgende Zeile …

...
PATH="$PATH:/opt/lampp/bin"

… ans Ende folgender zwei Dateien angefügt:

  • ~/.profile
  • ~/.bashrc

Datenbank (MySQL)

Cache aktivieren

Wir führen PHP-Scripts über die Linux-Shell aus. Gerade bei der Entwicklung neuer Scripts sind verschiedene Anläufe nötig, bis alle Bugs und nicht beabsichtigten Funktionen ausgemerzt sind. Da wir oftmals an einem Datenset von 60’000+ Seiten arbeiten, ist es unabdingbar, dass wir eine Cacheing-Lösung anwenden, um Datenbankabfragen im Kurzspeicher zwischenzulagern.

Folgende Parameter in my.cnf aktivieren den in MySQL vorhandenen Cache:

...
[mysqld]
...
query_cache_type = 1
query_cache_size = 512M
query_cache_limit = 32M

Der Geschwindigkeitsgewinn ist immens — nachdem ein Script mit SELECT-Statements zum ersten Mal ausgeführt wurde und dafür mehrere Minuten benötigte, rauscht es in den folgenden Malen innert Sekunden durch.

Programmierung (PHP und Python)

Helper-Funktionen und -Klassen

Wie aus der Web-Entwicklung gewohnt sollte man bei jedem der mit der Zeit entstehenden Scripts zu Beginn Klassen, allgemeine Einstellungen und Funktionen einbinden.

In der Funktionsbibliothek setze ich beispielsweise folgende wichtigen Parameter in globaler Form:

...
error_reporting(E_ALL);
date_default_timezone_set('Europe/Zurich');

Auch die Datenbankverbindung wird mittels meiner MySQL-Klasse hier erstellt und an alle Scripts weitergegeben, welche die Library einbinden.

Aufbau der Scripts

Als ich zum Projekt stiess herrschten Spaghetti-Code in teils monolithischen Scripts vor. Ich habe meine eigenen Scripts dann aber so entwickelt, dass sie dem Unix-Gedanken folgend normalerweise nur eine bestimmte Funktion ausführen, diese dafür aber ausgezeichnet und in sich abgeschlossen. So steht im Normalfall in jedem Script, welches Daten manipuliert, zuoberst ein SQL-Query, welches die zu verändernden Datenbankdaten auswählt.

Bei der Entwicklung wählt man hierbei ein Query, das einen oder nur wenige Werte aus der Datenbank ausliest und verarbeitet — in dieser Phase hat man keine Zeit, möglicherweise fehlerhafte Manipulationen an 60’000+ Seiten durchzuführen.

HTML mit regulären Ausdrücken parsen?

Nein, besonders nicht dann, wenn man konkret an Eigenschaften des SGML-Markup interessiert ist (bspw. Wohin zeigen Links?). Hierzu verwendet man die in PHP standardmässig enthaltene DOMDocument-Bibliothek.

Wichtig ist, dass man bei der Verwendung von E_ALL die loadHTML()-Funktion mittels @ stummschaltet, weil sonst das Terminal mit Warnungen über fehlerhaften HTML-Code (leider an der Tagesordnung) vollgespamt wird:

...
$dom = new DOMDocument();
@$dom->loadHTML($html);
$elements = $dom->getElementsByTagName('a');

HTML manipulieren — und Fallstricke

Für jede HTML-Seite erstellen wir eine Nur-Text-Version. Hierzu verwenden wir html2text.py von Aaron Swartz selig. Bevor das HTML aber umgewandelt wird, säubern wir die HTML-Datei auf eigene Faust. Auch hier kommt DOMDocument zum Zug.

Wir suchen dabei zuerst einmal Elemente jeglicher Art, deren ID oder Klasse den String nav, menu und breadcrumb enthält. Die Navigation interessiert uns nämlich nicht, und noch schlimmer: Sie verfälscht teilweise die Resultate, weil in der Navigation gesuchte Begriffe vorkommen.

Hierzu lade ich den HTML-Code wieder in eine DOMDocument und iteriere danach über alle Elemente auf der Suche nach den besagten IDs und Klassennamen:

function cleanNavMenuElements($html = null) {
		$dom = new DOMDocument();
		@$dom->loadHTML($html);
		
		$changesMade = false;
		
		$elements = $dom->getElementsByTagName('*');
		foreach($elements as $element) {
			if(preg_match('/^(html|body)$/',$element->nodeName)) {
				// Otherwise we might delete the whole DOM!
				continue;
			}
			
	        if($element->hasAttribute('class') && preg_match('/(nav|menu|breadcrumb)/i',$element->getAttribute('class')) > 0) {
				status('Found element with class ' . $element->getAttribute('class'));
				$element->parentNode->removeChild($element);
				
				$changesMade = true;
				
				// Don't go further if we removed this node already
				continue;
			}
			
			if($element->hasAttribute('id') && preg_match('/(nav|menu|breadcrumb)/i',$element->getAttribute('id')) > 0) {
				status('Found element with id ' . $element->getAttribute('id'));
				$element->parentNode->removeChild($element);
				
				$changesMade = true;
			}
	    }
		
		if(!$changesMade) {
			return $html;
		}
		
		return $dom->saveHTML();
	}

Ein Problem manifestiert sich bei der Manipulation aber: Jegliche Anpassungen erfolgen live, was verwirrende Folgen für Schleifen haben kann.

Da die Suche nach obigen ID- und Klassennamen nicht alle Navigationselemente eliminiert, suche ich in einem zweiten Anlauf Tabellen und Listen, deren Elemente ausschliesslich Links enthalten. Dies ist ein guter Indikator, ein Navigationsblock gefunden zu haben.

Hier ist das Problem des sich bei jeder Iteration veränderndem DOM aber sehr ausgeprägt. Wenn ich deshalb durch td-Elemente und li-Elemente iteriere, verwende ich nicht foreach() sondern eine for()-Schleife, deren Counter $i ich immer dann zurücksetze, wenn ich ein Element entferne. Ansonsten wird aus Erfahrung in der Folge eines (oder mehrere Elemente) übersprungen. Damit dies klappt, arbeite ich den DOM Tabellen- respektive Listenweise ab:

$containers = $dom->getElementsByTagName($containerTag);
	
foreach($containers as $container) {
	$items = $container->getElementsByTagName($itemTag);
	
	for($i = 0; $i < $items->length; $i++) {
		$item = $items->item($i);
		
		if($item === null) {
			continue;
		}
		
		$otherTagsPresent = false;
		foreach($item->childNodes as $child) {
			$tag = $child->nodeName;

			if($tag == '#text') {
				$text = trim($child->nodeValue);
				$len = strlen($text);
				if($len < 1) {
					//#text is empty, thus not relevant
					continue;
				}
			}

			if($tag != 'a') {
				$otherTagsPresent = true;
				continue;
			}
		}

		if(!$otherTagsPresent) {
			$item->parentNode->removeChild($item);
			$i = $i-1;
		}
	}

Weiterverarbeitung der Daten durch Nicht-IT-Profis

Zur Weiterverarbeitung der Auswertungen durch andere, in IT nicht versierte Mitarbeiter habe ich eine Funktion geschrieben, welche eine Pfadangabe sowie CSV-Daten als Argumente übertragen erhält. Die Datei wird geschrieben und gleich anschliessend mittels eines kleinen Python-Scripts (Stichwort: openpyxl) in das bei uns hauptsächlich verwendete XLSX-Format konvertiert.

Tags: , , , , , , , , ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen

Mittwoch, 13. Februar 2013

Mit PHPExcel eine xlsx-Datei parsen und in MySQL importieren

Die einfachste Methode, die sich nach dem Studium der Dokumentation und Stackoverflow-Posts zum Thema PHPExcel ergeben hat, greift auf die PHPExcel_Worksheet::toArray()-Funktion zurück:

$objReader = PHPExcel_IOFactory::createReader(PHPExcel_IOFactory::identify($path));
$objReader->setReadDataOnly(true);

$objPHPExcel = $objReader->load($path);

$sheets = $objPHPExcel->getSheetNames();	
$sheetsRev = array_flip($sheets);

if(!isset($sheetsRev['Queries'])) {
	die('$sheetsRev[Queries] not set');
}

$objSheet = $objPHPExcel->setActiveSheetIndex($sheetsRev['Queries']);

$data = array();
foreach($objSheet->toArray() as $rowNum=>$row) {
	foreach($row as $cellNum=>$value) {
	...
	}
}

Es gibt eine andere Lösung, welche aber erforderlich macht, dass man die Dimension der einzulesenden Daten kennt; sprich die Anzahl der Spalten und Zeilen müssen vor der Iteration ausfindig gemacht werden. Mit toArray() überspringt man diesen unnötigen Schritt.

Tags: , , , ,
Labels: Programmierung

1 Kommentar | neuen Kommentar verfassen