2020年6月20日 星期六

[Javascript] Shopify App - 修正 location.replace 與 GA 追蹤碼 utm_* 的應用

認真使用 Shopify 大概也有一個多月了,使用了一些付費 Shopify App 在做本地化加強,發現這些第三方 app 還滿善用 variant 機制,原先是拿來設計一個商品有多種顏色等用途,現在則是應用在多儲存一些在地化的資訊。

因此,當發現需要轉化成在地化資訊時,會透過接近導網址架構,多添加 variant 資訊:

var new_url = window.location.protocol + '//' + window.location.host + window.location.pathname + '?variant=' + variant_id;

然而這樣的小機制,卻把 utm_* 的追蹤碼都給去掉了 XD 所以在幫他補強一下:

var new_url = window.location.protocol + '//' + window.location.host + window.location.pathname + '?variant=' + variant_id;

if (window.location && window.location.href) {
var m = window.location.href.match(/[\?&]([^=]+=[^&]+)/g);
if (m) {
var params = null;
for (var i=0; i<m.length ; ++i) {
if (m[i].indexOf('utm_') > 0) {
if (params == null) {
params = m[i].substr(1);
} else {
params += '&' + m[i].substr(1);
}
}
}
if (params != null) {
new_url += '&' + params;
}
}
}

收工!

2020年6月7日 星期日

[PHP] 解決 CodeIgniter 4 Command line 輸出緩衝/不即時的問題 (ob_start / ob_end_flush) @ macOS 10.15

有點忘記了,好像在 CodeIgntier 3 (CI3) 建置 Command line 運行的任務時,可以很即時看到運算的輸出結果(預設是 testing mdoe?),但在 CI 4 時,卻必須等到所有任務都做完了,才會一次輸出結果。這對於在 Jenkins 系統運行時,無法時看到運行過程是很不方便的。

追蹤 CI 4 的架構,起因是 system/CodeIgniter.php:run 中,運行時呼叫了 Events::trigger('pre_system'); ,在 app/Config/Events.php 可以看到定義了 pre_system 的工作內容,大意就是當 ENVIRONMENT 不是 testing 時,就會開啟 ob_start 使用機制。

在 CI 架構上,使用 ob_start 也有個好處,那就是寫一些網頁服務時,有時是內容產出跟 header 送出夾雜在一起。當在 PHP 使用 header 送出資料時,是不允許前面已經輸出過資料的。這時靠 ob_start / ob_end_flush 應用,就可以完美排除這種問題。

回過頭來,面對 command line 的任務,若想要排除 ob_start 的影響,可以試著包裝一個輸出訊息的函式,並在裡頭輸出完資料後,再靠這兩段補強輸出資料,如:

function debug($message) {
echo "[DEBUG] $message\n";
while (\ob_get_level() > 0)
\ob_end_flush();
}

或是乾脆在負責 Command line 任務的建構子中,直接靠 ob_get_level 和 ob_end_flush 清掉輸出緩衝的設計:

public function __construct() {
while (\ob_get_level() > 0)
\ob_end_flush();
}

如此就可以得到即時的輸出資料了

2020年6月2日 星期二

[Linux] AddTrust External CA Root expired 處理方式 @ Ubuntu

週日也不幸踩到這個雷了,當時只解了幾個,沒想到這個範圍很大很大。
故事是我們採用 Namecheap 服務來簽署 Wildcard SSL 憑證,而使用到 AddTrust External CA Root 簽署。

當 AddTrust External CA Root 有效期間只到 2020/05/30 10:48:38 時,時間一到後,導致用戶連到我們的服務產生失敗,以 curl 來說,他預設依賴 OS 提供的系統憑證資訊:

$ curl https://ourservice.exmaple.com
curl: (60) server certificate verification failed. CAfile: /etc/ssl/certs/ca-certificates.crt CRLfile: none
More details here: http://curl.haxx.se/docs/sslcerts.html

curl performs SSL certificate verification by default, using a "bundle"
 of Certificate Authority (CA) public keys (CA certs). If the default
 bundle file isn't adequate, you can specify an alternate file
 using the --cacert option.
If this HTTPS server uses a certificate signed by a CA represented in
 the bundle, the certificate verification probably failed due to a
 problem with the certificate (it might be expired, or the name might
 not match the domain name in the URL).
If you'd like to turn off curl's verification of the certificate, use
 the -k (or --insecure) option.

而 Chrome Browser 很佛心,從軟體層自動幫轉到 USERTrust RSA Certification Authority。

問題的解法:

正確解法:直接再重新簽核新的憑證,新的憑證簽署時會略過已過期的項目。
臨時解法:將 https client side 的 AddTrust External CA Root 註解起來 

臨時解法,以 Ubuntu 內的 https client 為例,將 mozilla/AddTrust_External_Root.crt 那行註解起來

$ sudo vim /etc/ca-certificates.conf && sudo sh -c 'apt update && apt install ca-certificates && update-ca-certificates -f -v'

臨時解法只能解決 https client 是已知的範圍,像是服務的 client 都是自己控制的,這時就可以先解決那些 client 憑證的檢驗機制,但追到更細時,大部分的 https client 靠 OS 提供的憑證資訊就只要改一次,如果 https client 上跑 node.js 等應用,他們可能會依賴其他相關套件來維護憑證資訊,不一定會用 OS 內紀錄的憑證資訊,這時要排除會非常痛苦。因此,正確解法是重新簽發憑證

2020年5月19日 星期二

Shopify 開店筆記 - 跨境電商、依重量計算運費、依地區限制購物

google SHOP stock 2020/05/16

約莫協助公司維運自家電商已經有五年多。

通常電商的成績不佳,大部分會被認為是電商平台的設計問題,像是購物流程是否可以改善、網站配色是否不夠精美等等。因此,我們也開始研究是否朝向知名的網購平台 Shopify 來維運。Shopify 是一個體質非常好的電商平台,非常適合小試身手的平台,而這波疫情也讓 Shopify 股價扶搖直上。

其實,公司維運的電商平台老早就有用 Shopify 幾年了,如幫其他客戶經營副牌、白牌等。對我而言是一個熟悉又陌生的平台,因為整體上的維運都已經不需要透過工程師。趁這次把品牌電商遷移到 Shopify 上,順手記錄碰到的課題,主要分成收錢(金流)、寄貨(物流)、買家交易過程和其他需求等筆記,而 Shopify 其實有不錯的 Shopify App Store,可以自行添購(通常是訂閱式,每月支付),有些真的滿夠用,十分方便。

收錢/金流:

提供常見的 Paypal 支付,以及 Shopify Payment 機制,而這兩點都恰好因為已有 Paypal 帳號跟有香港辦公室的關係無痛打通。這兩個金流的使用都有地緣限制,像 Shopify Payment 還可以有 Google pay / Apple Pay ,只是不見得買家的地方可以使用。

金流上當客戶在網站上完成支付後,Shopify 便會立即發信給 商店聯絡電子郵件,就是在 "後台 -> 設定 -> 一般 -> 商店聯絡電子郵件" 設定。

此外 Paypal 帳號也是只限制 PayPal 商業帳戶,不能用個人的。

寄貨/物流:

Shopify 的物流可以簡單到自己寄送即可,單純把訂單標記成已出貨,並且可以填寫 tracking code 及查詢的方式(使用的物流),就完成了!

