DbCreator.js 10 KB

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