poniedziałek, 28 grudnia 2015

Numerologia stosowana (lies, damned lies and statistics)

,,Po dziennik Fakt (Ringier Axel Springer Polska) w okresie od maja do października 2015 roku sięgało 10,33%. Drugie miejsce zajęła Gazeta Wyborcza (Agora), której poziom czytelnictwa wzrósł w ciągu roku o 0,39 pkt. proc. do 8,58%.'' (za Gazeta Wyborcza dogania...).

Na samym końcu tekstu: ,,Podane liczby to wskaźniki CCS -- Czytelnictwo Cyklu Sezonowego''.

Ki ch#j ten CCS? Bo liczby wydają się mooocno napompowane. Okazuje się, że CCS oznacza jaki procent w populacji stanowią osoby, które zetknęły się z tym tytułem choćby raz w okresie cyklu sezonowego... (za Wskaźniki PBC). Jeszcze żeby było wiadomo co oznacza ,,zetknęły się''? Zawinęły karpia w sklepie albo pocięły jako ersatz Toilettenpapier?

Skądinąd wiadomo (z innej strony na tym samym serwerze), że sprzedaż ww. jest licha (por. Spadła sprzedaż kioskowa wszystkich dziennikóww). Proste przemożenie 30136052 (Polacy w wieku 15--75 lat) x 10,33% i 8,58% odpowiednio daje w wyniku 3,1 i 2,6 mln czytelników przy 300 i 180 tys sprzedanych egz. co oznacza, że 1 egz. Faktu czyta 10 osób, a GW nawet 15... Kto chce niech wierzy.

Ja bym zamiast pytać ,,czy się zetknął'' pytał się o treść 1 dowolnego tekstu z gazety ,,z którą się miało zetknięcie''... Ciekawe ile by wtedy wyszło...

Dane z artykułów w formacie CSV są poniżej:

tytul;CCS2015;CCS2014;platne2015;druk2015;platne2014;druk2014;cz2015;cz2014;razem2015u;razem2014u;druk2015u;druk2014u
Fakt;10.33;11.74;308616;305178;326748;323002;3113054;3537973;1008.7;1082.8;1020.1;1095.3
Gazeta Wyborcza;8.58;8.19;177447;106794;191127;113292;2585673;2468143;1457.2;1291.4;2421.2;2178.6
SuperExpress;3.58;4.55;145647;143954;153387;151735;1078871;1371190;740.7;893.9;749.5;903.7
Przegląd Sportowy;3.57;2.90;31484;30518;35546;34500;1075857;873946;3417.2;2458.6;3525.3;2533.2
Dziennik Gazeta Prawna;1.68;1.59;54089;9887;58027;11412;506286;479163;936.0;825.8;5120.7;4198.8
Rzeczpospolita;1.62;1.94;56560;7897;58056;9352;488204;584639;863.2;1007.0;6182.1;6251.5
PulsBiznesu;0.31;0.20;13314;1655;13996;1743;93422;60272;701.7;430.6;5644.8;3458.0
Parkiet Gazeta Giełdy;0.09;0.08;5332;1319;5015;1532;27122;24109;508.7;480.7;2056.3;1573.7

Gdzie: CCS2015 -- wskaźniki CCS dla 2015 r.; platne2015 -- rozpowszechnianie płatne razem w 2015; druk2015 -- sprzedaż egz. wydań drukowanych; cz2015 = CCS2015 * 30136052; razem2015u = platne2015 / druk2015 * 100%; druk2015u = druk2015 / druk2015 * 100%.
Dla roku 2014 analogicznie.

Ponadto:

Sprzedaż ogółem to suma sprzedaży egzemplarzowej wydań drukowanych, sprzedaży egzemplarzowej e-wydań oraz wszystkich form prenumeraty wydań drukowanych i prenumeraty e-wydań.

Rozpowszechnianie płatne razem to suma sprzedaży egzemplarzowej wydań drukowanych, sprzedaży egzemplarzowej e-wydań, prenumeraty wydań drukowanych i prenumeraty e-wydań oraz innych płatnych formy rozpowszechniania wydań drukowanych i innej płatnej dystrybucji e-wydań.

poniedziałek, 30 listopada 2015

Pobieranie twitów za pomocą Perla i API Twittera

Poniższy skrypt Perlowy służy do pobierania najnowszych twitów (Tweets) użytkowników identyfikowanych poprzez ich screen_name. Twity są dopisywane do bazy, która jednocześnie pełni rolę pliku konfiguracyjnego. Przykładowo, aby twity użytkownika maly_wacek były dodane do bazy należy wpisać do niej wpis (w dowolnym miejscu, dla porządku najlepiej na początku):

INIT;maly_wacek;;INIT

Ściśle rzecz biorąc po pierwszym dodaniu do bazy, powyższy wpis jest już niepotrzebny, ale też nie przeszkadza. Baza jest zapisywana w taki sposób, że najnowszy tweet każdego użytkownika jest na końcu, zatem po przeczytaniu pliku, w wyniku przypisania $Users{$tmp[1]} = $tmp[0] (por. poniżej), hash %Users zawiera wszystkich użytkowników oraz id_str ich ostatnio pobranego twita. Zapewne niespecjalnie optymalny sposób archiwizacji, ale prosty i działa:

#!/usr/bin/perl
use Net::Twitter;

# Z UTF8 w Perlu jest zawsze problem:
use open ":encoding(utf8)";
use open IN => ":encoding(utf8)", OUT => ":utf8";

my $timelineBase = "timelines.log";

if ( -f "$timelineBase" ) {

   open (BASE,  $timelineBase) ||
      die "Cannot open: $timelineBase";

   while (<BASE>) { chomp();
      @tmp = split /;/, $_;
      $Users{$tmp[1]} = $tmp[0]; # last id_str
   }
}

close (BASE) ;

## ###  ####

open (BASE,  ">>$timelineBase") ;

my $nt = Net::Twitter->new(legacy => 0);

my $nt = Net::Twitter->new(
   traits   => [qw/API::RESTv1_1/],
   consumer_key        => "######",
   consumer_secret     => "######",
   access_token        => "######",
   access_token_secret => "######", );

foreach $user ( keys %Users ) {
   my @message ; my $screen_name = $user ;
   my $result ;

   if ( $Users{$user} eq 'INIT' ) {
     ## max ile się da, wg dokumentacji 3200
     $result = $nt->user_timeline({
       screen_name => $screen_name, count=> '3200' })
   }
   else {
     $result = $nt->user_timeline({
       screen_name => $screen_name, 
         since_id => $Users{$user}, });
   }

   foreach my $tweet ( @{$result} ) {
      $text_ = $tweet->{text} ;
      $text_ =~ s/;/\,/g; $text_ =~  s/\n/ /g;
      $date_ = $tweet->{created_at} ;
      push ( @message, $tweet->{id_str} .  ";" \
         . "$screen_name;$date_;$text_" );
   }

   ## Drukuj posortowane:
   my $tweetsC;
   foreach my $tweet ( sort (@message) ) {
      $tweetsC++ ; print BASE $tweet . "\n"; }
   if ( $tweetsC > 0 ) {
       print STDERR "fetched $tweetsC for $screen_name\n"; }
}

close (BASE)

Uwaga: poprzez API można pobrać twity użytkowników, którzy zablokowali nam możliwość oglądania ich konta (inna sprawa po co oglądać takiego palanta).

Utworzenie aplikacji na apps.twitter.com

Należy się zalogować na stronie apps.twitter.com/. Kliknąć Create New App.

Wybrać Name (np. tprzechlewski.app), Description, Website i Callback URL.

Wybrać Keys and Access Tokens i pobrać wartości: Consumer Key oraz Consumer Secret.

Przewinąć zawartość strony i wybrać Create my access token. Zostaną wygenerowane Access Token oraz Access Token Secret, które także należy pobrać.

Na potrzeby wyżej opisanego skryptu to wystarczy. Pobrane wartości wstawiamy w miejsca oznaczone jako ######

Instalowanie Net::Twitter

Na jednym z moich komputerów ciągle działa dość archaiczna wersja Debiana Lenny:

$ cat /proc/version
Linux version 2.6.32-5-kirkwood (Debian 2.6.32-30)

$ cat /etc/issue
Debian GNU/Linux 5.0 \n \l

$ perl --version
This is perl, v5.10.0 built for arm-linux-gnueabi-thread-multi
Copyright 1987-2007, Larry Wall

Z poważnym obawami, że się uda spróbowałem:

cpan> install Net::Twitter
Strange distribution name

Pomaga (por. tutaj):

cpan> install IO::AIO 

Potem:

cpan> install YAML
cpan> install Net::Twitter

Ściąga się milion pakietów. Przy testowaniu Net-HTTP-6.09 system zawisł na etapie t/http-nb.t (pomogło Ctr-C), ale finał był pomyślny, tj. Net::Twitter został zaistalowany.

