2023年12月30日 星期六

[投資] 2023 年投資回顧與未來展望

攝影師:Walls.io

2023 年使用了定期定額+再平衡的操作,今年用市值來看績效比較單純,市值增加 62.5%,若不計算入金也就是純投資的部份,市值增加 51%,表現超越台美股大盤(台股大盤 24%、NASDAQ 43%),記得去年投資回顧有提到,大盤投報好的時候,我的投報可能會落後,看來打破了這個預期;既然時間用的少,大盤好與壞時的績效都還可以接受,那就繼續這麼做下去吧。

重點數字

市值增加:62.5%
市值(不含入金)增加:51%
平均現金水位:11.7%
台股大盤投報:24%
NASDAQ 投報:43%

2024 年的策略:定期定額 + 再平衡

定期定額:每個月操作一次,輪流投入每個標的,當標的歷史價格低於十年前太多時,當次投入兩倍

再平衡:每半年將投報超過 20% 或平均的標的,賣出賺錢的部份

2024 年標的

006208、00878、5880 合庫金、2412 中華電、QQQ、ARKW、AAPL、TSLA

2023 年市值變化圖





2023年12月3日 星期日

[App] 百貨小工具隱私權保護政策

非常歡迎您使用百貨小工具,為了讓您安心使用百貨小工具的各項服務,特此向您說明百貨小工具的隱私權保護政策,以保障您的權益,請您詳閱下列內容:


隱私權保護政策的適用範圍
隱私權保護政策內容,包括百貨小工具如何處理在您使用百貨小工具時收集到的使用情況,但絕對不會收集個人隱私資料。隱私權保護政策不適用於百貨小工具以外的相關連結網站,也不適用於非百貨小工具所委託或參與管理的人員。

個人資料的蒐集、處理及利用方式
當您使用百貨小工具時,百貨小工具會記錄您的點選資料,做為改進百貨小工具的參考依據,此記錄為內部應用,絕對不會對外公佈。我們會視需要公佈統計數據,但不會涉及個人資料。

資料之保護
百貨小工具完全使用 Google 提供的開發環境與平台工具,完全沒有使用第三方資源。

隱私權保護政策之修正
百貨小工具隱私權保護政策將因應需求隨時進行修政,修正後的條款將刊登於此。

2023年10月18日 星期三

[iOS][Swift][App] Xcode 15 and iOS 17 Error: DT_TOOLCHAIN_DIR cannot be used to evaluate LIBRARY_SEARCH_PATHS, use TOOLCHAIN_DIR instead

Error message

升級到 iOS 17 後,也得升級到 Xcode 15 才能使用了,但原本的 project 卻無法 build 了,遇到的就是上圖的 error,以下是解決方法

  • 請確保 xcode-select -p 有選在正確的 Xcode 15 路徑上
  • brew upgrade cocoapods
    • 若不是使用 brew 的話請使用下面
      • gem update cocoapods
  • Delete Pod directory in your App(if present)
  • pod install
  • pod repo update
  • pod install
  • Clean project 且重 build

Reference

  1. Xcode 15 and iOS 17 - Error: DT_TOOLCHAIN_DIR cannot be used to evaluate LIBRARY_SEARCH_PATHS, use TOOLCHAIN_DIR instead #12065 (link)

2023年8月29日 星期二

[iOS][Swift][App] 實作搜尋建議

Photo by Ketut Subiyanto

簡單記錄一下實作搜尋建議的方法,完整的程式在下方

struct ContentView: View {
    @State private var searchText = ""
    @State private var selectedSuggestion = ""
    // 建議列表
    @State private var suggestions: [String] = ["Apple 蘋果", "ALLSAINTS", "BOTTEGA VENETA", "COSME DECORTE 黛珂", "Nike 耐吉", "Dell 戴爾", "COS", "Zara", "Emilie Louis", "L.ERICKSON", "SPRAYGROUND", "gubami Social-法式tapas餐廳", "Sarabeth's"]
    @State private var showSuggestions = false
    
