Die Wiener Luftgüte, Parsing der Daten

IT | 16. April 2017

1 / 1
Startseite

Bevor die Messwerte auf der gewünschten Weise angezeigt und ausgewertet werden können, müssen sie aus der Webseite der MA22 extrahiert und strukturiert gespeichert werden. Dazu benutze ich eine Kombination aus Unix-Funktion, Perl-Skript, PHP-Webservice und MySQL-Datenbank.

Die Datenquelle

Die Daten der MA22 werden in einer - archaisch anmutenden - HTML-Tabelle veröffentlicht:

Netzwerkgeräte

Das Shell-Skript

Das verwendete Shell-Skript holt die HTML-Datei mit einem curl-Kommando, ersetzt mittels sed diverse HTML-Sonderzeichen und startet ein Perl-Skript zum Parsen der Daten. Dieses Perl-Skript übergibt die Daten an einen Webservice auf dem NAS der die Daten in eine Datenbank schreibt.

#!/bin/bash
curl -L -o /home/worker/lqVienna/tb-aktuell.htm https://www.wien.gv.at/ma22-lgb/tb/tb-aktuell.htm
sed -i 's/\ä/\ä/g;s/\ö/ö/g;s/\ü/ü/g;s/\ß/ß/g' /home/worker/lqVienna/tb-aktuell.htm
perl /home/worker/lqVienna/lqVienna.pl
rm /home/worker/lqVienna/tb-aktuell.htm

Daten extrahieren mit Perl

Perl habe ich zuletzt vor einem dutzend Jahren benutzt. Die Sprache ist beliebt und mächtig genug um eine eigene "Perl-Kultur" zu entwickeln. So widmen sich Entwickler z.B. unter anderem dem Thema "Perl-Haiku", also dem Verfassen "poetischer" Dreizeiler in Perl.

Mein Perl-Skript ist dagegen so "straight", so unpoetisch, so plump wie möglich. Es lädt die komplette HTML-Seite und arbeitet sie Zeile für Zeile ab. Werden Keywords (Straßen- oder Ortsnamen) gefunden, werden die zugehörigen Messdaten aus der Zeile extrahiert. So wird für jede Messstelle und jeden Messwert ein HTML-Aufruf für den Webservice erzeugt und abgesetz. Der Aufruf ist im Prinzip wie folgt aufgebaut:

http://lqvienna.bagend.local/admin/add.php?key="SECURITY_KEY"&cs=NO2&ts=1492373656&ix=1&v1=14&v2=15&zt=8:00&v3=19

Dabei ist

  • http://lqvienna.bagend.local/admin/add.php: Der aufgerufene Webservice
  • key="SECURITY_KEY": Ein Schlüssel der vom Webservice verifiziert werden muss
  • cs=NO2: Die Kennung für die Datenart (hier Stickstoffdioxid)
  • ts=1492373656: Der Unix-Timestamp der Verarbeitung (auf dem Raspberry)
  • ix=1: Der Index der Messstelle
  • v1=14: Ein Messwert (hier der akt. Messwert)
  • v2=15: Ein Messwert (hier der max. Messwert der letzten 24h)
  • zt=8:00: Die Uhrzeit, die die MA22 auf der Webseite vermerkt hat
  • v3=19: Ein Messwert (hier der Mittelwert der letzten 24h)
#!/usr/bin/perl

use utf8;
utf8::encode( $_ );
#use strict;
use warnings;
use DateTime;
use WWW::Mechanize;

# Decklaration der Subroutinen
sub ind;
sub NO2;
sub O3;
sub PM10;
sub PM25;
sub SO2;
sub CO;
sub trim($);

# UX-Zeit ermitteln
$timestamp = time();

# Schlüssel
$KEY = 'SEKURITYKEY';
# URL von add.php
$url = "http://lqvienna.bagend.local/admin/add.php?key=".$KEY."&cs=";

@Zeilen = ( "" );
open( MESSDAT, "/home/worker/lqVienna/data/tb-aktuell.htm" ) || 
	die "lqVienna: Datei mit Messdaten nicht gefunden\n";
