Задача: очистить веб-сайт Hacker News и вывести выбранное количество сообщений в консоль с помощью команды hackernews -p [num of posts].

Во время разработки я пробовал несколько разных библиотек, и мое последнее решение написано с использованием:

  • node-fetch для API получения,
  • cheerio для выбора элементов HTML,
  • commander за помощь в запуске программы из командной строки и разрешение настраиваемых командных флагов, таких как: hackernews -p 5 и hackernews --help.

Быстрая настройка: mkdir <name>, cd <name>, запустите npm init -y (вопросы пропускаются), npm i (устанавливает модули узлов), затем создайте server.js файл, в который мы будем писать наш код.

Также создайте файл .gitignore со следующим:

# dependencies
/node_modules

Этот код будет игнорировать модули узлов при отправке на GitHub.

ПЛАН:

Давайте разберемся с нашим подходом. Что мы хотим делать и в каком порядке.

  1. Очистите веб-сайт. Получите необработанный HTML. Мы получим один объединенный html для всех нужных страниц в зависимости от количества требуемых постов.
  2. Извлеките нужные нам значения из этого HTML. Cheerio выполнит здесь работу.
  3. Подтвердите значения. Нам нужно убедиться, что значение комментариев - это число, действительный uri и т. Д.
  4. Сделайте это через интерфейс командной строки. Добавьте hackernews -p [num of posts] команду для запуска из консоли. Что делает его CLI (интерфейс командной строки). Здесь мы будем использовать commander и добавим несколько вещей в package.json.

Давайте рассмотрим это.

ШАГ 1. Очистите веб-сайт.

Добавьте зависимости:

npm i node-fetch
npm i cheerio

Затем в server.js введите:

const fetch = require('node-fetch')
const cheerio = require('cheerio')
const getPagesArray = (numberOfPosts) =>
  Array(Math.ceil(numberOfPosts / 30))   //divides by 30 (posts per page)
    .fill()                          //creates a new array
    .map((_, index) => index + 1)   //[1, 2, 3, 4,..] PagesArray
const getPageHTML = (pageNumber) =>
  fetch(`https://news.ycombinator.com/news?p=${pageNumber}`)
    .then(resp => resp.text())   //Promise
const getAllHTML = async (numberOfPosts) => {
  return Promise.all(getPagesArray(numberOfPosts).map(getPageHTML))
    .then(htmls => console.log(htmls.join(''))) //one JOINED html
}
getAllHTML(5)   // get all HTML for 5 posts

… И запустите node server в консоли, чтобы увидеть вывод html для 5 сообщений.

В функции getPagesArray мы вычисляем, сколько страниц нам нужно будет извлечь, чтобы получить html, необходимый для numberOfPosts необходимого. Если бы мы хотели 125 сообщений, Math.ceil(125 / 30) = 5, а затем:

… Затем [1,2,3,4,5].map(getPageHTML)

Шаг 1 завершен. У нас есть необработанный HTML.

ШАГ 2: Извлеките нужные нам значения.

Теперь мы воспользуемся программой cheerio, чтобы получить title, uri, author, points, comments и rank публикации из имеющегося у нас HTML. И мы поместим объект сообщения в массив results только на необходимое нам количество сообщений.

const getPosts = (html, posts) => {
    let results = []
    let $ = cheerio.load(html)
$('span.comhead').each(function() {
      let a = $(this).prev()
let title = a.text()
      let uri = a.attr('href')
      let rank = a.parent().parent().text()
let subtext = a.parent().parent().next().children('.subtext').children()
      let author = $(subtext).eq(1).text()
      let points = $(subtext).eq(0).text()
      let comments = $(subtext).eq(5).text()
let obj = {
         title: title,
         uri: uri,
         author: author,
         points: points,
         comments: comments,
         rank: parseInt(rank)
      }
      if (obj.rank <= posts) {
        results.push(obj)
      }
    })
    if (results.length > 0) {
      console.log(results)   
      return results
    }
  }

Измените функцию getAllHTML для вызова функции getPosts в конце:

const getAllHTML = async (numberOfPosts) => {
  return Promise.all(getPagesArray(numberOfPosts).map(getPageHTML))
    .then(htmls => getPosts(htmls.join(''), numberOfPosts))
}
getAllHTML(5)   

… И запустите node server в консоли, чтобы увидеть вывод html из 5 сообщений в этом формате:

Шаг 2 завершен. Теперь у нас есть массив объектов с нужными нам данными.

ШАГ 3: Проверьте значения.

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

