Newer
Older
ez-indexation / app / node_modules / rd-teeft / index.js
@kieffer kieffer on 7 Mar 2017 14 KB v0.0.0
/* 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;