Mój inny system jest już nowszy a instalacja Net::Twitter bezproblemowa:

$ cat /etc/issue
Fedora release 21 (Twenty One)
  
$ perl --version
This is perl 5, version 18, subversion 4 (v5.18.4) built for x86_64-linux-thread-multi
(with 25 registered patches, see perl -V for more detail)
Copyright 1987-2013, Larry Wall

$ yum install perl-Net-Twitter

Automatyzacja

Teraz wystarczy umieścić w crontab na przykład taki wpis:

# 48 min po północy codziennie
48 0 * * * /home/tomek/bin/twitter.sh 

Co zawiera twitter.sh jest oczywiste

Protokoły wyborcze do pobrania

Jak ktoś jest zainteresowany, to pobrane ze strony PKW protokoły wyborcze, są dostępne tutaj. Na razie są protokoły z wyborów parlamentarnych 2015 r. oraz (słynnych) wyborów samorządowych 2014 r. -- odpowiednio 224 i 553 Mb (po spakowaniu).

sobota, 21 listopada 2015

Rozkład komisji obwodowych według liczby oddanych głosów

Rozkład komisji obwodowych według liczby oddanych głosów (na podstawie szczegółowych wyników wyborów do Sejmu RP, pobranych ze strony PKW -- por. Web scrapping protokołów wyborczych ze strony PKW):

komisje <- read.csv("komisje_glosy_razem.csv", sep = ';',  header=T, na.string="NA");
str(komisje);

hist(komisje$glosy, breaks=seq(0, 3200, by=25), col="orange",
     freq=TRUE,main="Komisje wg liczby oddanych głosów",
     xlab="# głosów",ylab="# komisji (N = 27859)" )

mtext(text="https://github.com/hrpunio/Data/tree/master/sejm", 4, cex=0.7)
text(3200,100, "Me = 495\nQ1 = 265\nQ3 = 782...", 2, cex=0.7,  adj=c(0,0));

fivenum(komisje$glosy);

quantile(komisje$glosy, c(.10));
quantile(komisje$glosy, c(.05));
quantile(komisje$glosy, c(.90));

czwartek, 12 listopada 2015

Wiek posłów ósmej kadencji Sejmu RP

Na stronie www.sejm.gov.pl już dziś pojawiły się strony o nowowybranych posłach 8 kadencji. Strony można ściągnąć na przykład takim oto prostym skryptem basha:

#!/bin/bash
# Przykładowy URL: http://www.sejm.gov.pl/Sejm8.nsf/posel.xsp?id=002&type=A
padtowidth=3
for ((i=1;i<=460;i++)) ; do
  ## parametr id w URLu ma wartość 001--460
  ## za pomocą printf/tricku z padtowidth dodajemy wiodące zera:
  POSEL=`printf "%0*d\n" $padtowidth $i`
  wget 'http://www.sejm.gov.pl/Sejm8.nsf/posel.xsp?id='$POSEL'&type=A'\
     -O $POSEL.html
done

Na stronach na razie jest niewiele informacji, ale jest data urodzenia, liczba zdobytych głosów oraz okręg wyborczy z którego poseł został wybrany. Za pomocą prostych skryptów Perla można wydłubać te dane, dodać informacje o wieku/płci i zapisać w pliku CSV:

imnz;rokur;wiek;klub;miejsce;okreg;glosy;plec
Adam Abramowicz;1961-03-10;54;PiS;NA;7 Chełm;10500;M
Andrzej Adamczyk;1959-01-04;56;PiS;NA;13 Kraków;18514;M
...

Jak wygląda struktura wiekowa w poszczególnych klubach? (na poniższym wydruku symbole x.1, x.2, x.3, x.4 oraz x.5, to odpowiednio: wartość minimalna, pierwszy kwartyl, mediana, trzeci kwartyl oraz wartość maksymalna)

p <- read.csv("Sejm_8_u.csv", sep = ';',  header=T, na.string="NA");
boxplot (wiek ~ klub, p, xlab="Klub", ylab="Wiek", col='yellow')

aggregate (p$wiek, list(Klub = p$klub), fivenum)
aggregate (p$wiek, list(Klub = p$klub), na.rm=TRUE, mean)

A jak wyglądała średnia wieku w poszczególnych kadencjach Sejmu?

p <- read.csv("Sejm1-8.csv", sep = ';',  header=T, na.string="NA");
boxplot (wiek ~ kadencja, p, xlab = "Kadencja", ylab = "Wiek", col='yellow')

aggregate (p$wiek, list(Kadencja = p$kadencja), fivenum)

 Kadencja  x.1  x.2  x.3  x.4  x.5
1     1991 22.0 37.0 43.0 49.0 70.0
2     1993 24.0 39.0 45.0 50.0 74.0
3     1997 23.0 40.5 46.0 51.0 72.0
4     2001 26.0 43.0 49.0 54.0 78.0
5     2005 23.0 41.0 47.0 53.0 67.0
6     2007 22.0 41.0 48.0 54.0 78.0
7     2011 22.0 42.0 50.0 56.0 73.0
8     2015 23.0 41.5 51.0 59.0 77.0

aggregate (p$wiek, list(Kadencja = p$kadencja), na.rm=TRUE, mean)

  Kadencja        x
1     1991 43.19438
2     1993 45.21535
3     1997 46.42500
4     2001 48.28221
5     2005 46.55230
6     2007 47.32948
7     2011 48.86739
8     2015 49.74783

Dane pobrane ze strony http://www.sejm.gov.pl/Sejm8.nsf/poslowie.xsp?type=A są dostępne tutaj.

piątek, 6 listopada 2015

Web scrapping protokołów wyborczych ze strony PKW

Ze strony PKW ściągnąłem szczegółowe wyniki wyborów do Sejmu RP. Szczegółowe w tym sensie, że pobrałem protokoły ze wszystkich 27859 komisji obwodowych. Takie protokoły są dostępne pod adresem:

http://parlament2015.pkw.gov.pl/321_protokol_komisji_obwodowej/IdKomisji

Identyfikatory obwodowych komisji da się pobrać ze strony PKW metodą ,,kolejnych przybliżeń'': najpierw okręgi, potem powiaty, potem gminy a na końcu w każdej gminie lista komisji obwodowych. Ponieważ otrzymałem tyle komisji obwodowych ile podaje PKW (por. tutaj), to zakładam że niczego nie pogubiłem.

Sprawdzenie danych zaczynamy od podsumowanie liczby głosów ważnych, które zostały oddane na kandydatów z każdego komitetu:

Komitet L.kandydatów L.głosów L.głosów* Różnica Okr19 Okr19* Różnica
KORWIN 899 722921 722999 78 21757 21767 10
KUKIZ 839 1338610 1339094 484 26546 26573 27
Kongres N. Prawicy 116 4852 4852 0 x x x
Razem 571 550343 550349 6 9469 9475 6
Samoobrona 119 4266 4266 0 x x x
BRAUN 202 13113 13113 0 x x x
JOW Bezpartyjni 138 15184 15656 472 x x x
Mniejszość Niemiecka 24 27530 27530 0 x x x
Obywatele do Parlamentu 40 1964 1964 0 266 266 0
Ruch Społeczny RP 59 3941 3941 0 186 186 0
STONOGA 299 42668 42731 63 x x x
Zjed. dla Śląska 42 18668 18668 0 x x x
PETRU 858 1155364 1155370 6 15942 15948 6
PiS 918 5711661 5711687 26 58317 58343 26
PO 914 3661455 3661474 19 32240 32259 19
PSL 916 779874 779875 1 796 797 1
ZLEW 905 1146837 1147102 265 7948 7956 8
* dane zagregowane ze strony PKW

Jak widać są rozbieżności (kolumny 3--5).

Po podliczeniu głosów w każdym obwodzie osobno okazuje się, że źródłem problemów jest m.in. okręg #19, w którym liczone są głosy za granicą (kolumna 6--8 w tabeli powyżej). W szczególności brak jest protokołu z komisji 97770 (baza Bagram/Afganistan, por. POLSKA-OKRĘGI-OKRĘG NR: 19-Zagranica-Zagranica) co być może wynika z konieczności zachowania tajemnicy wojskowej. Zakładając, że w Bagram PiS/PO/PSL/Petru/Razem dostały odpowiednio 26/19/1/6/6 głosów, to w przypadku 12 z 17 komitetów wynik się zgadza (problem stanowią KUKIZ, ZLEW, KORWIN, Bezpartyjni i Stonoga).

Drążąc temat wyliczyłem liczbę głosów dla komitetu JOW Bezpartyjni, który zarejestrował listy w 8 okręgach wyborczych:

Nr okręgu L.głosów L.głosów* Różnica
02 2068 2540 472
18 1045 1045 0
21 1772 1772 0
22 2289 2289 0
33 2344 2344 0
34 1426 1426 0
36 1973 1973 0
39 2267 2267 0
* dane zagregowane ze strony PKW

