Il y a 389 articles et 19 utilisateurs inscrits (ce bandeau est défini dans MediaWiki:Sitenotice)
Conversion thesaurus morphalous vs pleade-singulier-pluriel-masc-fem
Introduction
Un projet de lexique morpho-syntaxique, lemme, flexion et forme fléchie est mené par le laboratoire producteur ATILF - Analyse et traitement informatique de la langue française - UMR 7118 (Nancy, FR) https://www.ortolang.fr/market/lexicons/morphalou/v3.1. Morphalou3 est diffusé sous licence LGPL-LR
Voici un script qui convertie le fichier Morphalou3.1_LMF.xml en thesaurus singulier pluriel et masculin féminin pour ThesX.
On ne converse que les lemmes et les formes fléchies. Les conjugaisons ne sont pas conservées.
Download la dernière version de morphalous
Récupérer la dernière version de morphalous ici : https://www.ortolang.fr/market/lexicons/morphalou/v3.1
Le fichier a utiliser est dans l'archive ZIP : Morphalou3.1_formatLMF_toutEnUn.zip et il s'appelle : Morphalou3.1_LMF.xml morphalou/5/morphalou_lmf/Morphalou3.1_formatLMF_toutEnUn/Morphalou3.1_LMF.xml
Le script morphalou31_lmf_to_pleade_sp_gender_option1.py
Version singulier-pluriel et masculin-féminin.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
morphalou31_lmf_to_pleade_sp_gender_option1.py
Morphalou 3.1 (LMF) -> thésaurus Pleade :
- Un concept par "groupe" (fusion masc/fém quand possible) + variantes sing/plur
- Option 1: masculin/féminin inclus dans le même concept
Heuristique de fusion masc/fém:
- On construit une clé à partir des <originatingEntry> du lemmatizedForm
(target + texte). Si plusieurs lexicalEntry partagent une clé, on fusionne
leurs formes (lemmes + pluriels, etc.).
Streaming XML (iterparse) : OK pour ~483 Mo.
Usage:
python3 morphalou31_lmf_to_pleade_sp_gender_option1.py \
--input Morphalou3.1_LMF.xml \
--output pleade-sing-plur-genre-morphalou31.xml \
--thesaurus-id pleade-sing-plur-genre-morphalou31
"""
from __future__ import annotations
import argparse
import datetime as dt
import re
import unicodedata
import xml.etree.ElementTree as ET
from collections import defaultdict
from dataclasses import dataclass, field
from typing import Dict, Set, Optional, Iterable, Tuple, List
def norm_space(s: str) -> str:
return re.sub(r"\s+", " ", s.strip())
def strip_ns(tag: str) -> str:
return tag.split("}", 1)[1] if "}" in tag else tag
def slugify_fr(s: str) -> str:
s = norm_space(s).lower()
s = s.replace("’", "'")
s = re.sub(r"[']/+", "-", s)
s = re.sub(r"[\s_]+", "-", s)
s = unicodedata.normalize("NFKD", s)
s = "".join(ch for ch in s if not unicodedata.combining(ch))
s = re.sub(r"[^a-z0-9-]+", "", s)
s = re.sub(r"-{2,}", "-", s).strip("-")
return s or "concept"
def text_of_first(parent: ET.Element, child_name: str) -> Optional[str]:
for c in list(parent):
if strip_ns(c.tag) == child_name and c.text and c.text.strip():
return norm_space(c.text)
return None
def normalize_number(v: Optional[str]) -> Optional[str]:
if not v:
return None
v = v.strip().lower()
if v in ("singular", "singulier", "sg", "s"):
return "singular"
if v in ("plural", "pluriel", "pl", "p"):
return "plural"
if v in ("invariable", "inv"):
return "invariable"
if "sing" in v:
return "singular"
if "plur" in v:
return "plural"
if "invar" in v:
return "invariable"
return None
def normalize_gender(v: Optional[str]) -> Optional[str]:
if not v:
return None
v = v.strip().lower()
if v in ("masculine", "m", "masc"):
return "masculine"
if v in ("feminine", "f", "fem"):
return "feminine"
return v
def iter_lexical_entries(path: str) -> Iterable[ET.Element]:
for event, elem in ET.iterparse(path, events=("end",)):
if strip_ns(elem.tag) == "lexicalEntry":
yield elem
elem.clear()
@dataclass
class EntryInfo:
lemma: str
category: Optional[str]
gender: Optional[str]
# All observed forms (orthography) from inflectedForm, with number
forms: Set[Tuple[str, Optional[str]]] = field(default_factory=set)
# Origin keys from lemmatizedForm/originatingEntry
origin_keys: Set[str] = field(default_factory=set)
def extract_entry_info(entry: ET.Element) -> Optional[EntryInfo]:
lemma = None
category = None
gender = None
forms: Set[Tuple[str, Optional[str]]] = set()
origin_keys: Set[str] = set()
for formset in entry:
if strip_ns(formset.tag) != "formSet":
continue
for form in list(formset):
t = strip_ns(form.tag)
if t == "lemmatizedForm":
lemma = text_of_first(form, "orthography") or lemma
category = text_of_first(form, "grammaticalCategory") or category
gender = normalize_gender(text_of_first(form, "grammaticalGender")) or gender
# originatingEntry keys (target + texte)
for oe in list(form):
if strip_ns(oe.tag) == "originatingEntry":
target = oe.attrib.get("target", "").strip()
txt = norm_space(oe.text) if oe.text and oe.text.strip() else ""
# IMPORTANT : on ne garde la clé que si le texte n'est PAS vide
if target and txt:
origin_keys.add(f"{target}:{txt}")
elif t == "inflectedForm":
wf = text_of_first(form, "orthography")
num = normalize_number(text_of_first(form, "grammaticalNumber"))
if wf:
forms.add((wf, num))
if not lemma:
return None
return EntryInfo(
lemma=lemma,
category=category,
gender=gender,
forms=forms,
origin_keys=origin_keys,
)
def make_unique_ids(keys: Iterable[str]) -> Dict[str, str]:
used: Dict[str, int] = {}
out: Dict[str, str] = {}
for k in keys:
base = slugify_fr(k)
if base not in used:
used[base] = 1
out[k] = base
else:
used[base] += 1
out[k] = f"{base}-{used[base]}"
return out
def indent(elem: ET.Element, level: int = 0) -> None:
i = "\n" + level * " "
if len(elem):
if not elem.text or not elem.text.strip():
elem.text = i + " "
for e in elem:
indent(e, level + 1)
if not elem.tail or not elem.tail.strip():
elem.tail = i
else:
if level and (not elem.tail or not elem.tail.strip()):
elem.tail = i
def build_groups(
morphalou_xml: str,
allowed_categories: Set[str],
max_origin_key_freq: int,
) -> Dict[str, List[EntryInfo]]:
"""
2-pass grouping:
- pass1: count origin_key frequency (only keys with non-empty target+text)
- pass2: group by an origin_key only if its global freq <= max_origin_key_freq
else fallback to lemma (no fusion)
"""
# PASS 1: frequency count
key_freq: Dict[str, int] = defaultdict(int)
for entry in iter_lexical_entries(morphalou_xml):
info = extract_entry_info(entry)
if not info or not info.category:
continue
if allowed_categories and info.category not in allowed_categories:
continue
for k in info.origin_keys:
key_freq[k] += 1
# PASS 2: grouping
groups: Dict[str, List[EntryInfo]] = defaultdict(list)
for entry in iter_lexical_entries(morphalou_xml):
info = extract_entry_info(entry)
if not info or not info.category:
continue
if allowed_categories and info.category not in allowed_categories:
continue
# keep only "safe" keys
safe_keys = [k for k in info.origin_keys if key_freq.get(k, 0) <= max_origin_key_freq]
if safe_keys:
gk = sorted(safe_keys)[0]
else:
gk = f"lemma:{info.lemma}"
groups[gk].append(info)
return groups
def choose_primary_lemma(entries: List[EntryInfo]) -> str:
# priorité: masculine, sinon premier par ordre alpha
masc = [e.lemma for e in entries if e.gender == "masculine"]
if masc:
return sorted(masc, key=lambda s: s.lower())[0]
return sorted([e.lemma for e in entries], key=lambda s: s.lower())[0]
def write_pleade(
out_path: str,
thesaurus_id: str,
thesaurus_name: str,
description: str,
groups: Dict[str, List[EntryInfo]],
keep_groups_without_plural: bool,
) -> None:
root = ET.Element(
"thesaurus",
attrib={"id": thesaurus_id, "{http://www.w3.org/XML/1998/namespace}lang": "fr"},
)
infos = ET.SubElement(root, "informations")
ET.SubElement(infos, "nom").text = thesaurus_name
ET.SubElement(infos, "description").text = description
mod = ET.SubElement(infos, "modification")
ET.SubElement(mod, "date").text = dt.datetime.now().replace(microsecond=0).isoformat()
# Prépare un id unique par "nom primaire" (pas par group_key) pour rester lisible
primary_names = []
merged_data = [] # list of (primary_lemma, variants_set)
for gk, entries in groups.items():
if not entries:
continue
primary = choose_primary_lemma(entries)
variants: Set[str] = set()
# 1) Ajouter les autres lemmes du groupe (masc/fém)
for e in entries:
if e.lemma != primary:
variants.add(e.lemma)
# 2) Ajouter les pluriels (de toutes les entrées fusionnées)
for e in entries:
for wf, num in e.forms:
if num == "plural" and wf != primary:
variants.add(wf)
# Option: si aucun variant (ni autre lemme, ni pluriel), on peut ignorer
if not variants and not keep_groups_without_plural:
continue
primary_names.append(primary)
merged_data.append((primary, variants))
# IDs uniques par primary lemma
primary_names_sorted = sorted(set(primary_names), key=lambda s: s.lower())
primary_to_id = make_unique_ids(primary_names_sorted)
# Écrit les concepts dans un ordre stable
merged_data.sort(key=lambda t: t[0].lower())
for primary, variants in merged_data:
concept = ET.SubElement(root, "concept", attrib={"id": primary_to_id[primary]})
relations = ET.SubElement(concept, "relations")
ET.SubElement(relations, "nom").text = primary
for v in sorted(variants, key=lambda s: s.lower()):
va = ET.SubElement(relations, "va", attrib={"ref": v})
va.text = v
tree = ET.ElementTree(root)
indent(root)
tree.write(out_path, encoding="utf-8", xml_declaration=True)
DEFAULT_DESCRIPTION = (
"Généré depuis Morphalou 3.1 (ATILF, ORTOLANG, LGPL-LR). Objectif : recherche sans distinguer singulier/pluriel.\n"
"Analyse et traitement informatique de la langue française - UMR 7118 (ATILF) (2023). Morphalou [Lexique]. "
"ORTOLANG (Open Resources and TOols for LANGuage) - www.ortolang.fr, v3.1, "
"https://hdl.handle.net/11403/morphalou/v3.1."
)
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--input", required=True, help="Morphalou3.1_LMF.xml")
ap.add_argument("--output", required=True, help="XML Pleade de sortie")
ap.add_argument("--thesaurus-id", required=True, help="ID du thésaurus Pleade (nouvel ID)")
ap.add_argument("--thesaurus-name", default="Dictionnaire sing/plur + genre (Morphalou 3.1)", help="Nom")
ap.add_argument("--description", default=DEFAULT_DESCRIPTION, help="Description (avec citation)")
ap.add_argument(
"--categories",
default="commonNoun,adjective",
help="grammaticalCategory à inclure (séparées par virgules), ex: commonNoun,adjective",
)
ap.add_argument(
"--keep-groups-without-variant",
action="store_true",
help="Conserver aussi les groupes sans variantes (sinon ils sont ignorés)",
)
ap.add_argument(
"--max-origin-key-freq",
type=int,
default=3,
help="Fréquence max d'une clé originatingEntry pour autoriser une fusion (défaut: 3)",
)
args = ap.parse_args()
allowed_categories = {norm_space(x) for x in args.categories.split(",") if norm_space(x)}
groups = build_groups(args.input, allowed_categories, args.max_origin_key_freq)
write_pleade(
out_path=args.output,
thesaurus_id=args.thesaurus_id,
thesaurus_name=args.thesaurus_name,
description=args.description,
groups=groups,
keep_groups_without_plural=args.keep_groups_without_variant,
)
# Stats sommaires
total_groups = len(groups)
print(f"OK: groupes traités={total_groups} -> {args.output}")
if __name__ == "__main__":
main()
Les commandes
Changer les droits pour rendre exécutable :
chmod +x morphalou31_lmf_to_pleade_sp_gender_option1.py
Lancer le script python
python3 morphalou31_lmf_to_pleade_sp_gender_option1.py \
--input chemin/vers/Morphalou3.1_LMF.xml \
--output pleade-sing-plur-genre-morphalou31.xml \
--thesaurus-id pleade-sing-plur-genre-morphalou31 \
--max-origin-key-freq 2
Intégration dans ThesX
Après un upload sur le serveur où se trouve ThesX :
Importation dans ThesX en REST via eXist
curl -sS -u USER:MOTDEPASSE -T pleade-sing-plur-genre-morphalou31.xml -H "Content-Type: application/xml" http://127.0.0.1:[le-port-a-verifier]/exist/rest/db/thesx/pleade-sing-plur-genre-morphalou31.xml
Vérification de la bonne intégration dans ThesX
curl -sS -u USER:MOTDEPASSE -H "Accept: application/xml" -L -D - http://127.0.0.1:[le-port-a-verifier]/exist/rest/db/thesx
Par exemple, pour ad44 :
curl -sS -u USER:MOTDEPASSE -H "Accept: application/xml" -L -D - http://127.0.0.1:9005/exist/rest/db/thesx
HTTP/1.1 200 OK
Date: Tue, 27 Jan 2026 13:32:16 GMT
Set-Cookie: JSESSIONID=1kmudcahmkythljbzek38qmgf;Path=/exist
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Content-Type: application/xml; charset=UTF-8
Last-Modified: Tue, 01 Oct 2019 09:46:50 GMT
Created: Tue, 01 Oct 2019 09:46:50 GMT
Transfer-Encoding: chunked
Server: Jetty(8.1.9.v20130131)
<exist:result xmlns:exist="http://exist.sourceforge.net/NS/exist">
<exist:collection name="/db/thesx" created="2019-10-01T11:46:49.906+02:00" owner="admin" group="dba" permissions="rwxr-xr-x">
<exist:resource name="dic-sing-plu-ad44.xml" created="2019-10-01T11:50:10.993+02:00" last-modified="2019-10-01T11:50:10.993+02:00" owner="admin" group="dba" permissions="rw-r--r--"/>
<exist:resource name="thesaurus-w-minuscules.xml" created="2019-10-01T11:51:01.059+02:00" last-modified="2019-10-01T11:51:01.059+02:00" owner="admin" group="dba" permissions="rw-r--r--"/>
<exist:resource name="thesaurus-matiere-exist.xml" created="2026-01-21T10:33:54.225+01:00" last-modified="2026-01-21T10:33:54.225+01:00" owner="admin" group="dba" permissions="rw-r--r--"/>
<exist:resource name="pleade-singulier-pluriel-morphalou31.xml" created="2026-01-27T13:19:56.844+01:00" last-modified="2026-01-27T13:19:56.844+01:00" owner="admin" group="dba" permissions="rw-r--r--"/>
<exist:resource name="pleade-sing-plur-genre-morphalou31.xml" created="2026-01-27T14:31:42.53+01:00" last-modified="2026-01-27T14:31:42.53+01:00" owner="admin" group="dba" permissions="rw-r--r--"/>
</exist:collection>