while(  ) {    
    push( @Zeilen,$_ );
}
close( MESSDAT );

for( @Zeilen ) {
    if( /aktuelle / ) {
        $case   = "LQ";
        $mzeit  = substr( $_, 35, 2 );
        $mzeit  = $mzeit.":00";
        $awert  = substr( $_, 50, 1 );
    }
    if( /chste / ) {
        $case   = "LQ";
        $hwert  = substr( $_, 51, 1 );
        $values = $case."&ts=".$timestamp."&zt=".
									$mzeit."&v1=".$awert."&v2=".$hwert;
	    post2php( $values );
	}
    if( /Stephan/  ||
        /Tabor/    ||
        /AKH/      ||
        /Belgrad/  ||
        /Laaer/    ||
        /Kaiser/   ||
        /A23/      ||
        /Gaudenz/  ||
        /Hietzing/ ||
        /Kendler/  ||
        /Schaf/    ||
        /Hermann/  ||
        /Hohe/     ||
        /Gericht/  ||
        /Lobau/    ||
        /Stadlau/  ||
        /Liesing/ ) {
        # Index Messstelle zuweisen
        &ind();
        # NO2-Tabelle befüllen
        &NO2();
    }
    if( /Stephan/  ||
        /Laaer/    ||
        /Hermann/  ||
        /Hohe/     ||
        /Lobau/ ) {
        # O3-Tabelle befüllen
        &O3();
    }
    if( /Tabor/    ||
        /AKH/      ||
        /Belgrad/  ||
        /Laaer/    ||
        /Kaiser/   ||
        /A23/      ||
        /Gaudenz/  ||
        /Kendler/  ||
        /Schaf/    ||
        /Gericht/  ||
        /Lobau/    ||
        /Stadlau/  ||
        /Liesing/ ) {
        # PM10-Tabelle befüllen
        &PM10();
    }    
    if( /Tabor/    ||
        /AKH/      ||
        /A23/      ||
        /Kendler/  ||
        /Lobau/    ||
        /Stadlau/ ) {
        # PM25-Tabelle befüllen
        &PM25();
    }
    if( /Stephan/  ||
        /Kaiser/   ||
        /A23/      ||
        /Schaf/    ||
        /Hermann/  ||
        /Hohe/     ||
        /Stadlau/ ) {
        # SO2-Tabelle befüllen
        &SO2();
    }
    if( /Tabor/    ||
        /A23/      ||
        /Gaudenz/  ||
        /Hietzing/ ) {
        # CO-Tabelle befüllen
        &CO();
    }
}

# Funktion Messstelle Index zuweisen
sub ind {
    if( /Stephan/ )  { $MSindex = "0";  }
    if( /Tabor/ )    { $MSindex = "1";  }
    if( /AKH/  )     { $MSindex = "2";  }
    if( /Belgrad/ )  { $MSindex = "3";  }
    if( /Laaer/ )    { $MSindex = "4";  }
    if( /Kaiser/ )   { $MSindex = "5";  }
    if( /A23/  )     { $MSindex = "6";  }
    if( /Gaudenz/ )  { $MSindex = "7";  }
    if( /Hietzing/ ) { $MSindex = "8";  }
    if( /Kendler/ )  { $MSindex = "9";  }
    if( /Schaf/ )    { $MSindex = "10"; }
    if( /Hermann/ )  { $MSindex = "11"; }
    if( /Hohe/ )     { $MSindex = "12"; }
    if( /Gericht/ )  { $MSindex = "13"; }
    if( /Lobau/ )    { $MSindex = "14"; }
    if( /Stadlau/ )  { $MSindex = "15"; }
    if( /Liesing/ )  { $MSindex = "16"; }
}

