#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""The per-document local index: document identifier -> term occurrences and positions.
A ``.nindlocalindex`` file is a :class:`~nind.NindIndex.NindIndex` keyed by
*internal* document id (a dense ``1..nombreDocuments`` sequence), plus an
external <-> internal id translation table built at open time from each
document's stored ``identifiantExterne``. Each document's definition lists
every term that occurs in it (delta-encoded relative term ids) together
with the positions ("localisations") it occurs at - the data
:class:`~nind.nind_engine.NindEngine` uses for term-frequency and
document-length calculations.
"""
__author__ = "jys"
__copyright__ = "Copyright (C) 2017 LATEJCON"
__license__ = "GNU LGPL"
__version__ = "2.1.2"
# Author: jys <jy.sage@orange.fr>, (C) LATEJCON 2017
# Copyright: 2014-2017 LATEJCON. See LICENCE.md file that comes with this distribution
# This file is part of NIND (as "nouvelle indexation").
# NIND is free software: you can redistribute it and/or modify it under the terms of the
# GNU Less General Public License (LGPL) as published by the Free Software Foundation,
# (see <http://www.gnu.org/licenses/>), either version 3 of the License, or any later version.
# NIND is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
# even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Less General Public License for more details.
import sys
from os import getenv, path
from io import StringIO
import codecs
try:
from .NindPadFile import calculeRejpartition
from .NindIndex import NindIndex
except ImportError:
# run directly (not as part of the installed package): see docs/cli.md
from NindPadFile import calculeRejpartition
from NindIndex import NindIndex
try:
from nind import _native as native
except ImportError:
# nind._native is a compiled extension: not available when introspecting
# the pure-Python source without building it (e.g. Sphinx autodoc, see
# docs/conf.py). Constructing NindLocalindex with lexicon_identification
# still requires it - only module import is tolerant.
native = None
NINDLOCALINDEX_EXT = '.nindlocalindex'
def usage():
if getenv("PY") != None: script = sys.argv[0].replace(getenv("PY"), '$PY')
else: script = sys.argv[0]
print (f"""© l'ATEJCON.
o Analyse un fichier nindlocalindex du système nind et affiche les statistiques
o Peut donner l'identifiant interne et externe du dernier document indexé
o Peut dumper nindlocalindex sur <fichier>-dump.txt
o Peut donner la liste des identifiants externes de tous les documents indexés
o Peut afficher les données correspondant à un document spécifié par son
identifiant externe
Le format du fichier est défini dans le document LAT2017.JYS.470.
usage : {script} <fichier> [ <analyse> | <dernier> | <dumpe> | <ident> | <affiche> <ident> ]
exemple : {script} FRE.nindlocalindex
exemple : {script} FRE.nindlocalindex dern
exemple : {script} FRE.nindlocalindex dump
exemple : {script} FRE.nindlocalindex iden
exemple : {script} FRE.nindlocalindex affi 3456
""")
def main():
try:
if len(sys.argv) < 2 : raise Exception()
localindexFileName = path.abspath(sys.argv[1])
action = 'analyse'
if len(sys.argv) > 2 : action = sys.argv[2]
identExterne = 0
if len(sys.argv) > 3 : identExterne = int(sys.argv[3])
#la classe
nindLocalindex = NindLocalindex(localindexFileName)
if action.startswith('anal'): nindLocalindex.analyseFichierLocalindex(True)
elif action.startswith('der'):
(noInterne, noExterne) = nindLocalindex.donneMaxIdentifiants()
print (f'dernier document indexé : n° interne {noInterne}, n° externe : {noExterne}')
elif action.startswith('dump'):
outFilename = localindexFileName + '-dump.txt'
outFile = codecs.open(outFilename, 'w', 'utf-8')
nbLignes = nindLocalindex.dumpeFichier(outFile)
outFile.close()
print (f'{nbLignes} lignes écrites dans {outFilename}')
elif action.startswith('id'):
listeIdentifiants = nindLocalindex.donneidentifiantsExternes()
listeIdentifiants.sort()
#les affiche, un par ligne
for (externe, interne) in listeIdentifiants: print (externe, ' <-> ', interne)
elif action.startswith('affi'):
print (nindLocalindex.afficheDocument(identExterne))
else: raise Exception()
except Exception as exc:
if len(exc.args) == 0: usage()
else:
print ("******************************")
print (exc.args[0])
print ("******************************")
raise
sys.exit()
############################################################
# <dejfinition> ::= <flagDejfinition=19> <identifiantDoc> <identifiantExterne> <longueurDonnejes> <donnejesDoc>
# <flagDejfinition=19> ::= <Entier1>
# <identifiantDoc> ::= <Entier3>
# <identifiantExterne> ::= <Entier4>
# <longueurDonnejes> ::= <Entier3>
# <donnejesDoc> ::= { <donnejesTerme> }
# <donnejesTerme> ::= <identTermeRelatif> <catejgorie> <nbreLocalisations> <localisations>
# <identTermeRelatif> ::= <EntierSLat>
# <catejgorie> ::= <Entier1>
# <nbreLocalisations> ::= <Entier1>
# <localisations> ::= { <localisationRelatif> <longueur> }
# <localisationRelatif> ::= <EntierSLat>
# <longueur> ::= <Entier1>
##############################
# <spejcifique> ::= <maxIdentifiantInterne> <nombreDocuments>
# <maxIdentifiantInterne> ::= <Entier4>
# <nombreDocuments> ::= <Entier4>
############################################################
FLAG_DEJFINITION = 19
#<maxIdentifiantInterne>(4) <nombreDocuments>(4) = 8
TAILLE_SPEJCIFIQUES = 8
#<flagDejfinition=19>(1) <identifiantDoc>(3) <identifiantExterne>(4) <longueurDonnejes>(3) = 11
TAILLE_TESTE_DEJFINITION = 11
[docs]
class NindLocalindex(NindIndex):
"""Read-only per-document local index: term occurrences and positions, keyed by document id.
Documents are addressed by their *external* id everywhere in the
public API (:meth:`donneListeTermes`, :meth:`afficheDocument`); the
class transparently translates to/from the internal id used inside the
file, via a table built once at open time. :meth:`donneListeTermes`
delegates to the ``nind._native`` bindings and requires
``lexicon_identification``; the diagnostic methods
(``analyseFichierLocalindex``, ``dumpeFichier``, ``afficheDocument``)
work without it, remaining hand-rolled pure-Python parsing since
``nind._native`` doesn't expose that introspection.
"""
def __init__(self, localindexFileName, lexicon_identification=None):
"""Open ``localindexFileName`` read-only and build the external<->internal id table.
:param localindexFileName: path to the ``.nindlocalindex`` file.
:param lexicon_identification: the ``nind._native.Identification``
of the :class:`~nind.NindLexiconindex.NindLexiconindex` this
local index was built against (see its ``donneIdentification``).
Required only for :meth:`donneListeTermes`; the diagnostic
methods work without it.
:raises Exception: if the file's pad-file envelope is invalid, or
its specifics block has the wrong size.
"""
NindIndex.__init__(self, localindexFileName)
if native is None:
raise ImportError('nind._native compiled extension is not available (build it via "uv sync")')
self._has_lexicon_identification = lexicon_identification is not None
#sans identification rejelle, ouvre quand mesme (sans vejrification croisejes) pour les diagnostics
effective_identification = lexicon_identification if lexicon_identification is not None else native.Identification(0, 0)
base = localindexFileName
if base.endswith(NINDLOCALINDEX_EXT): base = base[:-len(NINDLOCALINDEX_EXT)]
self._native = native.NindLocalIndex(base, is_writer=False,
lexicon_identification=effective_identification)
#rejcupehre l'adresse et la longueur des spejcifiques
(offsetSpejcifiques, tailleSpejcifiques) = self.donneSpejcifiques()
if tailleSpejcifiques != TAILLE_SPEJCIFIQUES:
raise Exception('%s : taille incompatible des spécifiques'%(self.latFileName))
self.seek(offsetSpejcifiques, 0)
#<maxIdentifiantInterne> <nombreDocuments>
self.maxIdentifiantInterne = self.litNombre4()
nombreDocuments = self.litNombre4()
#initialisation traduction identitiant externe -> identifiant interne
self.docIdTradExtInt = {}
for noDocInterne in range(1, self.maxIdentifiantInterne +1):
(offsetDejfinition, longueurDejfinition) = self.donneAdresseDejfinition(noDocInterne)
if offsetDejfinition == 0: continue
self.seek(offsetDejfinition, 0)
#<flagDejfinition=19> <identifiantDoc> <identifiantExterne>
if self.litNombre1() != FLAG_DEJFINITION:
raise Exception('%s : pas FLAG_DEJFINITION à %08X'%(self.latFileName, offsetDejfinition))
if self.litNombre3() != noDocInterne:
raise Exception('%s : %d pas trouvé à %08X'%(self.latFileName, ident, offsetDejfinition+1))
noDocExterne = self.litNombre4()
self.docIdTradExtInt[noDocExterne] = noDocInterne
#######################################################################
#trouve les donnejes
def __donneDonnejes(self, identifiant):
#lit la définition du mot
(offsetDejfinition, longueurDejfinition) = self.donneAdresseDejfinition(identifiant)
if offsetDejfinition == 0: return False, 0, 0, 0 #identifiant pas trouve
self.seek(offsetDejfinition, 0)
#<flagDejfinition=19> <identifiantDoc> <identifiantExterne> <longueurDonnejes>
if self.litNombre1() != FLAG_DEJFINITION:
raise Exception('%s : pas FLAG_DEJFINITION à %08X'%(self.latFileName, offsetDejfinition))
if self.litNombre3() != identifiant:
raise Exception('%s : %d pas trouvé à %08X'%(self.latFileName, index, offsetDejfinition+1))
identifiantExterne = self.litNombre4()
longueurDonnejes = self.litNombre3()
tailleExtension = longueurDejfinition - longueurDonnejes - TAILLE_TESTE_DEJFINITION
if tailleExtension < 0:
raise Exception('%s : %d incohérent à %08X'%(self.latFileName, identifiant, offsetDejfinition+5))
return True, longueurDonnejes, tailleExtension, identifiantExterne
#######################################################################
#retourne la structure dejcrivant les localisations de termes pour le document spejcifiej
[docs]
def donneListeTermes(self, noDocExterne):
"""Return every term occurrence and its positions for a given document.
French *donneListeTermes* = "gives term list".
:param noDocExterne: the document's external identifier.
:return: a list of ``nind._native.Term`` (``.term``, ``.cg``,
``.localisation``: list of ``Localisation`` with
``.position``/``.length``), one per term occurring in the
document. Empty list if ``noDocExterne`` is unknown.
:raises Exception: if opened without ``lexicon_identification``.
"""
if not self._has_lexicon_identification:
raise Exception('%s : donneListeTermes nécessite lexicon_identification à l\'ouverture'%(self.latFileName))
#nind._native.NindLocalIndex.get_local_def prend directement l'identifiant externe
#(la traduction externe -> interne est faite en interne par le C++)
local_def = self._native.get_local_def(noDocExterne)
return local_def if local_def is not None else []
#######################################################################
#retourne les identifiants interne et externe du dernier document indexej
[docs]
def donneMaxIdentifiants(self):
"""Return the internal and external identifiers of the last-indexed document.
French *donneMaxIdentifiants* = "gives max identifiers".
:return: an ``(internalId, externalId)`` tuple, or ``(0, 0)`` if
that document has since been erased.
"""
#trouve les donnejes du dernier doc indexej
trouvej, dummy, dummy, identifiantExterne = self.__donneDonnejes(self.maxIdentifiantInterne)
if not trouvej: return (0, 0) # il a ejtej effacej
return (self.maxIdentifiantInterne, identifiantExterne)
#######################################################################
#analyse du fichier
[docs]
def analyseFichierLocalindex(self, trace):
"""Validate the file and report corpus-wide document/occurrence statistics.
French *analyseFichierLocalindex* = "analyzes local-index file".
Extends :meth:`~nind.NindIndex.NindIndex.analyseFichierIndex` with
totals (document count, term-document and term-position occurrence
counts) and a per-document occurrence-count distribution.
:param trace: if ``True``, print a human-readable report to stdout.
:return: ``False`` if the underlying index is invalid; otherwise
prints the report when ``trace`` is set (no explicit return
value on the success path).
"""
cestbon = self.analyseFichierIndex(trace)
if not cestbon: return False
if trace: print ("======LOCALINDEX=======")
try:
#trouve le max des identifiants
maxIdent = self.donneMaxIdentifiant()
totalExtensions = nbExtensions = 0
totalDonnejes = 0
totalLocalisations = 0
totalTermDoc = 0
occurrences = []
for identifiant in range(maxIdent):
try:
trouvej, longueurDonnejes, tailleExtension, identifiantExterne = self.__donneDonnejes(identifiant)
if not trouvej: continue
if tailleExtension > 0: nbExtensions += 1
totalExtensions += tailleExtension
totalDonnejes += longueurDonnejes + TAILLE_TESTE_DEJFINITION
#examine les données
noTerme = localisationAbsolue = 0
nbOccurrences = 0
noTermSet = set()
finDonnejes = self.tell() + longueurDonnejes
while self.tell() < finDonnejes:
#<identTermeRelatif> <catejgorie> <nbreLocalisations> <localisations>
nbOccurrences +=1
noTerme += self.litNombreSLat()
noTermSet.add(noTerme)
catejgorie = self.litNombre1()
nbreLocalisations = self.litNombre1()
totalLocalisations += nbreLocalisations
for i in range (nbreLocalisations):
#<localisationRelatif> <longueur>
localisationAbsolue += self.litNombreSLat()
longueur = self.litNombre1()
occurrences.append(nbOccurrences)
totalTermDoc += len(noTermSet)
except:
if trace: print ('*******ERREUR SUR IDENTIFIANT :', identifiant)
raise
if trace:
nbDonnejes, occurrencesMin, occurrencesMax, totalOccurrences, moyenne, ejcartType = calculeRejpartition(occurrences)
total = totalDonnejes + totalExtensions
print ("DONNÉES % 10d (%6.2f %%) % 9d occurrences"%(totalDonnejes, float(100)*totalDonnejes/total, nbDonnejes))
print ("EXTENSIONS % 10d (%6.2f %%) % 9d occurrences"%(totalExtensions, float(100)*totalExtensions/total, nbExtensions))
print ("TOTAL % 10d %08X"%(total, total))
print ("=============")
print ("DOCUMENTS % 10d "%(nbDonnejes))
print ("TERMES-DOCS % 10d occurrences"%(totalTermDoc))
print ("TERMES % 10d occurrences"%(totalOccurrences))
print ("LOCALISATIONS % 10d occurrences"%(totalLocalisations))
print ("=============")
print ("DOCUMENT MAX % 10d occurrences de termes"%(occurrencesMax))
print ("DOCUMENT MIN % 10d occurrences de termes"%(occurrencesMin))
print ("MOYENNE % 10d occurrences de termes"%(moyenne))
print ("ÉCART-TYPE % 10d occurrences de termes"%(ejcartType))
print ("=============")
print ("%0.2f octets / occurrence de terme"%(float(self.donneTailleFichier())/totalOccurrences))
except Exception as exc:
cestBon = False
if trace: print ('ERREUR :', exc.args[0])
if trace: print ("=============")
try:
#rejcupehre l'adresse et la longueur des spejcifiques
(offsetSpejcifiques, tailleSpejcifiques) = self.donneSpejcifiques()
if tailleSpejcifiques != TAILLE_SPEJCIFIQUES:
raise Exception('%s : taille incompatible des spécifiques'%(self.latFileName))
self.seek(offsetSpejcifiques, 0)
#<maxIdentifiantInterne> <nombreDocuments>
maxIdentifiantInterne = self.litNombre4()
nombreDocuments = self.litNombre4()
if trace:
print ("Max identifiant interne utilisé: %d"%(maxIdentifiantInterne))
print ("Nombre de documents indexés : %d"%(nombreDocuments))
except Exception as exc:
cestBon = False
if trace: print ('ERREUR :', exc.args[0])
#######################################################################
#dumpe le fichier lexique sur un fichier texte
[docs]
def dumpeFichier(self, outFile):
"""Dump every document's full term/position data to a text file, for inspection.
French *dumpeFichier* = "dumps file".
:param outFile: a writable text file object (UTF-8).
:return: the number of documents written.
"""
nbLignes = 0
#trouve le max des identifiants
maxIdent = self.donneMaxIdentifiant()
for identifiant in range(maxIdent):
#trouve les donnejes
trouvej, longueurDonnejes, tailleExtension, identifiantExterne = self.__donneDonnejes(identifiant)
if not trouvej: continue #identifiant pas trouve
nbLignes +=1
outFile.write('%07d/%07d:: '%(identifiant, identifiantExterne))
#examine les données
noTerme = localisationAbsolue = 0
finDonnejes = self.tell() + longueurDonnejes
while self.tell() < finDonnejes:
#<identTermeRelatif> <catejgorie> <nbreLocalisations> <localisations>
noTerme += self.litNombreSLat()
catejgorie = self.litNombre1()
nbreLocalisations = self.litNombre1()
outFile.write('[%d](%d)'%(noTerme, catejgorie))
localisationsList = []
for i in range (nbreLocalisations):
#<localisationRelatif> <longueur>
localisationAbsolue += self.litNombreSLat()
longueur = self.litNombre1()
localisationsList.append('%d(%d)'%(localisationAbsolue, longueur))
outFile.write('<%s> '%(','.join(localisationsList)))
outFile.write('\n')
return nbLignes
#######################################################################
[docs]
def donneidentifiantsExternes(self):
"""Return every ``(externalId, internalId)`` pair known to this file.
French *donneIdentifiantsExternes* = "gives external identifiers".
Used by :class:`~nind.nind_engine.NindEngine` to enumerate the
whole corpus (e.g. to compute the total document count and average
document length).
:return: a list of ``(noDocExterne, noDocInterne)`` tuples.
"""
return list(self.docIdTradExtInt.items())
#######################################################################
#dejcode les donnejes associejes ah un terme
[docs]
def afficheDocument(self, noDocExterne):
"""Format one document's full term/position data as a human-readable string.
French *afficheDocument* = "displays document". Same content as
one :meth:`dumpeFichier` line, computed for a single document.
:param noDocExterne: the document's external identifier.
:return: a formatted string, or ``"<id> : inconnu"`` if
``noDocExterne`` is unknown.
"""
rejsultat = StringIO()
#trouve l'identifiant interne
if noDocExterne not in self.docIdTradExtInt: return '%d : inconnu'%(noDocExterne)
noDocInterne = self.docIdTradExtInt[noDocExterne]
rejsultat.write('%07d/%07d:: '%(noDocInterne, noDocExterne))
#trouve les donnejes
trouvej, longueurDonnejes, tailleExtension, identifiantExterne = self.__donneDonnejes(noDocInterne)
if not trouvej: longueurDonnejes = 0 #identifiant pas trouvej, document effacej
#examine les données
noTerme = localisationAbsolue = 0
finDonnejes = self.tell() + longueurDonnejes
while self.tell() < finDonnejes:
#<identTermeRelatif> <catejgorie> <nbreLocalisations> <localisations>
noTerme += self.litNombreSLat()
catejgorie = self.litNombre1()
nbreLocalisations = self.litNombre1()
rejsultat.write('[%d](%d)'%(noTerme, catejgorie))
localisationsList = []
for i in range (nbreLocalisations):
#<localisationRelatif> <longueur>
localisationAbsolue += self.litNombreSLat()
longueur = self.litNombre1()
localisationsList.append('%d(%d)'%(localisationAbsolue, longueur))
rejsultat.write('<%s> '%(','.join(localisationsList)))
return rejsultat.getvalue()
#######################################################################
if __name__ == '__main__':
main()