2014年9月12日 星期五

[Python] 使用 Apache Web Server Access.log 把玩 CartoDB 視覺化地圖 @ Ubuntu 12.04



只要資料有 Geolocation,就能夠把玩 CartoDB 了 :P 若可以的話,再加上時間就更完美了。因此,最容易拿到的測資是 Apache web server log,把 access.log 挑點東西出來即可,至於 Geolocation 就用 ip 反查吧!

從 access.log 取出 ip list:

$ grep -v "^localhost\|::1" /var/log/apache2/access.log | awk '{print $1}' | uniq

首先,先到 Maxmind 下載最新的 GeoLite2-City 資訊:

$ wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz
$ gunzip GeoLite2-City.mmdb.gz


安裝 geoip-bin 工具:

$ sudo apt-get install geoip-bin
$ geoiplookup 8.8.8.8
GeoIP Country Edition: US, United States

$ geoiplookup -f GeoLite2-City.mmdb 8.8.8.8
Error Traversing Database for ipnum = 134744072 - Perhaps database is corrupt?
Segmentation fault (core dumped)


囧...只好裝一下新版 maxmind python sdk 寫一段小 code:

$ sudo pip install geoip2

$ vim t.py
mport sys
import geoip2.database
reader = geoip2.database.Reader('GeoLite2-City.mmdb')
try:
        response = reader.city(sys.argv[1])
        print str(response.location.latitude)+","+str(response.location.longitude)
except Exception, e:
        pass
$ python t.py 8.8.8.8
37.386,-122.0838


接著,就乾脆寫 python 來處理 access.log 吧 XDD 用 command line 好像太冗長了。

$ sudo cp /var/log/apache2/access.log /tmp/access.log
$ sudo chmod 644 /tmp/access.log
$ vim log.py
import geoip2.database
reader = geoip2.database.Reader('GeoLite2-City.mmdb')
try:
        log = open('/tmp/access.log','rb').read()
        for rec in log.split('\n'):
                fields = rec.split(' ')
                try:
                        if fields[0] == 'localhost' or fields[0] == '::1' :
                                continue
                        response = reader.city(fields[0])
                        print fields[3][1:]+","+str(response.location.latitude)+","+str(response.location.longitude)
                except Exception, e:
                        pass
except Exception, e:
        pass

$ python log.py > log.csv
$ cat log.csv
...
07/Sep/2014:22:12:43,35.685,139.7514
07/Sep/2014:22:13:53,39.4899,-74.4773
...


對於時間格式不用擔心,直接丟進 cartodb.com 請他幫你處理!




匯入後,預設都是 string,可以把 field1 設成 date type,field2 跟 field3 都設成 number type,弄完順便 rename 一下,接著再點選 geo 欄位,可以採用 field2 跟 field3 來生成,如此就完成 CartoDB  table 製作。








最後再去視覺化那邊,挑一下以 date 為基準的時間變化,就可以有不錯的視覺圖表。

2014年9月11日 星期四

Android 開發筆記 - 透過 gsutil 取得 Google Play App Customer Reviews

依照官方文件簡介,對於 Android app 的評論,系統都會儲存在 Google Cloud Storage 裡頭,需要透過 gsutil 取出來使用,在此透過 pip 安裝 gsutil:

$ sudo pip install gsutil

使用前,需要先到 Google Play Developer Console -> Your android app -> 評分與評論 -> 底下最下方找到 ID,如 pubsite_prod_rev_0123456789。

接著,先設定 gsutil 存取權限:

$ gsutil config
This command will create a boto config file at /home/username/.boto
containing your credentials, based on your responses to the following
questions.
Please navigate your browser to the following URL:
https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&client_id=############.apps.googleusercontent.com&access_type=offline
In your browser you should see a page that requests you to authorize access to Google Cloud Platform APIs and Services on your behalf. After you approve, an authorization code will be displayed.

Enter the authorization code:


並透過瀏覽器得到 authorization code,輸入完後,會在詢問 Project ID,這時請記得填寫類似這串 pubsite_prod_rev_0123456789 的 ID。

之後,就可以透過 gsutil cp -r gs://pubsite_prod_rev_0123456789/reviews/reviews* 取得所有評論。

$ gsutil cp -r gs://pubsite_prod_rev_0123456789/reviews/reviews_* android_review/

這些 reviews 都是 CSV 格式,且第一列有顯示欄位資訊:

Package Name,App Version,Reviewer Language,Reviewer Hardware Model,Review Submit Date and Time,Review Submit Millis Since Epoch,Review Last Update Date and Time,Review Last Update Millis Since Epoch,Star Rating,Review Title,Review Text,Developer Reply Date and Time,Developer Reply Millis Since Epoch,Developer Reply Text,Review Link

看來只要寫隻簡易的 tool 就可以處理完畢囉:

$ file reviews_*.csv
reviews_*.csv: Little-endian UTF-16 Unicode text, with very long lines


因此,寫 php script 的話,可以先用 iconv 轉 UTF-8 後,再搭配 str_getcsv 處理:

<?php
$raw = file_get_contents(...);
$result = iconv($in_charset = 'UTF-16LE' , $out_charset = 'UTF-8' , $raw);
if( false !== $result )
$raw = $result;

$raw_lines = explode("\n", $raw);
array_shift($raw_lines); // title
foreach( $raw_lines as $line ) {
$fields = str_getcsv($line);
print_r($fields);
}

iOS 開發筆記 - 透過 iTunesConnect RSS 處理 App Customer Reviews 資料

以 Facebook 而言,可以輕易得知 App ID 為 284882215,而 iTunesConnect 有提供 RSS 方式查詢 Customer Reviews:

https://itunes.apple.com/us/rss/customerreviews/id=284882215/sortBy=mostRecent/xml

其中,上述有三個主要可變動的參數:country(us), app id (284882215), format(xml)

最近比較愛 json 的:

https://itunes.apple.com/us/rss/customerreviews/id=284882215/sortBy=mostRecent/json

想要台灣區評價:

https://itunes.apple.com/tw/rss/customerreviews/id=284882215/sortBy=mostRecent/json

問題應該就是 country 到底有哪些,可以用既定資料每個都抓一遍啦。

對於格式有興趣的可以用得到可讀性較佳的資料格式:

$ curl https://itunes.apple.com/tw/rss/customerreviews/id=284882215/sortBy=mostRecent/json | python -mjson.tool

若仔細看的話,還可以看到 User ID 、追蹤到 User 對其他款 app 評價等,此外,也有 first page 跟 last page 存取方式:

https://itunes.apple.com/tw/rss/customerreviews/page=1/id=284882215/sortby=mostrecent/json
https://itunes.apple.com/tw/rss/customerreviews/page=10/id=284882215/sortby=mostrecent/json

最大頁數只有到 10 而已(CustomerReviews RSS page depth is limited to 10)

至於要用 json 還是 xml 好?目前的心得是... json 會缺少 user comment updated 資訊,而 xml 卻也會碰到 format error 的情況 Orz 只能...看著辦了 XD


以 PHP 處理為例:

$ cat test.php
<?php
$app_id = '284882215';
$country_list = array( 'tw', 'us');
$format = 'xml';
$page_list = array(1,2,3,4,5,6,7,8,9,10);
foreach( $country_list as $country ) {
        foreach( $page_list as $page ) {
                $url = "https://itunes.apple.com/$country/rss/customerreviews/id=$app_id/page=$page/$format";
                if ($format == 'json') {
                        $raw = json_decode(@file_get_contents($url), true);
                        //print_r($raw);
                        for ($i=1, $cnt=count($raw['feed']['entry']) ; $i<$cnt ; ++$i) {
                                print_r(array(
                                        'user_id' => $raw['feed']['entry'][$i]['id']['label'],
                                        'user_name' => $raw['feed']['entry'][$i]['author']['name']['label'],
                                        'user_uri' => $raw['feed']['entry'][$i]['author']['uri']['label'],
                                        'title' => $raw['feed']['entry'][$i]['title']['label'],
                                        'content' => $raw['feed']['entry'][$i]['content']['label'],
                                        'version' => $raw['feed']['entry'][$i]['im:version']['label'],
                                        'rating' => $raw['feed']['entry'][$i]['im:rating']['label'],
                                ));
                        }
                } else {
                        $raw = simplexml_load_string(@file_get_contents($url));
                        //print_r($raw);
                        for($i=1, $cnt = count($raw->entry); $i<$cnt ; ++$i) {
                                //print_r($raw->entry[$i]);
                                $imAttrs = $raw->entry[$i]->children('im', true);
                                //print_r($imAttrs);
                                print_r(array(
                                        'date' => (string)$raw->entry[$i]->updated,
                                        'user_id' => (string)$raw->entry[$i]->id,
                                        'user_name' => (string)$raw->entry[$i]->author->name,
                                        'user_uri' => (string)$raw->entry[$i]->author->uri,
                                        'title' => (string)$raw->entry[$i]->title,
                                        'content' => (string)$raw->entry[$i]->content[0],
                                        'version' => (string)$imAttrs->version,
                                        'rate' => (string)$imAttrs->rating,
                                ));
                        }
                }
                exit;
        }
}

