генерируем xlsx из rss фида
Published: 2022-03-27 06:22:06

нагулял задачу генерации таблиц из 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