Archiv Februar 2013

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

Keine Kommentare | neuen Kommentar verfassen

Sonntag, 3. Februar 2013

Einen lokalen Ordner in ein unter VirtualBox virtualisiertes Linux einbinden

Über das Wochenende durfte ich mich mit Compass Network Security AGs Hacking-Lab Live-Distro und darin enthaltenen Hacking-Übungen herumschlagen.

Die Live-Distro kann man als Image im ova-Format unter Mac OS X über den quelloffenen Virtualisierer VirtualBox in Betrieb nehmen.

Damit der Datenaustausch zwischen Gastgeber- und Gastbetriebssystem problemlos klappt, habe ich ein Ordner auf einer physischen Festplatte meines Mac minis in das Ubuntu-Linux integriert.

VirtualBox

Zuerst muss der künftig mit dem virtualisierten Betriebssystem geteilte Ordner mit VirtualBox selber bekannt gemacht werden:

  1. Rechtsklick auf die virtuelle Maschine
  2. Settings
  3. Shared Folders
  4. Machine Folders
  5. Add shared Folder definition
    • Folder Path: <Pfad auf den Ordner des Gastgeberbetriebssystems (hier: Mac OS X)>
    • Folder Name: <Eindeutiger Name, welcher bei der Einbindung in das Gastbetriebssystem benötigt wird (s. unten)>
    • [ ] Read-only (je nachdem, ob man Daten nur lesen oder aber auch unter Linux bearbeiten will)
    • [x] Auto-mount (bei mir musste ich das Laufwerk trotzdem manuell einbinden)

Ubuntu in der VM

Unter Ubuntu erstellt man zuerst einmal ein Verzeichnis, welches künftig als Mount-Point für den geteilten Ordner dienen soll:

# mkdir /mnt/Shared
# chmod 777 /mnt/Shared

Anschliessend führt man folgenden Mount-Befehl aus:

# sudo mount -t vboxsf <in VirtualBox definiert Folder Name (s. oben)> /mnt/Shared

Fertig!

Ab sofort kann man bspw. mittels TextMate unter Mac OS X ein Python-Script im geteilten Ordner bearbeiten und in der VM auf der Kommandozeile ausführen.

Tags: , , ,
Labels: Uncategorized

Keine Kommentare | neuen Kommentar verfassen