$ php test.php
Array
(
    [date] => 2014-09-10T09:10:00-07:00
    [user_id] => ###########
    [user_name] => ###########
    [user_uri] => https://itunes.apple.com/tw/reviews/id###########
    [title] => 有Bug請改善
    [content] => 點朋友的動態跑不出來!!
    [version] => 14.0
    [rating] => 1
)
Array
(
    [date] => 2014-09-10T09:03:00-07:00
    [user_id] => ###########
    [user_name] => ###########
    [user_uri] => https://itunes.apple.com/tw/reviews/id###########
    [title] => 一直自動關掉
    [content] => 用一用會一直自動跳掉,很頻繁,很煩。
    [version] => 14.0
    [rating] => 1

)

2014年9月10日 星期三

[SQL] SELECT IN SELECT 以及 Pagination 的使用 @ MySQL 5.6

使用 SQL 語法時,有時會需要從另一張 Table 取出清單,接著對清單內的資料做為基準再進行一次資料的擷取,直觀的想法大概是 SELECT something FROM Table1 WHERE id IN (SELECT id FROM Table2 WHERE ... )。

可惜上述語法是不行的 XD 要改成 JOIN 的做法:

SELECT something FROM Table1, (SELECT id FROM Table2) AS list WHERE Table1.id = list.id;

接著,偶爾會需要 pagination 的需求,加個 LIMIT 的用法,這時候又會想要回報全部有幾筆資料(對於一些搜尋引擎的設計,有些是採用預估的方式),以便前端可以估算有幾筆資料。

最簡單的解法是再用一個 SQL Query 去問 Table2 的 id 資料,但想要更快一點,就來試試 MySQL User-Defined Variables 吧!

SELECT something, @n AS total FROM Table1, (SELECT id, CASE WHEN @n > 0 THEN @n := @n + 1 ELSE @n := 1 END AS n FROM Table2, (SELECT @n := 0) AS init) AS list WHERE Table1.id = list.id;

如此一來,結果都會有個 total 筆數跟著,雖然仍不夠好,但也不錯啦 XD  而搭配 LIMIT OFFSET,COUNT 時,total 的資訊是來自掃 Table2 的資料,所以也能正常顯示:

SELECT something, @n AS total FROM Table1, (SELECT id, CASE WHEN @n > 0 THEN @n := @n + 1 ELSE @n := 1 END AS n FROM Table2, (SELECT @n := 0) AS init) AS list WHERE Table1.id = list.id LIMIT 0,10;

[C++] 使用 PCRE、RE2 進行 Match all (如同 PHP preg_match_all 效果) @ Ubuntu 14.04

對 PHP 來說:

$ vim t.php
<?php
$data = "..<a href='...'>...</a>.."; //file_get_contents(...);
if (preg_match_all("#<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>#", $data, $matches) )
        print_r($matches);
$ php t.php
Array
(
    [0] => Array
        (
            [0] => <a href='...'>...</a>
        )

    [1] => Array
        (
            [0] => ...
        )

    [2] => Array
        (
            [0] => ...
        )

)


對 PCRE 來說:

$ vim pcre_test.cpp
#include <pcre.h>
#include <iostream>

int main() {

const char *error;
int erroroffset;
pcre *preg_pattern_a_tag = pcre_compile("<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>", PCRE_MULTILINE, &error,  &erroroffset, NULL);

if (!preg_pattern_a_tag) {
std::cout << "ERROR\n";
return -1;
}

std::string raw = "..<a href='...'>...</a>..";

unsigned int offset = 0;
unsigned int len = raw.size();
int matchInfo[3*2] = {0};
int rc = 0;

while (offset < len && (rc = pcre_exec(preg_pattern_a_tag, 0, raw.c_str(), len, offset, 0,  matchInfo, sizeof(matchInfo))) >= 0) {
for (int n=0; n<rc ; ++n) {
int data_length = matchInfo[2*n+1] - matchInfo[2*n];
std::cout << "Found:[" << raw.substr(matchInfo[2*n], data_length) << "]\n";
}
offset = matchInfo[1];
}
return 0;
}
$ g++ -std=c++11 pcre_test.cpp -lpcre
$ ./a.out
Found:[<a href='...'>...</a>]
Found:[...]
Found:[...]


對 RE2 來說:

$ vim re2_test.cpp
#include <re2/re2.h>
#include <iostream>

int main() {

//RE2 preg_pattern_a_tag("<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>", RE2::Latin1);
RE2 preg_pattern_a_tag("<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>");

std::string raw = "..<a href='...'>...</a>..";

re2::StringPiece result_a_href, result_a_body;

while(RE2::PartialMatch(raw, preg_pattern_a_tag, &result_a_href, &result_a_body)) {
std::cout << "result_a_href:[" << result_a_href << "]\n";
std::cout << "result_a_body:[" << result_a_body << "]\n";
raw = result_a_body.data();
}
return 0;
}
$ g++ -std=c++11 re2_test.cpp /path/libre2.a -lpthread
$ ./a.out
result_a_href:[...]
result_a_body:[...]