而物流的複雜度其實可大可小,我認為比較重要的是運送費率的設計,Shopify 支援設定多個倉儲位置,假設設定了兩個處(香港和台灣),那必須替這兩個倉儲分別設定運送策略。假設只對香港設定寄送到全世界而台灣倉庫沒有任何運送設定,這時有一位台灣用戶下單時,他會被默認靠近台灣而使用台灣倉庫的寄送策略,但台灣倉庫沒設定運送到台灣的運費規則,而導致使用者無法完成下單(會顯示類似無法寄送的錯誤訊息)。

另外,若設計以重量方式計算運費時,假設以 0.5kg 為間隔單位,分別設定了 "小於0.5kg"、"0.5kg到1.0kg"、"1.0kg到1.5kg",1.5kg到2.0kg",這時若使用者買的東西總重落在 2.0kg 以下是可以的,且有運費合併計算的美意,但如果使用者買的總重量超過 2.0kg 時,一樣會落入無法寄送的錯誤訊息,因為沒有大於 2.0kg 的運費規則。

另外,也要留意裝箱的箱子重量,定義了依重量計費時,結果不如預期時,可能是箱子重量影響。

買家交易過程:

如果沒有調整過預設設定時,客戶在下單過程時,可以選用手機門號簡訊完成認證,快速驗身完就可以下單了,非常貼心。而訂單狀態會用簡訊通知對方。然而,若是在做跨境電商時,碰到訂單問題時,就得走電話聯繫,推論會十分痛苦。另一方面,快遞都會要求填寫聯絡電話,若客戶訂購時沒留下電話資訊也會很苦的。

因此,將會到 "後台 -> 設定 -> 結帳",在 "客戶聯絡" 方面,改成限定用 email 結帳。表單資訊,會要求填寫 "運送地址電話號碼"。作為跨境電商的訂單管理機制。

其他項目 - 賣樣品給客人:

Shopify 建立訂單草稿 是非常好用的項目!若業務想要銷售樣品給客人時,透過訂單草稿,可以自行打包幾個商品、給予折扣,再透過寄送電子發票,便直接通報客戶直接到 Shopify 付款,整個過程非常方便。讓業務很快地辦完事,負責金流物流的同事也只是例行處理自己的職責。十分完美。

可以多多參考 Shopify 的說明:訂單草稿

其他項目 - 提醒未完成結帳項目:

當用戶在 Shopify 平台下單時,未完成結帳的訂單,一樣在後台可以看到。Shopify 可以在 "後台 -> 設定 -> 結帳 -> 未完成結帳作業" ,設定幾小時後傳送提醒信件。如此可以設法拉回一些潛在用戶。

其他項目 - 跨境電商 - 依照地區給予不同價格:

這個應數主要是總代理跟代理之間的關係,如果某個地區有代理商了,總代理賣的價格通常不能低於代理商,不然代理商都賣不出貨了。面對這個需求有兩種解法,第一種就是故意在電商上不銷售到有代理商的區域,可以靠 運費費率 的設定,讓有代理商地區沒有運費規則,這樣用戶就不能下單。另一種要花點錢的機制,使用 Multi-Country-Pricing 等類似服務,依照地區給予不同價格,但 Shopify 的核心只有收一個幣別,就算透過 Multi-Country-Pricing 的機制能夠給予當地使用者觀看當地的幣值,最終在收費時,還是會轉回原先 Shopify 商店所規範的價格(如美金為單位)


其他項目 - 信件通知:

Shopify 會用 "後台 -> 設定 -> 一般 -> 商店詳細資訊 -> 客戶電子郵件" 當作 mail sender 發送。通常 Shopify 發送到自己管理的 mail server 時,可以透過 SPF record 來允許,增加信件不被規範到垃圾信中。只有時很慘的,自家 mail server 阻擋了 Shopify 寄來的信時,只能設法靠不同的 mail domain 來避開。


在不同 mail domain 避開的機制下,例如現在用 [email protected] 作為 商店聯絡電子郵件 ,只好再用 [email protected] 當作 客戶電子郵件 。此時必須記得去修改信件通知的資訊,避免信件通知信一直請客人寄信給 [email protected] 尋求客服。而 [email protected] 最好也在設定個自動轉寄到 [email protected] ,避免漏掉客戶的重要信件。願意花錢可以靠 AWS WorkMail 單一帳號一個月四美金頂著用,不願意花錢可以試試免錢的 Yandex.Mail 。

2020年5月8日 星期五

[Python] 使用 http.server.BaseHTTPRequestHandler 製作簡易 Proxy 機制 @ macOS, python 3

學會用 curl / wget 模仿一些 request 後,在一些特殊情境上,還是得弄個 proxy 出來,雖然有 man-in-the-middle Proxy: mitmproxy 可以使用,但有時就是想要單純一點,寫點小程式自娛一下 XD

故事的情境:

- 想用 VLC 播放一些 Streaming 來源,但該 streaming 在存取時要求多塞一些request header,直接播會收到 40X 回應
- VLC 預設只吃 OS Level 的 proxy 設定

基於上述情境,雖然靠 curl/wget 添加 request header 可以搞定,但 VLC 這類就無法達成播放指定來源時多添加 request header。

因此,除了靠 OS Level 的 Proxy server 添加 reader header,就剩寫一隻代抓資料,邊抓邊輸出 stream 的小小程式,程式碼:

import http.server
import urllib.parse
import requests
import time

class ProxyHTTPRequestHandler(http.server.BaseHTTPRequestHandler):
	protocol_version = 'HTTP/1.0'

	def do_GET(self, body=True):
		try:
			target_url = urllib.parse.parse_qs(urllib.parse.urlparse(self.path).query).get('url', None)
			print("[INFO] target_url: "+str(target_url))
			if target_url != None:
				target_url = target_url[0]
				req_header = self.parse_headers()
				#resp = requests.get(target_url, headers=req_header, verify=False, stream=True)
				resp = requests.get(target_url, headers=req_header, verify="certs.pem", stream=True)
				print("[INFO] resp.status_code: %d " % resp.status_code)
				if resp.status_code == 404:
					self.send_response(404)
					self.send_header('Content-Type','text/html')
					self.end_headers()
					self.wfile.write("NOT FOUND".encode())
				else:
					self.send_response(resp.status_code)
					for k in resp.headers.keys():
						print("[INFO] resp.headers: [%s][%s]" % (k, resp.headers[k]) )
						self.send_header(k, resp.headers[k])
					self.end_headers()
					for chunk in resp.iter_content(chunk_size=1024):
						if chunk:
							self.wfile.write(chunk)
							self.wfile.flush()
						else:
							time.sleep(0.05)
				
			else:
				self.send_response(404)
				self.send_header('Content-Type','text/html')
				self.end_headers()
				self.wfile.write("NOT FOUND".encode())
		finally:
			pass

	def parse_headers(self):
		req_header = {}
		for line in self.headers:
			line_parts = [o.strip() for o in line.split(':', 1)]
			if len(line_parts) == 2:
				req_header[line_parts[0]] = line_parts[1]
		return self.inject_header(req_header)
    
	def inject_header(self, headers):
		headers['Referer'] = 'https://example.com/'
       
		return headers
	

if __name__ == '__main__':
	server_address = ('0.0.0.0', 8081)
	httpd = http.server.HTTPServer(server_address, ProxyHTTPRequestHandler)
	print('http server is running')
	httpd.serve_forever()

