6.3 如何新增語言解析器
6.3 如何新增語言解析器
本章示範如何透過繼承抽象基類來新增語言解析器。這是一個完整的實作範例,展示了前面學到的 ABC、工廠模式和型別提示等概念。
前置知識
建議先閱讀:
場景說明
假設 Hook 系統需要支援新的配置格式(例如 TOML),我們需要:
- 建立繼承自
BaseParser的TomlParser類別 - 實作所有抽象方法
- 註冊到工廠
- 撰寫測試
步驟 1:了解基類介面
首先檢視抽象基類的定義:
1# parsers/base.py
2from abc import ABC, abstractmethod
3from typing import Optional
4
5class BaseParser(ABC):
6 """解析器抽象基類"""
7
8 def __init__(self, encoding: str = "utf-8"):
9 self.encoding = encoding
10
11 @abstractmethod
12 def parse(self, content: str) -> dict:
13 """
14 解析內容
15
16 Args:
17 content: 要解析的字串
18
19 Returns:
20 dict: 解析後的字典
21
22 Raises:
23 ParseError: 解析失敗時拋出
24 """
25 pass
26
27 @abstractmethod
28 def validate(self, content: str) -> bool:
29 """
30 驗證內容格式是否正確
31
32 Args:
33 content: 要驗證的字串
34
35 Returns:
36 bool: 格式正確返回 True
37 """
38 pass
39
40 @property
41 @abstractmethod
42 def file_extensions(self) -> list[str]:
43 """支援的檔案副檔名列表"""
44 pass
45
46 # 共用方法(不是抽象的)
47 def parse_file(self, path: str) -> dict:
48 """解析檔案"""
49 from pathlib import Path
50 content = Path(path).read_text(encoding=self.encoding)
51 return self.parse(content)步驟 2:實作新解析器
1# parsers/toml_parser.py
2"""
3TOML 解析器
4
5支援 TOML 格式的配置檔案解析。
6"""
7
8from typing import Optional
9from .base import BaseParser
10
11# 嘗試導入 toml 模組
12try:
13 import tomllib # Python 3.11+ 內建
14except ImportError:
15 try:
16 import toml as tomllib # 第三方套件
17 except ImportError:
18 tomllib = None
19
20class TomlParser(BaseParser):
21 """
22 TOML 格式解析器
23
24 支援 .toml 檔案的解析。
25
26 Example:
27 parser = TomlParser()
28 config = parser.parse_file("config.toml")
29 """
30
31 def __init__(self, encoding: str = "utf-8"):
32 """
33 初始化 TOML 解析器
34
35 Args:
36 encoding: 檔案編碼
37
38 Raises:
39 ImportError: 如果 toml 模組不可用
40 """
41 if tomllib is None:
42 raise ImportError(
43 "TOML parser requires 'tomllib' (Python 3.11+) "
44 "or 'toml' package. Install with: pip install toml"
45 )
46 super().__init__(encoding)
47
48 def parse(self, content: str) -> dict:
49 """
50 解析 TOML 內容
51
52 Args:
53 content: TOML 格式的字串
54
55 Returns:
56 dict: 解析後的字典
57
58 Raises:
59 ValueError: 如果 TOML 格式錯誤
60 """
61 try:
62 # Python 3.11+ 的 tomllib 需要 bytes
63 if hasattr(tomllib, 'loads'):
64 return tomllib.loads(content)
65 else:
66 # tomllib (3.11+) 只接受 bytes
67 return tomllib.load(content.encode(self.encoding))
68 except Exception as e:
69 raise ValueError(f"Failed to parse TOML: {e}") from e
70
71 def validate(self, content: str) -> bool:
72 """
73 驗證 TOML 格式
74
75 Args:
76 content: 要驗證的字串
77
78 Returns:
79 bool: 格式正確返回 True
80 """
81 try:
82 self.parse(content)
83 return True
84 except ValueError:
85 return False
86
87 @property
88 def file_extensions(self) -> list[str]:
89 """支援的副檔名"""
90 return [".toml"]步驟 3:註冊到工廠
1# parsers/__init__.py
2"""
3解析器模組
4
5提供多種格式的檔案解析功能。
6"""
7
8from .base import BaseParser
9from .factory import ParserFactory
10from .json_parser import JsonParser
11from .yaml_parser import YamlParser
12
13# 註冊內建解析器
14ParserFactory.register("json", [".json"])(JsonParser)
15ParserFactory.register("yaml", [".yaml", ".yml"])(YamlParser)
16
17# 嘗試註冊 TOML 解析器(可選依賴)
18try:
19 from .toml_parser import TomlParser
20 ParserFactory.register("toml", [".toml"])(TomlParser)
21except ImportError:
22 pass # TOML 支援不可用
23
24__all__ = [
25 "BaseParser",
26 "ParserFactory",
27 "JsonParser",
28 "YamlParser",
29]或者使用裝飾器直接在類別定義時註冊:
1# parsers/toml_parser.py
2from .factory import ParserFactory
3from .base import BaseParser
4
5@ParserFactory.register("toml", [".toml"])
6class TomlParser(BaseParser):
7 """TOML 解析器"""
8 # ... 實作 ...步驟 4:撰寫測試
1# tests/test_toml_parser.py
2"""
3TOML 解析器測試
4"""
5
6import unittest
7from unittest.mock import patch
8
9# 跳過測試如果 toml 不可用
10try:
11 from parsers.toml_parser import TomlParser
12 HAS_TOML = True
13except ImportError:
14 HAS_TOML = False
15
16@unittest.skipUnless(HAS_TOML, "TOML support not available")
17class TestTomlParser(unittest.TestCase):
18 """測試 TomlParser 類別"""
19
20 def setUp(self):
21 """測試前準備"""
22 self.parser = TomlParser()
23
24 def test_parse_simple_toml(self):
25 """測試解析簡單的 TOML"""
26 content = """
27 [server]
28 host = "localhost"
29 port = 8080
30 """
31 result = self.parser.parse(content)
32
33 self.assertEqual(result["server"]["host"], "localhost")
34 self.assertEqual(result["server"]["port"], 8080)
35
36 def test_parse_nested_toml(self):
37 """測試解析巢狀的 TOML"""
38 content = """
39 [database]
40 host = "localhost"
41
42 [database.connection]
43 timeout = 30
44 retries = 3
45 """
46 result = self.parser.parse(content)
47
48 self.assertEqual(result["database"]["host"], "localhost")
49 self.assertEqual(result["database"]["connection"]["timeout"], 30)
50
51 def test_validate_valid_toml(self):
52 """測試驗證有效的 TOML"""
53 content = '[section]\nkey = "value"'
54 self.assertTrue(self.parser.validate(content))
55
56 def test_validate_invalid_toml(self):
57 """測試驗證無效的 TOML"""
58 content = "this is not valid TOML ["
59 self.assertFalse(self.parser.validate(content))
60
61 def test_parse_invalid_raises_error(self):
62 """測試解析無效 TOML 時拋出錯誤"""
63 content = "invalid [ toml"
64 with self.assertRaises(ValueError):
65 self.parser.parse(content)
66
67 def test_file_extensions(self):
68 """測試檔案副檔名"""
69 self.assertIn(".toml", self.parser.file_extensions)
70
71@unittest.skipUnless(HAS_TOML, "TOML support not available")
72class TestTomlParserFactory(unittest.TestCase):
73 """測試 TOML 解析器與工廠的整合"""
74
75 def test_factory_creates_toml_parser(self):
76 """測試工廠可以建立 TOML 解析器"""
77 from parsers import ParserFactory
78
79 parser = ParserFactory.create("toml")
80 self.assertIsInstance(parser, TomlParser)
81
82 def test_factory_creates_from_file(self):
83 """測試工廠根據副檔名建立解析器"""
84 from parsers import ParserFactory
85
86 parser = ParserFactory.create_from_file("config.toml")
87 self.assertIsInstance(parser, TomlParser)
88
89if __name__ == "__main__":
90 unittest.main()步驟 5:更新文件
在 README 或相關文件中記錄新功能:
1## 支援的配置格式
2
3- JSON (.json) - 內建支援
4- YAML (.yaml, .yml) - 需要 PyYAML
5- TOML (.toml) - 需要 Python 3.11+ 或 toml 套件
6
7### 安裝 TOML 支援
8
9```bash
10# Python 3.11+ 內建支援
11# 或安裝第三方套件
12pip install toml
13```使用範例
1from parsers import ParserFactory
2
3# 自動選擇解析器
4parser = ParserFactory.create_from_file("config.toml")
5config = parser.parse_file("config.toml") 1
2## 完整檢查清單
3
4新增解析器時的檢查項目:
5
6- [ ] 繼承 `BaseParser`
7- [ ] 實作所有 `@abstractmethod`
8 - [ ] `parse(content: str) -> dict`
9 - [ ] `validate(content: str) -> bool`
10 - [ ] `file_extensions` 屬性
11- [ ] 處理可選依賴
12- [ ] 註冊到 `ParserFactory`
13- [ ] 撰寫單元測試
14 - [ ] 正常解析
15 - [ ] 錯誤處理
16 - [ ] 驗證功能
17 - [ ] 工廠整合
18- [ ] 更新文件
19- [ ] 更新 `__all__` 匯出
20
21## 常見問題
22
23### Q: 如果依賴套件不可用怎麼辦?
24
25在 `__init__` 中檢查並提供清楚的錯誤訊息:
26
27```python
28def __init__(self):
29 if required_module is None:
30 raise ImportError(
31 "TomlParser requires 'toml' package. "
32 "Install with: pip install toml"
33 )Q: 如何處理不同版本的 API?
1try:
2 import tomllib # Python 3.11+
3 def parse_toml(content: str) -> dict:
4 return tomllib.loads(content)
5except ImportError:
6 import toml
7 def parse_toml(content: str) -> dict:
8 return toml.loads(content)Q: 解析錯誤應該拋出什麼異常?
建議轉換為標準異常並保留原始資訊:
1try:
2 return toml.loads(content)
3except toml.TomlDecodeError as e:
4 raise ValueError(f"Invalid TOML: {e}") from e思考題
- 為什麼要將原始異常作為
from e傳遞? - 如何設計讓解析器支援串流處理(大檔案)?
- 如果要支援解析器鏈(先解密再解析),應該如何設計?
延伸閱讀(進階系列)
上一章:如何擴展共用模組 回到首頁:Python 維護工程師實戰指南