//VALIDATIONS:
const checkInput = (input) => {
  if (input.length > 0 && input.length < 256){
    return input
  }else {
    return input.substring(0,25)+"..."
  }
}
const checkURI = (uri) => {
  let regex = /(^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?)/
  if (regex.test(uri)){
    return uri
  }else {
    return "uri not valid"
  }
}
const checkPoints = (points) => {
  if (parseInt(points) <= 0) {
    return 0
  }else {
    return parseInt(points)
  }
}
const checkComments = (comments) => {
  if (comments === 'discuss' || comments === '' || parseInt(comments) <= 0) {
    return 0
  }else {
    return parseInt(comments)
  }
}

Измените getPosts, func для вызова функций проверки при формировании obj:

let obj = {
       title: checkInput(title),
       uri: checkURI(uri),
       author: checkInput(author),
       points: checkPoints(points),
       comments: checkComments(comments),
       rank: parseInt(rank)
    }

Шаг 3 завершен. Мы добавили функции проверки.

ШАГ 4. Давайте сделаем это через интерфейс командной строки.

То, что у нас есть сейчас, великолепно, мы вызываем getAllHTML(25) и получаем 25 сообщений в желаемом формате. Но мы хотим иметь возможность вызывать hackernews -p 25 из командной строки, чтобы получить эти 25 сообщений. Здесь нам поможет commander.

Добавьте зависимость с npm i commander. И потребовать это в server.js:

const program = require('commander')

Напишите эту «командирскую» функцию, которая будет вызывать наши getAllHTML и getPosts функции. Здесь мы указываем флаги, такие как -p и--posts. 30 - наше значение по умолчанию.

program
  .option('-p, --posts [value]', 'Number of posts', 30)
  .action(args =>
    getAllHTML(args.posts)
      .then(html => getPosts(html, args.posts))
  )
program.parse(process.argv)

А затем измените функцию getAllHTML, чтобы она возвращала только html:

const getAllHTML = async (numberOfPosts) => {
  return Promise.all(getPagesArray(numberOfPosts).map(getPageHTML))
    .then(htmls => htmls.join(''))
}

И последнее, что мы сделаем в нашем server.js файле, - это добавим эту строку shebang в стиле Unix вверху:

#!/usr/bin/env node

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

Теперь мы могли позвонить node server -p 10, чтобы получить 10 сообщений. Но мы хотим использовать hackernews или любое другое слово вместо node server. Итак, мы переходим к:

package.json

… Файл, в котором мы можем изменить несколько вещей, чтобы это произошло.

  1. Мы предоставим поле bin в нашем package.json, которое является отображением имени команды и имени локального файла. hackernews - это команда, которую я выбрал для вызова вместо node server, а ./server.js - это мой локальный файл сценария, который будет запускаться с этой командой. Этот формат позволяет нам при необходимости предоставлять более одного сопоставления скриптов.
"dependencies": {
    "cheerio": "^1.0.0-rc.2",
    "commander": "^2.18.0",
    "node-fetch": "^2.2.0"
  },
  "bin": {
    "hackernews": "./server.js"
  }
}

Затем запустите:

npm link

А теперь запустите hackernews -p 17 или hackernews --posts 17 или любое другое количество сообщений, которое вы хотите получить, и убедитесь, что это работает.

УДИВИТЕЛЬНЫЙ.

Вы всегда можете npm unlink и изменить название команды.

2. Теперь давайте настроим команду, которую будут запускать другие люди (которые клонировали ваше репо), чтобы все это нормально работало. Добавьте эту жирную строку там, где это указано:

"scripts": {
    "install-hackernews": "npm link && npm i -g",
    "test": "echo \"Error: no test specified\" && exit 1"
  },

Им нужно будет запустить:

npm run install-hackernews

… Чтобы запустить все остальные команды (глобальная установка link и npm i -g, чтобы hackernews -p 25 можно было запускать из любого другого каталога, например из рабочего стола). Здесь вы можете выбрать любое имя:

"<chosen-name>": "npm link && npm i -g",

Команда npm link позволяет нам локально «создать символическую ссылку на папку пакета», она локально установит любую команду, указанную в поле bin нашего package.json. Другими словами, npm link здесь похож на симулятор установки пакета NodeJS.

Шаг 4 завершен. Здесь у нас есть интерфейс командной строки.

Быстрый эксперимент:

Давайте быстро проверим, как все это работает, если мы создали новый файл с именем day.js и в нем написали только:

#!/usr/bin/env node
const days = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']
const now = new Date();
console.log("Today is " + days[now.getDay()] + String.fromCodePoint(0x1f43c))

Затем в package.json:

"bin": {
    "hackernews": "./server.js",
    "day": "./day.js"
  }

и запускаем: npm link, а затем day. Что ты видишь? Что бы вы увидели, если вернетесь на рабочий стол и запустите day?

Спасибо за кодирование! Вот ссылка на мое репо: