顯示具有 api 標籤的文章。 顯示所有文章
顯示具有 api 標籤的文章。 顯示所有文章

2024年8月7日 星期三

Python 開發筆記 - 使用 Nginx / WSGI / Gunicorn / Flask 進行 Python API 服務的上線整合 @ Ubuntu

近期工作上在 node.js 服務上,多開了一個 python api 服務做整合應用,目前先試著混合架構來擠擠機器資源而不是 micorservice 架構。

實作就是 Nginx 擋在前面,接著有些 requests 交給 node.js 運作,有些 requests 交給 python api 服務,整體上就是透過 Proxy Pass 架構:

$ cat /etc/nginx/conf.d/service.conf | grep location
    location / {

    location /node/ { rewrite ^/node/(.*)$ /$1 break; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; proxy_http_version 1.1; proxy_read_timeout 60; proxy_pass http://localhost:3000; }

    location /python/ { rewrite ^/python/(.*)$ /$1 break; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_redirect off; proxy_http_version 1.1; proxy_read_timeout 60; proxy_pass http://unix:/var/run/service-py.sock; }

其中 node.js 是跑在 3000 port 服務,而 python api 跑在 unix:/var/run/service-py.sock ,上述 Nginx 設定檔剛好可以作為筆記,屬於兩種不同的設計方式,此外,這邊收到 requests 導向到 node.js 或 python api 時,都會刻意再去掉一些 prefix ,讓後面的服務開發比較多彈性。

回到 python api ,此次採用 Flask framework,他的運行很簡單:

```
% cat app.py
...
if __name__ == '__main__':
    app.run(host='localhost', port=3001)
```

直接執行法:

% python3 app.py 
 * Serving Flask app 'app' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on http://localhost:3001
Press CTRL+C to quit

使用 Flask 指令執行:

$ FLASK_APP=app.py flask run --host 0.0.0.0 --port 3001
 * Serving Flask app 'app.py' (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:3001
 * Running on http://x.x.x.x:3001
Press CTRL+C to quit

最後則是為了穩定性,採用 Nginx + WSGI 整合方式,透過 Gunicorn 來工作:

$ cat wsgi.py 
from app import app

if __name__ == "__main__":
    app.run()

$ sudo gunicorn --workers 4 --bind unix:/var/run/service-py.sock -m 777 wsgi:app
[2024-08-07] [1701223] [INFO] Starting gunicorn 20.0.4
[2024-08-07] [1701223] [INFO] Listening at: unix:/var/run/service-py.sock (1701223)
[2024-08-07] [1701223] [INFO] Using worker: sync
[2024-08-07] [1701225] [INFO] Booting worker with pid: 1701225
[2024-08-07] [1701226] [INFO] Booting worker with pid: 1701226
[2024-08-07] [1701227] [INFO] Booting worker with pid: 1701227
[2024-08-07] [1701228] [INFO] Booting worker with pid: 1701228

如果要把運行也包裝系統指令方便處理,就只需:

$ cat /etc/systemd/system/my-py.service 
[Unit]
Description=my-py-service
After=network.target

[Service]
Type=simple
User=root
WorkingDirectory=/path/project
#ExecStart=/usr/bin/python3 app.py
#Environment=FLASK_APP=app.py
#ExecStart=/usr/bin/python3 -m flask run --port 3001 --host 0.0.0.0
ExecStart=/usr/bin/gunicorn --workers 4 --bind unix:/var/run/service-py.sock -m 777 wsgi:app
Restart=on-failure

[Install]
WantedBy=multi-user.target

後續就可以透過以下方式管理:

$ sudo systemctl status my-py.service
$ sudo systemctl stop my-py.service
$ sudo systemctl start my-py.service
$ sudo systemctl restart my-py.service

2023年10月4日 星期三

Python 開發筆記 - 使用 Gitlab API 列出 Projects、Branches 和 Commits

剛好碰到需要製作 Gitlab 整合類服務,開發完後就把很基礎的項目整理成小工具來支援 JSON 輸出,未來可以跟 jq 做一堆連續技:

用法:

% virtualenv venv
created virtual environment CPython3.11.5.final.0-64 in 120ms
...

% source venv/bin/activate
(venv) % 
(venv) % pip3 install gitlab-api-helper
(venv) % ./venv/bin/gitlab-api-helper 
{
    "info": [
        "config file not found: .env",
        "--api empty"
    ],
    "result": null,
    "version": "1.0.0"
}
usage: gitlab-api-helper [-h] [--apiSetupConfig APISETUPCONFIG]
                         [--apiAccessToken APIACCESSTOKEN] [--api API]
                         [--sinceType {day,week,month}]
                         [--sinceNumber SINCENUMBER]
                         [--lookup {project,branch,commit}]
                         [--lookupProjectID LOOKUPPROJECTID]
                         [--lookupBranch LOOKUPBRANCH]

A Simple Tool for Gitlab API Usage

options:
  -h, --help            show this help message and exit
  --apiSetupConfig APISETUPCONFIG
                        Read Default Info from config file
  --apiAccessToken APIACCESSTOKEN
                        Using Gitlab API with private_token.
  --api API             Gitlab API URL
  --sinceType {day,week,month}
                        commit date range type
  --sinceNumber SINCENUMBER
                        commit date range value
  --lookup {project,branch,commit}
                        query result
  --lookupProjectID LOOKUPPROJECTID
                        Gitlab Project ID
  --lookupBranch LOOKUPBRANCH
                        Branch Name

剩下就看 github.com/changyy/gitlab-api-helper 或 pypi.org/project/gitlab-api-helper/ 的簡介囉

2021年10月26日 星期二

Python 筆記 - 使用 AdSense / AdMob API 製作 Dashboard

約莫經營流量變現三年多,大部分的工作都是靠 adsense/admob 網站就夠用了,但如果要整合到其他儀表板的話,就必須靠 AdMob/AdSense API 來撈點資料。

原先一直只靠 AdSense API 做事,但是舊版 AdSense API 在 2021-10-12 下線了,接著新版 AdSense API 就不在有 AdMob 數據,因此需要分開存取。

筆記瑣事:
  • 使用 Python3 開發,並安裝套件相關套件
    • google-api-python-client
    • google-auth-oauthlib
  • AdSense 跟 AdMob 授權的 Scope 是分開的
    • 'https://www.googleapis.com/auth/admob.readonly'
    • 'https://www.googleapis.com/auth/adsense.readonly'
  • Google 專案必須分別啟用 AdSense Management api 跟 AdMob api 使用權
  • Google 專案的憑證,只需使用 "OAuth 2.0 用戶端 ID" ,並採用 "電腦版應用程式" 即可
  • AdSense 跟 AdMob API 輸出的 DATE 欄位的格式不同
    • "2021-10-26"
    • "20211026"
因此,若要符合之前舊版 Adsense 的輸出,需要分別使用 AdSense api v2 跟 AdMob API v1 來整理數據,例如先將 AdMob 收益用日期作為關鍵字紀錄,接著輸出 AdSense 來源時,在計算當日營收時,再順便查表累計 AdMob 收益。

完整程式碼:

    result = service.accounts().reports().generate(
        account=account_id,
        dateRange='CUSTOM',
        startDate_year=startDate['year'],
        startDate_month=startDate['month'],
        startDate_day=startDate['day'],
        endDate_year=endDate['year'],
        endDate_month=endDate['month'],
        endDate_day=endDate['day'],
        metrics=output['data']['metrics'],
        dimensions=output['data']['dimensions'],
        currencyCode="USD",
        reportingTimeZone="ACCOUNT_TIME_ZONE",
    ).execute()

AdMob API 片段程式碼:

    result = service.accounts().networkReport().generate(
        parent=account_id,
        body= {
            'reportSpec': {
                'dateRange' : {
                    'startDate': output['data']['startDate'],
                    'endDate': output['data']['endDate'],
                },
                'dimensions': output['data']['dimensions'],
                'metrics': output['data']['metrics'],
                'localizationSettings': {
                    'currencyCode': 'USD',
                    'languageCode': 'en-US',
                },
            },
        },
    ).execute()

2020年3月21日 星期六

[C] 嵌入式 Web Server + cgi-bin + JSON = thttpd + cgic + cJSON 開發筆記 @ macOS

記得七八年前,是在嵌入式平台上寫 C++ 的 CGI 練習,現在則是在試著寫 C 語言。以下幾個套件還滿夠用的,筆記一下:
而 cgic 跟 cJSON 都是很精簡的單一檔案,非常方便,建議直接看他們的 header file ,就可以很快了解其概念,其中 cJSON 適合看 README ,裡頭提到了需要面對的記憶體管理。

以下是 CMake 編譯範例:

# https://raw.githubusercontent.com/boutell/cgic/master/cgic.h
# https://raw.githubusercontent.com/boutell/cgic/master/cgic.c
set(CGIC_VERSION "cgic-2.07")

include_directories(deps/${CGIC_VERSION}/include)
add_library(cgic
        deps/${CGIC_VERSION}/src/cgic.c
)

# https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.12/cJSON.h
# https://raw.githubusercontent.com/DaveGamble/cJSON/v1.7.12/cJSON.c
set(CJSON_VERSION "cjson-1.7.12")
include_directories(deps/${CJSON_VERSION}/include)
add_library(cjson
deps/${CJSON_VERSION}/src/cJSON.c
)


如此,後續要編譯時,就可以:

add_executable(study-app.cgi
        src/study-app.c
)
target_link_libraries(study-app.cgi
        cgic
        cjson
        curl
)


相關筆記在此: changyy/thttpd-cgi-study/blob/master/src/study-app.c

2018年12月3日 星期一

Android 開發筆記 - Image Picker 與權限管理

在 Android 6.0 (API:23) 後,就算 AndroidManifest.xml 寫好要權限的部分,但實務上在進行時,依舊要主動要一次,讓用戶感受到真的要權限了:https://developer.android.com/training/permissions/requesting

做 Image Picker 時,若最終要讀取資料,那需要 android.permission.READ_EXTERNAL_STORAGE 權限,以下則是在 MainActivity 運行的範例:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest>


public class MainActivity extends AppCompatActivity {
private static final int REQUEST_SELECT_VIDEO = 1;
private static final int REQUEST_EXTERNAL_STORAGE = 2;
private static String[] PERMISSIONS_STORAGE = {
Manifest.permission.READ_EXTERNAL_STORAGE,
};

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE);
} else {
request_pick();
}
}

void request_pick() {
final Uri mUri = android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
final Intent mIntent = new Intent(Intent.ACTION_PICK, mUri);
final PackageManager mPackageManager = getPackageManager();
List<ResolveInfo> list = mPackageManager.queryIntentActivities(mIntent, PackageManager.MATCH_DEFAULT_ONLY);
if (list.size() > 1) {
startActivityForResult(
Intent.createChooser(
new Intent(Intent.ACTION_PICK, mUri), "選取圖片"
),
REQUEST_SELECT_VIDEO
);
} else {
startActivityForResult(mIntent, REQUEST_SELECT_VIDEO);
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == RESULT_OK) {
if (requestCode == REQUEST_SELECT_VIDEO) {
// ...
// 在處理檔案讀取時,缺少 android.permission.READ_EXTERNAL_STORAGE 會造成 IOException:
// open failed: EACCES (Permission denied)
// ...
}
}
}
}

2018年10月22日 星期一

[PHP] Dashboard 開發筆記 - 使用 Google Analytics 和 Google Adsense API 追蹤數據

AU_GroupByCountry

大概三個禮拜前已經做好了一些研究,一直偷懶沒寫點紀錄 :P 程式碼片段:

https://github.com/changyy/TrackingLibrary/tree/master/php/Codeigniter-3.1.9/application/libraries

取得 Google Analytics 數據 - Google_Service_Analytics 用法:

$this->load->library('GaQuery_lib', array(
'vendor/autoload.php' => 'vendor/autoload.php',
'access_token' => $access_token,
));

$ret = $output = $this->gaquery_lib->query(
$this->ga_profile_id,
'20180901', // $this->input->get_post('date_start'),
'20190930', // $this->input->get_post('date_end'),
'ga:users,ga:newUsers', // $this->input->get_post('metrics'),
[
'dimensions' => 'ga:countryIsoCode',
'sort' => '-ga:users'
], // $options
);


取得 Google Adsense 數據 - Google_Service_AdSense 用法:

$this->load->library('AdsenseQuery_lib', array(
'vendor/autoload.php' => 'vendor/autoload.php',
'access_token' => $access_token,
));

$ret = $output = $this->adsensequery_lib->getReport(
'20180901', // $this->input->get_post('date_start'),
'20190930', // $this->input->get_post('date_end'),
[
'dimension' => ['AD_UNIT_CODE'],
'metric' => [
'AD_REQUESTS',
'AD_REQUESTS_COVERAGE',
'AD_REQUESTS_CTR',
'CLICKS',
'COST_PER_CLICK',
'AD_REQUESTS_RPM',
'EARNINGS',
]
]
);


如此一來,光 GA 就可以組出不錯的數據,例如取出 60 天的數據,可以做出近30天與上個近30天的變化量,得知活躍用戶是否有對應成長,讓人決定要做哪個國家的客製化服務。

2017年3月7日 星期二

使用 Facebook Graph API 取得使用者資料:性別、年紀、地區、教育來做問卷調查

幾年前開發 sign in via Facebook,然後就停擺了好一陣子,最近市場研究想做個問卷,才發現當年收集的資料不怎齊全 XD 再加上 Facebook Graph api 也改版幾次,並且越來越重視個資保護,有很多權限必須額外取得才行。

請參考 https://developers.facebook.com/docs/graph-api/reference/user 定義最準。

而一般問卷常用的年紀,在 Facebook 只有 13/18/21 這三種數值可用,要再更仔細去請用 FB ads 去發了;而教育資訊跟地區資訊也得額外要求額外的權限 user_education_history 跟 user_location 才能得到

最後,在用 graph api 來取得這些資訊吧:

/me?fields=id,name,email,education,gender,location,age_range
{
  "id": "##",
  "name": "Yuan-Yi Chang",
  "email": "@gmail.com",
  "education": [
    {
      ...
      "type": "High School",
      ...
    },
    {
      ...
      "type": "College",
      ...
    },
    {
      ...
      "type": "Graduate School",
      ...
    }
  ],
  "gender": "male",
  "location": {
    "id": "110765362279102",
    "name": "Taipei, Taiwan"
  },
  "age_range": {
    "min": 21
  }
}


預計先這般儲存吧:

age_range => 13/18/21
gender => None/Male/Female/Unisex
education => None/HighSchool/College/GraduateSchool

2017年3月2日 星期四

[PHP] Facebook Graph API v2.2 升級提醒 - CodeIgniter 2.x 與 Facebook PHP SDK v5

這幾天一堆 fb app 被這種信轟炸:YourFBApp 的新開發人員重要通知,簡言之就是 facebook graph 舊版 api 即將在 2017/03/25 失效,請立即更新。

Facebook 開放平台變更紀錄 - https://developers.facebook.com/docs/apps/changelog

追了一下,主因是很多手上的服務是在 2014 年底開發,當時就是用 v2.2 graph api 沒錯,關鍵之處可以用搜尋:

facebook-php-sdk-v4 $ grep -r "v2.2" *
src/Facebook/FacebookRequest.php:  const GRAPH_API_VERSION = 'v2.2';


非常精準的地發現自己就是這個族群,也感謝 Facebook 賜予工作機會(誤),這樣每兩年就有新工作可以做,非常開心!接下來就是痛苦的開始,還是一口氣從 v4 升級到 v5 吧!

前提:

由於使用 PHP CodeIgniter 2.x 架構,再加上 HA 架構,因此需要處理 SESSION 的儲存機制,在此就用 DB 處理,而對 v4 SDK 中,是直接在 FacebookRedirectLoginHelper 改寫一份對於認證所需資料儲存的東西,但在 v5 架構就更漂亮了,可以把負責資料儲存的東西傳入,不需再改成 FacebookRedirectLoginHelper。若服務仍在單一機器上運行,可以略過此設計。

v4:

<?php
// FacebookRedirectLoginCIHelper
namespace Facebook;
use Facebook\FacebookRedirectLoginHelper;
class FacebookRedirectLoginCIHelper extends \Facebook\FacebookRedirectLoginHelper {
private $sessionPrefix = 'FBRLH_';
public function __construct($redirectUrl, $appId = null, $appSecret = null) {
parent::__construct($redirectUrl, $appId, $appSecret);
$this->ci = & get_instance();
}
protected function storeState($state) {
if ($this->ci->session->set_userdata($this->sessionPrefix . 'state', $state)) {
$this->state = $this->ci->session->set_userdata($this->sessionPrefix . 'state', $state);
return $this->state;
}
return NULL;
}
protected function loadState() {
return $this->state = $this->ci->session->userdata($this->sessionPrefix . 'state');
}
}

v5:

<?php
// FacebookSessionPersistentDataCIHandler
namespace Facebook\PersistentData;
use Facebook\Exceptions\FacebookSDKException;
class FacebookSessionPersistentDataCIHandler implements PersistentDataInterface {
protected $sessionPrefix = 'FBRLH_';
public function __construct($enableSessionCheck = true) {
$this->ci = & get_instance();
}
public function get($key) {
if ($this->ci)
return $this->ci->session->userdata($this->sessionPrefix . $key);
return null;
}
public function set($key, $value) {
if ($this->ci)
$this->ci->session->set_userdata($this->sessionPrefix . $key, $value);
}
}


初始化:

v4:

require 'path/facebook-php-sdk-v4/autoload.php';
use Facebook\FacebookRedirectLoginCIHelper;
use Facebook\FacebookRequest;
use Facebook\FacebookSession;
use Facebook\GraphUser;

FacebookSession::setDefaultApplication($this->config->item("api_key"), $this->config->item("secret_key"));

v5:

require 'path/php-graph-sdk-5.4.4/src/Facebook/autoload.php';
use Facebook\Facebook;
use Facebook\Authentication\AccessToken;
use Facebook\PersistentData\FacebookSessionPersistentDataCIHandler;
use Facebook\FacebookRequest;

$fb = new Facebook([
'app_id' => $this->config->item("api_key"),
'app_secret' => $this->config->item("secret_key"),
'persistent_data_handler' => new FacebookSessionPersistentDataCIHandler(),
//'default_graph_version' => 'v2.8',
]);


取得登入網址:

v4:

$helper = new FacebookRedirectLoginCIHelper($callback_url);
$login_url = $helper->getLoginUrl($this->config->item('fb_scope'));

v5:

$helper = $fb->getRedirectLoginHelper();
$login_url = $helper->getLoginUrl($callback_url, $this->config->item('fb_scope'));


登入完成取得 access_token:

v4:

$fb_session = $helper->getSessionFromRedirect();
$access_token = $fb_session->getAccessToken();

v5:

$accessToken = $helper->getAccessToken();


取得 longlived access_token:

v4:

$long_lived_token = $fb_session->getLongLivedSession()->getToken();

v5:
$long_lived_token = (string) $fb->getOAuth2Client()->getLongLivedAccessToken($access_token);


查詢個人資料:

v4:

$user_profile = (new FacebookRequest($fb_session, 'GET', '/me'))->execute()->getGraphObject(GraphUser::className());
$uid = $user_profile->getProperty('id');
$name = $user_profile->getProperty('name');
$email = $user_profile->getProperty('email');
$profile_url = $user_profile->getProperty('link');
if (empty($profile_url))
$profile_url = https://www.facebook.com/$uid";
$profile_image_link = "https://graph.facebook.com/$uid/picture";

v5:

$user_profile = $fb->get('/me', $accessToken)->getGraphNode();
$uid = $user_profile->getField('id');
$name = $user_profile->getField('name');
$email = $user_profile->getField('email');
$profile_url = $user_profile->getField('link');
if (empty($profile_url))
$profile_url = https://www.facebook.com/$uid";
$profile_image_link = "https://graph.facebook.com/$uid/picture";


從字串初始化並檢查 token 是否過期:

v4:

use Facebook\FacebookSession;

$session = new FacebookSession( $token );
return $session->validate();

v5:

return $fb->getOAuth2Client()->debugToken( $token )->getIsValid() == true;


api 查詢架構:

v4:

function query($token, $method, $api, $array_mode = true) {
if (!empty($token)) {
$session = new FacebookSession($token);
                 if (!$session->validate())
return false;
$response = ( new FacebookRequest( $session, $method, $api ) )->execute();
if ($array_mode)
return $response->getGraphObject()->asArray();
return $response->getGraphObject();
}
return false;
}

v5:

function _node_query($token, $method, $api, $array_mode = true) {
if (!empty($token)) {
try {
if (!strcasecmp($method, 'POST'))
return $array_mode ? $this->fb->post($api, $token)->getGraphNode()->asArray() : $this->fb->post($api, $token)->getGraphNode();

return $array_mode ? $this->fb->get($api, $token)->getGraphNode()->asArray() : $this->fb->get($api, $token)->getGraphNode();
} catch (Exception $e) {
var_dump($e);
}
}
return false;
}

function _edge_query($token, $method, $api, $array_mode = false) {
if (!empty($token)) {
try {
if (!strcasecmp($method, 'POST'))
return $array_mode ? $this->fb->post($api, $token)->getGraphEdge()->asArray() : $this->fb->post($api, $token)->getGraphEdge();

return $array_mode ? $this->fb->get($api, $token)->getGraphEdge()->asArray() : $this->fb->get($api, $token)->getGraphEdge();
} catch (Exception $e) {
var_dump($e);
}
}
return false;
}


取得使用者權限清單:

v4:

query($token, 'GET', '/me/permissions');

v5:

_edge_query($token, 'GET', '/me/permissions');


查詢朋友清單:

v4:

function get_friend_installed_app_list($token, &$have_next_page, $page = 1, $item_per_page = 25) {
$have_next_page = false;
$data = $this->query($token, 'GET', '/me/friends?fields=installed&limit='.($item_per_page).'&offset='.(($page - 1) * $item_per_page));
if(isset($data['data']) && ($cnt = count($data['data'])) > 0) {
if (isset($data['paging']) && is_object($data['paging']) && property_exists($data['paging'], 'next')) {
if ($cnt >= $item_per_page)
$have_next_page = true;
}
$output = array();
foreach($data['data'] as $user)
if (is_object($user) && property_exists($user, 'id'))
array_push($output, $user->id);
return $output;
}
return false;

}

v5:

function get_friend_installed_app_list($token, &$have_next_page, $page = 1, $item_per_page = 25) {
$have_next_page = false;
$data = $this->_edge_query($token, 'GET', '/me/friends?fields=installed&limit='.($item_per_page).'&offset='.(($page - 1) * $item_per_page), false);
$items = $data->asArray();
if(is_array($items) && ($cnt = count($items)) > 0) {
$paging = $data->getMetaData();
if (isset($paging['paging']) && isset($paging['paging']['next']))
if ($cnt >= $item_per_page)
$have_next_page = true;
$output = array();
foreach($items as $user)
if (isset($user['id']))
array_push($output, $user['id']);
return $output;
}
return false;
}


收工!

2016年6月23日 星期四

[PHP] 使用 Google api client 操作 Google Spreadsheet API (Google Docs - Excel)

最近在整理資料,由於使用情境尚未明朗,想了一下就把資料塞到 Excel 來呈現,要排序、輸出跟轉 PDF 檔都 ok ,再加上有定期更新的需求,就來找一下 Google Spreadsheet API 來用用:https://developers.google.com/sheets/ ,程式擺在:

https://github.com/changyy/codeigniter-library-google-spreadsheet

由於同事比較熟 PHP ,就繼續挑 PHP 路線了 :P 沒想到 PHP 相關的 library 滿少的,大概都推薦 github.com/asimlqt/php-google-spreadsheet-client 這套了,但他採用的 API 所實作出來的有些侷限,但已經可以滿足八成的需求了!

最後為了達成自己想幹的事,在 k 完文件跟摸索後,還是自己下海包一份當作筆記(實在是官方文件都沒寫很清楚,只能自己追細節),包括的功能:

  • 支援單獨使用,以及和 PHP CodeIgniter 合用
  • 使用 composer.json 維護相依性:只有使用 google/apiclient: https://github.com/google/google-api-php-client
  • 使用  Google Spreadsheet API v4
  • 取得 Spreadsheet 清單 (使用 v3 API)
  • 建立 Spreadsheet
  • 修改 Spreadsheet name
  • 建立 Worksheet
  • 刪除 Worksheet
  • 可對 Worksheet 凍結視窗(frozenRow/frozenColumn)
  • 更新 Cells 資料
  • 搭配 Web server (如 CodeIgniter Controller 架構),可以完成 Google OAuth 流程,取得 Access Token 跟 Refresh Token
  • 搭配 Google OAuth offline 權限,可以自動 renew access_token

整體上,若不需要建立 Spreadsheet 跟 frozenRowColumn 的功能,建議直接用 asimlqt/php-google-spreadsheet-client 即可,我只先完成自己需要的部分。

2016年1月26日 星期二

使用微軟翻譯 API (Translator API - Microsoft Translator)

有個工作拖了很久,最後請同事協助處理了一下,順手筆記 :) 由於 App 撒出去世界各地,所以總會有好心人士留留言給予鼓勵,無奈很多語言都看不懂 XD 人工用 Google 翻譯當然夠用,但量大的時候,就希望能夠自動翻譯一下。

