2019年12月31日 星期二

[眾籌] Puffin OS - Mobile Phone

Puffin OS Phone

這是一間規模已經大到破億 Mobile User 的公司,恰好一堆高手朋友都在裡頭發展著。

記得第一次接觸是 2009 還 2010 年,下一刻就 2013 年,再下一次就 2018 年。2018年底用著硬體廠的角色互動著,當時略知其發展,一年後就蹦出了這成品了。

此刻,依舊想著 server side rendering 的 remote browser 的需求是否強到持續養著一間公司呢?就像想著經濟學閒書所提,GDP 成長率差的國家,藉由新技術輸入,一口氣就躍進許多,而實力中等的國家隨著通訊技術的進步步調,通信品質的提升也不會慢太多。

無論如何,期待這間又猛又有夢想的公司,繼續茁壯著

2019年12月24日 星期二

[韓劇] Search: WWW / 검색어를입력하세요WWW / 請輸入檢索詞WWW


今年為了研究串流平台先後繳了學費給 NETFLIX、SPOTIFY和愛奇藝。在 NETFLIX 上看了一齣韓劇和台劇,在愛奇藝看了一部韓劇跟日劇,而大概快半年了,中午都用 SPOTIFY 聽聽別人講英文。

聊聊這次為何會在愛奇藝上看這部 Search:WWW ,主因是雙11才剛買了一年份 XD 其次是被這戲名給吸引到,看了下去才發現根本是跟自身領域高達七成相關,包含學生時代就在 Search Engine 的實驗室成長著、出了社會管了信箱就得處理離職人員信箱課題,在工作上除了負責網站服務外,近兩年又兼了流量變現任務,也體會了網站改版與流量變現議題的衝突。

一整個完完整整的就是工作縮影,而沒體驗到的大概是劇中都在開海神叉的車 XDD 人生啊...

不過 Search Engine 這話題基本上都已經算是15年前甚至20年前的故事了,而這戲的女主角們也都是略有年紀的,也稱得上是另一種老人回憶模式...

若時間往前轉到 15 年前、20年前,那一批當時隨網路服務蓬勃的前輩們,推論抓住機會的的確都開得起海神車,或是已經取得不少資源,可以做更多純興趣的投資了呢

2019年12月2日 星期一

FB 廣告投放筆記

FB廣告投放

2018 年夏天跑去聽了一門泰國老闆的經商之術,2019年秋天尾則是看到FB廣告大神的臨時加開課程就衝了一發 XD 本身不算這個領域的,但每次衝去接觸就有新的感觸,真的不錯!

這算是我第二次廣告投放聽課,上一次是在新創內部課程,距今大概 5 年了吧。聽這課程還滿不錯的,花個短短的四小時複習一下有哪些新東西,跟哪些容易遺忘的東西。剛好近兩年在搞流量變現,現在把思維反過來用。

這門課採用實務教學方式,過程沒有投影片,而是一頁頁瀏覽器直接操作 FB 廣告投放的後台,有許多一言難以道盡的心得,條列式如下:
  • FB User 興趣、地理位置、社群互動資訊,依舊是強項,這是 Google Ads 難以對抗的項目。消費產品的電商就是靠 FB Ads 啦
  • Online-Merge-Offline (OMO) 廣告投放早已很成熟,店家透過 Point of sale (POS) 店頭機系統可以將線下活動資料匯入到 FB 廣告後台,即可做到上線線下整合,例如對到店用餐的客戶投放線上購物的資訊,這兩者是互補而非互斥,例如實體餐飲店的體驗轉換成線上購買新式甜點等。而線下為了收集到客戶的 Email 和手機幾乎無所不用,辦個優惠/贈品活動都是(想起來那些送化妝贈品要留姓名電話的過程嗎?)
  • 不要期待自流量帶來的交易,且在 3C 產業不要再對已購買的再行銷。對既有客戶再行銷屬於耗材類的商品才有意義。
  • 不需太強調 A/B Test 的策略,實際上運作就會不斷追求更佳文案,自然會演化到最佳的路線(就像寫程式不見得一定不用靠 framework ,久了為了好維護就會自成 framework 的)
  • FB 為了用戶體驗也會評分你的文宣,依照評分機制評估該讓你的文宣給多少受眾觀看。鼓勵多和用戶互動,別人分享或留言都可以去按讚,大概可以參考蝦皮/全聯/政治/眾籌等其他小編文章,一樣有滿滿的互動。
  • 粗略引流成交的成本約 300-800 台幣,商品利潤有這等水準就該試試 FB Ads 買流量導購電商
  • 電商領域上,不要過度陷入到曝光價格/點擊價格的競局,重點還是訂單成交跟獲利
其中,我覺得最重要習得的策略是評估廣告文案的狀態。評估一則廣告文案是靠受眾佔比數量,若在條件中受眾的數量是 10萬,那實務上大概也只能接觸到 50% 而已,在接觸到 50% 之前,大概 5-10% 時就可以評估文案跟受眾的互動成績,以此決定是否要換文案或受眾。

這些評估策略的經驗還滿寶貴的,著重的不在廣告投放時間,而是更精準的受眾互動數據。

講師也提到一些概念,也驗證過去自己猜測的方向:
不要以為廣告一直打,用戶可能哪天想起會買
也就是廣告不是一直擺在那邊花錢,接著催眠自己哪天用戶會想起的。

實務上,有效的廣告投放可能一天內(最遲七天內)就會看到成效。而重複對一個用戶打廣告的方式也是用不同文案,永遠要避免同一個文案被一個用戶重複觀看,重複觀看可是傷了自己的荷包。

跟這類似的本業領域大概就是數據收集,不認真思考數據收集的目的/情境,就全部說收,殊不知網路儲存要費用、數據運算的算力也是要費用,搞到最後所有的收集就跟沒收集一樣,隨著時間而逝。當然也有反過來的,就是拼命想規劃情境收集,沒有明確的評鑑用途就淪為八卦資訊,數據價值又隨著時間衰減而無意義。

其中 A/B Test 就有那麼一點點味道,因為所有文案/策略都有時空背景,不見得訓練出某個經驗就可以一直沿用,搞到最後就是不需 A/B Test,永遠真槍實彈上場,心中自成一把尺可以評估該文案到底能不能再進步,因為久了就該略知哪些 CTR / CPC 是合理的、是可以再進步的。

這行業也是令人小抖,例如好不容易找到了高價值受眾,結果這款商品銷售完後,就得再打起精神再找下個受眾,這種沒完沒了的生活... XD 其實跟新創差不多,在時間限制下看誰先搶下市場。搶下市場或得資源後,就開始進入資本主義的保衛戰,建立更高的市場跨入門檻,一日復一日。這時就覺得當個投資人是多麼幸福的事,靠分散風險錢砸下去就好,不用浪費自己的青春年華啊。

2019年11月27日 星期三

[Go] 透過 Golang 操作系統內建 WebView @ macOS, WKWebView

最近在幫同事想想,到底有沒有什麼更方便的環境去協助同事分析網頁的組成。再加上自己想順便了解一下 golang 是如何跨平台的,就隨意找了這套:github.com/zserge/webview ,看了看,學到不少東西。

心得一:所謂的跨平台,是別人幫你做完所有苦力

在 webview.h 裡頭就定義清楚在 Windows / Linux / macOS 上所使用的 WebView 元件為何,超佛心的幫你串好常見的用法

心得二:macOS 的 WKWebView 就跟 iOS 內的沒啥兩樣

簡稱想要做到更細的東西,都得靠 hack ,黑來黑去好累啊。像是想要追蹤一個網頁形成過程到底用了哪些 resource ,於似乎想要追蹤所有 request ,接著又要想想該如何向 UIWebView 那麼方便查看,接著又要想想是不是要用 WKURLSchemeHandler ,又該怎樣把系統內定的 http/https 黑回來 NSURLProtocol 處理等等,還是要搞個 proxy mode 追蹤?

想著想著...啊不就換套 CEF 就好 XD

於是乎,我就放棄 zserge/webview 在 macOS 上操弄 WKWebView !不過也趁這個機會我練了一下怎樣從 C call ObjectiveC 或者說從 Go 怎樣操作 ObjectiveC,也是總緣份吧

這次操弄 zserge/webview 大概玩了:

- 欣賞 zserge/webview 大大怎樣提供跨平台呼叫瀏覽網頁的元件

- 從 C 操作 WKWebView 物件,接著再呼叫它的 methods 換掉 User-Agent 資訊

WEBVIEW_API void webview_set_user_agent(struct webview *w, const char *user_agent) {
  // https://developer.apple.com/documentation/webkit/wkwebview/1414950-customuseragent?language=objc
  objc_msgSend(w->priv.webview, sel_registerName("setCustomUserAgent:"), get_nsstring(user_agent));
}


- 從 C 操作 WKWebView 物件,接著再呼叫它的 methods 添加監聽 didStartProvisionalNavigation 和 didFinishNavigation

static void wk_webview_didStartProvisionalNavigation(id self, SEL cmd, id webView, id navigation) {
  // https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455621-webview?language=objc
  webview_print_log("at wk_webview_didStartProvisionalNavigation");
  //id absoluteString = objc_msgSend(objc_msgSend(webView, sel_registerName("URL")), sel_registerName("absoluteString"));
  webview_print_log( get_webview_current_url(webView) );
}

static void wk_webview_didFinishNavigation(id self, SEL cmd, id webView, id navigation) {
  // https://developer.apple.com/documentation/webkit/wknavigationdelegate/1455629-webview?language=objc
  webview_print_log("at wk_webview_didFinishNavigation");
}

// ...

  Class __WKNavigationDelegate = objc_allocateClassPair(
      objc_getClass("NSObject"), "__WKNavigationDelegate", 0);
  class_addProtocol(__WKNavigationDelegate,
                    objc_getProtocol("WKNavigationDelegate"));

// ...

  class_addMethod(
      __WKNavigationDelegate,
      sel_registerName(
          "webView:didStartProvisionalNavigation:"),
      (IMP)wk_webview_didStartProvisionalNavigation, "v@:@@");
  class_addMethod(
      __WKNavigationDelegate,
      sel_registerName(
          "webView:didFinishNavigation:"),
      (IMP)wk_webview_didFinishNavigation, "v@:@@");


收工!

2019年11月25日 星期一

jq 指令筆記 - 使用 to_entries 保留 key/value 資料,再用 select / index / match 過濾資料

