iT邦幫忙

2022 iThome 鐵人賽

DAY 5
0
自我挑戰組

基於自然語言處理的新聞意見提取應用開發筆記系列 第 5

[Day-05] 設計網路新聞的資料結構(使用 Python dataclass)

  • 分享至 

  • xImage
  •  

Day-05 內容

  • Python dataclass
    • 什麼是 dataclass?
    • dataclass 的用法與優點
      • 免去 __init__() 方法的程式碼
      • @dataclass() 的參數
      • field() 的使用
      • asdict() 將 data class 轉型成 dict
  • 設計網路新聞的資料結構
    • 網路新聞資料從哪來?
    • 依照提取的資訊的設計 Data Class

前幾日都在講一些幫助專案開發的工具、技巧與規範,今天開始便會與本次的挑戰主題 —「基於自然語言處理的新聞意見提取應用」關聯更加密切。

這次使用的繁體中文新聞資料主要來自台灣的一些網路新聞網站。從一篇新聞網頁上擷取下來的資料包含了不同的類型,比如說:新聞的發入日期(表現方式多變)、新聞中的照片內容(圖片連結與描述對)、新聞的數段內文(數個 html 中的 <p><\p> ),要在 Python 程式中的函數中傳遞這些資料,將資料包成 class 會讓之後寫程式輕鬆許多。

一般多數 Python 教程中都包含了 class 的基本用法,今天要介紹的 Data Class 用法比起一般的 class ,我認為能更容易的用來表示一篇網路新聞的內容,之後要儲存到資料庫也會更加便利。


Python dataclass

什麼是 dataclass?

dataclass 是屬於 Python 3.9 標準函式庫(Standard Library)中的模組,最早描述在 PEP 557 – Data Classes 當中,並且有著如下描述:

可以把 Data Classes 想成是 「具有預設值的可改變 nametuple(mutable namedtuples with defaults,nametuple 是另一種 python 用法)」 ,因為 Data Classes(dataclass 用法)使用普通的 class 定義語法,所以可以配合 inheritance 、 metaclasses 、 docstrings、自定義 methods、class factories以及其他 Python class 用法一同使用。

Data Class 會搭配 class decorator(裝飾器,@dataclass)使用,會檢查 class 定義中具有類型註釋(type annotation)的變數,如同 PEP 526 – Syntax for Variable Annotations 中所描述。


dataclass 的用法與優點

由於在還不知道 dataclass 得用法下,很難解釋其優點,所以在介紹 dataclass 的用法時,邊聊我認為 dataclass 的優點。

本段程式碼範例引用自 dataclasses — Data Classes,並參考文檔內容

下面是一個搭配 dataclass 定義庫存 class 的範例程式:

from dataclasses import dataclass

@dataclass
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    unit_price: float
    quantity_on_hand: int = 0

    def total_cost(self) -> float:
        return self.unit_price * self.quantity_on_hand

上面程式碼中的名稱與類型對(如:name: str)稱之為 field

免去 __init__() 方法的程式碼

仔細一看,有沒有發現上面的 class 缺少了一般都會有的 __init__,原因是使用 @dataclass 裝飾器,會自動根據 fields的資訊,在這裡是 name: strunit_price: floatquantity_on_hand: int = 0(這裡表示quantity_on_hand 預設為 0) 這幾個自定義的變數名稱與類型(type)對,建立相應的 __init__() 方法,並將其加入 class等同在這個範例中少寫下面部分:

def __init__(self, name: str, unit_price: float, quantity_on_hand: int = 0):
    self.name = name
    self.unit_price = unit_price
    self.quantity_on_hand = quantity_on_hand

優點(ㄧ)
讓修改 instance attribute (實例屬性)方便超多!
假設要為 InventoryItem 這個 class 新增一個 instance attribute,類型為 int 且名稱為 weight。
一般情況下需要更改 def __init__(self, weight:int, 其餘照舊),並新增 self.weight = weight。用了 @dataclass 後只需要在對的位置新增短短的一行 weight: int,減少了所需的步驟。