    var body: some View {
        VStack {
	    // 搜尋欄位
            TextField("Search", text: $searchText)
                .padding(.horizontal, 15)
                .padding(.vertical, 10)
                .background(Color(.systemGray6))
                .cornerRadius(8)
                .padding(.top, 10)
                .padding(.bottom, 5)
                .onChange(of: searchText) { newValue in
                    showSuggestions = !newValue.isEmpty
                }
            
            // 選擇搜尋建議後的顯示
            if !selectedSuggestion.isEmpty {
                Text("Selected: \(selectedSuggestion)")
                    .padding(.top, 10)
            }
                        
            if showSuggestions {
                List {
                    ForEach(filteredSuggestions, id: \.self) { suggestion in
                        Text(suggestion)
                            .onTapGesture {
                                selectedSuggestion = suggestion
                                searchText = ""  // 清空 TextField
                                showSuggestions = false  // 隱藏建議列表
                            }
                    }
                    if filteredSuggestions.isEmpty {
                        Text("找不到品牌")
                            .foregroundColor(.gray)
                    }
                }
                .background(Color.white)
                .cornerRadius(8)
                .shadow(radius: 5)
                .padding()
            }
            Spacer()  // 添加一個間距,以便頁面內容和建議列表不重疊
        }
        .padding()
        
        var filteredSuggestions: [String] {
            if searchText.isEmpty {
                return []
            }
            return suggestions.filter({ $0.localizedCaseInsensitiveContains(searchText) })
        }
    }
}

2023年8月26日 星期六

[iOS][Swift][App] 整合 Google AdMob 到 iOS App

Photo by Ketut Subiyanto

我在整合 Google AdMob 時卡了超久,Google 了也沒找到很直接能解決的方法,想說在此做個記錄,讓以後的我能照著做,也讓遇到類似問題的人能夠有個解法,以下是參考 官方說明 做的

第一動,Import the Mobile Ads SDK

先在 xocde project 的根目錄下(.xcodeproj 檔案的路徑)執行 pod init,這樣就能產生一個 Podfile 檔案,若沒安裝 cocopods 可以 follow 這裡 安裝,然後再按照 這裡 的做法,加

pod 'Google-Mobile-Ads-SDK'

在 Podfile 檔案,然後執行下面,記得此時要將 xcode 關閉

pod install --repo-update

這邊我遇到一個問題

[!] Unable to determine the platform for the `your project name` target.

解決方法是將 Podfile 檔案裡的第二行 uncomment 就可以了,完成後如下


platform :ios, '9.0'


第二動,Update your Info.plist

遇到的第一個問題是,我找不到 Info.plist 檔案

參考 這邊,原來 info.plist 變成了 project 設定裡的 tab

然後將 GADApplicationIdentifier (1個) 和 SKAdNetworkItems (49個) 輸入(如下圖紅框)


第三動,建立 banner structure 來呼叫顯示廣告
import GoogleMobileAds
struct BannerVC: UIViewControllerRepresentable {
    var bannerID: String
    var width: CGFloat

    func makeUIViewController(context: Context) -> UIViewController {
        let view = GADBannerView(adSize: GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(width))

        let viewController = UIViewController()
        view.adUnitID = bannerID
        
        view.rootViewController = viewController
        viewController.view.addSubview(view)

#if false
        /* Simulator */
        let request = GADRequest()
        let extras = GADExtras()
        extras.additionalParameters = ["suppress_test_label": "1"]
        request.register(extras)
        view.load(request)
#else
        /* Real device */
        view.load(GADRequest())
#endif
        
        return viewController
    }

    func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
}

struct Banner: View {
    var bannerID: String
    var width: CGFloat

    var size: CGSize {
        return GADCurrentOrientationAnchoredAdaptiveBannerAdSizeWithWidth(width).size
    }

    var body: some View {
        BannerVC(bannerID: bannerID, width: width)
            .frame(width: size.width, height: size.height)
    }
}
struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundColor(.accentColor)
            Text("Hello, world!")
        }
        .padding()
        Banner(bannerID: "ca-app-pub-3940256099942544/2934735716", width: UIScreen.main.bounds.width)
    }
} 

實作方式如上,這邊用的 bannerID 是測試使用的,請參考這邊,順利的話就可以看到下面的畫面

Simulator

若不想在 Simulator 上看到 "Test mode" 的字樣,將上面程式的 /* Simulator */ 上的 if false 改成 if true,就可將之移除如下圖,這功能在你上架 app 提供截圖時會用到
若是跑在實機上的話,上述的程式記得改成 if false,會看到如下畫面
Note

若想 build 在 Mac 上會遇到 No such module 'GoogleMobileAds' 的 error,這個問題我也想知道解答,歡迎提供給我,謝謝

環境

  • Mac Mini M1
  • Xcode Version 14.3 (14E222b)
  • iPhone 12 Pro Max (16.6)

Reference

  • COCOAPODS getting-start (link)
  • Mobile Ads SDK (iOS) Get Started (link)

2023年6月3日 星期六

[scrapy] 使用 scrapy 爬不到資料

Photo by ThisIsEngineering

記錄一下使用 scrapy 遇到的一些問題

問題:
用 browser 打開這個網頁明明就能看到畫面,但使用下面方法後
scrapy shell https://info.sogo.com.tw/tp1/floors/B2 
view(response)
卻只能看到下面錯誤

