# 5/25早上 比價程式實作 今天的比價程式實作,運用selenium以及beautifulsoup套件去截取yahoo拍賣上特定種類商品的商品資料,經過整理,在終端輸出,以及寫入csv檔案。 ## 為什麼選擇yahoo拍賣? 1. yahoo拍賣網站好爬,沒有特別的防爬 2. yahoo拍賣提供商家的好評數資料,可以在選購時做參考 ## 程式的製作過程 完成此程式需經過幾個步驟: 1. 引入網路爬蟲套件 2. 運用selenium開啟yahoo拍賣網站 3. 模擬鍵盤輸入滾動網頁 4. 運用beautiful截取商品資訊 5. 建立class以存取截取的商品資料 6. 以迴圈方式達成換頁功能 7. 以指定方式排序商品資料 8. 在終端印出資料 9. 將資料寫入csv檔案 那我們就按部就班的開始吧! ## 一、 引入網路爬蟲套件 總共需要引入6項套件 ![image](https://hackmd.io/_uploads/BkqQps5ZR.png) ```python= from selenium import webdriver import time from bs4 import BeautifulSoup from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By import csv ``` 1. 從selenium引入網路瀏覽器 2. 引入time 3. 引入beautifulsoup 4. 從selenium引入keys 5. 從selenium引入by 6. 引入csv 引入selenium的各套件可以幫助我們在開啟瀏覽器後做更多方便的操作,有Keys可以模擬鍵盤輸入,有By可以選擇以何種方式尋找資料,引入time可以在各段程式中等待上個程式完整執行,引入beautifulsoup可以作為截取資料的主要工具,最後引入csv以寫入將擷取的資料寫入csv檔案中。 ## 二、 運用selenium開啟yahoo拍賣網站 首先,觀察yahoo拍賣網站的,這是我在yahoo拍賣搜尋耳機的第二頁網址 https://tw.bid.yahoo.com/search/auction/product?p=%E8%80%B3%E6%A9%9F&pg=2 可以發現關鍵字以及頁數都有寫在內,只要我用字串處理改動網址中的兩項資訊,就能連結到不同搜尋、不同頁數的搜尋結果。 這邊宣告兩個變數,keyword以及page,分別記錄搜尋的關鍵字以及頁碼,然後用字串處裡的方式將他們串起來,形成url,以導向選定的網址。 ![image](https://hackmd.io/_uploads/Skplpi9ZA.png) ```python= keyword = pages = # 開啟瀏覽器 driver = # 設定目標網址 url = "https://tw.bid.yahoo.com/search/auction/product?p=" + keyword + "&pg=" + str(pages) # 根據keyword和頁數的網址 # 開啟網址 print(url) time.sleep(1) # 等個一秒 ``` 到這裡,我們應該已經成功開啟yahoo拍賣網站了! ## 三、 模擬鍵盤輸入滾動網頁 在剛開起網頁時,並非所有的商品資料都會在可擷取的狀態,在較下方的資料要等滑到那裡才會跑出來,所以我們需要寫一段程式來幫助我們完成下滑作業。 ![image](https://hackmd.io/_uploads/r1MB6jcb0.png) ```python= body = driver.find_element() # 對body輸入page_down以完成網頁下滑 for _ in range(18): # 下滑18次 # 實作send_keys下滑 ``` 我們看到的網頁的本體就是body部分,在較大的鍵盤中會有page_down按鍵,按下就可以往下滾動,跟按下方向鍵很像,我們先用selenium的find_element找出body,再來重複對其輸入page_down的Keys就可以一步一步往下滾動。 Keys的功能還有很多,他可以模擬所有鍵盤活動,有興趣了解更多可以參考: https://www.selenium.dev/selenium/docs/api/py/webdriver/selenium.webdriver.common.keys.html By的用法則可以參考: https://ithelp.ithome.com.tw/articles/10300961 ## 四、 運用beautifulsoup截取商品資訊 這次實作我選擇使用beautifulsoup截取html,以左邊的商品為例,我想要的資訊有5項: 1. 賣家,也就是 九元生活百貨 2. 好評數,範例中的是10351 3. 標籤,標籤中包含前兩項,用來分出沒有好評數的店家 4. 商品標題,也就是【九元生活百貨】 EA 高密度海綿耳機套/5cm 適用...(後面省略) 5. 價格,也就是$29 ![螢幕擷取畫面 2024-04-05 170200](https://hackmd.io/_uploads/HJyhDeEZ0.png)![image](https://hackmd.io/_uploads/B1ecxH-ZC.png) 而這五項資料在html中的classname分別是這五個紅底線標出的字串,要注意的是有些商品像是右邊這個是沒有好評數的,在第五步的時候會解決這個問題。 ![螢幕擷取畫面 2024-04-05 165815](https://hackmd.io/_uploads/r1g7uK1m0.png) 所以先建立一個html解析器,然後對這五個class做select。 ![image](https://hackmd.io/_uploads/HJv-0hHQR.png) ```python= soup = BeautifulSoup(driver.page_source, 'html.parser') # 用beautifulsoup擷取網站資訊 merchants = soup.find_all("span", class_="") # 擷取商家名稱 goodreviews = soup.find_all("div", class_ = "") # 擷取好評數 tags = soup.find_all("div", class_ = "") # 擷取標籤 titles = soup.find_all("span", class_ = "") # 擷取商品標題 prices = soup.find_all("span", class_ = "") # 擷取價格 ``` 如此一來就成功截取出五項資料的set了。 ## 五、 建立class以存取截取的商品資料 一個class是一個類別,建立一個類別相當於是建立了一個特定的物件種類,並且可以為這個種類新增實例,也可以為這個種類加入屬性和函式。 舉例來說,我可以設定一個class叫做人類,接著我可以創造很多人類,每個人類物件都是人類class底下的一個實例,但他們可以有不同的身高、體重、膚色、髮型。如果我在人類class下設定一個功能叫量身高,每個人類物件都能量身高,不過每個人得出的結果不一定一樣。 想看更詳細的class教學,可以參考: https://weilihmen.medium.com/%E9%97%9C%E6%96%BCpython%E7%9A%84%E9%A1%9E%E5%88%A5-class-%E5%9F%BA%E6%9C%AC%E7%AF%87-5468812c58f2 而以今天的實作為例,我建立了一個class名較merchantdise,接著我可以創造很多merchantdise,merchantdise是一個類別,有點像是一種字定義的type,我可以為merchantdise加入屬性,一個merchantdise要有賣家、好評數、標題、價格等屬性。 ![image](https://hackmd.io/_uploads/SJfO6o5ZA.png) ```python= class merchantdise: # 每個商品,包含商品標題、商家、價格、好評數 def __init__(): # 實做建構子,付值於四項變數 ``` 這個init是建構子,每當我創造一個新的merchantdise物件時,就會運作,並且建立時,我需要加上一個括號填入這個merchantdise的資料。 再來看一下程式,先建立一個list存入等等要創建的merchantdise,然後跟據擷取到的標籤數去做迴圈,因為每個商品都有標籤,所以這個迴圈會為每個商品都建立一個merchantdise去存他的資料,接著根據標籤中有沒有找到好評數的class去分成兩種狀況: 1. 這個商品有好評數 如果有好評數就可以直接將包括好評數在內的四個資料都放入括弧建立merchantdise,並且加入merchantdises中,要注意的是那四個資料中,好評數是使用review_count去作為index,而其他三個是使用tag_count去做index。 2. 這個商品沒有好評數 爭對這種問題,我建立了一個review_count來記錄目前數到goodreviews的哪一個,而沒有好評數的商品就以0填入好評數。 ![image](https://hackmd.io/_uploads/ByAZTa0Q0.png) ```python= tag_count = 0 # 有標籤的商品數 review_count = 0 # 有好評的商品數 merchantdises = [] # 用來裝入所有的商品 for tag in tags: # 為每個商品建立一個merchantdise物件 if str(tag).find("") != -1: # 如果商品標籤中有好評數 # 直接建立merchantdise物件 review_count += 1 # 有好評數的商品+1 else: # 如果商品沒有好評數 # 建立好評述為0的商品 tag_count += 1 # 換到下一個商品 time.sleep(5) ``` ## 六、 以迴圈方式達成換頁功能 根據第二步對yahoo拍賣網址的觀察,只要改動網址中的一個字,就可以決定要到第幾頁,所以使用迴圈便可以依序對多頁進行資料擷取。 ![image](https://hackmd.io/_uploads/SyrPt_GMR.png) ```python= for i in range(pages): # 重複輸入頁數次 #需要重複的內容 ``` --- 需要重複的內容: 1. 重置tag_count及review_count 2. url中的頁數改成str( i+1 ) 3. 開啟網址 4. 滾動網頁 5. beautifulsoup擷取 6. 將擷取的資料建立merchantdise並加入merchantdises 而不需要重複的內容: 1. 引入套件 2. 宣告merchantdises、keyword、pages 3. class merchantdise的宣告 4. 宣告driver,開啟瀏覽器 ## 已完成部分統整 已完成的包括引入套件、宣告變數、宣告merchantdise類別、開啟網頁、使用迴圈達到多頁擷取、使用beautifulsoup擷取資料、資料整理存入類別。 在這裡先展示一下目前為止的程式: ```python= from selenium import webdriver import time from bs4 import BeautifulSoup from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By import csv merchantdises = [] # 用來裝入所有的商品 keyword = input("請輸入想搜尋的商品:") pages = int(input("請輸入想擷取的頁數(每頁約60筆):")) class merchantdise: # 每個商品,包含商品標題、商家、價格、好評數 def __init__(self, title, merchant, price, goodreview): self.title = title # 商品標題 self.merchant = merchant # 商家 self.price = price # 價格 self.goodreview = goodreview # 好評數 driver = webdriver.Chrome() # 開啟瀏覽器 for i in range(pages): # 重複輸入頁數次 tag_count = 0 # 有標籤的商品數 review_count = 0 # 有好評的商品數 url = "https://tw.bid.yahoo.com/search/auction/product?p=" + keyword + "&pg=" + str(i + 1) # 根據keyword和頁數的網址 driver.get(url) # 開啟網址 print(url) time.sleep(1) # 等個一秒 body = driver.find_element(By.CSS_SELECTOR, 'body') # 對body輸入page_down以完成網頁下滑 for _ in range(18): # 下滑18次 body.send_keys(Keys.PAGE_DOWN) time.sleep(0.5) soup = BeautifulSoup(driver.page_source, 'html.parser') # 用beautifulsoup擷取網站資訊 merchants = soup.find_all("span", class_="sc-dlyefy sc-gKcDdr NlYrF dCTDDo") # 擷取商家名稱 goodreviews = soup.find_all("div", class_ = "sc-cwVdxJ DWNSG") # 擷取好評數 tags = soup.find_all("div", class_ = "sc-1f5heg1-0 cWqAKY") # 擷取標籤 titles = soup.find_all("span", class_ = "sc-dlyefy sc-gKcDdr sc-1drl28c-5 jHwfYO hMnBms jZWZIY") # 擷取商品標題 prices = soup.find_all("span", class_ = "sc-dlyefy sc-gKcDdr dfRcqf ligopQ") # 擷取價格 time.sleep(6) for tag in tags: # 為每個商品建立一個merchantdise物件 if str(tag).find("sc-cxtRbA iwvVQZ") != -1: # 如果商品標籤中有好評數 # 直接建立merchantdise物件 merchantdises.append(merchantdise(titles[tag_count].text, merchants[tag_count].text, prices[tag_count].text, int(goodreviews[review_count].text))) review_count += 1 # 有好評數的商品+1 else: # 如果商品沒有好評數 # 建立好評述為0的商品 merchantdises.append(merchantdise(titles[tag_count].text, merchants[tag_count].text, prices[tag_count].text, 0)) tag_count += 1 # 換到下一個商品 time.sleep(5) ``` ## 七、 以指定方式排序商品資料 到此為止,擷取資料的部分已經結束了,接下來是資料處理,要將merchantdises以使用者想要的方法去做排序。 先宣告兩個需要輸入的變數,sort_class和sort_way ![image](https://hackmd.io/_uploads/By5weKzfA.png) ```python= #宣告兩個變數 sort_class = sort_way = ``` 接著我就根據輸入的是"價格"或是"好評數"去做排序。 1. 好評數 這邊需要使用到sorted函式中的key功能,用了key之後排序的標準就是將list中的每個元素放進key設定的函式,然後根據回傳值去排序。 由於goodreview是一個字串型態的數字,且一定是整數,所以很好處理,只需將merchantdises中的每個merchantdise輸入後,就會回傳merchantdise.goodreview的整數。 補充說明:可以使用.去呼叫class中的變數,例如merchantdise.goodreview ![image](https://hackmd.io/_uploads/rJEBgtMMR.png) ```python= def compare_goodreview(merchantdise): # 好評數的排序 return int(merchantdise.goodreview) # 回傳數值好評數 # 將每個merchantdise物件輸入compare_goodreview函數,並依據回傳值排序 ``` 2. 價格 要排序價格比較麻煩,因為在yahoo拍賣中,價格有可能是一個數字,也有可能是一個範圍,對於一個範圍的價格我選擇以範圍的上限作為代表去比較。 在compare_price中,輸入一個merchantdise,如果在該商品的price中能找到" - ",代表,該商品的價格是一個範圍,則將" - "之後的字串作處理,如果是數字,就加入回傳的return_price中,得出價格後,轉成整數型別回傳去做排序。而如果沒有" - ",那就只是一個加了$符號的數字,直接做字串處理即可。 這裡有用到substring,我用到的是在字串後加上[ i: ],表示字串第 i 個字元及之後的部分。 如果想看更詳細的教學,可以參考: https://www.freecodecamp.org/chinese/news/how-to-substring-a-string-in-python/ ![image](https://hackmd.io/_uploads/r1PMeFMfC.png) ```python= def compare_price(merchantdise): # 用來將字串的價格轉為數字價格以方便排序 return_price = "" # 要return 的價格 if (merchantdise.price.count("-") == 1): # 如果價格為範圍型的 # 取出在"-"符號之後的數字加入price elif (merchantdise.price.count("-") == 0): # 如果價格為單數字型的 # 取出數字加入price return int(return_price) # 回傳價格數值 # 將每個merchantdise物件輸入compare_price函數,並且依據回傳值排序 merchantdises = sorted(merchantdises, key=compare_price) ``` 再來考慮升序和降序的問題,因為sorted的預設是由小到大,所以若輸入的是降序,需要將整個list反過來,升序則不需要,這時可以用到sorted的第二個功能reverse,設定reverse = True就可以將排序反過來。 ![image](https://hackmd.io/_uploads/H1u2lKGG0.png) ```python= merchantdises = sorted(merchantdises, key=compare_goodreview, reverse=True) ``` 總結來說,就是用兩層的if將其分成四種情況,還有輸入錯誤特殊情況 ![image](https://hackmd.io/_uploads/HJUbbtGz0.png) ```python= if sort_class == "價格": # 按照價格排序,預設是由小到大 if sort_way == "升序": # 實作排列 elif sort_way == "降序": # 實作排列 else: print("Input Error") # 第四個輸入不是升序也不是降序 elif sort_class == "好評數": # 按照好評數,預設是由小到大 if sort_way == "升序": # 實作排列 elif sort_way == "降序": # 實作排列 else: print("Input Error") # 第四個輸入不是升序也不是降序 else: print("Input Error") # 第三個輸入不是價格也不是好評數 ``` ## 八、 在終端印出資料 終於到了輸出階段了,先解決在終端輸出的問題,一般的輸出不行,不好看。 為了對齊輸出,用了兩種方法,第一種是自訂義的unicode檢查,另一種是內建的format格式化輸出,有內建的格式化輸出,為什麼還要自訂義對齊功能呢?因為格式化輸出對於有全形字又有半形字的字串並不能實質對齊。 1. 自訂義對齊 因為商家名稱中常出現既有全形字又有半形字的情況,所以選擇這種方式,一個字串被輸入進alignment函式中,並且拆分成字元,每個字元再輸入進is_full_width函式中,is_full_width函式中,我列出了unicode中比較常見的中英文符號全形字,然後用一個for迴圈去檢查這個字元是否為全形字,在計算出整個字串中有多少全形字、半形字後,將全形字算做長度2,半形字算做長度1,並且加上長度到達40個半形字長的空格,就可以實作對齊。 什麼是unicode呢?unicode是資訊領域中的統一字元編碼,以16進位表示,想知道更詳細的資料可以參考: https://zh.wikipedia.org/zh-tw/Unicode ![image](https://hackmd.io/_uploads/Byn69tGMA.png) ```python= def is_full_width(char): # 用unicode判斷字元是否為全形字 unicode_value = ord(char) full_width_range = [ (0xFF01, 0xFF5E), # 全形英文字母和數字 (0xFF10, 0xFF19), (0xFF21, 0xFF3A), (0xFF41, 0xFF5A), (0xFF01, 0xFF0F), # 全形標點符號 (0xFF1A, 0xFF20), (0xFF3B, 0xFF40), (0xFF5B, 0xFF60), (0xFFE0, 0xFFEE), (0x3000, 0x303F), # 中日韓符號和標點 (0x3100, 0x312F), # 注音 (0x4E00, 0x9FFF), # 中文字範圍 (0x20000, 0x2A6DF), # 中文補充 (0x2F800, 0x2FA1F), # 中日韓符號和標點 ] for start, end in full_width_range: if unicode_value >= start and unicode_value <= end: return True return False def alignment(text): # 補齊商家名稱的空格,用來對齊輸出 half = 0 # 半形字 full = 0 # 全形字 format_text = "" # 準備要回傳的字串 for chr in text: # 用is_full_width檢查每個字是半形還是全形 if is_full_width(chr): full += 1 else: half += 1 format_text += text # 先加上原本的字串 if full*2 + half < 40: # 全形算兩格,半形算一格 for _ in range(0,40-(full*2+half),1): # 並且把不足40格的部分加上空格 format_text += " " return format_text # 回傳長度為40的商家名稱 print(alignment(merchantdises[i].merchant)) ``` 2. format格式化輸出 python內建的format格式化輸出可以達到用空格將字串長度統一的輸出,字串長度並非肉眼看到的長度,而是字元數,在只有半形字的地方很好用。 在print內加入'{:<n}'.format(),可以將要輸出的字改為n格的輸出,不足處會用空格補齊,<改成>會變成像置右,<改成^會變成置中,:跟<中間加入任意符號會改成用該符號填滿不足處。 format還可以改變數字進位模式,可以取小數點位數,想知道更詳細的資料可以參考: https://www.runoob.com/python/att-string-format.html ![image](https://hackmd.io/_uploads/B1EriYGfR.png) ```python= # 實作 格式化輸出 ``` 整理兩種對齊方式,print的程式: ```python= def is_full_width(char): # 用unicode判斷字元是否為全形字 unicode_value = ord(char) full_width_range = [ (0xFF01, 0xFF5E), # 全形英文字母和數字 (0xFF10, 0xFF19), (0xFF21, 0xFF3A), (0xFF41, 0xFF5A), (0xFF01, 0xFF0F), # 全形標點符號 (0xFF1A, 0xFF20), (0xFF3B, 0xFF40), (0xFF5B, 0xFF60), (0xFFE0, 0xFFEE), (0x3000, 0x303F), # 中日韓符號和標點 (0x3100, 0x312F), # 注音 (0x4E00, 0x9FFF), # 中文字範圍 (0x20000, 0x2A6DF), # 中文補充 (0x2F800, 0x2FA1F), # 中日韓符號和標點 ] for start, end in full_width_range: if unicode_value >= start and unicode_value <= end: return True return False def alignment(text): # 補齊商家名稱的空格,用來對齊輸出 half = 0 # 半形字 full = 0 # 全形字 format_text = "" # 準備要回傳的字串 for chr in text: # 用is_full_width檢查每個字是半形還是全形 if is_full_width(chr): full += 1 else: half += 1 format_text += text # 先加上原本的字串 if full*2 + half < 40: # 全形算兩格,半形算一格 for _ in range(0,40-(full*2+half),1): # 並且把不足40格的部分加上空格 format_text += " " return format_text # 回傳長度為40的商家名稱 for i in range(len(merchantdises)): # 依序輸出對齊過的商家名稱、補齊到6格的好評數、補齊倒17格的價格、商品標題 print(alignment(merchantdises[i].merchant) + '{:<6}'.format(merchantdises[i].goodreview) + '{:<17}'.format(merchantdises[i].price) + merchantdises[i].title) ``` ## 九、 將資料寫入csv檔案 最後一個步驟,寫入csv檔案,csv(comma-separated values)檔案是以逗號分割資料、每一行都以迴車分隔的資料型態,跟xlsx有點不一樣。 在這次實作中,這種形似二維陣列的儲存方式很適合,在寫入一個檔案前,第一步是開啟該檔案,並且選擇以"w"模式,也就是複寫,newline = "" 是為了讓換行能順利被解析。 接著設定寫入器,將第一行的各項數據名寫入後,就是運用迴圈將剩下的資料也都寫入,比較特別的是這裡用到了try - except功能,這個功能是當不確定會不會有exception出現時,可以先try,若出現exception則做出應對。 這次的實作截取了網路上的商家與商品,其中會參雜無法編碼的特殊符號,所以使用try - except,避免掉因為特殊符號而停止寫入檔案的問題,用了三層的try - except去確認到底是商家名稱、商品標題或是兩個都有問題,出現問題的部分將改成設定好的錯誤訊息。 對於python csv檔案的運用,如果想看更詳細資料可以參考: https://ithelp.ithome.com.tw/articles/10292897 對於try - except功能,如果想看更詳細資料可以參考: https://steam.oxxostudio.tw/category/python/basic/try-except.html ![image](https://hackmd.io/_uploads/SyXVd_wfA.png) ```python= with open("shopping_result.csv","w",newline="") as csvfile: # 開啟shopping_result.csv檔案以寫入 writer = csv.writer(csvfile) # 寫入器 writer.writerow(["賣家","好評數","價格","商品名稱"]) # 寫入第一列的各項數據名 for i in range(len(merchantdises)): try: # 先嘗試寫入資料 writer.writerow([merchantdises[i].merchant,merchantdises[i].goodreview,merchantdises[i].price,merchantdises[i].title]) except UnicodeEncodeError: #如果出現unicode編碼錯誤,也就是無法寫入的字元 try: # 嘗試將商品名稱改成錯誤訊息 # 實作寫入,包函特殊情況 except UnicodeEncodeError: # 如果還是不行試試看把商品名稱改為錯誤訊息 try: # 實作寫入,包函特殊情況 except UnicodeEncodeError: # 再不行就是兩個都改 # 實作寫入,包函特殊情況 ``` ## 實作總結 放個完整程式碼: ```python= from selenium import webdriver import time from bs4 import BeautifulSoup from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.by import By import csv merchantdises = [] # 用來裝入所有的商品 keyword = input("請輸入想搜尋的商品:") pages = int(input("請輸入想擷取的頁數(每頁約60筆):")) sort_class = input("請輸入""價格""或""好評數""以選擇排序方式:") sort_way = input("請輸入""降序""或""升序""以選擇排序模式:") class merchantdise: # 每個商品,包含商品標題、商家、價格、好評數 def __init__(self, title, merchant, price, goodreview): self.title = title # 商品標題 self.merchant = merchant # 商家 self.price = price # 價格 self.goodreview = goodreview # 好評數 def compare_price(merchantdise): # 用來將字串的價格轉為數字價格以方便排序 return_price = "" # 要return 的價格 if (merchantdise.price.count("-") == 1): # 如果價格為範圍型的 for chr in merchantdise.price[merchantdise.price.index("-"):]: # 取出在"-"符號之後的數字加入price if chr in ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"): return_price += chr elif (merchantdise.price.count("-") == 0): # 如果價格為單數字型的 for chr in merchantdise.price: if chr in ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9"): # 取出數字加入price return_price += chr return int(return_price) # 回傳價格數值 def compare_goodreview(merchantdise): # 好評數的排序 return int(merchantdise.goodreview) # 回傳數值好評數 def is_full_width(char): # 用unicode判斷字元是否為全形字 unicode_value = ord(char) full_width_range = [ (0xFF01, 0xFF5E), # 全形英文字母和數字 (0xFF10, 0xFF19), (0xFF21, 0xFF3A), (0xFF41, 0xFF5A), (0xFF01, 0xFF0F), # 全形標點符號 (0xFF1A, 0xFF20), (0xFF3B, 0xFF40), (0xFF5B, 0xFF60), (0xFFE0, 0xFFEE), (0x3000, 0x303F), # 中日韓符號和標點 (0x3100, 0x312F), # 注音 (0x4E00, 0x9FFF), # 中文字範圍 (0x20000, 0x2A6DF), # 中文補充 (0x2F800, 0x2FA1F), # 中日韓符號和標點 ] for start, end in full_width_range: if unicode_value >= start and unicode_value <= end: return True return False def alignment(text): # 補齊商家名稱的空格,用來對齊輸出 half = 0 # 半形字 full = 0 # 全形字 format_text = "" # 準備要回傳的字串 for chr in text: # 用is_full_width檢查每個字是半形還是全形 if is_full_width(chr): full += 1 else: half += 1 format_text += text # 先加上原本的字串 if full*2 + half < 40: # 全形算兩格,半形算一格 for _ in range(0,40-(full*2+half),1): # 並且把不足40格的部分加上空格 format_text += " " return format_text # 回傳長度為40的商家名稱 driver = webdriver.Chrome() # 開啟瀏覽器 for i in range(pages): # 重複輸入頁數次 tag_count = 0 # 有標籤的商品數 review_count = 0 # 有好評的商品數 url = "https://tw.bid.yahoo.com/search/auction/product?p=" + keyword + "&pg=" + str(i + 1) # 根據keyword和頁數的網址 driver.get(url) # 開啟網址 print(url) time.sleep(1) # 等個一秒 body = driver.find_element(By.CSS_SELECTOR, 'body') # 對body輸入page_down以完成網頁下滑 for _ in range(18): # 下滑18次 body.send_keys(Keys.PAGE_DOWN) time.sleep(0.5) soup = BeautifulSoup(driver.page_source, 'html.parser') # 用beautifulsoup擷取網站資訊 merchants = soup.find_all("span", class_="sc-dlyefy sc-gKcDdr NlYrF dCTDDo") # 擷取商家名稱 goodreviews = soup.find_all("div", class_ = "sc-cwVdxJ DWNSG") # 擷取好評數 tags = soup.find_all("div", class_ = "sc-1f5heg1-0 cWqAKY") # 擷取標籤 titles = soup.find_all("span", class_ = "sc-dlyefy sc-gKcDdr sc-1drl28c-5 jHwfYO hMnBms jZWZIY") # 擷取商品標題 prices = soup.find_all("span", class_ = "sc-dlyefy sc-gKcDdr dfRcqf ligopQ") # 擷取價格 time.sleep(6) for tag in tags: # 為每個商品建立一個merchantdise物件 if str(tag).find("sc-cxtRbA iwvVQZ") != -1: # 如果商品標籤中有好評數 # 直接建立merchantdise物件 merchantdises.append(merchantdise(titles[tag_count].text, merchants[tag_count].text, prices[tag_count].text, int(goodreviews[review_count].text))) review_count += 1 # 有好評數的商品+1 else: # 如果商品沒有好評數 # 建立好評述為0的商品 merchantdises.append(merchantdise(titles[tag_count].text, merchants[tag_count].text, prices[tag_count].text, 0)) tag_count += 1 # 換到下一個商品 time.sleep(5) if sort_class == "價格": # 按照價格排序,預設是由小到大 if sort_way == "升序": merchantdises = sorted(merchantdises, key=compare_price, reverse=False) # 將每個merchantdise物件輸入compare_price函數,並且依據回傳值排序 elif sort_way == "降序": merchantdises = sorted(merchantdises, key=compare_price, reverse=True) # 將每個merchantdise物件輸入compare_price函數,並且依據回傳值排序 else: print("Input Error") # 第四個輸入不是升序也不是降序 elif sort_class == "好評數": # 按照好評數,預設是由小到大 if sort_way == "升序": merchantdises = sorted(merchantdises, key=compare_goodreview, reverse=False) # 將每個merchantdise物件輸入compare_goodreview函數,並且依據回傳值排序 elif sort_way == "降序": merchantdises = sorted(merchantdises, key=compare_goodreview, reverse=True) # 將每個merchantdise物件輸入compare_goodreview函數,並且依據回傳值排序 else: print("Input Error") # 第四個輸入不是升序也不是降序 else: print("Input Error") # 第三個輸入不是價格也不是好評數 print("從yahoo拍賣搜尋:" + keyword + "\t以" + sort_class + sort_way) # 印出這次運行的輸入 print('{:<38}'.format("賣家") + '{:<4}'.format("好評") + '{:<15}'.format("價格") + "標題") # 印出各項數據名 for i in range(len(merchantdises)): # 依序輸出對齊過的商家名稱、補齊到6格的好評數、補齊倒17格的價格、商品標題 print(alignment(merchantdises[i].merchant) + '{:<6}'.format(merchantdises[i].goodreview) + '{:<17}'.format(merchantdises[i].price) + merchantdises[i].title) with open("shopping_result.csv","w",newline="") as csvfile: # 開啟shopping_result.csv檔案以寫入 writer = csv.writer(csvfile) # 寫入器 writer.writerow(["賣家","好評數","價格","商品名稱"]) # 寫入第一列的各項數據名 for i in range(len(merchantdises)): try: # 先嘗試寫入資料 writer.writerow([merchantdises[i].merchant,merchantdises[i].goodreview,merchantdises[i].price,merchantdises[i].title]) except UnicodeEncodeError: #如果出現unicode編碼錯誤,也就是無法寫入的字元 try: # 嘗試將商品名稱改成錯誤訊息 writer.writerow([merchantdises[i].merchant,merchantdises[i].goodreview,merchantdises[i].price,"title with unencodable character"]) except UnicodeEncodeError: # 如果還是不行試試看把商品名稱改為錯誤訊息 try: writer.writerow(["merchant name with unecodable character",merchantdises[i].goodreview,merchantdises[i].price,merchantdises[i].title]) except UnicodeEncodeError: # 再不行就是兩個都改 writer.writerow(["merchant name with unecodable characteer",merchantdises[i].goodreview,merchantdises[i].price,"title with unencodable character"]) ```