@dataclass() 的參數

下面三種 @dataclass 的用法是等價的:

@dataclass
class C:
    ...

@dataclass()
class C:
    ...

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
   ...

其中第三種括號中的內容顯示了 @dataclass 參數的預設值,這些參數代表的意義如下:

  • init:

    • 預設為 True
    • init=True 時,將產生一個 __init__() 方法。
    • 如果 class 已自行定義 __init__() ,則忽略此參數。
  • repr:

    • 預設為 True
    • init=True 時,將產生一個 __repr__() 方法,生成的 repr 字串將具有 class 名稱以及每個 instance attribute 名稱和 repr ,並按照在 instance attribute 在class 中的定義的順序。
    • 此例子的 repr 字串為 InventoryItem(name='widget', unit_price=3.0, quantity_on_hand=10)

優點(二)
用 print() 印出 instance 時,repr 字串中會包含了 instance attribute 的值,這對於 debug 與很有幫助,如下所示:

>>> class A:
...     def __init__(name: str="YOU"):
...             self.name = name
>>> a = A()
>>> print(a)
<__main__.A object at 0x104b9bd00>
...
>>> from dataclasses import dataclass
>>> @dataclass
... class B:
...     name: str = "ME"
>>> b = B()
>>> print(b)
B(name='ME')
  • eq:

    • 預設為 True 時,將產生一個 __eq__() 方法,只能用來比較完全相同類別(type)的 instance。此方法將 class 作為其字串的元组(tuple),按順序比較。
  • order:

    • 預設為 Flase
    • 設為 True 時,則將產生 __lt__()__le__()__gt__()__ge__() 方法。 此方法將 class 作為其字串的元组(tuple)比較,但只能用來比較完全相同類別(type)的 instance。
  • unsafe_hash:

    • 預設為 Flase 時,則根據 eqfrozen 的設定,產生 hash() 方法,由內建的 hash() 實作。
    • 此參數應該多數情況下,不太會需要改動。
    • 更詳細的說明請參考: dataclasses — Data Classes
  • frozen:

    • 預設為 Flase
    • 設為 True 時,對 field 的賦值將產生 exception,模擬唯讀 frozen instances。另外如過在 class 中定義 __setattr__() or __delattr__() 將引發 TypeError。

優點(三)
將 frozen 設定為 True 可以避免 instance attribute 的值不小心遭到改動,有助於維持資料的正確性。
試圖改動 instance attribute 將造成 dataclasses.FrozenInstanceError,如下面的說明範例:

>>> from dataclasses import dataclass
>>> @dataclass(frozen=True)
... class D:
...     name:str = "XXX"
... 
>>> d = D()
>>> print(d)
D(name='XXX')
>>> d.name = "OOO"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 4, in __setattr__
dataclasses.FrozenInstanceError: cannot assign to field 'name'

field() 的使用

像是原先範例中其中一個 field quantity_on_hand: int = 0 這種使用方法固然沒有問題,但有時候會需要用到 field() 這個函數。

下面的程式碼片段中,有使用到兩個重要的設定

