2024年4月19日 星期五

Python 學習筆記 : 用 json 模組讀寫 .json 檔

最近在整理 Python 網頁爬蟲筆記, 覺得有兩個內建模組很常用, 第一個是 csv 模組, csv 檔是 Excel 試算表的純文字版, csv 模組提供讀寫 csv 檔的函式可快速地將檔案內容轉成 Python 字典或串列, 用法參考 : 


第二個是 json 模組, 這是 Python 用來處理 JSON 格式資料的內建模組, 與 csv 模組一樣, 直接匯入即可使用 :

import json  

JSON  (JavaScript Object Notation) 是一種結構化資料表示法, 源自 Javascript 的物件定義語法, 因為具有輕量與可讀性高的特性, 逐漸取代較複雜的 XML 成為最普及的 Web 資料交換格式, 也被許多程式語言支援, 很多 Web API 採用 JSON 格式來回傳, 很多 NoSQL 資料庫也以 JSON 來儲存資料, 參考 : 


JSON 資料格式有兩種語法, 第一種是與 Python 字典類似, 由鍵值對組成的 Javascript 物件, 語法如下 :

{"key1": "value1", "key2": "value2", ........}   
 
規則 : 
  • 鍵值對必須用大括號括起來, 每組鍵值對以逗號隔開
  • 鍵 (key) 必須是字串, 且須使用雙引號括起來, 不可用單引號
  • 值 (value) 可以是所有 Javascript 資料型態 : 數值, 字串, true/false, 陣列, null
  • 物件內不可使用任何註解
  • 交換資料時存成副檔名為 .json 檔案, 每一個 .json 檔只能含有一個物件 
例如 :

{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}

第二種 JSON 資料表示法為 Javascript 陣列, 語法與 Python 串列一樣, 元素放在中括號裡面以逗號隔開, 可以是任何 Javascript 資料型態 :

[e1, e2, e3, ...]

例如 : 

["Tony", "male", 26, 172.5, true, null]

不論是陣列或物件, 在一個 JSON 檔裡面只能有一個陣列或一個物件, 如果有多個物件, 可以將其放入一個陣列裡成為物件陣列, 例如 : 

[{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null},
 {"name": "Jane", "gender": "female", "age": 22, "height": 167.2, "married": false, "religion": null}]

如果有多個陣列, 可以將其放入另一個陣列裡成為多維陣列, 例如 : 

[["Tony", "male", 26, 172.5, true, null], ["Jane", "female", 22, 167.2, false, null]]

注意, 這些 JSON 資料都是以字串形式做交換, json 模組提供了 loads() 與 dumps() 函式可用來在 JSON 字串與 Python 字典之間做轉換. 


1. 檢視 json 模組內容 :   

匯入 json 模組後可以用 dir() 函式檢視其內容 : 

>>> import json      
>>> dir(json)   
['JSONDecodeError', 'JSONDecoder', 'JSONEncoder', '__all__', '__author__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', '_default_decoder', '_default_encoder', 'codecs', 'decoder', 'detect_encoding', 'dump', 'dumps', 'encoder', 'load', 'loads', 'scanner']     

也可以用下列自訂模組 members 之 list_members() 函式來進一步了解那些是類別與函式 :

# members.py
import inspect 
def varname(x): 
    return [k for k,v in inspect.currentframe().f_back.f_locals.items() if v is x][0]
def list_members(parent_obj):
    members=dir(parent_obj)
    parent_obj_name=varname(parent_obj)       
    for mbr in members:
        child_obj=eval(parent_obj_name + '.' + mbr) 
        if not mbr.startswith('_'):
            print(mbr, type(child_obj))  

將此函式存成 members.py 模組, 放在目前供作目錄下, 然後匯入其 list_members() 函式來檢視 json 模組 : 

>>> from members import list_members    
>>> list_members(json)   
JSONDecodeError <class 'type'>
JSONDecoder <class 'type'>
JSONEncoder <class 'type'>
codecs <class 'module'>
decoder <class 'module'>
detect_encoding <class 'function'>
dump <class 'function'>
dumps <class 'function'>
encoder <class 'module'>
load <class 'function'>
loads <class 'function'>
scanner <class 'module'>

常用的函式如下表 :


 json 常用函式 說明
 load(file) 從檔案參考 file 讀取 JSON 資料為 Python 字典後傳回
 loads(str) 從 JSON 字串 str 讀取 JSON 資料為 Python 字典後傳回
 dump(data, file) 將 Python 字典寫入 .json 檔案參考 file
 dumps(data [, sort_keys, indent]) 將 Python 字典轉成 JSON 字串後傳回


這四個函式會在 JSON 字串與 Python 字典之間進行轉換, 注意有無 s 的差別在於, 有 s 的處理對象是 JSON 字串; 而無 s 的則是 ,json 檔案. 

Javascript 與 Python 資料類型之對照如下表 :


 JSON 資料類型 Python 資料類型
 物件 object 字典 dict
 陣列 array 串列 list
 字串 string 字串 str
 整數 int 整數 int
 浮點數 Number 浮點數 float
 布林值 true 布林值 True
 布林值 false 布林值 False
 空值 null 空值 None


注意, Javascript 的 true/false 對應於 Python 的 True/False, 而 Javascript 的 null 則對應於 Python 的 None. 


2. 呼叫 json.loads() 將 JSON 字串轉成 Python 字典/串列 :   

json.loads(str) 用來將 JSON 字串轉成 Python 字典, 傳入值為一個 JSON 字串, 傳回值為一個 Python 字典或串列, 例如 :   

>>> json_str='{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}'    
>>> obj=json.loads(json_str) 
>>> type(obj)   
<class 'dict'>
>>> obj    
{'name': 'Tony', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}

可見 json.loads() 將 JSON 字串載入後轉成 Python 字典, 其值也轉成對應之 Python 資料型態. 

其次來看多個物件放在陣列的情況, 如上所述, JSON 資料中只能有一個 Javascript 物件, 若有多個物件要放在陣列中成為物件陣列 :

>>> json_str='[{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}, {"name": "Jane", "gender": "female", "age": 22, "height": 167.2, "married": false, "religion": null}]'     # JSON 資料為兩個物件組成之陣列
>>> obj=json.loads(json_str)     
>>> type(obj)   
<class 'list'>
>>> obj   
[{'name': 'Tony', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}, {'name': 'Jane', 'gender': 'female', 'age': 22, 'height': 167.2, 'married': False, 'religion': None}]

可見 loads() 會將 JSON 字串轉成字典串列. 


3. 呼叫 json.dumps() 將 Python 字典/串列轉成 JSON 字串 :    

