Das Lehrprojekt ist eine Node.js Applikation, wobei diverse NLP-Verfahren implementiert wurden. Das Ziel des Lehrprojekts war eine «eRecruiting»-Lösung (Prototyp) zu programmieren. Folgende Frameworks und Technologien wurden für das Lehrprojekt verwendet:
- Express (Bibliothek für Node.js Core Modul HTTP)
- Express-Fileupload (Express Extension für die Datenübertragung)
- Express-Session (Express Extension für das Session Objekt)
- Express-Validator (Express Extension für die Validierung von Formularen)
- Express-Handlebars (Express Extension für das Verwenden von Handlebars)
- Body-Parser (Express Extension für das Auslesen von Formularübermittlungen)
- MongoDB Atlas (NoSQL-Datenbank)
- Mongoose (Datenbank Framework für die Arbeit mit MongoDB)
- HBS (Handlebars Template Engine Machine)
- Bootstrap 4.0.0 (CSS-Framework)
- Natural (Node.js NLP Bibliothek)
- StanfordNLP (Entity Recognition via Algorithmia)
- Textract (DOCX- und PDF-Parser)
- Debug (Debugging Utility)
- Nodemon (Dateiüberwachung für die Entwicklung der Applikatio)n
- Chalk (CSS Utility für die Konsole)
- Python-Shell (Python 3.6)
Weiterführend wurden diverse Kernmodule von Node.js verwendet. Diese werden nicht via ``npm install` installiert. Diese sind folgende:
- fs (Modul für das Arbeiten mit dem Filesystem des Benutzers resp. Server)
- util (Konsolen Utility)
- path (Modul für das Arbeiten mit Dateien und Pfaden)
Eine kurze Anleitung wie man am besten die Applikation lokal installiert und testet:
- Github Repository
iemployee
klonen - Alle Abhängigkeiten installieren mit:
npm install
- Applikation starten mit:
npm start
oder mitnode .\bin\server
- Optional kann man einen Port-Flag angeben. Der Port ist frei wählbar:
node .\bin\server --port 8080
- In der Konsole erscheinen folgende Notifikatione:
NodeApp started
,MongoDB started
undPython started
06 Aug 11:28:43 - [NodeApp started] - Status: [ok], Port: [3000]
06 Aug 11:28:43 - [Python3 started] - Status: [ok]
06 Aug 11:28:44 - [MongoDB started] - Status: [ok]
- Applikation via
localhost:port/
aufrufen
Siehe Kapitel «Deployment», falls die Applikation lokal nicht funktionieren sollte.
Nachfolgend alle benutzerdefinierten NLP-Funktionen, welche ich im Rahmen des Lehrprojekts programmiert habe. Nachfolgend alle NLP-Funktionen:
Die Funktion data_extract() zerlegt ein unstrukturiertes Dokument des Formats *.docx oder *.pdf. Weiterführend erstellt die Funktion eine *.txt-Datei und strukturiert den Dokumentnamen.
function data_extract(user, output, vacancy) {
// FileExtension
let fileExtension = output || '.txt';
let appendix = Object.keys(user.docs);
// Iterate through all files
for (let i = 0; i < user.paths.length; i++) {
// Extract Data
textract.fromFileWithPath(user.paths[i], function (error, text) {
// Create Path + Filename
let filename = user.name + '_' + user.prename + '_' + appendix[i] + fileExtension;
let filepath = path.join(__dirname, '/../uploads/' + vacancy + '/parsed/', filename);
// Create file; Move to uploads/parsed/ dir
fs.writeFile(filepath, text, function (err) {
if (err) throw err;
});
});
}
}
Die Funktion get_corpus() extrahiert den Textkörper aus einem Anschreiben.
function get_corpus(string) {
const regex = /(Dear.*)\sYours.*sincerely.*$/gm;
var str = string;
var m;
var res = [];
while ((m = regex.exec(str)) !== null) {
// Avoid infinite loops with zero-width matches
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
m.forEach((match, groupIndex) => {
res.push(`${match}`);
});
return res;
}
}
Die Funktion _tfidf_vec(cleanText, users) generiert einen TF-IDF Vektor von allen Dokumenten
function _tfidf_vec(cleanText, users) {
// Get Length of Document Collection
let keys = Object.keys(users.nlp.input)
// Add all Documents
tfidf.addDocument(cleanText);
tfidf.addDocument(users.nlp.input.cv);
tfidf.addDocument(users.nlp.input.rf);
// TF-IDF Vectorize for entire document collection
for(let i=0;i<keys.length;i++) {
tfidf.listTerms(i).forEach(function(item) {
tfidfResult[item.term] = item.tfidf;
});
}
}
Die Funktion _get_tfidf_score() berechnet den Score des TF-IDF Vektors
/**
*
* @param {object} tfidf
* @return {float}
*/
function _get_tfidf_score(tfidf) {
let values = Object.values(tfidf);
let length = values.length;
var max = values.reduce((a, b) => { return Math.max(a, b) });
return max/length*100;
}
Die Funktion _get_digital_trie ist eine effiziente Präfix-basierende Suchfunktion.
function _get_digital_trie(tags, tokens) {
// Init return obj
let result = {};
// Add Token to Trie
trie.addStrings(tokens);
// Make a basic digital trie
for(let i = 0;i<tags.length;i++) {
if(trie.contains(tags[i])) {
result[tags[i]] = 1;
} else {
result[tags[i]] = 0;
}
}
// Score calculation
let values = Object.values(result);
let sum = values.reduce(function(acc, val) { return acc + val; });
let divider = tags.length;
let score = sum.toPrecision(6) / divider.toPrecision(6);
// Return
return [result, score];
}
Die Funktion _summary() fasst das Anschreiben zusammen, basierend auf Pattern Recognition.
/**
*
* @param {string} tag
* @param {array} corpus
* @return array[objs] => Object accessible via input property
*/
function _summary(tag, corpus) {
// Create RegEx Pattern
let patt = new RegExp(".*"+tag+".*$", "gi");
// Iterate through corpus array
for(let i=0;i<corpus.length;i++) {
// Execute Search
var res = patt.exec(corpus[i]);
// if not null
if(res !== null) {
summaryResult.push(res);
}
}
return summaryResult;
}
Die Funktion tokenize() segmentiert den Textkorpus nach pro Satzzeichen oder Wort.
let corpus = get_corpus(users.nlp.input.ml);
let cleanText = sentence.tokenize(corpus[1]);
let tokens = words.tokenize(corpus[1]);
Die Funktion recursiveIter() iteriert rekursiv durch das Resultat der StanfordNLP Entity Recognition Pipeline.
/**
*
* @param {object} obj
* @param {string} entity
* @return {array}
*/
function recursiveIter(obj, entity) {
for (i in obj) {
if (typeof obj[i] == "object") {
recursiveIter(obj[i], entity)
} else if(obj[i] == entity) {
entityResult.push(obj[0].toString());
}
}
return entityResult;
}
Die Funktion StanfordNLP Entity Recognition
// GET - Entity Recognition
algorithmia.client("simYi/0ziOKGNge4PedKMON0lT81")
.algo("StanfordNLP/Java2NER/0.1.1")
.pipe(corpus[1])
.then(function(data) {
var keys = Object.keys(data.result);
entResOrg = recursiveIter(data.result, 'ORGANIZATION');
entResLoc = recursiveIter(data.result, 'LOCATION');
});
Nebst den Hauptfunktionen für die NLP-Aufgaben, gibt es auch weitere Funktionen, die für das Lehrprojekt wichtig waren. Nachfolgend alle Funktionen mit dem Dateisystem:
Die Funktion getDate() generiert ein String mit dem Datum für die Namenskonvention nach der Datenübertragung/Datenextrahierung.
function getDate() {
// Get Partials
var today = new Date();
var dd = today.getDate();
var mm = today.getMonth() + 1;
var yyyy = today.getFullYear() + '';
// Append Zero if needed
if (dd < 10) {
dd = '0' + dd
}
// Append Zero if needed
if (mm < 10) {
mm = '0' + mm
}
// Build Date String
today = mm + dd + yyyy.substr(2, 2);
return today;
}
Die Funktion createDirectories() erstellt ein entsprechendes Verzeichnis auf dem Server, sobald eine Bewerbung übermittelt wird.
function createDirectories(dir) {
if (fs.existsSync(dir)) {
return;
} else {
// Parent + Subdirectories
fs.mkdirSync(dir);
fs.mkdirSync(path.join(dir, 'original'));
fs.mkdirSync(path.join(dir, 'parsed'));
}
};
Die Funktion grab() ermöglicht ein Argument Flag für den gewünschten Port.
/**
* Argument Variable grab
*/
function grab(flag) {
let index = process.argv.indexOf(flag);
return (index === -1) ? null : process.argv[index + 1];
}
Die Applikation verfügt über eine Python Shell. Nachfolgend die Funktion in Node, welche das Python Skript anspricht.
function _python_nltk(req, res, user) {
// Get Raw Text Data from User Object
var ml = user.nlp.input.ml;
var cv = user.nlp.input.cv;
var rf = user.nlp.input.rf;
// Create Options Object for Python
var options = {
args: [
ml,
cv,
rf
]
}
// Execute Python Script
PythonShell.run('python/natural.py', options, function(err,data) {
if(err) res.send(err);
res.send(data[0].toString('utf8'));
});
}
Das Python-Skript nutzt NLTK als Natural Language Processing Bibliothek und ermöglicht eine Segmentierung. Nachfolgend das Python-Skript:
#!/usr/bin/env python
import sys
import re
from nltk import word_tokenize
# Get raw text data
ml = sys.argv[1]
cv = sys.argv[2]
rf = sys.argv[3]
# Output
print(word_tokenize(ml))
Falls die Applikation lokal getestet wird, dann muss Python 3.6 auf dem System installiert sein.
Aufgrund der Applikationsgrösse wurde ein URL-Routing-Verfahren angewendet. Der Vorteil eines Routers ist die Separation der verschiedenen Serverdateien. Dies hat zufolge, dass der gesamte Source Code der Applikation übersichtlicher gestaltet werden kann. Der Dateipfad für die Scripts ist /routes
. Nachfolgend alle verwendeten Server-Router:
- routes/index.js Frontend Routing für die Landingpage
- routes/jobs.js Frontend Routing für die Ansicht der Arbeitnehmer
- routes/nlp.js Backend Routing für die Ansicht der Arbeitgeber
- routes/users.js Middleware für die Formularübermittlung
Für weitere Informationen betreffend URL-Routing siehe Dokumentation von Express.
Die Applikation wurde mit Heroku Dyno veröffentlicht. Heroku ist eine Plattform für Cloud-Applikationen. Die Veröffentlichung erfolgt dabei via GitHub oder Heroku CLI. Da es den Rahmen für diese Arbeit sprengen würde, wurde auf eine Staging Pipeline verzichtet, d.h. das Deployment erfolgt direkt mit dem Master Branch. Die Applikation ist mit SSL-Zertifizierung aufrufbar unter: iEmployee
Eine benutzerdefinierte Domäne http://iemployee.ch existiert ebenfalls, jedoch ohne SSL-Zertifikat.
Das Debugging der veröffentlichten Version auf Heroku funktioniert wie folgt:
- Heroku CLI installieren.
- Nach erfolgreicher Installation Befehlskonsole (cmd) öffnen und
heroku login
eingeben. - Benutzername:
info@ajaybachmann.ch
und Passwort:q2w3p0o9ZZ!
eingeben. - Debug-Log aufrufen mit:
heroku logs --tail -a iemployee
Die Applikation verfügt über Testdaten, damit die NLP-Funktionen getestet werden können. Nachfolgend die Struktur des Ordners Testdaten
:
Testdaten/
├── Anschreiben.docx
├── Lebenslauf.docx
└── Arbeitszeugnis.docx
In der Node.js-Community findet man gegenwärtig nur wenige NLP-Bibliotheken für die deutsche Sprache. Aus diesem Grund wurden explizit NLP-Bibliotheken für die englische Sprache verwendet. Weiterführend muss beim Anschreiben auf folgendes RegEx-Pattern berücksichtigt werden:
(Dear.*)\sYours.*sincerely.*$/gm
.
Copyright (c) 2018 Allan Bachmann
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.