同事研究後,發現微軟翻譯 API 有免費使用額度(一個月200萬個字),所以就挑這來做了!

首先,註冊一下帳號:https://datamarket.azure.com/account ,填一下基本資料即可,一開始就會得到一組主要帳戶金鑰跟客戶識別碼,這組可以用來測試一些 API。

當要正式使用時,請至開發人員頁面,註冊一個應用程式(重導網址隨便輸入即可,在此不會用到),用新註冊到的金鑰跟客戶識別碼來工作即可。不然會收到類似的訊息:

TranslateApiExceptionMethod: Translate()Message: Cannot find an active Azure Market Place Translator Subscription associated with the request credentials.message

整個微軟翻譯 API 的使用流程:

Step 1: 透過 client_id 和 client_secret 換得 token

<?php

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://datamarket.accesscontrol.windows.net/v2/OAuth2-13/');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query(array(
'grant_type' => 'client_credentials',
'scope' => 'http://api.microsofttranslator.com'
'client_id' => 'YourAppID',
'client_secret' => 'YourAppSecretKey'
)));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$ret = curl_exec($ch);
curl_close($ch);
$token = json_decode($ret)->access_token;


Step 2: 帶著 token 跟待查的資料去問微軟翻譯 API

<?php

// https://msdn.microsoft.com/en-us/library/ff512421.aspx
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'http://api.microsofttranslator.com/v2/Http.svc/Translate?'.http_build_query(array(
'text' => '第二十四個夏天後',
'from' => null,
'to' => 'en',
)));
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Bearer '.$token,
));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$ret = curl_exec($ch);
curl_close($ch);
echo strip_tags($ret)."\n";