如此跑起來後,就可以用 http://localhost:8081/?url=https://exmaple.com/streaming 機制,例如 VLC 軟體直接輸入 http://localhost:8081/?url=https://exmaple.com/streaming 位置來嘗試播放。

2020年5月7日 星期四

Raspberry Pi 3 + RetroPie = 電視遊樂器 - 超任為例 @ macOS

Pi 3 + SNES

之前為了研究音控買了個 Pi 3 搭配 Google AIY Projects Voice Kit ,結果研究完就晾在那邊一年多了吧?反而音控設備已買了成品 Google Home mini 和 小愛音箱。最近無聊就想研究一下樹莓派怎樣玩遊戲,實在是已經不只一次看到什麼月光寶盒再賣,還是嘗試一下好了。

果真有個 RetroPie 就是專門做了超級多的電玩模擬器,裝下就搞定了!有幾件事要留意一下:
  • Pi 3 內建無線網卡,但安裝完卻沒法設定,連 ifconfig 都看不到
    • 解法:裝完 RetroPie 系統後,需要先設定 WI-FI Country ,才能正常設定無線網路
  • 在網路上亂找到的 ROM 檔案,擺入後卻無法被偵測或運行
    • 解法:還是裝一套 PC 版的來驗證吧 :P 有時真的是網路上的檔案格式問題,在 macOS 可以試試 OpenEMU 這套
  • 如何從 PC 傳 ROM 進入 Pi 3 + RetroPie
    • 解法:善用 RetroPie 的環境,可以把 SSH 打開,靠 SCP 或是透過 SAMBA 網路芳鄰進去也很方便
其他動作:
  • 在 https://retropie.org.uk/download/ 下載 Pi 3 作業系統
    • https://github.com/RetroPie/RetroPie-Setup/releases/download/4.6/retropie-buster-4.6-rpi2_rpi3.img.gz
  • 在 macOS command line 連續動作處理(此例 SD Card 位於 /dev/disk4,請不要隨意複製指令執行!)
    • % sudo diskutil unmountDisk /dev/disk4 % unzip retropie-buster-4.6-rpi2_rpi3.img.gz % sudo time dd bs=1m if=retropie-buster-4.6-rpi2_rpi3.img of=/dev/rdisk4 2729+0 records in 2729+0 records out 2861563904 bytes transferred in 506.080069 secs (5654370 bytes/sec) 506.10 real 0.01 user 2.08 sys % sudo diskutil unmountDisk /dev/disk4
  • 當 Pi 3 + RetroPie 啟動後,設定 Wi-fi Country 跟啟動 SSH
    • $ sudo raspi-config
      • Change User Password
      • Localisation Otions
        • Change WI-FI Country
      • Network Options
        • Wi-fi
      • Interfacing Options
        • SSH
      • Update
      • Finish
  • 更新系統以及 RetroPie 眾多資料(模擬器)
    • $ sudo apt update $ sudo apt upgrade $ sudo apt dist-upgrade $ sudo apt autoremove $ sudo apt install tmux vim

2020年5月4日 星期一

重溫超任「三國志3 中文版」

SENS 三國志3 中文版

記得第一次接觸是小學的時候,去幼稚園同學家的遊樂器材店,就這樣初次接觸三國志遊戲。下一次,則是 PC 版的三國志五了!

記得小時候搭配藍截者,有那種重返前一刻狀態的密技,就這樣用新增武將時,用耐心找到單一能力達 100 的員工 XD 此例是君主單純調配能力達到武力 100 ,其餘軍師則是花耐心找到智力 100。

就這樣,配一個君主跟三名武將,從許昌出發,而智力 100 的特色是做事不斷去煩軍師,只要軍師說 ok 就肯定 ok!透過偽書降低對方忠誠度,或是錄用對方都是軍師說好時,一定 100% 成功率的。沒想到這時把玩這款遊戲,一不小心就噴了 3 小時,更讓我想起工作上的瑣事

大概有三件事:

  • 大家特愛當中庸者,什麼都要摸一點,像是能力各個 85 甚至 95,都還不如單一項目專精 100 。單一能力達 100 時,才會進入另一個模式。
  • 單一武將兩個能力很好,不如兩個武將各一個能力好,在這種回合制上,就凸顯出兩個人的好用。例如一個人放偽書,另一個來錄用。因為有時對方身上兵權很重,第一次放偽書,可能下一回合兵權就被去除
  • 數字化,明明很普通的遊戲,卻還是被數字吸引著,養成一種無奈的耐心跟自律,規律地聽著洗腦的背景,時間就消逝了,而成就領地卻增加
這些是恰好跟工作有關,不要當英雄把責任跟任務攬在身上,一個人一天只有 24 小時!要多培養夥伴能力並珍惜單一能力技能很強的夥伴!最後,則是流量變現上持續數據化、遊戲化。

2020年4月27日 星期一

海潮之聲

Untitled

晚上 coding 時,看了一眼 NETFLIX ,看到國高中看過的動畫電影 海潮之聲 ,看著時間才一小時很剛好,就投放到 Chromecast 後,改在電視前面 coding ,也很不錯的在播完前沒多久收尾了 side project。

海潮之聲的故事,說真的記不太得,在看之前,我一直把劇情跟 兒時的點點滴滴 混在一起 XD 雖然我記得著海潮之聲是校園男女故事,但我腦袋裡一直想著海潮之聲有畢業後的同學會,好像在回憶什麼故事似的,看完才發現我真的錯很大 XD 這是一齣很直觀的故事敘述,雖然動畫電影的開頭是個倒敘。

此時此刻,看著這純純的愛情故事,也令人想起跟朋友的交流,記得有位朋友跟我分享起戀情上發生的事,這類通常是屬於訴苦。當時我就分享了我年輕時對於交往的想法,那就像翻開一本書,時而歡樂,時而禁書,又可能是悲傷、開朗的、故作堅強的、萬般神秘的。讓人常常一翻開就背負著一種責任要看完它。

搭配著海潮之聲的劇情發展,真是有所共鳴啊...因為神秘而多問了什麼、因為關心而多付出了什麼,殊不知情感的流露就這般地灑了滿地。

別忘了,要記得學會收個尾囉!

註1:用 NETFLIX 找 海潮 時,不能用兒童角色,這是一部 7+ 動畫電影
註2:在夏天聽原聲帶很應景 - Spotify : The Ocean Waves (Original Soundtrack)

2020年4月24日 星期五

[書] 流量池

Untitled

最初是因為瑞幸咖啡的關係,追了一下 CMO 楊飛經歷,就順手 K 完這本書。

關於瑞幸的故事,可以參考滿多不錯的影片:

【中国商业史09】瑞幸咖啡资本骗局大起底:老板卷走150亿,好兄弟却亏了8000万美金,背后究竟发生了什么?——冲浪普拉斯


CK投資理財|瑞幸咖啡背後的龐氏騙局(看完就全明白了!)|華爾街割韭菜,瑞幸咖啡絕非第一家!


不過,對於 CMO 的貢獻,倒是不可抹滅的。畢竟公司的行銷活動還是扎實的被執行出成績了。

對於這本非常值得番一遍,雖然我自己看得很痛苦 XD

主要是一樣的道理不斷的用其他案例呈述,且經驗多為天時地利人和的八卦屬性,也就是沒那個舞台就算知道了這些經驗,也終將隨著時間而無意義。但只靠一本書,就多看一下別人的眼界,還是很值得的。

