Archiv ‘Programmierung’

Dienstag, 19. Februar 2013

cld zwecks Spracherkennung in Python-Scripts integrieren

CLD ist der Compact Language Detector welcher in Google-Projekten zum Einsatz kommt — primär einmal in Googles Web-Browser Google Chrome. Mit diesem Detektor ist es möglich, die Sprache eines Textes zu erkennen.

Der Code des Detektors ist quelloffen, weshalb findige Entwickler den Code aus dem Google-Projekt herausgelöst haben und als eigenständige Linux-Library bereitstellen:

CLD – Compact Language Detector

Ich habe diesen Detektor in ein Projekt integriert und dabei die PHP-basierte Lösung Package Information: Text_LanguageDetect abgelöst, weil ich dessen Erkennungsgenauigkeit nicht als herausragend empfunden habe.

Download und Kompilation der Library

Zuerst klonte ich den Quellcode auf das Entwicklungssystem:

$ cd /tmp
$ git clone https://github.com/mzsanford/cld.git

Anschliessend versuchte ich die Kompilierung und Installation, doch dies schlug fehl:

$ cd /tmp/cld
$ ./configure
configure: error: cannot find install-sh or install.sh in "." "./.." "./../.."

Zuerst musste ich mir folgende Ubuntu/Debian-Pakete herunterladen:

# apt-get install build-essential libtool autoconf automake pkg-config

Via: run autoreconf -f -i -Wall,no-obsolete to fix install-sh problem

Anschliessend musste ich die Kompilierungsinformationen des Projektes neu generieren:

$ cd /tmp/cld
$ autoreconf -f -i -Wall,no-obsolete

Nach diesem Schritt klappte es auch mit der Kompiliererei:

$ cd /tmp/cld
$ ./configure

Installation der Library

Nun musste die Library noch installiert werden, was mit folgendem Befehl erfolgte:

# cd /tmp/cld
# make install

Installation der Python-Bindings

Noch war ich nicht am Ziel. Nachdem die Shared Library installiert war, fehlten noch die Python-Bindings, mit welchen die Library mittels eines simplen import cld in ein Python-Script importiert werden kann:

# cd /tmp/cld/ports/python
# make install
...
pycldmodule.cc:5:20: Schwerwiegender Fehler: Python.h: Datei oder Verzeichnis nicht gefunden
Kompilierung beendet.

Sorry, aber den Laptop habe nicht ich aufgesetzt — Linux läuft bei mir immer unter der englischen Sprache …

Es fehlte der Python-Quellcode, welchen ich folgendermassen nachrüstete:

# apt-get install python-dev

Anschliessend funktionierte auch der folgende Befehl sauber:

# cd /tmp/cld/ports/python
# make install

Die Originalversion dieses Artikels liess das Paket pkg-config nicht installieren. Ist dieses Paket nicht vorhanden, erscheint bei der Kompilierung folgende Fehlermeldung:

/tmp/cld/ports/python# make
python -u setup.py build
Traceback (most recent call last):
  File "setup.py", line 12, in 
    **pkgconfig('cld'))
TypeError: __init__() keywords must be strings
make: *** [build] Error 1

Nachdem pkg-config nachinstalliert wird, klappt es mit der Installation der Python-Bindings problemlos.

Via: Error when installing python bindings

Integration in Python-Scripts

Leider funktionierte die Integration der Library in ein Python-Script mittels import cld nicht:

Traceback (most recent call last):
File "", line 1, in 
ImportError: libcld.so.0: cannot open shared object file: No such file or directory

Zuerst musste noch die Shell-Variable LD_LIBRARY_PATH mit dem Pfad auf cld angepasst werden (sowohl in .bashrc als auch in .profile, weil ich immer noch nicht begriffen habe, welche Datei bei einem Shell-Login geladen wird — ich sollte wohl mal Shell startup scripts lesen, doch diese Grafik sagt wohl schon alles zum Thema aus):

...
export LD_LIBRARY_PATH="/usr/local/lib:/usr/local/lib/cld"

Via: Language detection with Google’s Compact Language Detector

Alternativ kann das Verzeichnis über die Verzeichnisstruktur /etc/ld.so.conf.d auch systemweit gesetzt werden (Via: How to define LD_LIBRARY_PATH for all applications).

Anschliessend konnte ich mit wenigen Zeilen Python-Code die Spracherkennung für Texte aktivieren:

import cld
...
detectedLangName, detectedLangCode, isReliable, textBytesFound, details = cld.detect(plaintext, pickSummaryLanguage=False, removeWeakMatches=False)

Tags: , , , , , ,
Labels: Programmierung

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

Freitag, 30. November 2012

Mit Notepad++ Text in Uppercase verwandeln

Ab und zu ist man in der Situation, Text in Kleinbuchstaben in Grossbuchstaben umzuwandeln. Hier hilft Notepad++, ein quelloffener Texteditor, welcher auf keiner Windows-Mühle fehlen darf:

Ctrl-Shift-U

Quelle: Notepad++ keyboard shortcuts

Tags: ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen

Freitag, 23. November 2012

SBB Techie-Challenge: Runde 4

Wer bei der SBB Techie-Challenge auf Facebook miträtselt und sich partout keinen Reim aus dem Java-Code machen kann und will, führe folgenden Python-Code aus:

def sbb(v,w,s1,s2):
	for y in range(0,v*w):
		str = ""
		for x in range (0,v*w):
			if(((x/w) % 2 == 0) != ((y/w) % 2 == 0)):
				str += s1
			else:
				str += s2
		
		print str


sbb(8,4," ","#")

Was des Rätsels Lösung ist verrate ich aber nicht.

PS: Ob SBB wirklich an Facebook-Script-Kiddies interessiert ist sei dahingestellt.

Tags: , ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen

Sonntag, 7. Oktober 2012

Syntax eines PHP-Scripts auf der Kommandozeile überprüfen

Nicht immer produziere ich auf dem lokalen Mac Code in einem Verzeichnis, welches über den lokalen Web-Server angesprochen werden kann. Wenn ich den Code auf den Server des Kunden kopiert habe und mir dort eine gähnende Leere entgegenblickt, ist die Vermutung in der Regel ein Syntax-Problem, welches den PHP-Parser zum Abbruch bewegt. So genau weiss man das natürlich nicht, da man als guter Entwickler display_errors auf einem Produktivsystem auf 0/off/false gesetzt hat und Fehlermeldungen nur in das error_log ablegt.

Entweder öffnet man das error_log via FTP, oder aber man lässt die Syntax auf dem Mac selber von der Kommandozeilenversion von PHP überprüfen:

php -l <Pfad zum syntaktisch defekten PHP-Script>

Dies führt entweder zur Absolution mit

No syntax errors detected in <Pfad zum syntaktisch defekten PHP-Script>

oder aber im unglücklicheren Fall zur folgenden oder ähnlichen Fehlermeldung:

Parse error: parse error in <Pfad zum syntaktisch defekten PHP-Script> on line 146
Errors parsing <Pfad zum syntaktisch defekten PHP-Script>

Tags: , , , , ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen

Mittwoch, 18. Juli 2012

Microsoft Excel beschwert sich über „Too many different cell formats“

Es vergeht keinen Tag am Bürorechner, an welchem man nicht eine neue Fehlermeldung in Microsoft-Produkten präsentiert kriegt – und sich beim der Recherche zum Fehler wieder einmal wünschen würde, jeden einzelnen Entwickler in Redmond einen unendlich langsamen Tod sterben zu lassen.

Microsoft gibt seinen Endanwendern die grandiose Möglichkeit, jede einzelne Tabellenzeile mit einem individuellen Format zu versehen. Rahmendicke, Hintergrundfarben, Schriftschnitte können individuell definiert werden. Und jede so formatierte Zelle ergibt ein neues Zellformat, welches in der Excel-Datei zentral gesichert wird.

Das Schöne daran ist, dass es offenbar Endanwender in Schwellenländern gibt, welche es bei der Erledigung von ausgelagerten Arbeiten fertigbringen, annähernd 65’000 Formate in einer einzigen Excel-Datei mit ca. 20 Sheets unterzubringen. Heilige Shiva! Das Problem: Excel kriegt massiven Schluckauf, sobald diese Limite (wahrscheinlich mit der Grenze für unsigned integers) erreicht ist – nichts geht mehr, keine Zellformatierung, kein Zusammenführen von Zellen, ja nicht einmal kopieren geht mehr. Und dazu die lapidare Fehlermeldung:

Too many different cell formats.

Die Lösung ist im Grunde simpel: Das Dokument muss analysiert und nicht verwendete Styles gelöscht werden. Dafür gibt es mittlerweile unzählige Varianten, davon hier nur drei:

  • Kommerziell. 39 Dollar will der Entwickler des Style Reduction Tools haben, damit die Excel-Endanwender wieder arbeiten und nicht Excel-Fehlermeldungen wegglicken und Abstürze überwinden müssen.
  • Handgestrickt. Auf Stackoverflow findet sich VBA-Code, welchen man mit entsprechenden Kenntnissen über eine Excel-Datei laufen lassen kann. Fallstricke:
    • .xlsx-Dateien müssen zuerst in .xlsm-Dateien umkonvertiert werden
    • Danach muss man das Developer-Ribbon aktivieren
    • Anschliessend fügt man ein Modul in die Excel-Datei ein
    • Nun kopiert man den Source-Code in das Modul hinein
    • Schlussendlich muss unter Tools > References noch „Microsoft Scripting Runtime“ aktiviert werden
    • Jetzt kann man die Funktion starten, fliegt aber rasch auf die Schnauze, weil Dim iStyleCount As Integer als Integer definiert ist und deshalb überläuft
    • Nachdem der Laptop für ca. 20-30 Minuten wie totgefroren ist, erscheint dann die erlösende Meldung: Statt der ursprünglich 64999 gefundenen Styles sind nach der Bereinigung nur noch effektiv 10 verwendete Stück im Dokument
  • Beschleunigt. Da sich Microsoft des Problems bewusst ist, verlinken sie in ihrem KB-Artikel zur Fehlermeldung auf ein kostenfreies Tool eines unabhängigen Entwicklers (.NET4.0 version of the XLStylesTool is now available), welcher die Styles auf OOXML-Ebene säubert, was einen massiven Geschwindigkeitsgewinn gegenüber der VBA-Version ermöglicht. WICHTIG: Nachdem die Applikation gestartet und die Datei ausgewählt wurde, muss zwingend [x] „Commit changes …“ ausgewählt werden, damit die Anpassungen gespeichert werden können.

VBA-Code

Via: Too Many Different Cell Formats, ergänzt/überarbeitet von eMeidi.com

' Description:
'    Borrowed largely from http://www.jkp-ads.com/Articles/styles06.asp
' Additional debug message piping to .txt file in C:\ by eMeidi.com
' Bugfixing iStyleCount from Integer to Long by eMeidi.com

Option Explicit

' Description:
'    This is the "driver" for the entire module.
Public Sub DropUnusedStyles()

    Dim styleObj As Style
    Dim rngCell As Range
    Dim wb As Workbook
    Dim wsh As Worksheet
    Dim str As String
    Dim iStyleCount As Long
    Dim dict As New Scripting.Dictionary    ' <- from Tools / References... / "Microsoft Scripting Runtime"

    ' wb := workbook of interest.  Choose one of the following
    ' Set wb = ThisWorkbook ' choose this module's workbook
    Set wb = ActiveWorkbook ' the active workbook in excel
    
    Dim strPath As String
    strPath = "C:\excelFormatCleanerDebug.txt"
    Dim f
    f = FreeFile()
    Open strPath For Output As f
    'Print #f, Text

    Debug.Print "BEGINNING # of styles in workbook: " & wb.Styles.Count
    'GoTo Bla
    
    ' dict := list of styles
    For Each styleObj In wb.Styles
        str = styleObj.NameLocal
        iStyleCount = iStyleCount + 1
        Call dict.Add(str, 0)    ' First time:  adds keys
        'Debug.Print str & ", ";
        Print #f, str & ", "
    Next styleObj
    Debug.Print "  dictionary now has " & dict.Count & " entries."
    ' Status, dictionary has styles (key) which are known to workbook
    'GoTo Bla
    
    ' Traverse each visible worksheet and increment count each style occurrence
    For Each wsh In wb.Worksheets
        If wsh.Visible Then
            For Each rngCell In wsh.UsedRange.Cells
                str = rngCell.Style
                dict.Item(str) = dict.Item(str) + 1     ' This time:  counts occurrences
                Print #f, str & "+1"
            Next rngCell
        End If
    Next wsh
    ' Status, dictionary styles (key) has cell occurrence count (item)


    ' Try to delete unused styles
    Dim aKey As Variant
    On Error Resume Next    ' wb.Styles(aKey).Delete may throw error

    For Each aKey In dict.Keys

        ' display count & stylename
        '    e.g. "24   Normal"
        Debug.Print dict.Item(aKey) & vbTab & aKey
        Print #f, dict.Item(aKey) & vbTab & aKey

        If dict.Item(aKey) = 0 Then
            ' Occurrence count (Item) indicates this style is not used
            Call wb.Styles(aKey).Delete
            If Err.Number <> 0 Then
                Debug.Print vbTab & "^-- failed to delete"
                Err.Clear
            End If
            Call dict.Remove(aKey)
        End If

    Next aKey

    Debug.Print "ENDING # of style in workbook: " & wb.Styles.Count
Bla:
End Sub

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

Keine Kommentare | neuen Kommentar verfassen

