2021年10月22日 星期五

使用中國境內公司建立 Google Adsense 帳號以及收匯事宜:Google 合同、發票



起因是2021年春天起,Google開始對 Youtube Ads 項目開始有預扣稅額,原本一直用香港收匯,這時美國與香港關係上,在 Youtube Ads 項目上需預扣 30% 稅額。後來發現中國跟美國有租稅優惠,可以省掉這段,因此改用中國境內公司來收稅,由於公司剛好有這樣的組織架構,就來試試看吧。

首先,建立個中國 Google Adsense 上,最容易踩到的雷就是地址驗證無法通過 XD Google 永遠都寄平信去驗證地址,接著地址是在園區內,不知是不是少寫了聯絡人跟電話啥關係?連寄了四次都沒收到,是的,一次要耗時一個月。

最後跟 Adsense 客服要求,能否公司支付快遞費,請用快遞吧!誰知道,只要累積四次沒驗證通過後,這時就蹦出了另一種驗證方式:主動遞交公司資訊證明文件(需有地址資訊)。就這樣,拿個公司營業執照上傳後一天就搞定了,那過去四個月的時間是?接著 Google Adsense 立馬就匯錢到指定的中國境內銀行,又是另一個故事了。

Google Adsense 匯款到中國境內銀行時,銀行會先把錢擺在虛擬帳號(中間帳),並且要求公司提交文件,包括與 Google 合同以及發票。故事的起因是廣告費歸類在服務費,這時就得提供這些資訊,中國境內收匯真的滿嚴格的,這些在香港銀行沒有碰到。

其實 Adsense 網站上並沒有哪邊可以下載 Google 合同,並且討論區只有滿滿的抱怨文,可能大多都是使用個人身份收匯吧?大部分的解法是嘗試拿 "Adsense 服務條款" 列印下來去申請看看。

再次詢問 Adsense 客服,才發現這也是個申請服務 XD 在信中說明後,得到回信,要求提供公司中英文名字和公司銀行中英文名字後,可以在一週內收到 Google 合同(就一份文件帶有 Google 章),而發票部分,請用原先的 付款收據 即可,把網頁輸出成 PDF 遞交即可,如此終於可以收下錢了。

如此,結案 Orz 一段將近半年的旅程真是漫長。

[書] 矽谷阿雅 追不到夢想就創一個!從台灣記者到臉書電商產品經理的顛覆筆記

斷斷續續地把這本書看完了。原本是在 kobo 買大量書籍湊總額買的XD 剛好那一陣子常看到他的出現,好像在宣傳他的課程,看了點書目跟一堆名人推薦,想說應該有什麼精神是可以吸收的就買了,就這樣把它看完了。


最大的心得還是凡事積極進取,成功的人總會找方法的。另外,則是對美國昂貴的學費,多了另一種投資思維。書末提到了美國昂貴的學費,通常是一種投資概念,出去後只要花 2-3 年就可以賺回來,同時,進入名校獲得的標籤/人脈是無價的人生寶藏。


翻完書後,讓我想起以前念大學時,有碰過那種教授不收補習班出身或是要從同大學部上來的學生,粗略了解教授的意思,想要找到那種有潛力自學的人。過去大學教育的本質著重在讓學生有自學能力,教授稍微侷限在找出有潛力的人,因此故意避開那類不自我整理知識的人才。我以前覺得這類教授的說詞也不錯,但出社會後,反而更加能接受透過”方法”加速自己成長還是最重要的,甚至近幾年的數位教育夯,甚至有那個風向,主導老師們不要在課堂教育,讓最會教的人製作學習影片,讓學生看最會教的人就好,而一般實體課仍保留,主要讓學生來問問題。


如同創業圈常講:點子不重要,執行力才重要。(雖然並不是說點子不重要啦 XD 努力的方向錯了也是場空的)


回過頭來,這本書帶給人的心態是不錯的,當發現自己有弱點時,請努力去補好它,而不是當個藉口就停滯下來。此外,則是著重在團體戰角度,我習慣把這稱作資方思維(花錢買別人的時間),中性一點應該是管理者思維,凡事不要自己單打獨鬥去證明什麼,而是透過團體戰,缺什麼就找什麼樣人才來幫忙。


這時跟幾位朋友哈拉這位作者時,包括人在 FB 當 RD 的學弟,立馬都互給出很強烈的負面思維 XD 容易把管人工作當作零產出,可能是職場上碰到太多悶虧了吧?這時,只能凡事盡可能正向思考吧!不因言廢人,也不因人廢言。


這本書提到的一些精髓,我覺得還是很受用醒腦的,適合 25 歲以下的人閱讀,超過 25歲可能心思都被定型或是早已有所對應成就。

2021年10月19日 星期二

Python 筆記 - 研究 yt-dlp / youtube-dl 流程與開發方式

幫同事查了一下程式邏輯,下手的方向:
  • 入口點在哪
  • 到底怎樣跑
  • 我要怎樣擴充或貢獻一個影音網站 extractor
github.com/yt-dlp/yt-dlp 來看,入口點就直接看 yt-dlp.sh 即可:

% git clone https://github.com/yt-dlp/yt-dlp
% cd yt-dlp 
% python3 yt_dlp/__main__.py 
Usage: __main__.py [OPTIONS] URL [URL...]

__main__.py: error: You must provide at least one URL.
Type yt-dlp --help to see a list of all options.

接著掃一下 yt_dlp/__main__.py 和 yt_dlp/__init__.py 就略懂他在處理 command line 的參數,而程式的本體是在 yt_dlp/YoutubeDL.py 檔案,一樣在追個 yt_dlp/YoutubeDL.py 裡的 class YoutubeDL 中的 def __init__() 裡,可以看到 self.add_default_info_extractors() 這步,如此就把 yt_dlp/extractor/ 眾多網站影音分析爬蟲狀態初始化好。

接著,給予一則影音網站連結:

% python3 yt_dlp/__main__.py https://www.facebook.com/UserID/videos/VideoID/

程式就會在 yt_dlp/YoutubeDL.py extract_info() 分析做事,像是把所有 extractor 都瀏覽一次,透過呼叫 ie.suitable(url) 的機制判斷。這時可以對應在 yt_dlp/extractor/common.py 中,看到 class InfoExtractor(object) 裡的 suitable() 定義並用 _match_valid_url() 和 _VALID_URL 實作,這時回去猜 yt_dlp/extractor/facebook.py 時,找不到 suitable() 也找不到 _match_valid_url() 時,就可以猜應該是 _VALID_URL 的實作方式了。

回到 yt_dlp/YoutubeDL.py 時,在 extract_info() 分析做事時,找到合適的 extractor 後,就會立即靠 ie.extract(url) 解析,而實際實作是靠 _real_extract()。再找一下 yt_dlp/extractor/facebook.py 中是否有定義 extract() 或 _real_extract(),即可當作解析網站的程式邏輯入口點。

最後,想要擴充某個網站或是修補加強某個網站,就可以去改對應的 extractor 了。並且可以把在當前目錄下,不斷靠 yt-dlp.sh 去實測,也滿方便的。

2021年10月16日 星期六

藝術品

用說書服務大概衝刺了不少知識或資訊的累積,真的滿優的,雖說也會擔心不小心被洗腦了 XD 整體上還是很佩服樊登說書,就是一個 CP 值極佳的知識吸收工具,工具的好壞還是回歸使用者怎樣看待與善用。

大概前幾年開始用 podcast 聽一些國內外的訪談演講,便會覺得國外內容發達,拿來練英聽是不錯,但當作知識吸收就效率慢,剛好沒點太多技能在非母語上,很難 N倍速吸收。並且扣除掉演講類型的 podcast 外,許多都太過碎片化,直到碰到說書服務,且一口氣就是講 30-60 分鐘的說書,跟檯面上只說個 5-15 分鐘的 Youtuber 說書內容有很顯著的程度差距的,最重要的是可以融會貫通的提及幾本經典書共通點。暫時先持續享受這速食知識體驗,接著買了一堆電子書等著清 囧

回過頭來,發現年紀到了一個時間點後,看看周邊的大大們也對等的累積成就,沒有人是停留在之前的狀態的,就算有也長出不一樣的技術樹,如此就會想起跟幾位前輩哈拉一些八卦議題,俗稱的正妹的朋友還是正妹,強者的朋友還是強者。大概就能力越強、責任越大,人越有地位,主動攀談的朋友就肯定越多 XD 對應的八卦是強者們的領域,絕對存在許多雜訊或誘惑的。

回顧著周邊大大們,就像美術館裡的藝術品,只能遠遠欣賞著,沒什麼互動機會。當彼此努力向前者,也各處開花的,大家各自在自己的機緣處深耕,倒也不用說什麼離開舒適圈的事,強者是自成生態系,其舒適圈腹地是不斷擴大的,能力強大到做什麼事都像在舒適圈,世界的美好就是建構在這種最大亂度,不要求每個人都走同樣的路線。

最後 ,筆記一下近期的閒逛,無意間看到了一位要捐出版稅的大大,說真的我是被捐版稅吸引到的,恰好書名的領域跟工作有點相關,就仔細看了一下內容,依照內容原先以為也是三五、四十歲的人生經驗,結果在挖掘下去,才發現是二字尾的大大,這時就很納悶,究竟是怎樣的成長環境造就了這等人才?果真個性影響一生的選擇,其成長過程並不是俗稱的速成路線,在年輕時期便經歷過家庭/求學/人生各種議題的摸索跟闖蕩,造就目前的成就,瞬間想起在研究單位的好友,高中時期半工半讀、大學碰到恩師,接著在最高學府闖蕩時,碰到不認同的教授就選擇放棄不讀 XD 真的佩服。