人生就是有那種怪怪的堅持,明明寫個 php 或 python 就立刻可以解掉的需求,偏偏愛用 jq 來處理 XD

故事是來自於有個 key-value pair 的 json 資料:

% echo '{"A":{"field":"v1"},"B":{"field":"v2"}}' | jq ''
{
  "A": {
    "field": "v1"
  },
  "B": {
    "field": "v2"
  }
}


想要透過 jq 過濾時,也能保留 key 資料,這時就用 to_entries 來達成:

% echo '{"A":{"field":"v1"},"B":{"field":"v2"}}' | jq 'to_entries[]'
[
  {
    "key": "A",
    "value": {
      "field": "v1"
    }
  },
  {
    "key": "B",
    "value": {
      "field": "v2"
    }
  }
]

% echo '{"A":{"field":"v1"},"B":{"field":"v2"}}' | jq 'to_entries[]'
{
  "key": "A",
  "value": {
    "field": "v1"
  }
}
{
  "key": "B",
  "value": {
    "field": "v2"
  }
}


接著要再過濾指定欄位帶有 關鍵字 時,就靠 select 跟 index 來達成:

% echo '{"A":{"field":"v1"},"B":{"field":"v2"}}' | jq 'to_entries[] | select( .value.field | index("v2") >= 0 )'
{
  "key": "B",
  "value": {
    "field": "v2"
  }
}


此例是輸出 value.field 數值帶有 v2 關鍵字。而 index 之外的還有 match 等支援 regular expression 的用法,只是要用 !match 時有點卡卡串不太起來,就乾脆用 index 來處理。

2019年11月24日 星期日

[JS] Google Adsense 自動廣告 - 關閉 錨定廣告/重疊廣告 方式

前陣子跟 Google Adsense 的新加坡客戶經理聊過幾句,被她引導開啟了一些自動化廣告最佳化的項目。最近發現公司的網站出現 錨釘 廣告,想說是不是不小心誤上程式碼,最後想起應當是自動化廣告的機制。

對於一些頁面不想開啟置頂或置底的錨定廣告,可用以下方式關閉:

<script>
(adsbygoogle = window.adsbygoogle || []).push({
enable_page_level_ads: false
});
</script>


剛好去年是要手動靠 enable_page_level_ads: true 啟用的,一時之間還以為實驗的程式碼不小心發布了 :P 因為去年研究時感受到他很容易覆蓋掉一些網路應用的版面,如導覽功能,因此決議不讓他上線。

2019年11月20日 星期三

[PHP] OAuth / Sign in with Apple JS - 使用 Apple JS SDK 讓網站支援 Apple ID 登入

SignInWithApple00

最近幫看同事串 Sign In with Apple 好像有很多不順的地方,拿自己的 Apple developer 帳號試試 :P 相關文件:
首先就來闖關吧,先進行 Sign In with Apple 設定,需要指定某個 email domain 給 Apple 跟用戶溝通,此例用 appid.changyy.org 網域為例:

Certificates, Identifiers & Profiles -> More -> Sign In with Apple -> Configure

SignInWithApple03

在添加網域進去之前,請記得先設定 SPF DNS Record,不然 Apple 一驗證不合 SPF 時,又得等 DNS Cache 更新等到天荒地老 Orz

在此先添加 Type=TXT 的 DNS Record 吧!

v=spf1 include:amazonses.com -all

SignInWithApple02

接著用 dig 驗一下:

% dig -t txt appid.changyy.org

; <<>> DiG 9.10.6 <<>> -t txt appid.changyy.org
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 47410
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;appid.changyy.org. IN TXT

;; ANSWER SECTION:
appid.changyy.org. 119 IN TXT "v=spf1 include:amazonses.com -all"

;; Query time: 56 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Wed Nov 20 00:48:09 CST 2019
;; MSG SIZE  rcvd: 92


添加 appid.changyy.org 網域,按下 Register 時,接著努力驗證通過即可。如果沒有先添加 SPF 就會出現類似訊息:The domain 'appid.changyy.org' is not SPF compliant.

接著再回到 Apple Developer 網站繼續按 Register (不是立刻通過,要等 DNS Cache 過期),就可以進行後續的認證了,如 https://appid.changyy.org/.well-known/apple-developer-domain-association.txt 配置等。接著又得搞 https 連線,又進入了 免費SSL/TLS憑證 - Let's Encrypt 與 NGINX 的設定 XD 在此不贅述。

終於可以進入 Apple Developer Account 其他設定了,首先要建立 Services IDs 時,會要求有一個 App ID 為 primary App ID ,這件事也代表 Sign In with Apple 的核心還是 App ,此例新建一個 App ID = org.changyy.apple.app-id 為例,並且在下方勾選 Sign In with Apple 且 Enable as a primary App ID。

接著,建立 Services IDs :

Certificates, Identifiers & Profiles -> Identifiers -> Add -> Services IDs -> 建立一個 org.changyy.sign-in-with-apple 並啟用 Sign In with Apple -> Configure -> Web Domain = appid.changyy.org 而 Return URLs = https://appid.changyy.org/callback.php 並按 Add 和 Save -> 再按 Continue 完成

SignInWithApple08

最後,再來建立一組 Key 用來溝通:

Certificates, Identifiers & Profiles ->  Keys -> 添加一組 Key Name = SignInWithAppleKey,記得勾選 Sign In With Apple -> 點擊 Configure 挑選完 App ID 按 Save -> 最後會下載一個 AuthKey_KeyID.p8 檔案,就是後續溝通的項目。

SignInWithApple10

SignInWithApple11

如此一來,在上述的過程中可以得到以下關鍵物:

  • Service ID Identifier = org.changyy.sign-in-with-apple (後續是 OAuth 的 Client_ID)
  • Key ID = 在 *.p8 的檔名上,或是在 Certificates, Identifiers & Profiles -> Keys -> SignInWithAppleKey 瀏覽可看到
  • Team ID = 在 Apple Developer 登入後右上角可看見,或是在 Certificates, Identifiers & Profiles -> Identifiers -> 隨意一組 App ID -> App ID Prefix 就有標記 (Team ID) 資訊
  • Return URLs = 在 Service ID 內編輯的,如 https://appid.changyy.org/callback.php

把這些資訊弄個 JSON 紀錄:

$ cat settings.json
{
        "CLIENT_ID": "org.changyy.sign-in-with-apple",
        "SCOPES": "name email",
        "REDIRECT_URI": "https://appid.changyy.org/callback.php",
        "STATE": "",
        "TEAM_ID": "YourTeamID",
        "KID" : "YourKeyID",
        "Key_P8_PATH" :"../keystore/AuthKey_YourKeyID.p8",
        "" : ""
}


最後把 https://github.com/changyy/sign-in-with-apple-js 拿來用,在根目錄建立 keystore 並擺放 AuthKey_YourKeyID.p8 以及上述 settings.json 擺在 /path/sign-in-with-apple-js/php/settings.json ,在把 https://appid.changyy.org/ Document_Root 設定在此專案的 /path/sign-in-with-apple-js/php 目錄上,如此用 https://appid.changyy.org/ 時,就會有以下畫面:

SignInWithApple12

點擊後就會被引導完成 Apple 登入,登入後會導向到 https://appid.changyy.org/callback.php 並且看到簡單的 OAUTH code 的使用:
oepnssl = OpenSSL 1.1.1  11 Sep 2018
jwt_header = Array
(
    [typ] => JWT
    [alg] => ES256
    [kid] => YourKeyID
)

jwt_payload = Array
(
    [iss] => YourTeamID
    [iat] => 1574184061
    [exp] => 1574187661
    [aud] => https://appleid.apple.com
    [sub] => YourServiceID
)

apple_public_keys = Array
(
    [keys] => Array
        (
            [0] => Array
                (
                    [kty] => RSA
                    [kid] => AIDOPK1
                    [use] => sig
                    [alg] => RS256
                    [n] => lxrwmuYSAsTfn-lUu4goZSXBD9ackM9OJuwUVQHmbZo6GW4Fu_auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD4eRtY-RNwCWdjNfEaY_esUPY3OVMrNDI15Ns13xspWS3q-13kdGv9jHI28P87RvMpjz_JCpQ5IM44oSyRnYtVJO-320SB8E2Bw92pmrenbp67KRUzTEVfGU4-obP5RZ09OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysyd_JhmqX5CAaT9Pgi0J8lU_pcl215oANqjy7Ob-VMhug9eGyxAWVfu_1u6QJKePlE-w
                    [e] => AQAB
                )

        )

)

apple_public_key_pem = 

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlxrwmuYSAsTfn+lUu4go
ZSXBD9ackM9OJuwUVQHmbZo6GW4Fu/auUdN5zI7Y1dEDfgt7m7QXWbHuMD01HLnD
4eRtY+RNwCWdjNfEaY/esUPY3OVMrNDI15Ns13xspWS3q+13kdGv9jHI28P87RvM
pjz/JCpQ5IM44oSyRnYtVJO+320SB8E2Bw92pmrenbp67KRUzTEVfGU4+obP5RZ0
9OxvCr1io4KJvEOjDJuuoClF66AT72WymtoMdwzUmhINjR0XSqK6H0MdWsjw7ysy
d/JhmqX5CAaT9Pgi0J8lU/pcl215oANqjy7Ob+VMhug9eGyxAWVfu/1u6QJKePlE
+wIDAQAB
-----END PUBLIC KEY-----


apple_public_key_alg = RS256


[Lcobucci\JWT]
request data = Array
(
    [client_id] => YourServiceID
    [client_secret] => A.B.C1
    [code] => c
    [grant_type] => authorization_code
    [redirect_uri] => https://YourServiceIDReturnURL
)


response = Array
(
    [access_token] => a
    [token_type] => Bearer
    [expires_in] => 3600
    [refresh_token] => r
    [id_token] => id_token
)



[Firebase\JWT]
request data = Array
(
    [client_id] => YourServiceID
    [client_secret] => A.B.C2
    [code] => c
    [grant_type] => authorization_code
    [redirect_uri] => https://YourServiceIDReturnURL
)


response = Array
(
    [error] => invalid_client
)



[Firebase\JWT and \Lcobucci\JWT\Signer\Ecdsa\MultibyteStringConverter]
request data = Array
(
    [client_id] => YourServiceID
    [client_secret] => A.B.C3
    [code] => c
    [grant_type] => authorization_code
    [redirect_uri] => https://YourServiceIDReturnURL
)


