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).
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 ######
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
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
Geokodowanie to zamiana adresu lub nazwy miejsca na parę współrzędnych.
Perlowy moduł Google::GeoCoder::Smart
wykorzystany
w poniższym skrypcie używa geolokalizatora Google'a:
#!/usr/bin/perl use Google::GeoCoder::Smart; $geo = Google::GeoCoder::Smart->new(); $location = $ARGV[0]; my $coords = addr2coords( $location ); ## ## ## ## ## ## sub addr2coords { my $a = shift ; ## address for example "Sopot,Polska" my $r = shift || 'n'; ## flag--order of coordinates lat/lng or lng/lat my ($lat, $lng) ; ## ## consult cache first ; $GeoCodeCache is a global hash ## ## if (exists $GeoCodeCache{"$a"} ) { ($lat,$lng) = split (" ", $GeoCodeCache{"$a"} ); } else { my ($resultnum, $error, @results, $returncontent) = $geo->geocode("address" => "$a"); $resultnum--; if ($resultnum > 0) { print STDERR "** Location $a occured more than once! **" } if ( $error eq 'OK' ) { 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"; } # KML order lng/lat else { return "$lat $lng"; ## GPX order lat/lng } }
Jest limit 2500 żądań/dzień (24 godziny, przy czym nie jest dokładnie opisane kiedy następuje `reset' licznika, tj. rozpoczyna się następna doba). Jeżeli się przekroczy limit to:
perl ./coordinates.pl Wrocław ** Location Wrocław not found! due to OVER_QUERY_LIMIT ****
Ponieważ w bibliotekach Perla jest wszystko są także moduły Geo::Coder::Many i Geo::Coder::Multiple, który potrafią korzystać z wielu Geokoderów na raz (Google, Yahoo, Bing), zwiększając w ten sposób dzienny limit. Nie używałem...
Dopisane 29 stycznia 2012: W sieci via Google można znaleźć informacje, że reset ma miejsce ,,at midnight 12:00 PST.'' Ale w tym przypadku coś nie bardzo się zgadza, bo exact midnight PST byłoby o 9:00 rano (8:00 GMT), a blokę na mój IP zdjęli około 16.00. (A kwota wyczerpała się o jakieś 18--19 dnia poprzedniego--dokładnie nie pamiętam.)
W książce Flickr Hacks Paula Bauscha i Jima Bumgardnera
do manipulowania danymi w formacie XML
przesyłanymi z flickr.com
używa się XML::Simple
. Jakoś XML::Simple
nie wydaje mi się specjalnie
simple w tym wypadku. Zdecydowanie wolę XML::LibXML
,
który pozwala przede wszystkim na dotarcie do poszczególnych fragmentów dokumentów XML
w prosty sposób bez używania karkołomnych referencji.
Przykładowo poniższy skrypt (fragment) wykonuje zapytanie flickr.photos.search
[najistotniejszą częścią skryptu -- zaznaczoną odmiennym kolorem tła --
jest pętla do { ... }
]:
use XML::LibXML;
use LWP::Simple;
use Getopt::Long;
require 'login2flickr.rc';
our $api_key;
our $my_flickr_id;
my $tags_txt;
my $show_help;
my ($user_id, $all_users, $report_views);
my @Photos;
GetOptions( "all" => \$all_users,
"user=s" => \$user_id, "views" => \$report_views,
"tags=s" => \$tags_txt);
unless ( $user_id ) { $user_id = $my_flickr_id; }
if ( $all_users ) { $user_id = ''; } ## search all photos
my $parser = XML::LibXML->new;
my $pageno;
my $method = 'flickr.photos.search';
my $max_per_page = 500;
my ($total_pages, $current_page);
my $search_query = "&extras=geo,views"; # http://www.flickr.com/services/api/flickr.photos.search.html
if ( $user_id ) { $search_query .= "&user_id=$user_id"; }
if ( $tags_txt ) { $search_query .= "&tags=$tags_txt" }
do {
$pageno++;
my $resp = get ( "http://www.flickr.com/services/rest/?" .
"method=$method&api_key=$api_key&per_page=$max_per_page&page=$pageno" .
$search_query );
## sprawdź czy jest OK:
my $photos = $parser->parse_string($resp);
unless ( $photos->getElementsByTagName('rsp' )->[0]->getAttribute( 'stat' ) eq 'ok') {
die "Problems with retriving photo list!\n" }
push @Photos, $photos->getElementsByTagName('photo' );
$total_pages = $photos->getElementsByTagName('photos' )->[0]->getAttribute('pages' );
$current_page = $photos->getElementsByTagName('photos' )->[0]->getAttribute('page' );
# Dla dużych zbiorów zdjęć może długo trwać, więc wypisz że coś robisz:
printf STDERR " *** Page %d of %d retreived...\n", $current_page, $total_pages;
} while ( $current_page lt; $total_pages );
## Lista @Photos zawiera wszystkie zdjęcia...
## ... dalsza część skryptu ...
Pętla jest niezbędna ponieważ flickr API
organicza wielkość zwracanego dokumentu do maksymalnie 500 zdjęć.
Jeżeli zdjęć jest więcej, to zapytanie należy zadać wielokrotnie, zmieniając wartość parametru
page
. Przy czym liczbę wszystkich stron określa wartość atrybutu pages
.
Po ściągnięciu dokumentu skrypt sprawdza czy wszystko jest OK. Jeżeli tak, to za pomocą
getElementsByTagName
pobiera wszystkie elementy photo
i dodaje
je do listy @Photos
. Teraz pobierane są wartości atrybutów pages
/page
w celu ustalenia czy wszystko już ściągnięto. Jeżeli pages
< page
to pętla działa dalej...
Zwracam uwagę, że od jakiegoś czasu zapytanie może zawierać parametr extras
, dzięki
któremu zwracany dokument XML zawiera dodatkowe informacje, takie jak: tagi, współrzędne geograficzne,
liczbę odsłon, itd...
Jest to opisane w API na flickr.com a także
tutaj.
W wyżej przedstawionym skrypcie deklaracja:
my $search_query = "&extras=geo,views";
Oznacza, że do standardowych opisów zdjęć będą dodane także współrzędne geograficzne (oczywiście
tylko wtedy gdy zdjęcia będą nimi oznaczone) oraz liczba odsłon. BTW dodanie extras=views
powoduje, że kiedyś niemożliwy do wykonania poprzez API problem ustalenia ile razy zdjęcie było
oglądane teraz jest jak najbardziej wykonalny. Zatem skrypty -- opisane
w Nowa usługa na flickr.com
oraz Czy flickr umie liczyć -- lepsze rozwiązanie
-- są już niepotrzebne.
Kompletny skrypt pobierający zdjęcia z flickr.com i wypisujący plik GPX dla tych, które zawierają współrzędne geograficzne jest tutaj. Ale uwaga: próba wykonania zestawienia wszystkich wież ciśnień z flickr.com:
flickr_photo_gsearch.pl -a -t 'wieżaciśnień,wasserturm,watertower' > wasserturme-alles.xml
Skończyła się pobraniem ponad 23 tysięcy zdjęć (z tego około 6 tys. było geokodowane). Plik GPX pn. wasserturme-alles.xml miał 1,8 Mb i za nic nie szło tego wyświetlić w przeglądarce. Za duże... Mniejsze pliki są ok: kaplice [mapa ] albo moje wieże ciśnień [mapa ].
`Na szybko' zrobiłem trzy skrypty wykorzystujące
Ebay REST API. Skrypty
mają ułatwić wyszukiwanie oraz archiwizowanie
interesujących mnie aukcji. Próbowałem wykorzystać do ww. celu gotowe narzędzie ale jakoś nic interesującego
nie znalazłem. Bibioteka Net::eBay
jakaś taka słabo udokumentowana
i nieporęczna mi się wydała podobnie jak WWW::Search::Ebay
. (Por. skrypt
opisany np. tutaj.)
Najbardziej skomplikowany z moich skryptów wykorzystuje metodę
FindItemsAdvanced
do wyszukania aukcji. Metoda
FindItemsAdvanced
ma parametr MaxEntries
określający liczbę zwracanych aukcji.
Parametr ten ma domyślną wartość 20
a maksymalną akceptowaną wartością
jest 200
(jeżeli podamy 0
to metoda zwróci liczbę
pasujących do zapytania aukcji). Jeżeli aukcji jest więcej niż wynosi wartość
MaxEntries
, to aby ściągnąć kolejne aukcje metodę FindItemsAdvanced
trzeba wykonać
wielokrotnie,
podając odpowiednią wartość parametru PageNumber
. Przykładowo
jeżeli aukcji jest 121, a wartość MaxEntries
wynosi 40, to trzeba
FindItemsAdvanced
wykonać 4 razy.
Parametr ItemSort
ustala porządek sortowania. Wartość
StartDate
tego parametru powoduje, że aukcje są posortowane według
daty wstawienia na ebay (daty/czasy są definiowane w GMT oczywiście).
W związku z takim działaniem metody FindItemsAdvanced
skrypt ebay_rest_search.pl
najpierw ustala liczbę pasujących
aukcji, a następnie pobiera kolejno strony w pętli do momentu aż znajdzie
aukcję już pobraną (aukcje są przechowywane w haszu %IdxLog
,
który jest przechowywany na dysku
przy wykorzystaniu funkcji store
/retrieve
modułu Storable).
Nowe aukcje są dodawane do hasza %IdxLog
:
## Uproszczony fragment skryptu: while ( ($current_page >= 0) && ($current_page <= $last_page ) ) { $current_page++; $xml = make_call('search_text' => $search_text, 'per_page' => $MAX_PER_PAGE, 'site_id' => "$this_site_id", 'page' => $current_page); my $xm = $xmlp->XMLin($xml, ForceArray => 0); my $list = $xm ->{'SearchResult'}->{ItemArray}->{Item} ; foreach $item ( @{$list} ) { $item_id = $item->{'ItemID'}; if ( exists $IdxLog{ $item_id } ) {## zakończ $current_page = -99; last ; } else { $new_items++; $IdxLog{ $item_id } = [ $item->{'Title'}, $item->{'StartTime'}, $item->{'EndTime'}, $item->{'GalleryURL'}, $item->{'ListingStatus'}, $item->{'ViewItemURLForNaturalSearch'}, $day_added ]; } } }
Metoda XMLin
jest z modułu XML::Simple
. Moduł ten
definiuje prosty interfejs do dokumentów XML. Po wykonaniu XMLin
,
zawartość dokumentu jest dostępna w postaci elementów zagnieżdżonych list/haszy.
Można oczywiście analizować dokument XML w inny sposób, nie upieram się
że XML::Simple
jest najlepszym pomysłem.
Uwaga: metoda FindItemsAdvanced
nie umożliwia przeszukania całej
bazy eBay (odpowiednik wyszukiwanie zaawansowane->preferowana lokalizacja->cały świat).
Parametr siteid
określa, który serwis eBay ma być przeszukany. Jeżeli
podamy id=0
to zapytanie dotyczyć będzie ebay.com, a jeżeli
id=212
, to ebay.com.pl, itd.
Aby przeszukać cały ebay to trzeba wykonać FindItemsAdvanced
wielokrotnie.
Następnie hasz %IdxLog
jest przeglądany w pętli
celem archiwizacji aukcji, które się zakończyły:
for $id (sort { $IdxLog{$a}[2] cmp $IdxLog{$b}[2] } keys %IdxLog) { $end_time = $IdxLog{$id}[2]; # drugi element listy to czas zakończenia $time_to_end = $now_in_seconds - ebaydate2seconds( $end_time ); if ($time_to_end > 0 && $status =~ /Active/) {# Aukcja zakończona $result = `ebay_rest_item.pl -i $id`; ## ściągnij innym skryptem if ($? > 0 ) { warn "Problems backuping $id\n"; } else { $IdxLog{$id}[4] = 'Completed'; # zmień status na zakończoną $completed_items++; } } }
Zmienna $now_in_seconds
, to liczba
sekund od epoch
a funkcja ebaydate2seconds
zmienia napis zawierający czas w formacie zwracanym w dokumencie XML na liczbę sekund
od epoch. Jeżeli różnica czasu bieżącego a czasu zakończenia
aukcji wskazuje, ze aukcja się zakończyła jest ona ściągana za pomocą
skryptu ebay_rest_item
:
#!/usr/bin/perl use LWP::Simple; use Getopt::Long; my $verbose = 1; use lib "$ENV{HOME}/.ebay"; ## configuration files GetOptions('item=s' => \$ebay_item_id, 'help' => \$print_help, ); unless ($ebay_item_id) { die "Usage: $0 -i <item_id>\n"; } our $netebay_rc; require("netebay.rc"); my $url = "http://open.api.ebay.com/shopping?callname=GetSingleItem" . "&responseencoding=XML" . "&appid=$EBayRC{p_appid}" . "&siteid=0" . "&version=525" . "&ItemID=$ebay_item_id" . "&IncludeSelector=Description,Details"; my $xml = get $url ; ## In a scalar context m//g iterates through the string, returning true each time ## it matches. If you modify the string in any way, the match position is reset. my %Pics; while ($xml =~ /<PictureURL>([^<>]+)<\/PictureURL>/mg) { $Pics{$1} = 1; } my $pic, $ori_pic; for $pic (keys %Pics ) { $ori_pic = $pic; $ori_pic =~ s/\?/\\\?/g; # zmieniamy meta znaki: ? $pic =~ s/\?[^\?]+$//g; my ($content_type, $document_length, $modified_time, $expires, $server) = head($pic); $pic_no++; if ( $content_type =~ /image/ ) { $content_type =~ m/\/(.+)$/; $local_file = "${ebay_item_id}_${pic_no}.$1"; } else { $local_file = "${ebay_item_id}_${pic_no}.JPG?"; } ## problems with content_type warn "Storing $pic in $ARCH_DIR/$local_file ... \n"; getstore($pic, "$ARCH_DIR/$local_file"); ## zapisz lokalną nazwę jako atrybut ## Uwaga na metaznaki (? już jest obezwładniony): $xml =~ s/<PictureURL>$ori_pic/<PictureURL local='$local_file'>$ori_pic/; } open (LOG, ">$ARCH_DIR/$ebay_item_id.xml" ) || die " ** cannot open $ARCH_DIR/$ebay_item_id.xml **"; print LOG "$xml\n";
Ten skrypt zapisuje po prostu dokument XML zwrócony przez metodę GetSingleItem
.
Argumentem skryptu jest id aukcji. Jedyne utrudnienie związane jest z pobraniem
zdjęć. Adresy URL do zdjęć są zdefiniowane wewnątrz elementów PictureURL
.
Aby uprościć skrypt elementy te są wyszukiwane przy pomocy prostego wyrażenia
regularnego. Konstrukcja:
while ($napis =~ /wzorzec/mg) { ## $1 zawiera dopasowany do wzorca napis }
Będzie dopasowywać wzorzec
do $napisu
iteracyjnie. W każdej
iteracji zmienna $1
będzie zawierała kolejny napis
pasujący do wzorca
. Aby powyższe działało prawidłowo $napis
nie może być modyfikowany wewnątrz pętli (i nie jest). Adresy URL są składowane
w haszu %Pics
.
Teraz hasz %Pics
jest przeglądany w pętli. Zdjęcia są ściągane
a dodatkowo dokument XML jest modyfikowany -- też za pomocą wyrażeń regularnych --
w taki sposób, że do elementu dodawana jest
informacja o nazwie lokalnej
zdjęcia. Nazwa lokalna jest tworzona automatycznie jako:
${ebay_item_id}_${pic_no}.$1
, gdzie ${pic_no}
jest kolejnym
numerem zdjęcia dla aukcji o numerze ${ebay_item_id}
.
Ostatni skrypt jest najprostszy i drukuje fragment drzewa kategorii eBay (konkretnie
podkategorie dla kategorii, której id podano jako argument skryptu). Korzeń drzewa
ma id równe -1
.
#!/usr/bin/perl use LWP::Simple; use Getopt::Long; use lib "$ENV{HOME}/.ebay"; ## configuration files use $ebay_site_id = 'US'; ## 0 is US ebay GetOptions( 'site=s' => \$ebay_site_id, 'category=i' => \$ebay_cat_id, 'help' => \$print_help, ); unless ($ebay_cat_id) {$ebay_cat_id = -1 } ## top-level our $netebay_rc; require("netebay.rc"); my $site_id_code = $Sites_Ids{$ebay_site_id}; my $url = "http://open.api.ebay.com/Shopping?callname=GetCategoryInfo" . "&appid=$EBayRC{p_appid}" . "&version=533" . "&siteid=$site_id_code" . "&CategoryID=$ebay_cat_id" . "&IncludeSelector=ChildCategories"; print (get $url) . "\n" ;
Ten skrypt przyda się do udoskonalenia działania ebay_rest_search
,
tak żeby przeszukiwania dotyczyły tylko wybranych kategorii.
Niestety eBay
nie ma jednolitej hierarchii kategorii. Ten sam przedmiot może być różnie
klasyfikowany.
No i tyle. Pozostaje umieścić ebay_rest_search
w crontabie. Skrypty są tutaj.
Niedawno (13 grudnia 2007) Flickr uruchomił nowy serwis pn. stats! Użytkownicy serwisu z wykupioną usługą pro (ca 25 USD, czyli niedużo -- jak to się czasy zmieniły BTW) mają dostęp do różnych statystyk dotyczących swoich zdjęć. Informacje o usłudze zobaczyłem dzisiaj. Żeby z niej skorzystać trzeba się zasubskrybować -- czytaj nacisnąć duży guzik ze stosownym napisem. Wówczas ukazuje się napis, że OK, ale statystyki będą jutro. Grozę potęguje animacja gościa z pneumatycznym młotem. Widocznie jestem jednym z pierwszych bo były od razu -- wystarczyło odświeżyć stronę.
A więc pro-user dostaje takie informacje jak: liczba odsłon dla każdego zdjęcia oraz statystyki adresów referer w podziale na ,,wczoraj'' oraz ,,ogółem''. Ponadto wykres liniowy liczby odsłon zawierający dzienne dane z ostatniego miesiąca oraz zestawienie tabelaryczne zawierające podsumowania liczby odsłon (wczoraj, ostatni tydzień, ostatni miesiąc oraz ogółem) dla stron głównych (photostream), zbiorów (sets), kolekcji (collections) i stron pojedynczych zdjęć.
Statystki ,,oglądnięć'' dostępne ze strony
http://www.flickr.com/photos/tprzechlewski/stats/allphotos/
(Nie ma co klikać, więc i linka nie ma -- strona dostępna po
zalogowaniu:-) są kompletne, tj. dla każdego zdjęcia dostępna jest
liczba odsłon (z wczoraj, z ostatniego tygodnia oraz ogółem).
W pierwszej chwili pomyślałem, że nowa usługa renders obsolete
jak mawiają Anglicy moje
skrypty,
które z takim zapałem ostatnio udoskonalałem.
Ale niekoniecznie! Dane
ze strony stats! primo nie są trwałe (niektóre tak -- ale nie wszystkie,
np. dzienne statystyki oglądalności dostępne są tylko dla ,,wczoraj''),
secundo żeby były trwałe trzeba je ściągać na swój komputer
na przykład codziennie. No i tu się przyda programik
flickr_store_views.pl
,
który potrafi się zalogować na flickr.com, pobrać
stronę i zapisać co trzeba na dysk.
Zamiast ściągać 170 stron i z nich
wyciągać potrzebne informacje wystarczy teraz ściągnąć kilka stron
spod adresu tprzechlewski/stats/allphotos/yesterday/pageliczba
robiąc to metodycznie, czyli codziennie.
Jeszcze link z forum z informacjami nt. omawianej wyżej usługi. Your stats will eventually be a rolling 28 day window, czyli dostępne będą tylko dane z ostatniego miesiąca. Zwracam też uwagę na komentarz dot. tłumaczenia słowa interestingness na język hiszpański jako interesidad. Że niby takie słowo nie istnieje. Faktycznie nie ma w słowniku a google znajduje raptem 4 strony z interesidad (w tym powyższa). Ale jak przetłumaczyć interestingness (za WordNet: the power of attracting or holding one's interest), np. na j. polski? Moc przyciągania?
Dopisane 13 maja 2008:
Skrypt
flick-store-views.pl
przestał działać. Coś zostało zmienione w procedurze logowania.
Nie chce mi się tego poprawiać...
Znacząco poprawiłem swój tryb do dodawania zdjęć na flikr.com. Teraz wszystko się dzieje
wewnątrz Emacsa łącznie z uruchomieniem wysyłania plików na serwer. W starej wersji
pliki konfiguracyjne czytał Emacsowy moduł xml.el
co było może
i eleganckie ale odbywało się przeraźliwie wolno. W nowej wersji plik konfiguracyjny
jest plikiem lispowym wygenerowanym skryptem Perla z plików XML.
Jak to działa opisałem
na oddzielnej stronie.
Jedna sprawa jest tajemnicza:
#!/usr/bin/perl -w require 'login2flickr.rc'; require 'flickr_utils.rc'; my @tmpx = get_sets_ids(); my @tmpy = get_pools_ids();
W plikach login2flickr.rc
oraz flickr_utils.rc
są zdefiniowane
procedury, który czytają plik z dysku i zwracają zmienne. W szczególności
flickr_utils.rc
zawiera dwie prawie identyczne
procedury (get_sets_ids
oraz get_pools_ids
), czytające
różne pliki konfiguracyjne. Kurcze... na jednym komputerze perl zwraca błąd:
Undefined subroutine &main::get_pools_ids called at... a na drugim działa.
Ten sam perl, ta sama wersja FC5, jedna procedura z pliku dołączanego
poleceniem require
jest zdefiniowana druga nie...
Wystarczy zmienić kolejność poleceń require
żeby powyższe działało w obu systemach. Nic mi do głowy nie przychodzi...
Wymyśliłem lepsze rozwiązanie problemu
opisanego tutaj.
Ściągane strony są obrabiane ,,w locie'' a w pliku na dysku są zapisywane
tylko informacje o liczbie odsłon.
Mówiąc konkretniej korzystam z metod
store
/retrive
modułu Storable
,
zapisując/czytając hasz
postaci $PhotoLog{data}{photoid}= views
.
Generowaniem podsumowań zajmuje się inny skrypt, który wypisuje wyniki w postaci
dobrze sformatowanego fragmentu dokumentu HTML (,,opakowanego''
wewnątrz elementu <div>
).
Ten fragment następnie można wstawić w odpowiednie miejsce strony HTML.
Wreszcie ostatni skrypt tworzy wykres
liniowy (przy wykorzystaniu modułu GD::Graph
) liczby odsłon
oraz liczby odsłoniętych zdjęć. Trzy skrypty można połączyć w całość:
#!/bin/bash perl flick-store-views.pl if [ "$?" -ne "0" ] ; then echo "** Problems ..." ; exit 1 ; fi perl flick-report-views.pl -lang=pl -max=25 > 00-pl.phtml && \ perl flick-graph-views.pl if [ "$?" -ne "0" ] ; then echo "** Problems ..." ; exit 1 ; fi echo "OK"
Jakoś tak... Przykład wykorzystania
jest na tych stronach [1]
[2]
[3].
Skrypty można pobrać
stąd
(flick-store-views.pl,
flick-report-views.pl oraz
flick-graph-views.pl.)
Powyższe można nawet wsadzić do crona.
Skrypty wykorzystują m.in. moduły GD::Graph::lines
oraz
Storable
. Ten pierwszy musiałem doinstalować do mojej FC5.
Z tym był zresztą pewien kłopot ponieważ yum
nie zadziałał--nie
wiem czemu. Ostatecznie sprawę rozwiązało ,,ręczne'' ściągnięcie
perl-GDGraph-1.4307-1.fc5.noarch.rpm
,
perl-GDTextUtil-0.86-7.fc5.noarch.rpm
i perl-GD-2.35-1.fc5.i386.rpm
ze strony
rpm.pbone.net.
Przy okazji dowiedziałem się jak ,,porządnie'' wycentrować tabelę w oknie przeglądarki:
<table style='margin-left: auto; margin-right: auto;' ...
Łączną liczbę odsłon dla
wszystkich zdjęć -- por. poprzedni
wpis na ten temat -- można ustalić
programistycznie ściągając wszystkie strony albumu (mają one URLe
kończące się na pageliczba
, tj. page1
,
page2
, page3
, itp.). Pod miniaturą
każdej fotografii jest odpowiednia informacja. Zwykłe
LWP::Simple
w zupełności by do tego wystarczył:
use LWP::Simple; my $max_page = $ARGV[0] || 1; # nie podano ile -- ściągnij pierwszą $urlbase = "http://www.flickr.com/photos/tprzechlewski/page"; for ($p=1; $p =< $max_page; $p++ ) { $content = get($urlbase . $p ); die "Couldn't get it!" unless defined $content; print $content; }
Niech powyższy kod zapisano w ftotal.pl
.
Teraz można by np. ściągnąć wszystkie strony podając
ftotal.pl 161 > ftotal.log
a następnie wyłuskać odpowiednie
informacje z pliku ftotal.log
innym skryptem.
Ale... Ale jest jeden problem. Flickr słusznie nie liczy odsłon (zalogowanego) właściciela albumu. Skrypt nie autoryzuje dostępu i działa jako ,,osoba trzecia'' więc jednocześnie sztucznie nabija statystykę. Jeżeli skrypt byłby uruchamiany cyklicznie zmieniłby znacząco statystykę odsłon głównych stron w albumie. Przykładowo w moim przypadku byłoby to dodanie 161 odsłon, bo tyle liczy -- obecnie -- stron głównych mój album. Można by machnąć ręką, ale z drugiej strony jakby udało się skrypt zalogować...
Logowanie
do www.flickr.com
jest
jednak cokolwiek skomplikowane.
Kombinowałem na różne sposoby szukając w google albo gotowców albo
podpowiedzi. Pierwszym ,,podejrzanym''
był moduł WWW::Mechanize
:
#!/usr/bin/perl use WWW::Mechanize; my $mech = WWW::Mechanize->new( autocheck => 1 ); $mech->credentials( 'login' => 'password' ); $mech->get( 'http://www.flickr.com/photos/tprzechlewski/page1/' ); print $mech->content();
Nie działa... Próbowałem, też WWW::Mechanize::Shell
,
opisany przykładowo w tekście Michaela Schilli
Simple
Data Scraper
(tutaj
jest polskie tłumaczenie). BTW były pewne kłopoty z jego zainstalowaniem, ponieważ
make test
kończy się błędem...
Szukając wskazówek do rozwiązania mojego problemu
znalazłem także potencjalnie przydatny tekst pt.
Secure
Web site access with Perl. Podsumowując WWW::Mechanize
okazał
się strzałem w płot, ale może się przyda do czegoś innego...
Rozwiązanie znalazłem -- jak to często bywa -- trochę przypadkowo. Punktem wyjścia
były skrypty ze
strony: coder.home.cosmic-cow.net
(ich kopie
umieściłem tutaj).
Następnie posługując się wtyczką do Firefoxa pn. Live HTTP headers
ustaliłem co i gdzie trzeba zmienić.
Skrypt wykorzystuje moduły HTTP::Request::Common
i LWP::UserAgent
, koncepcyjnie jest mało skomplikowany ale dość rozwlekły
bo liczy ca 100 wierszy. Nie będę go więc cytował, jest dostępny
tutaj.
Nie do końca jestem też pewny czy wszystkie wywołania GET
są potrzebne, ale nie mam czasu/wiedzy tego optymalizować,
ważne że działa, tj. udaje zalogowanego
użytkownika serwisu flickr.
Skrypt działa w ten sposób, że ściąga n stron ,,głównych'' z mojego konta
flickr (te URLe przypominam kończą się na pageliczba
). Liczbę
n, podaną jako parametr wywołania skryptu,
ustalam ,,empirycznie'' konsultując
się z flickr.com (ewentualnie w wersji gold-extended skryptu można by to zautomatyzować):
perl flick-total-views.pl 161 > flick-total-views.log && \ perl -h flick-aggr-totals.pl flick-total-views.log > stats.html
Skrypt flick-aggr-totals.pl
zlicza
co trzeba parsując flick-total-views.log
.
Na razie pomija pliki, które nie były wcale oglądane (tj. 0 views),
ale to łatwo poprawić/zmienić, bo
kod HTML generowany przez www.flickr/ nie jest specjalnie zaplątany.
Wynik pierwszego zastosowanie ww. skryptów zamieściłem
na mojej stronie.
Flickr wyświetla liczbę że tak powiem odsłon pod każdym zdjęciem, zbiorczo dla każdego zbioru oraz łącznie dla całego albumu (zakreślone na czerwono na rysunkach poniżej). Wydawać by się mogło, że np. sumując odsłony dla wszystkich zdjęć otrzyma się łączną liczbę odsłon w albumie, to znaczy ile razy oglądano nasze zdjęcia. Już na pierwszy rzut oka widać, że tak nie jest. Po prostu nic nie jest sumowane a każdy licznik ,,liczy'' swoją stronę. Odzielnie jest sumowana ,,strona główna'', oddzielnie każda strona dla pojedynczego zdjęcia i oddzielnie strona główna każdego zbioru.
A jak obliczyć łączną liczbę odsłon dla wszystkich zdjęć?
Wydawałoby się, że to pryszcz, bo flickr słynny jest ze swojego API.
Akurat tego jednak nie da się ustalić -- nie ma takiej metody.
Wprawdzie
flickr.activity.userPhotos
.
zwraca m.in. liczbę wyświetleń każdej pojedynczej strony, ale
tylko dla stron na których coś się stało: dodano komentarz, ktoś
dodał taga albo dodał zdjęcie do swoich ulubionych. Do tego maksymalnie
można ściągnąć 50 zdjęć na raz (maksymalna wartość per_page
),
parametr timeframe
może przyjąć maksymalnie
wartość jednego miesiąca (30d
, większe wartości są ignorowane)
a metodę można uruchomić
powtórnie nie częściej niż co godzinę (czyli co godzinę można ściągnąć jedną stronę).
Poddałem się...
Nie ustaliłem wprawdzie ile było odsłon zdjęć w moim albumie
ale eksperymentując z API flickera odkryłem przynajmniej jak można
obejść się bez perlowego pakietu
Flickr-API
(ale nie bez Perla). Otóż wystarczą moduły LWP::Simple
oraz Digest::MD5
:
Niektóre metody nie wymagają uwierzytelnienia. Ich wywołanie jest szczególnie
proste -- nie jest potrzebny nawet moduł Digest::MD5
-- i sprowadza się do konstruowania adresów
URL według następującego schematu (znak \
na końcu oznacza kontynuację wiersza):
http://www.flickr.com/services/rest/?method=metoda¶metr1=wartość1\ ¶metr2=wartość2...
W metodach, które uwierzytelnienia wymagają sprawa się komplikuje. Trzeba
podać api_key, auth_token oraz secret (poniżej nazwany shared_secret)
opisane tutaj
i/lub w dokumentacji modułu Flickr-Upload
.
Najpierw należy zbudować napis według schematu:
secretapi_keyapi_keyauth_tokenauth_tokenmethodmethodarg1wart1arg2wart2 ...
Następnie utworzyć jego skrót za pomocą funkcji MD5. W przypadku Perla może to
wyglądać jak na poniższym przykładzie (metoda flickr.activity.userPhotos
ma argumenty
page
, per_page
oraz timeframe
).
Obliczony skrót dodajemy jako ostatnią część adresu URL:
Digest::MD5 qw(md5_hex); my $method = 'flickr.activity.userPhotos'; ## ... ## skrót MD5: my $api_sig = md5_hex( "${shared_secret}api_key${api_key}auth_token${auth_token}method${method}" . "page${page}" . "per_page${per_page}" . "timeframe${timeframe}" ) ; my $url = "http://www.flickr.com/services/rest/?method=$method" . "&api_key=$api_key" . "&auth_token=$auth_token" . "&page=$page" . "&per_page=$per_page" . "&timeframe=$timeframe" . "&api_sig=$api_sig" ; ## wstaw skrót tutaj print $url;
Przy okazji -- jak to często bywa -- znalazłem ciekawą stronę dotyczącą języka Perl. Jest też na ww. stronie opis pakietu Flickr-Upload, z którego też korzystam. Norman Walsh zaimplementował nawet API flickra w XSLT -- ciekawe ale przydatność taka sobie.