DbCreator.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  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 parsedCallback = async(chunk) => {
  38. for (const rec of chunk) {
  39. rec.id = ++id;
  40. if (!rec.del)
  41. bookCount++;
  42. else
  43. bookDelCount++;
  44. if (!rec.author) {
  45. if (!rec.del)
  46. noAuthorBookCount++;
  47. rec.author = 'Автор не указан';
  48. }
  49. //авторы
  50. const author = rec.author.split(',');
  51. if (author.length > 1)
  52. author.push(rec.author);
  53. for (let i = 0; i < author.length; i++) {
  54. const a = author[i];
  55. let authorRec;
  56. if (authorMap.has(a)) {
  57. const authorTmpId = authorMap.get(a);
  58. authorRec = authorArr[authorTmpId];
  59. } else {
  60. authorRec = {tmpId: authorArr.length, author: a, value: a.toLowerCase(), bookId: []};
  61. authorArr.push(authorRec);
  62. authorMap.set(a, authorRec.tmpId);
  63. if (author.length == 1 || i < author.length - 1) //без соавторов
  64. authorCount++;
  65. }
  66. authorRec.bookId.push(id);
  67. }
  68. }
  69. await db.insert({table: 'book', rows: chunk});
  70. recsLoaded += chunk.length;
  71. callback({recsLoaded});
  72. if (chunkNum++ % 10 == 0)
  73. utils.freeMemory();
  74. };
  75. //парсинг 1
  76. const parser = new InpxParser();
  77. await parser.parse(config.inpxFile, readFileCallback, parsedCallback);
  78. utils.freeMemory();
  79. //отсортируем авторов и выдадим им правильные id
  80. //порядок id соответствует ASC-сортировке по author.toLowerCase
  81. callback({job: 'author sort', jobMessage: 'Сортировка'});
  82. authorArr.sort((a, b) => a.value.localeCompare(b.value));
  83. id = 0;
  84. authorMap = new Map();
  85. for (const authorRec of authorArr) {
  86. authorRec.id = ++id;
  87. authorMap.set(authorRec.author, id);
  88. delete authorRec.tmpId;
  89. }
  90. utils.freeMemory();
  91. //теперь можно создавать остальные поисковые таблицы
  92. const parseBookRec = (rec) => {
  93. //авторы
  94. if (!rec.author) {
  95. if (!rec.del)
  96. noAuthorBookCount++;
  97. rec.author = 'Автор не указан';
  98. }
  99. const author = rec.author.split(',');
  100. if (author.length > 1)
  101. author.push(rec.author);
  102. const authorIds = [];
  103. for (const a of author) {
  104. const authorId = authorMap.get(a);
  105. if (!authorId) //подстраховка
  106. continue;
  107. authorIds.push(authorId);
  108. }
  109. //серии
  110. if (rec.series) {
  111. const series = rec.series;
  112. let seriesRec;
  113. if (seriesMap.has(series)) {
  114. const seriesId = seriesMap.get(series);
  115. seriesRec = seriesArr[seriesId];
  116. } else {
  117. seriesRec = {id: seriesArr.length, value: series.toLowerCase(), authorId: new Set()};
  118. seriesArr.push(seriesRec);
  119. seriesMap.set(series, seriesRec.id);
  120. }
  121. for (const id of authorIds) {
  122. seriesRec.authorId.add(id);
  123. }
  124. }
  125. //названия
  126. if (rec.title) {
  127. const title = rec.title;
  128. let titleRec;
  129. if (titleMap.has(title)) {
  130. const titleId = titleMap.get(title);
  131. titleRec = titleArr[titleId];
  132. } else {
  133. titleRec = {id: titleArr.length, value: title.toLowerCase(), authorId: new Set()};
  134. titleArr.push(titleRec);
  135. titleMap.set(title, titleRec.id);
  136. }
  137. for (const id of authorIds) {
  138. titleRec.authorId.add(id);
  139. }
  140. }
  141. //жанры
  142. if (rec.genre) {
  143. const genre = rec.genre.split(',');
  144. for (const g of genre) {
  145. let genreRec;
  146. if (genreMap.has(g)) {
  147. const genreId = genreMap.get(g);
  148. genreRec = genreArr[genreId];
  149. } else {
  150. genreRec = {id: genreArr.length, value: g, authorId: new Set()};
  151. genreArr.push(genreRec);
  152. genreMap.set(g, genreRec.id);
  153. }
  154. for (const id of authorIds) {
  155. genreRec.authorId.add(id);
  156. }
  157. }
  158. }
  159. //языки
  160. if (rec.lang) {
  161. const lang = rec.lang;
  162. let langRec;
  163. if (langMap.has(lang)) {
  164. const langId = langMap.get(lang);
  165. langRec = langArr[langId];
  166. } else {
  167. langRec = {id: langArr.length, value: lang, authorId: new Set()};
  168. langArr.push(langRec);
  169. langMap.set(lang, langRec.id);
  170. }
  171. for (const id of authorIds) {
  172. langRec.authorId.add(id);
  173. }
  174. }
  175. }
  176. callback({job: 'search tables create', jobMessage: 'Создание поисковых таблиц'});
  177. //парсинг 2
  178. while (1) {// eslint-disable-line
  179. //пробегаемся по сохраненным книгам
  180. const rows = await db.select({
  181. table: 'book',
  182. where: `
  183. let iter = @getItem('book_parsing');
  184. if (!iter) {
  185. iter = @all();
  186. @setItem('book_parsing', iter);
  187. }
  188. const ids = new Set();
  189. let id = iter.next();
  190. while (!id.done && ids.size < 10000) {
  191. ids.add(id.value);
  192. id = iter.next();
  193. }
  194. return ids;
  195. `
  196. });
  197. if (rows.length) {
  198. for (const rec of rows)
  199. parseBookRec(rec);
  200. } else {
  201. break;
  202. }
  203. }
  204. //чистка памяти, ибо жрет как не в себя
  205. authorMap = null;
  206. seriesMap = null;
  207. titleMap = null;
  208. genreMap = null;
  209. for (let i = 0; i < 3; i++) {
  210. utils.freeMemory();
  211. await utils.sleep(1000);
  212. }
  213. //config
  214. callback({job: 'config save', jobMessage: 'Сохранение конфигурации'});
  215. await db.create({
  216. table: 'config'
  217. });
  218. const stats = {
  219. recsLoaded,
  220. authorCount,
  221. authorCountAll: authorArr.length,
  222. bookCount,
  223. bookCountAll: bookCount + bookDelCount,
  224. bookDelCount,
  225. noAuthorBookCount,
  226. titleCount: titleArr.length,
  227. seriesCount: seriesArr.length,
  228. genreCount: genreArr.length,
  229. langCount: langArr.length,
  230. };
  231. //console.log(stats);
  232. const inpxHash = await utils.getFileHash(config.inpxFile, 'sha256', 'hex');
  233. await db.insert({table: 'config', rows: [
  234. {id: 'inpxInfo', value: parser.info},
  235. {id: 'stats', value: stats},
  236. {id: 'inpxHash', value: inpxHash},
  237. ]});
  238. //сохраним поисковые таблицы
  239. const chunkSize = 10000;
  240. //author
  241. callback({job: 'author save', jobMessage: 'Сохранение авторов книг'});
  242. await db.create({
  243. table: 'author',
  244. index: {field: 'value', depth: config.indexDepth},
  245. });
  246. //вставка в БД по кусочкам, экономим память
  247. for (let i = 0; i < authorArr.length; i += chunkSize) {
  248. const chunk = authorArr.slice(i, i + chunkSize);
  249. await db.insert({table: 'author', rows: chunk});
  250. }
  251. authorArr = null;
  252. await db.close({table: 'author'});
  253. utils.freeMemory();
  254. //series
  255. callback({job: 'series save', jobMessage: 'Сохранение серий книг'});
  256. await db.create({
  257. table: 'series',
  258. index: {field: 'value', depth: config.indexDepth},
  259. });
  260. //вставка в БД по кусочкам, экономим память
  261. for (let i = 0; i < seriesArr.length; i += chunkSize) {
  262. const chunk = seriesArr.slice(i, i + chunkSize);
  263. for (const rec of chunk)
  264. rec.authorId = Array.from(rec.authorId);
  265. await db.insert({table: 'series', rows: chunk});
  266. }
  267. seriesArr = null;
  268. await db.close({table: 'series'});
  269. utils.freeMemory();
  270. //title
  271. callback({job: 'title save', jobMessage: 'Сохранение названий книг'});
  272. await db.create({
  273. table: 'title',
  274. index: {field: 'value', depth: config.indexDepth},
  275. });
  276. //вставка в БД по кусочкам, экономим память
  277. let j = 0;
  278. for (let i = 0; i < titleArr.length; i += chunkSize) {
  279. const chunk = titleArr.slice(i, i + chunkSize);
  280. for (const rec of chunk)
  281. rec.authorId = Array.from(rec.authorId);
  282. await db.insert({table: 'title', rows: chunk});
  283. if (j++ % 10 == 0)
  284. utils.freeMemory();
  285. await utils.sleep(100);
  286. }
  287. titleArr = null;
  288. await db.close({table: 'title'});
  289. utils.freeMemory();
  290. //genre
  291. callback({job: 'genre save', jobMessage: 'Сохранение жанров'});
  292. await db.create({
  293. table: 'genre',
  294. index: {field: 'value', depth: config.indexDepth},
  295. });
  296. await db.insert({table: 'genre', rows: genreArr});
  297. genreArr = null;
  298. await db.close({table: 'genre'});
  299. utils.freeMemory();
  300. //lang
  301. callback({job: 'lang save', jobMessage: 'Сохранение языков'});
  302. await db.create({
  303. table: 'lang',
  304. index: {field: 'value', depth: config.indexDepth},
  305. });
  306. await db.insert({table: 'lang', rows: langArr});
  307. langArr = null;
  308. await db.close({table: 'lang'});
  309. utils.freeMemory();
  310. //кэш-таблицы
  311. callback({job: 'done', jobMessage: ''});
  312. }
  313. }
  314. module.exports = DbCreator;