@dataclass
class C:
    mylist: list[int] = dataclasses.field(default_factory=list, repr=False)
    value: int=7
  1. 如果不希望在 print() instance 時顯示 mylist 的資訊,則設定 repr=False
  2. 不能寫 mylist: list[int] = [],所以用 field(default_factory=list

其他細節請查看 dataclasses — Data Classes


asdict() 將 data class 轉型成 dict

優點(四)
使用 asdict() 讓我們可以很輕鬆的將 data class 轉型成 dict。
像是網頁 API 常使用到的 json 格式,很多時候作法會需要用到將 Python dict 轉換成 json。如果使用一般的 class 就會需要自己寫 function 將 class 裡的資訊轉成 dict,相比之下 asdict() 就方便的多。

>>> from dataclasses import dataclass
>>> @dataclass
>>> class InventoryItem:
...     name: str
...     unit_price: float
...     quantity_on_hand: int = 0
...
>>> item = InventoryItem(name="ITEM", unit_price=1.25)
>>> print(item)
InventoryItem(name='ITEM', unit_price=1.25, quantity_on_hand=0)
>>> import dataclasses
>>> dataclasses.asdict(item)
{'name': 'ITEM', 'unit_price': 1.25, 'quantity_on_hand': 0}

綜合上述四個使用 Data Class 的優點,我認為配合 Data Class 來定義用來儲存新聞資料的 Class,在撰寫程式碼與後續更動及維護上會更加流暢省力。下一段將分享我如何配合 Data Class 定義用來儲存新聞資料的 Class。


設計網路新聞的資料結構

網路新聞資料從哪來?

「基於自然語言處理的新聞意見提取應用」中的新聞指的是繁體中文的網路政治類別新聞,預計會搜集下列媒體的政治新聞:

為了從新聞網站上獲得新聞資料,打算使用 Python 撰寫爬蟲程式。常見的 Python 爬蟲工具有:seleniumrequestsbeautifulsoup等。配合上述工具所寫出的程式碼,會先使用 seleniumrequests等工具,獲取新聞文章頁面的 html,接著使用 beautifulsoup進行解析,並提取出目標資訊。


依照提取的資訊的設計 Data Class

能從網頁 html 中提取的資訊會因新聞媒體而不同。透過觀察上一段列出的新聞媒體的文章網頁,我整理想提取的資訊,如下結構所示:

  • 新聞文章網頁 html
    • 文章發佈時間
    • 新聞媒體名稱
    • 新聞文章類別(目標政治類別)
    • 記者資訊
    • 新聞文章標題
    • 新聞文章內文(有分數段成數個 html 中的 <p><\p>
    • 新聞文章連結
    • 新聞文章中的圖片連結與其描述(可能有多張)
    • hash tag 與其連結(有些媒體沒有此項目)
@dataclass(frozen=True)
class NewsData:
    """The class to hold the news data.
    """
    time: datetime # 文章發佈時間
    source: str # 新聞媒體名稱
    category: str # 新聞文章類別
    author: str # 記者資訊
    title: str # 新聞文章標題
    content: list[str] # 新聞文章內文(有分數段)
    url: str # 新聞文章連結
    images: list[ImageData] # 新聞文章中的圖片連結與其描述(可能有多張)
    hash_tags: list[HashTagData] # hash tag 與其連結(有些媒體沒有此項目)

上面結構中的「新聞文章中的圖片與其描述」、「hash tag 與其連結」有屬於各自的結構,如下所示:

  • 新聞文章中的圖片連結與其描述
    • 圖片連結
    • 圖片描述
@dataclass(frozen=True)
class ImageData:
    """The class to hold the image data.
    """
    url: str # 圖片連結
    describe: str # 圖片描述
  • hash tag 與其連結
    • tag 名稱
    • tag 連結
@dataclass(frozen=True)
class HashTagData:
    """The class to hold the hash tag data.
    """
    tag: str # tag 名稱
    href: int # tag 連結

使用 Data Class 所定義的新聞資料 class 是不是看起來相當簡潔?設定 frozen=True 還可以避免資料不小心被改動到。對於當中的資料類型(Type)也表示的非常清楚。這樣的作法會有助於簡化之後將上面的 NewsData class 轉換成可以使用 psycopg 的格式,再傳到 PostgreSQL 私服器的複雜度,這部分之後應該會介紹到,那麼今天的內容就先到這裡嘍~


寫的有些匆忙,如果文章有錯誤,歡迎指正~
/images/emoticon/emoticon41.gif


上一篇
[Day-04] 專案的 Python 風格(採用 Google Style Guides 搭配 yapf)
下一篇
[Day-06] 以 PostgreSQL 建立新聞資料庫(採用 AWS Amazon Aurora Serverless v2)
系列文
基於自然語言處理的新聞意見提取應用開發筆記17
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言