Здравствуйте,
Я доработал вышеприведённый скрипт на Python, чтобы сделать его универсальным: теперь он запрашивает список слов и, если он есть, лексикон, а затем выводит файл SMF для импорта в FLEx.
#!/usr/bin/python3
"""
wordlist_to_sfm.py
Экспортирует список слов Paratext в формат SFM (Standard Format Markers)
для импорта в FLEx (FieldWorks Language Explorer).
Использование:
python wordlist_to_sfm.py
Скрипт запросит:
- XML-файл списка слов Paratext (например, экспортированный из инструмента списков слов Paratext)
- Необязательно: XML-файл лексикона Paratext для предоставления английских глосс
- Путь к выходному файлу для результирующего файла .sfm
По умолчанию экспортируются только слова, отмеченные как "Correct" (Правильные) в списке слов.
Это можно изменить, отредактировав настройки INCLUDE_* ниже.
Формат вывода — MDF (Multi-Dictionary Formatter), стандартный диалект SFM, используемый FLEx для импорта лексиконов.
"""
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):
"""
Прочитать английские глоссы из XML-файла лексикона Paratext.
Возвращает словарь, ключами которого являются формы лексем, а значениями — тип лексемы (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 (стандартный BOM UTF-8 или дважды закодированный 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):
"""
Прочитать XML списка слов Paratext и вернуть слова, проходящие фильтры 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():
"""
Основная процедура экспорта.
Двухпроходный подход:
Проход 1 — записать записи, у которых есть глоссы из лексикона (сначала более полные данные)
Проход 2 — записать оставшиеся одобренные слова, для которых не было записи в лексиконе
Это гарантирует, что записи с глоссами не будут дублироваться.
"""
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
# Проход 1: записи, присутствующие и в лексиконе, и в одобренном списке слов
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
# Проход 2: одобренные слова без записи в лексиконе — экспортируются без глосс
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()