response = Array
(
    [error] => invalid_grant
)



[Firebase\JWT without openssl_pkey_get_private and \Lcobucci\JWT\Signer\Ecdsa\MultibyteStringConverter]
request data = Array
(
    [client_id] => YourServiceID
    [client_secret] => A.B.C4
    [code] => c
    [grant_type] => authorization_code
    [redirect_uri] => https://YourServiceIDReturnURL
)


response = Array
(
    [error] => invalid_grant
)
網路上滿多人在討論 firebase/jwt 的用法,但其實在 2019/11/20 來看,firebase/jwt 仍就沒有完整 Sign In with Apple 所需的 JWT 編碼格式,我則是靠 Lcobucci/JWT 套件完成打通  Sign In with Apple 的,並且看懂為何 firebase/jwt 還不能打通 :P 拿著 Lcobucci/JWT 內的 MultibyteStringConverter 來小試身手果真就通了。再找個時間貢獻 firebase/jwt 來修正好了,這次為了檢驗 firebase/jwt 失敗問題,大概至少看了 5套 php-jwt 的寫法,結果大多都是從 luciferous/jwt fork 擴充的,這可是 2011 的程式呢。

而跟 Apple Auth API 溝通的結果,若本身 JWT 製作踩到演算法等問題只會收到 {"error":"invalid_client"},但這也包含 OAUTH REDIRECT_URI 不合法等等,如果演算法打通了,而 Code 過期或是被重複使用時,會收到 {"error":"invalid_grant"} 資訊。

2019年11月17日 星期日

車庫與猶豫的距離

猶豫 是對自己太客氣

一陣子沒哈拉了,最近越來越少碎碎念...該更加碎碎念,以此推進自己。

倒垃圾時,看到對面的車庫是間廣告公司,大約25坪的空間,之前燈會期間也看過他們在設計小型裝飾,一直不以為意。於是乎用大大寫的 台灣公司資料 查詢一下,原來是一間成立不到 3 年,資本額500萬的廣告公司!真猛,也讓人回顧自身狀態,有種說不出的感概。

記得 10年前 的家族聚會,表弟常常哈拉邀我幫他寫網站,他腦中有很多點子想實現,我則是百般拒絕,因為我也有想做的事 XD 隨著彼此的路線忽遠忽近,每年依舊只有那一兩次的聚會碰面,就這樣我持續深耕資訊業,而表弟放棄了資訊業探索了自身興趣、音樂、影視業。今年金鐘獎走過紅毯,也更有實力能哈拉想投資的遊戲產業等等。

在台北生活了好一陣子,真的越來越有那種深刻的感受:機會很多,是不是你的是另一回事。走那學生時期的老師們教導正規路線,永遠都是不夠的。而一生的機會可能沒幾次,出現了就好好把握!而成天的躊躇不前或老想著用一招吃全部,永遠什麼都掌握不到。

時間永遠不夠用,只能持續專注。相同的時間,只要持續付出,永遠在其他地方會長出新的足跡。

2019年11月16日 星期六

[C] 查看目前程式的記憶體用量 @ macOS 10.15

記得碩士生活很常被記憶體追殺,那時都是靠 unix tools 或是 /proc/ 查看指定的記憶體,都忘了其實是靠 process 自己去查詢資料,這次工作要協助 debug 抓資訊,就把它完成了:

#include <stdio.h>
#include <stdlib.h>

// Memory Usage
#include <sys/types.h>
#include <sys/sysctl.h>

void simple_wait() {
size_t wait_buf_size = 32;
char *wait_buf;
wait_buf = (char *)malloc(wait_buf_size * sizeof(char));
getline(&wait_buf, &wait_buf_size, stdin);
free(wait_buf);
}

void show_memory_usage() {
struct rusage usage;
printf("\n-- Memory Usage -- Begin --\n");

if(0 == getrusage(RUSAGE_SELF, &usage)) {
printf("\tBytes:\t%ld\n", usage.ru_maxrss);
printf("\t= \t%.3f KB\n", usage.ru_maxrss / 1024.0);
printf("\t= \t%.3f MB\n", usage.ru_maxrss / 1024.0 / 1024.0);
} else {
printf("\tREAD ERROR\n");
}

printf("-- End -- Memory Usage --\n");
}

int main(int argc, char *argv[]) {

show_memory_usage();
printf("press enter to continue\n");
simple_wait();

return 0;
}


用法:

$ gcc t.c
$ ./a.out

-- Memory Usage -- Begin --
Bytes: 671744
= 656.000 KB
= 0.641 MB
-- End -- Memory Usage --
press enter to continue


只是在 man page: https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/getrusage.2.html 的描述:

ru_maxrss    the maximum resident set size utilized (in kilobytes).

但看數據總覺得是 bytes 啊 XD 先記錄起來,有空再追蹤

2019年10月18日 星期五

零產出的時代

黑白拍

囧...最近沒什麼產出,開始感受到時間/年紀的殘忍。有些點子想執行,偏偏熱情很快燃燒殆盡,不太能像以前一個月、三個月就衝出點東西。也有可能年紀變大後,面對襲來的現實不能持續裝傻。

今年還剩最後一季,該想想如何像以前那樣,持續的累積東西,就算再慢,也可以靠版本控制快速回顧、接續開發。

2019年10月7日 星期一

喝杯茶,聊聊天

西門町聚餐

又好一陣子沒聚餐了,甚至忘了上次聚餐到一半接到電話就趕去外婆家。大概三年沒來這間,這次順應著研究所同學休假回國,幾個人聚了起來,湊合湊合成了個圓桌。

差不多到了那個年紀,彼此不會羨慕彼此的生活,有的有家庭,有的有事業,有的有愛情,有的有等待,不約而同地都有著數不完的故事。東說一下,西指一下。說聊得起勁卻還是有些生疏,說聊得太淡然,卻有能鏗鏘有力的道述著不是。

這肯定是中年大叔的聚餐吧?說話點到為止,不會分享得太多,卻一被問起就一整個像粽子被拉起來。別有一番風味。

2019年9月26日 星期四

[開箱] 眾籌 - Hidden 2 隱形筆電架

Hidden 2 隱形筆電架

被學長推坑,晃了兩個禮拜才去下單買了兩組輕薄筆電架。沒想到今年下半年起,終於開始在眾籌網站在購物了 XD 其實眾籌網站已經不是眾籌,而是行銷用途,所有的新品幾乎都靠眾籌在推銷。這款大概是...今年花最少錢的眾籌案子,還有兩個稍貴的還沒發貨。

之前看非 coding 的同事用個高高的筆電架,總覺得要打字不會因為高度而很難打很快嗎?這次換我來體驗一下,究竟長久用下去到底有什麼變化。

2019年9月20日 星期五

jq 指令筆記 - 整理 JSON 資料,使用 select / index 過濾關鍵字

用 jq 去整理 api/json 的資料的。整個需求是:

  • API 回傳的 JSON 資料中,是一個 array 形式,裡頭的元素是 key-value pair
  • 透過 jq 把符合我需要的 資料列出
  • 檢查在某些條件上,有哪些東西,最後回歸到 comm 的工具幫忙導出結果進行比較

筆記一下 jq 項目:

$ cat /tmp/api.json | jq '.["data"]'
[
  {
    "field": "hello"
  },
  {
    "field": "world"
  }
]
$ cat /tmp/api.json | jq '.["data"] | .[] '
{
  "field": "hello"
}
{
  "field": "world"
}
$ cat /tmp/api.json | jq '.["data"] | .[] | select (.field | index("e") > 0) '
{
  "field": "hello"
}
$ cat /tmp/api.json | jq '.["data"] | .[] | select (.field | index("e") > 0) | .field '
"hello"


如此,可以結果導入檔案,如果需要比較檔案內的差異,就可以用 comm 指令來做事

使用 comm 指令,找尋存在 A 檔案卻不在 B 檔案內的關鍵字

這個需求是為了過濾一些條件,找出某筆資料存在 A 檔案,卻不在 B 檔案的用法。這時用 comm 這個指令就能達成功效(通常都還會用 sort/uniq 指令搭配):

$ cat /tmp/a.log
1
2
3
4
5


$ cat /tmp/b.log
3
5
8
9


$ comm -23 /tmp/a.log /tmp/b.log
1
2
4

2019年9月18日 星期三

[macOS] 處理 LSEP 特殊符號

最近在製作產品 landing page 時,同事發現對岸同事給的資料裡,複製出來貼到網頁上還是會看到特殊符號 L SEP 的字符。這是因為大家的文字編輯不同,再加上整理資料時都採用複製貼上,因此把這換行符號 unicode U+2028 字符貼到網頁或對應的語言檔(或是 HTML 寫 &#8232 )

解法:看看編輯器是否能取代了

VIM 解法:搜尋時,輸入 \%u2028 就能找到,接著再靠 :%s/\%u2028//g 就取代完畢了

iOS 開發筆記 - 使用 Swift 完成 GA Measurement Protocol 實作

大概去年夏天,偶爾會寫一點 Swift 程式,雖然對 Objective c 比較熟,但時勢變遷就該順應潮流。那時採用 GoogleAnalytics 套件,但 Google Analytics for App 要在今年秋天下線了,Google一直要大家改用 Firebase ,且 Google 牌 cocoapods GoogleAnalytics 也的確幾年沒更新了。只是 Firebase 就明顯要人多花錢,例如要先把 Firebase 的數據匯入到 GA 上免錢,但有些查詢又要搞到 BigQuery 才行,雖然 BigQuery 有免費額度 啦 XD 因此趁 GA Measurement Protocol 還沒下線前,多用用他吧!把以前是 App Screen 就設法轉成 Web Pageview 吧,唯一的缺點是 Session 這類,若要硬做也是一招啦,在此就都不管了。

在此只包裝成 3 個函式供人使用即可,分別是取得 clientID、回報 pageview、回報 event 既可:

override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.

GAEvent(trackingID: "UA-########-#", clientID: getClientID(), eventCategory: "TestEC", eventAction: "TestEA", eventLabel: "TestEL", eventValue: 0)

GAPageView(trackingID: "UA-########-#", clientID: getClientID(), path:"/")
}

func getClientID() -> String {
// example
return UUID().uuidString
}

