DbCreator.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. const InpxParser = require('./InpxParser');
  2. const utils = require('./utils');
  3. class DbCreator {
  4. constructor(config) {
  5. this.config = config;
  6. }
  7. async run(db, callback) {
  8. const config = this.config;
  9. //book
  10. await db.create({
  11. table: 'book'
  12. });
  13. callback({job: 'load inpx', jobMessage: 'Загрузка INPX'});
  14. const readFileCallback = async(readState) => {
  15. callback(readState);
  16. };
  17. //поисковые таблицы, ниже сохраним в БД
  18. let authorMap = new Map();//авторы
  19. let authorArr = [];
  20. let seriesMap = new Map();//серии
  21. let seriesArr = [];
  22. let titleMap = new Map();//названия
  23. let titleArr = [];
  24. let genreMap = new Map();//жанры
  25. let genreArr = [];
  26. let langMap = new Map();//языки
  27. let langArr = [];
  28. //stats
  29. let authorCount = 0;
  30. let bookCount = 0;
  31. let noAuthorBookCount = 0;
  32. let bookDelCount = 0;
  33. //stuff
  34. let recsLoaded = 0;
  35. let id = 0;
  36. let chunkNum = 0;
  37. const splitAuthor = (author) => {
  38. if (!author) {
  39. author = 'Автор не указан';
  40. }
  41. const result = author.split(',');
  42. if (result.length > 1)
  43. result.push(author);
  44. return result;
  45. }
  46. const parsedCallback = async(chunk) => {
  47. for (const rec of chunk) {
  48. rec.id = ++id;
  49. if (!rec.del) {
  50. bookCount++;
  51. if (!rec.author)
  52. noAuthorBookCount++;
  53. } else {
  54. bookDelCount++;
  55. }
  56. //авторы
  57. const author = splitAuthor(rec.author);
  58. for (let i = 0; i < author.length; i++) {
  59. const a = author[i];
  60. const value = a.toLowerCase();
  61. let authorRec;
  62. if (authorMap.has(value)) {
  63. const authorTmpId = authorMap.get(value);
  64. authorRec = authorArr[authorTmpId];
  65. } else {
  66. authorRec = {tmpId: authorArr.length, author: a, value, bookId: []};
  67. authorArr.push(authorRec);
  68. authorMap.set(value, authorRec.tmpId);
  69. if (author.length == 1 || i < author.length - 1) //без соавторов
  70. authorCount++;
  71. }
  72. authorRec.bookId.push(id);
  73. }
  74. }
  75. await db.insert({table: 'book', rows: chunk});
  76. recsLoaded += chunk.length;
  77. callback({recsLoaded});
  78. if (chunkNum++ % 10 == 0)
  79. utils.freeMemory();
  80. };
  81. //парсинг 1
  82. const parser = new InpxParser();
  83. await parser.parse(config.inpxFile, readFileCallback, parsedCallback);
  84. utils.freeMemory();
  85. //отсортируем авторов и выдадим им правильные id
  86. //порядок id соответствует ASC-сортировке по author.toLowerCase
  87. callback({job: 'author sort', jobMessage: 'Сортировка'});
  88. authorArr.sort((a, b) => a.value.localeCompare(b.value));
  89. id = 0;
  90. authorMap = new Map();
  91. for (const authorRec of authorArr) {
  92. authorRec.id = ++id;
  93. authorMap.set(authorRec.author, id);
  94. delete authorRec.tmpId;
  95. }
  96. utils.freeMemory();
  97. //теперь можно создавать остальные поисковые таблицы
  98. const parseField = (fieldValue, fieldMap, fieldArr, authorIds) => {
  99. if (fieldValue) {
  100. const value = fieldValue.toLowerCase();
  101. let fieldRec;
  102. if (fieldMap.has(value)) {
  103. const fieldId = fieldMap.get(value);
  104. fieldRec = fieldArr[fieldId];
  105. } else {
  106. fieldRec = {id: fieldArr.length, value, authorId: new Set()};
  107. fieldArr.push(fieldRec);
  108. fieldMap.set(value, fieldRec.id);
  109. }
  110. for (const id of authorIds) {
  111. fieldRec.authorId.add(id);
  112. }
  113. }
  114. };
  115. const parseBookRec = (rec) => {
  116. //авторы
  117. const author = splitAuthor(rec.author);
  118. const authorIds = [];
  119. for (const a of author) {
  120. const authorId = authorMap.get(a);
  121. if (!authorId) //подстраховка
  122. continue;
  123. authorIds.push(authorId);
  124. }
  125. //серии
  126. parseField(rec.series, seriesMap, seriesArr, authorIds);
  127. //названия
  128. parseField(rec.title, titleMap, titleArr, authorIds);
  129. //жанры
  130. if (rec.genre) {
  131. const genre = rec.genre.split(',');
  132. for (const g of genre) {
  133. let genreRec;
  134. if (genreMap.has(g)) {
  135. const genreId = genreMap.get(g);
  136. genreRec = genreArr[genreId];
  137. } else {
  138. genreRec = {id: genreArr.length, value: g, authorId: new Set()};
  139. genreArr.push(genreRec);
  140. genreMap.set(g, genreRec.id);
  141. }
  142. for (const id of authorIds) {
  143. genreRec.authorId.add(id);
  144. }
  145. }
  146. }
  147. //языки
  148. parseField(rec.lang, langMap, langArr, authorIds);
  149. }
  150. callback({job: 'search tables create', jobMessage: 'Создание поисковых таблиц'});
  151. //парсинг 2
  152. while (1) {// eslint-disable-line
  153. //пробегаемся по сохраненным книгам
  154. const rows = await db.select({
  155. table: 'book',
  156. where: `
  157. let iter = @getItem('book_parsing');
  158. if (!iter) {
  159. iter = @all();
  160. @setItem('book_parsing', iter);
  161. }
  162. const ids = new Set();
  163. let id = iter.next();
  164. while (!id.done && ids.size < 10000) {
  165. ids.add(id.value);
  166. id = iter.next();
  167. }
  168. return ids;
  169. `
  170. });
  171. if (rows.length) {
  172. for (const rec of rows)
  173. parseBookRec(rec);
  174. } else {
  175. break;
  176. }
  177. }
  178. //чистка памяти, ибо жрет как не в себя
  179. authorMap = null;
  180. seriesMap = null;
  181. titleMap = null;
  182. genreMap = null;
  183. for (let i = 0; i < 3; i++) {
  184. utils.freeMemory();
  185. await utils.sleep(1000);
  186. }
  187. //config
  188. callback({job: 'config save', jobMessage: 'Сохранение конфигурации'});
  189. await db.create({
  190. table: 'config'
  191. });
  192. const stats = {
  193. recsLoaded,
  194. authorCount,
  195. authorCountAll: authorArr.length,
  196. bookCount,
  197. bookCountAll: bookCount + bookDelCount,
  198. bookDelCount,
  199. noAuthorBookCount,
  200. titleCount: titleArr.length,
  201. seriesCount: seriesArr.length,
  202. genreCount: genreArr.length,
  203. langCount: langArr.length,
  204. };
  205. //console.log(stats);
  206. const inpxHash = await utils.getFileHash(config.inpxFile, 'sha256', 'hex');
  207. await db.insert({table: 'config', rows: [
  208. {id: 'inpxInfo', value: parser.info},
  209. {id: 'stats', value: stats},
  210. {id: 'inpxHash', value: inpxHash},
  211. ]});
  212. //сохраним поисковые таблицы
  213. const chunkSize = 10000;
  214. const saveTable = async(table, arr, nullArr, authorIdToArray = true) => {
  215. arr.sort((a, b) => a.value.localeCompare(b.value));
  216. await db.create({
  217. table,
  218. index: {field: 'value', unique: true, depth: 1000000},
  219. });
  220. //вставка в БД по кусочкам, экономим память
  221. for (let i = 0; i < arr.length; i += chunkSize) {
  222. const chunk = arr.slice(i, i + chunkSize);
  223. if (authorIdToArray) {
  224. for (const rec of chunk)
  225. rec.authorId = Array.from(rec.authorId);
  226. }
  227. await db.insert({table, rows: chunk});
  228. await utils.sleep(100);
  229. }
  230. nullArr();
  231. await db.close({table});
  232. utils.freeMemory();
  233. };
  234. //author
  235. callback({job: 'author save', jobMessage: 'Сохранение авторов книг'});
  236. await saveTable('author', authorArr, () => {authorArr = null}, false);
  237. //series
  238. callback({job: 'series save', jobMessage: 'Сохранение серий книг'});
  239. await saveTable('series', seriesArr, () => {seriesArr = null});
  240. //title
  241. callback({job: 'title save', jobMessage: 'Сохранение названий книг'});
  242. await saveTable('title', titleArr, () => {titleArr = null});
  243. //genre
  244. callback({job: 'genre save', jobMessage: 'Сохранение жанров'});
  245. await saveTable('genre', genreArr, () => {genreArr = null});
  246. //lang
  247. callback({job: 'lang save', jobMessage: 'Сохранение языков'});
  248. await saveTable('lang', langArr, () => {langArr = null});
  249. //кэш-таблицы запросов
  250. await db.create({table: 'query_cache'});
  251. await db.create({table: 'query_time'});
  252. callback({job: 'done', jobMessage: ''});
  253. }
  254. }
  255. module.exports = DbCreator;