нагулял задачу генерации таблиц из 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!
исходный код: https://github.com/arkatriymfalnaya/xlsx-from-rss-generator
Top comments (0)