Zatem całe manko jest w okręgu #02. PKW podaje też stosowne dane w rozbiciu na powiaty (por. tutaj). Przykładowo dla powiatu ząbkowickiego (kod teryt 0224) JOW Bezpartyjni mieli otrzymać 237 głosów. W skład tego powiatu wchodzi 7 gmin, m.in. gmina Bardo (teryt 022401), w której to gminie JOW Bezpartyjni miał otrzymać 14 głosów (por. tutaj). Na terenie gminy Bardo działały 4 obwodowe komisje wyborcze (por. tutaj). W komisji #1 (Centrum Kultury i Promocji Bardo, ul. Kolejowa 12, 57-256 Bardo) na komitet JOW Bezpartyjni oddano 3 głosy. PKW udostępnia też szczegółowy protokół z tejże komisji (por. tutaj) -- wystarczy kliknąć w adres na stronie POLSKA-OKRĘGI-OKRĘG NR: 2-ząbkowicki-Bardo dla wyników komitetu JOW Bezpartyjni. No i na tym protokole każdy z kandydatów JOW Bezpartyjni ma w rubryce Liczba oddanych głosów 0 głosów.

Podsumowując powyższy dłuższy wywód: według protokołu JOW Bezpartyjni zdobył w tym obwodzie 0 głosów, ale według informacji zbiorczej na innej stronie 3 głosy.

Zatem się wyjaśniło że sumując informacje z protokołów komisji nie ma szans na otrzymanie poprawnego wyniku (ale to co mam jest wynikiem prawie dokładym--błąd jest niewielki). Pozostaje tajemnicą PKW dlaczego ich system działa tak pokracznie.

Dane pobrane ze strony http://parlament2015.pkw.gov.pl/ są dostępne tutaj. Pliki komisja_84873_protokol-0.png--komisja_84873_protokol-4.png to zrzuty ekranu ilustrujące ,,przypadek JOW Bezpartyjni w gminie Bardo'' opisany wyżej.

Wszelkie komentarze/uwagi/poprawki mile widziane:-)

Komisje z rekordowym poparciem wg komitetów

Komisje z rekordowym poparciem wg komitetów w wyborach do Sejmu (2015). Generalnie to są specyficzne komisje i/lub takie komisje, w których frekwencja była bardzo mała. Poniżej po jednym przykładzie dla 8 wybranych komitetów:

Komitet %głosów ważnych Ogółem*Adres Id komisji
KORWIN 50,0 14 Stowarzyszenie MONAR Ośrodek Leczenia... 102943
KUKIZ 82,86 35 DPS w Osinach 106282
KW Razem 40,00 5 NZO Dom Sue Ryder Bydgoszcz 86967
KWW Zbigniewa Stonogi 36,93 417 Świetlica Wiejska w Lubieszewie 102231
PETRU 50,00 2 Zespół Zakładów Opieki Zdrowotnej w Nowogrodźcu 83629
PO 100,00 1 Zakład Opieki Zdrowotnej Świnoujście 111436
PSL 100,00 1 Lokal Szpitala Pomocy Maltańskiej Oddział w Barczewie 107560
PiS 100,00 57 DPS w Kurozwękach 106804
ZLEW 61,29 31 DPS w Nakle/Notecią 86194
* głosów ogółem na wszystkich kandydatów.

Pełna lista dla 25 komisji z najwyższym poparcie dla każdego komitetu jest tutaj albo tutaj.

wtorek, 3 listopada 2015

Semper Fidelis

Poparcie dla trzech najpopularniejszych partii politycznych w komisjach zorganizowanych w zakładach karnych i aresztach śledczych (181 komisji). Procent głosów ważnych:

Województwo PO Kukiz PiS L. głosów
pomorskie 62.3 11.5 9.3 2175
dolnośląskie 55.6 13.2 10.9 2891
mazowieckie 55.5 13.1 9.5 3235
warmińsko-mazurskie 55.4 13.4 11.6 1550
lubuskie 54.4 12.5 10.7 894
śląskie 54.4 17.6 8.6 2869
zachodniopomorskie 54.2 13.2 10.9 1943
wielkopolskie 53.8 12.8 11.8 1790
kujawsko-pomorskie 51.4 13.8 11.3 1734
łódzkie 50.3 17.0 10.7 1522
opolskie 48.9 18.1 12.6 1492
małopolskie 47.2 18.2 14.0 1522
lubelskie 43.4 21.1 13.6 1364
podlaskie 45.9 19.3 12.3 826
świętokrzyskie 41.9 22.3 14.5 523
podkarpackie 40.1 18.8 15.6 959
POLSKA 52.5 15.3 11.2 27289

Na podstawie danych pobranych ze strony http://parlament2015.pkw.gov.pl/ metodą webscrappingu. Dane są dostępne tutaj.

wtorek, 29 września 2015

Weight of RWC players

Scrapping various Web pages I managed to gather data on players participating in last 4 Rugby World Cups. Is there a trend in body mass of rugby players participating in RWC tournaments?

Using Plotly API via Perl script (described here Box-plot chart with plot.ly and Perl API) I can quickly plot series of boxplots:

# cpan install WebService::Plotly in case  WebService::Plotly is not installed
plotly_boxplot.pl -col=5 -by=0 -title='RWC players by weight' -sep=';' rwc1999-2015.csv

Resulting boxplots and data can be viewed here.

piątek, 31 lipca 2015

There is a cold summer this year in Sopot

Temperature in Sopot in July 2015
Temp in Sopot/July 2015

The following CSV (on-demand generated from raw data with simple Perl script) file contains outdoor temperature registred every hour starting from 2010 (with DS18B20 sensor):

dayhr;No;y2010;y2011;y2012;y2013;y2014;y2015;day30
d070100;001;14.6;17.5;14.9;10.1;12.9;12.2;0
d070101;002;13.4;16.7;14.1;10.1;12.8;12.5;3600
d070102;003;12.8;16.3;14.3;10.2;12.7;12.1;7200

dayhr is a label and day30 denotes number of seconds from the beginning od the period (for the first observation day30 equals 0, for the second 3600 etc.) The chart was produced with the following custom R script:

require(ggplot2)
library(scales)
number_ticks <- function(n) {function(limits) pretty(limits, n)}

d <- read.csv("july-by-day.csv", sep = ';',  header=T, na.string="NA");

datestart <- ISOdate(2015, 7, 1, tz = "");
d30 <- datestart + d$day30;
d[,"d30"]  <- d30;

ggplot(d, aes(x = d30)) +
  geom_line(aes(y = y2015, colour = 'y2015'), size=.3) +
  geom_line(aes(y = y2014, colour = 'y2014'), size=.3) +
  geom_smooth(aes(y = y2015, colour = 'y2015'), size=1) +
  geom_smooth(aes(y = y2014, colour = "y2014"), size=1) +
  ylab(label="Temp [C]") +
  xlab(label="Observation") +
  scale_y_continuous(breaks=number_ticks(15)) +
  scale_x_datetime(breaks = date_breaks("5 days")) +
  theme(legend.title=element_blank()) +
  ggtitle("Temperature in July in Sopot") +
  theme(legend.position=c(.6, .9)) +
  theme(legend.text=element_text(size=12));

ggsave(file="Temp-M7-2015.pdf", width=15, height=8)

czwartek, 30 lipca 2015

Wizjoner czy prowizjoner?

O zmarłych dobrze albo wcale, więc ten tekst nie jest o niespodziewanie zmarłym ,,najbogatszym-Polaku'' dr. Kulczyku, tylko o mediach, masowo publikujących wspomnienia po nim...

W tychże wspomnieniach słowo wizjoner jest odmienianie przez wszystkie przypadki i zwykle poprzedzone przymiotnikiem wielki. Jak wizjoner, to musi być i wizja a tej próżno próżno szukać. Ktoś tam wymienia autostradę (cyt: Ta autostrada jest fajnym pomnikiem tego, co pozostawił dla nas. Był wizjonerem.) inny twierdzi że wizją był browar (kupiony od SkarbuPaństwa i sprzedany dawno temu).

Wielki wizjoner a tak niewiele zostawił?

Uploading pictures to Picasaweb with Perl/LWP::UserAgent

Previously described bash script allows for uploading a file to PicasaWeb only. With the following (simplified) Perl script one can upload pictures as well as upload with metadata (title description and tags) or create/list albums:

#!/usr/bin/perl

use strict;

use LWP::UserAgent;
use Getopt::Long;
use File::MimeInfo;
use XML::LibXML;

my $AlbumId ="6170354040646469009";
my $profileID="default";

my $Action = 'u'; ## x| u | c | l (default action is Upload)
my $entryTitle = ''; my $entryDescription = '';
my $entryKeywords = '';
my $ActionUpload =''; my $ActionList = '';
my $ActionCreate = ''; my $ActionXload = '';
my $ImageFile = '';
my $dummyReq='';