此外也回憶起自己的一些個性,像是本身是略悲觀主義,但是屬於那種把最差的情境思考後,反而更大膽去嘗試任何事,且任何事的起步並不是追求一步到位的完美,而是追求不斷修正去趨近想要的目標。恰恰好看到有類似個性的人,感到共鳴。

認識一個人,就像翻開一本書,翻開的那一瞬間,就無法再把書闔上了。所以,書要慎選,不要亂翻,以免不小心就被感動了。

2021年9月20日 星期一

[書] 再讀一次: 你要如何衡量你的人生

今年春天被推坑聽了 樊登說書 ,這是個付費服務,趁活動就來個買一送一,大概每週都可以聽個 10本書,滿不錯的。前幾年我也用 podcast 聽一些英文頻道,像是 Talks at Google, Master of Scale with Reid Hoffman 等,後來發現,練著英聽不如加速自己的資訊/知識廣度的吸收,反而愛上樊登說書,像是陪小孩睡覺、運動時,可以用 2x 速度吸收,雖然對應有點減少了英聽的機會。

在三個多月內聽了幾百本書後,在回憶有哪些想深讀的書籍,其中第一本就是 克里斯汀生 的 你要入如何衡量你的人生 。事實上,我也趁 KOBO 電子書特價時,也把 克雷頓‧克里斯汀生 相關的書籍都買來收藏,還包括:

創新者的用途理論、創新者的DNA、創新者的修煉、創新者的解答、繁榮的悖論、精讀克里斯汀生

不過,我大概還有一半還沒看過 XD 這次算是第二或第三次讀這本書,也買過實體書歲給幾位親友。每次讀都有種很深的省思,特別是用來調整生活步調、心態的時候,非常適合。部分篇章或思維比較強調宗教的,這部分我個人是給予尊重,但不一定接受,我自己解釋這些是信仰,有信仰的人,就像獵人漫畫中的庫拉皮卡,可以對生活注入更強烈的意志,只要表現出來的是好的,也都是好事的。

再次筆記一下再次閱讀的心情,要時常提醒自己,終究一生想要成為怎樣的人、是否有時常關注家庭跟親子發展、是否有播點時間去評估家庭成員的知識發展,有許多的投資是需要長時間的投入的,不能老想著立竿見影的標的。

2021年9月10日 星期五

Node.js 筆記 - Loopback 2 與 MySQL 查詢緩慢的瓶頸排除 @ loopback-connector-mysql, connectionlimit

花了兩天幫同事追一個問題:為何有個服務重啟後,一直處於不穩甚至無法服務的狀態,但隨著時間是有機會變穩定的。

這算是一個 IoT 老服務了,當裝置上線時會跟服務保持連線(Websocket),其中有一些認證跟服務會使用到 DB server。當運行好一段時間,沒什麼大問題,反應速度也很良好。但碰到服務發布時,都會有一段期間無法正常使用。

此現象主要是 IoT 裝置們當初設計了自動連上雲台的架構,每台裝置啟動後,依序連上雲服務。但是,當服務更新時,若採到斷線機制時,將導致大量的裝置重新連線,而連線過程若有要認證就會佔用到 DB query 資源。此時若有些回應慢,將導致有些裝置認為服務不正常而自行斷線,並且又進入 retry 機制,導致災難性的自家人打自家人 DDoS 世界奇觀。

追了好一陣子,鎖定了一套使用 node.js - loopback 2 框架的服務,並且追到 DB query 瓶頸。主要透過對 db 查詢結果做 cache ,可以大幅度改善狀態,去減少 db query。並且當下 db 並未忙碌到回應緩慢。包括用其他對應機器追蹤查詢 db server 效率,以及在雲台出問題的機器上,直接用 mysql command line 查詢,即可交叉驗證應當是 node.js - loopback 的狀態問題。

原本也有意先把套件都升一升,可惜要跳到 LoopBack 4 並不是簡單的事 Orz 最後,老覺得這件事還是不太正常,就用 concurrent limit loopback mysql 關鍵字逛逛,大部分都在討論連線異常,很少提到連線數的問題,很幸運最後找到 connectionlimit 關鍵字,並且在查一下 loopback-connector-mysql 得知預設是只用 10 這個數字!

  
if (isNaN(s.connectionLimit)) {
    s.connectionLimit = 10;
  }

因此,就只需要在 loopback 框架下的 datasources.json ,多定義 connectionLimit 即可,例如來個 100 ,並且在系統上追蹤連線數:

$ netstat -np --inet | grep "YourDBServerIP:3306" | wc -l
100

這樣就搞定啦!當時想說 loopback 2 太舊太難升,是不是弄個類似 mysql command line 查詢機制來頂著用 XD 還因此配合 server 環境用 php5 寫好小工具了:

% echo '{"db_host":"DB_IP","db_user":"DB_USER","db_pass":"password","db_database":"DB_NAME","db_query":"SELECT count(*) FROM device;"}'  | jq ''     
{
  "db_host": "DB_IP",
  "db_user": "DB_USER",
  "db_pass": "password",
  "db_database": "DB_NAME",
  "db_query": "SELECT count(*) FROM device;"
}

% echo '{"db_host":"DB_IP","db_user":"DB_USER","db_pass":"password","db_database":"DB_NAME","db_query":"SELECT count(*) FROM device;"}'  | php db_helper.php /tmp/db_helper.log N M | jq ''
{
  "input": [
    [
      {
        "count(*)": "1234567"
      }
    ]
  ],
  "log": [
    0.34358501434326,
    0.11107587814331,
    0.47047901153564
  ],
  "status": true
}

還設計了 db_helper.php 能不能支援最多跑 N 隻,每隻也來個 timeout M 秒,並透過 lock 機制來保護溝通等,想想這段旅行也是滿好玩的。先把尚未妥善測試的 php5 工具丟到 github 記一下。

2021年8月25日 星期三

[macOS] 使用 hashcat 驗證/回憶 iOS iPhone/iPad iTunes 備份檔 密碼

故事起源於幫長輩換手機,這時手機有用過 itunes 備份過一份並且加密,但...怎麼都想不起密碼。在網路上找尋解法時,大概可以看到幾款破解軟體(推論是免費試用,真正要破解時需加價)。此刻的不方便,反而要感謝起 Apple 資安服務,高規格保護資料!大家的破解原理都是一樣的:暴力解-猜密碼。

研究後,發現有個 hashcat 的 open source 統包了眾多加密系統的猜密碼任務,而網路上 2017年也以人講解使用過程,還滿仔細的:

Crack Encrypted iOS backups with Hashcat

筆記一下過程:
  1. 先找到 iOS 備份檔擺放的位置: https://support.apple.com/en-us/HT204215
    • macOS: ~/Library/Application Support/MobileSync/Backup/
  2. 備份都是目錄結構,找到目錄中的 Manifest.plist 檔案
  3. 依照裡頭的資訊,準備餵給 hashcat 的資料,可分成 iOS 10 以上跟 iOS 9 以前,組成 inputFromManifest.txt 資料
    • Less then iOS 10:
      • $itunes_backup$*<ver>*<WPKY>*<ITER>*<SALT>**
    • iOS 10 or later:
      • $itunes_backup$*<ver>*<WPKY>*<ITER>*<SALT>*<DPIC>*<DPSL>
如此,就可以靠 hashcat 來運算,其原理包括支援平行處理(GPU)去算出密碼,包括從字典檔、暴力猜等等的。此外,關於上述 2~3 步驟,其實也有人佛心提供工具:github.com/philsmd/itunes_backup2hashcat,透過 perl 小程式自動幫人從 Manifest.plist 組出資訊:

% perl itunes_backup2hashcat.pl yourBackupDir/Manifest.plist 
$itunes_backup$*10*#######*####*######*######*##################
% perl itunes_backup2hashcat.pl yourBackupDir/Manifest.plist > inputFromManifest.txt

後續就著重 hashcat 的指令筆記,若要更多指令資訊,逛個官方網站是最方便的:hashcat.net/wiki/doku.php?id=hashcat

以在 https://www.youtube.com/watch?v=MMySnPzsPYU 內提供的範例資訊:

% cat inputFromManifest.txt 
$itunes_backup$*10*c8c96e8d6175f1356da6dcf5791ad76be166232b4a01bd6974abb27c39034993591c15ba03a14e3b*10000*0ca3ccfc453b32f9ca9aceb754b87f4e0ab9cb95*10000000*bdee6869caa7999e9576f390a248a29f38d10d6f

這時都是 iOS 10 的密碼,因此在使用 hashcat 會採用 -m 148000 參數。若是 iOS 9 以前,要用 -m 14700 參數這些都有定義在官方網站的 wiki 中。

第一招:暴力解,此例猜4個數字:

% hashcat -d 1 -m 14800 ./inputFromManifest.txt -a 3 '?d?d?d?d'
hashcat (v6.2.3) starting

OpenCL API (OpenCL 1.2 (Jun 17 2021 15:24:17)) - Platform #1 [Apple]
====================================================================
* Device #1: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz, 16320/16384 MB (4096 MB allocatable), 4MCU
* Device #2: Intel(R) Iris(TM) Plus Graphics 640, skipped

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates

Optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt
* Brute-Force
* Slow-Hash-SIMD-LOOP
* (null)

Watchdog: Temperature abort trigger set to 100c

Host memory required for this attack: 1 MB

[s]tatus [p]ause [b]ypass [c]heckpoint [f]inish [q]uit => 

這時隨時都可以按 s 去看暴力解的進度:

Session..........: hashcat
Status...........: Running
Hash.Name........: iTunes backup >= 10.0
Hash.Target......: $itunes_backup$*10*c8c96e8d6175f1356da6dcf5791ad76b...d10d6f
Time.Started.....: Wed Aug 25 20:37:03 2021 (6 mins, 50 secs)
Time.Estimated...: Wed Aug 25 23:30:00 2021 (2 hours, 46 mins)
Kernel.Feature...: Pure Kernel
Guess.Mask.......: ?d?d?d?d [4]
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:        1 H/s (13.34ms) @ Accel:128 Loops:256 Thr:1 Vec:4
Recovered........: 0/1 (0.00%) Digests
Progress.........: 0/10000 (0.00%)
Rejected.........: 0/0 (0.00%)
Restore.Point....: 0/1000 (0.00%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:8056320-8056576
Candidate.Engine.: Device Generator
Candidates.#1....: 1234 -> 1124
Hardware.Mon.SMC.: Fan0: 100%
Hardware.Mon.#1..: Temp: 66c

[s]tatus [p]ause [b]ypass [c]heckpoint [f]inish [q]uit => 

而幸運解出密碼時,會顯示以下資訊,主要是 Status 會顯示 Cracked 而密碼則是紀錄在冒號後面 (:1234)

$itunes_backup$*10*c8c96e8d6175f1356da6dcf5791ad76be166232b4a01bd6974abb27c39034993591c15ba03a14e3b*10000*0ca3ccfc453b32f9ca9aceb754b87f4e0ab9cb95*10000000*bdee6869caa7999e9576f390a248a29f38d10d6f:1234
                                                          
Session..........: hashcat
Status...........: Cracked
Hash.Name........: iTunes backup >= 10.0
Hash.Target......: $itunes_backup$*10*c8c96e8d6175f1356da6dcf5791ad76b...d10d6f
Time.Started.....: Wed Aug 25 20:37:03 2021 (8 mins, 26 secs)
Time.Estimated...: Wed Aug 25 20:45:29 2021 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Mask.......: ?d?d?d?d [4]
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:        1 H/s (11.09ms) @ Accel:128 Loops:256 Thr:1 Vec:4
Recovered........: 1/1 (100.00%) Digests
Progress.........: 512/10000 (5.12%)
Rejected.........: 0/512 (0.00%)
Restore.Point....: 0/1000 (0.00%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:9984-9999
Candidate.Engine.: Device Generator
Candidates.#1....: 1234 -> 1124
Hardware.Mon.SMC.: Fan0: 100%
Hardware.Mon.#1..: Temp: 65c

Started: Wed Aug 25 20:36:57 2021
Stopped: Wed Aug 25 20:45:30 2021

第二招:將可能的密碼寫在 myPasswordlist.txt 中去驗證,指令:

% cat myPasswords.txt 
5678
09876
1234

% hashcat -d 1 -m 14800 ./inputFromManifest.txt ./myPasswords.txt 
hashcat (v6.2.3) starting

OpenCL API (OpenCL 1.2 (Jun 17 2021 15:24:17)) - Platform #1 [Apple]
====================================================================
* Device #1: Intel(R) Core(TM) i5-7360U CPU @ 2.30GHz, 16320/16384 MB (4096 MB allocatable), 4MCU
* Device #2: Intel(R) Iris(TM) Plus Graphics 640, skipped

Minimum password length supported by kernel: 0
Maximum password length supported by kernel: 256

Hashes: 1 digests; 1 unique digests, 1 unique salts
Bitmaps: 16 bits, 65536 entries, 0x0000ffff mask, 262144 bytes, 5/13 rotates
Rules: 1

Optimizers applied:
* Zero-Byte
* Single-Hash
* Single-Salt
* Slow-Hash-SIMD-LOOP
* (null)

Watchdog: Temperature abort trigger set to 100c

Host memory required for this attack: 1 MB

Dictionary cache built:
* Filename..: ./myPasswords.txt
* Passwords.: 3
* Bytes.....: 16
* Keyspace..: 3
* Runtime...: 0 secs

The wordlist or mask that you are using is too small.     
This means that hashcat cannot use the full parallel power of your device(s).
Unless you supply more work, your cracking speed will drop.
For tips on supplying more work, see: https://hashcat.net/faq/morework

Approaching final keyspace - workload adjusted.           

$itunes_backup$*10*c8c96e8d6175f1356da6dcf5791ad76be166232b4a01bd6974abb27c39034993591c15ba03a14e3b*10000*0ca3ccfc453b32f9ca9aceb754b87f4e0ab9cb95*10000000*bdee6869caa7999e9576f390a248a29f38d10d6f:1234
                                                          
Session..........: hashcat
Status...........: Cracked
Hash.Name........: iTunes backup >= 10.0
Hash.Target......: $itunes_backup$*10*c8c96e8d6175f1356da6dcf5791ad76b...d10d6f
Time.Started.....: Wed Aug 25 20:52:58 2021 (10 secs)
Time.Estimated...: Wed Aug 25 20:53:08 2021 (0 secs)
Kernel.Feature...: Pure Kernel
Guess.Base.......: File (./myPasswords.txt)
Guess.Queue......: 1/1 (100.00%)
Speed.#1.........:        0 H/s (0.18ms) @ Accel:128 Loops:256 Thr:1 Vec:4
Recovered........: 1/1 (100.00%) Digests
Progress.........: 3/3 (100.00%)
Rejected.........: 0/3 (0.00%)
Restore.Point....: 0/3 (0.00%)
Restore.Sub.#1...: Salt:0 Amplifier:0-1 Iteration:9984-9999
Candidate.Engine.: Device Generator
Candidates.#1....: 5678 -> 1234
Hardware.Mon.SMC.: Fan0: 20%
Hardware.Mon.#1..: Temp: 58c

...

大概就這兩招筆記一下,未來忘記密碼時,就可以靠 hashcat 幫忙批次測試記憶中的密碼了

2021年7月20日 星期二

Node.js 筆記 - 使用 Puppeteer 進行 JS Injection 與 Custom function 定義實作 @ Puppeteer v10, Node.js v16.5.0

之前使用 Puppeteer 方便自己撰寫一些監控 http requests ,像是即時監控 remote resource 變化。接著,要來把玩 JS Injection。

如果使用 Puppeteer 的用法是包括持續瀏覽網頁的話,那適合在網頁 onload 的情況下植入:

// https://pptr.dev/#?product=Puppeteer&version=v10.0.0&show=api-event-domcontentloaded
// https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
//
// The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM has been loaded, without waiting for resources to finish loading.
//
page.on('load', async () => {
console.log('on.load');
});

如果是瀏覽網頁後,立馬植入 JS Code 運行並且任務就結束了,那就是適合在 network 連線閒置後處理:

await page.goto(target_url, {
waitUntil: 'networkidle0',
});
console.log('networkidle0');

而 JS Injection 的方式:

await page.evaluate((js_code) => {return Promise.resolve(window.eval(js_code));}, js_resource_content);

其中 js_resource_content 就是預計植入的 JS Code 字串。例如有多份程式碼要植入,可以跑回圈:

for(let i=0 ; i<js_resource.length ; ++i) {
console.log('inject: '+js_resource[i]+', size: '+js_resource_content[i].length);
await page.evaluate((js_code) => {return Promise.resolve(window.eval(js_code));}, js_resource_content[i]);
}

其中 js_resource[] 紀錄的是 js code 網址,而 js_resource_content[] 是紀錄該 js code 網址的內容。

最後,提一下自訂函數的寫法:

// https://github.com/puppeteer/puppeteer/blob/main/examples/custom-event.js
// Define a window._puppeteer_helper function on the page.
await page.exposeFunction('_puppeteer_helper', (data) => {
console.log(`_puppeteer_helper fired`);
});

如此,又可以靠 JS Injection 去呼叫 window._puppeteer_helper ,或是靠 phage.evaluate 執行了:

let result = await page.evaluate((data_to_page) => {
let data = data_to_page;
return Promise.resolve(window.eval(`
window._puppeteer_helper();
`));
}, 'nothing');

2021年7月14日 星期三

XBOX 遊戲目錄 6歲以下 幼兒遊戲清單 @ XBox One S

當年買 XBOX ONE S 時,想說花五千左右買來當 藍光DVD播放器 的,結果...不小心就再加碼了停產的 Kinect 感應器、連接線,接著又入坑買了些體感的遊戲,像是首選目標是 Just Dance 以及 Kinect 運動大會:對抗賽 ,事實證明,果真該先查遊戲品價再買 XD Kinect 運動大會 似乎鳥掉不少,玩起來滿常跟感應狀態不順,大概只有玩網球跟保齡球比較順吧。不過 Just Dance 的體驗滿讚的,堪稱完美的體感互動。

近一年半,因為台灣疫情的關係,開始在 XBox.com 較大筆的消費買了不少遊戲,當時以直在想 XBox Store 裡一堆 18禁 血腥遊戲,還真不知該買什麼遊戲給小孩,之後在網路上找了一下,找到不少人推薦樂高的遊戲,入門款:樂高玩電影 LEGO THE MOVIE VIDEOGAME,趁著 XBox Store 特價時候,入手小試身手。以下的遊戲就偏向把手遊戲,對於 Kinect 體感就停留在 Just Dance 才會打開來用了。

先提使用 XBox Store 就是買數位版遊戲心得,好處:
  • 不定期會有大幅度的特價
  • 購買不用等實體光碟片寄到家
  • 不用擔心 XBOX 遊樂器哪天開始挑片
缺點:
  • 遊戲很大時,要等下載很花時間,家裡網速不快時,都可能會等上數小時才能下載完畢(20GB 遊戲大小,每秒 10MB/s 下載,要花 35分鐘)
  • 遊戲不玩時,不像實體遊戲片可以在二手市場交易,之前買的 Just Dance 2018 是實體光碟片,想玩買新的一代時,就把舊的在二手市場賣了,可以回收一半以上還滿不錯的(買一千多賣六七百)。
接著來聊聊樂高遊戲了!請留意,樂高遊戲,某個角度上還是帶有暴力意識,分級來說都是建議12歲以上的小孩的,例如要打掉建築物、打壞人等等,只是 XBOX 上太少幼兒把玩的遊戲,就試著引導他們當作解題遊戲。若很在意分級設計,還是避開比較安心。

買樂高遊戲還有個重點:可以兩人一起玩的。

小孩第一次接觸樂高遊戲,在看不懂文字上果真難上手,需要大人陪著,但大概撐過去後,便開始感受到遊戲有趣的點:解題、尋找成就感。雖然他們根本連劇情都不懂,也不懂樂高音樂以及出場的角色們,但小孩很會腦補,就像玩扮家家酒那樣。看著小孩這樣兩週玩著這款玩到幾乎破關,覺得滿驚訝的。大概一天玩 2 小時左右。有些場景因為沒有讀劇情,不懂文字上描述解題方向而卡關著,推論應該有八成都可以自己摸索,剩下兩成要去幫忙點一下。

第一次感受到長劇情遊戲帶給小孩的樂趣。回想起以前玩 PC 或 TV 遊戲時,其實很多看不懂也繼續玩,像是國小玩超任的熱血硬派也看不懂日文,一樣暑假天天玩天天練功了一個多月。(現在靠模擬器跟電腦加速玩,大概一週內就能破關)

爾後趁 XBox Store 不定期特價(感覺兩三週會有一輪樂高系列遊戲特價),就順便收購老遊戲而不是買最新不特價的遊戲,收藏了:
其中,我自己覺得 The LEGO Movie 2 Videogame 複雜度又比第一代高,就算玩過第一代也不一定能上手,因為複雜度上更重文字的描述,像是要建制對應的東西,選單更加複雜,可能將近要花 50% 時間帶小孩玩,相較之下還是 The LEGO Movie Videogame 適合學齡前小孩把玩,算是致敬第一代才收藏 XD 整體上其實可以不用花錢買它的,他比較偏給青少年玩的遊戲。

依序嘗試了 The LEGO Movie Videogame、The LEGO NINJAGO Movie Video Game、LEGO® CITY Undercover、The LEGO Movie 2 Videogame、LEGO Marvel Super Heroes、LEGO® The Hobbit™,感覺 NINJAGO 跟 LEGO Marvel Super Heroes還滿快上手的,其餘有的是劇情看不懂稍微複雜就卡關了。由於樂高很經典,再加上上述遊戲都稱得上老遊戲了,在網路上還滿多攻略,甚至 bilibili 還有玩家闖關的影片紀錄,若擔心不會玩,可以先查一下

此外,在 NETFLIX 上剛好有很多樂高小電影,也可以搭配播來看看,像是 NETFLIX - 樂高玩電影 2

其他推薦遊戲:
扣掉樂高後,迪士尼大冒險跟 Woodle Tree Adventures 都是很超值的遊戲!有興趣可以嘗試迪士尼大冒險,裡頭的遊戲可以玩上好一陣子。以上的價錢都是台幣。

有興趣的可以一週逛個 XBox Store 優惠區一下 https://www.xbox.com/zh-tw/games/all-games?cat=onsale ,看看有沒有想收藏的遊戲。只是,現在 Switch 當道,不需刻意為了玩這些遊戲買 XBox ,親子遊戲還是 Switch 比較多。

2021年7月7日 星期三

MacBook Pro 13吋 換喇叭揚聲器 Speaker @ A1502, Retina, 13-inch, Early 2015,

MacBook 13 A1502 Speaker

Macbook 已經用了五年多了,大概年初開始發現喇叭破音,有看到很多坊間解法,像是拆掉喇叭後,試圖找到破點,甚至塗上一點薄薄的白膠來修復,但我第一次看拆喇叭的影片,就懶了 XD

接著原本單邊正常,撐了兩三個月終於一起壞光光了... 於是乎進入藍牙小喇叭的用法,結果因為疫情關係要看個直播又要開啟藍芽喇叭,久而久之,還是覺得不方便。且不知道是不是外接喇叭久了,換了揚聲器後,反而覺得內建音效很差...耳朵被慣壞了!

就這樣...隨意找了一下,用 A1502 (自己的 MacBook 型號) 加 喇叭 或 揚聲器 這關鍵字,就可以輕鬆找到哪邊有在賣,一對大概都賣台幣 1100 以上。再加上本身就已經敗過 米家精修螺絲起子套裝組 ,這時需要的工具都備齊了。

最後就拆一下筆電,斷一下電源,在輕輕的把螺絲卸下,並且照拆除的順序再裝回去即可。不是太大的問題。

由於 Youtube 上面已經有許多教學影片,非常建議先看過一次,留意拆法,像是喇叭要拆離主機板時該怎樣做。

MacBook 13 A1502 Speaker

MacBook 13 A1502 Speaker

[Linux] 修復 Ubuntu 12.04 無法安裝軟體 - W: Failed to fetch 404 @ Ubuntu 12.04

老機器環境:

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 12.04.5 LTS
Release: 12.04
Codename: precise

主要是在 server 運行 apt-get update 時,會出現一堆 404 問題:

W: Failed to fetch http://archive.ubuntu.com/ubuntu/dists/precise-backports/multiverse/binary-i386/Packages 404

追了一下應當是 12.04 已經不維護了。運氣不錯,有找到 old-releases.ubuntu.com 位置:
  • https://archive.ubuntu.com/ubuntu/
  • https://old-releases.ubuntu.com/ubuntu/
接著就動手換掉 /etc/apt/sources.lis 即可,包括來源 server 換完後,記得要再把 /var/lib/apt/lists 先移除:

$ sudo mv /var/lib/apt/lists /var/lib/apt/lists-backup
$ sudo mkdir -p /var/lib/apt/lists
$ sudo apt-get update

如此就會修復相關資訊,可以在靠 apt-get 安裝軟體了。

此外,若剛好在除錯時,刪除了已安裝過的軟體,接著做了上述後,又要安裝時,會碰到資訊對不上的問題,如:

dpkg:error processing package XXX (--configure):
subprocess installed post-installation script returned error exit status 10

解法就是清除 /var/lib/dpkg/info/ 下相關套件的資料後,重新安裝或是跑完後續流程:

$ sudo mkdir /var/lib/dpkg/info/backup
$ sudo mv /var/lib/dpkg/info/XXX* /var/lib/dpkg/info/backup/
$ sudo apt-get install -f

2021年7月3日 星期六

[開箱] 電子書閱讀器 Kobo Elipsa 10.3 吋

Kobo 10.2吋 電子書閱讀器

在 Kobo 也花了五千買電子書了 XD 一開始還滿喜歡用 PC app 觀看,想說還是買一台電子書閱讀器來用用吧!之前買過 Kindle 6吋的經驗,加上沒外出看書的需求,還是想大尺寸一點,剛好跟朋友哈拉 Kobo 沒出過大尺寸的,沒想到哈拉後沒幾週就看到推出 10.3吋的閱讀器,當時很快就下單,沒有想太多。

Kobo 10.2吋 電子書閱讀器

把玩的心得:
  • 裸機不含保護殼還滿輕的: 384g
  • 加上保護殼跟筆: 744g
Kobo 10.2吋 電子書閱讀器

大概 2010年 前研究電子書領域時,就曾跟某書店高層開會過,分享過心得,認為一瓶水 600g 是可以接受的範圍(替換掉女生包包內的東西),因此很期待 iPad 問世,我覺得加保護殼真的會覺得小重 QQ 但一台破萬也讓人不得不保護啊 XD 或許這就是 amazon kindle 6吋主流的原因吧?最終還是方便為主。

Kobo 10.2吋 電子書閱讀器

回過頭來,我自己使用 Kobo Elipsa 10.3 吋的心得,小缺點:
  • 首次登入 kobo 時,會輸入帳密,輸入後發現螢幕上會有鍵盤的殘影(但不影響使用)
  • 把 Kindle Paperwhite 6 翻頁速度快些,但跟 iPad 體驗上還是有一段落差
  • 觸控筆採用 AAAA 6號電池,稍微不好買,網路訂最方便(2個50元)
  • 據說 Kobo 一直以來都有各個裝置同步不即時的問題,甚至已經接近無法同步的體驗
優點:
  • 比 iPad 輕,只有讀書跟寫筆記功能,專心讀書
  • 不加保護殼時,質感不錯(可惜實務上應當還是會裝著保護殼)
  • 搭配 Kobo 觸控筆,比我想像中驚艷不少,像是梳理閱讀時可以隨便畫,很讚(看起來 iPad app 看不到?)
Kobo 10.2吋 電子書閱讀器

其他意外...則是把電子書擺在桌上,小孩拿起 kobo 筆畫一畫,就發生慘案了 <囧> 只能說這就是意外吧...目前看起來還不影響閱讀,讓他去吧...還是把電子書當作耗材,書中的資訊才是核心啊。

Kobo 電子書 刮傷

2021年6月27日 星期日

Node.js 筆記 - 以 HTTP API 提供 ffmpeg 轉檔,以 jpg 轉成 png 為例 @ node.js v10.24.1, macOS 11.4

最近接獲一個任務研究了一下 Javascript 與 H264 的解決方案,首先會立馬想到 ffmpeg 方案,就研究了以下 JS 專案:
其中 ffmpeg.wasm 無法在 node.js v10 運行。在不考慮 node.js 升級的前提上,就來把玩其他三者,而 tinyh264 的源頭是 h264bsd 專案,在 h264bsd 專案中有 js solution 可以挖出來跑,包括裡頭有 github.com/oneam/h264bsd/blob/master/js/test_node.js 這隻程式很小巧美麗,可以欣賞一下。而裡頭把玩最多的是 ffmpeg.js 這專案,有興趣可以看看他的 Makefile ,很簡單清楚,比較特別是他分別產出 ffmpeg-webm.js 和 ffmpeg-mp4.js ,兩者不同的地方是 ffmpeg 支援的 encoder/muxer 不同,可能是為了精簡 js code size 因此就拆成 webm 跟 mp4 的用法。這邊把玩的是善用 ffmpeg.js Makefile 去重編 ffmpeg-*.js 增加 demuxer/decoder 和 encoder/muxer。

這邊滿適合看一下 ffmpeg 框架:


接著,工作上的任務是讓 input 支援 h264,而 output 支援 jpg/png 等。這邊把範圍限縮到 input 支援 jpg ,輸出支援 png 來筆記。最後包裝個 HTTP API 出來筆記一下,結果花最多時間是在確認如何從檔案系統讀出檔案資料,以及透過 http.request 將圖片資料下載回來 Orz 還派上 md5sum 來檔案內容是否正常。

而 ffmpeg.js 真的設計得不錯,用起來很方便,可以參考 local-file-cmd.js 用法:

const ffmpeg = require('./ffmpeg-20210624_1550/ffmpeg-mp4.js');
//const ffmpeg = require('./ffmpeg-default/ffmpeg-mp4.js');
const fs = require('fs');
const crypto = require('crypto');

let from_local_file = '/tmp/test.jpg';
let raw_data = fs.readFileSync(from_local_file);
let testData = Uint8Array.from(raw_data);
const md5sum = crypto.createHash('md5');
md5sum.update(raw_data);
console.log('[INFO][from_local_file][size:'+raw_data.length+'][md5:'+md5sum.digest('hex')+'][path:'+from_local_file+']');

const result = ffmpeg({
MEMFS: [{name: 'test.jpg', data: testData}],
arguments: ['-i', 'test.jpg', 'test.png']
})
const out = result.MEMFS[0];
const outputContent = Buffer.from(out.data);

console.log('[INFO] Done. Input size: ' + raw_data.length +', testData size: '+testData.length+', output size: '+outputContent.length);

更多資訊:changyy/study-wasm-ffmpeg-jpg-to-png

2021年6月12日 星期六

Node.js 筆記 - 使用 Puppeteer 監控瀏覽網頁的 Requests 跟 HTML Code @ macOS 11.4, node v14.14.0, npm 6.14.5

緣起於在追蹤網站架構時,每次都靠 chrome browser 開發人員工具去追蹤特定的 request 還好,若需要大量瀏覽找尋一些蛛絲馬跡時,就會很煩!以前很閒時,就會跳下去寫 chrome extension 或是熱衷於 C++ 時,就會寫一點點  Chromium Embedded Framework 來寫個小型瀏覽器,最後想起來試試看 Puppeteer 吧!

使用 Puppeteer 很輕鬆,除了是很常見的 Javascript 語言外,Puppeteer 套件提供了網頁端常見的事件偵測、所發的 Request 清單。就這樣拼拼湊湊即可完工:

(async () => {
const browser = await puppeteer.launch({headless: false});
//const page = await browser.newPage();
const context = await browser.createIncognitoBrowserContext();
const page = await context.newPage();
await page.emulate(device);
await page.setRequestInterception(true);

// https://pptr.dev/#?product=Puppeteer&version=v10.0.0&show=api-event-domcontentloaded
// https://developer.mozilla.org/en-US/docs/Web/API/Window/DOMContentLoaded_event
//
// The DOMContentLoaded event fires when the initial HTML document has been completely loaded and parsed, without waiting for stylesheets, images, and subframes to finish loading.
//
page.on('domcontentloaded', () => {
console.log('on.domcontentloaded');
});

// https://pptr.dev/#?product=Puppeteer&version=v10.0.0&show=api-event-domcontentloaded
// https://developer.mozilla.org/en-US/docs/Web/API/Window/load_event
//
// The load event is fired when the whole page has loaded, including all dependent resources such as stylesheets and images. This is in contrast to DOMContentLoaded, which is fired as soon as the page DOM has been loaded, without waiting for resources to finish loading.
//
page.on('load', async () => {
console.log('on.load');
watchTags(page, watch_tags);
});

// https://pptr.dev/#?product=Puppeteer&version=v10.0.0&show=api-event-framenavigated
// https://pptr.dev/#?product=Puppeteer&version=v10.0.0&show=api-class-frame
page.on('framenavigated', frame => {
console.log('on.framenavigated: '+frame.url());
});

// https://pptr.dev/#?product=Puppeteer&version=v10.0.0&show=api-event-request
// https://pptr.dev/#?product=Puppeteer&version=v10.0.0&show=api-class-httprequest
page.on('request', request => {
watchRequest(page.url(), request.url());
if (skip_resource_type[ request.resourceType() ])
request.abort();
else
request.continue();
});

// https://pptr.dev/#?product=Puppeteer&version=v10.0.0&show=api-event-response
// https://pptr.dev/#?product=Puppeteer&version=v10.0.0&show=api-class-httpresponse
page.on('response', response => {
//console.log('on.response: '+response.url());
});

while (true) {
if (url_init.length > 0) {
const target_url = url_init.shift();
await page.goto(target_url, {
waitUntil: 'networkidle0',
});
console.log('networkidle0');
}
if (url_init.length > 0)
await sleep(5000);
else
await sleep(50);
}
await browser.close();
})();

如此,搭配 command line 使用,就可很快的監控想要的 request 規則,或是 HTML DOM Tree 的變化,例如監控 <script> 的讀取位置在哪:

% node index.js -url "https://tw.yahoo.com" -tag "script.src"
...
on.load
[WATCH][TAG] script: src
Web URL: [https://tw.yahoo.com/]
[
  { src: 'https://s.yimg.com/wi/ytc.js' },
  {
    src: 'https://tw.mobi.yahoo.com/polyfill.min.js?features=array.isarray%2Carray.prototype.every%2Carray.prototype.foreach%2Carray.prototype.indexof%2Carray.prototype.map%2Cdate.now%2Cfunction.prototype.bind%2Cobject.keys%2Cstring.prototype.trim%2Cobject.defineproperty%2Cobject.defineproperties%2Cobject.create%2Cobject.freeze%2Carray.prototype.filter%2Carray.prototype.reduce%2Cobject.assign%2Cpromise%2Crequestanimationframe%2Carray.prototype.some%2Cobject.getownpropertynames%2Clocale-data-en-us%2Cintl%2Clocale-data-zh-hant-tw&version=2.1.23'
  },
  { src: 'https://s.yimg.com/aaq/yc/2.9.0/zh.js' },
  {
    src: 'https://s.yimg.com/ud/fp/js/vendor.174522d6d76b51858b93.min.js'
  },
  { src: 'https://s.yimg.com/ud/fp/js/common.js' },
  { src: 'https://s.yimg.com/oa/consent.js' },
  { src: 'https://consent.cmp.oath.com/cmpStub.min.js' },
  { src: 'https://consent.cmp.oath.com/cmp.js' },
  { src: 'https://s.yimg.com/ss/rapid3.js' },
  { src: 'https://s.yimg.com/rq/darla/boot.js' },
  {
    src: 'https://s.yimg.com/ud/fp/js/main.6622c6aeda051386afaa.min.js'
  },
  { src: 'https://mbp.yimg.com/sy/os/yaft/yaft-0.3.22.min.js' },
  {
    src: 'https://mbp.yimg.com/sy/os/yaft/yaft-plugin-aftnoad-0.1.3.min.js'
  },
  { src: 'https://s.yimg.com/aaq/vzm/perf-vitals_1.0.0.js' },
  {
    src: 'https://fc.yahoo.com/sdarla/php/client.php?dm=1&lang=zh-Hant-TW'
  },
  {
    src: 'https://s.yimg.com/aaq/c/e7fecc0.caas-abu_highlander.min.js'
  }
]
...

2021年6月4日 星期五

[Linux] 升級 Red Hat Enterprise Server 的 OpenSSL, OpenSSH 服務 @ Red Hat Enterprise Linux Server release 5.3

這是一台滿舊的機器,大概 10 年前吧。同事想要 git clone 時,出現了失敗問題,追蹤一下是機器的 openssl 過舊,由於機器有他的任務在,不好意思亂更新系統資源,怕爆!所以採用 tarball 的安裝方式。

openssl 太舊時,連用 wget/curl 去下載 https 來源時都會失敗的,只好靠 http 或是 scp 搬東西進去。

$ lsb_release -a
Distributor ID: RedHatEnterpriseServer
Description: Red Hat Enterprise Linux Server release 5.3 (Tikanga)
Release: 5.3
Codename: Tikanga

$ openssl version
OpenSSL 0.9.8e-fips-rhel5 01 Jul 2008

只好挑個 /opt/tarball 安裝啦,沒想到要編譯 openssl 又會碰到 perl 版本不夠新:

$ wget http://www.cpan.org/src/5.0/perl-5.10.0.tar.gz
$ cd ~/perl-5.10.0
$ ./configure.gnu --prefix=/opt/tarball
$ make -j4 install

接著再安裝個 zlib ,採用 static 方式方便後續大家使用:

$ wget http://zlib.net/zlib-1.2.11.tar.gz
$ cd ~/zlib-1.2.11
$ ./configure --prefix=/opt/tarball --static
$ make install

編譯 openssl:

$ wget http://www.openssl.org/source/openssl-1.1.1k.tar.gz 
$ cd ~/openssl-OpenSSL_1_1_1k 
$ PATH=/opt/tarball/bin:$PATH ./config --prefix=/opt/tarball
$ make -j4 install

編譯 openssh 工具包:https://github.com/openssh/openssh-portable/releases

$ cd ~/openssh-portable-V_8_6_P1
$ autoreconf
$ LD_LIBRARY_PATH=/opt/tarball/lib:$LD_LIBRARY_PATH ./configure --prefix=/opt/tarball/ --with-ssl-dir=/opt/tarball
$ LD_LIBRARY_PATH=/opt/tarball/lib:$LD_LIBRARY_PATH make -j4 install
$ file /opt/tarball/bin/ssh
/opt/tarball/bin/ssh: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), for GNU/Linux 2.6.9, stripped

接著偷懶拉 code 方式:

$ GIT_SSH_COMMAND="LD_LIBRARY_PATH=/opt/tarball/lib:$LD_LIBRARY_PATH /opt/tarball/bin/ssh" git clone ...

或是在環境變數添加好:

LD_LIBRARY_PATH=/opt/tarball/lib:$LD_LIBRARY_PATH 
PATH=/opt/tarball/bin:$PATH

搞定!

2021年5月9日 星期日

[韓劇] 認識的妻子

前幾天想說好久沒用愛奇藝,上去逛了一下,想說看一下「消失的情人節」,結果幾分鐘被朋友提醒可以去 NETFLIX 看高畫質 XD 就這樣又遠離了愛奇藝。倒是在 NETFLIX 看完後,又被推薦服務推坑「認識的妻子」,主因是去年底看過韓劇「Start-Up」,內裡的另一位反派恰好也在這齣戲裡演出。

這是一部 2018 年初的韓劇。這故事跟那時很夯的穿越劇有點關係,但真正吸引到我的,卻是第一集描述雙薪家庭養小孩的困境,真的養了小孩更有感的,感受到劇情安排的那種生活壓力。

不知是不是年紀過 35 的關係,對於大叔熟女演的戲都特別有興趣看完 XD 就這樣花個一天追了 12 集,完食!隨後逛了一下男女主角的 IG ,才發現現實的靜態的照片反而吸引不了我,看來還是劇情關係,讓人喜歡笑起來古靈精怪的女主角,且我不太愛學生時代的裝扮,反而是 OL 時期的精明搭配特有的笑容令人印象深刻。

此時會觀看這齣戲也滿有緣分的,像是幾天前聽股癌 Podcast 回憶起在軍中看了 about time ,讓我回想起對時間的印象,像是電影裡提到小孩出生後,就不適合回到過去,避免改變到未來的小孩狀態。接著恰逢表弟的第一個小孩來到世上,為了體貼老婆,包括月子中心住滿30天、放棄親餵等規劃,著實已把生活的目標跟品質都規劃好也定位好,以及另一位年收稅率達 20% 級距的網路名人在哀哀叫初乳擠得多痛苦和 20% 後就沒了眾多社會福利,政府這稅務規劃到底是助生還阻生 XD

總總點滴,都讓人好好回憶著家庭、小孩、婆媳、雙薪、工作的天秤究竟要怎樣平衡著,就這樣一口氣就看完了這戲!

2021年4月22日 星期四

[PHP] 下載 Maxmind GeoIP Legacy Databases 和 ngx_http_geoip_module 相關處理 @ macOS 11.2, PHP 7.3.24

由於 Maxmind 在推 GeoIP2 ,不開放 .dat 的 GeoIP DB 了,以前下載位置:

  • http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz
  • http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz
現在官方都說要改用 GeoLite2 DB ,其格式:
  • GeoIP2 Binary (.mmdb)
  • GeoIP2 CSV 
目前先偷懶不處理 nginx ,因為 nginx 採用 GeoIP Legacy Databases。

有找到一個網站 https://www.miyuru.lk/geoiplegacy,從中下載:
連續動作:

% php -v
WARNING: PHP is not recommended
PHP is included in macOS for compatibility with legacy software.
Future versions of macOS will not include PHP.
PHP 7.3.24-(to be removed in future macOS) (cli) (built: Dec 21 2020 21:33:25) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.24, Copyright (c) 1998-2018 Zend Technologies

% php composer.phar require geoip/geoip:~1.16

% wget https://dl.miyuru.lk/geoip/maxmind/city/maxmind4.dat.gz
% gunzip -d maxmind4.dat.gz

% cat test-ip-via-geoip.php
<?php
require 'vendor/autoload.php';
$gi = geoip_open("maxmind4.dat",GEOIP_STANDARD);
$country = geoip_country_code_by_addr($gi, $argv[1]);
echo "lookup [".$argv[1]."], result: [$country]\n";

% php test-ip-via-geoip.php 8.8.8.8
lookup [8.8.8.8], result: [US]

% nslookup tw.yahoo.com
Server: 8.8.8.8
Address: 8.8.8.8#53

Non-authoritative answer:
tw.yahoo.com canonical name = atsv2-fp-shed.wg1.b.yahoo.com.
Name: atsv2-fp-shed.wg1.b.yahoo.com
Address: 180.222.102.201
Name: atsv2-fp-shed.wg1.b.yahoo.com
Address: 180.222.102.202

% php test-ip-via-geoip.php 180.222.102.202
lookup [180.222.102.202], result: [IN]

% curl ipinfo.io/180.222.102.202
{
  "ip": "180.222.102.202",
  "hostname": "media-router-fp74.prod.media.vip.tp2.yahoo.com",
  "city": "Taoyuan City",
  "region": "Taiwan",
  "country": "TW",
  "loc": "24.9937,121.2970",
  "org": "AS24506 YAHOO! TAIWAN",
  "timezone": "Asia/Taipei",
  "readme": "https://ipinfo.io/missingauth"
}

% nslookup facebook.com
Server: 8.8.8.8
Address: 8.8.8.8#53

Non-authoritative answer:
Name: facebook.com
Address: 31.13.87.36

% curl ipinfo.io/31.13.87.36
{
  "ip": "31.13.87.36",
  "hostname": "edge-star-mini-shv-01-tpe1.facebook.com",
  "city": "Hong Kong",
  "region": "Central and Western",
  "country": "HK",
  "loc": "22.2783,114.1747",
  "org": "AS32934 Facebook, Inc.",
  "timezone": "Asia/Hong_Kong",
  "readme": "https://ipinfo.io/missingauth"
}

% nslookup www.gov.tw
Server: 8.8.8.8
Address: 8.8.8.8#53

Non-authoritative answer:
Name: www.gov.tw
Address: 223.200.155.55

% php test-ip-via-geoip.php 223.200.155.55
lookup [223.200.155.55], result: [TW]

% curl ipinfo.io/223.200.155.55
{
  "ip": "223.200.155.55",
  "hostname": "223-200-155-55.hinet-ip.hinet.net",
  "city": "Hualien City",
  "region": "Taiwan",
  "country": "TW",
  "loc": "23.9769,121.6044",
  "org": "AS4782 Data Communication Business Group",
  "timezone": "Asia/Taipei",
  "readme": "https://ipinfo.io/missingauth"
}

2021年4月9日 星期五

[PHP] 透過 Maxmind GeoIP DB 統計用戶資訊 @ macOS 11.2, PHP 7.3.24

真是超級久沒用 maxmind.com 的 GeoIP DB 了,一時之間還以為沒有免費使用的方式,追了一下是要註冊帳號才能下載,而 maxmind 也有提供自動化每天更新 GeoIP DB 的機制。

在此對一份類似 Access Logs 做分析,將其轉成 CSV 格式來分析,其中 CSV 裡頭有 id 跟 remote_ip 兩個欄位,將 remote_ip 分析完後直接歸類屬於哪個 country_code,程式碼很簡單:

% php -v

WARNING: PHP is not recommended
PHP is included in macOS for compatibility with legacy software.
Future versions of macOS will not include PHP.
PHP 7.3.24-(to be removed in future macOS) (cli) (built: Dec 21 2020 21:33:25) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.24, Copyright (c) 1998-2018 Zend Technologies

% cat composer.json
{
  "require": {
    "geoip2/geoip2": "~2.0"
  }
}

% php composer.phar install
No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.
Loading composer repositories with package information
Updating dependencies
Lock file operations: 4 installs, 0 updates, 0 removals
- Locking composer/ca-bundle (1.2.9)
- Locking geoip2/geoip2 (v2.11.0)
- Locking maxmind-db/reader (v1.10.0)
- Locking maxmind/web-service-common (v0.8.1)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 4 installs, 0 updates, 0 removals
- Installing composer/ca-bundle (1.2.9): Extracting archive
- Installing maxmind/web-service-common (v0.8.1): Extracting archive
- Installing maxmind-db/reader (v1.10.0): Extracting archive
- Installing geoip2/geoip2 (v2.11.0): Extracting archive
2 package suggestions were added by new dependencies, use `composer suggest` to see details.

Generating autoload files
1 package you are using is looking for funding.
Use the `composer fund` command to find out more!

% cat job.php
<?php

require 'vendor/autoload.php';

// https://github.com/maxmind/GeoIP2-php
use GeoIp2\Database\Reader;
$reader = new Reader('GeoLite2-City.mmdb');

// https://www.php.net/manual/en/function.fgetcsv.php
$row = 1;
$header_row = NULL;
$header = array();
$country_group = array(); 
$lookup = array();

$tracking_time_cost_per_run = microtime(true);
echo "[".date("Y-m-d H:i:s")."]\tinit\n";
if (($handle = fopen("access_log.csv", "r")) !== FALSE) {
while (($data = fgetcsv($handle, 10240, ",")) !== FALSE) {
if (is_null($header_row)) {
$header_row = $data;
foreach($header_row as $index => $name) {
$header[$name] = $index;
}
continue;
}
$remote_ip = $data[ $header['remote_ip'] ];
$data_id = $data[ $header['id'] ];

try {
$record = $reader->city($remote_ip);
} catch (Exception $e) {
continue;
}
if (!is_null($record) && property_exists($record, 'country') && !is_null($record->country) && isset($record->country->isoCode)) {
if (!isset($country_group[$record->country->isoCode]))
$country_group[$record->country->isoCode] = array();
$hash_key = $remote_ip.'-'.$record->country->isoCode;
if (!isset($lookup[$hash_key])) {
$lookup[$hash_key] = 1;
array_push($country_group[$record->country->isoCode], $data_id);
}
}
++$row;
if ($row % 100000 == 0) {
echo "[".date("Y-m-d H:i:s")."]\t".number_format($row).", time cost: ".(microtime(true) - $tracking_time_cost_per_run)."\n";
$tracking_time_cost_per_run = microtime(true);
file_put_contents('/tmp/lookup-result.json', json_encode($country_group));
}
}
fclose($handle);

echo "[".date("Y-m-d H:i:s")."]\t".number_format($row)."\n";
echo "Total #: $row\n";
print_r($header);
file_put_contents('lookup-result.json', json_encode($country_group));
}

產出:
% time php job.php
[2021-04-09 21:03:51] init
[2021-04-09 21:05:18] 100,000, time cost: 86.867056131363
[2021-04-09 21:06:41] 200,000, time cost: 83.395349025726 
...

% jq '' /tmp/lookup-result.json | head -n10

{
  "TW": [
    "1",
    "68",
    "101",
    "121",
    "147",
    "193",
    "236",
    "259", 
... 

就把一堆 record id 擺在某個 country_code 下面,每 85秒處理完 10萬筆資料。

2021年3月14日 星期日

[動漫] 排球少年!!

 

 來源:WIKI

前幾天晚上想測試一下愛奇藝 CDN 不在台灣後,影響有多大時(實際上愛奇藝沒了台灣這邊的CDN後,速度真的受到不小影響,一直緩衝,幾乎不可用)當下就交叉比對了 NETFLIX ,就挑了 排球少年 這動畫。共有四季,就這樣一週多一點,都用 1.5倍速 完食!

老實說,我並沒有很喜歡這畫風,論運動的話,大家更愛看帥哥眾多的網球少年。會一直看下去的主因是自己國高中也打了六年排球,在班際排球賽上,我也常常負責舉球手這個位置,原因沒有別的,因為排球風氣還不盛行,湊起六人後,隊友大多都是打籃球的,跳得高但基本動作較不熟,就非常適合當主攻手,由相對球齡長的我,角色就是幫忙托球給予攻擊機會。當初國一會選擇排球也滿有趣的,因為國小自己點太多技能在躲避球,幾乎沒在打籃球,一進入國中後對大家一窩蜂衝籃球沒有太大興趣,就挑了排球社。還記得第一天社團活動時,跑去遠一點的撿球,就...故意高吊遠傳兩三個排球場距離,球卻不偏不倚的砸中排球社高中學姊,不認識的學姊還很好心的騙我說眼鏡是自己壞的 XD 當下很蠢以為真的是自己壞,事後想起真是人太好!(後來...好像被詛咒一樣,每當自己把球往高空拋超高時,很容易打中別人的頭 Orz 高中時期又一次打到自己的高中好友)

記得自己國高中垂直跳60cm左右,周邊一票人都 80cm 或是身高 175cm 以上,最扯的是有一位 170cm 卻可以垂直跳 94cm 的學長,所以,對於排球少年的故事很有共鳴的!舉凡跳發、飄浮球都打過,我自己應該是主要用飄浮球發球的。甚至大一體育課也可以靠發球吃香得個幾分 XD

這動畫對我來說,滿多球員內心戲,例如為何打排球、為何不想努力衝刺一番、為何不想認輸、為何想偷懶等等的,滿好玩的,有些心境跟當年自己玩排球有像,甚至上了大學後,短暫加入系隊幾個月有感。像是看到別人身體條件很優,就開始想偷懶,自己找理由畫地自限。

真的想起了很多青春歲月,雖然沒有像故事裡不斷去追逐競賽,但打排球的時間,卻填滿了我國高中至少三四年眾多的下課活動,幾乎每天下課等車前,都會玩個30分以上。可惜上大學後,心態越來越疲憊?無動力,就真的很快退出系隊,幾乎沒在打了。

最後,對於這齣動畫共鳴滿滿,但也難抵擋後面稍微膩的慢步調 XD 像是第三季 10 集動畫就只有一場比賽,卻佔了一季啊。有機會再來看看漫畫了,有興趣想看看排球少年動畫的,可以在 愛奇藝瘋動畫(僅第四季)NETFLIX 逛逛囉!

2021年3月4日 星期四

[開箱] EZCast Beam V3 ,順便與 EZCast Beam J2 體積比較

 EZCast Beam V3

最近想找個伴手禮送人,想著想著就想起投影機很適合給家裡有小孩的人。挑了偏中低價位的投影機,至少有 720P 的輸出效果。剛好手中也有台 EZCast Beam J2 就拿來比較一下大小。

EZCast Beam V3

EZCast Beam V3

比完才發現 EZCast Beam J2 真的是袖珍版的微投影機 XD

這台 EZCast Beam V3 的服務項目:

  • HDMI 輸入 (投影機最基本需求)
  • USB 輸入
    • 支援 iPhone/iPad 透過有線完成 Airplay Mirror 投放
    • 支援 Android 透過有線完成 Chromecast Mirror 
    • 支援從隨身碟讀取資料來播放,如影片/照片/音樂
  • 無線輸入
    • 由於支援2.4G/5G 無線網路連線,有無線投放功能,如 DLNA / Miracast / Google Home Mirror / Airplay ,甚至官網還有提到 Youtube app 也可以搜尋到此裝置來投放
送人著重在體驗,力求極速上手,個人比較看重 USB 輸入,讓手機內容投放出去,方便第一!

更多資訊可以到官網 EZCast Beam V3 或是 EZCast網站購物 或是官方直營的 EZCast蝦皮商店 逛逛。

2021年3月1日 星期一

初探保險 - MY83保險網 / Finfo保險資訊站

來源:FINFO.TW 30歲罐頭保單的年費變化

差不多該研究保險了,之前一直偷懶是認為健保已非常夠用了!然而,對於家庭角色來說,保險有其特別的意義,如果有玩過遊戲 太空戰士七 的話,其寶石搭配的設計非常豐富,其中有一招是被敵人KO時,招喚不死鳥讓全體成員復活的機制,保險就是類似這種效果,核心設計是降低身旁親人的壓力,在經濟許可上,播一些擺在保險上當作理財規劃的一環作為保護。

這次研究保險時,主要透過了 MY83保險網FINFO保險資訊站 ,兩者的罐頭保單體驗都在伯仲之間,後來就留言跟專業的保險員互動,果真,很多事還是交給專業的來吧!保險員他們有的優勢:
  • 考過試
  • 比一般人熟悉保單設計、優惠和組合搭配
  • 有些專屬保險產品不是一般人可以買到的,屬於獨家商品
其實,這就跟開證券戶一樣,在網路可以痛快的申請完畢,但取得的交易手續費是公定價。如果透過證券營業員幫忙開戶,就可以拿到優惠的交易手續費。而透過保險員,有一些保險產品是獨家販售的。

我認為有興趣的可以大膽一點留訊息,實在是這些網站為了給網站使用者有更佳的服務體驗,有做了以下事務:
  • 網站平台透過考試/面試挑選配合的保險業務員
  • 每個業務員是被網站平台分配到客戶的
  • 網站平台還會時時數據追蹤,以確認保險業務員成交的轉換率為何
  • 保險業務員本身需要這類網站來取得來源
透過上述架構,有機會被分配到不錯的保險業務員,且業務員為了能夠有成交/業績/永續配分配到客戶,服務品質不會太差的。當然,也要花點心思去看對方配置的保險方案,去理解保險員配置思維。例如有次保單保險員配了兩個台灣人壽的主約,仔細去看才發現配置兩個主約的目的是讓其中一個附約的保障金額可以拉高,有些附約的保障金額限制不能超過主約保障金額的五倍價格,而通常主約都保得很低,靠附約搭配提升不少值。這時拆分多個主約就有其設計架構。

這些便是我這次接觸保險的心得,套句以前有當過保險業務的長輩分享,可以著重意外跟失能即可,而實支實付也可以考慮,但投資理財保單則不用花太多心思,因為投資類的方式有太多了,像是自我投資也是一種。

最後一提,失能可以分成意外失能跟疾病失能,後者的保費貴跟條件嚴謹,像是身體狀況有定期回診的,通常就無法用一般合理的保險額度來保疾病失能,但也存在非常昂貴的疾病失能險或是不保證能年年續保的失能險可挑選的。有興趣可以多多利用 FINFO保險資訊站 服務,可以觀看年紀與保費變化走勢囉。

2021年2月21日 星期日

[動畫] 弱角友崎同學 , 輕小說

農曆年節開始玩起了 DOS GAME 大富翁三 ,溫習了一下 XD 有了社會歷練後,玩起大富翁遊戲時,停留在銀行時,立馬辦貸款、辦信用卡並借好借滿(遊戲內是幾乎無風險的操作),經過股市立刻買好買滿,隨著戰況激烈,物價指數攀升,當然,股價也有對應的成長的。這大概就是長大後才懂的事,小時玩這遊戲還沒這種企圖心。

接著就回憶起在 bilibili 看到的 動畫 弱角友崎同學 這齣輕小說 動漫化的作品,提醒自己要好好進攻現實生活 XD 

這故事讓人想起自己國中時期,國一時段考都考個四百多名吧,全校 670 人,也就是全班 60人中,大概 35名之後。成天也在玩 PC Game ,倒也想不起自己有沒認真唸書過。但是,進入國二後,很神奇地在 物理 得到了解題興趣,彷彿不用花太多時間,只需理解又不用花時間背,極爽 XD 就這樣因為單一科物理的學習優勢,讓我整體學習心態改變,開始會用力花時間也把歷史和地理也背好,如此讓政體成績進入到全校排名 100 內。

這些過程,就像小說/漫畫 弱角友崎同學 的情節有那麼一點點近似,在人生中找到一點回饋感,進而把握去攻略人生。一樣的思維在十多年前也可以用在寫 Blog、研究技術等等,要再來找找生活回饋感了!

2021年2月18日 星期四

[開箱] ADONIT NOTE+ 觸控筆 與 iPad 8 ,使用 SketchBook app 測試傾斜效果

之前買了像 小米液晶手寫板 給小孩塗鴉,非常開心。最近在想怎樣引導小孩產生高質量的塗鴉,想著想著手邊就蹦出一台 iPad 8 跟第三方觸控筆 ADONIT NOTE+ 了!!說真的太久沒買 iPad ,第一次買是2011年的 iPad2 (代號 A1395) ,第二次買是 2013 年的 iPad Air (代號 A1474),最近才發現 iPad 第七代已經來到 10.2 吋。而添購 iPad 8 的主因之一是追求觸控筆支援,以及福利機價差不多,就乾脆買新的了!

試用了一下還滿順的,可惜的 2013 年 iPad Air 如預期的完全無法用,不然我也想添購一隻筆來輔助一下 XD 像是變成另一個畫板等等

2021年2月9日 星期二

Clubhouse 體驗心得


大概六天前剛拿到朋友的邀請,上去體驗了一下,非常新鮮。

聽到了海外大陸人在跟台灣人聊政治,且是很有理性的論述民主的好壞,像是台灣人說民主也沒因此比較多選擇(藍綠兩黨),還是會被媒體給包裝起來,但是!若選錯市長時,民主至少可以罷免!對於過去不認同的政治,可以身體力行去表達自己的心聲。而常說的民主會拖累經濟的議題,恰好有位在德國的大陸人在分享北上廣深的 GDP 其實跟台灣差不多,再加上自己老鄉連廁所都沒有,以此驗證民主也不會拖累經濟,講得還滿不錯的。

聽了在紐約的大陸人,在聊在美國或 App 朋友交際認識,不如線上遊戲那般親,線上遊線連討論個攻略,大家都毛起來教學 XD 而其他則是提到自己遠離曼哈頓,特不愛曼哈頓的急促、到時都要記得去抗爭(類似被欺負要去哪邊回報?!),聊仍在疫情中,跟大家介紹 crowdcow.com 可以買好吃又便宜的牛肉 XD 以及自己住的地方偏郊區,卻有什麼類似快捷大眾運輸,快速到市中心以及還可以轉乘到機場,且交通費便宜啊。是個很讚的聊天內容。

聽了一位台灣女生到北京外商 SaaS 發展,之前看了幾篇質量還不錯的科普文章,沒想到一年多沒更新是到北京發展了!聊了不少在 SaaS 的步調,包括公司估值方式或為何 SaaS 總是高估值、聊了訂閱制的經營方式,會有客戶經理在負責維護客戶忠誠度等等,也滿有意思的。途中也吸引到有一位從事投資的,回饋他在業界中常見的估值法,如同業比較法、評估新創的事業成長率等等,這段就還滿像 Youtuber 財經網紅 CK 的部分,就 CK 是一名在澳洲精算師,前陣子除了分享對總體經濟外,也分享她是怎樣評估股價(市值),過程就包括猜測成長率、套用估值公式等等,因此聽負責投資其他間公司的VC說話,特別有共鳴。

聽了葉丙成教授在那邊台語五四三,也滿逗趣的。不知哪個時間點自己也會變成那樣,喜歡跟幾位老友聊是非。讓我想起以前在工研院的老闆,英文很強,但總是愛台語,還會請特休去參加抗議活動,實在熱血 XD

聽了財報狗產品經理在分享他的工作方式,包括大老闆很挺他,也讓他去經營數個月的 podcast ,最終因為數據上並沒有顯著帶給財報狗對應的流量,再加上一集至少要五小時處理,每週一集的時間消耗也是很恐怖的。比較特別的是財報狗 PM 提到了一切都是數據導向,就算花兩週開發服務,最後的數據是差的,仍是會捨棄不上線的!而在招聘 RD 時,從一開始就會溝通可能高達 2/3 的 coding 都不會上線!大量專注在實驗、數據、決策等 growth hack 路線,對我而言是滿強化一些已知的手段,非常醒腦。而聊天提到了一些粗略的表單,沒想增加設計美感後,出現轉換率變差時,該怎樣去檢驗一直猜想,若拉不起來也不會上線的!核心 KPI 就是轉換率,轉換率沒變好就不會採用。非常專注。

也聽了一些早晨的新聞台,這我反而記憶最淺,而快速體驗後,感受到網路上的財經網紅會盡可能想搶流量,也不少在該領域非第一的網紅很努力地連續幾天都在開講,我一想到上班日都在做工作的事情,週末還要跑去聽工作相關的,而聊天品質又無法確定,實在累。

我想,嘗試過 clubhouse 後,對於一些尚未深入到 podcast 用戶,應當會感受非常新鮮。但對於已熟悉 podcast 來源者,可能會像我一樣又回味起 podcast 的高品質體驗。包括 podcast 可以快轉、可以看別人的評價,而 clubhouse 就是直播,且很有機會快速跟名人親近,是一種老早就存在的需求,但又重新被詮釋出來。然而 clubhouse 滿殺時間的,若參加到一場不錯的聊天,那心中會很充實,但如果加入到一場主持人不佳,講者也佳,想詮釋自己的想法也說不清時,那真的會聽不下去的 XD

果真核心項目是有限的時間資源,大家都在搶,搶人眼球、搶人耳朵、搶人時間,早已進入到任何的新創服務,就是要硬生生地佔領用戶的時間資源。而我,應該還是會回歸到 podcast 服務上,像是對岸的 得到app 也滿不錯的。這種專業的聲音產出內容,有那麼一種可預期的體驗感,有那麼可以快轉省時間的沾沾自喜感,而資訊/知識,其實也不打折的。

2021年2月3日 星期三

Flutter 開發文件之 iOS 與 Android 實作筆記 - 使用 Admob 以 Interstitial ads 為例


關於 flutter 使用 Admob 的架構,可參考網路上數篇文章:
此例筆記 iOS & Android 的設定方式,並且記錄碰到的錯誤訊息。

Android app 設定方式:

1. 更新 flutter_app/pubspec.yaml 添加 dependencies (記得要運行 flutter pub get)

firebase: ^8.0.0
firebase_admob: ^0.11.0+1

2. 安置從 Firebase 建立專案時得到的 google-services.json 檔案,擺在 flutter_app/android/app/ 中

3. 更新 flutter_app/android/build.gradle 的 buildscript -> dependencies 設定

//classpath 'com.android.tools.build:gradle:3.5.0'
// 為了修正錯誤訊息:error: unexpected element <queries> found in <manifest>.
classpath 'com.android.tools.build:gradle:4.0.0'
classpath 'com.google.gms:google-services:4.3.3'

4. 更新 flutter_app/android/gradle/wrapper/gradle-wrapper.properties 的 gradle 版本至 6.1.1

#為了修正錯誤訊息:Minimum supported Gradle version is 6.1.1. Current version is 5.6.2. If using the gradle wrapper
distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip

5. 增加 flutter_app/app/build.gradle 的 apply 項目

apply plugin: 'com.google.gms.google-services'

6. 更新 flutter_app/android/app/src/main/AndroidManifest.xml 添加 meta data (僅範例程式數值)

<meta-data android:name="com.google.android.gms.ads.APPLICATION_ID" android:value="ca-app-pub-3940256099942544~3347511713"/>

7. 更新 flutter_app/lib/main.dart (此改自於範例程式,讓 add Button 可以產生一個插頁廣告)

iOS app 設定方式:

1. 更新 flutter_app/pubspec.yaml 添加 dependencies(記得要運行 flutter pub get)

firebase: ^8.0.0
firebase_admob: ^0.11.0+1

2. 安置從 Firebase 建立專案時得到的 GoogleService-Info.plist ,用 Xcode 打開專案,把 GoogleService-Info.plist 拖拉擺入跟 Info.plist 的同階目錄位置。

% open flutter_app/ios/Runner.xcworkspace
% ls flutter_app/ios/Runner/GoogleService-Info.plist

3. 更新 Info.plist 內容,添加資訊 GADApplicationIdentifier 等資訊(僅範例程式數值)

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-3940256099942544~1458002511</string>
<key>SKAdNetworkItems</key>
  <array>
    <dict>
      <key>SKAdNetworkIdentifier</key>
      <string>cstr6suwn9.skadnetwork</string>
    </dict>
  </array>

4. 更新 Podfile ,上方添加 platform :ios, '10.0'

為了修正錯誤訊息: 
Automatically assigning platform `iOS` with version `9.0` on target `Runner` because no platform was specified. Please specify a platform for this target in your Podfile.

5. 更新 flutter_app/lib/main.dart (此改自於範例程式,讓 add Button 可以產生一個插頁廣告)

如此就收工啦!而 flutter_app/lib/main.dart 內容如下:

import 'package:flutter/material.dart';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_admob/firebase_admob.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
        visualDensity: VisualDensity.adaptivePlatformDensity,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  MobileAdTargetingInfo targetingInfo = MobileAdTargetingInfo(
    keywords: <String>['flutterio', 'beautiful apps'],
    contentUrl: 'https://flutter.io',
    childDirected: true,
    nonPersonalizedAds: true,
  );

  InterstitialAd _interstitialAd;

  InterstitialAd createInterstitialAd() {
    return InterstitialAd(
      adUnitId: InterstitialAd.testAdUnitId,
      targetingInfo: targetingInfo,
      listener: (MobileAdEvent event) {
        print("InterstitialAd event $event");
      },
    );
  }

  @override
  void initState() {
    super.initState();
    FirebaseAdMob.instance.initialize(appId: FirebaseAdMob.testAppId);
    //_interstitialAd = createInterstitialAd()..load();
  }

  @override
  void dispose() {
    _interstitialAd?.dispose();
    super.dispose();
  }

  void _incrementCounter() {
    _interstitialAd?.dispose();
    _interstitialAd = createInterstitialAd()..load()..show();
    setState(() {
      _counter++;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}