後來發現,這是因為少設定了 USER_AGENT 的關係,user agent 通常是用來識別 request 這方的軟體、os、device 等資訊,有些 server 會檢查這個 http header 資訊,若沒有的話,就不會回傳正確 response

處理方式:
在 settings.py 裡找到 USER_AGENT 設定,將一個正確的 user agent 填入即可,這邊我是參考 chrome 的 user agent 填入(如下)

問題:
網頁能打開後,卻找不到品牌列表,用 browser 打開往下 scroll 後,會看到如下的畫面,品牌列表的部份看起來是被放在這樣的結構裡

<div class="tab-content">
...
<a href="xxxx" class="brandBox">...</a>

但是使用
scrapy shell https://info.sogo.com.tw/tp1/floors/B2 
view(response)
卻只能看到 <div class="tab-content"> 裡沒有 brandBox 的資料

分析:
用 chrome 的 inspect 可以找到品牌列表是用這個 request 來的

而這個 request 是由 jquery-3.2.0.min.js 裡呼叫的
由此就能確定,找不到品牌列表是因為他是 javascript 的 request 而來的,scrapy 預設是爬靜態網頁,所以我邊需要針對這個 javascript 的 request 多做處理才行

這邊分享兩個 debug 技巧,
1. 在 spider 程式內呼叫 scrapy shell 的功能,在 def parse() 裡寫到下面即可
from scrapy.shell import inspect_response
inspect_response(response, self)
2. 在 spider 程式內印出 response 網頁資料,做法如下
def write_to_file(self, words):
    with open("logging.log", "a") as f:
        f.write(words)

def parse():
    self.write_to_file("response text: %s" % response.text)

處理方式:
使用 selenium 幫忙處理 javascript 的 request,照下面方法將 selenium 安裝且設定好,使用 selenium 的 function 做同一個 url 的 request,response 的資料裡就有品牌列表了
安裝 selenium
pip install scrapy-selenium
安裝 chrome driver
這裡下載跟自己使用的 chrome driver 相近的版本
在 settings.py 裡做好設定
總共四個設定,SELENIUM_DRIVER_EXECUTABLE_PATH 就是填上面下載的 chrome driver 檔案的路徑
在 spider 中使用
這邊不使用原本的 start_urls,而是使用如下的做法
from scrapy_selenium import SeleniumRequest
def start_request(self):
    target_url = "xxx"
    yield SeleniumRequest(url=target_url, callback=self.parse)

原本使用下面兩行去看有多少品牌是 0,設定成功之後,就可以看到 60
brands_num = len(response.xpath('//a[@class="brandBox"]'))
print("brands_num: " + (str)(brands_num))

Reference

2023年2月26日 星期日

[Web] 開源傳說小工具網頁版

Photo by Ben Taylor

在 [App] iOS 傳說小工具誕生以及網頁版結束維護 有提到,網頁版傳說小工具會想 open source,好讓有興趣的人可以繼續維護,現在完成開源了。

開源傳說對決網頁版
https://github.com/bradchow/toolsofrov_web

開源的程式碼原本是在 Heroku 上運作在 php 後台的,所以只要有 php 環境,放上這些檔案就可以直接使用,我只有把下面程式拿掉,然後加上一些 readme.md 和 LICENSE.md

1. Firebase
2. Google Analytics
3. Google Adsense

2023年1月20日 星期五

[App] iOS 傳說小工具誕生以及網頁版結束維護

Photo by Brett Jordan

網頁版傳說小工具使用的主機 Heroku,已經停止免費提供網頁服務了,傳說小工具一直以來的變現能力都不好,所以也不打算花錢買 Heroku 的服務,雖然網頁版傳說小工具就這樣畫上句點,但未來有打算將他 open source,讓有興趣的人可以繼續維護。

網頁版從 2017 年開始服務,當時還提到若要開發 iOS 版必定要花上很長一段時間,我一直沒有忘記這件事,工作緣故讓我必須多接觸 iOS 相關環境,這也正是我著手進行 iOS 版的契機,雖然變現能力不好,Apple Developer 的保護費費用也相當高(每年 100 美金),但在 Android Play Store 後台看到還是有不少人在使用,決定還是要完成這件事,2017 年推出了 Android 版,在經過了 5 年多之後,iOS 版傳說小工具終於上架了,同樣的,第一版功能還蠻陽春,讓我們慢慢改進

傳說小工具 App Store 連結
https://apps.apple.com/app/id1664793884

傳說小工具粉絲專頁
https://www.facebook.com/toolsofrov/