收工。

2015年8月21日 星期五

Javascript 開發筆記 - 透過 Google Maps Routing API 畫路徑 (Directions Service)

Google Maps Directions Service

使用 GPS Location 畫圖時,點太多太精細,容易畫很慢,點太少太粗糙時,發現畫出的線會很糟,例如截彎取直的現象。雖然正解應該是先把精細路徑畫出圖來用,例如每個 zoom level 都畫好路線圖,而非動態畫線、畫點。但就是想偷懶透過 Google Maps API 來畫圖 XD 因此就研究了一下 Directions Service:https://developers.google.com/maps/documentation/javascript/directions

2000個點

假設原路線共有 2000 個精細點,接著取 200 個 sample 點出來,接著想想看怎樣用 Google Maps API 來畫圖。如果單純把這兩百個點畫連線,就會出現截彎取直的現象,因此,想到 Google 導航,透過導航機制自動幫你把路線畫得服服貼貼,畢竟 GPS 收集時也還有誤差問題,真正要做的好,還得把 GPS 位置修到路徑上,有許多眉眉角角。

在使用 Google Maps Directions Service 時,要留意限制,例如有 OVER_QUERY_LIMIT、MAX_WAYPOINTS_EXCEEDED 等限制,不是你想做就可以做:

  • OVER_QUERY_LIMIT indicates the webpage has sent too many requests within the allowed time period.
  • MAX_WAYPOINTS_EXCEEDED indicates that too many DirectionsWaypoints were provided in the DirectionsRequest. The maximum allowed waypoints is 8, plus the origin, and destination. Google Maps API for Work customers are allowed 23 waypoints, plus the origin, and destination. Waypoints are not supported for transit directions.

