2024年2月25日 星期日

[scrapy] 實戰記錄 - 台北 101 網站

攝影師:Pixabay

為了增加百貨公司品牌爬蟲的內容, 近期開始研究台北 101 網站,找到這裡可以看到全部的品牌,把這些都爬出來就行了

先找出正確的 element

從下圖可看出,這些品牌存在於 "listarea logo" class 下面的 "cardobj lazyload show" class 中

問題:

準備好 request https://www.taipei-101.com.tw/tw/shopping/brandsearch 然後去拿 "cardobj lazyload show" class 裡的資料,但會發現抓不到資料,將 response.text 印出來後存成檔案才發現,"listarea logo" class 裡是沒有內容的

處理方式:

發現這些品牌資訊是另外 request 去拿到 json 檔案回來 parse 而成

這邊直接找出此 request,直接用 python 去抓回來 parse 完成





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