最後就把這本書介紹給長輩們翻一翻 XD 恰好有一位長輩對流量變現很感興趣,要我開些書單,結果我亂開 XD 這次看到這本頗有名氣的,既不會太深的技術,也有很多案例,非常適合給未涉入過深此領域的人,嚐鮮一下。

2020年4月13日 星期一

[GAME] NBA 2K20 有趣的 MyGM 球隊經營 @ Xbox One

NBA2K20-MyGM01

因疫情的關係,也開始接觸遊戲,買了好一陣子,這週末才點進去 MyGM 的內容,一看真不得了,把球員跟球團經營的薪資都有曝光 XD 真是不錯!其中球團內的常見職務都有給予一些徽章,例如做好就給好徽章,做不好就給壞徽章。

NBA2K20-MyGM02

其中財務長真的好有感:
好的財務長,透過人脈關係,可以減少 10% 行銷費用
壞的財務長,缺乏人脈關係,增加經營成長 10%
壞的總經理特助,愛跟媒體洩露交易資訊
非常有趣!其實擺在公司內的高管經營也都是很合情合理的,如何不內耗,真是一門學問啊。

想起五六年前踏入創業圈的高中學長,分享當年完成A輪後,就趕緊搭建明星團隊,用錢找高手、名人,以此增加企業經營效率跟估值!可惜的,時也運也命也,有些時機錯過了就沒了。

註:結果我還是沒玩多少 NBA 2K20 XDD 都在玩 FIFA 20 ...

2020年4月12日 星期日

[Linux] Dropbox 缺少的 shared libraries @ Ubuntu 18.04 / dropbox-lnx.x86_64-94.4.384

最近更新 Ubuntu 18.04 的環境,順便重抓 Dropbox 來更新。

https://www.dropbox.com/zh_TW/install-linux
$ cd ~ && wget -O - "https://www.dropbox.com/download?plat=lnx.x86_64" | tar xzf -

一執行發現缺了不少 shared libraries ?很怪,印象中以前沒這樣過,順手筆記一下裝了哪些:

$ sudo apt install libxfixes3 libxdamage1 libglapi-mesa libxcb-glx0 libxcb-dri2-0 libxcb-dri3-0 libxcb-present0 libxcb-sync1 libxshmfence1 libxxf86vm1

2020年4月8日 星期三

楓林網 8maple.ru 數據分析

好久沒看到版權事件了 Orz 今天網路界的不小的新聞就是楓林網被抓了,從新聞節錄下來的數據:
  • 兩位台大碩工程師,年收2000萬
  • MAU 3000萬
  • 廣告月收入 200萬
  • 機器月支出 30萬
從 Alexa.com 得知的數據:台灣網站排名掉到 145 左右

01

02

03

新聞上提到有成立廣告行銷公司,假設純靠 Google Adsense 的話,推敲如下:
MAU = 3000萬
廣告收入 台幣 200萬
每天需賺 7萬台幣 => 2333 美金 
若 eCPM 為以下數值
- 0.03 美金 => 廣告曝光 7776萬次
- 0.06 美金 => 廣告曝光 3888萬次
- 0.09 美金 => 廣告曝光 2590萬次
- 0.12 美金 => 廣告曝光 1970萬次 
再假設 DAU 是 MAU 的 1/10, 1/6, 1/2 的話
- 300萬
- 500萬
- 1000萬 
那用戶一天需看 3次 ~ 20 次廣告 
若用戶中午看一次,或是晚上在逛一次,或是一進站可以看到 2-3 則廣告,那單一用戶一天看個 6-10 個廣告也不會太誇張。以此數字來推論 DAU 應當落在 300萬-1000萬之間都還算合理。
單純靠 Google Adsense 其實也不會差太多。說不定改用插頁廣告,還有機會 eCPM 衝到 2-3 美金呢 XD

聊聊題外話,恰好昨晚逛 Alexa Top 100 網站找點子,意外看到一間美妝網站的流量從 50 闖進 30 名,應該是半年不到的時間。這個案例很令人感到驚奇,一間資本額不高的行銷公司(破百但沒千萬),在低卡可以看到一些聘人產文章的八卦故事,竟然能如此成長快速。

問幾位周邊未滿30的女同事都說沒在逛,但已經很容易在 FB 的廣告上被打中。最後跟強者大大細聊後,推論應當是...被高人收購了,以及目前的市場活動熱絡著:


果真是時也運也命也啊,只能處處把握機會!美妝聯盟一到位,美妝網站們就可以一起接收廣告流量啦。

只是同事打趣的說,最近口罩盛行,美妝上半年可能很辛苦。

註:盜版內容極有可能無法獲得 google adsense 廣告來源

2020年4月4日 星期六

再看一次「心之谷」/ 「側耳傾聽」



最近宮崎駿搬上了 NETFLIX ,就順手滑了一下 XD 明明昨晚在看「地獄」(瘟疫+WHO),今晚卻在看宮崎駿心之谷。

一開始只是想回顧片中經典的合唱曲,看著看著也把後半段也都追完了。令人回想起初中生活,當時引響最深的就是心之谷的這首歌。述說著自己當年的青澀與茫無目標的生活,想做什麼,卻什麼也沒頭緒。

此時此刻,時間貢獻給工作、家庭,近幾年提不起勁的,隨手的 side project 也了無新意,很適合回顧這動畫,想想、動動身,別再遲疑了。

2020年3月28日 星期六

[Python] 數據分析筆記 - 透過 pandas, scikit-learn 和 xgboost 分析 Kaggle airbnb-recruiting-new-user-bookings 案例

記得 2017 年也曾註冊 Kaggle 帳號,在上面挑個題目試試手氣,當時也有選中 Airbnb 來研究,可惜當年沒堅持下去,太多有趣的事了 XD 今年春天就來複習一下。

當初我是先從史丹佛 Andrew Ng 的課程看的,但大概只看個幾集就沒繼續,再過一陣子後就是台大教授林軒田的機器學習基石,我印象中有看完,因為我還在遲疑要不要接著看另一個進階課程,那時過境遷,沒再用都忘光了!
這些課程看著看著就不小心恍神了,接著自己僅用著一些原理去土砲...殊不知 Pandas 跟 scikit-learn 套件有多好用,當時只粗略用 Pandas 當 csv parser ,剩下的資料轉換、陣列計算還自己刻 numpy 架構去運算。所幸,終於有個更適合我這種懶人的微課程,那就是 Kaggle 的 Faster Data Science Education
我大概只需要從第三章 Intermediate Machine Learning 走完一遍就得到我想要的東西了。接著想找個戰場試試手氣,就又回想起 Kaggle 的 airbnb-recruiting-new-user-bookings 數據
接著用 airbnb-recruiting-new-user-bookings 關鍵字問個 google ,會發現到現在也有非常多人拿他當例子來分析,經典果真歷久不衰!大概不用破百行,就可以組出 airbnb 數據分析達八成的水準:

匯入函式庫:

from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
from xgboost import XGBClassifier

import numpy as np
import pandas as pd

import datetime


匯入 csv 檔案:

train_users = pd.read_csv('input/train_users_2.csv')

將 age 內容調整,包含去除輸入錯誤(太大或大小者),例如明顯輸入的是西元年,就順便幫轉一下:

data_checker = train_users.select_dtypes(include=['number']).copy()
data_checker = data_checker[ (data_checker.age > 1000) & (data_checker.age < 2010) ]
data_checker['age'] = 2015 - data_checker['age'] # 推論當年的資料,用 2015 年來相減對方不小心輸錯的出生年來得到年紀