Montag, 16. Juli 2012

PHPs serialize und unserialize-Funktion mit Unicode-Problemen

Ich habe es mir angewöhnt, regelmässig in das PHP-Error-Log auf meinem privaten Web-Server zu schauen, in welches mit E_ALL jede erdenkliche Art von Fehlern aufgezeichnet werden.

Und kürzlich häuften sich durch einen Spider verursachte Fehler in der Form:

...
PHP Notice:  unserialize(): Error at offset 1573 of 20531 bytes in /var/www/sites/domain.ch/administration/inkludierung/file.php on line 1816
...

Heute hatte ich Zeit, dem Problem auf den Grund zu gehen. Die erste Aufgabe bestand einmal daraus, den serialisierten String vor der Speicherung in die Datenbank mit dem serialisierten String nach dem Extrakt aus der Datenbank zu vergleichen. Hierzu schrieb ich die beiden Zeichenketten in Dateien im Web-Root. Wie sich mit einem Blick erkennen liess, war der serialisierte String nach dem Zurücklesen aus der Datenbank um 30 Bytes grösser.

Was war die Ursache? Natürlich vermutete ich auf Anhieb Probleme mit dem Zeichensatz und Sonderzeichen, von welchen es in der deutschen Sprache halt ziemlich viele gibt (jedenfalls aus der Optik von 127 ASCII-Zeichen). Da PHP Offsets in Bytes (und nicht Zeichen) angibt, war es hilflos, den String in einem Texteditor wie TextMate anzuschauen. Stattdessen rief ich 0xED auf, und musste danach „offset 1573“ in Hex umrechnen — mit dem erstbesten Tool, welches ich im Web fand: Hexadecimal Conversion. Der Blick auf den Array-Wert bestätigte meine Befürchtung: Es handelte sich um eine Zeichenkette, welche deutsche Umlaute enthielt.

NB: Der Offset lag aber ca. 10 Zeichen hinter dem Auftreten der Sonderzeichen, aber immer noch innerhalb des Strings, welcher die Sonderzeichen enthielt.

Nach etwas Googlen realisierte ich, dass PHPs serialize() und unserialize() nicht den besten Ruf besitzen und äusserst fehleranfällig sind — besonders, wenn es um Multibyte-Strings geht. Nachdem ich kurzfristig auf json_encode() und json_decode() wechseln wollte, das Vorhaben aber abbrechen musste, weil ich mein Mega-Array mangels nicht fortlaufender Keys teilweise in Objekte umgewandelt zurückerhielt, stiess ich auf folgenden Blog-Post:

PHP Serialize() & Unserialize() Issues

Seit ich die serialisierten Zeichenketten noch durch Base64-En- resp. Dekodierung durchlaufen lasse, bleibt mein PHP-Error-Log leer:

...
$toDb = base64_encode(serialize($sitemap));
...
$sitemap = unserialize(base64_decode($fromDb));

Einziger Nachteil: Durch Base64-Encodierung setzt der String 7 Kilobytes an Länge an. Aber mir soll’s Recht sein.

Tags: , ,
Labels: Programmierung

4 Kommentare | neuen Kommentar verfassen

Sonntag, 6. Mai 2012

PHP kriegt das mit den Nummern nicht gebacken

Da las ich vor einigen Tagen einen interessanten Beitrag über die haarsträubenden Schwächen von PHP — und erfuhr eines der genannten Problem heute am eigenen Leibe.

Eine meiner Funktionen skaliert Bilder, um sie danach aus der Mitte heraus zuzuschneiden. Vor dem Zuschnitt wird das Bild skaliert. Ziel ist es dabei, das Bild so zu skalieren, dass sowohl die Höhe und Breite des Zielformats überschritten werden, denn so lässt sich mit gutem Gewissen ein Zuschnitt vornehmen.

Mein Code sieht folgendermassen aus:

...
for($i = 0; $i<=1; $i++) {
        $factor = $dimDestination[$i]/$dimOriginal[$i];

        $dimTests[$i] = array($dimOriginal[0]*$factor, $dimOriginal[1]*$factor, $factor);
}

$factor = null;

foreach($dimTests as $key=>$dimTest) {
        if($dimTest[0] >= $dimDestination[0] && $dimTest[1] >= $dimDestination[1]) {
                $factor = $dimTest[2];
                $keySelected = $key;
        }
        else {
                $this->debug->add($dimTest[0] . '<' . $dimDestination[0] . ', ' . $dimTest[1] . '<' . $dimDestination[1]);
        }
}

if($factor === null) {
        $this->debug->add('$factor ' . $factor . ' is null');
        return false;
}