因此,需要將 200 個點,切成若干次 request ,每一個 request 含頭尾只能有 8 個座標點,其中給予頭尾就可以導航了,而其中的 6 個中繼點只是可以精細導航路線不會飄走。大概是這樣的概念處理資料,200個點,以 8 個點為單位 = 25 次 requests ,會踩到 OVER_QUERY_LIMIT 。儘管可以再透過 call api 的頻率來解決,但我又偷懶把 200 個點限縮成 5~6 次的 requests,然後再連續的 GPS 中,透過平均個數抽 6 個點出來,讓一個 request 還是有八個點,只是更不精細。

總之,程式碼大概長這樣:

<html>
<head>
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?signed_in=true&callback=initMap" async defer></script>
<script>
var routes = [];
var points = [];
function initMap() {
var map = new google.maps.Map(document.getElementById('map'), {
zoom: 15,
center: {lat: 23.900422, lng: 121.603807}
});

var prev = null;
for (var i=0, cnt=data['results'].length; i<cnt ; ++i) {
var point = new google.maps.LatLng(data['results'][i]['location'].lat,data['results'][i]['location'].lng);
points.push({
location: point,
stopover: false, // 是否顯示
});

// add marker
if (0) {
var marker = new google.maps.Marker({
map: map,
position: point,
});
}
if (prev) {
// use multiple routes
routes.push({
origin: prev,
destination: point,
travelMode: google.maps.TravelMode.DRIVING
});
}
prev = point;
}

var directionsService = new google.maps.DirectionsService;
// multiple routes
// https://developers.google.com/maps/documentation/javascript/directions
if (0) {
for (var i=0, cnt=routes.length ; i<cnt ; ++i) {
console.log(i);
directionsService.route(routes[i], function(result, status) {
if (status == google.maps.DirectionsStatus.OK) {
var directionsDisplay = new google.maps.DirectionsRenderer({
suppressMarkers: true
});
directionsDisplay.setMap(map);
directionsDisplay.setDirections(result);
}
} );
}
}

// use multiple stops
// https://developers.google.com/maps/documentation/javascript/directions#Waypoints
var max_steps = 36;
for (var i=0, cnt=points.length; i < cnt ; i += max_steps) {
var stops = []; // max should be 8
var next_stop = Math.floor(max_steps / (8-2) );
for (var j=i+next_stop ; j<(max_steps - next_stop) && j < (cnt - 1) ; j += next_stop)  // 去頭去尾, 頭擺在 origin
stops.push(points[j]);
var request = {
origin: points[i].location,
destination: i+(max_steps - 1) < cnt ? points[i+max_steps-1].location : points[cnt-1].location,
waypoints: stops,
travelMode: google.maps.TravelMode.DRIVING,
};
directionsService.route(request, function(result, status) {
if (status == google.maps.DirectionsStatus.OK) {
var directionsDisplay = new google.maps.DirectionsRenderer({
suppressMarkers: true // 單純畫路線,不要顯示 marker
});
directionsDisplay.setMap(map);
directionsDisplay.setDirections(result);
} else {
console.log( status ); // OVER_QUERY_LIMIT, MAX_WAYPOINTS_EXCEEDED
}
} );
}
}