func GAPageView(trackingID:String, clientID:String, path:String ) {
var url = URLComponents(string: "https://www.google-analytics.com/collect")!
if trackingID.isEmpty || clientID.isEmpty || path.isEmpty {
return
}
url.queryItems = [
URLQueryItem(name: "v", value: "1"),
URLQueryItem(name: "tid", value: trackingID),
URLQueryItem(name: "cid", value: clientID),
URLQueryItem(name: "t", value: "pageview"),
URLQueryItem(name: "dh", value: "example.com"),
URLQueryItem(name: "dp", value: path),
]
//#if DEBUG
//url.queryItems?.append(URLQueryItem(name: "cd1", value: "DEBUG"))
//#endif
//if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, appVersion.isEmpty {
//    url.queryItems?.append(URLQueryItem(name: "cd2", value: appVersion))
//}

let request = URLRequest(url: url.url!)
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
//guard let data = data else { return }
//print(String(data: data, encoding: .utf8)!)
if let httpResponse = response as? HTTPURLResponse {
print("GAPageView: \(httpResponse.statusCode)" )
}
}
task.resume()
}

func GAEvent(trackingID:String, clientID:String, eventCategory:String, eventAction:String, eventLabel:String, eventValue:Int) {
var url = URLComponents(string: "https://www.google-analytics.com/collect")!
if trackingID.isEmpty || clientID.isEmpty || path.isEmpty {
return
}

url.queryItems = [
URLQueryItem(name: "v", value: "1"),
URLQueryItem(name: "tid", value: trackingID),
URLQueryItem(name: "cid", value: clientID),
URLQueryItem(name: "t", value: "event"),
URLQueryItem(name: "ec", value: eventCategory),
]
//#if DEBUG
//url.queryItems?.append(URLQueryItem(name: "cd1", value: "DEBUG"))
//#endif
//if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String, appVersion.isEmpty {
//    url.queryItems?.append(URLQueryItem(name: "cd2", value: appVersion))
//}

if !eventAction.isEmpty {
url.queryItems?.append(URLQueryItem(name: "ea", value: eventAction))
if !eventLabel.isEmpty {
url.queryItems?.append(URLQueryItem(name: "el", value: eventAction))
if eventValue >= 0 {
url.queryItems?.append(URLQueryItem(name: "ev", value: "\(eventValue)"))
}
}
}

let request = URLRequest(url: url.url!)
let task = URLSession.shared.dataTask(with: request) {(data, response, error) in
//guard let data = data else { return }
//print(String(data: data, encoding: .utf8)!)
if let httpResponse = response as? HTTPURLResponse {
print("GAEvent: \(httpResponse.statusCode)" )
}
}
task.resume()
}


其中 getClientID() 只是個示意,因為每次呼叫 UUID().uuidString 都會產生一組新的,可以把它存起來使用,或是針對某些情境設計規劃,像是服務有提供登入機制時,就改用從 user id 計算得出等那類即可。

2019年9月11日 星期三

[macOS] 修正 Python 錯誤訊息 (caused by URLError(SSLError(1, u'[SSL: TLSV1_ALERT_PROTOCOL_VERSION] tlsv1 alert protocol version (_ssl.c:590)'),))

查了一下,就是 openssl 版本不夠新。一開始還以為是自己用 MacPorts 維護,導致不是用系統 Python 的關係,但在追細一點就可以知道單純更新 openssl 就搞定了。

$ python -V
Python 2.7.16
$ /usr/bin/python -V
Python 2.7.10
$ /usr/bin/python -c "import json, urllib2; print json.load(urllib2.urlopen('https://www.howsmyssl.com/a/check'))['tls_version']"
TLS 1.2
$ python -c "import json, urllib2; print json.load(urllib2.urlopen('https://www.howsmyssl.com/a/check'))['tls_version']"
TLS 1.2

$ openssl version -a
OpenSSL 1.0.2s  28 May 2019
built on: reproducible build, date unspecified
platform: darwin64-x86_64-cc
options:  bn(64,64) rc4(ptr,int) des(idx,cisc,16,int) idea(int) blowfish(idx)
compiler: /usr/bin/clang -I. -I.. -I../include  -fPIC -fno-common -DOPENSSL_PIC -DZLIB -DOPENSSL_THREADS -D_REENTRANT -DDSO_DLFCN -DHAVE_DLFCN_H -isysroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.14.sdk -arch x86_64 -O3 -DL_ENDIAN -Wall -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DMD5_ASM -DAES_ASM -DVPAES_ASM -DBSAES_ASM -DWHIRLPOOL_ASM -DGHASH_ASM -DECP_NISTZ256_ASM
OPENSSLDIR: "/opt/local/etc/openssl"


就好好把 openssl 更新即可修正。感謝佛心 www.howsmyssl.com 測試服務:https://www.howsmyssl.com/

另外也可以直接拿 openssl 上場問問 Google server:

$ openssl s_client -connect google.com:443 -tls1_2

若自己的 openssl 不支援 TLS 1.2 時,會直接回應 unknown option -tls1_2

2019年8月22日 星期四

[macOS] 使用 cURL command 上傳檔案且修改 Content-Disposition filename 欄位資訊

故事是為了檢驗 embedded linux 上,一些 CGI 資安問題。例如有隻 CGI 提供檔案上傳時,通常上傳完後就把檔案擺在指定位置。但擺放到指定位置時,若沒有寫好,直接拿 filename 去擺放時,就有機會做邪惡的事了!

而透過 cURL 上傳檔案,通常是用以下簡單的指定就完成:

$ curl -F "file=@/tmp/sample.txt" http://192.168.1.1/cgi-bin/upload.cgi

但如果細看表單內容物時,產出的結果:

POST /cgi-bin/upload.cgi HTTP/1.1
Host: 192.168.1.1
Content-Type: multipart/form-data
Content-Length: ##

----------########
Content-Disposition: form-data; name="file";
filename="sample.txt"

...


這時,有些 CGI 開發上會直接拿上述 filename 參數資訊來用,寫不好就會產生跳脫問題,如:

Content-Disposition: form-data; name="file";
filename="../etc/sample.txt"


若底層是直接用 sprintf(des_filepath, "/tmp/%s", filename); 就會有機會逃脫去覆蓋掉東西。幸運的是 embedded linux 大多會設定檔案系統是 Read-only file system 來保護 :P 若是用 PHP 時,大多都靠 basename 來保護了。

最後,補上靠 curl 串改 filename 的招數:

$ curl -F "file=@/tmp/sample.txt; filename=../etc/sample.txt" http://192.168.1.1/cgi-bin/upload.cgi

2019年8月7日 星期三

[開箱] 小米 A3 / Android One

小米A3

上一次買小米是小米2S的時代了 XD 用完小米2S就改用 iPhone SE 至今!

為何想買支 Android phone 呢?其實數個月來一直被 Pixel 3 / Pixel 3a 打中慾望,再加上手上的測試機 ASUS 表現不怎好,而左想右想自己也不會跳去改用 Android phone 了,就花 Pixel 3a 半個預算買隻接近 Android 原生手機,Android One 還可有兩年作業系統更新、三年月月安全更新的保證,雖說 Android 系統改朝換代時,硬體需求變化極快,有些手機硬體規格都撐不到兩年就不敷使用了。

關於 Android One 的故事,可以參考 WIKI:https://zh.wikipedia.org/wiki/Android_One

小米A3

而我自己下單前也有點遲疑,像是 NFC 功能無法測試 <囧> 而有的人是嫌棄他的 CPU 運算能力不夠新不夠強悍。目前使用的感覺非常良好!重溫 Android 中。

小米A3

Google Analytics 筆記 - 透過 gtag.js 回報 pageview 時,進行 Custom dimensions 回報

在 gtag 的自訂維度的文件上,是以 event 來教學的:gtagjs/custom-dims-mets 且 gtag 的 pageview 文件也是滿精簡:gtagjs/pages

遙想以前 ga 可以很直觀地靠 set 指令 處理:

ga('create', 'UA-XXXX-Y', 'auto');
// 設定指數 1 的自訂維度值。
ga('set', 'cd1', 'Level 1');
// 送出自訂維度值及瀏覽量匹配資料。
ga('send', 'pageview');


最後東摸摸西摸摸,找到一招,就是要發送自訂維度時,一口氣完成定義跟使用:

var params = {
'send_page_view': true,
'page_title' : title,
'page_path': path,
};

var custom_value = getSpecialInfo();
if (custom_value) {
params['ab_user'] = custom_value;
params['custom_map'] = {
'dimension1': 'ab_user',
};
}
gtag('config', 'UA-XXXX-Y', params);


而為何需要使用到這些,其實就是想多做更多的用戶分類,例如 A/B Test 時,更細膩的追蹤用戶行為。若採用 Google Analytics 實驗功能,可以直接靠 實驗/實驗ID 來區分。另外 Google 實驗在 2019/08/07 起已經移師到 Google 最佳化工具囉。只是使用 optimize.google.com 服務時,有些使用情境不太適合,像是網頁一初始畫面就要依照 A/B Test 分群做 UI 改變時,設計上只能先等 optimize.google.com js sdk 初始化才能做事,這就會稍微給人體驗不佳。如果操作上跟網頁初始化無關,如用戶點擊按鈕才依照 A/B Test 分群做不一樣的效果,那就還滿適合善用 optimize.google.com 提供的功能。

目前單靠 google analytics 自訂維度,再靠土炮 A/B 分群方式,把用戶標籤記錄在 cookie 使用:

// AB Test
function getRandomValue(min,max){
return Math.floor(Math.random()*(max-min+1))+min;
}

if (!getCookie(ab_test_cookie_name)) {
var user_flag = getRandom(1,10) > 5 ? '201908-a' : '201908-b';
setCookie(ab_test_cookie_name, user_flag, 365, '/');
}


後續在 google analytics 專案內的行為流程中,就可以在"新增區隔",從 "進階條件" 撈出自訂維度來看行為。

2019年7月24日 星期三

[C] 透過 CMAKE 搜尋 OpenSSL 函式庫產出 MD5 資料

最近幫同事 debug embedded linux 問題,恰好需要用 MD5 來做 hash ,隨意找找就用 OpenSSL 函式庫就對了。再加上原先寫的小程式已經靠 CMAKE 維護了,就繼續整合使用

CMakeLists.txt:

$ cat CMakeLists.txt
cmake_minimum_required(VERSION 3.11)
project (MD5)

SET(SOURCE_DIR ${CMAKE_SOURCE_DIR}/src)