GetOptions("xload" => \$ActionXload, "upload" => \$ActionUpload,
  "list" => \$ActionList, "create" => \$ActionCreate,
  "title=s" => \$entryTitle, "description=s" => \$entryDescription,
  "keywords=s" => \$entryKeywords,
  "file=s" => \$ImageFile,
  "album=s" => \$AlbumId, ## UploadFile to Album
) ;

## Determine action:
if ( $ActionUpload ) {$Action = 'u'} elsif ( $ActionList ) { $Action = 'l'}
elsif ( $ActionCreate ) { $Action = 'c'}
elsif ( $ActionXload ) { $Action = 'x'}

OAuth 2.0 authorization is handled with Python script oauth2picasa.py. The script is an adapted/copy-pasted fragment of code borrowd from picasawebsync:

### Authenticate with external script (oauth2picasa.py):
my $ACCESS_TOKEN=`oauth2picasa.py`;
chomp($ACCESS_TOKEN);
print STDERR "*** AccessToken: $ACCESS_TOKEN [AlbumId: $AlbumId]\n";

my $req ; my $blog_entry ;

my $picasawebURL = "https://picasaweb.google.com/data/feed/api/user/$profileID";

if ( $Action eq 'c' ) {## Action: create album
  $req = HTTP::Request->new(POST => $picasawebURL );
  $req->header( 'Content-Type' => 'application/atom+xml' );

  $blog_entry = "<entry xmlns='http://www.w3.org/2005/Atom'
     xmlns:media='http://search.yahoo.com/mrss/'
     xmlns:gphoto='http://schemas.google.com/photos/2007'>"
  . "<title type='text'>$entryTitle</title>"
  . "<summary type='text'>$entryDescription</summary>"
  . "<media:group><media:keywords>$entryKeywords</media:keywords></media:group>"
  . "<category scheme='http://schemas.google.com/g/2005#kind'
     term='http://schemas.google.com/photos/2007#album'></category></entry>";

    $req->content($blog_entry);
} 
elsif ( $Action eq 'l' ) {## Action: list albums
  $req = HTTP::Request->new(GET => $picasawebURL );

} 
elsif ( $Action eq 'u' ) {## Action: Upload 1 photo w/o metadata
  my $mimeType = mimetype($ImageFile);

  ## https://developers.google.com/picasa-web/docs/2.0/developers_guide_protocol
  $req = HTTP::Request->new(POST => "$picasawebURL/albumid/$AlbumId" );
  $req->header( 'Content-Type' => "$mimeType" );
  $req->header( 'Slug' => "$ImageFile" );

  ## http://www.perlmonks.org/?node_id=131584
  open(FILE, $ImageFile);
  $req->content(join('',<FILE>));
  close(FILE);
}

To upload the binary image data along with its metadata, use MIME content type "multipart/related"; send photo metadata in one part of the POST body (Content-Type: application/atom+xml), and binary-encoded image data in another part. This is the preferred approach according to Picasa Web Albums Data API Picasa Web Albums Data API

elsif ( $Action eq 'x' ) {## Action: Upload 1 photo with metadata
  # https://groups.google.com/forum/#!topic/google-picasa-data-api/2qRfP0EIFhk
  my $mimeType = mimetype($ImageFile);

  $req = HTTP::Request->new(POST => "$picasawebURL/albumid/$AlbumId" );
  $req->header( 'Content-Type' => "multipart/related" );

  open(FILE, $ImageFile);
  my $add_photo_metadata = "<entry xmlns='http://www.w3.org/2005/Atom' 
     xmlns:media='http://search.yahoo.com/mrss/'>"
  . "<title type='text'>$entryTitle</title>"
  . "<summary type='text'>$entryDescription</summary>"
  . "<media:group><media:keywords>$entryKeywords</media:keywords></media:group>"
  . "<category scheme='http://schemas.google.com/g/2005#kind' 
     term='http://schemas.google.com/photos/2007#photo'></category></entry>";

  my $add_photo_data = join('',<FILE>); close(FILE);

  ## http://www.perlmonks.org/?node_id=131584
  $req->add_part(HTTP::Message->new(['Content-Type'
      => 'application/atom+xml'], $add_photo_metadata));
  $req->add_part(HTTP::Message->new(['Content-Type'
      => "$mimeType"], $add_photo_data));
}

$req->header( 'Authorization' => "Bearer $ACCESS_TOKEN" );
$req->header( 'GData-Version' => '2' );

## ### ###
my $res ;
my $ua = LWP::UserAgent->new;

$res = $ua->request($req);

if ($res->is_success) {
   my $decoded_response = $res->decoded_content;
   print "*** OK *** $decoded_response\n";
}

Usage:

Upload with metadata

picasaweb.pl -xload -title PICTURE-TITLE -descr DESCRIPTION \
  -keywords 'TAG1,TAG2' -file FILE.jpg -album ALBUMID

Upload w/o metadata:

picasaweb.pl -upload -file FILE.jpg -album 12345

Create album:

picasaweb.pl -create -title ALBUM-TITLE -descr DESCRIPTION \
   -keywords 'TAG1,TAG2'

List album:

picasaweb.pl -list

Source code: picasaweb.pl

wtorek, 28 lipca 2015

Spływ Krutynią

W zeszłą niedzielę przepłynąłem z Elką fragment Krutyni, konkretnie od rezerwatu koło wsi Krutyń do mostu we wsi Ukta (circa 15 km w czasie 3 godzin bez minuty). Opis trasy można znaleźć m.in. na stronach firmy Turystyka Aktywna Wodniak z której usług korzystaliśmy i możemy ją polecić jako solidną. Wypożyczenie kajaka kosztowało 55 PLN czyli tanio.

Miejscem startu było w naszym wypadku Jezioro Krutyńskie (rezerwat Krutynia), do którego zawiózł nas mikrobus Wodniaka. Bus nie może podjechać do samej wody (rezerwat!) więc kajak trzeba 50 m donieść samemu (albo dać zarobić 2 PLN miejscowym chłopakom, którzy przewiozą go wózkiem). Na trasie spływu jest jedna przenioska przy młynie wodnym w miejscowości Zielony Lasek, ale tam też są chłopcy z wózkami (koszt usługi tym razem 5 PLN).

Ślad całej wycieczki (ze zdjęciami) jest też tutaj.

poniedziałek, 13 lipca 2015

Głupi i głupszy: występ w Berlinie

Człowiek z obrazka (ten po prawej) znowy błysnął -- tym razem w Berlinie:

"Czyn Clausa von Stauffenberga, który targnął się na życie samego Hitlera, to ważne ogniwo europejskiego ruchu oporu -- podobnie jak... powstanie warszawskie" -- stwierdził Bronisław Komorowski na wykładzie wygłoszonym w ostatnim tygodniu podczas pożegnalnej podróży do Niemiec.

Jego występ był nie do strawienie nawet dla życzliwych mu mediów, jeszcze w maju agitujących za "najlepszym prezydentem 25 lecia". Newsweek odnosząc się do cytatu z akapitu wyżej: "Przesadził? Raczej tak..." Polityka udaje, że zdarzenia nie było -- no cóż nawet prof. Władyka miałby kłopoty z interpretacją historii wg. Bredzisława. Jedynie GłosCadyka, idąc w zaparte, broni swojego człowieka roku 2014: wszyscy w Niemczech byli brunatni, więc należy docenić, że ktoś był mniej brunatny niż ktoś inny (B.T. Wieliński w tekście: Prezydent Komorowski jedzie do Berlina. Prawica grzmi, że uczci Stauffenberga -- wroga Polski).

Patrona broni też głupszy:

Brak szacunku dla takiej osoby jest brakiem szacunku dla Polaków, którzy w walce z Hitlerem zginęli...

Wszyscy się okazuje walczyli z Hitlerem--jakimś cyborgiem, co sam jeden podbił pół Europy. Nałęcz udając głupiego, i zamiast Niemcy mówiąc Hitler, posługuje się prostacką propagandową kalką: całe zło to Hitler, a reszta systemu, w tym Wehrmacht -- no cóż niewiele mogli, a niektórzy bohatersko walczyli...

Całe szczęście, że Szef i jego dwór odchodzą w niebyt już za 24 dni.

środa, 8 lipca 2015

Konwersja Excela na CSV

Jak to z Microsoftem bywa nie jest łatwo. Są dwa formaty Excela -- stary (.xls) oraz nowy (.xlsx). Pakiety Perlowe Spreadsheet::Excel oraz Spreadsheet::ParseXLSX radzą sobie nieźle, aczkolwiek oczywiście gwarancji nie ma i być nie może skoro sam Excel czasami siebie samego nie potrafi zinterpretować.

No ale jest jeszcze trzeci format: jak plik .xlsx jest zabezpieczone hasłem (password protected). I na taką okoliczność nie ma zbyt wielu narzędzi. Można wszakże problem rozwiązać w dwóch krokach korzystając Libreoffice, który potrafi interpretować pliki Excela i można go uruchomić w trybie batch:

#!/bin/bash
XLS="$1"
TMP="${XLS%.*}.xlsx"
libreoffice --headless --convert-to xlsx "$XLS" --outdir ./xlsx-temp/
perl xslx2csv.pl ./xlsx-temp/"$TMP" "$OUTFILE"

Powyższy skrypt obsłuży wszystkie rodzaje plików Excela, zamieniając je najpierw na plik w formacie XLSX (plik password protected zostanie zmieniony na prawdziwy format XLSX, interpretowalny przez np. Spreadsheet::ParseXLSX).

Można od razu konwertować do CSV (--convert-to csv), ale konwersji będzie podlegać tylko pierwszy arkusz. Jak interesuje nas na przykład drugi, to kicha... nie da się (a przynajmniej ja nie wiem jak to osiągnąć). Inny problem to zamiana XLSX→XLSX -- nie ma w LibreOffice możliwości określenia nazwy pliku wynikowego, a próba:

libreoffice --headless --convert-to xlsx plik.xlsx

Kończy się błędem. Na szczęście jest obejście w postaci opcji --outdir. Plik wyjściowy -- o tej samej nazwie co wejściowy -- jest zapisywany w innym katalogu i problem rozwiązany.

Po zamianie Excela na ,,kanoniczny'' XLSX do konwersji na CSV można wykorzystać następujący skrypt Perla:

#!/usr/bin/perl
# Wykorzystanie perl xslx2csv.pl plik.xslx [numer-arkusza]

use Spreadsheet::ParseXLSX;
use open ":encoding(utf8)";
use open IN => ":encoding(utf8)", OUT => ":utf8";

$xslxfile = $ARGV[0]; 
$ArkuszNo = $ARGV[1] || 1; ## domyślnie arkuszu 1

my $source_excel = new Spreadsheet::ParseXLSX;
my $source_book = $source_excel->parse("$xslxfile")
  or die "Could not open source Excel file $xslxfile: $!";

# Zapisuje zawartość wybranego arkusza do hasza %csv
my %csv = ();

foreach my $sheet_number (0 .. $source_book->{SheetCount}-1) {
  my $sheet = $source_book->{Worksheet}[$sheet_number];

  print STDERR "*** SHEET:", $sheet->{Name}, "/", $sheet_number, "\n";
  if ( $ArkuszNo ==  $sheet_number + 1 ) {

    next unless defined $sheet->{MaxRow};
    next unless $sheet->{MinRow} <= $sheet->{MaxRow};
    next unless defined $sheet->{MaxCol};
    next unless $sheet->{MinCol} <= $sheet->{MaxCol};

    foreach my $row_index ($sheet->{MinRow} .. $sheet->{MaxRow}) {
       foreach my $col_index ($sheet->{MinCol} .. $sheet->{MaxCol}) {
          my $source_cell = $sheet->{Cells}[$row_index][$col_index];
	  if ($source_cell) {
	    $csv{$row_index}{$col_index} = $source_cell->Value;
	  }
       }
    }
  }
}

Arkusz jest w haszu %csv. Jak go przekształcić/wydrukować itp. pozostawiam inwencji ewentualnego czytelnika.

Wysyłanie posta na blogger.com z wykorzystaniem Google API

GoogleCL przestało działać, bo Google przestało obsługiwać wersję OAuth 1.0. Ponadto, wygląda na to, że dostosowanie tego użytecznego narzędzia do wersji OAuth 2.0 bynajmniej nie jest trywialne na co wskazują liczne (ale do tej pory bezskuteczne) prośby i wołania o aktualizację GoogleCL, które można znaleźć w Internecie.

Ponieważ poszukiwania w miarę podobnego zamiennika zakończyły się niepowodzeniem, nie pozostało nic innego zmajstrować coś samodzielnie. Autoryzację OAuth 2.0 mam już opanową -- obsługuje ją Pythonowy skrypt oauth2picasa.py. (Skrypt jest (zapożyczonym) fragmentem z projektu picasawebsync). Wystarczyło dorobić następujący prosty skrypt Perlowy (por. także: Publishing a blog post):

#!/usr/bin/perl
# *** Wyslanie posta na blogger.com ***
use strict;
use LWP::UserAgent;
use XML::LibXML;
use Getopt::Long;

my $profileID="default";
my $blogID = '1928418645181504144'; # Identyfikator bloga
my $blog_entry ;

## Na wypadek gdy ktoś ma kilka blogów moża podać na któr
## ma być wysłany post używając opcji -blog
GetOptions( "blog=s" => \$blogID, "post=s" => \$blog_entry) ;

if ( $blog_entry eq '' ) {
print STDERR "*** USAGE: $0 -b blog -p message (-b is optional) ***\n" }

## sprawdź czy post jest well formed:
my $parser = XML::LibXML->new();
eval {my $res_  = $parser->parse_string($blog_entry) };
if ($@) { die "*** Error parsing post message! \n"; }

my $ACCESS_TOKEN=`oauth2blogger.py`; # pobierz ACCESS_TOKEN
print STDERR "*** AccessToken: $ACCESS_TOKEN ***\n";

my $req = HTTP::Request->new(
  POST => "https://www.blogger.com/feeds/$blogID/posts/default");

$req->header( 'Content-Type' => 'application/atom+xml' );
$req->header( 'Authorization' => "Bearer $ACCESS_TOKEN" );
$req->header( 'GData-Version' => '2' );

$req->content($blog_entry);

my $ua = LWP::UserAgent->new;
my $res = $ua->request($req);

# Jeżeli coś jest nie tak poniższe drukuje verbatim:
# http://www.perlmonks.org/bare/?node_id=464442
# $ua->prepare_request($req); print($req->as_string); exit ;

if ($res->is_success) {
   my $decoded_response = $res->decoded_content;
   print STDERR "*** OK *** $decoded_response\n"; }
else { die $res->status_line; }

Wykorzystanie:

perl blogger_upload.pl -p 'treść-posta'

Treść posta musi być oczywiście w formacie xHTML i zawierać się wewnątrz elementu content, który z kolei jest wewnątrz elementu entry. Element entry zawiera także title określający tytuł posta, oraz elementy category zawierające tagi. Przykładem może być coś takiego:

<entry xmlns='http://www.w3.org/2005/Atom'>
 <title type='text'>Marriage!</title>
 <content type='xhtml'>
    <div xmlns="http://www.w3.org/1999/xhtml">
      <p>Mr. Darcy has proposed marriage to me!</p>
      <p>He is the last man on earth I would ever desire to marry.</p>
      <p>Whatever shall I do?</p>
    </div>
  </content>
  <category scheme="http://www.blogger.com/atom/ns#" term="marriage" />
  <category scheme="http://www.blogger.com/atom/ns#" term="Mr. Darcy" />
</entry>

Opisany skrypt jest tutaj: blogger_upload.pl.

wtorek, 23 czerwca 2015

Mój nowy PCet

Intel NUC/Akasa case
Intel NUC/Obudowa Akasa

Niedawno zdecydowałem się na upgrade i stanęło na Intel NUC, konkretnie model D54250WYKH wyposażony w procesor Core i5-4250U/Haswell (nieobciążony pobiera podobno 6 watów!). Jak wiadomo NUC sprzedawany jest jako tzw. kadłubek/barebone zatem trzeba samodzielnie dokupić i zamontować dysk oraz pamięć RAM. Intel coś tam rekomenduje i jest dostępna na stosownej stronie (URL tej strony można znaleźć w kartonie z NUCem) lista zweryfikowanego sprzętu, ale ja wybrałem coś czego na liście nie było: pamięć 2x Kingston SODIMM PC-1600 DDR3L 4GB CL11 KVR16LS11/4 oraz dysk SSD Samsung 850 EVO (500 Gb) z interfejsem SATA bo taki miałem (domyślnym jest mSATA, z tym że obudowa D54250WYKH jest na tyle duża, że pozwala na instalację dwóch dysków, a system może być uruchamiany z obu).

NUC ma w środku wiatrak i do tego ten wiatrak całkiem wyraźnie słychać -- być może jestem przewrażliwiony bo są też opinie w stylu: jest praktycznie niesłyszalny gdy pracuje. Usiłowałem zmniejszyć prędkość wiatraka poprzez stosowne ustawienia BIOSa (zmiana control mode na manual plus zmniejszenie duty cycle z fabrycznych 40% na 30%, por. Intel NUC DN2820FYKH Bay Trail System Review) i wtedy faktycznie komputer działał ciszej, ale temperatura procesora (pod obciążeniem) potrafiła wzrosnąć do 85 stopni.