var data =
{
  "results": [
      {
         "elevation" : 17.00912857055664,
         "location" : {
            "lat" : 23.900397,
            "lng" : 121.603762
         },
         "resolution" : 610.8129272460938
      },
      {
         "elevation" : 20.88261413574219,
         "location" : {
            "lat" : 23.898869,
            "lng" : 121.603903
         },
         "resolution" : 610.8129272460938
      },
      {
         "elevation" : 22.29166030883789,
         "location" : {
            "lat" : 23.896661,
            "lng" : 121.60384
         },
         "resolution" : 610.8129272460938
      },
      {
         "elevation" : 20.24537467956543,
         "location" : {
            "lat" : 23.895185,
            "lng" : 121.60387
         },
         "resolution" : 610.8129272460938
      },
      {
         "elevation" : 30.49811744689941,
         "location" : {
            "lat" : 23.891678,
            "lng" : 121.603371
         },
         "resolution" : 610.8129272460938
      },
      // ...
   ]
};
</script>
</head>
<body>
<div id="map" style="width:100%; height:100%;"></div>
</body>
</html>

2015年5月22日 星期五

Google API 筆記 - 使用 Google OAuth 2.0 API @ Ubuntu 14.04, PHP 5.5


準備替服務整合 Google Plus 登入方式,因此研究了一下 Google OAuth 2.0 API 的用法:
  • 在 Google Developer Console 建立一個 Google Project
  • 開啟此 Google Project 的 OAuth 使用,並設置"重新導向 URI",此處 URI 雖然不支援萬用符號,但支援輸入多筆,還算夠用。
整個過程下來,得到 client_id、client_secret 兩筆資料,如此一來就可以進行 OAuth 使用:

先組出 Google OAuth 所需的網址提供使用者授權,需要填寫 scope, response_type, redirect_uri, access_type, approval_prompt, client_id,例如:

$init_url = 'https://accounts.google.com/o/oauth2/auth?'. http_build_query( array(
'scope' => 'email profile',
'response_type' => 'code',
'redirect_uri' => $google_oauth_callback_url,
'access_type' => 'offline',
'approval_prompt' => 'force',
'client_id' => $google_project_client_id,
));
header('Location: '.$init_url);


接著,在 callback_url 的網頁中,接了 Google OAuth 回傳的 code 後,從 server site 發 requests 進行處理:

$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://www.googleapis.com/oauth2/v3/token');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query( array(
'client_id' => $google_project_client_id,
'client_secret' => $google_project_secret_key,
'code' => $google_ret_code,
'grant_type' => 'authorization_code',
'redirect_uri' => $google_oauth_callback_url,
)));
$ret = @json_decode(curl_exec($ch), true);


若一切順利的話,在 $ret 中,就可以拿到 access_token 了(此例需開啟 Google+ API)

$profile = @json_decode(file_get_contents('https://www.googleapis.com/plus/v1/people/me?'.http_build_query( array(
'access_token' => $ret['access_token']
))), true);

print_r($profile);


最後,若測試完後,想要替自己用的 Google account 刪除 app 授權的話,可以這邊刪除:

https://security.google.com/settings/security/permissions?pli=1

2015年5月19日 星期二

透過 jq, sed, grep 從 wikipedia api 取出台灣地區鄉鎮市區列表 @ Ubuntu 14.04

靈光一想,想到撈一下台灣各地區,看到了這個:wiki 中華民國臺灣地區鄉鎮市區列表

http://zh.wikipedia.org/wiki/%E4%B8%AD%E8%8F%AF%E6%B0%91%E5%9C%8B%E8%87%BA%E7%81%A3%E5%9C%B0%E5%8D%80%E9%84%89%E9%8E%AE%E5%B8%82%E5%8D%80%E5%88%97%E8%A1%A8

接著想抓個鄉鎮對應表,就想起 wiki api:

http://zh.wikipedia.org/w/api.php?action=query&titles=%E4%B8%AD%E8%8F%AF%E6%B0%91%E5%9C%8B%E8%87%BA%E7%81%A3%E5%9C%B0%E5%8D%80%E9%84%89%E9%8E%AE%E5%B8%82%E5%8D%80%E5%88%97%E8%A1%A8&prop=revisions&rvprop=content&format=json

接著,再搭配 jq、sed、grep 縮一下資料量:

$ curl 'http://zh.wikipedia.org/w/api.php?action=query&titles=%E4%B8%AD%E8%8F%AF%E6%B0%91%E5%9C%8B%E8%87%BA%E7%81%A3%E5%9C%B0%E5%8D%80%E9%84%89%E9%8E%AE%E5%B8%82%E5%8D%80%E5%88%97%E8%A1%A8&prop=revisions&rvprop=content&format=json' | jq  '.query.pages[].revisions[0]|to_entries[0].value' | sed 's/\\n/\n/g' | grep -P '<small>(.*)</small> \[\[(.*)\]\]' -o | sed 's/<small>\(.*\)<\/small> \[\[\(.*\)\]\]/\1\t\2/g' | sed 's/\(.*\)\t\(.*\)|\(.*\)/\1\t\3/g'

成果:

基隆市 仁愛區
基隆市 中正區
基隆市 信義區
基隆市 中山區
基隆市 安樂區
基隆市 暖暖區
基隆市 七堵區
臺北市 中正區
臺北市 大同區
臺北市 中山區
臺北市 松山區
臺北市 大安區
臺北市 萬華區
臺北市 信義區
臺北市 士林區
臺北市 北投區
臺北市 內湖區
臺北市 南港區
臺北市 文山區
新北市 板橋區
新北市 新莊區
新北市 中和區
新北市 永和區
新北市 土城區
新北市 樹林區
新北市 三峽區
新北市 鶯歌區
新北市 三重區
新北市 蘆洲區
新北市 五股區
新北市 泰山區
新北市 林口區
新北市 淡水區
新北市 金山區
新北市 八里區
新北市 萬里區
新北市 石門區
新北市 三芝區
新北市 瑞芳區
新北市 汐止區
新北市 平溪區
新北市 貢寮區
新北市 雙溪區
新北市 深坑區
新北市 石碇區
新北市 新店區
新北市 坪林區
新北市 烏來區
桃園市 桃園區
桃園市 中壢區
桃園市 平鎮區
桃園市 八德區
桃園市 楊梅區
桃園市 蘆竹區
桃園市 大溪區
桃園市 龍潭區
桃園市 龜山區
桃園市 大園區
桃園市 觀音區
桃園市 新屋區
桃園市 復興區
新竹市 東區
新竹市 北區
新竹市 香山區
新竹縣 竹北市
新竹縣 竹東鎮
新竹縣 新埔鎮
新竹縣 關西鎮
新竹縣 湖口鄉
新竹縣 新豐鄉
新竹縣 峨眉鄉
新竹縣 寶山鄉
新竹縣 北埔鄉
新竹縣 芎林鄉
新竹縣 橫山鄉
新竹縣 尖石鄉
新竹縣 五峰鄉
宜蘭縣 宜蘭市
宜蘭縣 頭城鎮
宜蘭縣 礁溪鄉
宜蘭縣 壯圍鄉
宜蘭縣 員山鄉
宜蘭縣 羅東鎮
宜蘭縣 蘇澳鎮
宜蘭縣 五結鄉
宜蘭縣 三星鄉
宜蘭縣 冬山鄉
宜蘭縣 大同鄉
宜蘭縣 南澳鄉
苗栗縣 苗栗市
苗栗縣 造橋鄉
苗栗縣 西湖鄉
苗栗縣 頭屋鄉
苗栗縣 公館鄉
苗栗縣 銅鑼鄉
苗栗縣 三義鄉
苗栗縣 大湖鄉
苗栗縣 獅潭鄉
苗栗縣 卓蘭鎮
苗栗縣 竹南鎮
苗栗縣 頭份鎮
苗栗縣 三灣鄉
苗栗縣 南庄鄉
苗栗縣 後龍鎮
苗栗縣 通霄鎮
苗栗縣 苑裡鎮
苗栗縣 泰安鄉
臺中市 中區
臺中市 東區
臺中市 南區
臺中市 西區
臺中市 北區
臺中市 北屯區
臺中市 西屯區
臺中市 南屯區
臺中市 太平區
臺中市 大里區
臺中市 霧峰區
臺中市 烏日區
臺中市 豐原區
臺中市 后里區
臺中市 石岡區
臺中市 東勢區
臺中市 和平區
臺中市 新社區
臺中市 潭子區
臺中市 大雅區
臺中市 神岡區
臺中市 大肚區
臺中市 沙鹿區
臺中市 龍井區
臺中市 梧棲區
臺中市 清水區
臺中市 大甲區
臺中市 外埔區
臺中市 大安區
彰化縣 彰化市
彰化縣 員林鎮
彰化縣 和美鎮
彰化縣 鹿港鎮
彰化縣 溪湖鎮
彰化縣 二林鎮
彰化縣 田中鎮
彰化縣 北斗鎮
彰化縣 花壇鄉
彰化縣 芬園鄉
彰化縣 大村鄉
彰化縣 永靖鄉
彰化縣 伸港鄉
彰化縣 線西鄉
彰化縣 福興鄉
彰化縣 秀水鄉
彰化縣 埔心鄉
彰化縣 埔鹽鄉
彰化縣 大城鄉
彰化縣 芳苑鄉
彰化縣 竹塘鄉
彰化縣 社頭鄉
彰化縣 二水鄉
彰化縣 田尾鄉
彰化縣 埤頭鄉
彰化縣 溪州鄉
南投縣 南投市
南投縣 埔里鎮
南投縣 草屯鎮
南投縣 竹山鎮
南投縣 集集鎮
南投縣 名間鄉
南投縣 鹿谷鄉
南投縣 中寮鄉
南投縣 魚池鄉
南投縣 國姓鄉
南投縣 水-{里}-鄉
南投縣 信義鄉
南投縣 仁愛鄉
雲林縣 斗六市
雲林縣 斗南鎮
雲林縣 林內鄉
雲林縣 古坑鄉
雲林縣 大埤鄉
雲林縣 莿桐鄉
雲林縣 虎尾鎮
雲林縣 西螺鎮
雲林縣 土庫鎮
雲林縣 褒忠鄉
雲林縣 二崙鄉
雲林縣 崙背鄉
雲林縣 麥寮鄉
雲林縣 臺西鄉
雲林縣 東勢鄉
雲林縣 北港鎮
雲林縣 元長鄉
雲林縣 四湖鄉
雲林縣 口湖鄉
雲林縣 水林鄉
嘉義市 東區
嘉義市 西區
嘉義縣 太保市
嘉義縣 朴子市
嘉義縣 布袋鎮
嘉義縣 大林鎮
嘉義縣 民雄鄉
嘉義縣 溪口鄉
嘉義縣 新港鄉
嘉義縣 六腳鄉
嘉義縣 東石鄉
嘉義縣 義竹鄉
嘉義縣 鹿草鄉
嘉義縣 水上鄉
嘉義縣 中埔鄉
嘉義縣 竹崎鄉
嘉義縣 梅山鄉
嘉義縣 番路鄉
嘉義縣 大埔鄉
嘉義縣 阿里山鄉
臺南市 中西區
臺南市 東區
臺南市 南區
臺南市 北區
臺南市 安平區
臺南市 安南區
臺南市 永康區
臺南市 歸仁區
臺南市 新化區
臺南市 左鎮區
臺南市 玉井區
臺南市 楠西區
臺南市 南化區
臺南市 仁德區
臺南市 關廟區
臺南市 龍崎區
臺南市 官田區
臺南市 麻豆區
臺南市 佳里區
臺南市 西港區
臺南市 七股區
臺南市 將軍區
臺南市 學甲區
臺南市 北門區
臺南市 新營區
臺南市 後壁區
臺南市 白河區
臺南市 東山區
臺南市 六甲區
臺南市 下營區
臺南市 柳營區
臺南市 鹽水區
臺南市 善化區
臺南市 大內區
臺南市 山上區
臺南市 新市區
臺南市 安定區
高雄市 楠梓區
高雄市 左營區
高雄市 鼓山區
高雄市 三民區
高雄市 鹽埕區
高雄市 前金區
高雄市 新興區
高雄市 苓雅區
高雄市 前鎮區
高雄市 旗津區
高雄市 小港區
高雄市 鳳山區
高雄市 大寮區
高雄市 鳥松區
高雄市 林園區
高雄市 仁武區
高雄市 大樹區
高雄市 大社區
高雄市 岡山區
高雄市 路竹區
高雄市 橋頭區
高雄市 梓官區
高雄市 彌陀區
高雄市 永安區
高雄市 燕巢區
高雄市 田寮區
高雄市 阿蓮區
高雄市 茄萣區
高雄市 湖內區
高雄市 旗山區
高雄市 美濃區
高雄市 內門區
高雄市 杉林區
高雄市 甲仙區
高雄市 六龜區
高雄市 茂林區
高雄市 桃源區
高雄市 那瑪夏區
屏東縣 屏東市
屏東縣 潮州鎮
屏東縣 東港鎮
屏東縣 恆春鎮
屏東縣 萬丹鄉
屏東縣 崁頂鄉
屏東縣 新園鄉
屏東縣 林邊鄉
屏東縣 南州鄉
屏東縣 琉球鄉
屏東縣 枋寮鄉
屏東縣 枋山鄉
屏東縣 車城鄉
屏東縣 滿州鄉
屏東縣 高樹鄉
屏東縣 九如鄉
屏東縣 鹽埔鄉
屏東縣 里港鄉
屏東縣 內埔鄉
屏東縣 竹田鄉
屏東縣 長治鄉
屏東縣 麟洛鄉
屏東縣 萬巒鄉
屏東縣 新埤鄉
屏東縣 佳冬鄉
屏東縣 霧台鄉
屏東縣 泰武鄉
屏東縣 瑪家鄉
屏東縣 來義鄉
屏東縣 春日鄉
屏東縣 獅子鄉
屏東縣 牡丹鄉
屏東縣 三地門鄉
澎湖縣 馬公市
澎湖縣 湖西鄉
澎湖縣 白沙鄉
澎湖縣 西嶼鄉
澎湖縣 望安鄉
澎湖縣 七美鄉
花蓮縣 花蓮市
花蓮縣 鳳林鎮
花蓮縣 玉里鎮
花蓮縣 新城鄉
花蓮縣 吉安鄉
花蓮縣 壽豐鄉
花蓮縣 光復鄉
花蓮縣 豐濱鄉
花蓮縣 瑞穗鄉
花蓮縣 富里鄉
花蓮縣 秀林鄉
花蓮縣 萬榮鄉
花蓮縣 卓溪鄉
臺東縣 臺東市
臺東縣 成功鎮
臺東縣 關山鎮
臺東縣 長濱鄉
臺東縣 池上鄉
臺東縣 東河鄉
臺東縣 鹿野鄉
臺東縣 卑南鄉
臺東縣 大武鄉
臺東縣 綠島鄉
臺東縣 太麻里鄉
臺東縣 海端鄉
臺東縣 延平鄉
臺東縣 金峰鄉
臺東縣 達仁鄉
臺東縣 蘭嶼鄉
金門縣 金城鎮
金門縣 金湖鎮
金門縣 金沙鎮
金門縣 金寧鄉
金門縣 烈嶼鄉
金門縣 烏坵鄉
連江縣 南竿鄉
連江縣 北竿鄉
連江縣 莒光鄉
連江縣 東引鄉