# macOS
# $ mkdir b ; cd b;
# $ cmake .. -DOPENSSL_ROOT_DIR=/usr/local/opt/openssl -DOPENSSL_LIBRARIES=/usr/local/opt/openssl/lib
#
find_package(OpenSSL REQUIRED)
include_directories(${OPENSSL_INCLUDE_DIR})

ADD_EXECUTABLE(main
${SOURCE_DIR}/main.c
)
TARGET_LINK_LIBRARIES(main OpenSSL::SSL)


程式碼:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <openssl/md5.h>

const char * get_md5_string(const unsigned char *in, char *output) {
int i;
for(i = 0 ; i < MD5_DIGEST_LENGTH ; i++) {
sprintf(output + (i)*2, "%02x", in[i]);
}
output[MD5_DIGEST_LENGTH*2] = '\0';
return output;
}

int main(int argc, char *argv[]) {
unsigned char digest[MD5_DIGEST_LENGTH];
char readable_digest[MD5_DIGEST_LENGTH*2 + 1];

char *test = "HelloWorld";
char *input;

if (argc > 1)
input = argv[1];
else
input = test;

MD5_CTX context;
MD5_Init(&context);
MD5_Update(&context, input, strlen(input));
MD5_Final(digest, &context);

get_md5_string(digest, readable_digest);

printf("MD5: [%s]\n", readable_digest);

return 0;
}


編譯跟運行:

$ mkdir b
$ cd b ; çmake ..
$ make
$ ./main HelloWorld
MD5: [68e109f0f40ca72a15e05cc22786f8e6]
$ echo -ne "HelloWorld" | md5
68e109f0f40ca72a15e05cc22786f8e6

2019年7月3日 星期三

公司營運資方人力成本 筆記

公司營運人力成本級距

最近證交所公布了 "發布國內上市公司106年度「平均員工薪資費用」排序前50名及後50名資訊" ,引起了一陣騷動,各大媒體也一直拿來報,但很少人仔細去看證交所的那篇文章,覺得滿有意思的,筆記一下:
發布國內上市公司106年度「平均員工薪資費用」排序前50名及後50名資訊

經統計,844家國內上市公司106年度公告之財務報告,稅前淨利合計2兆5,764億元,較105年度成長3,599億元(增幅16.24%),營運有獲利者計697家(占比82.58%);106年度員工福利費用(包括薪資、勞健保、退休金及其他)總額計1兆3,753億元,較105年度之1兆3,065億元增加5.27%,106年度之總人均員工福利費用112.11萬元較105年度之108.93萬元增加2.92%;另106年度員工薪資費用總額計1兆1,450億元(占員工福利費用比重約83.25%),員工人數合計約123萬人,總人均年薪約93萬元。
有關國內上市公司106年度平均員工薪資費用之統計情形說明如下:
一、平均員工薪資費用「排序前50名」之公司名單(依金額高至低排序)詳附件一:http://www.twse.com.tw/downloads/zh/about/press_room/doc/news-20180620001.xlsx
(一)排序前50名公司之員工年薪平均約144.3萬元至287.5萬元,營運有獲利者計48家公司(占比96%)。
(二)以產業別分析,屬電子產業者計38家公司(占比76%,其中半導體業者計19家),顯示其產業競爭力強、較有能力或意願將獲利回饋員工。
(三)排序前50名公司中,計19家公司之員工薪資費用包含依股份基礎給付(如員工認股權、限制權利新股、買回股份轉讓予員工等)之評價金額,其中電子業公司占16家,主係企業為延攬人才或獎酬員工所致
我一開始也以為那份薪資水準是基層人員的平均,雖說平均仍不如中位數準確(平均男女有一顆蛋蛋),但也有些參考價值,但仔細看 Excel 檔案後,發現那份是包含董事:
http://www.twse.com.tw/downloads/zh/about/press_room/doc/news-20180620001.xlsx 
資料來源及統計範圍說明:
(一) 就842家公告106年度財報之國內上市公司,參酌中小企業認定標準剔除員工人數未達100人之公司,並衡酌控股公司實際營運主體為各子公司,爰排除投資控股公司及金融控股公司後,以687家國內上市公司為統計範圍。
(二) 依前述公司106年度(個體、個別)財務報告附註揭露之員工薪資費用及員工人數,核計「平均員工薪資費用」算術平均數後進行排序。
(三) 「員工」可能以全職、兼職、永久、不定時或臨時之方式提供服務予企業,員工包括董事及其他管理人員。各公司財報附註揭露之員工包括董事、經理人、全職員工及臨時工等,惟不包含派遣或業務承攬外包人員。
(四) 「員工薪資費用」包括薪資、獎金、紅利等,亦包含依股份基礎給付評價之金額,暨給付董事之報酬、酬勞。
所以,只要包含董事管理層福利,參考價值就低不少 Orz 只能期待那篇證交所文章所提:
綜上,為精進上市公司之員工薪酬統計作業,使其更具可比較性、合理性、客觀性,經參酌金管會107年4月24日發布「新版公司治理藍圖」將循序推動上市公司揭露員工薪酬資訊之計畫項目(非擔任主管職務之員工人數及員工薪酬中位數、平均數),及金管會發布各業別「財務報告編製準則」規定之員工人數及員工福利費用性質別資訊(預告自107年度財務報告起將單獨揭露「董事酬金」及「未兼任員工之董事人數」),暨參考行政院主計總處之薪資統計重要名詞定義(如:受僱之全時員工、總薪資中位數等),證交所將配合增訂相關資訊申報項目及定義,並研議低薪公司之篩選標準,暨洽請渠等公司說明經營績效與員工薪酬之關聯性與合理性,期能以市場機制促使上市公司訂定合理之員工報酬,落實公司治理與企業社會責任。
盡快推出 "非擔任主管職務之員工人數及員工薪酬中位數、平均數" ,這個才是有幫助的資訊。

後來,跟朋友哈拉一陣子,跑去研究第二名的和泰汽車,筆記一下 “公司營運人力成本” 的容易忽略的項目:

  • 年薪用 14 個月、12個月的差異
  • 年薪用獎金制填補

這邊就簡化成年薪 14個月 跟 12個月 的差別筆記:

  • 首先,公司需要負擔員工的勞保(級距)、健保(級距)、退休金(級距6%) 的人力成本,這是最基本的法規
  • 勞保級距對於高科技行業很容易就進入到 Max 值,當月薪超過 45,800 後,就統一繳固定額度
  • 健保的級距很廣,可以到月薪18萬
  • 退休金的級距也廣,但只有到月薪15萬,依據對應級距提播 6%

在網路上可以看到有位和泰汽車的業務,說月薪四萬,但加了獎金可達年薪九十萬。這個現象,若把年薪拆分成12個月月薪,那月薪應當落在七萬五。

在以上的條件上製作表格,粗略可以得知,若公司聘人落在 20 人時,用月薪4萬+年獎金制度,會比純月薪制度省了快一個人力成本出來,也就是一樣的支出預算,用獎金制度可以多聘請一人。

公司營運人力成本範例

參考資料:

2019年6月26日 星期三

