Parsen einer XML Datei mit PHP und Aufbau einer geeigneten Datenbank
Als erstes sollten wir uns überlegen wie die Daten sinnvoll in einer
relationalen Datenbank gespeichert werden.
Wir benötigen auf jeden Fall zwei Tabellen für die Top Level Domains (TLD)
und die Server. Die Server Tabelle wird auch den Suchstring beinhalten.
Da die Server und TLD Tabelle noch keinen Bezug haben
benötigen wir noch eine dritte Tabelle die die Server IDs mit den TLD IDs
in Bezug bringt.
Als Beispiel Tabellen nehmen wir ein Szenario, bei dem zwei Whois Server
für die .de TLD existieren
und ein Whois Server für .com und .net verantwortlich ist.
server
| server_id | server | string |
| 1 | whois.denic.de | no entries found |
| 2 | whois.nic.de | no entries found |
| 3 | whois.crsnic.net | no match for |
tld_server
| tld_id | server_id |
| 1 | 1 |
| 1 | 2 |
| 2 | 3 |
| 3 | 3 |
PHP und mySQL
Um in PHP mySQL Operationen durchzuführen
muss man sich erst mit dem mysqld verbinden.
Dies geschieht mit
mysql_connect().
Kommen Sie bitte nicht auf die Idee
mysql_pconnect() zu verwenden,
ausser Sie wissen genau was Sie tun.
Persistente Verbindungen lohnen sich fast nur auf einem eigenen Server,
der sich nur um Ihre Seite kümmert. Ansonsten können sehr schnell viele
offene Verbindungen da sein die nicht mehr genutzt werden und
es ist nicht mehr möglich neue aufzubauen.
Wenn nun die Verbindung steht, wählen wir als erstes mit
mysql_select_db() unsere Datenbank aus.
Natürlich muss diese schon vorher angelegt worden sein.
Jetzt können wir mit mysql_query() SQL Befehle ausführen.
Erstellen der Tabellen
Wir müssen einmalig unsere drei Tabellen (tld, server und tld_server)
erstellen. Für solche einmaligen Operationen kann man auch
phpMyAdmin benutzen,
um dort direkt SQL Befehle einzugeben oder die GUI zu verwenden.
Tabellen werden mit dem mySQL Befehl
CREATE TABLE erstellt.
Vorher sollten wir wissen welchen
Typ eine Spalte hat, und wo unsere
Indexe liegen.
Für die Größe und Performance ist es wichtig, dass die Typen nicht unnötig groß
vom Speicherverbrauch werden.
Es reicht daher aus, alle IDs den Typ SMALLINT zuzuweisen.
Eine SMALLINT Zahl geht von 0 bis 65535 und ist daher mehr als ausreichend.
Alle sämtlichen Felder sind Strings.
Diese Strings werden kaum die maximale Größe eines VARCHARS von 255 Zeichen
überschreiten.
Indexe (oder auch Schlüssel) werden überall dort benötigt, wo Suche, Sortierung
oder Verknüpfung
(JOINs)
vorkommen.
Also ist es schonmal eine gute Idee (und üblich) die IDs von tld und server
als Primärschlüssel festzulegen. Der Primärschlüssel von tld_server wird aus
den beiden Feldern zusammengesetzt.
Das tld Feld in der tld Tabelle wird auch ein Index,
denn später werden wir bei der Abfrage über die TLD den Whois Server ermitteln.
1 if (!mysql_connect('localhost', 'user', 'pwd')) {
2 die ('Could not connect to mysqld.');
3 }
4
5 if (!mysql_select_db('myDB')) {
6 die ('Could not select database: '.mysql_error());
7 }
8
9
10 mysql_query('
11 CREATE TABLE tld (
12 tld_id SMALLINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
13 tld VARCHAR(255) NOT NULL,
14
15 UNIQUE(tld)
16 )
17 ');
18
19
20 mysql_query('
21 CREATE TABLE server (
22 server_id SMALLINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
23 server VARCHAR(255) NOT NULL,
24 string VARCHAR(255) NOT NULL
25 )
26 ');
27
28
29 mysql_query('
30 CREATE TABLE tld_server (
31 tld_id SMALLINT UNSIGNED NOT NULL,
32 server_id SMALLINT UNSIGNED NOT NULL,
33
34 PRIMARY KEY(tld_id, server_id)
35 )
36 ');
Jetzt können bei diesen ganzen mySQL Operationen Fehler auftreten.
Diese Fehler wird PHP nicht ausgeben, da sie auf der Seite des mysqld passieren.
Aber PHP bietet die mysql_error() Funktion an, welche den letzten Fehler ausgibt.
Um überhaupt zu merken, dass ein Fehler passiert ist, kann jede mysql_* Funktion
false zurückgeben.
Vollständigerweise müsste obiges Beispiel auch jedes mysql_query() auf false
überprüfen und gegebenenfalls mysql_error() ausgeben.
mySQL Schreiboperationen
mySQL bietet zum Einfügen die Methode
INSERT.
Wenn mysql_query() kein false zurück gibt heißt das noch nicht,
dass die Operation erfolgreich war. Es kann z.B. sein, dass die einzufügende
Zeile bereits existiert (Primärschlüssel dürfen nicht zweimal vorkommen) und
daher nichts eingefügt wurde.
PHP liefert dafür die Methode
mysql_affected_rows(), welche angibt,
wieviele Zeilen geändert wurden.
Um bereits vorhandene Datensätze zu manipulieren gibt es in mySQL
UPDATE.
Auch hier gibt mysql_affected_rows() die Anzahl der geänderten Daten zurück.
Mit DELETE kann man Datensätze wieder entfernen.
Folgendes Beispiel füllt die Tabellen mit obigen Daten:
1 mysql_query('
2 INSERT INTO tld (tld) VALUES ("de"), ("com"), ("net")
3 ');
4
5 mysql_query('
6 INSERT INTO server (server, string)
7 VALUES
8 ("whois.denic.de", "no entries found"),
9 ("whois.nic.de", "no entries found"),
10 ("whois.crsnic.de", "No match for")
11 ');
12
13 mysql_query('
14 INSERT INTO tld_server (tld_id, server_id)
15 VALUES (1, 1), (1, 2), (2, 3), (3, 3)
16 ')
Jetzt hat sich allerdings ein kleiner Fehler eingeschlichen.
Jeder Suchstring muss klein geschrieben sein.
Der String von whois.crsnic.com ist es allerdings nicht.
Also bessern wir ihn mit UPDATE aus:
1 mysql_query('
2 UPDATE server
3 SET string="no match for"
4 WHERE server_id = 3
5 ');
mySQL Leseoperationen
Um für unser Vorhaben perfekt gerüstet zu sein brauchen
wir noch eine Möglichkeit um Datensätze zu lesen.
mySQL bietet dafür
SELECT an.
Und um das Ergebnis Zeilenweise als Array zu erhalten gibt es die PHP-Funktion
mysql_fetch_assoc().
Wenn uns allerdings nur die Anzahl der gefundenen Datensätze interessiert
genügt ein Aufruf von mysql_num_rows().
Dann lesen wir doch mal alle tld Datensätze ausser der .de Domain:
1 $result = mysql_query('
2 SELECT tld, tld_id FROM tld
3 WHERE tld != "de"
4 ');
5
6 while ($row = mysql_fetch_assoc($result)) {
7 echo $row['tld'];
8 }
Um die Server und TLD in die Datenbank zu schreiben
betrachten wir Sie als Objekte.
Um genauer zu sein Objekte der Klasse DB_Element.
Als erstes brauchen wir eine Klassenmethode connect()
welche sich einfach nur mit dem DBMS verbindet,
und die Datenbank auswählt.
Der Konstruktor erhält ein Assoziatives Array (Hashtabelle),
mit den Daten dieses Objektes und setzt damit das Attribut $data.
Ausserdem legt er das $table Attribut fest.
Die read() Methode soll ein existierendes Objekt finden und gegebenenfalls
$id setzen.
Das geschieht in dem der Datensatz mittels SELECT aus der Datenbank gelesen wird.
Die Felder für den Datensatz werden über die Schlüssel von $data ermittelt.
Zusätzlich wird noch die ID gelesen. Die setzt sich typischerweise aus
$table.'_id' zusammen.
Falls dann wirklich ein Datensatz gefunden wurde, wird die ID gesetzt und
der Datensatz als Array (ohne ID) zurückgegeben.
getSetString() gibt ein SQL SET Statement zurück. Dieser String
kann in einer UPDATE oder INSERT Operation genutzt werden.
Um das Objekt in die Datenbank zu schreiben benutzen wir eine insert() Methode.
Sie macht eine SQL INSERT Query mit dem $data Attribut.
Bei Erfolg wird das Attribut $id mit
mysql_insert_id() gesetzt.
Die update() Methode kümmert sich um eine SQL UPDATE Query.
Dabei wird das Attribut $id in der WHERE Bedingung genommen, um das UPDATE
einzuschränken.
Die save() Methode sorgt dafür,
dass das Objekt nur dann nicht gespeichert wird, wenn es bereits existiert
und gleich ist.
Erst wird geprüft, ob das Objekt bereits existiert.
Falls ja und es wurde geändert, wir ein update() durchgeführt.
Ansonsten wird ein insert() ausgeführt.
Um das vorhandene Objekt aus der Datenbank zu entfernen macht
die delete() Methode eine DELETE Query mit Hilfe des $id Attributs.
1 class DB_Element {
2
3 var $data, $id, $table;
4
5 function DB_Element($data, $table) {
6 $this->data = $data;
7 $this->table = $table;
8 }
9
10 function connect($user = 'user', $pwd = 'pwd', $db = 'myDB', $server = '') {
11 if (!mysql_connect($server, $user, $pwd)) {
12 trigger_error('Could not connect to mysqld.');
13 return false;
14 }
15
16 if (!mysql_select_db($db)) {
17 trigger_error(mysql_error());
18 return false;
19 }
20
21 return true;
22 }
23
24 function getID() {
25 return $this->id;
26 }
27
28 function read($key) {
29 $query = 'SELECT `'.$this->table.'_id`';
30 foreach ($this->data as $name => $value) {
31 $query .= ', `'.$name.'`';
32 }
33 $query .= ' FROM `'.$this->table.'` WHERE `'.$key.'`="'.$this->data[$key].'"';
34
35 if (!$result = mysql_query($query)) {
36 trigger_error(mysql_error());
37 return false;
38 }
39
40 if (mysql_num_rows($result) != 1) {
41 return false;
42 }
43
44 $result = mysql_fetch_assoc($result);
45
46 $this->id = $result[$this->table.'_id'];
47 unset($result[$this->table.'_id']);
48 return $result;
49 }
50
51 function getSetString() {
52 $set = 'SET';
53 foreach ($this->data as $name => $value) {
54 $set .= "\n`".$name.'`="'.$value.'",';
55 }
56 return substr($set, 0, -1);
57 }
58
59 function insert() {
60 $query = 'INSERT into `'.$this->table.'` '.$this->getSetString();
61 $return = mysql_query($query);
62 if ($return && mysql_affected_rows() == 1) {
63 $this->id = mysql_insert_id();
64 }
65 return $return;
66 }
67
68 function update() {
69 $query = 'UPDATE `'.$this->table.'`
70 '.$this->getSetString().'
71 WHERE `'.$this->table.'_id`='.$this->id;
72 return mysql_query($query);
73 }
74
75 function save($key) {
76 $read = $this->read($key);
77 if ($read) {
78 if ($read != $this->data) {
79 return $this->update();
80 }
81 return true;
82 }
83
84 return $this->insert();
85 }
86
87 function delete() {
88 $query = 'DELETE FROM `'.$this->table.'`
89 WHERE `'.$this->table.'_id`='.$this->id;
90 return mysql_query($query);
91 }
92
93 }
Jetzt können wir uns endlich an's Parsen ranmachen.
Dafür benutzen wir PHPs XML Parser.
Dieser Parser ist sehr einfach zu bedienen.
Man gibt ihm häppchenweise XML Daten und er ruft
je nach Ereignis eine vorher
spezifizierte Methode auf.
Als erstes muss ein Parser erstellt werden.
Dafür sorgt xml_parser_create().
xml_parse() setzt ihn in Bewegung.
Allerdings passiert so noch nichts.
Man muss dem Parser sagen welche Methode er bei welchem Ereignis aufrufen soll.
Das macht man mit xml_set_element_handler() für die Start- und Endtags
und xml_set_character_data_handler() für
den Textinhalt eines Elements.
Diese Klasse wird als Basisklasse dienen.
Unser Parser wird je nach Element ein neues Objekt
für die Ereignismethoden erhalten.
D.h. wir haben in dieser Basisklasse die
drei Methoden für die verschiedenen Ereignisse:
-
element_start() wird aufgerufen, wenn ein neues Element beginnt.
Diese Methode ist aber noch leer, weil ihr verhalten
erst in der entsprechenden Elementklasse bestimmt wird.
-
element_cdata() ist für den Textinhalt eines Elements zuständig.
Diese Methode wird auch erst bei Gebrauch in einer Elementklasse
implementiert.
-
element_end() wird beim Schließen des Elementtags aufgerufen.
Sie gibt dem Parser das alte Objekt (welches dann wieder aktuell ist)
für die Ereignisse wieder und ruft noch die final() Methode
von diesem Objekt auf. Das alte Objekt ist das $parent Attribut,
welches im Konstruktor übergeben wurde.
Dann gibt es noch Hilfsmethoden die notwendige Aufgaben vereinfachen.
Um dem Parser zu sagen, welches Objekt gerade verantwortlich ist,
gibt es die setHandler() Methode. Ein Aufruf der setHandler() macht
das Objekt zum Verantwortlichen.
getCallBack() gibt ein Array zurück welches die Methoden dieses Objekts
als Callbackmethoden auszeichnet. Die Verwendung von getCallBack() ist
notwendig, da wirklich nur die Referenz auf das Objekt in dem Array sein
darf. Ansonsten wird eine Kopie angelegt, und es gibt verschiedene Objekte.
Dann brauchen wir noch eine getName() Methode welche
nur den Namen des Elements ausgibt. Diese Methode muss von jeder Elementklasse
implementiert werden.
Der Konstruktor wird beim Start dieses Elements aufgerufen, erhält
also ein Array mit den Attributen
und ruft die init() mit diesen Attributen auf.
Die init() ist in der Basisklasse ohne Funktion.
1 class Parser {
2
3 var $parent;
4
5 function Parser(&$parent, $attr) {
6 $this->C($parent, $attr);
7 }
8
9 function C(&$parent, $attr) {
10 $this->parent = &$parent;
11 $this->init($attr);
12 }
13
14 function setHandler($parser) {
15 xml_set_element_handler($parser, $this->getCallBack('element_start'),
16 $this->getCallBack('element_end'));
17 xml_set_character_data_handler($parser, $this->getCallBack('element_cdata'));
18 }
19
20 function &getCallBack($func) {
21 $cb = array();
22 $cb[] = &$this;
23 $cb[] = $func;
24 return $cb;
25 }
26
27 function init($attr) {
28 }
29
30 function final() {
31 }
32
33 function element_cdata($parser, $data) {
34 }
35
36 function element_start($parser, $name, $attr) {
37 }
38
39 function element_end($parser, $name) {
40 if ($name == $this->getName()) {
41 $this->parent->setHandler($parser);
42 $this->final();
43 }
44 }
45
46 function getName() {
47 trigger_error('not implemented');
48 }
49
50 }
Diese Klasse erbt von der Parser Klasse. Sie ist für das Wurzelelement
SERVERLIST zuständig. D.h. mit Ihr beginnt alles.
Dann ist schonmal klar, dass die getName() "SERVERLIST" zurückgeben muss.
element_end() wird gar nichts machen, da es kein Elternelement mehr gibt.
element_start() wird nur auf das SERVER Element reagieren.
Es wird einfach die Kontrolle an ein neues Server_Parser Objekt abgegeben.
Mit dem Konstruktor wird das Parsen in Gang gesetzt.
Als erstes erstellen wir einen Parser. Dann lesen wir den Inhalt des
File Objekts, welches dem Konstruktor übergeben wurde.
Wir setzen diese Objekt als das Verantwortliche für den Parser
und beginnen auch gleich mit dem Parsen.
1 class Main_Parser extends Parser {
2
3 function Main_Parser(&$file) {
4 $parser = &xml_parser_create();
5 $xml = $file->read_once();
6
7 if (!$xml) {
8 return;
9 }
10
11 if (!DB_Element::connect()) {
12 return;
13 }
14
15 $this->setHandler($parser);
16 xml_parse($parser, $xml, true);
17 }
18
19 function element_start($parser, $name, $attr) {
20 if($name == 'SERVER') {
21 $obj = &new Server_Parser($this, $attr);
22 $obj->setHandler($parser);
23 }
24 }
25
26 function getName() {
27 return 'SERVERLIST';
28 }
29
30 function element_end($parser, $name) {
31 }
32
33 }
Diese Klasse erbt auch von der Parser Klasse.
Ein Server_Parser Objekt übernimmt während dem Parsen die Verantwortung
für ein SERVER Element.
Dann muss natürlich die getName() das auch zurückgeben.
Ein Objekt hat zwei Attribute:
$data ist ein Array mit den Daten für DB_Element, welche
am Ende gespeichert werden.
$domains ist ein Array mit den DB_Element Objekten von den Domains
die zu diesem Server gehören.
addDomain() fügt ein DB_Element Objekt zum $domains Attribut hinzu.
Diese Methode wird von den Domain_Parser Objekten aufgerufen.
Die init() kriegt ja das Array mit den Attributen des Elements.
Davon interessiert uns nur der HOST Eintrag, mit dem wir unser
$data Attribut erweitern.
Die Ereignis Methode element_start() soll nur auf die Elemente
DOMAIN und AVAILSTRING reagieren.
Bei einem neuen DOMAIN Element, wird die Kontrolle an ein neues
Domain_Parser Objekt übergeben.
Bei einem AVAILSTRING Element wird der Inhalt von dem Element
an setString() von diesem Server_Parser Objekt geleitet.
setString() erweitert das $data Attribut mit einem "string" Wert und
setzt die Methode für ein CDATA wieder zurück auf element_cdata().
1 class Server_Parser extends Parser {
2
3 var $data = array(), $domains = array();
4
5 function init($attr) {
6 $this->data['server'] = $attr['HOST'];
7 }
8
9 function element_start($parser, $name, $attr) {
10 switch ($name) {
11
12 case 'AVAILSTRING':
13 xml_set_character_data_handler($parser,
14 $this->getCallBack('setString'));
15 break;
16
17 case 'DOMAIN':
18 $obj = &new Domain_Parser($this, $attr, $this);
19 $obj->setHandler($parser);
20 break;
21
22 default: return;
23
24 }
25 }
26
27 function addDomain(&$domain) {
28 $this->domains[] = &$domain;
29 }
30
31 function setString($parser, $string) {
32 $this->data['string'] = $string;
33 xml_set_character_data_handler($parser, $this->getCallBack('element_cdata'));
34 }
35
36 function getName() {
37 return 'SERVER';
38 }
39
40 ...
41
42 }
Wenn nun das ganze SERVER Element abgearbeitet wurde,
kann es endlich in der final() Methode gespeichert werden.
Wir speichern allerdings nur Server, die einen Availstring und
Domains haben.
Wir machen mit dem $data Attribut ein DB_Element Objekt und speichern
dieses mit der save() Methode.
Dann gehen wir in einer Foreach-Schleife
alle DB_Elemente des $domains Attributes durch.
In dieser Schleife wird die Domain mittels der save() Methode gespeichert.
Aus den IDs des Server Objekts und des jeweiligen Domain Objekts wird
ein neues DB_Element Objekt erzeugt, welches in die tld_server Tabelle mit
der insert() Methode eingefügt wird.
Damit wird der Bezug zwischen Server und Domain gespeichert.
Falls keine Domain gespeichert werden konnte, löschen wir den Server wieder
mit der delete() Methode.
1 class Server_Parser extends Parser {
2
3 ...
4
5 function final() {
6 if (!isset($this->data['string']) || count($this->domains) == 0) {
7 return;
8 }
9
10 $db = &new DB_Element($this->data, 'server');
11 $db->save('server');
12 if (!$db->save('server')) {
13 return false;
14 }
15
16 $savedDomains = false;
17
18 foreach ($this->domains as $domain) {
19 if (!$domain->save('tld')) {
20 continue;
21 }
22
23 $ref = &new DB_Element(array('tld_id' => $domain->getID(),
24 'server_id' => $db->getID()), 'tld_server');
25
26 $ref->insert();
27
28 $savedDomains = true;
29 }
30
31 if (!$savedDomains) {
32 $db->delete();
33 }
34
35 }
36
37 }
Kommen wir zur letzten Klasse unseres Parsers.
Die Domain_Parser Klasse hat auch die Parser Klasse als Elternklasse.
Als Attribute kommt sie nur mit $server, dem SERVER Element, aus.
Der Konstruktor setzt das $server Attribut
und ruft in diesem Server_Parser Objekt die Methode addDomain()
mit einem neuen DB_Element Objekt auf. Dieses DB_Element Objekt speichert
den Namen der Domain.
Natürlich wird auch hier die getName()
mit DOMAIN als Ausgabe überschrieben.
element_start() muss sich nur um DOMAIN Elemente kümmern.
Dabei wird einfach die Kontrolle an ein weiteres Domain_Parser Objekt übergeben.
1 class Domain_Parser extends Parser {
2
3 var $server;
4
5 function Domain_Parser(&$parent, $attr, &$server) {
6 $this->C($parent, $attr);
7 $this->server = &$server;
8 $this->server->addDomain(new DB_Element(array('tld' => $attr['NAME']), 'tld'));
9 }
10
11 function element_start($parser, $name, $attr) {
12 if($name == 'DOMAIN') {
13 $obj = &new Domain_Parser($this, $attr, $this->server);
14 $obj->setHandler($parser);
15 }
16 }
17
18 function getName() {
19 return 'DOMAIN';
20 }
21
22 }
Um die vorhandenen Tabellen aufzufüllen, oder zu aktualisieren,
reicht es ein Main_Parser Objekt zu erzeugen.
Beim Erzeugen wird dem Main_Parser Objekt ein File_Objekt
im Konstruktor übergeben.
1 DB_Element::connect();
2 $file = &new File('serverlist.xml');
3 $parser = &new Main_Parser($file);
PHP Manual Referenz
mySQL Manual Referenz
|