# Funktion NO2
sub NO2 {
    $case    = "NO2";
    $HMWakt  = trim( substr( $_, 18, 4 ) );
    $HMWmax  = trim( substr( $_, 22, 4 ) );   
    $HMWzeit = trim( substr( $_, 26, 7 ) );
    $MW24    = trim( substr( $_, 33, 5 ) );
    if ( $HMWakt ne "---" ) {    
        $values  = $case."&ts=".$timestamp."&ix=".$MSindex.
										"&v1=".$HMWakt."&v2=".$HMWmax."&zt=".$HMWzeit.
										"&v3=".$MW24;
		post2php( $values );
	}
}

# Funktion O3
sub O3 {
    $case   = "O3";
    $MWakt  = trim( substr( $_, 40, 4 ) );
    $MWmax  = trim( substr( $_, 44, 4 ) );
    $MWzeit = trim( substr( $_, 48, 7 ) );
    $MW8akt = trim( substr( $_, 55, 5 ) );
    $MW8max = trim( substr( $_, 60, 4 ) );
    $Bewert = trim( substr( $_, 65, 1 ) );
    if ( $MWakt ne "---" ) {
        $values = $case."&ts=".$timestamp."&ix=".$MSindex.
									"&v1=".$MWakt."&v2=".$MWmax."&zt=".$MWzeit.
									"&v3=".$MW8akt."&v4=".$MW8max."&v5=".$Bewert;
		post2php( $values );
    }
}

# Funktion PM10
sub PM10 {
    $case   = "PM10";
    $MW24   = trim(substr( $_, 68, 5 ) );  
    $values = $case."&ts=".$timestamp.
							"&ix=".$MSindex."&v1=".$MW24;
    post2php( $values );    
}

# Funktion PM25
sub PM25 {
    $case   = "PM25";
    $MW24   = trim( substr( $_, 75, 5 ) );  
    $values = $case."&ts=".$timestamp.
							"&ix=".$MSindex."&v1=".$MW24;
    post2php( $values );        
}

# Funktion SO2
sub SO2 {
    $case   = "SO2";
    $HMWakt = trim( substr( $_, 82, 3 ) );
    $HMWmax = trim( substr( $_, 85, 4 ) );
    $MW24   = trim( substr( $_, 89, 5 ) );    
    $values = $case."&ts=".$timestamp."&ix=".$MSindex.
							"&v1=".$HMWakt."&v2=".$HMWmax."&v3=".$MW24;
    post2php( $values );	   
}

# Funktion CO
sub CO {
	$case   = "CO";
	$MW8akt = trim( substr( $_, 96, 4 ) );
	$MW8max = trim( substr( $_, 101, 4 ) );
	$values = $case."&ts=".$timestamp."&ix=".$MSindex.
						"&v1=".$MW8akt."&v2=".$MW8max;
	post2php( $values );	  
}

sub post2php {
    $ua  = WWW::Mechanize->new();
    $URL = $url.$values;
		# print "URL: $URL\n";
    $response = $ua->get( $URL );
    if ( $response->is_success ) {
      # $c = $ua->content;
      print "Daten erfolgreich importiert."
      # print ( "$c" );
    } else {
      print "Fehler beim öffnen der URL!";
			# die $response->status_line;
    }
}

# Funktion trim
sub trim($) {
	$string = shift;
	$string =~ s/^\s+//;
	$string =~ s/\s+$//;
	return $string;
}

Der Webservice