Android 開發筆記 - 添加 assets 資源 (resource://android/assets 目錄)

AndroidAssetsFolder02

每次都忘,紀錄一下:
  1. 在 Android Studio 的 Project 位置,點擊 app 目錄,接著按右鍵,選 New -> Folder -> Assets Folder,用預設 main 中即可
  2. 接著 Android Studio 就會自動長出 assets 目錄,在路徑上為 app/src/main/assets/ 
AndroidAssetsFolder01

2019年6月18日 星期二

Google API 筆記 - 使用 Google OAuth 2.0 與 IdToken 和 People API 取得 Email address

大概在 2015 年開發的 Google OAuth2 登入流程 已經需要更新了。當初一直靠 Google Plus API 取得用戶的 Email (GMail) 資訊,結果 Google+ APIs 已經在 2019/03/07 領便當了
https://developers.google.com/+/integrations-shutdown
停用 Google+ 整合服務
2019 年 3 月 7 日起,所有網站上與行動應用程式中的 Google+ 整合功能將全部停止運作。詳細情形如下:
網路整合 (例如外掛程式與互動訊息) 將停止提供服務。如果網站擁有者未採取任何行動,網站的版面配置和/或功能可能會受到影響。
行動應用程式整合 (例如 +1 按鈕、分享到 Google+ 與應用程式活動) 將停止運作。
我們將從 1 月下旬起逐漸停用相關功能。最早從 2019 年 1 月 29 日起,網站與行動應用程式整合功能會偶爾發生失敗的情形。
我們強烈建議開發人員儘快從其網站上和/或行動應用程式中移除相關程式碼。我們也將陸續停用 Google+ API 和 Google+ 登入功能。詳情請參閱這份額外
通知。
雖然我們即將停用 Google+ 一般使用者版本,但我們同時致力為企業機構 提供 Google+ 服務。Google+ 即將換上全新風貌並加入新功能,如需詳細資訊, 請參閱這篇網誌文章
原先是靠這 Google Plus API 取得 Email address:

<?php
$profile_ret = @json_decode(file_get_contents('https://www.googleapis.com/plus/v1/people/me?'.http_build_query( array(
'access_token' => $access_token,
))), true);


現在就改靠 People API:https://developers.google.com/people/api/rest/v1/people/get

<?php
$profile_ret =  @json_decode(file_get_contents('https://people.googleapis.com/v1/people/me?'.http_build_query( array(
'personFields' => 'names,nicknames,emailAddresses,coverPhotos,photos',
'access_token' => $access_token,
))), true);


在 2019 年 06 月,這隻 Google Plus api : https://www.googleapis.com/plus/v1/people/me 仍正常工作著,說不定它就真的是一直從 People API 服務著。

除此之外,原先 Google OAuth2 已經推薦改用 IdToken 進行認證,若是走 IdToken 時,可以在當下就拿到 Email Address 了:

<?php

require_once 'vendor/autoload.php';

$client = new Google_Client();
$client->setClientId($oauth_project_client_id);
$client->setClientSecret($oauth_project_secret_key);
$client->setAccessType("offline");
$client->addScope(['email','profile']);
$client->setRedirectUri($oauth_callback_url);

$token = $client->fetchAccessTokenWithAuthCode($code);
$token_data = $client->verifyIdToken();

$uid = $token_data['sub'];
$email = isset($token_data['email_verified']) && $token_data['email_verified'] ?$token_data['email'] : NULL;


如此就搞定取得用戶 Email address 的項目了!

其他筆記:

$ curl -s 'https://people.googleapis.com/v1/people/me?personFields=names%2Cnicknames%2CemailAddresses%2CcoverPhotos&access_token=USER_ACCESS_TOKEN'
{
  "resourceName": "people/UID",
  "etag": "XXXXXXXXXXXX",
  "names": [
    {
      "metadata": {
        "primary": true,
        "source": {
          "type": "PROFILE",
          "id": "UID"
        }
      },
      "displayName": "NICKNAME",
      "familyName": "LAST_NAME",
      "givenName": "FIRST_NAME",
      "displayNameLastFirst": "LAST_NAME, FIRST_NAME"
    }
  ],
  "coverPhotos": [
    {
      "metadata": {
        "primary": true,
        "source": {
          "type": "PROFILE",
          "id": "UID"
        }
      },
      "url": "https://XXX.googleusercontent.com/cXXXXXXX",
      "default": true
    }
  ],
  "emailAddresses": [
    {
      "metadata": {
        "primary": true,
        "verified": true,
        "source": {
          "type": "ACCOUNT",
          "id": "UID"
        }
      },
      "value": "USER_EMAIL@gmail.com"
    }
  ]
}

2019年6月15日 星期六

透過 BuyAndShip 購買 Amazon JP 的 Kindle Paperwhite 完整教學

Kindle Paperwhite

大概十年前,在工作上也算半隻腳踏入電子書領域,那時已經拿著幾台 Kindle 把玩,但最後投入了 iPad 懷抱 XD 在 2010 買了 iPad,沒想到 2019 年終於買一台 Kindle 來用用。這是台 Amazon JP 牌的 Kindle ,預設的商城是 Amazon JP。

事隔 10 年,拿到 Kindle 時,真的覺得很小台。記得 10 年前還沒這麼小?可能是當時有實體 keyboard 吧。長大概是兩張名片長,寬是兩張名片寬再多一點。

Kindle Paperwhite 02

這次是第一使用 BuyAndShip 服務,因為 Amazon JP Kindle 只能送貨到日本當地,就試試看在日本有倉庫的 BuyAndShip 國際網購代運。他是香港的公司,在世界幾處有倉庫,像我想買 Amazon JP Kindle 且Amazon JP把此商品限定寄送日本當地時,就把收信人填寫 BuyAndShip 在日本倉庫的地址,收信人則帶有自己在 BuyAndShip 的帳號資訊(姓名 + BuyAndShip 代號)。當 BuyAndShip 日本倉庫收件後,會立即再運到香港。BuyAndShip 用戶在填集運單發貨到台灣宅配收工。

整個 BuyAndShip 是秤重計費的。最近 BuyAndShip 台灣有活動,新建帳戶有 120 點數(若透過推薦人,首次交易完14天後又可以再多 200 點數),一點代表一塊台幣,而 120 點足以支付一磅的商品,而 Kindle 是 0.8 磅!若 Amazon JP 寄送到日本當地免運,那就沒有其他費用要支出,全程只有 Amazon JP 刷卡費(+銀行信用卡國際費用),理論上境外商品進台灣還有關稅問題,但這段我不是很懂,BuyAndShip.com.tw 在官網上說台灣的關稅他都包了 XD

www.buyandship.com.tw

整個流程:

1. 註冊 BuyAndShip 帳號( 若想再拿額外 200 點數,請點此 https://www.buyandship.com.tw/invite/2793172439/ 並且在推薦欄位上寫 2793172439

BuyAndShip 推薦朋友

2. 完成帳號註冊後,可在海外倉庫地址上,取得倉庫資訊,網頁上都有範例請你填寫收件人的方式,以及倉庫地址等等:

BuyAndShip 海外倉庫地址

後續就是 Amazon JP 為例,在此就不多提 Amazon JP 帳號的註冊等(其實有些可以台灣收件的商品,就可以不用靠 BuyAndShip 了)。接著在 Amazon JP 購物時,就填寫寄送到 BuyAndShip 倉庫,主要是收件人和地址要有 BuyAndShip 個人代號。

接著 Amazon JP 發貨後,會顯示是靠哪家公司寄送以及貨品追蹤碼:

AmazonJP發貨追蹤碼資訊

如此剩下的事就都在 BuyAndShip 網站了!首先填寫申報貨件

01申報貨件

02申報貨件確認

03完成申報貨件

如此就完成第一階段的任務,接下來就是等待,等待貨品寄送到日本倉庫!這時我覺得 BuyAndShip 最方便的地方是每一步都會有狀態回報。當商品寄送到倉庫後,除了 Amazon JP 可以查看送貨情況外,當然 BuyAndShip 也會更新狀態,將貨件狀態更改為海外入庫,而後續也不用做什麼,會自動再轉成準備海外出庫,進入海外出庫後,下一步就是香港倉庫:

04海外入庫

05海外出庫

在香港倉庫時,就可以填寫轉運單,這時就會挑選要宅配回台灣哪裡了,在此步就會要付錢給 BuyAndShip 囉!由於創建帳號有 120 點,就在此用點數支付完畢:

06香港倉庫

填寫完就進入轉運單追蹤:

08準備發貨-黑貓宅配

一開始還不會有宅配的資訊,大概過個1~2天就會出現(此例使用黑貓宅急便),雖然有單號了,還是查不到(因為黑貓只是把寄貨單發到各處,不代表已經被黑貓收件),如此,可以什麼都不管了,只等著黑貓寄貨到家囉。

收到商品後,看起來 BuyAndShip 也沒拆開,直接在包一層就寄出去了!

BuyAndShip

拆開後都是 Amazon JP 的包裝封套,拆開後是 Amazon JP 對商品的保護:

Kindle Paperwhite 04

Kindle Paperwhite 05

經過這次試用 BuyAndShip 後,覺得令人不安的地方都有被 BuyAndShip 不斷更新貨品狀態而解決了!加上在首次使用靠 120 點數而不用付費,更是降低戒心 XD 往後若有不能直寄台灣的商品,就還會再來用用看的。目前比較搞不懂的還是進台灣的關稅問題,目前 BuyAndShip 都說會自行吸收處理,未免太佛心了吧 :P 但 BuyAndShip 有限制商品大小,應當也是種自保機制。

2019年6月2日 星期日

[書] Zero to One 再讀一次

Zero to One

當年跟風買了這本書,印象中是 2016 年。不過,一直有其他事中斷而沒看完。這次一口氣把它解決了!這其實不會很多,才兩百五十頁的量。

在看玩這本書之前,我反而先看完了很多類似概念的書籍,若要說最相關的大概屬這本吧
這次讀後感最深的反而是:
  • 真正處於優勢的公司,會刻意裝傻裝嫩來持續壟斷市場
  • 樂觀主義+確定的未來、樂觀主義+不確定的未來
    • 樂觀主義+確定的未來:會大膽衝刺嘗試新東西
    • 樂觀主義+不確定的未來:專研營運效率的最佳化(追求已知的 CP值)
  • 公司的經營上可分成所有權(股權)、經營權(日常營運)、控制權(督導公司),若有人身兼兩職就容易有紛爭
    • 新創:創辦人、員工、董事會
    • 草創:創辦人、創辦人、創投
  • 行銷與產品,兩者產出的品質都很重要
    • 第一次得知行銷重要剛好都是在 2014 年,在聽以前的老闆談論 OnePlus 興起的過程,另一個案例則是大自己一歲的學長成功透過眾籌推銷產品,拿了錢後才開始做產品!
    • 兩者重要的意義是,不能擺爛 XD 不能想靠行銷來掩飾掉產品的缺點,也不能因為自認產品好而就不花心力行銷
    • 行銷成本可細分成:病毒式行銷(1美金)、市場行銷(100美金)、死亡地帶、個人銷售(一萬美金)、複雜銷售(100萬美金),其中死亡地帶是指 100~10000 美金之間,很難評估該怎樣行銷的處境,屬於銷售瓶頸
  • 打造美好的未來
    • 歷史上的軌跡總是興衰交替著,這跟其他書籍分享的概念很像,如何抓到機會就看個人的本領了 XD
看完這本書後,心情比當年更沉靜許多,當年的情緒噴張,非常想衝出個大事業,但隨著經歷的增加,真的,一切都是默默努力順勢而來的成果,急不得的。像在李笑來的書裡就很明確地在談論要重視自身發展,也就是 “專注力 > 時間 > 金錢” 的概念。若擺在面對新興產業的發展時,就會明瞭此刻的無力是因為過去自己的能力並沒有相對應的累積或是舞台還沒出現,應當繼續專注自己的發展(想要的未來),不是到處跟風(包含從技術角度學了一卡車的新東西,某個角度來說,該成果無法在未來產生價值也是種時間浪費)

最後,這本書是在 2014 年 10 月出第一版的,書後面談論了幾間公司(都是 Paypal 幫),其中特斯拉的情況剛好很好玩,書裡提到他的盛況,但最近剛好是特斯拉的慘況 XD 股價跌回 2016 年了:

TSLA stock

2019年5月19日 星期日

Javascript 開發筆記 - 強制 HTTPS 瀏覽機制

HTTPS redirect

用 Blogger 搭配自訂網域時,可以靠 cloudflare 提供免費的 SSL 憑證,以此提供免費的 HTTPS 加密瀏覽體驗,然而,在 Blogger.com 使用 HTTPS 時,有個選項詢問是否開啟 HTTPS 重新導向,若採用 blogspot.com 網域是可以打開的,但使用自訂網域則不適合導向,會產生 loop。

這時就靠 javascript 躲在 <HEAD> 來做事吧

<script>

if (location.protocol != 'https:') {
location.href = 'https:' + window.location.href.substring(window.location.protocol.length);
}

</script>

Google Adsense 無效流量 排除方式

Adesne 無效流量

Google Adsense 有自己一套的無效流量判斷方式(黑盒子),在今年三月時,忽然發現無效流量大量提升,由於有追蹤各大服務的廣告單元,幾乎可以判定某個服務全部都被判定為無效流量,該廣告單元的收入全部歸零。

這時只好寄信煩一下 Google Adsense 支援小組,負責回應的,可以透露無效流量由高到底的廣告單元,如此就能證實自己的猜想,緊接著要想想到底該怎樣解決。

由於 Google Adsense 為了避免亂買流量、亂跟人行銷合作。因此並不透露什麼判定規則,只會像 NPC 回饋:
  • 用戶來自哪里?
  • 用戶如何與我的應用進行互動?
  • 用戶在我的應用中查看了哪些屏幕?
後來想了一陣子後,想到的“網域切換"的行爲,會不會被誤判成無效流量(買流量),事實證明真的解掉了!而我一開始則不斷抱怨服務已經超過半年沒改版,怎會突然被判定成無效流量?最終 Google Adsense 支援小組則是回饋:不是誤判,而是 Google Adsense 判斷機制變嚴格了!

此例被判定成無效流量的情境:
  1. 為了做服務轉移,從 A Domain 轉換到 B Domain 來維護,其中 A Domain 還富有 Session 等大量追蹤機制初始化
  2. A 跟 B Domain 都有註冊到 Google Adsense 
  3. 服務使用時,會很快從 A Domain 轉址到 B Domain
後續排除的解法就轉址時,從原本要進入 A Domain 內的 PHP Code 處理,改移至到 Web server rewrite rules 來排除,加速更快的跳轉,也避開 A Domain 的 session 初始化。

2019年5月18日 星期六

[書] 通往財富自由之路:教你如何變得更有價值!早晚有一天,可以不再為了生活出售自己的時間

通往財富自由之路

買了好一陣子,當初是一位剛加入幣圈的同輩想找我參加,而後反而聊到了這本書。當時聊完沒多久就買了,擺了半年才翻開。這本書的確滿值得翻一下,原本覺得書名很俗氣,翻了一下才知道這是作者 growth hack 手段:取個誘人的名字。

這本書對我來說,提點到 "專注力 > 時間 > 金錢" ,的確是當頭棒喝,隨著工作能力的增加,有留意到 "時間>金錢" 的概念,像是在台北市究竟要買車跟搭計程車的議題,不只養車位很貴,連找停車位都很耗時。但專注力真的被漠視了,它也能擴展到不要去跟八卦、更關注在自身成長等等。像前陣子就滿常隨別人一起抱怨些事,這些都很浪費青春的 Orz

此外,有提到多往未來看幾眼,不要短視。包含不要跟現在/過去的自己拼命、不需跟風,努力朝著未來望望,想想未來想變成什麼樣的人。書的後半段倒是呼應起書名,再講一點投資策略,這比我覺得很難評論對錯(例如單純挑大公司投資),但仍呼應著拿著錢買別人的時間是最低成本(時間 > 金錢),而此書撰寫時恰逢比特幣盛起,所以也有點沾到鋒頭,以作者抱著不放的思維,大概推論持數十萬個比特幣:比特币首富李笑来,手里到底有多少个比特币?李笑来通过比特币到底赚了多少钱?

無論如何,還是把重心擺重在個人成長,也提及未來甭想著退休,那個老一輩的思維 XD

2019年5月5日 星期日

服務流量變現和數據追蹤儀表板 - 透過 Google Adsense 和 Google Analytics 建置免費的儀表板

AnalyticsXAdsense

至今執行了一年半的流量變現任務,也進入了成長瓶頸。進入成長瓶頸時,會越來越依賴數據的推敲跟追蹤。雖然翻著 GA 跟 Adsense 報表也很夠用,但如果要追蹤的項目超過二十項時,就會非常累了,非常耗時,便開始適合建置 Dashboard 一眼觀看。

好在一年前已經有想好規則,但一直沒空製作儀表板,這週末剛好擠出了點時間,就把這些想法筆記也產出個雛形:github.com/changyy/TrackingLibrary

需求:
  • 想要追蹤新舊使用者類型的價值(value per person),究竟我們可以從單一使用者身上挖出多少礦呢?
  • 想要細膩到小小功能的變現能力
  • 能否讓 GA 數據跟 Adsense 數據自動匹配
推敲方式:
  • 讓新舊使用者觀看到的廣告單元是不一樣的
  • 讓想追蹤的小小功能的廣告單元是不一樣的
  • 在 GA 數據裡,採用 event ,並且對 event report 進行設計;在 Adsense 則是在廣告名稱進行設計。後續就可以依據撈出來的資料,自動組出對應的報表。
最後產出的規則:

有兩個服務 Service1, Service2 ,其中 Service 1 共有 1 個功能,而 Service 2 有 3 個功能。假設 Service 2 的第三個功能還有兩個子功能。

Service1
Function1 [GA event report = (ec="pageview",ea="Service1",el="Function1")]
Service2
Function1 [GA event report = (ec="pageview",ea="Service2",el="Function1")]
Function2 [GA event report = (ec="pageview",ea="Service2",el="Function2")]
Function3 [GA event report = (ec="pageview",ea="Service3",el="Function3")]
SubFunction1
Google Analytics event = (ec="pageview",ea="Service3",el="Function3,SubFunction1")
Google Adsense AdName =
New User 觀看 = "[#]NU,Service2,Function3,SubFunction1"
Old User 觀看 = "[#]OU,Service2,Function3,SubFunction1"
SubFunction2 [GA event report = (ec="pageview",ea="Service3",el="Function3,SubFunction2")]


上述的高度結構後,也不是沒有缺點的,缺點就是 Adsense 的廣告單元會爆多,例如自己經手製作的廣告單元就高達兩百個,如某個想關注服務其底下共有 80 個廣告單元時,這時靠 Adsense 內建的報表,篩選了 80 個廣告單元時,這時報表很容易產不出來的。而用 GA 觀看時,因為不是用常見的網頁 ga pageview 的方式,而是靠 ga event 查看,這時也會有些不方便。上述更別說還有對應的服務開發時,需要設置規則,讓新舊使用者、不同子服務觀看不同的廣告單元和 GA event report。

最後,則是 Dashboard 要產生時,需要呼叫多次 adsense api 跟 analytics api ,而免費版使用要留意會不會踩到額度限制。

其他資訊 - 關於 TrackingLibrary 的使用:
  1. 先將程式碼中 ga_adsense_rule.json 複製至 dev.json ,程式判斷出 dev.json 就會以此為優先處理
  2. 先建立一個 Google Cloud Project,來使用 Google Adsense / Google Analytics API。
    • 切換到 "API 與服務" -> 憑證 -> OAuth 同意畫面
      • 應用程式名稱:Tracking
      • 已授權網域:changyy.org
    • 切換到 "API 與服務" -> 憑證 -> 建立憑證
      • OAuth 用戶端 ID
        • 網路應用程式:TrackingDashboard
        • 已授權的 JavaScript 來源:http://tracking.changyy.org:8000
        • 已授權的重新導向 URI:http://tracking.changyy.org:8000
    • 取得 "這是您的用戶端 ID" 並更新在 dev.json
      • 將各個 project -> google_api_project 欄位更新成 XXXXXX.apps.googleusercontent.com 即可
  3. 開啟 Google Adsense / Google Analytics API
    • API和服務 -> 資訊主頁 -> 啟用 API和服務
      • 搜尋 "Analytics API" 以及點擊啟用
      • 搜尋 "AdSense Management API" 以及點擊啟用
  4. 切換至程式碼,編輯 dev.json ,建置想要關注的 GA 專案,例如單純複製 GA only 專案內容,只修改 project -> analytics -> ga_profile_id 欄位即可
  5. 切換到專案 php/web 位置,並運行本地端 web server ,即可瀏覽
    • $ cd TrackingLibrary/php/web
    • $ php -S tracking.changyy.org:8000 ../tools/ci-routing.php
    • $ open "http://tracking.changyy.org:8000/dashboard"

2019年4月8日 星期一

閒聊閒書

閒書

其實,我不怎買書的,更別說看資訊領域以外的書籍 XD 但近幾年反而開始買了一堆書,開始當作收藏吧。這趟返鄉後,把老家書架上的書籍列了列,拍個照記錄。這些對我而言,是僅不到一半的閒書,這是之前翻完了就放到老家的。這些非本業的書,其實也提醒我不少事情的,實在感謝。

大學失落時,學弟送了我一本「個性影響一生的成敗」,看完後帶回家時,不久後就被奇利咬壞 XD 其實我也想不起那本書在說什麼,看看網路上的目錄,其實跟我這一兩年翻的書目差不多,但我真的翻完就忘了 ...

幾年前聽了創業家兄弟的分享,開始買了「鐵飯碗,有什麼了不起?」翻翻,這週又翻一下複習,果真依舊有 fu,去年則是又買了「成功,就是要快速砍掉重練」,只是這類書籍,閱讀後的心聲,就像創業圈常說的:別人的成功大多是不可複製的,很多場景是很難使出其招數,真的要天時地利人和的。所以大部分對我而都像在聽八卦,聽完就不小心忘記了

其他本業書籍大概就「砍掉重練:30歲開始也不遲的工作術」等,還有一篇在 2014 年寫的草稿一直沒寫完,那是「Startup 2.0 工程師創業手冊」讀後心得,這類書籍主要是著重在自己/公司角度的福利爭取,如股份比例等。這一兩年的大概就「創業就是要細分壟斷」、「給力」、「電商 Zero to One:從0到1」等等,偏向我想多了解的項目,如電商、如企業文化的培養、營運策略等等。的確很醒腦,像有時不夠貪心,享受著小確幸,這類書會當頭棒喝地,告訴自己要珍惜目前的優勢,要趁勢而為。

只是,對我深刻的書籍,反而都是囧星人的說書,像「你要如何衡量你的人生」,接著再翻一下「斜槓人生」、「刻意練習」等等,但那種「被討厭的勇氣」、「這輩子,只能這樣嗎?你是自己最大的敵人」則是還沒翻完 XD 讓我想起以前在工研院圖書館借的書「師父:那些我在課堂外學會的本事」,回歸到原理屬性的意念是令人深刻的。

我想,大概會想推薦別人翻翻看「你要如何衡量你的人生」和「刻意練習」,但刻意練習其實看沒多久就可以了解其概念,後面越看越痛苦,因為例子很多 XD

不知道這種靠緣分翻書的情境,還會怎樣走下去呢。特別是網路上已經有看完的訊息,光個 PTT 八卦版就許多知識了。

2019年4月6日 星期六

[書] 刻意練習 讀後感

刻意練習

近一兩年的書籍閱讀,起因算是囧星人,雖然他已因個人事件而隱居,但仍感謝他說書,讓我有幸可以多多認識一些書籍。「刻意練習」這本書,買了快一年終於把它看完了,去年翻一下就停住。在這之前我則是看過「斜槓青年」,從中習到一個觀點:『沒有強度的練習,只是徒勞無功。』而這個念頭在我翻完「斜槓青年」幾個月後就差不多忘了 XD 處於略懂還不夠身體力行,但一翻完「刻意練習」後,整個念頭強度又再度襲來。

「刻意練習」很不錯,儘管整本書可能翻了 1/3 就知道他要提的概念(後面就必須靠意志力才能把它看完?)。在這個時間點對我就是醍醐灌頂,例如工作上該怎樣想想如何推進同事們的成長、家庭上要想想如何教育小孩,對自己而言,則是要找方式加強進步強度,像是 side project 挑選或是健身強度等等。如果在創業的路上,那刻意練習提到的新法也適用於公司的成長。

這本書提到了幾個重要的觀念:
  • 每個專家都是透過大量的練習才獲得專業的水準
  • 天賦只是入門門檻低了點,能夠快速得到成果,但後續都還是靠大量練習才得以精進
  • 有強度的練習,人人都可以有一定的水準
  • 如何有效練習?找老師/換環境!沒老師該怎辦?專注投入、意見回饋和解決問題
讓我回想起一些成長過程,像是國中發現自己在物理課的理解不差,進而轉化成一種積極的態度,我把著當作小小的天賦,使得自己有動力去把其他非擅長的科目補起來(大學考試時,物理表現卻極差 XD);碩班時期,因老闆個性,有了另類的教育過程,透過大量寫程式/重複造輪學習資訊技能,這個過程反而被教育成不迷戀 library/framework 甚至 paper,凡事多了解原理即可,不要害怕自己設計不出來,更不要迷戀大型專案。

我記得剛出社會很刻意的寫寫 blog ,當時只是無聊想知道能不能賺廣告費(事實上很低),但反而長出了不少方向,像是文章標題該怎樣訂才容易被找到(SEO)、每次寫筆記等於反芻學習到的資訊、方便自己以後快速找到工作筆記等等,當然也有意外的事件,像是有人會留言問要不要接外包、要不要加入新創團隊、要不要出書等等,比較好玩的是在研發單位時,寫了很多 AWS 功能簡介,結果兩三年後在業界的同學,反而看著自己的 blog 去認識 AWS 服務,對他可省去花時間親自體驗,對我則是出現莫名好玩的關聯,可以哈拉幾句。

幾年前有回學校分享工作心得,我反而拿寫 blog 這個小題目分享了一下,要能找到一個閉環來持續動作,其實這些過程就跟「斜槓青年」和「刻意練習」提到的概念非常近似,就是要找到有效的成長方式,要找到一個動力/念頭去支持。

此外,書中提到學鋼琴/學網球過程是個很讚的案例,一開始只是挑離家近的地方學習,等到技能進展到一個水準後,想要更精進時,便開始找專家/教練來教導,這時就不會是挑離家近了,甚至可能進行搬遷。更貼近一點的案例就是台北的小學,一堆家長為了讓小孩能夠念明星學校,開始遷戶籍 XD 導致學校為了處理這種問題開始要求入學前幾年就必須在某某里,甚至要求家長小孩要一起在該里才行。更誇張一點的是中學也這樣搞,搞得變成小孩出生沒多久,戶籍就得該在學區才得以拿到入學的資格門票。

回過頭來,在工作上我也面臨到"沒有老師"的地步,再精進只有幾招:自己找題目衝刺或換個環境。其中同輩通常是挑選換環境,像是加入有規模的外商(主因是外商福利誘人,導致強者群聚),這效率是驚人的。但如果公司還有舞台,那就好好珍惜,透過有效的回饋法來成長吧!

另外,如果從資方角度來看待目標達成率時,會讓人想起「給力」那本書,就為了追求目標達成效率,寧願再聘適合的新人才,而不願等同事成長(其實就是立馬辭退對方)。站在資方角度時,不需要自己去拼、花時間,而是反過來買別人的時間!若這個人才可以有對應的生產力時,給予 1.5 倍甚至多倍薪水都是值得的!反之不是人才時,花再多時間都是無益的。雖然殘忍,但資方為求生存也只能這樣下去,而勞資雙方的妥協保障,就淪落到基本的法規守則了,辭退時,是盡可能是優退路線。

最後,我還是推薦可以翻翻「刻意練習」這本書,而「斜槓青年」可以當做個入門(比較偏八卦、炫技),而「刻意練習」則是稍微偏向方法論,雖然書中的大量例子可能會讓人翻沒多久就沒興趣看完,但他的確也是刻意練習的招數,持續洗腦加深印象 XD

2019年4月4日 星期四

[macOS] 製作 macOS High sierra USB 安裝碟 @ macOS 10.13.6

build macOS USB install 10.13

手上這台 Macbook Pro 已經無法安裝 macOS 10.14 了,正準備更換 SSD ,因此想先準備 USB 安裝碟時,發現竟然無法製作 Orz 透過官網教學文才找到 macOS 10.13 下載位置  ( https://itunes.apple.com/tw/app/macos-high-sierra/id1246284741?ls=1&mt=12 ) ,發現僅 15MB 的大小,執行指令時會看到以下錯誤訊息:

$ time sudo /Applications/Install\ macOS\ High\ Sierra.app/Contents/Resources/createinstallmedia --volume /Volumes/8GUSB/
/Applications/Install macOS High Sierra.app does not appear to be a valid OS installer application.


最後研究一下,原來要靠安裝流程,讓系統根目錄 macOS Install Data 會產生必要的資料,接著安裝過程下載完必須的軟體時,會停在要重開機的路上,這時就可以關閉安裝,並把必要的資料取出來使用。

$ tree /macOS\ Install\ Data/ ; sudo du -hd1 /macOS\ Install\ Data/
/macOS\ Install\ Data/
├── AppleDiagnostics.chunklist
├── AppleDiagnostics.dmg
├── BaseSystem.chunklist
├── BaseSystem.dmg
├── InstallESDDmg.pkg
├── InstallInfo.plist
├── Locked\ Files
└── index.sproduct

1 directory, 7 files
 20K /macOS Install Data//Locked Files
4.9G /macOS Install Data/


接著可透過"顯示套件"的方式,在 Install macOS High Sierra.app 內的 Contents 中,建立 SharedSupport 目錄,並把 macOS Install Data 複製進去:

$ tree /Applications/Install\ macOS\ High\ Sierra.app/Contents/SharedSupport
/Applications/Install\ macOS\ High\ Sierra.app/Contents/SharedSupport
├── AppleDiagnostics.chunklist
├── AppleDiagnostics.dmg
├── BaseSystem.chunklist
├── BaseSystem.dmg
├── InstallESDDmg.pkg
├── InstallInfo.plist
└── index.sproduct

0 directories, 7 files


就可以製作 USB 安裝碟:

$ time sudo /Applications/Install\ macOS\ High\ Sierra.app/Contents/Resources/createinstallmedia --volume /Volumes/8GUSB/
Password:
Ready to start.
To continue we need to erase the volume at /Volumes/8GUSB/.
If you wish to continue type (Y) then press return: Y


Updated @ 2019/04/06:

雖說可以順利製作出開機碟,但發現該 USB 無法完成 10.13.6 作業系統的安裝。推論是系統安裝流程已被修改的關係,應當是系統安裝時,會要求要安裝的系統碟的根目錄上,配有 "/macOS Install Data/" 資料,才能重開機後完成安裝流程。

以上並未驗證 :P 因為這次重安裝是為了把筆電換上新的 SSD ,因此單純先把原本的系統碟透過外接方式重啟,並跑正常的系統安裝流程,安置在新的 SSD ,而安裝過程就完全不靠 USB 開機碟。

或許把 SharedSupport 複製一份到新 SSD 的 "/macOS Install Data/" 有可能就可以搞定?!

2019年3月17日 星期日

[書] 給力:矽谷有史以來最重要文件NETFLIX 維持創新動能的人才策略

給力

這書買了半年,堆積了一陣子後,今年陸續清ㄧ清,直到今天終於看完最後章節。最後章節是在講如何跟公司內不適任的同事分手 XD 明明是很簡單的概念,卻很難執行。我在工作上也曾被長官要求評估成員績效,並要求要認真考慮是否適任,因此閱讀起來特別有共鳴。

回過頭來,給力這本書還滿不錯的,醒腦。書裡提的管理、文化建置的概念是對的事,但實務上很難,難在需要權限,難在如何讓公司成員明知彼此的供需問題,難在用家庭成員情感經營時,很難開除家人。甚至身為一般階級的管理者也難以展現,大概只有公司創辦人/高階管理者才行吧?可惜的公司開得越久,也越難執行,實在是人多就有政治,人老就會油條。

目前我比較偏向在新團隊的建置時,可以引入這等概念,包含聘請高手時,要想想該人的薪資是否符合市場,要想想公司的營收是否可以支持,若該人可以有兩人以上的貢獻時,要解放成本概念,大膽給予兩倍、三倍薪資。一切的目的是為了公司加速成長,以此維持公司競爭力。

透過 Google Search 評估 subdomain 類服務人數規模

google-search-subdomain

例如 tumblr.com 的服務是每一個用戶可以創建多個部落格,但每一個部落格就是一個 subdomain.tumblr.com,對於這類服務的發展,這時就靠 Google Search 來評估使用人數,雖然 Google Search 的資料筆數也是推估的,但如果服務使用人不多,還是可以快速跳掉最終頁拿查看。

allinurl:tumblr.com site:.tumblr.com -www.tumblr.com

如此,搭配其他資續,可以推敲一下服務市佔率。

2019年3月16日 星期六

Google Analytics 使用筆記 - 設定目標(Goal)追蹤關鍵字成效

ga-goal-tracking

很久以前就想研究,一直拖到工作上買 Adwords 需要追蹤成效時,才開始學習 XD 而設定 Goal 其實非常簡單,就是在專案管理介面上,切換到最右邊有個"目標"可以設定,填一填就搞定,其中用 GA event 是最彈性的是部分,可以在對應的網頁動作上,發個 GA event 就能追蹤了。

ga-goal-setup-01

ga-goal-setup-02

ga-goal-setup-03

ga-goal-setup-04

如此回到 GA 專案 -> 客戶開發 -> 廣告活動 -> 所有廣告活動/付費關鍵字/隨機關鍵字 ,就可以看到 “轉換” 那邊,可以有目標來關注,十分方便。

就 Adwords 就可以不斷調整、找尋最洽當的關鍵字購買。