dumps() 是 loads() 的反函式, 它可將 Python 字典/串列轉成 JSON 字串, 例如 : 

>>> data={'name': 'Tony', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}   
>>> json_str=json.dumps(data)   
>>> type(json_str)   
<class 'str'>   
>>> json_str      
'{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}'

下面是將字典串列轉成 JSON 字串的測試 :

>>> data=[{'name': 'Tony', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}, {'name': 'Jane', 'gender': 'female', 'age': 22, 'height': 167.2, 'married': False, 'religion': None}]   
>>> json_str=json.dumps(data)    
>>> json_str    
'[{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}, {"name": "Jane", "gender": "female", "age": 22, "height": 167.2, "married": false, "religion": null}]'

當物件多的時候會很難閱讀, 可用 indent 參數來調整縮排, 例如 : 

>>> json_str=json.dumps(data, indent=4)     
>>> json_str   
'[\n    {\n        "name": "Tony",\n        "gender": "male",\n        "age": 26,\n        "height": 172.5,\n        "married": true,\n        "religion": null\n    },\n    {\n        "name": "Jane",\n        "gender": "female",\n        "age": 22,\n        "height": 167.2,\n        "married": false,\n        "religion": null\n    }\n]'

要用 print() 輸出才看得到效果 :

>>> print(json_str)    
[
    {
        "name": "Tony",
        "gender": "male",
        "age": 26,
        "height": 172.5,
        "married": true,
        "religion": null
    },
    {
        "name": "Jane",
        "gender": "female",
        "age": 22,
        "height": 167.2,
        "married": false,
        "religion": null
    }
]


4. 呼叫 json.dump() 將 Python 字典/串列寫入 .json 檔案 :    

呼叫 json.dump() 並傳入 Python 字典/串列與檔案參考可將其寫入 json 檔案 (ANSI 編碼) : 

>>> data={'name': 'Tony', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}  
>>> with open('test.json', 'w') as f:  
    json.dump(data, f)   

開啟目前工作目錄下的 test.json 內容如下, 編碼格式為 ANSI :

{"name": "Tony", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}

可見 dump() 已經將 true/false 轉成 True/False, None 轉成 null 了, 同時也把原先字典使用的單引號全部轉成雙引號, 因為 JSON 格式中字串必須使用雙引號. 

如果字典中所有非英文字母會轉成 unicode, 例如將 name 欄位值改成 "金秀賢" :  

{"name": "\u91d1\u79c0\u8ce2", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}


5. 呼叫 json.load() 載入 .json 檔案轉成 Python 字典/串列 :   

呼叫 json.load(file) 可以從檔案 file 讀取 JSON 資料, 它會傳回一個字/串列, 例如上面範例的檔案 test.json : 

{"name": "\u91d1\u79c0\u8ce2", "gender": "male", "age": 26, "height": 172.5, "married": true, "religion": null}

>>> with open('test.json', 'r') as f:   
    data=json.load(f)   
                    
>>> data  
{'name': '金秀賢', 'gender': 'male', 'age': 26, 'height': 172.5, 'married': True, 'religion': None}

2024年4月18日 星期四

啟用微星桌機 Win 11 家用版

前陣子跟 momo 買的 MSI 電競桌機上周把 NVidia 函式庫與 PyTorch 都安裝好了, 本周在忙爬蟲, 直到今天才有空來啟動 Win 11. 電腦出貨時 Win 11 已安裝好, 箱內附隨機版光碟, 上面有啟動碼, 過七天試用期後拆封刮出啟動碼, 到系統中按下右邊的 "立即啟用" 鈕 : 




按 "變更產品金鑰" 右邊的 "變更" 鈕 :  




輸入產品金鑰按下一步 : 




按 "啟用" 即可 :



2024年4月17日 星期三

Python 學習筆記 : 網頁爬蟲實戰 (三) 證交所休市日期

這篇其實是 2023/3/7 開始寫的, 寫到一半卻擱筆了, 最近整理爬蟲筆記時被我撈出來, 今天就拿這網頁當練習範例唄. 我去年 (2023) 初曾撰寫一個盯盤程式佈署到樹莓派, 利用 Crontab 設定周一到周五執行, 但是遇到連假 (例如中秋節) 股市休市但程式還是照樣丟出 Line Notify 通知盤勢訊息 (其實是上一個交易日舊聞),  如果有證交所休市訊息就可以解決此問題.     

證交所每年年底都會在網站公布次年休市日期, 網址如下 : 





只要在年底或元旦那天去爬這網頁並更新資料庫 (一年一次), 則盯盤程式在執行時就可先查詢此資訊, 判斷要不要發出盤勢訊息. 注意, 此張表格內的日期並非全部都是休市日期, 有三天是交易日期, 因此擷取下來後需剔除交易日才能用. 


1. 分析網頁內容為靜態網頁還是 JS 動態生成 :    

檢視網頁原始碼可知, 休市日期的表格內容並未出現在原始碼中, 初步研判是用 Javascript 產生 內容插入樣式類別為 main-content 的 div 容器 :

<div class="main-content">
    <div class="rwd-table  dragscroll grid all F1 L2_ G2 G3"></div>
</div>

使用 Chrome 擴充功能 Quick Javascript Switcher 檢查, 當關閉 Javascript 時表格就消失, 開啟時就出現, 確認表格內容係透過 Javascript 程式生成 (例如 Ajax), 這種網頁用 requests 套件無法直接擷取到目標資訊. 關於 Quick Javascript Switcher 參考 :


這種網站最常見的是利用 Ajax 方式透過 HttpXML 以非同步方式向伺服器提出資料請求, 通常回應一個 JSON 檔, 此檔案之 URL 必須利用瀏覽器的開發者工具觀察客戶端與伺服端之間的網路傳遞過程. 以 Chrome 來說, 在目標網頁上按 F12 即可開啟開法者工具頁面, 然後按 Ctrl+R 重新整理頁面, 先按開發者工具頁面的 "Networking" 鈕, 再按 "Fetch/XHR" 鈕 : 




觀察伺服器回應中時間最長的那個資源, 通常是透過 Ajax 傳送回應資料, 點一下若又方框顯示該資源為 JSON 格式的目標資料就對了 : 




在該資源上按滑鼠右鍵, 點選 "Copy/Copr URL" 複製此資源之 URL 如下 : 


直接貼到瀏覽器網址列即可看到此 JSON 資料 :




網頁中的 main.js 就是用此 JSON 資料填到樣式類別為 main-content 的 div 容器來生成表格內容. 所以爬蟲的擷取對象便從 holiday.htm 網頁轉為上面的 JSON 網址了. 