2015年5月7日 星期四

Javascript 開發筆記 - 使用 Yuotube Player API for iframe Embeds 將 videos 串成 playlist

透過 Youtube Player API 可以動態把一堆 Youtube video 串成一個 playlist 來播放。網路上不少範例,但很少看到處理多則 video 的方式,有也是一則 video 搭配一個 player 架構,摸了一下終於搞懂其設計架構,片段程式碼:

<div id="player"></div>
<script>
var player;
var videos = ['LzHFD1sEqpE', 'iV8JDbtXZm4', '-sjfQWGkGF4'];
(function() {
if (videos.length) {
var tag = document.createElement('script');
tag.onload = function() {
YT.ready(function() {
player = new YT.Player('player', {
playerVars: {
playlist: videos.join(","),
}
});
});
}
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
}
})();
</script>


更多資訊請參考:https://developers.google.com/youtube/iframe_api_reference

2015年3月11日 星期三

Facebook 開發筆記 - Facebook app 無法取得真實的 account id

最近在開發 Facebook app 時,發現 Facebook API 已經有更新不少東西,其中有一項還滿不錯的,第三方 Facebook app 無法取得 Facebook user 的真實 ID 資訊,每個 app 會各別拿到一個,例如在 A app 跟 B app 詢問 graph api: /me 時,拿到的 id 會不一樣。然而,不一樣的 ID 卻仍可以一樣定位到同一個使用者。

這個最大的缺點是各個 Facebook app 沒有互通的資訊,但對於各個 facebook app 取得到的資訊卻還是一樣夠用。此外,可以用 www.facebook.com/id 去測試,可以發現仍可以定位到同一個 user,算是滿貼心的隱私設計。

https://developers.facebook.com/docs/graph-api/reference/v2.2/user
The id of this person's user account. This ID is unique to each app and cannot be used across different apps. Our upgrade guide provides more info about this.

2015年2月14日 星期六

Call Facebook API without token at Linode: error: message: Application request limit reached

昨晚開了一台 Linode Server,想說來研究一下 Facebook api,結果:

$ curl "https://graph.facebook.com/fql?q=SELECT%20total_count%20FROM%20link_stat%20WHERE%20url%20='www.google.com'"
{"error":{"message":"(#4) Application request limit reached","type":"OAuthException","code":4}}


不死心,發 ticket 抱怨一下,想說會不會前一個用戶太操,得到了免費換了 IP 。結果仍是一樣的。接著,想起有美國的機器,測了一下仍是如此。

幾番測試後,我猜 Linode IP Range 大概被關注很久,預設都不給用,看來人紅也是種困擾 XD

以上 command 在任何一台的機器,預期得到的正確結果為:

$ curl "https://graph.facebook.com/fql?q=SELECT%20total_count%20FROM%20link_stat%20WHERE%20url%20='www.google.com'"
{"data":[{"total_count":10366203}]}

2015年2月12日 星期四

AWS 筆記 - 使用 AWS Glacier 服務備份大量資料 @ Ubuntu 14.04

這個服務大概 2014 年夏天就開始關注並且在秋天嘗試使用,曾經也寫過一篇筆記但...最後沒發表 XD 實在是 AWS Glacier 如其名,大部份的指令真的像冰河緩慢移動 Orz 接著忙了就忘了。這次下定決心來寫一篇筆記。

簡言之,費用比 AWS S3 便宜,但是存取 AWS Glacier 的行為更像傳統磁帶模式,很多操作動作,會先給你一個 Job ID,還得手動用此 ID 去確認結果。例如想查閱已經有多少檔案,會發一個需求 Job ID 給你,而非馬上跟你說已經有多少檔案了。

