問候,
我將上述 Python 腳本進行通用化處理,使其會提示輸入詞表以及(如果有的話)詞典,並輸出一個 SFM 檔案以匯入 FLEx。
#!/usr/bin/python3
"""
wordlist_to_sfm.py
將 Paratext 詞表匯出為 SFM(標準格式標記)格式,
以便匯入 FLEx(FieldWorks Language Explorer)。
用法:
python wordlist_to_sfm.py
該腳本將提示輸入:
- Paratext 詞表 XML 檔案(例如從 Paratext 的詞表工具匯出)
- 可選的 Paratext 詞典 XML 檔案,用於提供英文釋義
- 生成的 .sfm 檔案的輸出路徑
預設情況下,只匯出在詞表中标記為「正確」的詞。
這可以通過編輯下面的 INCLUDE_* 設定來更改。
輸出格式為 MDF(多詞典格式化器),這是 FLEx 用於詞典匯入的標準 SFM 方言。
"""
import codecs
import os
import xml.etree.ElementTree as etree
# --- 設定 ----------------------------------------------------------------
# 控制哪些拼寫類別包含在匯出中。
# 設為 True 以包含該類別中的詞,設為 False 以排除它們。
INCLUDE_CORRECT = True # 翻譯者已批准的詞
INCLUDE_UNKNOWN = False # 尚未審核的詞
INCLUDE_INCORRECT = False # 被標記為拼寫錯誤的詞
# 設為 True 以將詞的語料庫頻率寫為註解欄位(\co_count)
INCLUDE_COUNT = True
# -----------------------------------------------------------------------------
def prompt_path(prompt_text, default):
"""顯示帶有可選預設值的提示;返回用戶輸入或預設值。"""
display = f" [{default}]" if default else ""
value = input(f"{prompt_text}{display}: ").strip()
return value if value else default
def get_user_inputs():
"""詢問用戶運行匯出所需的三個檔案路徑。"""
print("Paratext 詞表至 SFM 匯出器")
print("-----------------------------------\n")
# 詞表是必需的 - 持續詢問直到找到檔案
wordlist_path = prompt_path("詞表檔案 (XML)", "Notsi-WL.xml")
while not os.path.exists(wordlist_path):
print(f" 找不到 '{wordlist_path}'。請檢查路徑並重試。")
wordlist_path = prompt_path("詞表檔案 (XML)", "Notsi-WL.xml")
# 詞典是可選的 - 如果找不到則靜默跳過
lexicon_path = prompt_path("用於釋義的詞典檔案(按 Enter 鍵跳過)", "")
if lexicon_path and not os.path.exists(lexicon_path):
print(f" 找不到 '{lexicon_path}'。繼續執行,但不包含釋義。")
lexicon_path = ""
output_path = prompt_path("輸出檔案名稱", "wordlist_for_flex.sfm")
return wordlist_path, lexicon_path, output_path
def load_lexicon_glosses(lexicon_path):
"""
從 Paratext 詞典 XML 檔案中讀取英文釋義。
返回一個以詞素形式為鍵的字典,其中每個值包含詞素類型(Word, Prefix, Suffix)和一個英文釋義字串列表。
如果未提供詞典路徑,則返回空字典。
"""
glosses = {}
if not lexicon_path:
print("未提供詞典 - 條目將在不包含釋義的情況下匯出。")
return glosses
print(f"正在從以下位置讀取詞典:{lexicon_path}")
with open(lexicon_path, "rb") as f:
data = f.read()
# 去除任何 BOM 變體(標準 UTF-8 BOM,或雙重編碼的 BOM)
for bom in (b"\xef\xbb\xbf", b"\xc3\xaf\xc2\xbb\xc2\xbf"):
if data.startswith(bom):
data = data[len(bom):]
break
xmldoc = etree.fromstring(data)
for item in xmldoc.findall("Entries/item"):
lexeme = next(item.iter("Lexeme"), None)
if lexeme is None:
continue
form = lexeme.get("Form", "")
lex_type = lexeme.get("Type", "Word") # Word, Prefix, 或 Suffix
# 收集此條目的所有英文釋義(可能有多個義項)
entry_glosses = [
gloss.text
for gloss in item.iter("Gloss")
if gloss.get("Language") == "en" and gloss.text
]
if form and entry_glosses:
glosses[form] = {"type": lex_type, "glosses": entry_glosses}
print(f" 找到 {len(glosses)} 個帶有英文釋義的條目。")
return glosses
def build_approved_list(wordlist_path):
"""
讀取 Paratext 詞表 XML 並返回通過 INCLUDE_* 過濾器的詞。
返回一個已批准詞形式的列表,以及一個包含每個詞的語料庫計數、連字符和形態學分解的元數據字典。
"""
wordlist = etree.parse(wordlist_path)
all_items = wordlist.getroot().findall("item")
print(f"正在從以下位置讀取詞表:{wordlist_path}")
print(f" 在列表中發現 {len(all_items)} 個詞。")
approved = []
metadata = {}
for item in all_items:
spelling = item.attrib.get("spelling", "Unknown")
word = item.attrib.get("word", "")
include = (
(spelling == "Correct" and INCLUDE_CORRECT) or
(spelling == "Unknown" and INCLUDE_UNKNOWN) or
(spelling == "Incorrect" and INCLUDE_INCORRECT)
)
if include and word:
approved.append(word)
metadata[word] = {
"count": item.attrib.get("count", "0"),
"hyphenation": item.attrib.get("hyphenation", ""),
"morphology": item.attrib.get("morphology", ""),
"morph_approved": item.attrib.get("morphologyApproved", "False"),
"specificcase": item.attrib.get("specificcase", ""),
}
print(f" {len(approved)} 個詞被標記為正確並準備匯出。")
return approved, metadata
def write_sfm_entry(outfile, word, lex_type, glosses, meta, include_count):
"""
將單個 SFM 詞典條目寫入輸出檔案。
遵循的 MDF 慣例:
\\lx - 詞素(標題詞);詞綴在結合側加連字符
\\sn - 義項編號(每個釋義寫一次)
\\ge - 該義項的英文釋義
\\mr - 形態學字串(當被翻譯者批准時)
\\co_* - 用於 FLEx 沒有標準標記的元數據的註解欄位
"""
# 寫入標題詞,添加連字符以顯示詞綴結合點
outfile.write("\n\\lx ")
if lex_type == "Suffix":
outfile.write("-")
outfile.write(word)
if lex_type == "Prefix":
outfile.write("-")
outfile.write("\n")
# 將詞綴類型記錄為註解,以便在 FLEx 匯入後保留
if lex_type != "Word":
outfile.write(f"\\co_type {lex_type}\n")
# 語料庫頻率 - 對於在詞典工作期間優先處理條目很有用
if include_count and meta:
outfile.write(f"\\co_count {meta['count']}\n")
# 形態學:如果分析已獲批准,則使用標準 \\mr 標記,
# 否則將其存儲為註解,以避免匯入未經驗證的數據
if meta and meta["morphology"]:
marker = "\\mr" if meta["morph_approved"] == "True" else "\\co_morph"
outfile.write(f"{marker} {meta['morphology']}\n")
# 為每個釋義寫入一個義項塊
for sense_num, gloss_text in enumerate(glosses, start=1):
outfile.write(f"\\sn {sense_num}\n")
outfile.write(f"\\ge {gloss_text}\n")
def main():
"""
主要匯出例程。
兩遍處理方法:
第一遍 - 寫入具有詞典釋義的條目(先寫入更豐富的數據)
第二遍 - 寫入剩餘的已批准詞,這些詞沒有詞典條目
這確保了帶釋義的條目不會重複。
"""
wordlist_path, lexicon_path, output_path = get_user_inputs()
print()
approved_words, metadata = build_approved_list(wordlist_path)
lexicon_glosses = load_lexicon_glosses(lexicon_path)
# 以 UTF-8 打開輸出檔案並寫入 MDF 標頭
outfile = codecs.open(output_path, mode="w", encoding="utf-8")
outfile.write("\\_sh v3.0 400 MDF\n\\_DateStampHasFourDigitYear\n\n")
remaining = list(approved_words) # 第一遍之後仍需寫入的詞
with_glosses = 0
# 第一遍:同時出現在詞典和已批准詞表中的條目
for form, lex_data in lexicon_glosses.items():
if form not in approved_words:
print(f" 跳過 '{form}'(在詞典中但未在詞表中标記為正確)")
continue
if form in remaining:
remaining.remove(form)
write_sfm_entry(outfile, word=form, lex_type=lex_data["type"],
glosses=lex_data["glosses"], meta=metadata.get(form),
include_count=INCLUDE_COUNT)
with_glosses += 1
# 第二遍:沒有詞典條目的已批准詞 - 在不包含釋義的情況下匯出
for word in remaining:
write_sfm_entry(outfile, word=word, lex_type="Word", glosses=[],
meta=metadata.get(word), include_count=INCLUDE_COUNT)
total = with_glosses + len(remaining)
outfile.close()
print(f"\nExport 完成。")
print(f" 寫入的總條目數 : {total}")
print(f" 帶有釋義的 : {with_glosses}")
print(f" 沒有釋義的 : {len(remaining)}")
print(f" 輸出檔案 : {output_path}")
if __name__ == "__main__":
main()