2. 直接擷取 JSON 資料 :      

經過上面的觀察分析, 將擷取目標轉為 JSON 資料網址, 可用 requests 來抓 : 

>>> import requests   
>>> url='https://www.twse.com.tw/rwd/zh/holidaySchedule/holidaySchedule?response=json&_=1713199193355'   
>>> response=requests.get(url)    
>>> response   
<Response [200]>  

可見不用設 User-Agent 假裝是瀏覽器, 就可以直接抓 JSON 資料 : 

>>> type(response.text)   
<class 'str'>  
>>> response.text   
'{"stat":"ok","date":"20240101","title":"113 年市場開休市日期","fields":["日期","名稱","說明"],"data":[["2024-01-01","中華民國開國紀念日","依規定放假1日。"],["2024-01-02","國曆新年開始交易日","國曆新年開始交易。"],["2024-02-05","農曆春節前最後交易日","農曆春節前最後交易。"],["2024-02-06","市場無交易,僅辦理結算交割作業",""],["2024-02-07","市場無交易,僅辦理結算交割作業",""],["2024-02-08","農曆除夕前一日","2月8日(星期四)調整放假,於2月17日(星期六)補行上班,但不交易亦不交割。"],["2024-02-09","農曆除夕","依規定放假1日。"],["2024-02-10","農曆春節","依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>"],["2024-02-11","農曆春節","依規定於2月10日至2月12日放假3日。\\r\\n\\r\\n2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。\\r\\n"],["2024-02-12","農曆春節","依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>"],["2024-02-13","農曆春節","依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>"],["2024-02-14","農曆春節","依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>"],["2024-02-15","農曆春節後開始交易日","農曆春節後開始交易。"],["2024-02-28","和平紀念日","依規定放假1日。"],["2024-04-04","兒童節","依規定放假1日。"],["2024-04-05","民族掃墓節","依規定放假1日。"],["2024-05-01","勞動節","依規定放假1日。"],["2024-06-10","端午節","依規定放假1日。"],["2024-09-17","中秋節","依規定放假1日。"],["2024-10-10","國慶日","依規定放假1日。"]],"queryYear":2024,"total":20}'

可見欄位標題是放在 fields 欄位, 而表格資料放在 data 欄位, 我們可以用內建模組 json 的 loads()函式將此 JSON 字串轉成 Python 字典以利後續處理 : 

>>> import json  
>>> twse_market_dates=json.loads(response.text)    
>>> twse_market_dates['fields']   
['日期', '名稱', '說明']
>>> twse_market_dates['data']   
[['2024-01-01', '中華民國開國紀念日', '依規定放假1日。'], ['2024-01-02', '國曆新年開始交易日', '國曆新年開始交易。'], ['2024-02-05', '農曆春節前最後交易日', '農曆春節前最後交易。'], ['2024-02-06', '市場無交易,僅辦理結算交割作業', ''], ['2024-02-07', '市場無交易,僅辦理結算交割作業', ''], ['2024-02-08', '農曆除夕前一日', '2月8日(星期四)調整放假,於2月17日(星期六)補行上班,但不交易亦不交割。'], ['2024-02-09', '農曆除夕', '依規定放假1日。'], ['2024-02-10', '農曆春節', '依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>'], ['2024-02-11', '農曆春節', '依規定於2月10日至2月12日放假3日。\r\n\r\n2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。\r\n'], ['2024-02-12', '農曆春節', '依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>'], ['2024-02-13', '農曆春節', '依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>'], ['2024-02-14', '農曆春節', '依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>'], ['2024-02-15', '農曆春節後開始交易日', '農曆春節後開始交易。'], ['2024-02-28', '和平紀念日', '依規定放假1日。'], ['2024-04-04', '兒童節', '依規定放假1日。'], ['2024-04-05', '民族掃墓節', '依規定放假1日。'], ['2024-05-01', '勞動節', '依規定放假1日。'], ['2024-06-10', '端午節', '依規定放假1日。'], ['2024-09-17', '中秋節', '依規定放假1日。'], ['2024-10-10', '國慶日', '依規定放假1日。']]

可見 twse_market_dates['data'] 是一個二維串列, 可以用 for 迴圈走訪每列資料 : 

>>> for item in twse_market_dates['data']:    
 print(item)  
                 
['2024-01-01', '中華民國開國紀念日', '依規定放假1日。']
['2024-01-02', '國曆新年開始交易日', '國曆新年開始交易。']
['2024-02-05', '農曆春節前最後交易日', '農曆春節前最後交易。']
['2024-02-06', '市場無交易,僅辦理結算交割作業', '']
['2024-02-07', '市場無交易,僅辦理結算交割作業', '']
['2024-02-08', '農曆除夕前一日', '2月8日(星期四)調整放假,於2月17日(星期六)補行上班,但不交易亦不交割。']
['2024-02-09', '農曆除夕', '依規定放假1日。']
['2024-02-10', '農曆春節', '依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>']
['2024-02-11', '農曆春節', '依規定於2月10日至2月12日放假3日。\r\n\r\n2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。\r\n']
['2024-02-12', '農曆春節', '依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>']
['2024-02-13', '農曆春節', '依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>']
['2024-02-14', '農曆春節', '依規定於2月10日至2月12日放假3日。<br><br>2月10日及2月11日適逢星期六及星期日,於2月13日(星期二)及2月14日(星期三)補假2日。<br>']
['2024-02-15', '農曆春節後開始交易日', '農曆春節後開始交易。']
['2024-02-28', '和平紀念日', '依規定放假1日。']
['2024-04-04', '兒童節', '依規定放假1日。']
['2024-04-05', '民族掃墓節', '依規定放假1日。']
['2024-05-01', '勞動節', '依規定放假1日。']
['2024-06-10', '端午節', '依規定放假1日。']
['2024-09-17', '中秋節', '依規定放假1日。']
['2024-10-10', '國慶日', '依規定放假1日。']

這樣就比較像表格資料, 好閱讀也好處理了. 注意, 此將表格裡的日期並非全部都是休市日, 例如黃底色之 2024-01-02 (星期二), 2024-02-05 (星期一), 2024-02-15 (星期四) 這三天, 名稱欄位均有 '交易日', 所以接下來要掃描此表格之列, 搜尋名稱欄位是否含有 '交易日', 不含才放入休市日串列 closed.  

方法是把日期欄位中的 'YYYY-MM-DD' 格式字串轉成 datetime 物件, 然後呼叫其 weekday() 方法來取得星期幾整數 (0=星期一, 1=星期二, ... 6=星期日). 