整體上,要上傳資料一定要走 AWS API 方式,也就是到 AWS IAM 上建立一個 user,給予以下權限:
  • Amazon Glacier Full Access (另一個則是 Amazon Glacier Read Only Access)
  • Amazon SQS Full Access (下載檔案會用到, 若工作僅上傳則不需要, 沒有時的錯誤訊息: Access to the resource https://sqs.ap-northeast-1.amazonaws.com/ is denied)
  • Amazon SNS Full Access (下載檔案會用到, 若工作僅上傳則不需要, 沒有時的錯誤訊息: User: arn:aws:iam::####:user/#### is not authorized to perform: SNS:CreateTopic on resource)
我記得去年摸時,是使用 Amazon SimpleDB (sdb:*) 相關的權限,印象中跟 metadata 有關,不過現在卻沒查到了?! :P 接著,替使用者建立一組 access key/ secret key 來使用,而往後的上傳、下載等是靠這組 API KEY 工作。

此外,使用 AWS Glacier 前,要稍微瞭解一下操作方式
  • 不同的 Data Center 會因為電價等關係,所以儲存的費用不一樣。
  • 上傳資料前,除了要挑選 Data Center 外,還需要類似建立一個類似目錄的儲存單位 (Vault)
  • 除了網頁版 GUI 可以進行 Create Vault, Delete Vault 外,其餘都一律透過 API 進行,其中 Delete Vault 還必須確定裡頭沒其他檔案
  • 透過 API 操作時,需要的基本參數為 API KEY、Data Center (region/endpoint) 等資料
雖然用過 python 版工具 https://github.com/uskudnik/amazon-glacier-cmd-interface,這次就來用用 Java 版吧!https://github.com/MoriTanosuke/glacieruploader

建立 Vault (可透過 AWS Web Console)操作:

$ java -jar uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar -e "https://glacier.ap-northeast-1.amazonaws.com" -v changyy-vault -c
INFO  Using end point: https://glacier.ap-northeast-1.amazonaws.com
INFO  Creating vault changyy-vault...
INFO  Vault changyy-vault created. {Location: /##########/vaults/changyy-vault}
LastInventoryDate: null
NumberOfArchives: 0
SizeInBytes: 0
VaultARN: arn:aws:glacier:ap-northeast-1:##########:vaults/changyy-vault
VaultName: changyy-vault


上傳檔案(最後的 archive 接的資料就是該筆料的 ID):

$ java -jar uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar -e "https://glacier.ap-northeast-1.amazonaws.com" -v changyy-vault --upload ~/uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar
INFO  Using end point: https://glacier.ap-northeast-1.amazonaws.com
INFO  Starting to upload $HOME/uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar to vault changyy-vault...
INFO  Uploaded archive ########################################


切檔上傳,此例是 128MB 為單位,適合檔案很大的情境(Archive ID 接的資料就是該筆料的 ID):

$ java -jar uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar -e "https://glacier.ap-northeast-1.amazonaws.com" -v changyy-vault --upload ~/uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar --multipartupload ~/TargetBigFile --partsize 134217728
INFO  Using end point: https://glacier.ap-northeast-1.amazonaws.com/
INFO  Multipart uploading TargetBigFile to vault changyy-vault with part size 134217728 (128.00MB).
INFO  Upload ID (token): ################################################
INFO  Part 1/187 (bytes 0-134217727/*) uploaded, checksum: e19319be5e3c5d3f45a1ce7ef9ab3644b6933ec01c0754285babf45eb46b5b0b
...
INFO  Part 187/187 (bytes 24964497408-24993715534/*) uploaded, checksum: d0219f53b4f54431495211bfd8880fe52597354acc51aa077dcb67cacee69f53
INFO  Uploaded Archive ID: ################################################
INFO  Local Checksum: a1500723e11892cc2bb297d5d6f97a08035e30810ae0e3342184fbed4e2c2d5b
INFO  Remote Checksum: a1500723e11892cc2bb297d5d6f97a08035e30810ae0e3342184fbed4e2c2d5b
INFO  Checksums are identical, upload succeeded.


然而,剛上傳完是無法馬上下載的 :P 而想要查詢檔案列也是,必須先發一個"查詢列表"的工作(得到 Job ID),等工作結束後才能查詢到結果(取得檔案列表)

發出"查詢 Vault 檔案列表"的需求:

$ java -jar uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar -e "https://glacier.ap-northeast-1.amazonaws.com" -v changyy-vault -l
INFO  Using end point: https://glacier.ap-northeast-1.amazonaws.com
INFO  Starting inventory listing for vault changyy-vault...
INFO  Inventory Job created with ID
8KDBk2AS_9bYC8dIOBHxjitqxaLhEklXPfU6jZO-t-su3cp1k3NaHFIUpFaBiJDXDGFyzYyqaw-3MboGNlJ2W6kKDzmt


若這個 Vault 是剛建立的,還會有類似錯誤訊息:vaults/changyy-vault cannot be initiated yet, as Amazon Glacier has not yet generated an initial inventory for this vault.

取得"查詢 Vault 檔案列表"的結果:

$ java -jar uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar -e "https://glacier.ap-northeast-1.amazonaws.com" -v changyy-vault -l 8KDBk2AS_9bYC8dIOBHxjitqxaLhEklXPfU6jZO-t-su3cp1k3NaHFIUpFaBiJDXDGFyzYyqaw-3MboGNlJ2W6kKDzmt

若工作未做完,會顯示錯誤訊息:ERROR The job is not currently available for download。做完的話,會顯示清單,其中 Description 在這套 Java 工具下,會自動填寫上傳的檔名:

ARN: arn:aws:glacier:ap-northeast-1:##############:vaults/changyy-vault
------------------------------------------------------------------------------
Description:  uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar
Archive ID: q_SfW7MNmTE1_9xBbmzP5MvnEGYYmF8wCIe2aYs4_7NAXjn8fEO4nl97QZ-deJ_hDsKni7n5z0avn8gEdAnFfzMV4xE9FlF2Fr3UualyZj0b4LNSq9cENWYWoueSma9Kq8zGuwA9IA
CreationDate: 2015-02-07T11:17:36Z
Size: 19496656 (18.60MB)
SHA: d07cddbcbe3a83dba2b4ca654760bba4b77f92ae1ecc9f1fbffad337730fece0


下載檔案:

$ java -jar uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar -e "https://glacier.ap-northeast-1.amazonaws.com" -v changyy-vault --download q_SfW7MNmTE1_9xBbmzP5MvnEGYYmF8wCIe2aYs4_7NAXjn8fEO4nl97QZ-deJ_hDsKni7n5z0avn8gEdAnFfzMV4xE9FlF2Fr3UualyZj0b4LNSq9cENWYWoueSma9Kq8zGuwA9IA --target /tmp/test.jar
INFO  Using end point: https://glacier.ap-northeast-1.amazonaws.com
INFO  Downloading archive q_SfW7MNmTE1_9xBbmzP5MvnEGYYmF8wCIe2aYs4_7NAXjn8fEO4nl97QZ-deJ_hDsKni7n5z0avn8gEdAnFfzMV4xE9FlF2Fr3UualyZj0b4LNSq9cENWYWoueSma9Kq8zGuwA9IA from vault changyy-vault...
INFO Archive downloaded to /tmp/test.jar


整個過程不會馬上進入下載 Orz 例如我下載一個 52MB 的檔案,整個過程耗費 245 分鐘...絕對不是下載速度太慢,而是準備流程要等好一陣子。

刪除檔案:

$ java -jar uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar -e "https://glacier.ap-northeast-1.amazonaws.com" -v changyy-vault --delete q_SfW7MNmTE1_9xBbmzP5MvnEGYYmF8wCIe2aYs4_7NAXjn8fEO4nl97QZ-deJ_hDsKni7n5z0avn8gEdAnFfzMV4xE9FlF2Fr3UualyZj0b4LNSq9cENWYWoueSma9Kq8zGuwA9IA
INFO  Using end point: https://glacier.ap-northeast-1.amazonaws.com
INFO  Deleting archive q_SfW7MNmTE1_9xBbmzP5MvnEGYYmF8wCIe2aYs4_7NAXjn8fEO4nl97QZ-deJ_hDsKni7n5z0avn8gEdAnFfzMV4xE9FlF2Fr3UualyZj0b4LNSq9cENWYWoueSma9Kq8zGuwA9IA from vault changyy-vault...
INFO  Archive q_SfW7MNmTE1_9xBbmzP5MvnEGYYmF8wCIe2aYs4_7NAXjn8fEO4nl97QZ-deJ_hDsKni7n5z0avn8gEdAnFfzMV4xE9FlF2Fr3UualyZj0b4LNSq9cENWYWoueSma9Kq8zGuwA9IA deletion started from vault changyy-vault.


刪除 Vault:

$ java -jar uploader-0.0.8-SNAPSHOT-jar-with-dependencies.jar -e "https://glacier.ap-northeast-1.amazonaws.com" -v changyy-vault --delete-vault
INFO  Using end point: https://glacier.ap-northeast-1.amazonaws.com
INFO  Deleting vault changyy-vault...


若 vault 內還有檔案會有錯誤訊息:Vault not empty or recently written to: arn:aws:glacier:ap-northeast-1:############:vaults/changyy-vault。此外,若先去刪檔案,再來執行也會有一樣的問題,因為這是冰川啊 XD 刪檔也是慢慢地

整體心得,AWS Glacier 操作上真的很煩,因為太慢了。此外,也必須把那些 File Archive ID 記好,或是任何工作的 Job ID 記好,後續才能工作。有興趣可以在玩看看視窗介面,例如 CrossFTP 等,在操作檔案列表時,會跟你說要數小時(>5小時)才會得知結果,唯一的好處就是 CrossFTP 會幫你把一些 Job ID 記住吧