From 1e3f93d68ee5db3739084039be57279c14d0a62b Mon Sep 17 00:00:00 2001 From: syuilo Date: Thu, 7 Sep 2017 14:52:07 +0900 Subject: [PATCH] :v: --- src/tools/analysis/core.ts | 4 +- src/tools/analysis/extract-user-keywords.ts | 59 +++++++++++--- src/tools/analysis/mecab.js | 85 +++++++++++++++++++++ 3 files changed, 135 insertions(+), 13 deletions(-) create mode 100644 src/tools/analysis/mecab.js diff --git a/src/tools/analysis/core.ts b/src/tools/analysis/core.ts index 5dcce2626..20e5fa6c5 100644 --- a/src/tools/analysis/core.ts +++ b/src/tools/analysis/core.ts @@ -1,8 +1,7 @@ const bayes = require('./naive-bayes.js'); -const MeCab = require('mecab-async'); +const MeCab = require('./mecab'); import Post from '../../api/models/post'; -import config from '../../conf'; /** * 投稿を学習したり与えられた投稿のカテゴリを予測します @@ -13,7 +12,6 @@ export default class Categorizer { constructor() { this.mecab = new MeCab(); - if (config.categorizer.mecab_command) this.mecab.command = config.categorizer.mecab_command; // BIND ----------------------------------- this.tokenizer = this.tokenizer.bind(this); diff --git a/src/tools/analysis/extract-user-keywords.ts b/src/tools/analysis/extract-user-keywords.ts index 9ef778439..5251a0d1d 100644 --- a/src/tools/analysis/extract-user-keywords.ts +++ b/src/tools/analysis/extract-user-keywords.ts @@ -1,18 +1,56 @@ -const MeCab = require('mecab-async'); - +const MeCab = require('./mecab'); import Post from '../../api/models/post'; import User from '../../api/models/user'; -import config from '../../conf'; +import parse from '../../api/common/text'; + +const stopwords = [ + 'ー', + + 'の', 'に', 'は', 'を', 'た', 'が', 'で', 'て', 'と', 'し', 'れ', 'さ', + 'ある', 'いる', 'も', 'する', 'から', 'な', 'こと', 'として', 'い', 'や', 'れる', + 'など', 'なっ', 'ない', 'この', 'ため', 'その', 'あっ', 'よう', 'また', 'もの', + 'という', 'あり', 'まで', 'られ', 'なる', 'へ', 'か', 'だ', 'これ', 'によって', + 'により', 'おり', 'より', 'による', 'ず', 'なり', 'られる', 'において', 'ば', 'なかっ', + 'なく', 'しかし', 'について', 'せ', 'だっ', 'その後', 'できる', 'それ', 'う', 'ので', + 'なお', 'のみ', 'でき', 'き', 'つ', 'における', 'および', 'いう', 'さらに', 'でも', + 'ら', 'たり', 'その他', 'に関する', 'たち', 'ます', 'ん', 'なら', 'に対して', '特に', + 'せる', '及び', 'これら', 'とき', 'では', 'にて', 'ほか', 'ながら', 'うち', 'そして', + 'とともに', 'ただし', 'かつて', 'それぞれ', 'または', 'お', 'ほど', 'ものの', 'に対する', + 'ほとんど', 'と共に', 'といった', 'です', 'とも', 'ところ', 'ここ', '感じ', '気持ち', + + 'about', 'after', 'all', 'also', 'am', 'an', 'and', 'another', 'any', 'are', 'as', 'at', 'be', + 'because', 'been', 'before', 'being', 'between', 'both', 'but', 'by', 'came', 'can', + 'come', 'could', 'did', 'do', 'each', 'for', 'from', 'get', 'got', 'has', 'had', + 'he', 'have', 'her', 'here', 'him', 'himself', 'his', 'how', 'if', 'in', 'into', + 'is', 'it', 'like', 'make', 'many', 'me', 'might', 'more', 'most', 'much', 'must', + 'my', 'never', 'now', 'of', 'on', 'only', 'or', 'other', 'our', 'out', 'over', + 'said', 'same', 'see', 'should', 'since', 'some', 'still', 'such', 'take', 'than', + 'that', 'the', 'their', 'them', 'then', 'there', 'these', 'they', 'this', 'those', + 'through', 'to', 'too', 'under', 'up', 'very', 'was', 'way', 'we', 'well', 'were', + 'what', 'where', 'which', 'while', 'who', 'with', 'would', 'you', 'your', 'a', 'i' +]; const mecab = new MeCab(); -if (config.analysis.mecab_command) mecab.command = config.analysis.mecab_command; function tokenize(text: string) { - const tokens = mecab.parseSync(text) + if (text == null) return []; + + // パース + const ast = parse(text); + + const plain = ast + // テキストのみ(URLなどを除外するという意) + .filter(t => t.type == 'text' || t.type == 'bold') + .map(t => t.content) + .join(''); + + const tokens = mecab.parseSync(plain) // キーワードのみ .filter(token => token[1] == '名詞' && (token[2] == '固有名詞' || token[2] == '一般')) // 取り出し - .map(token => token[0]); + .map(token => token[0].toLowerCase()) + // ストップワード + .filter(word => stopwords.indexOf(word) === -1 && word.length > 1); return tokens; } @@ -36,7 +74,7 @@ User.find({}, { }); async function extractKeywordsOne(id, cb) { - console.log(`extracting keywords of ${id} ...`); + process.stdout.write(`extracting keywords of ${id} ...`); // Fetch recent posts const recentPosts = await Post.find({ @@ -48,7 +86,7 @@ async function extractKeywordsOne(id, cb) { sort: { _id: -1 }, - limit: 1000, + limit: 10000, fields: { _id: false, text: true @@ -56,7 +94,8 @@ async function extractKeywordsOne(id, cb) { }); // 投稿が少なかったら中断 - if (recentPosts.length < 10) { + if (recentPosts.length < 300) { + process.stdout.write(' >>> -\n'); return cb(); } @@ -81,7 +120,7 @@ async function extractKeywordsOne(id, cb) { // Lookup top 10 keywords const topKeywords = keywordsSorted.slice(0, 10); - process.stdout.write(' >>> ' + topKeywords.join(' ')); + process.stdout.write(' >>> ' + topKeywords.join(', ') + '\n'); // Save User.update({ _id: id }, { diff --git a/src/tools/analysis/mecab.js b/src/tools/analysis/mecab.js new file mode 100644 index 000000000..82f7d6d52 --- /dev/null +++ b/src/tools/analysis/mecab.js @@ -0,0 +1,85 @@ +// Original source code: https://github.com/hecomi/node-mecab-async +// CUSTOMIZED BY SYUILO + +var exec = require('child_process').exec; +var execSync = require('child_process').execSync; +var sq = require('shell-quote'); + +const config = require('../../conf').default; + +// for backward compatibility +var MeCab = function() {}; + +MeCab.prototype = { + command : config.analysis.mecab_command ? config.analysis.mecab_command : 'mecab', + _format: function(arrayResult) { + var result = []; + if (!arrayResult) { return result; } + // Reference: http://mecab.googlecode.com/svn/trunk/mecab/doc/index.html + // 表層形\t品詞,品詞細分類1,品詞細分類2,品詞細分類3,活用形,活用型,原形,読み,発音 + arrayResult.forEach(function(parsed) { + if (parsed.length <= 8) { return; } + result.push({ + kanji : parsed[0], + lexical : parsed[1], + compound : parsed[2], + compound2 : parsed[3], + compound3 : parsed[4], + conjugation : parsed[5], + inflection : parsed[6], + original : parsed[7], + reading : parsed[8], + pronunciation : parsed[9] || '' + }); + }); + return result; + }, + _shellCommand : function(str) { + return sq.quote(['echo', str]) + ' | ' + this.command; + }, + _parseMeCabResult : function(result) { + return result.split('\n').map(function(line) { + return line.replace('\t', ',').split(','); + }); + }, + parse : function(str, callback) { + process.nextTick(function() { // for bug + exec(MeCab._shellCommand(str), function(err, result) { + if (err) { return callback(err); } + callback(err, MeCab._parseMeCabResult(result).slice(0,-2)); + }); + }); + }, + parseSync : function(str) { + var result = execSync(MeCab._shellCommand(str)); + return MeCab._parseMeCabResult(String(result)).slice(0, -2); + }, + parseFormat : function(str, callback) { + MeCab.parse(str, function(err, result) { + if (err) { return callback(err); } + callback(err, MeCab._format(result)); + }); + }, + parseSyncFormat : function(str) { + return MeCab._format(MeCab.parseSync(str)); + }, + _wakatsu : function(arr) { + return arr.map(function(data) { return data[0]; }); + }, + wakachi : function(str, callback) { + MeCab.parse(str, function(err, arr) { + if (err) { return callback(err); } + callback(null, MeCab._wakatsu(arr)); + }); + }, + wakachiSync : function(str) { + var arr = MeCab.parseSync(str); + return MeCab._wakatsu(arr); + } +}; + +for (var x in MeCab.prototype) { + MeCab[x] = MeCab.prototype[x]; +} + +module.exports = MeCab;