首先從內建套件 datetime 匯入 datetime 模組, 然後呼叫其 strptime() 函式並傳入日期字串與格式參數 '%Y-%m-%d', 它會傳回該日期的 datetime 物件, 呼叫此物件之 weekday() 方法即可取得星期整數, 以表格中的最後一個休市日國慶日為例 : 

>>> from datetime import datetime   
>>> the_date=datetime.strptime('2024-10-10', '%Y-%m-%d')   
>>> the_date   
datetime.datetime(2024, 10, 10, 0, 0)   
>>> type(the_date)  
<class 'datetime.datetime'>   
>>> the_date.weekday()   
3

傳回 3 表示 2024 中華民國國慶日為星期四 (正確). 參考 :


接下來要掃描二維串列 holiday 中每一列, 檢查 '日期' 欄的 weekday() 傳回值, 的 '名稱' ''  

>>> closed=[]      # 儲存休市日期
>>> for item in twse_market_dates['data']:   # 走訪每一列
    if '交易日' not in item[1]:         # '名稱' 欄位沒有 '交易日'
        closed.append(item[0])         # 將日期放入串列
                 
>>> closed   
['2024-01-01', '2024-02-06', '2024-02-07', '2024-02-08', '2024-02-09', '2024-02-10', '2024-02-11', '2024-02-12', '2024-02-13', '2024-02-14', '2024-02-28', '2024-04-04', '2024-04-05', '2024-05-01', '2024-06-10', '2024-09-17', '2024-10-10']

可見已將 2024-01-02, 2024-02-05, 2024-02-15 這三個交易日自表中移除, 剩下的都是休市日. 接下來就可以檢查今天是否有開市, 規則只有兩個 : 
  • 週六週日例假日休市 : 即使週六補班股市也不開市
  • 日期在 closed 串列中 : 休市
先從內建 datetime 套件匯入 date 模組, 呼叫其 today() 函式, 它會傳回一個 date 物件, 用 str() 可將此物件轉成日其字串, 例如 :

>>> from datetime import date    
>>> today=date.today()   
>>> today   
datetime.date(2024, 4, 17)     
>>> type(today)   
<class 'datetime.date'>  
>>> str(today)   
'2024-04-17'  
>>> the_week=today.weekday()   
>>> the_week   
2

可見今天 2024-04-17 是週三. 

也可以用 datetime.datetime.now() 來做, 但傳回的是 datetime 物件, 若要取得單獨的日期可以呼叫其 date() 函式來取得, 例如 : 

>>> from datetime import datetime    
>>> today_time=datetime.now()     
>>> today_time      
datetime.datetime(2024, 4, 17, 10, 56, 36, 293862)      
>>> today=today_time.date()     # 去除時間部分只留日期
>>> today    
datetime.date(2024, 4, 17)    
>>> the_week=today.weekday()   
>>> the_week   
2

參考 :


判斷今天是否休市先判斷是否為例假日, 或者該日期字串在 closed 串列裡, 例如 : 

>>> the_week in [5, 6] or str(today) in closed  
False

以上周六 (2024-04-13) 為例, 可用 datetime 模組的 strptime() 函式來將日期字串轉成 datetime 物件後再呼叫 weekday() 取得星期幾, 例如 : 

>>> the_date=datetime.strptime('2024-04-13', '%Y-%m-%d')   
>>> the_date   
datetime.datetime(2024, 4, 13, 0, 0) 
>>> the_week=the_date.weekday()   
>>> the_week   # 5=星期六
5
>>> is_closed=the_week in [5, 6] or str(today) in closed   
>>> is_closed     # 例假日都休市
True   

下面是檢查 2024-02-12 是否休市 (是) : 

>>> the_date=datetime.strptime('2024-02-12', '%Y-%m-%d')   
>>> the_date   
datetime.datetime(2024, 2, 12, 0, 0)
>>> the_week=the_date.weekday()   
>>> the_week    # 週一
0
>>> is_closed=the_week in [5, 6] or str(today) in closed   
>>> is_closed
True  

雖然是周一, 但該日在 closed 串列中 (春節連假之年初三), 因此休市. 


3. 休市日判斷函式 :    

將上面測試過程寫成函式 twse_is_closed() 如下 : 

import requests
import json
from datetime import datetime

def get_closed_dates():
    url='https://www.twse.com.tw/rwd/zh/holidaySchedule/' +\
        'holidaySchedule?response=json&_=1713199193355'
    response=requests.get(url)
    twse_market_dates=json.loads(response.text)
    closed=[]
    for item in twse_market_dates['data']:
        if '交易日' not in item[1]:  
            closed.append(item[0])
    return closed

def is_twse_closed(the_date: str):
    the_date=datetime.strptime(the_date, '%Y-%m-%d').date()
    print(the_date)
    the_week=the_date.weekday()
    return the_week in [5, 6] or str(the_date) in closed

if __name__ == '__main__':
    closed=get_closed_dates()
    print(closed)
    the_date='2024-02-12'
    is_closed=twse_is_closed(the_date)
    print(f'{the_date} 休市 : {is_closed}')
    
此模組有兩個函式, get_closed_dates() 是爬蟲函式, 它會去 TWSE 抓今年的休市表格之 JSON 資料, 然後剔除裡面的交易日後傳回真正休市日期字串之串列. is_twse_closed() 函式則用來判斷所傳入之日期字串為休市日與否 (傳回 True/False). 執行結果如下 :

>>> %Run check_twse_closed.py   
['2024-01-01', '2024-02-06', '2024-02-07', '2024-02-08', '2024-02-09', '2024-02-10', '2024-02-11', '2024-02-12', '2024-02-13', '2024-02-14', '2024-02-28', '2024-04-04', '2024-04-05', '2024-05-01', '2024-06-10', '2024-09-17', '2024-10-10']
2024-02-12
2024-02-12 休市 : True  

2024年4月14日 星期日

2024 年第 15 周記事

因為上週日峰大師來鄉下家前打掃客廳閃到腰, 所以本周都沒有去河堤快走, 僅週五覺得好像腰不痠了, 就去大地游泳, 這次游 8 回才去做水療, 比上週增加 1 次, 共游了 200 公尺. 疫情前我都可以游到 12 回, 但現在覺得肺活量比以前差 (高度懷疑是否有染疫卻沒驗不知道). 保持游 8 回一段時間後再增加, 希望慢慢把肺活量拉回來. 

