Wprawdzie Sejm RP szybkimi krokami zmierza w stronę wybranego w 1936 r. Reichstagu określanego jako ,,najlepiej opłacany męski chór ma świecie'' (The best paid male chorus in the world -- określenie użyte podobno w amerykańskim czasopiśmie The Literary Digest) i być może już w kadencji ósmej ten stan ideału zostanie osiągnięty. Ale zanim to nastąpi zbierzmy dane dotyczące posłów wybranych do Sejmu 7 kadencji.
Zaczynam od ściągnięcia listy stron bibliograficznych posłów:
## Najpierw zestawienie posłów: wget -U XX http://www.sejm.gov.pl/sejm7.nsf/poslowie.xsp?type=A -O lista-poslow.html ## Potem dla każdego posła z zestawiena: cat lista-poslow.html | perl -e ' undef $/; $_ = <>; s/\n/ /g; while ($_ =~ m@href="/sejm7.nsf/posel.xsp\?id=([0-9]+)\&type=A@g ) { $id = $1; ## id posła $pos = "http://www.sejm.gov.pl//sejm7.nsf/posel.xsp?id=$id&type=A"; print STDERR $pos, "\n"; system ('wget', '-U', 'XXX', "-O", "7_$id.html", $pos); sleep 3; ## --let's be a little polite-- }
Powyższe pobiera 460 stron, każda zawierająca biografię jakiegoś posła.
Z kolei poniższy skrypt wydłubuje co trzeba (data scraping) ze strony zawierających biografię pojedynczego posła (np. tego).
#!/usr/bin/perl # use Storable; use Google::GeoCoder::Smart; $geo = Google::GeoCoder::Smart->new(); # Domyślny kraj my $GeoCodeCacheName = 'geocode.cache'; my $NewCoordinatesFetched=0; # global flag my $kraj = 'Polska'; my $SLEEP_TIME = 3 ; # Rok wyborów my $baseyr = 2011; my ($data_day, $data_mc, $data_yr); undef $/; $_ = <>; s/\n/ /g; # Retrieve geocode cash (if exists) # http://curiousprogrammer.wordpress.com/2011/05/11/faking-image-based-programming-in-perl/ my %hash = %{ retrieve("$GeoCodeCacheName") } if ( -f "$GeoCodeCacheName" ) ; if (m@<h2>([^<>]+)</h2>@) { $posel = recode($1); } # zgadnij płeć patrząc na imię (jeżeli kończy się na a->kobieta): my @tmp_posel = split " ", $posel; if ( $tmp_posel[0] =~ m/a$/ ) { $sex='K' } else { $sex='M' } # id posła: if (m@http://www.sejm.gov.pl/sejm7.nsf/posel.xsp\?id=([0-9]+)@) {$id = $1; } # liczba oddanych na posła głosów: if (m@<p class="left">Liczba g\łos\ów:</p>[ \n\t]*<p class="right">([^<>]+)</p>@) { $glosy = $1 ; } # Data i miejsce urodzenia:</p><p class="right">29-06-1960<span>, </span>Gdańsk</p> if (m@Data i miejsce urodzenia:[ \n\t]*</p>[ \n\t]*<p class="right">([0-9\-]+)<span>,[ \n\t]*\ [ \n\t]*</span>([^<>]+)</p>@ ) { $data = recode ($1); ($data_day, $data_mc, $data_yr) = split "-", $data; ##print STDERR "R/M/D: $data_day, $data_mc, $data_yr\n"; $mce = recode ($2); } # Zawód: if (m@<p class="left">Zaw\ód:</p>[ \nt]*<p class="right">([^<>]+)</p>@ ) { $zawod = recode($1) ; } if (m@klub.xsp\?klub=([^"<>]+)@ ) { $klub = recode($1); } # Klub poselski: if (m@Okr\ęg wyborczy:</p>[ \t\n]*<p class="right">([^<>]+)</p>@) { $okr = recode($1); ## Pierwszy wyraz to numer okręgu, reszta nazwa (może być wielowyrazowa) ($okrnr, $okrnz) = $okr =~ m/([^ \n\t]+)[ \n\t]+(.+)/; ## -- sprawdzić czy działa -- } # Poprawienie błędów if ($mce =~ /Ostrowiec Świetokrzyski/) {$mce = 'Ostrowiec Świętokrzyski' } elsif ($mce =~ /Stargard Szaczeciński/) {$mce = 'Stargard Szczeciński' ; } elsif ($mce =~ /Białograd/ ) {$mce = 'Białogard'; } elsif ($mce=~ /Szwajcaria/) {$mce = 'Szwajcaria,Suwałki' } elsif ($mce=~ /Kocierz Rydzwałdzki/) { $mce= "Kocierz Rychwałdzki" } elsif ($mce=~ /Stąporów/) { $mce= "Stąporków" } elsif ($mce=~ /Szmotuły/) { $mce= "Szamotuły" } # Wyjątki: if ($posel=~ /Arkady Fiedler/ ) {$kraj = 'Wielka Brytania' } elsif ($posel=~ /Vincent-Rostowski/) {$kraj = 'Wielka Brytania' } elsif ($mce=~ /Umuahia/) {$kraj = 'Nigeria' } elsif ($posel=~ /Munyama/) {$kraj ='Zambia'; } ## Imię kończy się na `A' ale to facet: elsif ($posel=~ /Kosma Złotowski/ ) {$sex = "M"} # Sprawdź czy data jest zawsze w formacie dd-mm-yyyy: if ($data_yr < 1900) { print STDERR "*** $data_yr: $data for $posel ($id)\n"; } # geokodowanie (uwaga na limit) my $coords = addr2coords($mce); my $wiek = $baseyr - $data_yr; my $coords_okr = addr2coords($okrnz, 'r'); ($tmp_lat, $tmp_lng) = split " ", $coords; my $kml_line = "<LineString><coordinates>$tmp_lng,$tmp_lat $coords_okr</coordinates></LineString>"; print STDERR "$id : $sex : $posel : $wiek\n"; print "$id;$sex;$posel;$data;$wiek;$mce,$kraj;$coords;$klub;$glosy;$zawod;$okrnr;$okrnz;$kml_line\n"; ## Retrieve geocode cash if ($NewCoordinatesFetched) { store(\%hash, "$GeoCodeCacheName"); } ## ## ## ## ## ## sub recode { my $s = shift; $s =~ s/\ / /g; $s =~ s/\ł/ł/g; $s =~ s/\ó/ó/g; $s =~ s/\ń/ń/g; $s =~ s/\Ł/Ł/g; $s =~ s/\ź/ź/g; $s =~ s/\ż/ż/g; $s =~ s/\Ś/Ś/g; $s =~ s/\Ż/Ż/g; $s =~ s/\ę/ę/g; $s =~ s/\ą/ą/g; $s =~ s/\ś/ś/g; $s =~ s/\ć/ć/g; $s =~ s/[ \t\n]+/ /g; return $s; } ## ## ## ## ## ## sub addr2coords { my $a = shift ; my $r = shift || 'n'; my ($lat, $lng) ; ##consult cache first if (exists $GeoCodeCache{"$a"} ) { ($lat,$lng) = split (" ", $GeoCodeCache{"$a"} ); } else { my ($resultnum, $error, @results, $returncontent) = $geo->geocode("address" => "$a"); $resultnum--; $resultNo=$resultnum ; if (resultNo > 0) { print STDERR "** Location $a occured more than once! **" } if ($error eq 'OK') { $NewCoordinatesFetched=1; for $num(0 .. $resultnum) { $lat = $results[$num]{geometry}{location}{lat}; $lng = $results[$num]{geometry}{location}{lng}; ##print "*** LAT/LNG:$lat $lng ERROR: $error RES: $resultNo ***\n"; } } else { print STDERR "** Location $a not found! due to $error **" } } $GeoCodeCache{"$a"} = "$lat $lng"; ## store in cache sleep $SLEEP_TIME; if ($r eq 'r' ) { return "$lng,$lat"; } # w formacie KML else { return "$lat $lng"; } }
Skrypt wydłubuje id posła (id), imię i nazwisko (imnz), datę urodzenia (data_ur),
miejsce urodzenia (mce_ur), skrót nazwy partii do której należy (partia),
liczbę oddanych na niego głosów (log), zawód (zawod),
numer okręgu wyborczego (nrokregu) i nazwę okręgu (nzokregu). Do tego
zgadywana jest płeć posłanki/posła oraz obliczany jest wiek w chwili wyboru jako $baseyr - $data_yr
, gdzie
$data_yr
to rok urodzenia.
Miejsce urodzenia oraz nazwa okręgu są geokodowane. Współrzędne miejsca urodzenia są zapisywane jako zmienna wsp. Jako zmienna odleglosc zapisywana jest linia definiowana (w formacie KML) jako: współrzędne miejsce urodzenia--współrzędne okręgu wyborczego.
<LineString><coordinates>lng1,lat1 lng2,lat2</coordinates></LineString>
Zamiana kupy śmiecia (tj. wszystkich 460 plików HTML) na plik CSV zawierający wyżej opisane dane sprowadza się teraz do wykonania:
for i in 7_*html ; do perl extract.pl $i >> lista_poslow_7_kadencji.csv ; done
Jako ciekawostkę można zauważyć, że strony sejmowe zawierają 6 oczywistych błędów: Ostrowiec Świetokrzyski, Stargard Szaczeciński, Białograd, Kocierz Rydzwałdzki, Stąporów oraz Szmotuły. Ponadto wieś Szwajcaria k. Suwałk wymagała doprecyzowania. Czterech posłów urodziło się za granicą a pan Kosma jest mężczyzną i prosta reguła: jeżeli pierwsze imię kończy się na ,,a'' to poseł jest kobietą zawiodła.
Ostatecznie wynik konwersji wygląda jakoś tak (cały plik jest tutaj):
id;plec;imnz;dat_ur;wiek;mce_ur;wsp;partia;log;zawod;nrokregu;nzokregu;odleglosc 001;M;Adam Abramowicz;10-03-1961;50;Biała Podlaska,Polska;52.0324265 23.1164689;PiS;\ 12708;przedsiębiorca;7;Chełm;<LineString><coordinates>23.1164689,52.0324265 23.4711986,51.1431232</coordinates></LineString> ...
Teraz importuję plik jako arkusz kalkulacyjny do GoogleDocs, a następnie, na podstawie tego arkusza tworzę dokument typu FusionTable. Po wizualizacji na mapie widać parę problemów -- ktoś się urodził niespodziewanie w USA a ktoś inny za Moskwą. Na szczęście takich omyłek jest tylko kilka...
Geocoder nie poradził sobie z miejscowościami: Model/prem. Pawlak, Jarosław/posłowie: Kulesza/Golba/Kasprzak, Orla/Eugeniusz Czykwin, Koło/Roman Kotliński, Lipnica/J. Borkowski, Tarnów/A. Grad. We wszystkich przypadkach odnajdywane były miejsca poza Polską -- przykładowo Orly/k Paryża zamiast Orli. Ponadto pani poseł Józefa Hrynkiewicz została prawidłowo odnaleziona w Daniuszewie ale się okazało, że ta miejscowość leży na Białorusi a nie w Polsce. Też ręcznie poprawiłem...
Ewidentnie moduł Google::GeoCoder::Smart
ustawia
geocoder w trybie partial match, w którym to
trybie Google
intelligently handles incomplete input aka stara się być mądrzejszym od pytającego.
Może w trybie full match byłoby lepiej, ale to by wymagało doczytania.
Na dziś, po prostu poprawiłem ręcznie te kilka ewidentnie błędnych przypadków.
Po poprawkach wszystko -- przynajmniej na pierwszy rzut -- oka wygląda OK
Link do tabeli jest zaś tutaj.
Nawiasem mówiąc kodowanie
dokumentów na stronach www.sejm.gov.pl
jest co najmniej dziwaczne.
Klasyczny zbiór danych pn. niemieckie łodzie podwodne z 2WWŚ wg. typu, liczby patroli, zatopionych statków i okrętów oraz -- dodane dziś -- miejsca zatopienia (źródło: www.uboat.net). W zbiorze są 1152 okręty (pomięto 14, które były eksploatowane przez Kriegsmarine ale nie były konstrukcjami niemieckimi), z tego 778 ma określoną pozycję, na której zostały zatopione (z czego kilkadziesiąt w ramach operacji DeadLight). Można zatem powiedzieć, że dane dotyczące miejsca zatopienia/zniszczenia są w miarę kompletne, bo np. Kemp (1997) podaje liczbę 784 U-bootów zniszczonych w czasie 2WWŚ (dane są różne w zależności od źródła.)
Jeżeli U-boot zaginął/przepadł bez wieści/został zniszczony, ale nie wiadomo dokładnie gdzie, to nie występuje na mapie. Operacja DeadLight to ta wielka kupa kropek ,,nad'' Irlandią....
Link do danych/mapy na serwerze Google jest tutaj.
Dane były tylko zgrubie weryfikowane, więc mogą zawierać błędy (ale ,,na oko'' jest ich niewiele)...
Kemp Paul, U-Boats Destroyed: German Submarine Losses in World Wars, US Naval Institute Press, 1997 (isbn: 1557508593)
Pod tym adresem umieściłem moje ślady GPS (głównie rowerowe) z lat 2010--2012. W formacie KML bo to jedyny -- z tego co mi się wydaje -- obsługiwany przez Google Fusion Tables (GFT).
Moja procedura skopiowania danych z urządzenia GPS (zwykle jest to Garmin Legend) do GFT jest następująca:
Skryptem obsługującym program
gpsbabel
pobieram dane z urządzenia GPS do pliku w formacie GPX.
Skryptem kml2kml.sh
zamieniam plik GPX otrzymany w pierwszym kroku
na plik w formacie KML. Ponieważ plik ten jest ogromy (przykładowo z 60 kilowego pliku GPX
gpsbabel wygenerował plik o wielkości ponad 500 kb) zostaje on za pomocą
trywialnego arkusza XSLT zamieniony na znacznie mniejszą wersję uproszczoną:
#!/bin/bash # KMLSTYLE=~/share/xml/kml2kml.xsl # # Domyslna liczba punktow na sladzie COUNT=99 ## Domyslna nazwa Placemarka NAME=`date +%Y%m%d_%H`; while test $# -gt 0; do case "$1" in -name) shift; NAME="$1";; -name*) NAME="`echo :$1 | sed 's/^:-name//'`";; -max) shift; COUNT="$1";; -max*) COUNT="`echo :$1 | sed 's/^:-max//'`";; *) FILE="$1";; esac shift done ## Usun rozszerzenie z nazwy pliku wejsciowego OUT_FILE="${FILE%.*}" TMP_FILE=/tmp/${OUT_FILE}.kml.tmp echo "Converting $FILE to ${TMP_FILE}..." if [ -f $FILE ] ; then ## Konwersja GPX->KML gpsbabel -i gpx -f $FILE -x simplify,count=$COUNT -o kml -F $TMP_FILE else echo "*** ERROR *** File $FILE not found.... ***" ; exit fi ## Konwersja do uproszczonego KMLa (KML->KML) echo "Converting $TMP_FILE to ${OUT_FILE}.kml with maximum $COUNT points..." echo "KML' Placemark name is: $NAME..." xsltproc --stringparam FileName "$NAME" -o ${OUT_FILE}.kml $KMLSTYLE $TMP_FILE echo "Done..."
Zawartość arkusza XSLT zamieszczono na końcu tej notki.
Do interakcji z Fusion Tables wykorzystam zmodyfikowany skrypt (nazwany ftquery.sh
)
znaleziony na stronach
code.google.com:
#!/bin/bash # # Copyright 2011 Google Inc. All Rights Reserved. # Author: arl@google.com (Anno Langen) # http://code.google.com/intl/pl/apis/fusiontables/docs/samples/curl.html # -- Modified by TP -- MY_QUERY=$* function ClientLogin() { password='?1234567890?' email='looseheadprop1@gmail.com' local service=$1 curl -s -d Email=$email -d Passwd=$password -d \ service=$service https://www.google.com/accounts/ClientLogin | tr ' ' \n | grep Auth= | sed -e 's/Auth=//' } function FusionTableQuery() { local sql=$1 curl -L -s -H "Authorization: GoogleLogin auth=$(ClientLogin fusiontables)" \ --data-urlencode sql="$sql" https://www.google.com/fusiontables/api/query } FusionTableQuery "$MY_QUERY"
Teraz tworzę nową tabelę (wyklikowując co trzeba w przeglądarce) MyTracks
o następującej strukturze:
## Korzystamy z skryptu ftquery.sh ./ftquery.sh SHOW TABLES table id,name 2590817,MyTracks ## Tabela MyTracks ma zatem id=2590817 ./ftquery.sh DESCRIBE 2590817 column id,name,type col4,Date,string col0,Start,string col1,Stop,string col2,Location,location col3,Description,string
Kolumny zawierają odpowiednio: datę (Date
), czas
pierwszego
wpisu na śladzie GPX (Start
), czas ostatniego wpisu na
śladzie GPX (Stop
), ślad (Location
) oraz opis (Description
).
Kolumny Tabeli w Google Fusin Tables mogą być albo napisami (STRING
),
liczbami (NUMBER
), zawierać dane przestrzenne (LOCATION
) albo czas (DATETIME
).
Wstępne eksperymenty
z typem DATETIME
wskazują,
że jest z nim jakiś problem.
Primo
format czasu jest dość dziwaczny (np. gpsbabelowy zapis: 2012-01-07T09:19:47Z
nie jest rozpoznawany).
Także późniejsze filtrowanie w oparciu
o kolumnę DATETIME
też jakoś nie wychodzi, nawet jak zapisałem
datę w formacie YYY.MM.DD
,
który wg. dokumentacji
powinien być rozpoznawany.
Nie badając sprawy dogłębnie, zmieniłem po prostu
typ kolumn Date
, Start
, Stop
na STRING
.
Cytując za dokumentacją GFT: In a column of type LOCATION, the value can be a string containing an address, city name, country name, or latitude/longitude pair. The string can also use KML code to specify a point, line, or polygon... (cf. SQL API: Reference ). Zatem żeby wstawić linię śladu wystarczy przesłać napis zawierający współrzędne opakowane w następujący sposób:
<lineString><coordinates>lng,lat[,alt] lng,lat[,alt]...<coordinates></lineString>
Do tego służy następujący skrypt:
#!/bin/bash # STYLE=~/share/xml/gpxtimestamp.xsl MYTRACKS_TABLE_ID="2590817" GPX_FILE=$1 KML_FILE="${GPX_FILE%.*}".kml ## Ustal czas od--do sladu STARTTIME=`xsltproc --param Position '"First"' $STYLE $GPX_FILE` STOPTIME=`xsltproc --param Position '"Last"' $STYLE $GPX_FILE` DATE=`xsltproc --param Position '"First"' --param Mode '"Date"' $STYLE $GPX_FILE` if [ ! -f $GPX_FILE ] ; then echo "*** Error *** No GPX file: $GPX_FILE" ; exit ; fi if [ ! -f $KML_FILE ] ; then echo "*** Error *** No KML file: $KML_FILE" ; exit ; fi ## ## Wycinamy nastepujacym skryptem z uproszczonego pliku KML: TRACK=`perl -e 'undef $/; $t = <>; $t =~ m/(<coordinates>.*<\/coordinates>)/s; \ $t = $1; $t =~ s/[ \t\n]+/ /gm; print $t;' $KML_FILE` TRACK="<LineString>$TRACK</LineString>" ftquery.sh "INSERT INTO $MYTRACKS_TABLE_ID (Date, Start, Stop, Location, Description) VALUES ('$DATE', '$STARTTIME', '$STOPTIME', '$TRACK', '') "
I działa, tj. przesyła co trzeba na moje konto Google.
Google się chwali, że GFT radzi sobie z ogromnymi zbiorami danych. Mój ma -- na tem chwilem -- ponad 150 wierszy, a w każdym 99 punktów na śladzie, co daje łącznie approx 15,000 punktów do wykreślenia. Firefox/Opera coś tam wyświetla ale niewiele widać. Chrome radzi sobie najlepiej -- faktycznie da się to obejrzeć, przesuwać, powiększać... Wprawdzie nic z tych danych nie wynika, ale to już nie jest pytanie do Google.
Także filtr w połączeniu z mapą działa w Firefoksie i Operze tak sobie.
Wybieram zbiór tras,
np. za pomocą warunku Date < 20100901
(na tem chwilem jest to 7 wierszy)
następnie klikam Visualise Map i guzik -- nic nie widać.
Ale znowu w Chrome jest znacznie lepiej.
Dowód w postaci zrzutu ekranu obok.
Także wklejony poniżej link do mapy (pobrany
przez kliknięcie w guzik Get embeddable link) daje pozytywny wynik:
Przynajmniej w mojej wersji FF (8.0).
Zamiana pliku KML na uproszczony plik KML:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:kml="http://www.opengis.net/kml/2.2" > <xsl:output method="xml"/> <xsl:param name='FileName' select="'??????'"/> <xsl:template match="/"> <kml xmlns="http://www.opengis.net/kml/2.2" xmlns:gx="http://www.google.com/kml/ext/2.2"> <Document> <Placemark> <name><xsl:value-of select="$FileName"/></name> <MultiGeometry><LineString> <tessellate>1</tessellate> <coordinates> <xsl:for-each select="//kml:Placemark//kml:LineString"> <xsl:value-of select="kml:coordinates"/> </xsl:for-each> </coordinates></LineString></MultiGeometry></Placemark> </Document></kml> </xsl:template> </xsl:stylesheet>
Wydrukowanie daty/czasu z pliku w formacie GPX:
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:gpx="http://www.topografix.com/GPX/1/0"> <xsl:output method="text"/> <xsl:param name='Position' select="'First'"/> <xsl:param name='Mode' select="'Time'"/> <xsl:template match="/" > <xsl:for-each select='//*[local-name()="trkpt"]'> <xsl:choose> <xsl:when test="position()=1 and $Position='First' and $Mode='Time'"> <xsl:value-of select='substring(*[local-name()="time"], 12, 8)'/> <xsl:text> </xsl:text> </xsl:when> <xsl:when test="position()=1 and $Position='First' and $Mode='Date'"> <xsl:value-of select='translate(substring(*[local-name()="time"], 1, 10), "-", "")'/> <xsl:text> </xsl:text> </xsl:when> <xsl:when test="position()=last() and $Position='Last'"> <xsl:value-of select='substring(*[local-name()="time"], 12, 8)'/> <xsl:text> </xsl:text> </xsl:when> </xsl:choose> </xsl:for-each> </xsl:template> </xsl:stylesheet>