for idx,row in data_checker.iterrows():
        train_users.at[idx,'age'] = row['age']

data_checker = train_users.select_dtypes(include=['number']).copy()
data_checker = data_checker[ (data_checker.age >= 2010) | (data_checker.age >= 100) | (data_checker.age < 13) ]
data_checker['age'] = np.nan
for idx,row in data_checker.iterrows():
        train_users.at[idx,'age'] = row['age']


處理時間欄位,轉成 datetime 型態,並轉成 weekday:

data_checker = train_users.loc[:, 'timestamp_first_active'].copy()
data_checker = pd.to_datetime( (data_checker // 1000000), format='%Y%m%d')
train_users['timestamp_first_active'] = data_checker

str_to_datetime_fields = ['date_account_created', 'date_first_booking']

for field in str_to_datetime_fields:
        train_users[field] = pd.to_datetime(train_users[field])

# to weekday

train_users['first_active_weekday'] = train_users['timestamp_first_active'].dt.dayofweek
for field in str_to_datetime_fields:
        train_users[field+'_weekday'] = train_users[field].dt.dayofweek

# remove datetime fields

train_users.drop(str_to_datetime_fields, axis=1, inplace=True)
train_users.drop(['timestamp_first_active'], axis=1, inplace=True)


處理 label 資料,主要轉成 one-hot encoding,並且去除一些數值,統一轉成 NaN:

categorical_features = [
        'affiliate_channel',
        'affiliate_provider',
        #'country_destination',
        'first_affiliate_tracked',
        'first_browser',
        'first_device_type',
        'gender',
        'language',
        'signup_app',
        'signup_method'
]
for categorical_feature in categorical_features:
        train_users[categorical_feature].replace('-unknown-', np.nan, inplace=True)
        train_users[categorical_feature].replace('NaN', np.nan, inplace=True)
        train_users[categorical_feature] = train_users[categorical_feature].astype('category')

# https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.get_dummies.html
# Convert categorical variable into dummy/indicator variables.
train_users = pd.get_dummies(train_users, columns=categorical_features)


開始順練模型,建議先透過 sample 跑小量

# X = train_users.copy()
X = train_users.sample(n=3000,random_state=0).copy()
y = X['country_destination'].copy()
X = X.drop(['country_destination'], axis=1)

X_train, X_valid, y_train, y_valid = train_test_split(X, y)


print("Start to train...")

job_start = datetime.datetime.now()

my_model = XGBClassifier()
my_model.fit(X_train, y_train)

print("training done, time cost: ", (datetime.datetime.now() - job_start))

job_start = datetime.datetime.now()

predictions = my_model.predict(X_valid)
print("predict done, time cost: ", (datetime.datetime.now() - job_start))

print("score:", accuracy_score(predictions, y_valid))


運行結果:

Start to train...
training done, time cost:  0:00:14.270242
predict done, time cost:  0:00:00.056500
score: 0.8466666666666667


沒想到只需做一些處理,運算玩就有八成準確率了!以上還沒使用 sessions 資料。完整程式碼請參考:github.com/changyy/study-kaggle-airbnb-recruiting-new-user-bookings

2020年3月22日 星期日

[GoogleSheet] 透過 Query 取出動態欄位的極限值,以 GOOGLEFINANCE 為例

GoogleSheet, =QUERY(GOOGLEFINANCE("NASDAQ:ZM", "low","1/1/2020",365,"DAILY"),"Select Min(Col2) label Min(Col2)''",1)

故事是這樣的,目前透過 GoogleSheet 維護一些資訊,有些服務可以立即幫你長很多筆資料,但有時只需要其中一項極值資訊。

例如 GOOGLEFINANCE - https://support.google.com/docs/answer/3093281 可以一口氣列出幾定天數的數據,如:

=GOOGLEFINANCE("NASDAQ:ZM", "low","1/1/2020",365,"DAILY")

若只想看極值,就可以再透過 Query - https://support.google.com/docs/answer/3093343 處理

最低極值:

=QUERY(GOOGLEFINANCE("NASDAQ:ZM", "low","1/1/2020",365,"DAILY"),"Select Min(Col2) label Min(Col2)''",1)

最高極值:

=QUERY(GOOGLEFINANCE("NASDAQ:ZM", "high","1/1/2020",365,"DAILY"),"Select Max(Col2) label Max(Col2)''",1)

收工!真是美好的一天

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

2020年3月17日 星期二

使用 curl 仿 DLNA controller 指令播放影片: DLNA protocol + SOAP protocol

大概去年就拿 DLNA 當做個小題目研究了一下,最近工作又需要,再度翻出來複習一下。使用了別人寫的 python 小工具,非常小巧精美:github.com/cherezov/dlnap

裝置搜尋:

$ python dlnap.py
No compatible devices found.
$ python dlnap.py
Discovered devices:
 [a] Name1 @ 192.168.1.2
 [a] Name2 @ 192.168.1.3


播放影片:

$ python dlnap.py -d Name1 --play http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
No compatible devices found.

$ python dlnap.py -d Name1 --play http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4
Name1 @ 192.168.1.2
POST AVTransport/control HTTP/1.1
User-Agent: dlnap.py/0.15
Accept: */*
Content-Type: text/xml; charset="utf-8"
HOST: 192.168.1.2:60099
Content-Length: 558
SOAPACTION: "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"
Connection: close

<?xml version="1.0" encoding="utf-8"?>
         <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
            <s:Body>
               <u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
                  <InstanceID>0</InstanceID><CurrentURIMetaData></CurrentURIMetaData><CurrentURI>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4</CurrentURI>
               </u:SetAVTransportURI>
            </s:Body>
         </s:Envelope>
POST AVTransport/control HTTP/1.1
User-Agent: dlnap.py/0.15
Accept: */*
Content-Type: text/xml; charset="utf-8"
HOST: 192.168.1.2:60099
Content-Length: 401
SOAPACTION: "urn:schemas-upnp-org:service:AVTransport:1#Play"
Connection: close

<?xml version="1.0" encoding="utf-8"?>
         <s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
            <s:Body>
               <u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
                  <InstanceID>0</InstanceID><Speed>1</Speed>
               </u:Play>
            </s:Body>
         </s:Envelope>


可惜的,在測試播放影片時總不太順利,推論應當是兩個指令太快,改用 curl 指令測試就很正常:

1. 先設定影片

$ cat SetAVTransportURI.record
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:SetAVTransportURI xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
<CurrentURIMetaData></CurrentURIMetaData>
<CurrentURI>http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4</CurrentURI>
</u:SetAVTransportURI>
</s:Body>
</s:Envelope>

$ curl -v -X POST --data @SetAVTransportURI.record  -H 'SOAPACTION: "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"' -H 'Content-Type: text/xml; charset="utf-8"' http://192.168.1.2:60099/AVTransport/control
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 192.168.1.2:60099...
* TCP_NODELAY set
* Connected to 192.168.1.2 (192.168.1.2) port 60099 (#0)
> POST /AVTransport/control HTTP/1.1
> Host: 192.168.1.2:60099
> User-Agent: curl/7.68.0
> Accept: */*
> SOAPACTION: "urn:schemas-upnp-org:service:AVTransport:1#SetAVTransportURI"
> Content-Type: text/xml; charset="utf-8"
> Content-Length: 476
>
* upload completely sent off: 476 out of 476 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< EXT:
< CONTENT-TYPE: text/xml; charset="utf-8"
< SERVER: POSIX, UPnP/1.0, Intel MicroStack/1.0.2777
< Content-Length: 336
<
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <u:SetAVTransportURIResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">

      </u:SetAVTransportURIResponse>
   </s:Body>
* Connection #0 to host 192.168.1.2 left intact
</s:Envelope>


2. 呼叫播放

$ cat Play.record
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:Play xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">
<InstanceID>0</InstanceID>
<Speed>1</Speed>
</u:Play>
</s:Body>
</s:Envelope>

$ curl -v -X POST --data @Play.record  -H 'SOAPACTION: "urn:schemas-upnp-org:service:AVTransport:1#Play"' -H 'Content-Type: text/xml; charset="utf-8"' http://192.168.1.2:60099/AVTransport/control
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 192.168.1.2:60099...
* TCP_NODELAY set
* Connected to 192.168.1.2 (192.168.1.2) port 60099 (#0)
> POST /AVTransport/control HTTP/1.1
> Host: 192.168.1.2:60099
> User-Agent: curl/7.68.0
> Accept: */*
> SOAPACTION: "urn:schemas-upnp-org:service:AVTransport:1#Play"
> Content-Type: text/xml; charset="utf-8"
> Content-Length: 316
>
* upload completely sent off: 316 out of 316 bytes
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< EXT:
< CONTENT-TYPE: text/xml; charset="utf-8"
< SERVER: POSIX, UPnP/1.0, Intel MicroStack/1.0.2777
< Content-Length: 310
<
<?xml version="1.0" encoding="utf-8"?>
<s:Envelope s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
   <s:Body>
      <u:PlayResponse xmlns:u="urn:schemas-upnp-org:service:AVTransport:1">

      </u:PlayResponse>
   </s:Body>
* Connection #0 to host 192.168.1.2 left intact
</s:Envelope>

2020年3月15日 星期日

Xbox Project xCloud / Game Streaming 設定教學筆記 @ Xbox One S / Android 9 / 小米A3

Xbox xCloud game streaming

幾年前買了一台 Xbox One S 當藍光播放器,今年起,因為武漢肺炎等防疫關係,開始體驗 Xbox 商城。買了幾款遊戲試試,試了才發現 Xbox 真的發展的不錯!雖說遊戲方面推論還是會輸 PS ,但 Xbox 的整個模式已經漸漸成形,包括遊戲商城、包月100款遊戲(Xbox Game Pass)、包月每月兩款免費遊戲(Xbox Live Gold)等等,當然更免不了不時來一下遊戲特價,不斷地推坑 XD

原先是想多找哪邊有折扣,找著找著在 Xbox 台灣 FB 粉絲團看到新訊:
Xbox Project xCloud 雲端串流服務
原來在 2018 年 10 月就曾宣布 xCloud 了,本意是把主機雲端化,玩遊戲則是雲台運算並把結果 streaming 到手機上。而想要來體驗 Project xCloud (Preview) 時,發現現況也有 "主機串流" 服務,可以嘗試把 Xbox 輸出直播出去,這恰好打中我的需求:
  • 家中只有一台電視
  • 電視不能一直玩遊戲 / 有些暴力遊戲不宜給小孩看
這時,除了把 Xbox One HDMI out 輸出到另一台螢幕、投影機外,並沒有其他高招,大概就剩 HDMI WIFI 傳輸了!結果這時看到 Android app 已經可以來串流了,實在美好!

整個設定過程:
1. 將 Xbox One S 加入 Xbox One Preview Update
- Microsoft Store 下載 Xbox Insider 中心
- 加入 Xbox 測試人員計畫 https://beta.support.xbox.com/help/account-profile/manage-account/xbox-insider-program
- 安裝 Xbox One 更新預覽 版 (在系統區)
2. 設定主機串流
- 設定 -> 裝置與串流 -> 主機串流 -> 測試主機串流
- 可以看到要求更新控制器,以及電源選項要更新 
3. 更新藍芽控制器韌體
- 設定 -> 裝置與串流 -> 配件 -> 選擇 Xbox 無線控制器 -> 下方 ... 可以更新韌體 
4. 電源設定
- 設定 -> 一般 -> 電源模式與串流 -> 電源模式: 即時啟動
Xbox xCloud game streaming setup test

如此,再回去設定主機串流,測試主機串流,當測試通過才可以啟用!如果被網路上傳速度擋下,剛好手機有4G吃到飽,可以試看看把 Xbox 改用手機共享網路先設法通過測試,通過後再把 Xbox 網路改回來。

接著在 Google play 搜尋 xbox game streaming ,就能找到 Xbox Game Streaming (Preview) ,安裝完後,很快就能設定完成。主要把 Xbox 無線控制器跟手機藍芽配對後,就可以正常遠端 Xbox One S 了,嘗試把玩 NBA 2K20 或是快打旋風30周年慶,對於這種需要高更新畫面的遊戲,玩幾下就會看到極限了 XD 倒是老人回憶之旅那種篇益智遊戲,則非常的舒服。

Xbox xCloud Android Setup

Xbox xCloud Android Connect

如此就能體會主機串流的功能了!而大部分的體驗流程都是先到 google play 下載 xbox game streaming 後,接著得到你所在的區域並不支援 XD 殊不知要先從 Xbox One 更新預覽版才能開啟!

2020年3月13日 星期五

[macOS] GnuPG 筆記 - key generator / import / export / public key / private key

最近在跟大公司討論 DRM 事宜,要求信件採用 PGP 加密保護。筆記一下環境建置:

GnuPG 安裝:
  • 從 https://gnupg.org/download/ 到 GnuPG for OS X 區
  • 下載 GnuPG-2.2.19.dmg ,安裝後在 /usr/local/gnupg-2.2/
  • 推論環境變數可能就能找到 gpg2 ,或是用 /usr/local/gnupg-2.2/bin/gpg2
產生 GPG Keys(依照對方的使用設定):

$ gpg2 --full-generate-key
gpg (GnuPG) 2.2.19; Copyright (C) 2019 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
  (14) Existing key from card
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048)
Requested keysize is 2048 bits
Please specify how long the key should be valid.
         0 = key does not expire
      <n>  = key expires in n days
      <n>w = key expires in n weeks
      <n>m = key expires in n months
      <n>y = key expires in n years
Key is valid for? (0)
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: HelloWorld
Email address: [email protected]
Comment:
You selected this USER-ID:
    "HelloWorld <[email protected]>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.


Passphrase: HelloWorld!!


We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: key ####### marked as ultimately trusted
gpg: directory '/path/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/home/.gnupg/openpgp-revocs.d/#1#2#3#4#5#6#7#8#9#0.rev'
public and secret key created and signed.

pub   rsa2048 2020-03-13 [SC]
      #1#2#3#4#5#6#7#8#9#0
uid                      HelloWorld <[email protected]>
sub   rsa2048 2020-03-13 [E]


匯出 Public Key 靠這招:

$ gpg2 --armor --export "HelloWorld <[email protected]>" > your-pubkey.asc

$ gpg2 --armor --export "#1#2#3#4#5#6#7#8#9#0" > your-pubkey.asc


匯出 private key:

$ gpg2 --export-secret-keys "HelloWorld <[email protected]>" > your-private-key.asc

$ gpg2 --export-secret-keys "#1#2#3#4#5#6#7#8#9#0" > your-private-key.asc


列出目前的 keys:

$ gpg2 --list-keys
/path/.gnupg/pubring.kbx
------------------------------------
pub   rsa2048 2020-03-13 [SC]
      #1#2#3#4#5#6#7#8#9#0
uid           [ unknown] HelloWorld <[email protected]>
sub   rsa2048 2020-03-13 [E]

$ gpg2 --list-secret-keys
/path/.gnupg/pubring.kbx
------------------------------------
sec   rsa2048 2020-03-13 [SC]
      #1#2#3#4#5#6#7#8#9#0
uid           [unknown] HelloWorld <[email protected]>
ssb   rsa2048 2020-03-13 [E]


刪除 Keys,若該 key 組合內有 secret key ,需要先刪除 secret key  :

$ gpg2 --delete-secret-keys "#1#2#3#4#5#6#7#8#9#0"

刪除 public key:

$ gpg2 --delete-keys "#1#2#3#4#5#6#7#8#9#0"

匯入 Keys:

$ gpg2 --import your-private-key.asc
$ gpg2 --import your-pubkey.asc


使用 Keys 做加密,產生 *.gpg 檔案:

$ cat /tmp/text
hello world
$ gpg2 -r "#1#2#3#4#5#6#7#8#9#0" -e /tmp/text
$ ls /tmp/text.gpg
/tmp/text.gpg


使用 Keys 對 *.gpg 解密,若沒有 private key 獲得到:

$ gpg2 -r "#1#2#3#4#5#6#7#8#9#0" -d /tmp/text.gpg
gpg: encrypted with 2048-bit RSA key, ID ######, created 2020-03-13
      "HelloWorld <[email protected]>"
gpg: decryption failed: No secret key


有 private key 就會正常解出來:

$ gpg2 -r "#1#2#3#4#5#6#7#8#9#0" -d /tmp/text.gpg
gpg: encrypted with 2048-bit RSA key, ID #, created 2020-03-13
      "HelloWorld <[email protected]>"
hello world

2020年2月27日 星期四

kobo desktop 下載 , kobo app download @ macOS / Windows

昨晚跟在 FB 工作的學弟通個電話,聊了一陣子,被推坑看了一本 2017 年再版的書。找了一陣子,實體書都賣光了,只剩 KOBO 有電子書在賣,恰好當年 Kobo 在台灣推廣時,一直說第一次購書可以折 100 元,當時 NETFLIX 給力很夯,只是我最後買了實體書 XD 因此,那一百元折價一直沒用,就這樣很快地刷卡。

接著下一刻就囧了,大家都說可以用電腦看,找了很久台灣官網上沒有下載連結,輾轉得知改用美版網站就有了!
  • https://www.kobo.com/us/en/p/desktop 下方有 Download Now
    • https://kbdownload1-a.akamaihd.net/desktop/kobodesktop/kobosetup.dmg
    • https://kbdownload1-a.akamaihd.net/desktop/kobodesktop/kobosetup.exe
最近共用了 Kindle 看書、豆瓣閱讀 Mobile app 跟 Web 觀看付費書籍,接下來試試 Kobo 囉

2020年2月19日 星期三

[Linux] 在 Ubuntu 重編 git 處理 error: gnutls_handshake() failed: A TLS fatal alert has been received @ Ubuntu 12.04

當使用 git clone https:// 來源時,會出現以下錯誤訊息:

error: gnutls_handshake() failed: A TLS fatal alert has been received. while accessing https://service/project.git/info/refs
fatal: HTTP request failed

解法就是重編 git (細節似乎是 gnutls 問題)

流程:
  1. 到 https://github.com/git/git/releases 下載 git 程式碼(此例用 v2.25.1)
  2. 安裝 libcurl4-openssl-dev libexpat1-dev 等編譯環境
  3. 編他:./configure --with-expat --with-curl --with-openssl
連續動作:

$ dpkg -l | grep gnutls
ii  libcurl3-gnutls                             7.22.0-3ubuntu4.17                      Multi-protocol file transfer library (GnuTLS)
ii  libgnutls-dev                               2.12.14-5ubuntu3.14                     GNU TLS library - development files
ii  libgnutls-openssl27                         2.12.14-5ubuntu3.14                     GNU TLS library - OpenSSL wrapper
ii  libgnutls26                                 2.12.14-5ubuntu3.14                     GNU TLS library - runtime library
ii  libgnutls26:i386                            2.12.14-5ubuntu3.14                     GNU TLS library - runtime library
ii  libgnutlsxx27                               2.12.14-5ubuntu3.14                     GNU TLS library - C++ runtime library
ii  libneon27-gnutls                            0.29.6-1ubuntu1                         HTTP and WebDAV client library (GnuTLS enabled)
$ sudo apt-get install libcurl4-openssl-dev tcl-dev libexpat1-dev
$ wget https://github.com/git/git/archive/v2.25.1.tar.gz
$ tar -xvf v2.25.1.tar.gz && cd git-v2.25.1
$ make configure
$ ./configure --with-expat --with-curl --with-openssl
$ make test
$ sudo make install


其中 make test 會跑一陣子,可以選擇執行 XD

2020年2月8日 星期六

[macOS] 替 Macbook Pro 2015 13寸 升級為 1TB SSD / MacBook Pro (Retina, 13-inch, Early 2015) / MacBookPro12,1

MacbookProUpgradeSSD01

前陣子 幫 Macbook Pro 更換膨脹電池 時,意外發現這台 2015 年的 Macbook Pro 可以自行換 SSD,瞬間心花怒放 XD 實在是當年花了不少錢買了 256GB 的 Macbook Pro ,接著近半年想要編譯 Chromium / CEF 時,常常移來移去就是為了多生出個 40GB,更別說系統槽光 Xcode 要更新時必須砍掉 Xcode 重新安裝才行,不能當下直接更新成新版。就這樣,評估一下大家換 SSD 的心得就衝了。

首先 Apple SSD 是客製化的接頭,需要買一張轉卡。其次則是要買大廠 SSD 才不會踩到 macOS 深度睡眠的問題。於是乎就賭了一下,在蝦皮買了 250 元左右的轉卡(網路上也有人賣到 500),接著採購 INTEL 660p 1TB 的 SSD ,真是奢侈的空間啊!就這樣大概 4000 左右就完成更新了!而選擇買兩百多的轉卡,是因為覺得上頭沒有 IC?應當只是接頭轉換,再加上很多討論文就說買兩百多即可。而買 INTEL 660p 1TB (讀1800M/寫1800M) 而不買金士頓 A2000 1TB (讀2200M/寫2000M) 是覺得筆電已經五年前了,應當讀寫效率沒這麼好不需多追求多好,反正再過幾年說不定又換了筆電了?

而經歷過拆電池的洗禮,拆裝 SSD 變得十分簡單,反而出現的兩段插曲!

MacbookProUpgradeSSD03

MacbookProUpgradeSSD04

一則是 macOS USB 安裝碟出包,用去年做的 macOS USB 安裝碟開機會看到 macOS 多國語當機畫面,當下還以為硬體不合。後來換了一個 USB 重新製作就搞定

另一個則是 iCloud 同步問題,用了新硬碟後立刻重灌完畢,重灌後發現 "桌面與文件檔案夾" 並沒有自動同步,就是連目錄、檔案都沒看到任何蹤影,且連續兩天沒解決。試了很多怪招,像是回到舊硬碟關掉 iCloud、或是網路上常見的刪除 iCloud 帳號資料重新開機等等。後來覺得照片和 Safari 書籤/密碼都有同步,那就擺在個幾晚,果真第三天起床後就看到了!估計是 iCloud 的桌面被我擺太多東西,所以要花很多時間初始化同步環境。

當身邊就多了一隻 256GB SSD 時,覺得晾在很浪費,躊躇著到底要不要花錢買專用的外接盒,研究完想了兩天,牙一咬就下單了,後來得知連貴鬆鬆的 OWC-MAC Envoy Pro 外接盒不一定支援,例如 Apple 原廠 SSD 若是 Toshiba 就不行,有需要的話,建議可以先多問一下賣家。

MacbookProUpgradeSSD02

MacbookProUpgradeSSD05

看來年初繳的3C稅非常恐怖:
  • 轉卡含運: 250元
  • Intel 660p 1TB: 3900元
  • OWC-MAC Envoy Pro 外接盒: 3300元
再加上處理膨脹的電池問題,不多不少就剛好破萬 Orz

除了轉卡只能在蝦皮等非官方地方買外,其餘的則盡可能在知名網路購物平台購買,主要是東西很貴,不小心出問題很麻煩。

而除了 OWC 外,創建也有出一堆 Macbook SSD 升級包,像是升級包內的 SSD 不用轉卡,直接是 Apple 的形狀(?),但...真的貴不少!大概貴一倍吧,像是升級包 7000 元(含 SSD + 外接盒) 只能擁有 240GB 的空間,若是 1TB 可是要價 1萬五了! 可參考 JetDrive 855 關鍵字或是 PCHOME Apple專用★SSD ,真是商機無限啊。

最後 又逢全球疫情 "遠端工作" 的時機興起,剛好擴充完的筆電環境可以無憂無慮在家工作了 :P 

2020年2月1日 星期六

[macOS] 替 Macbook Pro 2015 13寸 換電池 / MacBook Pro (Retina, 13-inch, Early 2015) / MacBookPro12,1 / A1582

Retina, 13-inch, early 2015 換電池 A1582

我也忘了多久,可能有半年以上,筆電一直蓋不緊。昨晚不知哪根筋不對,仔細觀摩了一下,推論應當是電池膨脹,沒多久隨意查到 A1582 這個電池型號,就在 PCHOME 24小時下單,雖說 PCHOME 24 標記在 "副廠-其他牌子" 有點抖,還是下手了。果真拿到貨看了一下,有些線的外觀有點兒粗糙,推論應當還不影響什麼,至少裝完從系統資訊可以看到:

  電量資訊:
  剩餘電量(mAh): 6515
  已充飽電:
  正在充電:
  總充電容量(mAh): 6581
  健康狀態資訊:
  循環使用次數: 0
  狀態: 正常
而拆電池的心得:
需要勇氣跟一支尺!因為電池是被黏著的,要拆必須想辦法扳起來,尺是個好幫手。
購買來的電池,通常都會附上特殊的五跟六螺絲,用來拆背蓋以及裡頭的螺絲。

Retina, 13-inch, early 2015 換電池 A1582

個人龜毛了點,覺得不美觀的地方:

Retina, 13-inch, early 2015 換電池 A1582

接著,拆完筆電背蓋,可以看到膨脹的景觀:

Retina, 13-inch, early 2015 換電池 A1582

開始動工,先把電池連接線拔起來:

Retina, 13-inch, early 2015 換電池 A1582

再把中間的連接線處的螺絲蓋保護移除,並把中間接線拔掉:

Retina, 13-inch, early 2015 換電池 A1582

最後則是右邊還有個螺絲要移除:

Retina, 13-inch, early 2015 換電池 A1582

接著就努力把電池拔起來吧!我是先靠著膨脹的電池空隙,先把左右兩邊的電池靠著尺插入後,左右旋轉尺來小幅度處理:

Retina, 13-inch, early 2015 換電池 A1582

Retina, 13-inch, early 2015 換電池 A1582

拆到只剩中間,就真的比較難拆,有那種要很使勁的那種勇氣,因為中間黏的面積也大:

Retina, 13-inch, early 2015 換電池 A1582

Retina, 13-inch, early 2015 換電池 A1582

接著,就要嘗試連接新的電池了!由於賣家都說,建議驗證完電池沒問題再粘上去,但實務上新的電池有透明板子在保護,其實無法先擺入再看看,我最後是先讓電池稍微在那個位置處連上連接好,再用筆電背蓋護一下來開機驗身,驗完立馬關機,轉過來,在拔掉電池連接線。

Retina, 13-inch, early 2015 換電池 A1582

Retina, 13-inch, early 2015 換電池 A1582

如此,就可以撤掉新電池的透明背板等保護,喬好位置後再讓他好好黏好:

Retina, 13-inch, early 2015 換電池 A1582

收工!可以把舊電池回收了

Retina, 13-inch, early 2015 換電池 A1582

2020年1月21日 星期二

[開箱] EZCast Ultra 與 Chromecast Ultra 比較

EZCast Ultra 01

不知為何手邊多了這產品可以體驗一下。大概是年節將近,不知該帶什麼伴手禮 (誤)

EZCast Ultra 02

包裝盒上有六國語言,有繁體中文!

EZCast Ultra 03

內容物有 EZCast Ultra 主體一個、一條 USB <-> Typc-C 線(供電)、一條 HDMI 公對公。因此 EZCast Ultra 已經跟主流產品一樣,採用 Type-C 接頭供電了。對比三年前在 2016 年底出產的 Chromecast Ultra 還在採用 Micro-USB ,也算是一時之選吧?

EZCast Ultra 04

開機畫面還滿精美的,這灣好眼熟,不曉得是不是義大利的?

EZCast Ultra 05

接著下載 EZCast app ,掃螢幕上的 QRCode 有引導設定,還可以透過藍芽設定 EZCast Ultra 對外網路,滿方便的。

EZCast Ultra 06

最後,提一下跟 Chromecast Ultra 主要差別,因為我剛好是 Chromecast 重度使用者 XD

EZCas tUltra and Chromecast Ultra

  • 外觀上不會差太多,Chromecast Ultra 用 Micro-USB 接頭供電;EZCast Ultra 用 Type-C 接頭供電
  • 兩者都支援 4K 輸出
  • Chromecast Ultra 要求一定要有對外網路的使用環境;EZCast Ultra 沒有對外網路時,一樣可以使用,如透過 DLNA 協定,把區網內的 NAS 拉影片來播放
  • Chromecast Ultra 有支援 DRM,看 Netflix 必備的環境;EZCast Ultra 支援 MIRCAST/AirPlay/DLNA 常見標準協定,支援很多作業系統,若沒有在觀看 DRM 保護的數位影音,應當都還非常方便
  • Chromecast 閒置時可以播放個人照片;EZCast Ultra 則是有動態風景桌布可看
  • Chromecast 跟 EZCast Ultra 都有支援音控服務,但 Chromecast 是天生跟 Google 生態完美整合,如 Android / Chrome 都可方便投放,像常用 Spotify 就可以很方便的叫 Chromecast 或 Google Home mini 播放音樂
EZCast Ultra 有其跨 OS 優勢跟特色,補足 Chromecast Ultra 先天設計的限制,都是很不錯的產品,希望哪天 EZCast Ultra 可以補齊 DRM 就完美了!

據說很多公司想跟 NETFLIX 談 DRM 都不是很順利,大多要求 Android-based 類的產品,如何能取得 NETFLIX 公司信任,真是一門學問。