今年家中 7 株芒果樹結果率很差, 雖然元宵節後花期並無雨水, 花開得很爛慢, 但是不知為何結果率比去年差這麼多, 而且菜園中的那棵寥寥無幾, 大約結了 8 顆而已, 馬路邊的三棵更慘, 合計不超過 15 顆, 反而是紅杏出牆到隔鄰的較多. 但也沒法跟去年盛況相比. 我週六下午去套袋, 只用掉半袋 (約 50 張) 左右: 




套到屋後那棵樹時, 因為梯子不好放, 所以我將長梯斜靠樹身, 雖然感覺不適很牢靠, 還是仗著多年經驗爬上去, 才套一顆慘劇就發生了, 梯子從樹身滑落, 我抓住旁邊小樹枝, 但不到一秒已墜落水溝, 驚魂甫定發現只有左手肘擦傷, 幸好沒骨折. 正在懊惱樹枝怎麼那麼不中用時, 大馬路上一對騎機車的夫妻對我喊說有沒有怎樣, 我回說沒啦, 就一點擦傷而已. 原來他們要去高樹, 經過時剛好瞥見有人從梯子上掉下來, 以為是老人家, 所以又轉回來查看, 發現原來是個年輕人 .... 聽到這麼說, 我心情頓時又好起來了. 但想起來還是有點害怕, 下墜的時間很短, 一瞬間就發現自己掉在水溝裡, 以後不敢再爬高了, 手伸得到再套吧. 

周一晚上菁菁回家, 跟她一起看完迷你韓劇 "寄生獸" (6 集), 動畫效果做得不錯, 劇情描述這些寄生獸體會到人類組織力量的強大, 所以意圖寄生到人類領袖的大腦 ... 那些窮兵黷武的強盜國家領導人該不會就是寄生獸吧? "淚之女王" 進入第 11 集, 劇情即將反轉, 金智媛演技實在太棒了, 尤其是金秀賢買了一個鑽戒, 把車開到告白聖地的山坡, 想要表白心跡 (取消離婚) 卻慘遭拒絕. 第二天 金智媛以為他已上班, 跑到金秀賢房間把鑽戒套上手指, 不巧金秀賢卻進來要把鑽戒拿去退 ... 這一段我真的快笑死. 今天新聞說她 IG 粉絲突破 1 千萬耶 ! 另外也看完金南珠主演的 "美好世界", 劇情蠻勵志的, 結局還不錯. 明明只有第一集與最末集讓人覺得美好, 原來這是作家新作的書名啊!

本周仍專心在整理爬蟲筆記, 一邊看書練習一邊整理筆記, 這模式我覺得收穫蠻多的. 預約的 PyTorch 書周末已取回來, 所以下周要同時來學 PyTorch. 過去一個月聽了好幾堂 RAG, 現在是躍躍欲試, 但我 PyTorch 還沒學完, 感覺缺了把管用的武器. 爬蟲還是要繼續練習收尾, 都學了好幾年了今年應該一鼓作氣把筆記寫完.

Python 學習筆記 : 用 AES-Encryption 為帳號密碼加密與解密

周末將兩年半前買的 "Python 金融市場大賺錢投資聖經" 這本書帶回鄉下看, 我記得裡面有寫一些網路爬蟲的密技, 在其中第三章看到作者描述如何用 AES 為帳號密碼等機敏資料加密, 覺得還蠻有用的, 今天就來測試看看唄.


1. 下載 AES-Encryption : 

到下面這個 GitHub 下載 AES-Encryption 的模組 : 

按右上角的 Code 鈕選擇 Download ZIP 下載壓縮檔 : 




將此壓縮檔 AES-Encryption-main.zip 解開, 如果產生兩層 AES-Encryption 資料夾, 就複製最下層的 AES-Encryption 資料夾到工作目錄下, 例如我的工作目錄是 D:\python\test\ 則子目錄 AES_Encryption 底下就是解開的兩個 .py 檔 : 




第一個模組 en_decrype.py 利用一個第三方模組 Crypto 來做 AES 加密解密作; 第二個模組 encrype_process.py 則是利用 en_decrype.py 編寫了加密函式 input_new_encrype() 與解密函式 check_encrype(), 不過實際上使用時只需要呼叫 check_encrype() 即可, 若未找到密鑰名稱, 它會自動呼叫 input_new_encrype() 讓你輸入帳號密碼與名稱來建立密鑰. 這三個模組關係如下 :

Crypto 
     |__ encrype_process
                |__ encrype_process

所以要讓 check_encrype() 能用必須先有 Crypto 模組才行. Crypto 模組在 Python 中正式名稱是 pycrypto, 不過此套件已經停止更新很久了, 必須改安裝它的延伸套件 pycryptodome, 它裡面就有 Crypto 模組, 參考 :



2. 安裝 pycryptodome 套件 : 

可以使用 pip install 指令直接安裝 :

pip install pycryptodome 

D:\python\test>pip install pycryptodome   
Collecting pycryptodome
  Using cached pycryptodome-3.20.0-cp35-abi3-win_amd64.whl.metadata (3.4 kB)
Using cached pycryptodome-3.20.0-cp35-abi3-win_amd64.whl (1.8 MB)
Installing collected packages: pycryptodome
Successfully installed pycryptodome-3.20.0

我在 Thonny 套件管理安裝了最新版的 v3.20.0 :




安裝完就可匯入 Crypto 模組了 :

>>> import Crypto  
>>> dir(Crypto)   
['Cipher', 'Hash', 'Protocol', 'Random', 'Util', '__all__', '__builtins__', '__cached__', '__doc__', '__file__', '__loader__', '__name__', '__package__', '__path__', '__spec__', '__version__', 'version_info']

上面 en_decrypt.py 裡面第一行就是從 Crypto.Cipher 匯入 AES 類別來進行加密與解密 : 

from Crypto.Cipher import AES 

但我們只要直接使用上面 AES-Encryption 套件中第二個模 encrype_process.py 的 check_encrype() 函式就可以進行加密解密了. 


3. 使用 AES_encryption 套件 : 

使用 AES_Encryption 套件進行加密解密只要從它的 encrype_process 模組中匯入全部函式即可 :

from AES_Encryption.encrype_process import *

>>> from AES_Encryption.encrype_process import *     # 注意是 encrype 不是 encrypt

不過 encrype_process 模組中的 input_new_encrype() 函式是作者為了 Gmail 登入帳密而特製, 我將其改為較一般化的設定指引, 將其中 print() 與 input() 中的英文提示修改如下 :

