One place for hosting & domains

      вебсайта

      Скрейпинг веб-сайта с помощью Node.js и Puppeteer


      Автор выбрал фонд Free and Open Source Fund для получения пожертвования в рамках программы Write for DOnations.

      Введение

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

      В этом руководстве вы создадите приложение для веб-скрейпинга с помощью Node.js и Puppeteer. Ваше приложение будет усложняться по мере вашего прогресса. Сначала вы запрограммируете ваше приложение на открытие Chromium и загрузку специального сайта, который вы будете использовать для практики веб-скрейпинга: books.toscrape.com. В следующих двух шагах вы выполните скрейпинг сначала всех книг на отдельной странице books.toscrape, а затем всех книг на нескольких страницах. В ходе остальных шагов вы сможете отфильтровать результаты по категориям книг, а затем сохраните ваши данные в виде файла JSON.

      Предупреждение: этичность и законность веб-скрейпинга являются очень сложной темой, которая постоянно подвергается изменениям. Ситуация зависит от вашего местонахождения, расположения данных и рассматриваемого веб-сайта. В этом руководстве мы будем выполнять скрейпинг специального сайта books.toscrape.com, который предназначен непосредственно для тестирования приложений для скрейпинга. Скрейпинг любого другого домена выходит за рамки темы данного руководства.

      Предварительные требования

      Шаг 1 — Настройка веб-скрейпера

      После установки Node.js вы можете начать настройку вашего веб-скрейпера. Сначала вам нужно будет создать корневой каталог проекта, а затем установить необходимые зависимости. Данное руководство требует только одной зависимости, и вы установите эту зависимость с помощью npm, стандартного диспетчера пакетов Node.js. npm предоставляется вместе с Node.js, поэтому вам не придется устанавливать его отдельно.

      Создайте папку для данного проекта, а затем перейдите в эту папку:

      • mkdir book-scraper
      • cd book-scraper

      Вы будете запускать все последующие команды из этого каталога.

      Нам нужно установить один пакет с помощью npm (node package manager). Сначала инициализируйте npm для создания файла packages.json, который будет управлять зависимостями вашего проекта и метаданными.

      Инициализация npm для вашего проекта:

      npm отобразит последовательность запросов. Вы можете нажать ENTER в ответ на каждый запрос или добавить персонализированные описания. Нажмите ENTER и оставьте значения по умолчанию при запросе значений для точки входа: и тестовой команды:. В качестве альтернативы вы можете передать флаг y для npmnpm init -y— в результате чего npm добавит все значения по умолчанию.

      Полученный вами вывод будет выглядеть примерно следующим образом:

      Output

      { "name": "sammy_scraper", "version": "1.0.0", "description": "a web scraper", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "sammy the shark", "license": "ISC" } Is this OK? (yes) yes

      Введите yes и нажмите ENTER. После этого npm сохранит этот результат в виде вашего файла package.json.

      Теперь вы можете воспользоваться npm для установки Puppeteer:

      • npm install --save puppeteer

      Эта команда устанавливает Puppeteer и версию Chromium, которая, как известно команде Puppeteer, будет корректно работать с их API.

      На компьютерах с Linux для работы Puppeteer может потребоваться установка дополнительных зависимостей.

      Если вы используете Ubuntu 18.04, ознакомьтесь с данными в выпадающем списке «Зависимости Debian» в разделе «Chrome Headless не запускается в UNIX» документации Puppeteer по устранению ошибок. Вы можете воспользоваться следующей командой для поиска любых недостающих зависимостей:

      После установки npm, Puppeteer и любых дополнительных зависимостей ваш файл package.json потребует одной последней настройки, прежде чем вы сможете начать писать код. В этом руководстве вы будете запускать ваше приложение из командной строки с помощью команды npm run start. Вы должны добавить определенную информацию об этом скрипте start в package.json. В частности, вы должны добавить одну строку под директивой scripts для вашей команды start.

      Откройте в файл в предпочитаемом вами текстовом редакторе:

      Найдите раздел scripts: и добавьте следующие конфигурации. Не забудьте поместить запятую в конце строки test скрипта, иначе ваш файл не будет интерпретироваться корректно.

      Output

      { . . . "scripts": { "test": "echo "Error: no test specified" && exit 1", "start": "node index.js" }, . . . "dependencies": { "puppeteer": "^5.2.1" } }

      Также вы можете заметить, что puppeteer сейчас появляется под разделом dependencies в конце файла. Ваш файл package.json больше не потребует изменений. Сохраните изменения и закройте редактор.

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

      Шаг 2 — Настройка экземпляра браузера

      Когда вы открываете традиционный браузер, то можете выполнять такие действия, как нажатие кнопок, навигация с помощью мыши, печать, открытие инструментов разработчик и многое другое. Браузер без графического интерфейса, например, Chromium, позволяет вам выполнять эти же вещи, но уже программным путем без использования пользовательского интерфейса. В этом шаге вы настроите экземпляр браузера для вашего скрейпера. Когда вы запустите ваше приложение, оно автоматически откроет Chromium и перейдет на сайт books.toscrape.com. Эти первоначальные действия будут служить основой вашей программы.

      Вашему веб-скрейперу потребуется четыре файла .js: browser.js, index.js, pageController.js и pageScraper.js. В этом шаге вы создадите все четыре файла, а затем постепенно будете обновлять их по мере того, как ваша программа будет усложняться. Начнем с browser.js; этот файл будет содержать скрипт, который запускает ваш браузер.

      В корневом каталоге вашего проекта создайте и откройте файл browser.js в текстовом редакторе:

      Во-первых, необходимо подключить Puppeteer с помощью require, а затем создать асинхронную функцию с именем startBrowser(). Эта функция будет запускать браузер и возвращать его экземпляр. Добавьте следующий код:

      ./book-scraper/browser.js

      const puppeteer = require('puppeteer');
      
      async function startBrowser(){
          let browser;
          try {
              console.log("Opening the browser......");
              browser = await puppeteer.launch({
                  headless: false,
                  args: ["--disable-setuid-sandbox"],
                  'ignoreHTTPSErrors': true
              });
          } catch (err) {
              console.log("Could not create a browser instance => : ", err);
          }
          return browser;
      }
      
      module.exports = {
          startBrowser
      };
      

      Puppeteer имеет метод launch(), который запускает экземпляр браузера. Этот метод возвращает промис, поэтому вам нужно гарантировать, что промис исполняется, воспользовавшись для этого блоком .then или await.

      Вы будете использовать await для гарантии исполнения промиса, обернув этот экземпляр в блок try-catch, а затем вернув экземпляр браузера.

      Обратите внимание, что метод .launch() принимает в качестве параметра JSON с несколькими значениями:

      • headless – false означает, что браузер будет запускаться с интерфейсом, чтобы вы могли наблюдать за выполнением вашего скрипта, а значение true для данного параметра означает, что браузер будет запускаться в режиме без графического интерфейс. Обратите внимание, что, если вы хотите развернуть ваш скрейпер в облаке, задайте значение true для параметра headless. Большинство виртуальных машин не имеют пользовательского интерфейса, поэтому они могут запускать браузер только в режиме без графического интерфейса. Puppeteer также включает режим headful, но его следует использовать исключительно для тестирования.
      • ignoreHTTPSerrors – true позволяет вам посещать веб-сайты, доступ к которым осуществляется не через защищенный протокол HTTPS, и игнорировать любые ошибки HTTPS.

      Сохраните и закройте файл.

      Теперь создайте ваш второй файл .jsindex.js:

      Здесь вы подключаете файлы browser.js и pageController.js с помощью require. Затем вы вызовете функцию startBrowser() и передадите созданный экземпляр браузера в контроллер страницы, который будет управлять ее действиями. Добавьте следующий код:

      ./book-scraper/index.js

      const browserObject = require('./browser');
      const scraperController = require('./pageController');
      
      //Start the browser and create a browser instance
      let browserInstance = browserObject.startBrowser();
      
      // Pass the browser instance to the scraper controller
      scraperController(browserInstance)
      

      Сохраните и закройте файл.

      Создайте ваш третий файл .jspageController.js:

      pageController.js контролирует процесс скрейпинга. Он использует экземпляр браузера для управления файлом pageScraper.js, где выполняются все скрипты скрейпинга. В конечном итоге вы будете использовать его для указания категории, скрейпинг которой вы хотите выполнить. Однако сейчас вам нужно только убедиться, что вы можете открыть Chromium и перейти на веб-страницу:

      ./book-scraper/pageController.js

      const pageScraper = require('./pageScraper');
      async function scrapeAll(browserInstance){
          let browser;
          try{
              browser = await browserInstance;
              await pageScraper.scraper(browser); 
      
          }
          catch(err){
              console.log("Could not resolve the browser instance => ", err);
          }
      }
      
      module.exports = (browserInstance) => scrapeAll(browserInstance)
      

      Этот код экспортирует функцию, которая принимает экземпляр браузера и передает его в функцию scrapeAll(). Эта функция, в свою очередь, передает этот экземпляр в pageScraper.scraper() в качестве аргумента, который использует его при скрейпинге страниц.

      Сохраните и закройте файл.

      В заключение создайте ваш последний файл .jspageScraper.js:

      Здесь вы создаете литерал со свойством url и методом scraper(). url — это URL-адрес веб-страницы, скрейпинг которой вы хотите выполнить, а метод scraper() содержит код, который будет непосредственно выполнять скрейпинг, хотя на этом этапе он будет просто переходить по указанному URL-адресу. Добавьте следующий код:

      ./book-scraper/pageScraper.js

      const scraperObject = {
          url: 'http://books.toscrape.com',
          async scraper(browser){
              let page = await browser.newPage();
              console.log(`Navigating to ${this.url}...`);
              await page.goto(this.url);
      
          }
      }
      
      module.exports = scraperObject;
      

      Puppeteer имеет метод newPage(), который создает новый экземпляр страницы в браузере, а эти экземпляры страниц могут выполнять несколько действий. В методе scraper() вы создали экземпляр страницы, а затем использовали метод page.goto() для перехода на домашнюю страницу books.toscrape.com.

      Сохраните и закройте файл.

      Теперь файловая структура вашей программы готова. Первый уровень дерева каталогов вашего проекта будет выглядеть следующим образом:

      Output

      . ├── browser.js ├── index.js ├── node_modules ├── package-lock.json ├── package.json ├── pageController.js └── pageScraper.js

      Теперь запустите команду npm run start и следите за выполнением вашего приложения для скрейпинга:

      Приложение автоматически загрузит экземпляр браузера Chromium, откроет новую страницу в браузере и перейдет на адрес books.toscrape.com.

      В этом шаге вы создали приложение Puppeteer, которое открывает Chromium и загружает домашнюю страницу шаблона книжного онлайн-магазина—books.toscrape.com. В следующем шаге вы будете выполнять скрейпинг данных для каждой книги на этой домашней странице.

      Шаг 3 — Скрейпинг данных с одной страницы

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

      Изображение веб-сайта с книгами для скрейпинга

      Слева вы найдете раздел категорий, а справа располагаются книги. При нажатии на книгу браузер переходит по новому URL-адресу, который отображает соответствующую информацию об этой конкретной книге.

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

      Во-первых, если вы просмотрите исходный код домашней страницы с помощью инструментов разработчика в браузере, то сможете заметить, что страница содержит данные каждой книги внутри тега section. Внутри тега section каждая книга находится внутри тега list (li), и именно здесь вы найдете ссылку на отдельную страницу книги, цену и информацию о наличии.

      Просмотр исходного кода books.toscrape с помощью инструментов для разработчика

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

      Повторно откройте ваш файл pageScraper.js:

      Добавьте следующие выделенные строки. Вы поместите еще один блок await внутри блока await page.goto(this.url);:

      ./book-scraper/pageScraper.js

      
      const scraperObject = {
          url: 'http://books.toscrape.com',
          async scraper(browser){
              let page = await browser.newPage();
              console.log(`Navigating to ${this.url}...`);
              // Navigate to the selected page
              await page.goto(this.url);
              // Wait for the required DOM to be rendered
              await page.waitForSelector('.page_inner');
              // Get the link to all the required books
              let urls = await page.$$eval('section ol > li', links => {
                  // Make sure the book to be scraped is in stock
                  links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                  // Extract the links from the data
                  links = links.map(el => el.querySelector('h3 > a').href)
                  return links;
              });
              console.log(urls);
          }
      }
      
      module.exports = scraperObject;
      
      

      Внутри этого блока кода вы вызываете метод page.waitForSelector(). Он ожидает, когда блок, содержащий всю информацию о книге, будет преобразован в DOM, после чего вы вызываете метод page.$$eval(). Этот метод получает элемент URL-адреса с селектором section ol li (убедитесь, что методы page.$eval() и page.$$eval() возвращают только строку или число).

      Каждая книга имеет два статуса: In Stock (В наличии) или Out of stock (Распродано). Вам нужно выполнить скрейпинг книг со статусом In Stock. Поскольку page.$$eval() возвращает массив всех подходящих элементов, вы выполнили фильтрацию этого массива, чтобы гарантировать работу исключительно с книгами в наличии. Эта задача выполняется путем поиска и оценки класса .instock.availability. Затем вы вычленили свойство href в ссылках книг и вернули его с помощью метода.

      Сохраните и закройте файл.

      Повторно запустите ваше приложение:

      Приложение откроет браузер, перейдет на веб-страницу, а затем закроет его, когда задача будет выполнена. Теперь проверьте вашу консоль; она будет содержать все полученные URL-адреса:

      Output

      > book-scraper@1.0.0 start /Users/sammy/book-scraper > node index.js Opening the browser...... Navigating to http://books.toscrape.com... [ 'http://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html', 'http://books.toscrape.com/catalogue/tipping-the-velvet_999/index.html', 'http://books.toscrape.com/catalogue/soumission_998/index.html', 'http://books.toscrape.com/catalogue/sharp-objects_997/index.html', 'http://books.toscrape.com/catalogue/sapiens-a-brief-history-of-humankind_996/index.html', 'http://books.toscrape.com/catalogue/the-requiem-red_995/index.html', 'http://books.toscrape.com/catalogue/the-dirty-little-secrets-of-getting-your-dream-job_994/index.html', 'http://books.toscrape.com/catalogue/the-coming-woman-a-novel-based-on-the-life-of-the-infamous-feminist-victoria-woodhull_993/index.html', 'http://books.toscrape.com/catalogue/the-boys-in-the-boat-nine-americans-and-their-epic-quest-for-gold-at-the-1936-berlin-olympics_992/index.html', 'http://books.toscrape.com/catalogue/the-black-maria_991/index.html', 'http://books.toscrape.com/catalogue/starving-hearts-triangular-trade-trilogy-1_990/index.html', 'http://books.toscrape.com/catalogue/shakespeares-sonnets_989/index.html', 'http://books.toscrape.com/catalogue/set-me-free_988/index.html', 'http://books.toscrape.com/catalogue/scott-pilgrims-precious-little-life-scott-pilgrim-1_987/index.html', 'http://books.toscrape.com/catalogue/rip-it-up-and-start-again_986/index.html', 'http://books.toscrape.com/catalogue/our-band-could-be-your-life-scenes-from-the-american-indie-underground-1981-1991_985/index.html', 'http://books.toscrape.com/catalogue/olio_984/index.html', 'http://books.toscrape.com/catalogue/mesaerion-the-best-science-fiction-stories-1800-1849_983/index.html', 'http://books.toscrape.com/catalogue/libertarianism-for-beginners_982/index.html', 'http://books.toscrape.com/catalogue/its-only-the-himalayas_981/index.html' ]

      Это отличное начало, но вам нужно выполнить скрейпинг всех подходящих данных для конкретной книги, а не только ее URL-адреса. Теперь вы будете использовать эти URL-адреса для открытия каждой страницы и скрейпинга названия, автора, цены, наличия, универсального товарного кода, описания и URL-адреса изображения.

      Повторно откройте pageScraper.js:

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

      ./book-scraper/pageScraper.js

      const scraperObject = {
          url: 'http://books.toscrape.com',
          async scraper(browser){
              let page = await browser.newPage();
              console.log(`Navigating to ${this.url}...`);
              // Navigate to the selected page
              await page.goto(this.url);
              // Wait for the required DOM to be rendered
              await page.waitForSelector('.page_inner');
              // Get the link to all the required books
              let urls = await page.$$eval('section ol > li', links => {
                  // Make sure the book to be scraped is in stock
                  links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                  // Extract the links from the data
                  links = links.map(el => el.querySelector('h3 > a').href)
                  return links;
              });
      
      
              // Loop through each of those links, open a new page instance and get the relevant data from them
              let pagePromise = (link) => new Promise(async(resolve, reject) => {
                  let dataObj = {};
                  let newPage = await browser.newPage();
                  await newPage.goto(link);
                  dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
                  dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
                  dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                      // Strip new line and tab spaces
                      text = text.textContent.replace(/(rnt|n|r|t)/gm, "");
                      // Get the number of stock available
                      let regexp = /^.*((.*)).*$/i;
                      let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                      return stockAvailable;
                  });
                  dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
                  dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
                  dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
                  resolve(dataObj);
                  await newPage.close();
              });
      
              for(link in urls){
                  let currentPageData = await pagePromise(urls);
                  // scrapedData.push(currentPageData);
                  console.log(currentPageData);
              }
      
          }
      }
      
      module.exports = scraperObject;
      

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

      Предупреждение: обратите внимание, что вы реализовали ожидание выполнения промиса с помощью цикла for-in. Вы можете использовать любой цикл, но рекомендуется избегать итерации по вашим массивам с URL-адресами с помощью метода итерации по массивам, например, forEach, или любого другого метода, который использует функцию обратного вызова. Это связано с тем, что функция обратного вызова должна будет сначала пройти через очередь обратного вызова и цикл событий, в результате чего будет одновременно открываться несколько экземпляров страницы. Это будет накладывать заметно большую нагрузку на вашу память.

      Приглядитесь внимательней к вашей функции pagePromise. Ваше приложение для скрейпинга сначала создало новую страницу для каждого URL-адреса, а затем вы использовали функцию page.$eval() для настройки селекторов на получение подходящих данных, которые вы хотите собрать с новой страницы. Некоторые тексты содержат пробелы, символы табуляции, переносы строки и прочие специальные символы, которые вы удалили с помощью регулярного выражения. Затем вы добавили значение всех элементов данных, которые были получены во время скрейпинга страницы, в объект и зарезолвили этот объект.

      Сохраните и закройте файл.

      Запустите скрипт еще раз:

      Браузер открывает домашнюю страницу, затем переходит на страницу каждой книги и записывает данные скрейпинга для каждой из этих страниц. Следующий вывод будет отображен в консоли:

      Output

      Opening the browser...... Navigating to http://books.toscrape.com... { bookTitle: 'A Light in the Attic', bookPrice: '£51.77', noAvailable: '22', imageUrl: 'http://books.toscrape.com/media/cache/fe/72/fe72f0532301ec28892ae79a629a293c.jpg', bookDescription: "It's hard to imagine a world without A Light in the Attic. [...]', upc: 'a897fe39b1053632' } { bookTitle: 'Tipping the Velvet', bookPrice: '£53.74', noAvailable: '20', imageUrl: 'http://books.toscrape.com/media/cache/08/e9/08e94f3731d7d6b760dfbfbc02ca5c62.jpg', bookDescription: `"Erotic and absorbing...Written with starling power."--"The New York Times Book Review " Nan King, an oyster girl, is captivated by the music hall phenomenon Kitty Butler [...]`, upc: '90fa61229261140a' } { bookTitle: 'Soumission', bookPrice: '£50.10', noAvailable: '20', imageUrl: 'http://books.toscrape.com/media/cache/ee/cf/eecfe998905e455df12064dba399c075.jpg', bookDescription: 'Dans une France assez proche de la nôtre, [...]', upc: '6957f44c3847a760' } ...

      В этом шаге вы выполнили скрейпинг подходящих данных для каждой книги на домашней странице books.toscrape.com, но вы можете добавить гораздо больше функций. Например, если каждая страница с книгами имеет пагинацию, как вы получите книги со следующих страниц? Также в левой части веб-сайта указаны категории книг; что, если вам не нужны все книги, а только книги конкретного жанра? Теперь вы можете добавить эти функции.

      Шаг 4 — Скрейпинг данных с нескольких страниц

      Страницы на сайте books.toscrape.com с пагинацией имеют кнопку Next (Далее) под основным содержанием, а на страницах без пагинации этой кнопки нет.

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

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

      Повторно откройте pagescraper.js:

      Вы добавите новую функцию с именем scrapeCurrentPage() в ваш метод scraper(). Эта функция будет содержать весь код, который будет выполнять скрейпинг данных с отдельной страницы, а затем нажимать кнопку next, если она присутствует. Добавьте следующий выделенный код:

      ./book-scraper/pageScraper.js scraper()

      const scraperObject = {
          url: 'http://books.toscrape.com',
          async scraper(browser){
              let page = await browser.newPage();
              console.log(`Navigating to ${this.url}...`);
              // Navigate to the selected page
              await page.goto(this.url);
              let scrapedData = [];
              // Wait for the required DOM to be rendered
              async function scrapeCurrentPage(){
                  await page.waitForSelector('.page_inner');
                  // Get the link to all the required books
                  let urls = await page.$$eval('section ol > li', links => {
                      // Make sure the book to be scraped is in stock
                      links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                      // Extract the links from the data
                      links = links.map(el => el.querySelector('h3 > a').href)
                      return links;
                  });
                  // Loop through each of those links, open a new page instance and get the relevant data from them
                  let pagePromise = (link) => new Promise(async(resolve, reject) => {
                      let dataObj = {};
                      let newPage = await browser.newPage();
                      await newPage.goto(link);
                      dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
                      dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
                      dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                          // Strip new line and tab spaces
                          text = text.textContent.replace(/(rnt|n|r|t)/gm, "");
                          // Get the number of stock available
                          let regexp = /^.*((.*)).*$/i;
                          let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                          return stockAvailable;
                      });
                      dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
                      dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
                      dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
                      resolve(dataObj);
                      await newPage.close();
                  });
      
                  for(link in urls){
                      let currentPageData = await pagePromise(urls);
                      scrapedData.push(currentPageData);
                      // console.log(currentPageData);
                  }
                  // When all the data on this page is done, click the next button and start the scraping of the next page
                  // You are going to check if this button exist first, so you know if there really is a next page.
                  let nextButtonExist = false;
                  try{
                      const nextButton = await page.$eval('.next > a', a => a.textContent);
                      nextButtonExist = true;
                  }
                  catch(err){
                      nextButtonExist = false;
                  }
                  if(nextButtonExist){
                      await page.click('.next > a');   
                      return scrapeCurrentPage(); // Call this function recursively
                  }
                  await page.close();
                  return scrapedData;
              }
              let data = await scrapeCurrentPage();
              console.log(data);
              return data;
          }
      }
      
      module.exports = scraperObject;
      
      

      Первоначально вы задаете для переменной nextButtonExist значение false, а затем проверяете, присутствует ли кнопка. Если кнопка next существует, вы задаете для nextButtonExists значение true и переходите к нажатию кнопки next, после чего вызываете эту функцию рекурсивно.

      Если для nextButtonExists задано значение false, функция просто возвращает массив scrapedData.

      Сохраните и закройте файл.

      Запустите скрипт еще раз:

      На завершение работы скрипта может потребоваться время; ваше приложение теперь выполняет скрейпинг данных для более чем 800 книг. Вы можете закрыть браузер, либо нажать CTRL + C для завершения процесса.

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

      Шаг 5 — Скрейпинг данных по категории

      Чтобы выполнить скрейпинг данных по категориям, вам нужно будет изменить содержание файлов pageScraper.js и pageController.js.

      Откройте pageController.js в текстовом редакторе:

      nano pageController.js
      

      Вызовите скрейпер так, чтобы он выполнял скрейпинг исключительно книг о путешествиях. Добавьте следующий код:

      ./book-scraper/pageController.js

      const pageScraper = require('./pageScraper');
      async function scrapeAll(browserInstance){
          let browser;
          try{
              browser = await browserInstance;
              let scrapedData = {};
              // Call the scraper for different set of books to be scraped
              scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');
              await browser.close();
              console.log(scrapedData)
          }
          catch(err){
              console.log("Could not resolve the browser instance => ", err);
          }
      }
      module.exports = (browserInstance) => scrapeAll(browserInstance)
      

      Теперь вы передаете два параметра в метод pageScraper.scraper(), где второй параметр — категория книг, которые вы хотите получить, в данном случае это Travel. Но ваш файл pageScraper.js еще не распознает этот параметр. Вам также нужно будет изменить этот файл.

      Сохраните и закройте файл.

      Откройте pageScraper.js:

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

      ./book-scraper/pageScraper.js

      const scraperObject = {
          url: 'http://books.toscrape.com',
          async scraper(browser, category){
              let page = await browser.newPage();
              console.log(`Navigating to ${this.url}...`);
              // Navigate to the selected page
              await page.goto(this.url);
              // Select the category of book to be displayed
              let selectedCategory = await page.$$eval('.side_categories > ul > li > ul > li > a', (links, _category) => {
      
                  // Search for the element that has the matching text
                  links = links.map(a => a.textContent.replace(/(rnt|n|r|t|^s|s$|Bs|sB)/gm, "") === _category ? a : null);
                  let link = links.filter(tx => tx !== null)[0];
                  return link.href;
              }, category);
              // Navigate to the selected category
              await page.goto(selectedCategory);
              let scrapedData = [];
              // Wait for the required DOM to be rendered
              async function scrapeCurrentPage(){
                  await page.waitForSelector('.page_inner');
                  // Get the link to all the required books
                  let urls = await page.$$eval('section ol > li', links => {
                      // Make sure the book to be scraped is in stock
                      links = links.filter(link => link.querySelector('.instock.availability > i').textContent !== "In stock")
                      // Extract the links from the data
                      links = links.map(el => el.querySelector('h3 > a').href)
                      return links;
                  });
                  // Loop through each of those links, open a new page instance and get the relevant data from them
                  let pagePromise = (link) => new Promise(async(resolve, reject) => {
                      let dataObj = {};
                      let newPage = await browser.newPage();
                      await newPage.goto(link);
                      dataObj['bookTitle'] = await newPage.$eval('.product_main > h1', text => text.textContent);
                      dataObj['bookPrice'] = await newPage.$eval('.price_color', text => text.textContent);
                      dataObj['noAvailable'] = await newPage.$eval('.instock.availability', text => {
                          // Strip new line and tab spaces
                          text = text.textContent.replace(/(rnt|n|r|t)/gm, "");
                          // Get the number of stock available
                          let regexp = /^.*((.*)).*$/i;
                          let stockAvailable = regexp.exec(text)[1].split(' ')[0];
                          return stockAvailable;
                      });
                      dataObj['imageUrl'] = await newPage.$eval('#product_gallery img', img => img.src);
                      dataObj['bookDescription'] = await newPage.$eval('#product_description', div => div.nextSibling.nextSibling.textContent);
                      dataObj['upc'] = await newPage.$eval('.table.table-striped > tbody > tr > td', table => table.textContent);
                      resolve(dataObj);
                      await newPage.close();
                  });
      
                  for(link in urls){
                      let currentPageData = await pagePromise(urls);
                      scrapedData.push(currentPageData);
                      // console.log(currentPageData);
                  }
                  // When all the data on this page is done, click the next button and start the scraping of the next page
                  // You are going to check if this button exist first, so you know if there really is a next page.
                  let nextButtonExist = false;
                  try{
                      const nextButton = await page.$eval('.next > a', a => a.textContent);
                      nextButtonExist = true;
                  }
                  catch(err){
                      nextButtonExist = false;
                  }
                  if(nextButtonExist){
                      await page.click('.next > a');   
                      return scrapeCurrentPage(); // Call this function recursively
                  }
                  await page.close();
                  return scrapedData;
              }
              let data = await scrapeCurrentPage();
              console.log(data);
              return data;
          }
      }
      
      module.exports = scraperObject;
      

      Этот блок кода использует категорию, которую вы передали, для получения URL-адреса страницы, где находятся книги этой категории.

      page.$$eval() может принимать аргументы с помощью передачи этого аргумента в качестве третьего параметра для метода $$$eval() и определения его как третьего параметра в обратном вызове следующим образом:

      example page.$$eval() function

      page.$$eval('selector', function(elem, args){
          // .......
      }, args)
      

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

      Затем этот URL-адрес был использован для перехода на страницу, которая отображает категорию книг, для которой вы хотите выполнить скрейпинг, с помощью метода page.goto(selectedCategory).

      Сохраните и закройте файл.

      Запустите ваше приложение еще раз. Вы заметите, что приложение переходит к категории Travel, рекурсивно открывает книги данной категории одну за одной и записывает результаты:

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

      Шаг 6 — Выполнение скрейпинга данных из нескольких категорий и сохранение данных в виде файла JSON

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

      Вы сможете быстро добавлять дополнительные категории для скрейпинга; для этого вам потребуется добавлять одну дополнительную строку для каждого отдельного жанра.

      Откройте pageController.js:

      Измените ваш код для включения дополнительных категорий. В примере ниже к существующей категории Travel добавляются категории HistoricalFiction и Mystery:

      ./book-scraper/pageController.js

      const pageScraper = require('./pageScraper');
      async function scrapeAll(browserInstance){
          let browser;
          try{
              browser = await browserInstance;
              let scrapedData = {};
              // Call the scraper for different set of books to be scraped
              scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');
              scrapedData['HistoricalFiction'] = await pageScraper.scraper(browser, 'Historical Fiction');
              scrapedData['Mystery'] = await pageScraper.scraper(browser, 'Mystery');
              await browser.close();
              console.log(scrapedData)
          }
          catch(err){
              console.log("Could not resolve the browser instance => ", err);
          }
      }
      
      module.exports = (browserInstance) => scrapeAll(browserInstance)
      

      Сохраните и закройте файл.

      Запустите скрипт еще раз и посмотрите, как он выполняет скрейпинг данных для всех трех категорий:

      После получения всего необходимого функционала скрейпера в качестве заключительного шага вы добавите сохранение данных в более полезном формате. Теперь вы сможете сохранять их в файле JSON с помощью модуля fs в Node.js.

      Откройте pageController.js:

      Добавьте следующий выделенный код:

      ./book-scraper/pageController.js

      const pageScraper = require('./pageScraper');
      const fs = require('fs');
      async function scrapeAll(browserInstance){
          let browser;
          try{
              browser = await browserInstance;
              let scrapedData = {};
              // Call the scraper for different set of books to be scraped
              scrapedData['Travel'] = await pageScraper.scraper(browser, 'Travel');
              scrapedData['HistoricalFiction'] = await pageScraper.scraper(browser, 'Historical Fiction');
              scrapedData['Mystery'] = await pageScraper.scraper(browser, 'Mystery');
              await browser.close();
              fs.writeFile("data.json", JSON.stringify(scrapedData), 'utf8', function(err) {
                  if(err) {
                      return console.log(err);
                  }
                  console.log("The data has been scraped and saved successfully! View it at './data.json'");
              });
          }
          catch(err){
              console.log("Could not resolve the browser instance => ", err);
          }
      }
      
      module.exports = (browserInstance) => scrapeAll(browserInstance)
      

      В первую очередь вам потребуется добавить модуль fs из Node.js в файл pageController.js. Это гарантирует, что вы сможете сохранить ваши данные в виде файла JSON. Затем вы добавляете код, чтобы после завершения скрейпинга и закрытия браузера программа создавала новый файл с именем data.json. Обратите внимание, что data.json представляет собой строковый файл JSON. Следовательно, при чтении содержания data.json всегда необходимо парсить его в качестве JSON перед повторным использованием данных.

      Сохраните и закройте файл.

      Вы создали приложение для веб-скрейпинга, которое выполняет скрейпинг книг из нескольких категорий, а затем сохраняет полученные данные в файле JSON. По мере усложнения вашего приложения вам может потребоваться сохранять ваши данные в базе данных или работать с ними через API. То, как будут использованы эти данные, зависит от вас.

      Заключение

      В этом руководстве вы создали поискового робота, который рекурсивно скрейпит данные на нескольких страницах и затем сохраняет их в файле JSON. Коротко говоря, вы узнали новый способ автоматизации сбора данных с веб-сайтов.

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



      Source link

      Хостинг веб-сайта с Caddy в Ubuntu 18.04


      Автор выбрал фонд Free and Open Source Fund для получения пожертвования в рамках программы Write for DOnations.

      Введение

      Caddy — это простой и безопасный веб-сервер, имеющий ряд функций, которые могут быть полезны для хостинга веб-сайтов. Например, он может автоматически получать и управлять сертификатами TLS из Let’s Encrypt для использования HTTPS и включает поддержку HTTP/2. HTTPS — это система защиты трафика, которым пользователи обмениваются с сервером.Она очень быстро становится стандартом для любого запущенного в производственную среду веб-сайта. Без HTTPS браузеры Chrome и Firefox будут предупреждать, что ваш веб-сайт является «небезопасным» при отправке пользователями учетных данных.

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

      В этом обучающем руководстве вы выполните сборку Caddy из исходного кода и используете его для хостинга веб-сайта, защищенного с помощью HTTPS. Эти задачи подразумевают компиляцию, настройку с помощью Caddyfile и установку плагинов. В конце вы узнаете, как обновить вашу установку при появлении новой версии.

      Предварительные требования

      • Сервер Ubuntu 18.04 с привилегиями root и дополнительная учетная запись без прав root. Вы можете выполнить настройку, следуя указаниям документа Начальная настройка сервера для Ubuntu 18.04. В этом руководстве нашим пользователем без прав root будет sammy.
      • Зарегистрированное полное доменное имя. В этом обучающем руководстве мы будем использовать your_domain. Вы можете купить доменное имя на Namecheap, получить его бесплатно на Freenom или воспользоваться услугами любого предпочитаемого регистратора доменных имен.
      • Запись DNS A с your_domain​​​, указывающая на публичный IP-адрес вашего сервера. В руководстве Введение в DigitalOcean DNS содержится подробная информация по их добавлению.
      • Набор инструментов для языка Go, установленный на вашем сервере. Воспользуйтесь нашим руководством Установка Go и настройка локальной среды программирования в Ubuntu 18.04 для настройки Go. Создавать примеры проектов не нужно.
      • Персональный токен доступа (ключ API) с правами на чтение и запись для вашей учетной записи DigitalOcean. Его создание описано в материале Создание персонального токена доступа.

      Шаг 1 — Сборка Caddy

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

      Для целей настоящего обучающего руководства вы будете хранить исходный код в директории ~/caddy. Создайте директорию, запустив следующую команду:

      Перейдите в директорию:

      Вы сохраните исходный код для запуска и настройки Caddy в файле с именем caddy.go. Создайте файл с помощью следующей команды:

      Добавьте следующие строки:

      ~/caddy/caddy.go

      package main
      
      import (
          "github.com/caddyserver/caddy/caddy/caddymain"
      )
      
      func main() {
          // caddymain.EnableTelemetry = false
          caddymain.Run()
      }
      

      Этот код импортирует Caddy непосредственно из Github (с помощью Git) и запускает его, используя точку входа — функцию main. Если вы хотите активировать телеметрию, разкомментируйте строку caddymain.EnableTelemetry и задайте значение true. После внесения изменений сохраните и закройте файл.

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

      Output

      go: creating new go.mod: module caddy

      В настоящий момент вы можете выполнить сборку стоковой версии Caddy из исходного кода, запустив следующую команду:

      Вы получите большое количество данных в выводе с информацией о том, какие библиотеки Go, загруженные в качестве зависимостей, необходимы для компиляции. Полученный в результате исполняемый файл хранится в файле $GOPATH/bin, как описано в предварительных требованиях.

      После завершения попробуйте запустить Caddy:

      Результат будет выглядеть примерно так:

      Output

      Activating privacy features... done. Serving HTTP on port 2015 http://:2015 WARNING: File descriptor limit 1024 is too low for production servers. At least 8192 is recommended. Fix with `ulimit -n 8192`.

      Это означает, что Caddy успешно запущен и доступен на порту 2015. Вы можете проигнорировать предупреждение, поскольку этот лимит будет корректироваться в последующих шагах без вашего вмешательства. Для выхода нажмите CTRL + C.

      Вы выполнили сборку и запустили Caddy. В следующем шаге вы выполните установку Caddy в качестве службы для ее автоматического запуска при загрузке, а затем измените настройки принадлежности и разрешений для обеспечения безопасности сервера.

      Шаг 2 — Установка Caddy

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

      Для начала переместите бинарный файл Caddy в /usr/local/bin, стандартное местоположение для бинарных файлов, которые не управляются диспетчером пакетов Ubuntu и не используются для работы системы:

      • sudo mv $GOPATH/bin/caddy /usr/local/bin/

      Далее измените принадлежность бинарного файла Caddy на пользователя root:

      • sudo chown root:root /usr/local/bin/caddy

      Это предотвратит внесение изменений в исполняемый файл с помощью других учетных записей. Однако даже при использовании пользователя root для определения принадлежности Caddy рекомендуется запускать Caddy с помощью других учетных записей, не имеющих прав root и присутствующих в вашей системе. Это гарантирует, что в случае нарушения безопасности Caddy (или другой программы) злоумышленник не сможет изменять бинарный файл или выполнять команды от имени пользователя root.

      Далее установите для бинарного файла разрешения 755 — это позволит предоставить пользователю root полные права на чтение/запись/исполнение файла, в то время как остальные пользователи смогут только читать и исполнять файл:

      • sudo chmod 755 /usr/local/bin/caddy

      Поскольку процесс Caddy не будет запускаться с помощью пользователя root, Linux будет запрещать привязку к портам 80 и 443 (стандартные порты для HTTP и HTTPS соответственно), так как это привилегированные операции. Для удобства доступа к вашему домену Caddy необходимо привязать к одному из этих портов в зависимости от протокола. В противном случае вам потребуется добавить конкретный номер порта в URL-адрес домена в вашем браузере, чтобы просмотреть содержимое, которое он будет обслуживать.

      Чтобы позволить Caddy выполнять привязку к порту с низким номером без запуска root, воспользуйтесь следующей командой:

      • sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/caddy

      Утилита setcap определяет права для файлов. В этой команде она предоставляет право CAP_NET_BIND_SERVICE​​​ для бинарного файла Caddy, что позволяет исполняемому файлу выполнять привязку с номером ниже 1024. В этой команде она указывает возможности CAPNETBIND_SERVICE для бинарного файла Caddy, который позволяет привязку исполняющего файла к порту ниже 1024.

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

      Затем установите правильные разрешения для пользователя и группы:

      • sudo chown -R root:www-data /etc/caddy

      Установка пользователя в качестве root, а группы как www-data гарантирует, что у Caddy будет доступ к папке с правами на чтение и запись (через группу www-data) и что только учетная запись суперпользователя будет иметь аналогичные права на чтение и изменение. www-data — это используемые в Ubuntu по умолчанию пользователь и группа при работе с веб-сервером.

      В следующем шаге вы должны будете активировать автоматическое предоставление сертификата TLS из Let’s Encrypt. В процессе подготовки создайте директорию для хранения любых сертификатов TLS, которые будет получать Caddy, и укажите те же самые правила принадлежности, как и в случае с директорией /etc/caddy:

      • sudo mkdir /etc/ssl/caddy
      • sudo chown -R root:www-data /etc/ssl/caddy

      Caddy должен иметь возможность сохранять сертификаты в этой директории и считывать их оттуда, чтобы шифровать запросы. По этой причине необходимо изменить разрешения для директории /etc/ssl/caddy, чтобы она была доступна только посредством пользователя root и группы www-data:

      • sudo chmod 0770 /etc/ssl/caddy

      Далее создайте директорию для хранения файлов, которые будет размещать Caddy:

      Установите www-data в качестве владельца и группы директории:

      • sudo chown www-data:www-data /var/www

      Caddy считывает свою конфигурацию из файла с именем Caddyfile, который хранится в директории /etc/caddy. Создайте файл на диске, запустив следующую команду:

      • sudo touch /etc/caddy/Caddyfile

      Для установки службы Caddy загрузите юнит-файл systemd из репозитория Caddy на Github в директорию /etc/systemd/system, запустив следующую команду:

      • sudo sh -c 'curl https://raw.githubusercontent.com/caddyserver/caddy/master/dist/init/linux-systemd/caddy.service > /etc/systemd/system/caddy.service'

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

      • sudo chmod 644 /etc/systemd/system/caddy.service

      Затем перезагрузите службу systemd для обнаружения службы Caddy:

      • sudo systemctl daemon-reload

      Проверьте, удалось ли systemd обнаружить службу Caddy, запустив systemctl status:

      • sudo systemctl status caddy

      Результат будет выглядеть примерно так:

      Output

      ● caddy.service - Caddy HTTP/2 web server Loaded: loaded (/etc/systemd/system/caddy.service; disabled; vendor preset: e Active: inactive (dead) Docs: https://caddyserver.com/docs

      Если вы увидите аналогичный вывод, это значит, что новая служба была корректно обнаружена службой systemd.

      Во время выполнения предварительных требований по настройке сервера вы активировали ufw, простой брандмауэр, и разрешили подключения через SSH. Чтобы Caddy мог обслуживать трафик HTTP и HTTPS с вашего сервера, вам нужно будет разрешить прием такого трафика в ufw, запустив следующую команду:

      • sudo ufw allow proto tcp from any to any port 80,443

      Результат будет выглядеть следующим образом:

      Output

      Rule added Rule added (v6)

      Используйте ufw status, чтобы проверить, вступили ли в силу ваши изменения:

      Вывод должен выглядеть так:

      Output

      Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere 80,443/tcp ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6) 80,443/tcp (v6) ALLOW Anywhere (v6)

      Установка Caddy завершена, но вам необходимо настроить его для обслуживания любого контента. В следующем шаге вы должны будете настроить Caddy для обслуживания файлов из директории /var/www.

      Шаг 3 — Настройка Caddy

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

      Начнем с создания базового файла HTML в /var/www с названием index.html​​​:

      • sudo nano /var/www/index.html

      Добавьте следующие строки:

      /var/www/index.html

      <!DOCTYPE html>
      <html>
      <head>
        <title>Hello from Caddy!</title>
      </head>
      <body>
        <h1 style="font-family: sans-serif">This page is being served via Caddy</h1>
      </body>
      </html>
      

      При отображении в веб-браузере этот файл будет отображать заголовок с текстом This page is being served via Caddy​​​. Сохраните и закройте файл.

      Откройте файл конфигурации Caddyfile, созданный вами ранее:

      • sudo nano /etc/caddy/Caddyfile

      Добавьте следующие строки:

      /etc/caddy/Caddyfile

      :80 {
        root /var/www
        gzip
      }
      

      Это базовая конфигурация Caddy, которая объявляет, что порт 80 вашего сервера должен обслуживать файлы из директории /var/www, а для сокращения времени загрузки страниц со стороны клиента будет использоваться сжатие с помощью gzip.

      В большинстве случаев Caddy позволяет выполнять дальнейшую настройку директив конфигурации. Например, вы можете ограничить сжатие gzip только для файлов HTML и PHP и установить уровень сжатия 6 (1 — самый низкий, а 9 — самый высокий), добавив к директиве фигурные скобки и перечислив судбирективы ниже:

      /etc/caddy/Caddyfile

      :80 {
        root /var/www
        gzip {
            ext .html .htm .php
            level 6
        }
      }
      

      После внесения изменений сохраните и закройте файл.

      У Caddy есть огромное количество различных директив для самых разных случаев использования. Например, директива fastcgi может быть полезной для обеспечения возможностей PHP. Директива markdown может быть использована для автоматической конвертации файлов разметки в HTML, прежде чем обслуживать их, что может быть полезно для создания простого блога.

      Чтобы проверить, что все работает корректно, запустите службу Caddy:

      • sudo systemctl start caddy

      Затем запустите systemctl status, чтобы найти информацию о статусе службы Caddy:

      • sudo systemctl status caddy

      Вы увидите следующее:

      Output

      ● caddy.service - Caddy HTTP/2 web server Loaded: loaded (/etc/systemd/system/caddy.service; disabled; vendor preset: enabled) Active: active (running) since Thu 2020-03-12 11:17:49 UTC; 11s ago Docs: https://caddyserver.com/docs Main PID: 3893 (caddy) Tasks: 7 (limit: 1152) CGroup: /system.slice/caddy.service └─3893 /usr/local/bin/caddy -log stdout -log-timestamps=false -agree=true -conf=/etc/caddy/Caddyfile -root=/var/tmp Mar 12 11:17:49 caddy-article-update systemd[1]: Started Caddy HTTP/2 web server. Mar 12 11:17:49 caddy-article-update caddy[3893]: [INFO] Caddy version: v1.0.5 Mar 12 11:17:49 caddy-article-update caddy[3893]: Activating privacy features... done. Mar 12 11:17:49 caddy-article-update caddy[3893]: Serving HTTP on port 80 Mar 12 11:17:49 caddy-article-update caddy[3893]: http:// Mar 12 11:17:49 caddy-article-update caddy[3893]: [INFO] Serving http:// Mar 12 11:17:49 caddy-article-update caddy[3893]: [INFO][cache:0xc00007a7d0] Started certificate maintenance routine Mar 12 11:17:49 caddy-article-update caddy[3893]: [WARNING] Sending telemetry (attempt 1): Post "https://telemetry.caddyserver.com/v1/update/6a8159c4-3427-42 Mar 12 11:17:57 caddy-article-update caddy[3893]: [WARNING] Sending telemetry (attempt 2): Post "https://telemetry.caddyserver.com/v1/update/6a8159c4-3427-42 ...

      Теперь вы можете перейти на IP-адрес вашего сервера в браузере. Вы увидите следующий пример веб-страницы:

      Сообщение от Caddy

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

      Шаг 4 — Использование плагинов

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

      Репозиторий плагина minify на GitHub — hacdias/caddy-minify.

      Перейдите в директорию с исходным кодом, который вы создали в шаге 1:

      Для добавления плагина в Caddy вам потребуется импортировать его в файл caddy.go​​​, который вы использовали при сборке Caddy. Откройте файл caddy.go для редактирования:

      Импортируйте плагин minify, добавив выделенную строку:

      ~/caddy/caddy.go

      package main
      
      import (
          "github.com/caddyserver/caddy/caddy/caddymain"
      
          _ "github.com/hacdias/caddy-minify"
      )
      
      func main() {
          // caddymain.EnableTelemetry = false
          caddymain.Run()
      }
      

      Сохраните и закройте файл.

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

      Каждый раз при добавлении нового плагина необходимо выполнять повторную сборку Caddy. Это объясняется тем, что Go — это компилируемый язык программирования, то есть исходный код преобразуется в машинный код перед выполнением. Изменения, внесенные вами в определение импорта, изменили исходный код, но они не вступят в силу, пока бинарный файл не будет скомпилирован.

      Используйте команду go install для компиляции Caddy:

      После завершения необходимо переместить сгенерированный бинарный файл в /usr/local/bin​​​ и настроить разрешения для бинарного файла, как вы уже делали это ранее. Вы должны выполнять эти шаги каждый раз при выполнении пересборки Caddy, чтобы гарантировать функциональность и безопасность:

      • sudo mv $GOPATH/bin/caddy /usr/local/bin/
      • sudo chown root:root /usr/local/bin/caddy
      • sudo chmod 755 /usr/local/bin/caddy
      • sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/caddy

      Для начала использования плагина minify вам потребуется добавить директиву minify в Caddyfile. Откройте его для редактирования:

      • sudo nano /etc/caddy/Caddyfile

      Активируйте плагин, добавив следующую строку в блок конфигурации:

      /etc/caddy/Caddyfile

      :80 {
        root /var/www
        gzip
        minify
      }
      

      Перезапустите ваш сервер с помощью systemctl:

      • sudo systemctl restart caddy

      Теперь Caddy запущен и будет сжимать любые файлы, которые он обслуживает, включая файл index.html, созданный вами ранее. Вы можете наблюдать за «минимизацией» в работе, запросив содержимое вашего домена с помощью curl:

      Вы должны увидеть следующий вывод: Обратите внимание, что все ненужные пробелы были удалены, что подтверждает, что плагин minify работает.

      Output

      <!doctype html><title>Hello from Caddy!</title><h1 style=font-family:sans-serif>This page is being served via Caddy</h1>

      На этом шаге вы узнали, как расширить функционал Caddy с помощью плагинов. Далее вы должны будете активировать HTTPS, установив плагин tls.dns.digitalocean.

      Шаг 5 — Активация автоматического предоставления TLS с помощью Let’s Encrypt

      В этом разделе вы должны будете активировать автоматическое предоставление и обновление сертификата Let’s Encrypt, используя записи TXT DNS для верификации.

      Для верификации с помощью записей TXT DNS вы должны будете устанавливать плагин для взаимодействия с API DigitalOcean, который называется tls.dns.digitalocean. Процедура установки практически полностью повторяет процедуру установки плагина minify на предыдущем шаге. Для начала откройте caddy.go​​​:

      Добавьте репозиторий плагина в раздел с импортами:

      ~/caddy/caddy.go

      package main
      
      import (
          "github.com/caddyserver/caddy/caddy/caddymain"
      
          _ "github.com/hacdias/caddy-minify"
      
          _ "github.com/caddyserver/dnsproviders/digitalocean"
      )
      
      func main() {
          // caddymain.EnableTelemetry = false
          caddymain.Run()
      }
      

      Воспользуйтесь следующей командой для компиляции файла:

      Убедитесь, что Caddy остановлен с помощью systemctl, после чего завершите установку плагина, скопировав новую сборку Caddy, а затем снова настроив право владения и разрешения:

      • sudo systemctl stop caddy
      • sudo mv $GOPATH/bin/caddy /usr/local/bin/
      • sudo chown root:root /usr/local/bin/caddy
      • sudo chmod 755 /usr/local/bin/caddy
      • sudo setcap 'cap_net_bind_service=+ep' /usr/local/bin/caddy

      Далее необходимо настроить Caddy для работы с API DigitalOcean, чтобы настроить записи DNS. Caddy должен получить доступ к этому токену, используемому в качестве пременной среды, для настройки DNS DigitalOcean, поэтому вы должны будете изменить юнит-файл systemd:

      • sudo nano /etc/systemd/system/caddy.service

      Найдите строку, начинающуюся с Environment= в разделе [Service]. Эта строка определяет переменные среды, которые должны быть переданы в процесс Caddy. Добавьте пробел в конце этой строки, затем добавьте переменную DO_AUTH_TOKEN, за которой должен следовать токен, который вы только что создали:

      /etc/systemd/system/caddy.service

      [Service]
      Restart=on-abnormal
      
      ; User and group the process will run as.
      User=www-data
      Group=www-data
      
      ; Letsencrypt-issued certificates will be written to this directory.
      Environment=CADDYPATH=/etc/ssl/caddy DO_AUTH_TOKEN=your_token_here
      

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

      • sudo systemctl daemon-reload

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

      • sudo systemctl status caddy

      Вывод должен выглядеть следующим образом:

      Output

      ● caddy.service - Caddy HTTP/2 web server Loaded: loaded (/etc/systemd/system/caddy.service; disabled; vendor preset: enabled) Active: inactive (dead) Docs: https://caddyserver.com/docs ...

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

      • sudo nano /etc/caddy/Caddyfile

      Добавьте выделенные строки в Caddyfile, убедившись, что вы заменили your_domain на ваш собственный домен (вместо простого указания порта :80), и закомментируйте gzip:

      /etc/caddy/Caddyfile

      your_domain {
        root /var/www
        #gzip
        minify
        tls {
            dns digitalocean
        }
      }
      

      Использование домена, а не только порта для имени хоста, заставит Caddy обслуживать запросы через HTTPS. Директива tls определяет поведение Caddy при использовании TLS, а субдиректива dns указывает, что Caddy должен использовать систему DNS-01, а не HTTP-01.

      Теперь ваш веб-сайт готов к развертыванию. Запустите Caddy с помощью systemctl, а затем активируйте его, чтобы задать запуск при загрузке:

      • sudo systemctl start caddy
      • sudo systemctl enable caddy

      При открытии вашего домена в браузере вы будете автоматически перенаправлены на HTTPS, после чего будет отображаться то же самое сообщение.

      Ваша установка Caddy завершена и защищена, и вы можете продолжить настройку в соответствии с вашим вариантом использования.

      Если вы захотите обновить Caddy при выходе новой версии, вам потребуется обновить файл go.mod (он хранится в той же директории), который выглядит следующим образом:

      ~/caddy/go.mod

      module caddy
      
      go 1.14
      
      require (
              github.com/caddyserver/caddy v1.0.5
              github.com/caddyserver/dnsproviders v0.4.0
              github.com/hacdias/caddy-minify v1.0.2
      )
      

      Выделенная часть — это используемая вами версия Caddy. Когда новая версия появится на Github (см. страницу с тегами версии), вы можете заменить существующую версию в go.mod на новую, а затем скомпилировать Caddy согласно инструкциям из первых двух шагов. Вы можете сделать то же самое для всех импортируемых плагинов.

      Заключение

      Теперь у вас есть установленная служба Caddy, настроенная на вашем сервере и обслуживающая статичные страницы на желаемом домене, защиту которого обеспечивают TLS-сертификаты Let’s Encrypt.

      Следующим шагом будет разработка способа получения уведомлений при выходе новых версий Caddy. Например, вы можете использовать фид Atom для релизов Caddy или специальную службу, например dependencies.io.

      Дополнительную информацию о настройке Caddy см. в документации Caddy.



      Source link