arka triymfalnaya

генерируем xlsx из rss фида

нагулял задачу генерации таблиц из rss фида, парсеры выходят на арену. план такой: из терминала кормим скрипт набором аргументов с линком к rss фиду и его настройками, добавляем прогресс бары для отзывчивости, на выход отдаем сгенерированную таблицу.

сперва набросаем пачку зависимостей:

yarn add rss-parser
yarn add --dev exceljs moment posthtml progress request request-promise yargs

за дело! импортирую переменные и определяю парсер:

const ExcelJS = require("exceljs");
const Parser = require("rss-parser");
const posthtml = require("posthtml");
const rp = require("request-promise");
const moment = require("moment");
const ProgressBar = require("progress");
const yargs = require("yargs");

const parser = new Parser();

описываю обязательные и не очень аргументы, определяю входную функцию и на ходу рассказываю о прогрессе в терминал:

const options = yargs
	.usage(`Usage: -f <rss uri>`)
	.option("f", {
		alias: "feed",
		describe: "RSS feed uri",
		type: "string",
		demandOption: true,
	})
	.option("a", {
		alias: "amount",
		describe: "Needed RSS feed posts amount",
		type: "string",
	})
	.option("n", {
		alias: "outputFileName",
		describe: "XLS output file name",
		type: "string",
	})
	.option("o", {
		alias: "cellOptions",
		describe: "Sheet cell additional options",
		type: "array",
	}).argv;

process.stdout.write(`great options, bruh, let's start already!\n`);

entry(
	options.feed,
	options.amount,
	options.outputFileName,
	options.cellOptions,
);

главная функция будет принимать на вход обязательный линк на фид и необязательные количество постов, имя таблицы на выходе и набор кастомных настроек для ячеек. назначаю нужные столбцы и их ключи:

let entry = async (
	rssFeed,
	amount = 5,
	outputFileName = "result",
	cellOptions = [],
) => {
	process.stdout.write(`parsing your rss feed...\n`);
	let feed = await parser.parseURL(rssFeed);

	process.stdout.write(`creating excel workbook...\n`);
	const workbook = new ExcelJS.Workbook();
	const worksheet = workbook.addWorksheet(outputFileName);
	worksheet.columns = [
		{
			header: "text",
			key: "col_text",
		},
		{
			header: "url",
			key: "col_url",
		},
		{
			header: "images",
			key: "col_images",
		},
		{
			header: "time",
			key: "col_time",
		},
	];
};

приступаю к генерации новых строк и добавляю тикающий прогресс бар:

process.stdout.write(`generating posts from rss feed...\n`);
let generatedRows = await generatePostsMetaFromFeed(feed, amount);
let generatedRowsBar = new ProgressBar(
	"[:bar] :current/:total table rows generated\n",
	{
		incomplete: " ",
		complete: "#",
		total: generatedRows.length,
	},
);

функция generatePostsMetaFromFeed займется пирсингом элементов фида и генерацией набора с нужными таблице полями:

let convertFeedToPosts = (feed) => [...feed.items.map((item) => item.link)]; // для пагинации по страницам фида понадобятся линки

let generatePostsMetaFromFeed = async (feed, amount) => {
	let res = [];

	let posts = [];
	let feedLink = feed.link;

	if (amount > 10) {
		process.stdout.write(`wow, so much posts? taking care of it...\n`);
		let pages = Math.round(amount / 10); // пагинация для доступа к последующим страницам фида
		let pagesLoadingBar = new ProgressBar(
			"[:bar] :current/:total processed\n",
			{
				incomplete: " ",
				complete: "#",
				total: pages,
			},
		);

		posts.push(...convertFeedToPosts(feed));

		process.stdout.write(`loading needed pages...\n`);
		for (let i = 2; i <= pages; i++) {
			await rp(encodeURI(`${feedLink}?feed=rss&paged=${i}`))
				.then(async (rssPage) => {
					let parsedRSSFeed = await parser.parseString(rssPage);
					let isLastPage = i === pages;

					if (isLastPage) {
						let modItems = parsedRSSFeed.items.filter(
							(_, index) => index < amount % 10,
						);

						posts.push(
							...convertFeedToPosts({
								items: modItems,
							}),
						);
					} else {
						posts.push(...convertFeedToPosts(parsedRSSFeed));
					}

					pagesLoadingBar.tick();
				})
				.catch((err) => {
					console.error("huh, rss pagination failed", err.code);
				});
		}
	} else {
		process.stdout.write(`not a lot of posts, gonna be quick!\n`);
		posts.push(
			...convertFeedToPosts({
				items: feed.items.slice(0, amount),
			}),
		);
	}

	process.stdout.write(`time to generate some text for our table!\n`);
	let postsHandlingBar = new ProgressBar(
		"[:bar] :current/:total posts handled\n",
		{
			incomplete: " ",
			complete: "#",
			total: posts.length,
		},
	);

	for (let i = 0; i < posts.length; i++) {
		let postLink = posts[i];
		let title, description, image;

		await rp(postLink)
			.then((html) => {
				process.stdout.write(`wuush, working on it...\n`);
				posthtml()
					.use((tree) => {
						// парсим дерево и только нужные таблице значения нод
						tree.match(
							{
								tag: "title",
							},
							(node) => {
								title = node.content[0];
							},
						);
						tree.match(
							{
								attrs: {
									name: "description",
								},
								tag: "meta",
							},
							(node) => {
								description = node.attrs.content;
							},
						);
						tree.match(
							{
								attrs: {
									property: "og:image",
								},
								tag: "meta",
							},
							(node) => {
								image = node.attrs.content;
							},
						);
					})
					.process(html);

				postsHandlingBar.tick();
			})
			.catch((err) => {
				console.error("huh, post parsing failed", err);
			});

		res.push({
			title,
			description,
			image,
			link: postLink,
		});
	}

	return res;
};

строки сгенерированы, пора вернуться во входную entry функцию и прикрутить их к инстансу worksheet, используя метод addRow:

process.stdout.write(`making some rows for your sheet...\n`);
for (let i = 0; i < generatedRows.length; i++) {
	let { title, description, image, link } = generatedRows[i];
	let columnText = `${title}\n\n${description}\n\n${link}`;

	if (cellOptions.length) {
		cellOptions.forEach((cOption) => {
			if (cOption === "noImage") {
				image = "";
			}
			if (cOption === "noOGCard") {
				link = "";
			}
		});
	}

	worksheet.addRow({
		col_text: columnText,
		col_url: link,
		col_images: image,
		col_time: moment().add(i, "days").format("DD/MM/YYYY hh:mm").toString(),
	});

	generatedRowsBar.tick();
}

lift off! теперь можно отдавать таблицу:

process.stdout.write(`creating your ${outputFileName} file...\n`);
await workbook.xlsx
	.writeFile(`${outputFileName}.xlsx`)
	.then(() => {
		process.stdout.write(`${outputFileName} created allright!\n`);
	})
	.catch((err) => {
		process.stdout.write("huh, creating error: ", err);
	});

process.stdout.write(`all done, love!\n`);

profit!

source code