def input_new_encrype(fuc_name,key_path,result_path):
    print(f'名稱:{fuc_name}')
    user_id = input('帳號:')
    password = input('密碼:')
    print(result_path)
    result_file_path = (result_path+'encrype.config').replace('\n','')
    key = get_key(key_path,result_path)
    user_encrype = aes_encrypt(user_id, key)
    password_encrype = aes_encrypt(password, key)
    store_encrype = dict()
    store_encrype['name'] = fuc_name
    store_encrype['user_id'] = user_encrype
    store_encrype['password'] = password_encrype
    with open(result_file_path, 'a') as outfile:
        json.dump(store_encrype, outfile)
        outfile.write('\n')
    outfile.close()
    print('加密完成!')
    return user_id,password

參考 :


然後設定儲存金鑰檔與設定檔的資料夾路徑變數, 注意, 最好不要放在工作目錄下, 否則打包時很容易把金鑰都打包進去分享給他人, 其次是路徑結尾必須有斜線 "/", 例如 : 

>>> key_path="D:/key/"             # 金鑰路徑
>>> config_path="D:/config/"    # config 檔路徑

然後呼叫 check_encrype() 函式並傳入金鑰名稱, 金鑰路徑, 與設定檔路徑, 如果金鑰名稱存在會傳回解碼後的帳號與密碼組成之 tuple, 否則會出現輸入框要求設定金鑰名稱與鑰加密之帳密 : 

user_id, password=check_encrype(name, key_path, config_path)     

例如要加密我的網站 tony1966.cc 的登入帳密 : 

>>> user, pwd=check_encrype('tony1966.cc', key_path, config_path)    
名稱:tony1966.cc  
帳號:admin  
密碼:123456  
D:/config/  
加密完成!  

這時檢視 D 碟會發現已建立 key 與 config 這兩個資料夾, 裡面分別有 key.key 與 encrype.config 這兩個檔案 :





用 utf-8 編碼格式開啟 key.key 會發現是不可讀之亂碼 : 

�����\=�NA��6w�|���Y���7�

開啟 encrype.config 則是加密過的明碼 : 

{"name": "tony1966.cc", "user_id": "SgALjY8Tq7EnCo2uM45Z/MyIXtUU65qJDgDEDQv5jKE=\n", "password": "RlqD34SyRjK/Q3tGB9jx3dycB0PB9HiW1wrCtYfdYsU=\n"}

再次執行 check_encrype() 時因為有找到金鑰名稱 tony1966.cc 所以就傳回解碼後的帳密 : 

>>> user, pwd=check_encrype('tony1966.cc', key_path, config_path)    
>>> user   
'admin'  
>>> pwd    
'123456'   

這樣就能增強帳號密碼的安全性了. 如果要重設帳密, 只要將 key 與 config 這兩個資料夾刪除, 重新呼叫 check_encrype() 就可以再次設定了. 當然也可以用來儲存 OpenAI API 的 key, 例如 name 名稱可以取名為 tony1966_openai_key, 前面是註冊的 email 帳號 (我有多個帳號), 帳號就用 email, 而密碼就輸入 API Key, 解密回來時就用 password 當 API Key 用. 


2024-04-15 補充 :

我把修改後的 AES_Encryption 資料夾壓縮後放在 GitHub : 


2024年4月13日 星期六

Python 學習筆記 : 網頁擷取 (六) : Chrome 擴充功能 RestMan

今天在讀陳會安老師寫的 "Python 從網路爬蟲到生活應用超實務" 第 4-3 節看到 Chrome 擴充功能 Servistate HTTP Editor 的介紹, 此外掛程式可以讓我們很方便地編輯 headers 送出 GET 與 POST 請求. 但到 Chrome 商店搜尋卻找不到, 官網雖然還在, 但按下載鈕卻出現檔案不存在訊息 : 


雖然網路上似乎還找得到 CRX 安裝檔, 但我在陳老師另一本書 "Python 從初學到生活應用超實務" 的第 11-3 章找到類似的擴充功能 RestMan, 所以就改用此外掛來測試 HTTP 請求, 這在撰寫爬蟲程式前測試目標網站, 或測試 Web API 時非常有用. 另一款類似軟體是 PostMan, 參考 :


RestMan 是一款 Wen API 測試工具, 提供 GUI 介面可以方便地送出 HTTP 請求與檢視回應, 特別是對於 JSON 回應會予以格式化方便閱讀.

安裝 Chrome 擴充功能要先按 Chrome 右上角那三個垂直的點點按鈕, 然後點選 "擴充功能", 再點選 "前往 Chrome 線上應用程式商店" :




在線上商店右上角的搜尋框輸入 "RestMan" 後按 Enter 就可搜尋到此程式, 按一下此項目進入該擴充功能說明網頁, 按 "加到 Chrome" 鈕即安裝完成 :




這時按網址列後面的擴充功能按鈕會出現已安裝的擴充功能選單 : 




按 RestMan 右邊的圖釘按鈕就可以將其固定在工具列上 :



按此 RestMan 鈕會開啟新頁籤載入 RestMan 網頁, 在上面 URL 框輸入 httpbin 網址 :

http://httpbin.org/get   

httpbin 提供 HTTP 請求與回應服務, 它支援所有 HTTP 動詞, 採用 RESTful 方式制定了一套請求規則, 會將客戶端的請求 Echo 回應回去客戶端, 參考 :


所以如果輸入上面的 GET 請求網址, httpbin 會將請求標頭資訊以 JSON 格式回應 :




底下是其回應內容, 回應本體內回傳的是 JSON 資料 :




也可以在網址中附帶參數, 例如 :


回應本體內容為 : 

{
    "args": {
        "a": "1",   
        "b": "2"
    },
    "headers": {
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate",
        "Accept-Language": "zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7",
        "Host": "httpbin.org",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
        "X-Amzn-Trace-Id": "Root=1-661a9571-7ed4415e244b3df3611c5a0e"
    },
    "origin": "218.166.67.66",
    "url": "http://httpbin.org/get?a=1&b=2"
}

可見 httpbin 把請求標頭中的 a, b 兩參數放在 JSON 回應中的 args 參數中傳回來. 

下面改用 POST 方法傳送 JSON 資料給伺服器 : 




請求本體部分選擇 RAW 並輸入 {"a": "1", "b": "2"}, 右下角選 JSON : 




回應如下 :


{
    "args": {},
    "data": "{\"a\": \"1\", \"b\": \"2\"}",
    "files": {},
    "form": {},
    "headers": {
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate",
        "Accept-Language": "zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7",
        "Content-Length": "20",
        "Content-Type": "text/plain;charset=UTF-8",
        "Host": "httpbin.org",
        "Origin": "chrome-extension://ihgpcfpkpmdcghlnaofdmjkoemnlijdi",
        "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
        "X-Amzn-Trace-Id": "Root=1-661a97e0-2d34bcfa008b02ba1c127243"
    },
    "json": {
        "a": "1",
        "b": "2"
    },
    "origin": "218.166.67.66",
    "url": "http://httpbin.org/post"
}
 