Nie po to kupowałem dysk SSD żeby coś mi się tam kręciło -- za następne 300 PLN kupiłem obudowę Akasa Newton X fanless case. Obudowa jest większa od oryginalnej i znacząco cięższa (1,3 kg wg producenta), ale faktycznie pozwala na wyłączenie wiatraka (podczas przekładania płyty do nowej obudowy wiatrak został odkręcony zresztą). Temperatura procesora jaką odnotowałem nigdy nie była wyższa od 55 stopni a zwykle znajduje się w przedziale 35--45 stopni.

Z instalacją Fedory 21 -- bo takiej wersji Linuksa używam, nie było żadnych problemów (z dokładnością do błędów Fedory). Aby móc dalej korzystać z wiekowej drukarki HP6P kupiłem na Allegro za całe 9,90 PLN przejściówkę pn. konwerter kabel USB 2.0 wtyk--LPT gniazdo DB25 ponieważ NUC nie posiada gniazda LPT/IEEE 1284. Konwerter zadziałał od pierwszego strzału, co było miłym zaskoczeniem.

Drobny problem wystąpił z równie wiekowym skanerem Canon LIDE 25, który wieszał się po zeskanowaniu jednej strony. Się okazało, że powodem jest błąd w programie XSane/sane-backends w wersji 1.0.24. Rozwiązaniem było ściągnięcie z githuba i skompilowanie wersji 1.0.25.

Recenzji NUCa jest w Internecie multum, np. tutaj.

Wysyłanie plików na Picasaweb za pomocą Curla

Taki oto (uproszczony) skrypt basha używałem do niedawna do wysyłania plików na konto Picasaweb (por. Using cURL to interact with Google Data services oraz PicasaUploader):

USERNAME=SomeUsername # GoogleAccountUsername
PASSWORD=SomePasswd # GoogleAccountPasswd
ALBUM_ID=6008849823888405298 # ID of picasaweb album

MY_PIC="$1" ## filename of the picture to upload ##
PIC_TITLE=`basename $MY_PIC` # filename w/o extension
PIC_TYPE=`file -b --mime-type "$MY_PIC"`

AUTH_KEY=$( curl -s https://www.google.com/accounts/ClientLogin -d Email="$USERNAME"@gmail.com \
  -d Passwd="$PASSWORD" -d accountType=GOOGLE \
  -d source=Google-cURL-Example -d service=lh2 | awk -F\= '/^Auth=/{print $2}' )
ALBUM_XML="http://picasaweb.google.com/data/feed/api/user/$USER_ID/albumid/$ALBUM_ID?authkey=$AUTH_KEY"

URL=`curl -s --request POST --data-binary "@$MY_PIC" --header "Slug: $PIC_TITLE" \
 --header "Content-Type: $PIC_TYPE" \
 --header "Authorization: GoogleLogin auth=$AUTH_KEY" "$ALBUM_XML" | \
sed 's/.*media:content url='"'"'\([^'"'"']*\).*media:thumbnail url='"'"'\([^'"'"']*\).*/\1/'`

Zmienna URL zawiera url wysłanego na Picasaweb obrazka (otrzymany przez zaaplikowanie w potoku odpowiedniego skryptu seda).

Skrypt przestał działać ostatnio, ponieważ Google nie obsługuje już protokołu OAuth 1.0 (por. Google Identity Platform). Na szczęście w łatwy sposób możliwe było dopasowanie skryptu nowszego protokołu OAuth 2.0:

ACCESS_TOKEN=$(oauth2picasa.py) ## Acces token is managed with Python's script now
## Note that ALBUM_XML URL starts now from https:// now
ALBUM_XML="https://picasaweb.google.com/data/feed/api/user/$USER_ID/albumid/$ALBUM_ID"

URL=`curl -s --request POST --data-binary "@$MY_PIC" \
--header "GData-Version: 2" --header "Slug: $PIC_TITLE" \
--header "Content-Type: $PIC_TYPE" -H "Authorization: Bearer $ACCESS_TOKEN" $ALBUM_XML |  \
sed 's/.*media:content url='"'"'\([^'"'"']*\).*media:thumbnail url='"'"'\([^'"'"']*\).*/\1/'`

Uwaga: Autoryzację OAuth 2.0 obsługuje Pythonowy skrypt oauth2picasa.py. Skrypt jest (zapożyczonym) fragmentem z projektu picasawebsync:

#!/usr/bin/python

import os
import time
import httplib2
## https://github.com/google/oauth2client
## installed with pip install --upgrade oauth2client (or some other way)
from oauth2client import client

def oauthLogin():
        # using http://stackoverflow.com/questions/20248555/list-of-spreadsheets-gdata-oauth2/29157967#29157967
        from oauth2client.file import Storage

        filename = os.path.join(os.path.expanduser('~'), ".picasawebsync")
        client_secrets = os.path.join(os.path.expanduser('~'), ".config", "picasawebsync.json")

 storage = Storage(filename)
        credentials = storage.get()
        if credentials is None or credentials.invalid:  
         flow = client.flow_from_clientsecrets(client_secrets,
         scope='https://picasaweb.google.com/data/',
         redirect_uri='urn:ietf:wg:oauth:2.0:oob')     
                auth_uri = flow.step1_get_authorize_url()       
                print 'Authorization URL: %s' % auth_uri
                auth_code = raw_input('Enter the auth code: ')
                credentials = flow.step2_exchange(auth_code)
                storage.put(credentials)
        if credentials.access_token_expired:
                credentials.refresh(httplib2.Http())
                
        return credentials.access_token

# start of the program

gd_client = oauthLogin()

print '%s' % gd_client

Jak stworzyć OAuth 2.0 client ID opisano tutaj. Należy wybrać Client ID for native application. Następnie należy pobrać plik JSON, zmienić nazwę tego pliku na picasawebsync.json umieszczając go w katalogu .config swojego katalogu domowego (albo zmodyfikować skrypt, jeżeli plik ma być w innym miejscu).

Uruchomiony po raz pierwszy skrypt oauth2picasa.py wypisuje URL, który należy skopiować/wkleić do przeglądarki, celem uzyskania authcode. Pobrany authcode wklejamy w odpowiedzi na prompt Enter the auth code:.

Opisane skrypty są tutaj: picasa_upld.sh oraz oauth2picasa.py.

poniedziałek, 22 czerwca 2015

Uploading pictures to Picasaweb with Curl

My (simplified) old bash script for uploading images to Picasaweb (cf. Using cURL to interact with Google Data services and PicasaUploader):

USERNAME=SomeUsername # GoogleAccountUsername
PASSWORD=SomePasswd # GoogleAccountPasswd

MY_PIC="$1" ## filename of the picture to upload ##
ALBUM_ID=6008849823888405298 # ID of picasaweb album
PIC_TITLE=`basename $MY_PIC` # filename w/o extension
PIC_TYPE=`file -b --mime-type "$MY_PIC"`

AUTH_KEY=$( curl -s https://www.google.com/accounts/ClientLogin -d Email="$USERNAME"@gmail.com \
  -d Passwd="$PASSWORD" -d accountType=GOOGLE \
  -d source=Google-cURL-Example -d service=lh2 | awk -F\= '/^Auth=/{print $2}' )
ALBUM_XML="http://picasaweb.google.com/data/feed/api/user/$USER_ID/albumid/$ALBUM_ID?authkey=$AUTH_KEY"

URL=`curl -s --request POST --data-binary "@$MY_PIC" --header "Slug: $PIC_TITLE" \
 --header "Content-Type: $PIC_TYPE" \
 --header "Authorization: GoogleLogin auth=$AUTH_KEY" "$ALBUM_XML" | \
sed 's/.*media:content url='"'"'\([^'"'"']*\).*media:thumbnail url='"'"'\([^'"'"']*\).*/\1/'`