Bei einem speziell zugeschnittenen Bild wurde $factor immer auf null gesetzt und die Funktion brach mit false ab — obwohl das Bild eigentlich problemlos skaliert und zugeschnitten hätte werden sollen.

Nach einer halbstündigen Fehlersuche kam ich den Problem schlussendlich auf die Spur. Zuerst einmal schaute ich mir die in $this->debug gespeicherten Infos an:

emeidiImage->cropFromCenter()
505<505, 131.510416667<66

Was zum Teufel?! Wieso ist 505 kleiner als 505? Mit vardump() lichtete sich der Nebel über dem falschen Verhalten:

array(2) {
  [0]=>
  array(3) {
    [0]=>
    float(505)
    [1]=>
    float(131.510416667)
    [2]=>
    float(1.05208333333)
  }
  ...
}

array(2) {
  [0]=>
  int(505)
  [1]=>
  int(66)
}

Ich verglich float(505) mit int(505), und da lag wohl die Krux vergraben! Nun, dachte ich mir, wandeln wir die Dimension halt bei der Multiplikation der Ursprungsdimension mit dem Faktor in einen Integer-Wert um:

$dimTests[$i] = array((int)($dimOriginal[0]*$factor), (int)($dimOriginal[1]*$factor), $factor);

Weiterhin brach die Funktion ab. Was zum Teufel … ein erneuter Blick auf vardump() zeigte mir endlich die wirklich Ursache des Problems:

array(2) {
  [0]=>
  array(3) {
    [0]=>
    int(504)
    [1]=>
    int(131)
    [2]=>
    float(1.05208333333)
  }
  ...
}
array(2) {
  [0]=>
  int(505)
  [1]=>
  int(66)
}

Ahaaa! Irgendwie war der Fliesskommawert eben nicht ganz genau 505, sondern vielleicht nur 504.999999.

Nach folgender Anpassung unter Zuhilfenahme der Funktion ceil() lief die Funktion endlich fehlerfrei durch:

$dimTests[$i] = array(ceil($dimOriginal[0]*$factor), ceil($dimOriginal[1]*$factor), $factor);

Tjach. Heute habe ich deshalb gelernt, dass int(505) noch lange nicht float(505) ist.

Nachtrag

Das Problem tritt mit der bei cyon.ch installierten PHP-Version 5.2.17 auf, nicht aber mit der auf meinem Entwicklungsserver installierten PHP-Version 5.4.0-3 auf.

Tags: , ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen

Donnerstag, 8. März 2012

Bedingte Formatierung in Microsoft Access mit mehr als drei Prüfwerten

Von Microsoft ist man Benutzerunfreundlichkeit gewohnt, und so verhält es sich auch mit der bedingten Formatierung (Conditional Formatting) von Reporten in Microsoft Access: Der Wert eines Feldes kann mit den GUI-Tools nur auf drei Stati gleichzeitig geprüft werden.

Zum Glück kann man auf VBA ausweichen. Hierzu öffnet man die Design View des Reports, wählt die Sektion „Detail“ mit Klick auf dessen Balken aus, wechselt im Eigenschaftsfeld auf Events und erstellt dort für das Ereignis „On Format“ eine VBA-Prozedur mit dem Code Builder.

Dort kann man als geübter Entwickler dann mit dem Select Case-Konstrukt arbeiten:

Private Sub Detail_Format(Cancel As Integer, FormatCount As Integer)
    Dim sStatus As Variant 'To account for null values
    sWert = Me!Wert
    
    Select Case sWert
    Case "Yes"
        ' Grün
        cColor = RGB(0, 255, 0)
    Case "In most cases"
        ' Gelb
        cColor = RGB(255, 255, 0)
    Case "In almost half the cases"
        ' Orange
        cColor = RGB(255, 215, 0)
    Case "In a few cases"
        ' Fuchsia
        cColor = RGB(255, 0, 255)
    Case "No"
        ' Rot
        cColor = RGB(255, 0, 0)
    Case "Not applicable", "N/A"
        cColor = RGB(192, 192, 192)
    Case Else
        cColor = RGB(255, 255, 255)
    End Select
    
    Debug.Print "Setting Color to " & cColor & " based on " & sWert
    Me!Wert.BackColor = cColor
End Sub

Ein Nachteil hat das ganze aber: Die Hintergrundfarbe wird nur in der Druckvorschau angezeigt. Auch das wieder: Typisch Microsoft. Unbrauchbare Software aus dem letzten Jahrhundert.

Tags: , , , ,
Labels: Programmierung

Keine Kommentare | neuen Kommentar verfassen