tokepi
Version:
Tokenizer that transforms a string of sentences into an array of white-space separated strings of tokens
269 lines (224 loc) • 8.7 kB
JavaScript
var _ = require("underscore");
var _str = require("underscore.string");
var escapeRegExp = require("escape-regexp");
var EMOTICONS = require("emotional-emoticons");
/*
* Constants that help capture edge-case tokens such as emoticons and abbreviations.
*/
// RegExp that captures the initial tokens
var RE_TOKEN = /(\S+)\s/g;
// Emoticon definitions in the form of
// {facial-expression : {p: sentiment-value, e: [emoticon]}}
var EMOTICONS = {
"love":
{ p: +1.00,
e: ["<3", "♥"] },
"grin":
{ p: +1.00,
e: [">:D", ":-D", ":D", "=-D", "=D", "X-D", "x-D", "XD", "xD", "8-D"] },
"taunt":
{ p: +0.75,
e: [">:P", ":-P", ":P", ":-p", ":p", ":-b", ":b", ":c)", ":o)", ":^)"] },
"smile":
{ p: +0.50,
e: [">:)", ":-)", ":)", "=)", "=]", ":]", ":}", ":>", ":3", "8)", "8-)"] },
"wink":
{ p: +0.25,
e: [">;]", ";-)", ";)", ";-]", ";]", ";D", ";^)", "*-)", "*)"] },
"gasp":
{ p: +0.05,
e: [">:o", ":-O", ":O", ":o", ":-o", "o_O", "o.O", "°O°", "°o°"] },
"worry":
{ p: -0.25,
e: [">:/", ":-/", ":/", ":\\", ">:\\", ":-.", ":-s", ":s", ":S", ":-S", ">.>"] },
"frown":
{ p: -0.75,
e: [">:[", ":-(", ":(", "=(", ":-[", ":[", ":{", ":-<", ":c", ":-c", "=/"] },
"cry":
{ p: -1.00,
e: [":'(", ":'''(", ";'("] }
};
// RegExp that captures the emoticons as given in EMOTICONS, but allowing spaces in between the chars.
// The spaces are necessary since the tokenizer will first split the emoticons up and then join them together
// with spaces again.
var RE_EMOTICONS = [];
// for each emoticon string
_.pluck(_.values(EMOTICONS), "e").forEach(function (es) {
es.forEach(function (e) {
// split up into separate characters + escape each character + join back together in a string with " ?" in between.
// " ?" ::= optional space
RE_EMOTICONS.push(e.split("").map(escapeRegExp).join(" ?"));
});
});
// join all emoticons together with an "r" and create regexp
RE_EMOTICONS = new RegExp("(" + RE_EMOTICONS.join("|") + ")", "gi");
// All characters that could express punctuation
var PUNCTUATION = ".,;:!?()[]{}`'\"@#$^&*+-|=~_";
// Most of the abbreviations
var ABBREVIATIONS = [
"a.", "adj.", "adv.", "al.", "a.m.", "art.", "c.", "capt.", "cert.", "cf.", "col.", "Col.",
"comp.", "conf.", "def.", "Dep.", "Dept.", "Dr.", "dr.", "ed.", "e.g.", "esp.", "etc.", "ex.",
"f.", "fig.", "gen.", "id.", "i.e.", "int.", "l.", "m.", "Med.", "Mil.", "Mr.", "n.", "n.q.",
"orig.", "pl.", "pred.", "pres.", "p.m.", "ref.", "v.", "vs.", "w/"
];
// RegExp that captures abbreviations
var RE_ABBR1 = new RegExp(/^[A-Za-z]\.$/g); // single letter, "T. De Smedt"
var RE_ABBR2 = new RegExp(/^([A-Za-z]\.)+$/g); // alternating letters, "U.S."
var RE_ABBR3 = new RegExp(/^[A-Z][b|c|d|f|g|h|j|k|l|m|n|p|q|r|s|t|v|w|x|z]+.$/g); // capital followed by consonants, "Mr."
// RegExp that captures the sarcasm sign: "(!)"
var RE_SARCASM = /\( ?\! ?\)/g;
// Common contractions to separate
var REPLACEMENTS = {
"'d": " 'd",
"'m": " 'm",
"'s": " 's",
"'ll": " 'll",
"'re": " 're",
"'ve": " 've",
"n't": " n't"
};
// Paragraph line breaks (\n\n marks end of sentence).
var EOS = "END-OF-SENTENCE";
// Returns a list of sentences. Each sentence is a space-separated string of tokens (words).
// Handles common cases of abbreviations (e.g., etc., ...).
// Punctuation marks are split from other words. Periods (or ?!) mark the end of a sentence.
// Headings without an ending period are inferred by line breaks.
function find_tokens(string, punctuation, abbreviations, replace, linebreak) {
linebreak = def(linebreak, /n{2,}/g);
punctuation = def(punctuation, PUNCTUATION).replace(/\./g, "").split("");
abbreviations = def(abbreviations, ABBREVIATIONS);
replace = def(replace, REPLACEMENTS);
// Handle replacements (contractions)
Object.keys(replace).forEach(function (r) {
string = string.replace(new RegExp(r, "g"), replace[r]);
});
// Handle Unicode quotes.
string = string.replace(new RegExp("“", "g"), " “ ");
string = string.replace(new RegExp("”", "g"), " ” ");
string = string.replace(new RegExp("‘", "g"), " ‘ ");
string = string.replace(new RegExp("’", "g"), " ’ ");
// Collapse whitespace.
string = string.replace(new RegExp("\r\n", "g"), "\n");
string = string.replace(new RegExp(linebreak), " " + EOS + " ");
string = string.replace(new RegExp(/\s+/g), " ");
var tokens = [];
// Start parsing the tokens
(string + " ").split(RE_TOKEN).forEach(function (t) {
// Ignore ''
if (t.length > 0) {
var tail = [];
// Handle punctuation marks: tokens will possibly have leading or trailing punctuation.
// Also handles for example ("good:-)" -> ["good", ":-)"]
while (startsWithAny(t, punctuation) && (! _str.include(replace, t))) {
// Split leading punctuation.
if (startsWithAny(t, punctuation)) {
tokens.push(t[0]);
t = t.slice(1);
}
}
while (endsWithAny(t, punctuation.concat(["."])) && (! _str.include(replace, t))) {
// Split trailing punctuation and make token of it by pushing onto tail.
if (endsWithAny(t, punctuation)) {
tail.push(t[t.length-1]);
t = t.slice(0, t.length-1);
}
// Split ellipsis (...) before splitting period.
if (_str.endsWith(t, "...")) {
tail.push("...");
t = _str.rstrip(t.slice(0, t.length-3), ".");
}
// Split period (if not an abbreviation).
if (_str.endsWith(t, ".")) {
if (_.include(abbreviations, t) ||
RE_ABBR1.test(t) ||
RE_ABBR2.test(t) ||
RE_ABBR3.test(t)) {
break;
// Single period encountered at the end, split off
} else {
tail.push(t[t.length-1]);
t = t.slice(0, t.length-1);
}
}
}
// After parsing all trailing punctuations, push remaining word as token
if (t !== "") {
tokens.push(t);
}
// And add the trailing punctuation as token to.
tokens = tokens.concat(tail.reverse());
}
});
// Handle sentence breaks (periods, quotes, parenthesis).
var sentences = [[]];
var i = 0;
var j = 0;
while (j < tokens.length) {
// If end of sentence token
if (_.include(["...", ".", "!", "?", EOS], tokens[j])) {
// Collapse mutliple eos tokens + handle quote text " "
while (j < tokens.length &&
_.include(["'", "\"", "”", "’", "...", ".", "!", "?", ")", EOS], tokens[j])) {
if (_.include(["'", "\""], tokens[j]) &&
count(sentences[sentences.length-1], tokens[j]) % 2 === 0) {
break;
}
j++;
}
// Push all tokens up to end of sentence onto the last sentence entry
for (var k = i; k<j; k++) {
if (tokens[k] !== EOS) {
sentences[sentences.length-1] = sentences[sentences.length-1].concat(tokens[k]);
}
}
// Start new sentence entry
sentences.push([]);
i = j;
}
j++;
}
// Push remaining tokens onto last sentence
for (var l = i; l < j; l++) {
sentences[sentences.length-1] = sentences[sentences.length-1].concat(tokens[l]);
}
// Join each sentence with white-spaces and filter out zero-length sentences.
sentences = _.compact(sentences.map(function (s) {
return ((s.length > 0) ? s.join(" ") : false);
}));
// Join white-spaced sarcasm
sentences = sentences.map(function (s) {
return s.replace(RE_SARCASM, "(!)");
});
// Join white-spaced emoticons.
sentences = sentences.map(function (s) {
return s.replace(RE_EMOTICONS, function (a) {
return a.replace(/\ /g, "");
});
});
return sentences;
}
/*
* Auxiliary Functions
*/
// Occurrence count of element in vector
function count(vector, el) {
var ctr = 0;
vector.forEach(function (e) { if (el === e) { ctr++; }});
return ctr;
}
// True if the string starts with any element of given vector
function startsWithAny(string, vector) {
return _.any(vector.map(function (e) { return _str.startsWith(string, e); }));
}
// True if the string ends with any element of given vector
function endsWithAny(string, vector) {
return _.any(vector.map(function (e) { return _str.endsWith(string, e); }));
}
// Returns value if value is defined, otherwise defValue.
function def(value, defValue) {
if (_.isUndefined(value)) {
return defValue;
}
return value;
}
module.exports = find_tokens;