/* global module */ /* jslint node: true */ /* jslint indent: 2 */ 'use strict'; /* Module Require */ var utils = require('li-utils'), fs = require('fs'), path = require('path'), extend = require('util')._extend, Lemmatizer = require('javascript-lemmatizer'), snowballFactory = require('snowball-stemmers'), Tagger = require('./lib/tagger.js'), lexicon = require('./lib/lexicon.js'), DefaultFilter = require('./lib/defaultfilter.js'), TermExtraction = require('./lib/termextractor.js'); // Tagger + filter + extractor + lemmatizer var tagger = new Tagger(lexicon), filter = new DefaultFilter({ 'singleStrengthMinOccur': 1, // 10 par défaut 'noLimitStrength': 2 // 2 par défaut }), extractor = new TermExtraction({ 'filter': filter }), lemmatizer = new Lemmatizer(), stemmer = snowballFactory.newStemmer('english'); var business = {}; // Fichier contenant tous les chemins utiles à l'appli var paths = require('./resources/paths.json'); business.paths = utils.paths.init(paths, __dirname); // Chemins des fichiers utilisés par le module business.resources = utils.resources.require(business.paths); // Chargement de toutes les ressources (JSON) du module /* Constantes */ business.NOW = utils.dates.now(); // Date du jour formatée (string) business.NOT_ALPHANUMERIC = new RegExp('\\W', 'g'); // RegExp d'un caractère non alphanumérique business.DIGIT = new RegExp('\\d', 'g'); // RegExp d'un chiffre business.NOUN_TAG = new RegExp(/(\|)?N[A-Z]{1,3}(\|)?/g); // RegExp de toutes les formes du tag de nom business.VERB_TAG = new RegExp(/(\|)?V[A-Z]{1,3}(\|)?/g); // RegExp de toutes les formes du tag de verbe business.MAX_NOT_ALPHANUMERIC = 2; // Nombre max de caractère non alpha numérique business.MAX_DIGIT = 2; // Nombre max de numérique business.MIN_LENGTH = 4; // Longueur minimale d'un terme (incluse) business.SPECIFIC_TERM = new RegExp(/^([^a-zA-Z0-9]*|[!\-;:,.?]*)(\w+)([^a-zA-Z0-9]*|[!\-;:,.?]*)$/g); // RegExp permettant de découper un terme entouré par de la ponctuation business.SEPARATOR = '#'; business.LOGS = { // Logs des différents cas possibles (et gérés par le module) 'SUCCESS': 'TEI file created at ', 'ERROR_FILE': 'File not found', 'ERROR_EXTRACTION': 'Extracted terms not found', 'ERROR_VALIDATION': 'Valid terms not found', 'ERROR_LEMMATIZATION': 'Lemmatized terms not found', 'ERROR_TOKENIZATION': 'Tokens not found', 'ERROR_TAGGER': 'Tagged tokens not found' }; /* ** ------------------------------------------------------------------------------ ** Coeur du Code Métier ** ------------------------------------------------------------------------------ */ business.doTheJob = function(jsonLine, cb) { // Variables d'erreurs et de logs var _err = null, options = { errLogs: [], processLogs: [] }; // Récupération des données utiles var documentId = jsonLine.idIstex, file = utils.files.select(jsonLine.fulltext, [{ mime: 'text/plain', original: false }, { mime: 'text/plain' }]), corpusOutputPath = jsonLine.corpusOutput; // Si aucun fichier n'est trouvé if (!file) { options.processLogs.push(documentId + '\t' + business.LOGS.ERROR_FILE); return cb(_err, options); } // Lecture du fichier TXT fs.readFile(file.path, 'utf-8', function(err, data) { // Lecture impossible if (err) { _err = new Error(err); _err.code = 1; jsonLine.errCode = _err.code; jsonLine._errMsg = _err.message; options.errLogs.push(_err.toString()); return cb(_err, options); } /** * Représentation d'un texte dans rd-teeft * { * keywords: [], // Mots clés du texte * extraction: [], // Extraction sur tous le texte * terms: { // Toutes les termes présent dans le texte, sous leurs différentes formes * tagged: [], // taggés * sanitized: [], // sanitizés * lemmatized: [], // lemmatizés * }, * tokens: [], // Tous les tokens présent dans le texte * statistics: {} // Toutes les statistiques sur le texte * } */ var text = business.index(data); // S'il n'y a aucun tokens dans tout le texte, arrêt des traitements en cours if (text.tokens.length === 0) { options.processLogs.push(documentId + '\t' + business.LOGS.ERROR_TOKENIZATION); return cb(_err, options); } // S'il n'y a aucun terme taggé dans tout le texte, arrêt des traitements en cours if (text.terms.tagged.length === 0) { options.processLogs.push(documentId + '\t' + business.LOGS.ERROR_TAGGER); return cb(_err, options); } // S'il n'y a aucun terme lemmatizé dans tout le texte, arrêt des traitements en cours if (text.terms.lemmatized.length === 0) { options.processLogs.push(documentId + '\t' + business.LOGS.ERROR_LEMMATIZATION); return cb(_err, options); } // S'il n'y a aucun terme sanitizé dans tout le texte, arrêt des traitements en cours if (text.terms.sanitized.length === 0) { options.processLogs.push(documentId + '\t' + business.LOGS.ERROR_VALIDATION); return cb(_err, options); } // S'il n'y a aucun terme extrait dans tout le texte, arrêt des traitements en cours if (text.extraction.keys.length === 0) { options.processLogs.push(documentId + '\t' + business.LOGS.ERROR_EXTRACTION); return cb(_err, options); } // Construction de la structure de données pour le templates var data = { 'date': business.NOW, 'module': business.resources.config, // Infos sur la configuration du module 'document': { // Infos sur le document 'id': documentId, 'terms': text.keywords // Termes indexés } }, // Récupération du directory & filename de l'ouput output = utils.files.createPath({ corpusPath: corpusOutputPath, id: documentId, type: 'enrichments', label: business.resources.config.label, // extension: ['_', business.resources.config.version, '_', business.resources.config.resource, '.tei.xml'].join('') extension: '.tei.xml' }); // Récupération du fragment de TEI utils.enrichments.write({ 'template': business.paths.template, 'data': data, 'output': output }, function(err) { if (err) { // Lecture/Écriture impossible _err = new Error(err); _err.code = 1; jsonLine.errCode = _err.code; jsonLine._errMsg = _err.message; options.errLogs.push(_err.toString()); return cb(_err, options); } // Création de l'objet enrichement représentant l'enrichissement produit var enrichment = { 'path': path.join(output.directory, output.filename), 'extension': 'tei', 'original': false, 'mime': 'application/tei+xml' }; // Sauvegarde de l'enrichissement dans le jsonLine jsonLine.enrichments = utils.enrichments.save(jsonLine.enrichments, { 'enrichment': enrichment, 'label': business.resources.config.label }); // Tout s'est bien passé options.processLogs.push(documentId + '\t' + business.LOGS.SUCCESS + output.filename); return cb(_err, options); }); }); }; /** * Tokenize un texte * @param {String} text Texte à tokenizer * @return {Array} Liste des termes nettoyés */ business.tokenize = function(text) { var words = text.split(/\s/g), result = []; for (var i = 0; i < words.length; i++) { var term = words[i].toLowerCase(); // Now, a word can be preceded or succeeded by symbols, so let's // split those out var match; while (match = business.SPECIFIC_TERM.exec(term)) { for (var j = 1; j < match.length; j++) { if (match[j].length > 0) { if (j === 2) { result.push(match[j]); } else { result.push(business.SEPARATOR); } } } } } return result; }; /** * Retourne la traduction du tag * @param {str} tag Tag affecté par la classe Tagger * @return {str} tag compris par la classe Lemmatizer ou false */ business.translateTag = function(tag) { var result = false; if (tag === "RB") { result = "adv"; } else if (tag === "JJ") { result = "adj"; } else if (tag.match(business.NOUN_TAG)) { result = "noun"; } else if (tag.match(business.VERB_TAG)) { result = "verb"; } return result; }; /** * Filtre les termes ne respectant pas les conditions * @param {Array} terms Termes à filtrer * @return {Array} Liste de termes filtrés */ business.sanitize = function(terms) { var result = [], invalid = tagger.tag(business.SEPARATOR)[0]; for (var i = 0; i < terms.length; i++) { var value = invalid; if (terms[i].term.length >= business.MIN_LENGTH) { var na = terms[i].term.match(business.NOT_ALPHANUMERIC), d = terms[i].term.match(business.DIGIT); if ((!na || na.length <= business.MAX_NOT_ALPHANUMERIC) && (!d || d.length < business.MAX_DIGIT) && (!business.resources.stopwords[terms[i].stem])) { value = terms[i]; } } result.push(value); } return result; }; /** * Lemmatize des termes taggés (en traduisant le tag) * @param {Array} terms Termes taggés à lemmatizer * @return {Array} Liste de termes lemmatizé */ business.lemmatize = function(terms) { var result = []; for (var i = 0; i < terms.length; i++) { var trslTag = business.translateTag(terms[i].tag), lemma = terms[i].term; // Si la traduction est possible if (trslTag) { var _lemma = lemmatizer.lemmas(terms[i].term, trslTag); if (_lemma.length > 0) { lemma = _lemma[0][0]; // Récupération du terme lemmatizé } } result.push({ term: terms[i].term, tag: terms[i].tag, lemma: lemma, stem: stemmer.stem(terms[i].term) }); } return result; }; business.index = function(data) { // Représentation d'un texte par défaut var text = { 'keywords': [], // Mots clés du texte 'extraction': { // Extraction sur tout le texte 'terms': {}, 'keys': [] }, 'terms': { // Toutes les termes présent dans le texte, sous leurs différentes formes 'tagged': [], // taggés 'sanitized': [], // sanitizés 'lemmatized': [] // lemmatizés }, 'tokens': [], // Tous les tokens présent dans le texte 'statistics': { // Toutes les statistiques sur le texte // fréquences 'frequencies': { 'max': 0, 'total': 0 }, // spécificités 'specificities': { 'avg': 0, 'max': 0 } } }; // Tokenization du texte & Ajout d'une séparation à la fin de chaque segment de texte (pour l'extraction) text.tokens = business.tokenize(data); // S'il n'y a aucun tokens dans tout le texte, arrêt des traitements en cours if (text.tokens.length === 0) return text; // Tag des tokens text.terms.tagged = tagger.tag(text.tokens); // S'il n'y a aucun terme taggé dans tout le texte, arrêt des traitements en cours if (text.terms.tagged.length === 0) return text; // Lemmatization des termes taggés text.terms.lemmatized = business.lemmatize(text.terms.tagged); // S'il n'y a aucun terme lemmatizé dans tout le texte, arrêt des traitements en cours if (text.terms.lemmatized.length === 0) return text; // Retrait de tous les termes qui ne correspondent pas au format souhaité text.terms.sanitized = business.sanitize(text.terms.lemmatized); // S'il n'y a aucun terme sanitizé dans tout le texte, arrêt des traitements en cours if (text.terms.sanitized.length === 0) return text; // Configuration du Filter extractor.get('filter').configure(text.tokens.length); // Exctraction des termes text.extraction.terms = extractor.extract(extend([], text.terms.sanitized)); // Données sous forme d'objet text.extraction.keys = Object.keys(text.extraction.terms); // Liste des termes extraits // S'il n'y a aucun terme extrait dans tout le texte, arrêt des traitements en cours if (text.extraction.keys.length === 0) return text; // Calcul des statistiques sur les fréquences d'apparitions de chaque terme for (var i = 0; i < text.extraction.keys.length; i++) { // Clé du term dans text.extraction.terms var key = text.extraction.keys[i]; // Fréquence maximale pour ce texte if (text.statistics.frequencies.max < text.extraction.terms[key].frequency) { text.statistics.frequencies.max = text.extraction.terms[key].frequency; } // Fréquence totale pour ce texte text.statistics.frequencies.total += text.extraction.terms[key].frequency; } // Valeur par défaut var dValue = Math.pow(10, -5); // Calcul des scores de chaque terme + Calcul du total des fréquences for (var i = 0; i < text.extraction.keys.length; i++) { // Clé du term dans text.extraction.terms var key = text.extraction.keys[i]; // Valeur de la pondération du terme qui dépend de sa "représentativité" dans le vocabulaire (dictionnary.json) var weighting = business.resources.dictionary[key] || dValue; // Specificité = (fréquence d'apparition du terme) / (pondération) text.extraction.terms[key].specificity = ((text.extraction.terms[key].frequency / text.statistics.frequencies.total) / weighting); // Calcul de la spécificité maximale de ce segment if (text.statistics.specificities.max < text.extraction.terms[key].specificity) { text.statistics.specificities.max = text.extraction.terms[key].specificity; } } // Normalisation de la spécificité de chaque terme présent dans le texte & Somme de toutes les spécificités normalisée for (var i = 0; i < text.extraction.keys.length; i++) { // Clé du term dans text.extraction.terms var key = text.extraction.keys[i]; text.extraction.terms[key].specificity /= text.statistics.specificities.max; text.statistics.specificities.avg += text.extraction.terms[key].specificity; } // Calcul de la spécificité moyenne dans tout le document text.statistics.specificities.avg /= text.extraction.keys.length; // Liste des termes indexés text.keywords = []; // Sélection des résultats finaux for (var i = 0; i < text.extraction.keys.length; i++) { // Clé du term dans text.extraction.terms var key = text.extraction.keys[i]; if (!business.resources.config.truncate || (text.extraction.terms[key].specificity >= text.statistics.specificities.avg)) { text.extraction.terms[key].term = key; text.keywords.push(text.extraction.terms[key]); } } // Si le résultat doit être trié if (business.resources.config.sort) { function compare(a, b) { if (a.specificity > b.specificity) return -1; else if (a.specificity < b.specificity) return 1; else return 0; } text.keywords = text.keywords.sort(compare); } return text; }; module.exports = business;