可見參數是放在 data 與 json 屬性中傳回來. 

參考 :


高科大還書 1 本 : ChatGPT完整解析

昨天下班順路去母校還書 (被預約) :




Source : 博客來


此書篇幅不大, 但內容卻很豐富, 可以說是小而美的一本書. 

2024年4月12日 星期五

AI 生圖網站 ideogram.ai

今天參加 AI 生圖的內訓課程, 老師介紹的第一個工具是 ideogram.ai :


此網站可以免費生圖, 使用者可以用 Google 或 FB 帳號註冊 : 






註冊成功出現首頁, 上面是 prompt 輸入框, 底下是其他使用者的作品 (免費用戶的作品都是公開的) : 




我輸入提示 "a cute orange cat riding a motocycle " 生成如下圖片 :





哇, 效果好棒棒! 

按右上角的齒輪鈕會彈出設定視窗 :




可見免費帳戶每日有 25 個 Prompts 的額度可用. 

2024年4月11日 星期四

Python 學習筆記 : 網頁爬蟲實戰 (二) BBC 金融財經新聞

今天要練習的爬蟲對象是 BBC 金融財經新聞, 網址如下 :


本系列之前的筆記參考 :



1. 分析網頁原始碼定位目標 :  

在 Chrome 瀏覽 BBC 金融財經新聞網頁, 可知它是一個分頁型的網站, 我們的目標只是要擷取其第一頁新聞的標題與網址, 按滑鼠右鍵點選 "檢視原始碼" : 




但是發現其原始碼似乎經過壓縮變成一長串難以閱讀 :




這可以透過 "DirtyMarkup" 這個線上服務讓 HTML 原始碼格式整齊化 :


將網頁原始碼複製貼上到該網站右邊的輸入框, 按左方的 "Clean" 鈕即可 : 




參考 :


我們要找的新聞標題與超連結則是具有 class='focusIndicatorDisplayBlock' 樣式類別的 a 元素裡 :

<h2 class="bbc-10m3zrw e47bds20">
  <a class="focusIndicatorDisplayBlock bbc-uk8dsi e1d658bg0" href="https://www.bbc.com/zhongwen/trad/world-68786731">
    越南有史以來最大金融詐騙案開庭:女富豪張美蘭是誰
  </a>
</h2>
<time class="promo-timestamp bbc-1qkagz5 e1mklfmt0" datetime="2024-04-11">
  6 小时前
</time>

當然也可以在建立 BeautifulSoup 物件後呼叫 prettify() 方法來得到格式整齊的原始碼. 上面黃底色部分是用來定位資料的線索 (樣式類別), 藍底色部分是目標資料. 


2. 資料擷取測試 :    

首先匯入 requests 與 bs4 套件中的 BeautifulSoup 類別 :

>>> import requests   
>>> from bs4 import BeautifulSoup  

用 requests.get() 方法對網站提出 GET 請求 : 

>>> url='https://www.bbc.com/zhongwen/trad/business'   
>>> response=requests.get(url)                         # 提出 GET 請求
>>> soup=BeautifulSoup(response.text)    # 建立 BeautifulSoup 物件

我們可以呼叫 soup.prettify() 方法並使用 print() 來輸出格式整齊的網頁原始碼, 目標是放在有 class="focusIndicatorDisplayBlock" 樣式屬性的 a 元素裡的超連結與文字內容 : 

<h2 class="bbc-10m3zrw e47bds20">
  <a class="focusIndicatorDisplayBlock bbc-uk8dsi e1d658bg0" href="https://www.bbc.com/zhongwen/trad/world-68786731">
    越南有史以來最大金融詐騙案開庭:女富豪張美蘭是誰
  </a>
</h2>
<time class="promo-timestamp bbc-1qkagz5 e1mklfmt0" datetime="2024-04-11">
  6 小时前
</time>

這樣就可以用 find_all() 搜尋帶有 focusIndicatorDisplayBlock 樣式類別的全部 a 元素 :

>>> links=soup.find_all('a', {'class': 'focusIndicatorDisplayBlock'})   

然後用迴圈走訪傳回的 a 元素 Tag 物件串列, 並呼叫 get() 方法取得 href 屬性取得超連結, 用 text 屬性取得其文字內容即可 :

>>> for link in links:      
    the_time=link.parent.next_sibling.text       # 取得新聞發布時間
    print(f'{link.text.strip()} ({the_time})')      # strip() 去除左右之空格等控制字元
    print(link.get('href', None))   # 取得超連結
    