Das PHP-Skript add.php übernimmt die Daten per GET. Wurde der übergebene Securitykey erfolgreich verifiziert (ansonsten endet es kommentarlos) , wird aus dem Unix-Timestamp ein Datum im Format YYYY-MM-DD hh:mm:ss (MySQL Datetime) erzeugt und die Nutzdaten mittels Case-Entscheidung - basierend auf der übergebenen Datenart - in die passende Tabelle mit einem Prepared Statement eingefügt.

 $var) {
		if ($arg == 'key' AND $var == $SECURITYKEY) { $ValidKey=true; }
		if ($arg == 'cs') { $cs = $var; }
		if ($arg == 'ts') { $ts = $var; }
		if ($arg == 'ix') { $ix = $var; }
		if ($arg == 'zt') { $zt = $var; }
		if ($arg == 'v1') { $v1 = $var; }
		if ($arg == 'v2') { $v2 = $var; }
		if ($arg == 'v3') { $v3 = $var; }
		if ($arg == 'v4') { $v4 = $var; }
		if ($arg == 'v5') { $v5 = $var; }		
	}
	if (!$ValidKey) { echo 'Ungültiger Schlüssel!'; exit(); }
	if ( isset($cs) AND isset($ts) ) {
		// UX-Zeit in Datetime
		$dt = gmdate("Y-m-d H:i:s", $ts);
		// Eingehende Daten verarbeiten
		switch ( $cs ) {
			case 'LQ': 
				$sql = 'INSERT INTO tbl_LQ ( LQ_datetime, 
								LQ_zeit, LQ_aktwert, LQ_maxwert ) 
								VALUES ( ?, ?, ?, ? )';
				$stmt = $mysqli->prepare( $sql );        
				$stmt->bind_param( 'ssii', $dt, $zt, $v1, $v2 );       
				break;
			case 'NO2':	        
				$sql = 'INSERT INTO tbl_NO2 ( NO2_datetime, 
								NO2_index, NO2_zeit, NO2_HMWakt, 
								NO2_HMWmax, NO2_MW24 ) 
								VALUES ( ?, ?, ?, ?, ?, ? )';
				$stmt = $mysqli->prepare( $sql );        
				$stmt->bind_param( 'sisiii', $dt, $ix, $zt, $v1, $v2, $v3 );
				break;
			case 'O3':
				$sql = 'INSERT INTO 
								tbl_O3 ( O3_datetime, 
								O3_index, O3_1MWakt, O3_1MWmax, O3_MWzeit, 
								O3_MW8akt, O3_MW8max, O3_bewertung) 
								VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )';
				$stmt = $mysqli->prepare( $sql );        
				$stmt->bind_param( 'siiisiii', 
				$dt, $ix, $v1, $v2, $zt, $v3, $v4, $v5 );
				break;
			case 'PM10':
				$sql = 'INSERT INTO tbl_PM10 ( PM10_datetime, 
								PM10_index, PM10_wert ) 
								VALUES ( ?, ?, ? )';
				$stmt = $mysqli->prepare( $sql );        
				$stmt->bind_param( 'sii', $dt, $ix, $v1 );
				break;
			case 'PM25':
				$sql = 'INSERT INTO tbl_PM25 ( PM25_datetime, 
								PM25_index, PM25_wert ) 
								VALUES ( ?, ?, ? )';
				$stmt = $mysqli->prepare( $sql );        
				$stmt->bind_param( 'sii', $dt, $ix, $v1 );
				break;
			case 'SO2':
				$sql = 'INSERT INTO tbl_SO2 ( SO2_datetime, 
								SO2_index, SO2_HMWakt, SO2_HMWmax, SO2_MW24 ) 
								VALUES ( ?, ?, ?, ?, ? )';
				$stmt = $mysqli->prepare( $sql );        
				$stmt->bind_param( 'siiii', $dt, $ix, $v1, $v2, $v3 );
				break;
			case 'CO':
				$sql = 'INSERT INTO tbl_CO ( CO_datetime, 
								CO_index, CO_MW8akt, CO_MW8max ) 
								VALUES ( ?, ?, ?, ? )';
				$stmt = $mysqli->prepare( $sql );        
				$stmt->bind_param( 'sidd', $dt, $ix, $v1, $v2 );
				break;
		}                                
		$stmt->execute(); 
		$mysqli->close();        	               
	}
}
?>

Die Parameter für Auswahl und Anmeldung an der Datenbank erfolgt über die Include-Dateien db_connect.inc und db_config.inc

db_connect.inc

 set_charset("utf8");	
	// Monatsnamen in Deutsch ausgeben
	$mysqli -> query("SET lc_time_names = 'de_DE'");		
?>

db_config.inc



Tags


Mehr zum Thema