URL contains url of uploaded picture (obtained with piping curl's output via sed).

This script stoped working recently as Google no longer supports OAuth 1.0 (cf. Google Identity Platform). Fortunately it was pretty easy to modify it to support OAuth 2.0:

ACCESS_TOKEN=$(oauth2picasa.py) ## Acces token is managed with Python's script now
## Note that ALBUM_XML URL starts now from https:// now
ALBUM_XML="https://picasaweb.google.com/data/feed/api/user/$USER_ID/albumid/$ALBUM_ID"

URL=`curl -s --request POST --data-binary "@$MY_PIC" \
--header "GData-Version: 2" --header "Slug: $PIC_TITLE" \
--header "Content-Type: $PIC_TYPE" -H "Authorization: Bearer $ACCESS_TOKEN" $ALBUM_XML |  \
sed 's/.*media:content url='"'"'\([^'"'"']*\).*media:thumbnail url='"'"'\([^'"'"']*\).*/\1/'`

NOTE: OAuth 2.0 authorization is handled with tiny Python script oauth2picasa.py. The script is an adapted/copy-pasted fragment of code borrowd from picasawebsync:

#!/usr/bin/python

import os
import time
import httplib2
## https://github.com/google/oauth2client
## installed with pip install --upgrade oauth2client (or some other way)
from oauth2client import client

def oauthLogin():
        # using http://stackoverflow.com/questions/20248555/list-of-spreadsheets-gdata-oauth2/29157967#29157967
        from oauth2client.file import Storage

        filename = os.path.join(os.path.expanduser('~'), ".picasawebsync")
        client_secrets = os.path.join(os.path.expanduser('~'), ".config", "picasawebsync.json")

 storage = Storage(filename)
        credentials = storage.get()
        if credentials is None or credentials.invalid:  
         flow = client.flow_from_clientsecrets(client_secrets,
         scope='https://picasaweb.google.com/data/',
         redirect_uri='urn:ietf:wg:oauth:2.0:oob')     
                auth_uri = flow.step1_get_authorize_url()       
                print 'Authorization URL: %s' % auth_uri
                auth_code = raw_input('Enter the auth code: ')
                credentials = flow.step2_exchange(auth_code)
                storage.put(credentials)
        if credentials.access_token_expired:
                credentials.refresh(httplib2.Http())
                
        return credentials.access_token

# start of the program

gd_client = oauthLogin()

print '%s' % gd_client

How to create an OAuth 2.0 client ID in the Google Developers Console is described here. Choose Client ID for native application. Next download JSON file, rename it to picasawebsync.json and move it to .config directory in your HOME directory (or modify the script).

First time used oauth2picasa.py prompts to sign on to google and paste the link back to authenticate.

Source code: picasa_upld.sh and oauth2picasa.py.

środa, 27 maja 2015

My old Canon LIDE 25 scanner works only once

My old Canon CanoScan LIDE 25 scanner is not working properly with my new hardware (Intel NUC) and Fedora 21.

I suspected Fedora 21 bug, but the problem is with the hardware, exactly as describe here. Works once and got stuck when scanning next.

It is claimed problem is solved in version 1.025 of Sane. To compile Sane one has to install libusb-devel package first:

yum install libusb-devel

Then:

git clone git://git.debian.org/sane/sane-backends.git
cd sane-backends

# I would like to install whole stuff in /usr/local
# without removing original fedora package
./configure --prefix=/usr/local --sysconfdir=/usr/local/etc \
    --localstatedir=/var BACKENDS=plustek
make
make install

Sane works now.

I would like to avoid mixing original Sane files with the new ones, so I use --prefix and --sysconfdir options with appropriate values. New Sane will be installed in /usr/local and it will be run by default--no need to remove original Fedora packages.

wtorek, 26 maja 2015

Aksjomat Balcerowicza: im większe wpływy związków zawodowych, tym mniej miejsc pracy


TU density vs GDP

TU density vs emp. rate

TU density vs unemp. rate

Kontunuując minianalizę rozpoczętą w poprzednim wpisie, a dotyczącą zależności pomiędzy zatrudnieniem a uzwiązkowieniem (w związku ze śmiałą tezą L. Balcerowicza, że taka zależność istnieje i jest ujemna):

require(ggplot2)

## https://stats.oecd.org/Index.aspx?DataSetCode=UN_DEN
## http://stats.oecd.org/Index.aspx?DatasetCode=STLABOUR
## employment rate Q42012
d <- read.csv("union_density_and_gdp.csv", sep = ';',  header=T, na.string="NA");

## tu.density = ratio of  wage and salary earners
## that are trade union members, divided by the total number of wage and salary earners:
## gdppc = GDP per capita
ggplot(d, aes(d$tu.density, d$gdppc)) + geom_point() +
  geom_text(aes(label=d$iso),size=2.0, vjust=-0.35)  +
  xlab("TU density (%)") + ylab("GDPpc (tys USD)") +
  scale_colour_discrete(name="") +
  geom_smooth(method="lm", se=T, size=2)

lm <- lm(data=d, gdppc ~ tu.density ); summary(lm);

## employment rate vs tu.density:
ggplot(d, aes(d$tu.density,d$emprate)) + geom_point() +
  geom_text(aes(label=d$iso),size=2.0, vjust=-0.35)  +
  xlab("TU density (%)") + ylab("Empolyment rate (%)") +
  scale_colour_discrete(name="") +
  geom_smooth(method="lm", se=T, size=2);

lm <- lm(data=d, emprate ~ tu.density ); summary(lm);

## youth unemployment rate vs tu.density:
## http://www.oecd-ilibrary.org/employment/youth-unemployment-rate_20752342-table2
ggplot(d, aes(d$tu.density,d$yur)) + geom_point() +
  geom_text(aes(label=d$iso),size=2.0, vjust=-0.35)  +
  xlab("TU density (%)") + ylab("Youth unempolyment rate (%)") +
  scale_colour_discrete(name="") +
  geom_smooth(method="lm", se=T, size=2);

lm <- lm(data=d, yur ~ tu.density ); summary(lm)

Prosta regresja daje następujące rezultaty: zależność #1 pomiędzy GDP per capita a Trade Union Density jest słabo dodatnia (to już wiemy); zależność #2 pomiędzy współczynnikiem zatrudnienia a Trade Union Density też jest słabo dodatnia; zależność #3 pomiędzy stopą bezrobocia w grupie wiekowej 15--24 lat a Trade Union Density jest wprawdzie ujemna, ale statystycznie nieistotna (współczynnik $R^2$ do tego równy 1,4%).

Jak to wygląda graficznie widać na wykresach obok.

Zbiór danych jest do pobrania tutaj.

BTW: do konwersji pliku PDF na JPG wykorzystano:

convert -density 150 Rplots.pdf Rplots_%02d.png

Uwaga na koniec: zapis method="lm" jest bardziej poprawny niż method=lm zastosowany w poprzednim wpisie.

niedziela, 17 maja 2015

Im większe wpływy związków zawodowych, tym mniej miejsc pracy

Im większe wpływy związków zawodowych, tym mniej miejsc pracy
TU density vs GDP (OECD countries)
TU density vs GDP

Pan profesor Balcerowicz na finiszu kampanii prezydenckiej baaardzo mocno się zaangażował po stronie rządzącego układu, a to zaangażowanie przejawia się m.in. wzmożonym wypisaniem na Twitterze różnych mniej lub bardziej mądrych (zwykle mniej) sloganów (aka farmazonów). Np. "S" już poparła Dudę, który zabiega o poparcie OPZZ -- to zła wiadomość dla młodych. Im większe wpływy ZZ w państwie, tym mniej miejsc pracy..

Co szkodzi sprawdzić empirycznie tezę profesora? Pobrałem zatem ze strony stats.oecd.org dane dotyczące Trade Union Density (ratio of wage and salary earners that are trade union members, divided by the total number of wage and salary earners tj. udział fundusza płac związkowców do płac ogółem). A ze strony List of OECD countries by GDP per capita dane dotyczące GDP per capita (jakoś nie mogłem szybko odszukać tych liczb na stronie stats.oecd.org -- zakładam, że na wikipedia.org przepisano je bez błędów:-) Dane są z roku 2012.

require(ggplot2)

## https://stats.oecd.org/Index.aspx?DataSetCode=UN_DEN
d <- read.csv("union_density_and_gdp.csv", sep = ';',  header=T, na.string="NA");

ggplot(d, aes(d$tu.density,d$gdppc)) + geom_point() +
  geom_text(aes(label=d$iso),size=2.0, vjust=-0.35) +
  xlab("TU density (%)") + ylab("GDPpc (tys USD)") +
  scale_colour_discrete(name="") +
  geom_smooth(method=lm,se=T, size=2)

lm <- lm(data=d, gdppc ~ tu.density ); summary(lm)

Jak widać na wykresie Polska jest piąta od końca wśród krajów OECD pod względem GDP na głowę i szósta od końca jeżeli chodzi o wielkość uzwiązkowienia. Czepianie się związków w tej sytuacji (12,5% uzwiązkowienia w PL, podczas gdy przykładowo w Niemczech jest to 41.9%, a w Danii 67.2%) ma wszystkie znamiona obsesji podobnej przykładowo do popularnego wśród Palikotowców i innych antyklerykałów poglądu, iż jakoby Kościół Katolicki jest praprzyczyną wszelkiego zła (przynajmniej w PL).

Dodatkowo prosta regresja daje następujący rezultat: GDP = 0,25 tu.density + 30,5435, czyli 1% wzrost uzwiązkowienia daje 0,25 tys wzrostu GDP na głowę (dokładnie odwrotnie niż twierdzi Balcerowicz). Współczynnik przy zmiennej tu.density jest nawet istotny statystycznie (na poziomie 0,05) ale $R^2$ jest faktycznie bardzo marne -- 13%.

Zbiór danych jest do pobrania tutaj.

czwartek, 29 stycznia 2015

Faryzeuszowskie żale na Czerskiej

Głos Cadyka donosi: Austriacka gazeta wzywa Polskę, by przyznała się do udziału w Holocauście. I dalej: Czytam dzisiejsze wydanie austriackiego dziennika "Heute" i zastanawiam się, czy dziennikarstwo może upaść jeszcze niżej [...] Otóż red. Erich Nuler, który wybrał się na wtorkowe uroczystości 70. rocznicy wyzwolenia KL Auschwitz, napisał w korespondencji, że Bronisław Komorowski -- w przeciwieństwie do prezydenta i kanclerza Austrii, którzy mówili o udziale Austriaków w Holocauście -- o polskiej winie nie zająknął się nawet słowem. Jaka jest ta przemilczana polska odpowiedzialność za Holocaust?

Coż takiego oburza faryzeuszy z Czerskiej? To przecież ich postulaty. Kto jak nie GC rozpętał histerię pn. polska wina, promował czystych hochsztalerów w tym temacie typu ,,historyk'' Gross i jego beletrystyka oraz piał z zachwytu nad przeprosinami Kwaśniewskiego za zbrodnie niemieckiego Wehrmachtu (de jure -- za prawo i porządek na okupowanym obszarze odpowiada armia je zajmująca).

Inna sprawa, że faktycznie szczytem groteski jest uczenie Polaków przyzwoitości przez potomków Hitlera, Kaltenbrunnera, Globocnika czy Eberla (ten ostatni z Treblinki).

Urodzony w Wiedniu prof. Raul Hilberg (cf http://en.wikipedia.org/wiki/Raul_Hilberg) w swojej autobiografii The politics of memory: The journey of a Holocaust historian zauważa, że Austria [w roku 1992] nie jest najlepszym rynkiem dla moich książek a potem ironicznie: Najkrótsze Who's Who jakie widziałem to Austriackie wydanie z 1948 r. -- wypełnione [wyłącznie] narciarzami i śpiewakami operowymi...

sobota, 24 stycznia 2015

Kolorowy pasek na zadaną długość

Oto prosty kod LaTeXa, który ma w zamierzeniu spowodować wydrukowanie kolorowego paska o długości 44mm:


Rys: rudy pasek

\documentclass{article}
\usepackage{graphicx,color}
\begin{document}
\definecolor{Xrudy}{rgb}{0.8,0.34,0.0}
\colorbox{Xrudy}{\strut\vrule width44mm height1pt}
\end{document}

Niestety kolorowy pasek wcale nie jest długi na 44mm tylko dłuższy (co widać na rysunku obok: rudy pasek jest dłuższy od czarnej kreski, która faktycznie ma 44mm).

Konsultacja z dokumentacją wyjaśnia, że LaTeX dokłada pewne rzeczy od siebie:


\documentclass{article}
\usepackage{graphicx,color}
\begin{document}
\showthe\fboxrule %% wyświetl wartość parametru \fboxrule
\showthe\fboxsep %% wyświetl wartość parametru \fboxsep
\end{document}

Każde \showthe\parametr powoduje że LaTeX, zatrzymuje kompilację wyświetlając na ekranie wartość parametru (aby kontynuować kompilację należy nacisnąć klawisza ENTER):


> 0.4pt.
l.31 \showthe\fboxrule

?
> 3.0pt.
l.32 \showthe\fboxsep

Czyli ramka ma domyślą grubość 0,4pt a margines wewnętrzny jest szeroki na 3,0 pt.

Aby się pozbyć dodatkowych odstępów należy zapodać:


\fboxsep=0pt

Można sprawdzić, że to faktycznie działa:


\documentclass{article}
\usepackage{graphicx,color}
\begin{document}
\definecolor{Xrudy}{rgb}{0.8,0.34,0.0}
\setbox0\hbox{% zapisujemy do pudełka zero żeby je potem zmierzyć
\colorbox{Xrudy}{\strut\vrule width44mm height1pt}}
\showthe\wd0 %% szerokość pudełka zero zdefiniowanego wyżej

\fboxsep=0pt
\setbox0\hbox{\colorbox{Xrudy}{\strut\vrule width44mm height1pt}}
\showthe\wd0
\end{document}

W rezultacie LaTeX wyświetli podczas kompilacji:


> 131.19212pt.
l.30 \showthe\wd0

?
> 125.19212pt.
l.36 \showthe\wd0

131,19212pt - 125,19212pt = 6,000 pt jak w mordę...

poniedziałek, 19 stycznia 2015

Cron i ostatni dzień miesiąca

Wprawdzie nie wiadomo który dzień jest ostatnim w miesiącu, ale na pewno po nim będzie dzień z numerem 1. Numer dnia, który będzie jutro można zaś ustalić wykorzystując date:


$ date +%d -d tomorrow
$ 19

W dniach 28--31, o godzinie 23:55 sprawdź czy następny dzień ma numer jeden. Jeżeli tak, wykonaj SKRYPT.sh:


55 23 28-31 * * [ "$(date +%d -d tomorrow)" = "01" ] && SKRYPT.sh

niedziela, 18 stycznia 2015

Google Maps brzydko kombinuje z formatem KML

Google wyświetla coś takiego kiedy oglądam mapę z ostatniego wyjazdu:

Jedna z funkcji Map Google używanych na tej stronie wkrótce się zmieni. Konieczne będzie przeniesienie zawartości niestandardowej mapy.

Klikam w Dowiedz się więcej:

Po lutym 2015 roku nie będzie już można wyświetlać niestandardowej zawartości plików KML w klasycznej wersji Map Google. KML to format pliku używany przez Google Earth do wymiany informacji geograficznych.

Dalej nie bardzo wiadomo w czym będzie problem, ale przynajmniej powiedzieli, że to już niedługo, jak przestanie działać. Jeszcze jeden klik i w końcu wiadomo o co chodzi (ale po angielsku):

From February 2015, maps created in the classic Google Maps -- https://maps.google.com/ -- will no longer load KML/KMZ files from external websites. However, we know that KML files are a really useful way to work with geographic data, so we've added KML to Google My Maps, and continue to support this format with other Google Maps APIs. We hope that one of these options will meet your needs.

A więc google zmienił zdanie. Kiedyś twierdził, że cyt: KML upload and rendering has always been problematic with Maps i że lepiej (jeżeli już), to nie importować KML tylko wyświetlić go podając URL do dokumentu KML jako wartość parametru q, przykładowo:

https://mapy.google.pl/maps?q=http://pinkaccordions.homelinux.org/PLIK.kml

A teraz że wręcz przeciwnie: parametr q w ogóle nie zadziała.

Wraz ze zmianą zdania trzeba przyznać poprawiono wsparcie dla KML w Google Maps -- teraz już nie jest problematic (sprawdziłem jest OK).

Google wreszcie ustalił, że można korzystać z Google Maps bez zakładania konta Google i podjął stosowne działania? Nieładnie.

Zatem trzeba będzie zmienić sposób w jakim są wyświetlane pliki KML na Google Maps (teraz to się nazywa zdaje się Moje Mapy/My Maps). Na szczęście nie jest tego w moim przypadku za dużo.

wtorek, 13 stycznia 2015

Sorry, Charlie Hebdo

It seems Charlie Hebdo massacre is a big surprise in France but what shock me even more is that the plain fact that France is at war with ISIS (cf French planes carry out air strikes on Isis targets in Iraq) was so efficiently obliterated from the awarness of the french populations, that the attack is met with naive incredulity and the reaction to it is so infantile.

The victims are presented now as defenders of freedom of speech which is not the case. They just produced stupid pictures, went to war with religion and (to their surprise) the religion retaliated. True free speach hero is (among many others) Norman Finkelstein who comments Charlie Hebdo massacre with the picture reprinted here.

wtorek, 6 stycznia 2015

New Year in Vienna

With ZK and his family we have spent 3 days in Vienna. If you are interested click the map below to see journey GPX track and photos.

We visited Schönbrunn Palace (a former imperial summer residence), Stephansdom, and Kunsthistorisches museum (where one can see Pieter Bruegel's famous paintings including: The Tower of Babel, The Fight Between Carnival and Lent, and The Hunters in the Snow).

We also visited Flak Towers which are enormous concrete bunkers constructed during 2nd world war. FLAK stands for Flug Abwehr Kanone (or Flugzeugabwehrkanone) and Flak Towers were a part (not particularly effective--mobile system is always better for obvious reason) of German air defence system intended to protect cities and residential areas from Allied air raids. Their construction was planned in September 1942 and Vienna was the third city after Berlin and Hamburg that got them.

The Flak Towers fulfilled two purposes (cf. Flak-Türme Towers, Vienna: Nazi concrete heritage at Vienna's heart): They held cannons and spotlights that should fight airplanes from the ground; and they were important bunkers with an autonomous electricity, air and water supply system. They were built in pairs, comprising of larger Gefechtsturm (Combat Tower) and a smaller Leitturm (Fire-control tower). The three pairs of Vienna can be found in the Augarten in the second district, the Arenberg Park in the third district and one each in the Esterhazypark in the sixth and the Stiftskaserne in the seventh.

We visited Augarten and Arenberg towers.