越南有史以來最大金融詐騙案開庭:女富豪張美蘭是誰 (7 小时前)
https://www.bbc.com/zhongwen/trad/world-68786731
習近平會見美工商界代表:穩住外資同時鋪墊「三中全會」經濟改革 (2024年3月29日)
https://www.bbc.com/zhongwen/trad/business-68687690
中國年輕女性尋找「存錢搭子」,應對經濟困境 (2024年3月21日)
https://www.bbc.com/zhongwen/trad/business-68565927
來自中國的電商平台Temu如何顛覆美國的網購世界 (2024年3月19日)
https://www.bbc.com/zhongwen/trad/business-68604017
美國稱與中國貿易的下降也許是個積極信號 (2024年3月4日)
https://www.bbc.com/zhongwen/trad/world-68465292
過半省份2024GDP目標低於去年 中國經濟是否會進一步減速 (2024年2月19日)
https://www.bbc.com/zhongwen/trad/chinese-news-68335079
視頻, 香港經濟:農曆新年將至,市民感受如何、對經濟前景有何展望?節目全長, 5,39 (2024年2月7日)
https://www.bbc.com/zhongwen/trad/business-68224953
中國股市暴跌後,美國駐華使館微博成網友「哭牆」 (2024年2月5日)
https://www.bbc.com/zhongwen/trad/business-68201487
中國河南等省爆出GDP增速「改降為升」 「數據注水」的背後原因 (2024年1月31日)
https://www.bbc.com/zhongwen/trad/business-68153559
北京施壓與台灣「斷交潮」持續:民眾有何看法?邦交國會否「清零」? (2024年1月25日)
https://www.bbc.com/zhongwen/trad/world-68090660
中國經濟「三駕馬車」速度不均 2024需消除「信心赤字」 (2024年1月22日)
https://www.bbc.com/zhongwen/trad/business-68046635
香港「搶人才」策略能否吸引拖家帶口的大陸中年人士 (2024年1月12日)
https://www.bbc.com/zhongwen/trad/chinese-news-67944245
波音737 Max再次發生事故 監管單位追查 (2024年1月8日)
https://www.bbc.com/zhongwen/trad/science-67910238
習近平抵達越南展開訪問 修補中越「愛恨交織」的關係 (2023年12月12日)
https://www.bbc.com/zhongwen/trad/world-67689132
穆迪看衰中國經濟 政治局會議稱要加碼財政政策 (2023年12月8日)
https://www.bbc.com/zhongwen/trad/chinese-news-67660504
美國《香港經貿辦認證法》能帶來的實際影響 (2023年12月2日)
https://www.bbc.com/zhongwen/trad/chinese-news-67598929
視頻, 中國電動汽車市場快速增長,清潔能源成關鍵競爭領域節目全長, 3,02 (2023年12月1日)
https://www.bbc.com/zhongwen/trad/science-67586692
OpenAI聯合創始人山姆·阿爾特曼遭罷免後復職:我們都知道些什麼 (2023年11月23日)
https://www.bbc.com/zhongwen/trad/business-67484587
台灣人餐桌上的美中台外交角力:從洪都拉斯的白蝦談起 (2023年11月21日)
https://www.bbc.com/zhongwen/trad/chinese-news-67482759
郭台銘宣佈參選台灣總統兩個月後,其創辦的富士康在中國遭稅務調查 (2023年10月23日)
https://www.bbc.com/zhongwen/trad/chinese-news-67193077
「一帶一路」論壇之際習近平會晤普京 「共同威脅只會加強中俄關係」 (2023年10月18日)
https://www.bbc.com/zhongwen/trad/chinese-news-67142359
視頻, 「一帶一路」論壇:多國領導人齊聚北京 習近平與普京舉行雙邊會談節目全長, 3,28 (2023年10月18日)
https://www.bbc.com/zhongwen/trad/chinese-news-67144911
晶片大戰:台灣廠商助攻華為突破美國封鎖? (2023年10月18日)
https://www.bbc.com/zhongwen/trad/chinese-news-67084358
中國時隔四年再開「一帶一路」峰會的三大看點 (2023年10月17日)
https://www.bbc.com/zhongwen/trad/chinese-news-67132988

注意, 這裡因為我們直接取得 a 元素的 Tag 物件, 因此使用走訪親代 (父節點 parent) 再走訪平輩 (兄弟節點 next_sibling) 的方法取得 time 元素. 


3. 財經新聞標題擷取函式 :    

最後將上面的測試寫成一個函式 get_bbc_business_news() :

import requests
from bs4 import BeautifulSoup

def get_bbc_business_news():
    url='https://www.bbc.com/zhongwen/trad/business'
    response=requests.get(url)
    soup=BeautifulSoup(response.text, 'lxml')
    links=soup.find_all('a', {'class': 'focusIndicatorDisplayBlock'})
    title=[]
    href=[]
    for link in links:
        the_time=link.parent.next_sibling.text
        title.append(f'{link.text.strip()} ({the_time})')          
        href.append(link.get('href', None))
    return title, href

if __name__ == '__main__':
    title, href=get_bbc_business_news()
    for k, v in zip(title, href):
        print(k)
        print(v)

此處 get_bbc_business_news() 會傳回一個由標題串列, 超連結串列組成的 tuple, 將傳回值 title, href 傳給 zip() 來產生一個類似字典的 zip 物件 (以 title 為鍵, 以 href 為值), 然後用 print() 印出 k, v 值, 結果與上面一樣. 

Python 學習筆記 : 網頁擷取 (五) : Chrome 擴充功能 Quick Javascript Switcher

我在 "文科生也可以輕鬆學習網路爬蟲" 這本書中讀到, 在開發爬蟲程式前可以先安裝一個 Chrome 擴充功能 Quick Javascript Switcher 來判斷要爬的目標網頁是否透過 Javascript 來生成內容, 今天就來安裝看看唄! 

首先按 Chrome 右上角那三個垂直的點點按鈕, 然後點選 "擴充功能", 再點選 "前往 Chrome 線上應用程式商店" :




在線上商店網頁右上角的搜尋框輸入 "Quick Javascript Switcher" 後按 Enter 就可搜尋到此程式, 按一下此項目進入該擴充功能說明網頁 : 

 


按右上角的 "加到 Chrome" 鈕即安裝完成 :




回上一頁重新搜尋 "Quick Javascript Switcher" 會顯示 "已安裝" :




這時在瀏覽器右上角會出現 Quick Javascript Switcher 的圖示 : 


只要按此按鈕即可切換 (toggle) Javascript 的啟用或禁用, 若網頁內容為使用 Javascript 生成 (含 Ajax), 則在 Javascript 禁用時內容會消失, 啟用時才會出現, 例如 :


此網頁內容為 Javascript 生成 :

<!doctype html>
<html>
 <head>
  <meta charset="UTF-8">
  <meta http-equiv="cache-control" content="no-cache">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <title></title>
  <style>
    #container {border: 1px solid blue; width:200px; height:100px;}
  </style>
 </head>
 <body>
  <h3>Javascript 生成的網頁測試</h3>
  <div id="container"></div>
  <script>
    var container=document.getElementById('container');
    var list=document.createElement('ul');
    var items=['金智媛', '金秀賢', '孫藝珍'];
    for (var i=0; i < items.length; i++) {
      var listItem=document.createElement('li');
      listItem.textContent=items[i];
      list.appendChild(listItem);
      }
    container.appendChild(list);
  </script>
 </body>
</html>

此網頁在 id=container 的 div 元素內, 利用 Javascript 建立 ul-li 元素組成的清單, 當 Javascript 啟用時會顯示內容 :




當按下 Quick Javascript Switcher 的圖示禁用 Javascript 時內容會消失 :


像這種由 Javascript 生成內容的網站無法直接利用 BeautifulSoup 來剖析, 因為 requests 擷取到的網頁內容是 Javascript 執行前的內容, 所以 BeautifulSoup 所建立的語法樹物件, 必須改用 Selenium 模擬瀏覽器抓到 Javascript 執行後的網頁內容再交給 BeautifulSoup 剖析. 

>>> import requests   
>>> from bs4 import BeautifulSoup   
>>> url='https://tony1966.github.io/test/python/web_crawler/javascript_created_content.htm' 
>>> response=requests.get(url)  
>>> soup=BeautifulSoup(response.text, 'lxml')   
>>> soup.div   
<div id="container"></div>

可見剖析後並未看到 Javascript 生成的清單元素.