One place for hosting & domains

      помощью

      Создание REST API с помощью Prisma и PostgreSQL


      Автор выбрал Diversity in Tech Fund для получения пожертвования в рамках программы Write for DOnations.

      Введение

      Prisma — это набор инструментов для работы с базой данных с открытым исходным кодом. Он включает три основных инструмента:

      • Prisma Client: конструктор автоматически генерируемых типобезопасных запросов для Node.js и TypeScript.
      • Prisma Migrate: система моделирования данных и миграции, поддерживающая декларативный подход.
      • Prisma Studio: графический пользовательский интерфейс для просмотра и редактирования данных в вашей базе данных.

      Эти инструменты направлены на повышение производительности разработчика приложения при работе с базой данных. Одним из главных преимуществ Prisma является уровень абстракции, который она предоставляет: вместо формирования сложных SQL запросов или миграций схемы БД, разработчики приложения могут рассуждать о своих данных более интуитивно понятным образом, используя Prisma для работы с базой данных.

      В этом обучающем руководстве мы напишем REST API для небольшого приложения для ведения блога на TypeScript с помощью Prisma и базы данных PostgreSQL. Вы настроите локальную базу данных PostgreSQL с помощью Docker и реализуете маршруты REST API с помощью Express. После выполнения руководства у вас будет работающий на вашем локальном компьютере веб-сервер, который будет отвечать на различные HTTP запросы, а также выполнять чтение и запись в базу данных.

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

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

      Базовое знакомство с TypeScript и REST API будет полезно, но не обязательно для выполнения данного руководства.

      Шаг 1 — Создание проекта TypeScript

      В этом шаге мы настроим простой проект TypeScript с помощью npm. Этот проект послужит основой для REST API, который вы будете создавать при выполнении данного руководства.

      Во-первых, создайте новый каталог для вашего проекта:

      Затем перейдите в этот каталог и инициализируйте создание пустого проекта npm. Обратите внимание, что опция -y здесь означает, что вы хотите пропустить интерактивные запросы команды. Чтобы просмотреть запросы, удалите -y из команды:

      Дополнительную информацию об этих запросах вы можете найти в шаге 1 руководства Использование модулей Node.js с npm и package.json.

      Вы получите примерно следующий вывод с ответами по умолчанию:

      Output

      Wrote to /.../my-blog/package.json: { "name": "my-blog", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

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

      Выполните следующую команду для простой настройки TypeScript:

      • npm install typescript ts-node @types/node --save-dev

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

      • typescript: набор инструментов TypeScript.
      • ts-node: пакет для запуска приложений TypeScript без предварительной компиляции в JavaScript.
      • @types/node: определения типов TypeScript для Node.js.

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

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

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

      my-blog/tsconfig.json

      {
        "compilerOptions": {
          "sourceMap": true,
          "outDir": "dist",
          "strict": true,
          "lib": ["esnext"],
          "esModuleInterop": true
        }
      }
      

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

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

      Вы настроили простой проект TypeScript с помощью npm. После этого вы должны будете настроить свою базу данных PostgreSQL с помощью Docker и подключить Prisma к этой базе данных.

      Шаг 2 — Настройка Prisma с PostgreSQL

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

      Начните с установки Prisma CLI, воспользовавшись следующей командой:

      • npm install @prisma/cli --save-dev

      Рекомендуется использовать локальную установку Prisma CLI для вашего проекта (а не глобальную установку). Это помогает избежать конфликтов версий при наличии нескольких проектов Prisma на вашем компьютере.

      Далее вам нужно настроить вашу базу данных PostgreSQL с помощью Docker. Создайте новый файл Docker Compose с помощью следующей команды:

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

      my-blog/docker-compose.yml

      version: '3.8'
      services:
        postgres:
          image: postgres:10.3
          restart: always
          environment:
            - POSTGRES_USER=sammy
            - POSTGRES_PASSWORD=your_password
          volumes:
            - postgres:/var/lib/postgresql/data
          ports:
            - '5432:5432'
      volumes:
        postgres:
      

      Данный файл Docker Compose настраивает базу данных PostgreSQL, доступ к которой можно получить через порт 5432 контейнера Docker. Обратите внимание, что в настоящее время для базы данных установлены следующие учетные данные: sammy (пользователь) и your_password (пароль). Вы можете изменить эти учетные данные и указать предпочитаемые имя пользователя и пароль. Сохраните и закройте файл.

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

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

      Output

      Pulling postgres (postgres:10.3)... 10.3: Pulling from library/postgres f2aa67a397c4: Pull complete 6de83ca23e55: Pull complete . . . Status: Downloaded newer image for postgres:10.3 Creating my-blog_postgres_1 ... done

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

      Вы получите примерно следующий вывод:

      Output

      CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 8547f8e007ba postgres:10.3 "docker-entrypoint.s…" 3 seconds ago Up 2 seconds 0.0.0.0:5432->5432/tcp my-blog_postgres_1

      После запуска сервера базы данных вы сможете создать вашу настройку Prisma. Запустите следующую команду в Prisma CLI:

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

      Output

      ✔ Your Prisma schema was created at prisma/schema.prisma. You can now open it in your favorite editor.

      Обратите внимание, что рекомендуется использовать префикс npx в каждом вызове Prisma CLI. Это гарантирует, что будет использоваться именно локальная установка клиента.

      После запуска команды Prisma CLI создает новую папку с именем prisma в вашем проекте. Она содержит два следующих файла:

      • schema.prisma: главный файл конфигурации для вашего проекта Prisma (он будет включать вашу модель данных).
      • .env: файл dotenv для определения URL-адреса для подключения к базе данных.

      Чтобы гарантировать, что Prisma будет знать расположение вашей базы данных, откройте файл .env и измените значение переменной среды DATABASE_URL.

      Сначала откройте файл .env:

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

      my-blog/prisma/.env

      DATABASE_URL="postgresql://sammy:your_password@localhost:5432/my-blog?schema=public"
      

      Убедитесь, что вы можете изменить учетные данные для БД на данные, указанные в файле Docker Compose. Дополнительную информацию о формате URL-адреса подключения см. в документации Prisma.

      После выполнения этих действий сохраните и закройте файл.

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

      Шаг 3 — Определение модели данных и создание таблиц базы данных

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

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

      Сначала откройте файл schema.prisma с помощью следующей команды:

      • nano prisma/schema.prisma

      Теперь добавьте в файл следующие определения модели. Вы можете поместить модели в нижней части файла сразу после блока generator client:

      my-blog/prisma/schema.prisma

      . . .
      model User {
        id    Int     @default(autoincrement()) @id
        email String  @unique
        name  String?
        posts Post[]
      }
      
      model Post {
        id        Int     @default(autoincrement()) @id
        title     String
        content   String?
        published Boolean @default(false)
        author    User?   @relation(fields: [authorId], references: tag:www.digitalocean.com,2005:/community/tutorials/how-to-build-a-rest-api-with-prisma-and-postgresql-ru)
        authorId  Int?
      }
      

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

      Вы определяете две модели: User и Post. Каждая из этих моделей имеет ряд полей, которые представляют свойства модели. Модели будут отображаться в таблицах базы данных, а поля представляют отдельные столбцы.

      Обратите внимание на связь один-ко-многим между двумя моделями, определяемую связанными полями posts и author в таблицах User и Post. Это означает, что один пользователь может быть связан с несколькими постами.

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

      • npx prisma migrate save --experimental --create-db --name "init"

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

      • --experimental: обязательная опция, поскольку Prisma Migrate находится в настоящее время в экспериментальном состоянии.
      • --create-db: дает Prisma Migrate возможность создать базу данных с именем my-blog, которое указано в URL-адресе подключения.
      • --name "init": указывает имя миграции (будет использоваться при присвоении имени папке миграции, создаваемой в вашей файловой системе).

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

      Output

      New datamodel: // This is your Prisma schema file, // learn more about it in the docs: https://pris.ly/d/prisma-schema datasource db { provider = "postgresql" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } model User { id Int @default(autoincrement()) @id email String @unique name String? posts Post[] } model Post { id Int @default(autoincrement()) @id title String content String? published Boolean @default(false) author User? @relation(fields: [authorId], references: tag:www.digitalocean.com,2005:/community/tutorials/how-to-build-a-rest-api-with-prisma-and-postgresql-ru) authorId Int? } Prisma Migrate just created your migration 20200811140708-init in migrations/ └─ 20200811140708-init/ └─ steps.json └─ schema.prisma └─ README.md

      Вы можете ознакомиться с созданными миграционными файлами в каталоге prisma/migrations.

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

      • npx prisma migrate up --experimental

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

      Output

      . . . Checking the datasource for potential data loss... Database Changes: Migration Database actions Status 20200811140708-init 2 CreateTable statements. Done 🚀 You can get the detailed db changes with prisma migrate up --experimental --verbose Or read about them here: ./migrations/20200811140708-init/README.md 🚀 Done with 1 migration in 206ms.

      Prisma Migrate генерирует инструкции SQL, необходимые для выполнения миграции, и отправляет их базе данных. Ниже приводятся SQL инструкции, которые создают таблицы:

      CREATE TABLE "public"."User" (
        "id" SERIAL,
        "email" text  NOT NULL ,
        "name" text   ,
        PRIMARY KEY ("id")
      )
      
      CREATE TABLE "public"."Post" (
        "id" SERIAL,
        "title" text  NOT NULL ,
        "content" text   ,
        "published" boolean  NOT NULL DEFAULT false,
        "authorId" integer   ,
        PRIMARY KEY ("id")
      )
      
      CREATE UNIQUE INDEX "User.email" ON "public"."User"("email")
      
      ALTER TABLE "public"."Post" ADD FOREIGN KEY ("authorId")REFERENCES "public"."User"("id") ON DELETE SET NULL ON UPDATE CASCADE
      

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

      Шаг 4 — Знакомство с запросами Prisma Client в форме простого скрипта

      Prisma Client — это инструмент для автоматической генерации типобезопасных запросов, который вы можете использовать для программного чтения и записи данных в базу данных в приложении на базе Node.js или TypeScript. Вы будете использовать его для доступа к базе данных в рамках ваших маршрутов REST API в качестве замены для традиционных ORM, простых запросов SQL, собственных слоев доступа к данным или любого другого метода работы с базой данных.

      В этом шаге вы установите Prisma Client и познакомитесь с запросами, которые он позволяет отправлять. Прежде чем реализовать маршруты для вашего REST API в следующих шагах, вы должны будете познакомиться с запросами Prisma Client в форме простого исполняемого скрипта.

      Перейдите к установке Prisma Client для вашего проекта, открыв терминал и установив пакет npm Prisma Client:

      • npm install @prisma/client

      Затем создайте новый каталог с именем src, который будет содержать исходные файлы:

      Теперь создайте файл TypeScript внутри нового каталога:

      Все запросы Prisma Client возвращают промисы, которые вы можете ожидать в своем коде. Поэтому вы должны отправлять запросы внутри асинхронной функции.

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

      my-blog/src/index.ts

      import { PrismaClient } from '@prisma/client'
      
      const prisma = new PrismaClient()
      
      async function main() {
        // ... your Prisma Client queries will go here
      }
      
      main()
        .catch((e) => console.error(e))
        .finally(async () => await prisma.disconnect())
      

      Ниже приводится короткое пояснения шаблонного кода:

      1. Импорт конструктора PrismaClient из ранее установленного пакета npm @prisma/client.
      2. Создание объекта PrismaClient с помощью вызова конструктора и получения экземпляра с именем prisma.
      3. Определение асинхронной функции с именем main, куда вы позже будете добавлять запросы Prisma Client.
      4. Вызов функции main вместе с перехватом любых возможных исключений, а также гарантия того, что Prisma Client будет закрывать все открытые подключения к базе данных с помощью вызова prisma.disconnect().

      Получив в распоряжение функцию main, вы можете начинать добавлять запросы Prisma Client в скрипт. Измените содержание файла index.ts, которое должно выглядеть следующим образом:

      my-blog/src/index.ts

      import { PrismaClient } from '@prisma/client'
      
      const prisma = new PrismaClient()
      
      async function main() {
        const newUser = await prisma.user.create({
          data: {
            name: 'Alice',
            email: 'alice@prisma.io',
            posts: {
              create: {
                title: 'Hello World',
              },
            },
          },
        })
        console.log('Created new user: ', newUser)
      
        const allUsers = await prisma.user.findMany({
          include: { posts: true },
        })
        console.log('All users: ')
        console.dir(allUsers, { depth: null })
      }
      
      main()
        .catch((e) => console.error(e))
        .finally(async () => await prisma.disconnect())
      

      В этом коде вы используете два запроса Prisma Client:

      • create: создает новую запись User. Обратите внимание, что вы используете вложенную запись, что означает, что вы создаете записи User и Post в одном запросе.
      • findMany: считывает все существующие записи User из базы данных. Вы указываете опцию include, которая дополнительно загружает связанные записи Post для каждой записи User.

      Теперь запустите скрипт с помощью следующей команды:

      В терминале вы получили следующий вывод:

      Output

      Created new user: { id: 1, email: 'alice@prisma.io', name: 'Alice' } [ { id: 1, email: 'alice@prisma.io', name: 'Alice', posts: [ { id: 1, title: 'Hello World', content: null, published: false, authorId: 1 } ] }

      Примечание: если вы используете графический пользовательский интерфейс для базы данных, то сможете проверить, были ли созданы записи, посмотрев таблицы User и Post. Также вы можете просмотреть данные с помощью Prisma Studio, запустив команду npx prisma studio --experimental.

      Вы успешно воспользовались Prisma Client для чтения и записи данных в вашей базе данных. В оставшихся шагах вы будете применять эти новые знания для реализации маршрутов на образце REST API.

      Шаг 5 — Реализация вашего первого маршрута REST API

      В этом шаге вы установите Express для вашего приложения. Express — это популярный веб-фреймворк для Node.js, который вы будете использовать для имплементации ваших маршрутов REST API в рамках данного проекта. Первый маршрут, который вы будете реализовать, позволит вам получить всех пользователей из API с помощью запроса GET. Данные пользователя будут получены из базы данных с помощью Prisma Client.

      Начните с установки Express с помощью следующей команды:

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

      • npm install @types/express --save-dev

      После получения зависимостей вы можете перейти к настройке вашего приложения Express.

      Начните с открытия вашего главного исходного файла еще раз:

      Теперь удалите весь код в файле index.ts и замените его на следующий код для запуска вашего REST API:

      my-blog/src/index.ts

      import { PrismaClient } from '@prisma/client'
      import express from 'express'
      
      const prisma = new PrismaClient()
      const app = express()
      
      app.use(express.json())
      
      // ... your REST API routes will go here
      
      app.listen(3000, () =>
        console.log('REST API server ready at: http://localhost:3000'),
      )
      

      Ниже приводится краткое описание элементов кода:

      1. Импорт PrismaClient и express из соответствующих пакетов npm.
      2. Создание объекта PrismaClient с помощью вызова конструктора и получения экземпляра с именем prisma.
      3. Создание вашего приложения Express с помощью вызова express().
      4. Добавление промежуточного слоя ПО с помощью вызова express.json() для обеспечения возможности корректной обработки данных в формате JSON с помощью Express.
      5. Запуск сервера на порту 3000.

      Теперь вы можете реализовать свой первый маршрут. Между вызовами app.use и app.listen добавьте следующий код:

      my-blog/src/index.ts

      . . .
      app.use(express.json())
      
      app.get('/users', async (req, res) => {
        const users = await prisma.user.findMany()
        res.json(users)
      })
      
      app.listen(3000, () =>
      console.log('REST API server ready at: http://localhost:3000'),
      )
      

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

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

      Output

      REST API server ready at: http://localhost:3000

      Чтобы получить доступ к маршруту /users, вы можете указать в адресной строке браузера http://localhost:3000/users или воспользоваться любым другим клиентом HTTP.

      В этом руководстве мы будем тестировать все маршруты REST API с помощью curl, HTTP клиента для командной строки.

      Примечание: если вы предпочитаете использовать клиент HTTP с графическим пользовательским интерфейсом, вы можете использовать такие альтернативные варианты, как Postwoman или Advanced REST Client.

      Чтобы протестировать маршрут, откройте новое окно терминала или вкладку (чтобы ваш локальный веб-сервер мог продолжать работу) и выполните следующую команду:

      • curl http://localhost:3000/users

      Вы получите данные сущности User, которую вы создали в предыдущем шаге:

      Output

      [{"id":1,"email":"alice@prisma.io","name":"Alice"}]

      Обратите внимание, что в этот раз массив posts не включен в результат. Это происходит потому, что вы не передаете опцию include в вызов findMany при реализации маршрута /users.

      Вы успешно реализовали свой первый маршрут REST API в /users. В следующем шаге вы будете реализовывать оставшиеся маршруты REST API, чтобы добавить дополнительный функционал для вашего API.

      Шаг 6 — Реализация остальных маршрутов REST API

      В этом шаге вы будете реализовывать оставшиеся маршруты REST API для вашего приложения для ведения блога. В результате ваш веб-сервер будет обслуживать различные запросы типа GET, POST, PUT и DELETE.

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

      Метод HTTP Маршрут Описание
      GET /feed Получает все опубликованные посты.
      GET /post/:id Получает определенный пост по его идентификатору.
      POST /user Создает нового пользователя.
      POST /post Создает новый пост (в виде черновика).
      PUT /post/publish/:id Задает для поля published поста значение true.
      DELETE post/:id Удаляет пост по его идентификатору.

      Теперь вы можете перейти к реализации остальных маршрутов GET.

      Откройте файл index.ts с помощью следующей команды:

      Затем добавьте следующий код под реализацией маршрута /users:

      my-blog/src/index.ts

      . . .
      
      app.get('/feed', async (req, res) => {
        const posts = await prisma.post.findMany({
          where: { published: true },
          include: { author: true }
        })
        res.json(posts)
      })
      
      app.get(`/post/:id`, async (req, res) => {
        const { id } = req.params
        const post = await prisma.post.findOne({
          where: { id: Number(id) },
        })
        res.json(post)
      })
      
      app.listen(3000, () =>
        console.log('REST API server ready at: http://localhost:3000'),
      )
      

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

      Этот код реализует маршруты API для двух запросов GET:

      • /feed: возвращает список опубликованных постов.
      • /post/:id: возвращает конкретный пост по его идентификатору.

      Prisma Client используется в обеих реализациях. В реализации маршрута /feed запрос, который вы отправляете с помощью Prisma Client, отфильтровывает все записи Post, где в колонке published указано значение true. Кроме того, запрос Prisma Client использует опцию include для получения связанной информации об авторе для каждого возвращаемого поста. В реализации маршрута /post/:id вы передаете идентификатор, который будет получен из URL-адреса, для чтения конкретной записи Post из базы данных.

      Вы можете остановить работу сервера, нажав CTRL+C на клавиатуре. Затем перезапустите его с помощью следующей команды:

      Чтобы протестировать маршрут /feed, вы можете воспользоваться следующей командой curl:

      • curl http://localhost:3000/feed

      Поскольку ни один пост еще не был опубликован, ответ будет содержать пустой массив:

      Output

      []

      Чтобы протестировать маршрут /post/:id, вы можете воспользоваться следующей командой curl:

      • curl http://localhost:3000/post/1

      Она вернет пост, созданный вами ранее:

      Output

      {"id":1,"title":"Hello World","content":null,"published":false,"authorId":1}

      Далее реализуйте два маршрута POST. Добавьте следующий код в файл index.ts под реализацией трех маршрутов GET:

      my-blog/src/index.ts

      . . .
      
      app.post(`/user`, async (req, res) => {
        const result = await prisma.user.create({
          data: { ...req.body },
        })
        res.json(result)
      })
      
      app.post(`/post`, async (req, res) => {
        const { title, content, authorEmail } = req.body
        const result = await prisma.post.create({
          data: {
            title,
            content,
            published: false,
            author: { connect: { email: authorEmail } },
          },
        })
        res.json(result)
      })
      
      app.listen(3000, () =>
        console.log('REST API server ready at: http://localhost:3000'),
      )
      

      После этого сохраните и закройте файл.

      Этот код реализует маршруты API для двух запросов POST:

      • /user: создает нового пользователя в базе данных.
      • /post: создает новый пост в базе данных.

      Как и ранее, Prisma Client используется в обеих реализациях. В реализации маршрута /user вы передаете значения из тела запроса HTTP в запрос create Prisma Client.

      Маршрут /post имеет немного более сложную структуру: здесь вы не можете прямо передавать значения из тела HTTP запроса, вместо этого вам сначала нужно вручную извлечь их, а уже после этого передать эти полученные значения в запрос Prisma Client. Это вызвано тем, что структура JSON в теле запроса не соответствует структуре, которую ожидает Prisma Client, поэтому вам нужно вручную создать ожидаемую структуру.

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

      Чтобы создать нового пользователя с помощью маршрута /user, вы можете отправить следующий POST запрос с помощью curl:

      • curl -X POST -H "Content-Type: application/json" -d '{"name":"Bob", "email":"bob@prisma.io"}' http://localhost:3000/user

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

      Output

      {"id":2,"email":"bob@prisma.io","name":"Bob"}

      Чтобы создать новый пост с помощью маршрута /post, вы можете отправить следующий POST запрос с помощью curl:

      • curl -X POST -H "Content-Type: application/json" -d '{"title":"I am Bob", "authorEmail":"bob@prisma.io"}' http://localhost:3000/post

      В результате в базе данных будет создан новый пост, который будет привязан к пользователю с электронной почтой bob@prisma.io. В консоль будет выведен следующий текст:

      Output

      {"id":2,"title":"I am Bob","content":null,"published":false,"authorId":2}

      В заключение вы можете реализовать маршруты PUT и DELETE.

      Откройте файл index.ts с помощью следующей команды:

      Затем под реализацией двух маршрутов POST добавьте выделенный код:

      my-blog/src/index.ts

      . . .
      
      app.put('/post/publish/:id', async (req, res) => {
        const { id } = req.params
        const post = await prisma.post.update({
          where: { id: Number(id) },
          data: { published: true },
        })
        res.json(post)
      })
      
      app.delete(`/post/:id`, async (req, res) => {
        const { id } = req.params
        const post = await prisma.post.delete({
          where: { id: Number(id) },
        })
        res.json(post)
      })
      
      app.listen(3000, () =>
        console.log('REST API server ready at: http://localhost:3000'),
      )
      

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

      Этот код реализует маршруты API для одного запроса PUT и одного запроса DELETE:

      • /post/publish/:id (PUT): публикует пост по его идентификатору.
      • /post/:id(DELETE): удаляет пост по его идентификатору.

      Prisma Client снова используется в обеих реализациях. В реализации маршрута /post/publish/:id идентификатор поста, который будет опубликован, извлекается из URL-адреса и передается в запрос update Prisma Client. Реализация маршрута /post/:id, который будет удалять пост в базе данных, также извлекает идентификатор поста из URL-адреса и передает его в запрос delete Prisma Client.

      Еще раз остановите сервер, нажав CTRL+C на клавиатуре. Затем перезапустите его с помощью следующей команды:

      Вы можете протестировать маршрут PUT с помощью следующей команды curl:

      • curl -X PUT http://localhost:3000/post/publish/2

      В этом случае пост будет опубликован с идентификатором, равным 2. Если вы повторно отправите запрос для маршрута /feed, этот пост будет добавлен в ответ.

      Далее вы можете протестировать маршрут DELETE с помощью следующей команды curl:

      • curl -X DELETE http://localhost:3000/post/1

      В результате выполнения этой команды будет удален пост с идентификатором 1. Чтобы убедиться, что пост с этим идентификатором был удален, повторно отправьте запрос GET для маршрута /post/1.

      В этом шаге вы реализовали оставшиеся маршруты REST API для вашего приложения для ведения блога. API теперь отвечает на различные запросы GET, POST, PUT и DELETE и реализует функционал для чтения и записи данных в базе данных.

      Заключение

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

      В качестве следующих шагов вы можете реализовать дополнительные маршруты API или расширить схему вашей базы данных с помощью Prisma Migrate. Обязательно ознакомьтесь с документацией Prisma для получения информации о различных аспектах Prisma и изучите ряд готовых проектов в репозитории prisma-examples, воспользовавшись такими инструментами, как GraphQL или grPC API.



      Source link

      Создание сайта Drupal 9 на локальном компьютере с помощью Docker и DDEV


      Автор выбрал Diversity in Tech Fund для получения пожертвования в рамках программы Write for DOnations.

      Введение

      DDEV — это инструмент с открытым исходным кодом, использующий Docker для создания локальной среды разработки для различных структур PHP. Благодаря возможностям контейнеризации DDEV может существенно упростить работу над несколькими проектами, использующими разные технологические комплексы и несколько облачных серверов. В состав DDEV входят шаблоны для WordPress, Laravel, Magento, TYPO3, Drupal и т. д.

      Версия Drupal 9 была выпущена 3 июня 2020 г. для Drupal CMS. Drupal — это популярная структура PHP для создания и обслуживания сайтов и приложений любого масштаба, отличающаяся удобством использования и большой библиотекой модулей и тем.

      В этом обучающем модуле вы начнете создавать сайт Drupal 9 на своем локальном компьютере, используя DDEV. Это позволит вам сначала создать свой сайт, а затем, когда вы будете готовы, развернуть проект на производственном сервере.

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

      Для данного обучающего руководства вам потребуется следующее:

      Примечание. Вы можете создать сайт Drupal 9 с помощью DDEV на удаленном сервере, но в этом случае вам потребуется решение для доступа к localhost в браузере. Команда DDEV ddev share работает с ngrok, создавая защищенный туннель к серверу для вас и других пользователей для просмотра сайта в разработке. Для личного использования вы также можете установить графический пользовательский интерфейс на удаленном сервере и подключаться к сайту в разработке через браузер, используя этот интерфейс. Для этого следуйте указаниям руководства по установке и настройке VNC в Ubuntu 20.04. Чтобы графический пользовательский интерфейс был еще быстрее, следуйте указаниям нашего руководства по настройке удаленного рабочего места с помощью X2Go в Ubuntu 20.04.

      Шаг 1 — Установка DDEV

      На этом шаге мы установим DDEV на локальный компьютер. Вариант 1 содержит инструкции для macOS, а Вариант 2 — инструкции для Linux. Мы тестировали этот обучающий модуль с DDEV версии 1.15.0.

      Вариант 1 — Установка DDEV в macOS

      DDEV рекомендует пользователям macOS использовать для установки инструмента диспетчер пакетов Homebrew. Используйте следующую команду brew для установки последнего стабильного выпуска:

      • brew tap drud/ddev && brew install drud/ddev/ddev

      Если вы предпочитаете абсолютно новейшую версию, вы можете использовать brew для установки ddev-edge:

      • brew tap drud/ddev-edge && brew install drud/ddev-edge/ddev

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

      • ddev poweroff
      • brew upgrade ddev

      После установки или обновления DDEV используйте команду ddev version для проверки вашего программного обеспечения:

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

      Output

      DDEV-Local version v1.15.0 commit v1.15.0 db drud/ddev-dbserver-mariadb-10.2:v1.15.0 dba phpmyadmin/phpmyadmin:5 ddev-ssh-agent drud/ddev-ssh-agent:v1.15.0 docker 19.03.8 docker-compose 1.25.5 os darwin router drud/ddev-router:v1.15.0 web drud/ddev-webserver:v1.15.0

      DDEV включает мощный инструмент CLI (или интерфейс командной строки). Запустите ddev, чтобы узнать о самых распространенных командах:

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

      Output

      Create and maintain a local web development environment. Docs: https://ddev.readthedocs.io Support: https://ddev.readthedocs.io/en/stable/#support Usage: ddev [command] Available Commands: auth A collection of authentication commands composer Executes a composer command within the web container config Create or modify a ddev project configuration in the current directory debug A collection of debugging commands delete Remove all project information (including database) for an existing project describe Get a detailed description of a running ddev project. exec Execute a shell command in the container for a service. Uses the web service by default. export-db Dump a database to a file or to stdout help Help about any command hostname Manage your hostfile entries. import-db Import a sql file into the project. import-files Pull the uploaded files directory of an existing project to the default public upload directory of your project. list List projects logs Get the logs from your running services. pause uses 'docker stop' to pause/stop the containers belonging to a project. poweroff Completely stop all projects and containers pull Pull files and database using a configured provider plugin. restart Restart a project or several projects. restore-snapshot Restore a project's database to the provided snapshot version. sequelpro This command is not available since sequel pro.app is not installed share Share project on the internet via ngrok. snapshot Create a database snapshot for one or more projects. ssh Starts a shell session in the container for a service. Uses web service by default. start Start a ddev project. stop Stop and remove the containers of a project. Does not lose or harm anything unless you add --remove-data. version print ddev version and component versions Flags: -h, --help help for ddev -j, --json-output If true, user-oriented output will be in JSON format. -v, --version version for ddev Use "ddev [command] --help" for more information about a command.

      Дополнительную информацию об использовании DDEV CLI можно найти в официальной документации по DDEV.

      После установки DDEV на локальном компьютере вы будете готовы установить Drupal 9 и начать разработку сайта.

      Вариант 2 — Установка DDEV в Linux

      В операционной системе Linux вы можете установить DDEV, используя Homebrew для Linux или официальный скрипт для установки. Для начала обновите в Ubuntu список пакетов в диспетчере пакетов apt (вы можете использовать apt в Debian или аналогичный диспетчер пакетов для вашего дистрибутива Linux):

      Установите обязательные пакеты из официального репозитория Ubuntu:

      • sudo apt install build-essential apt-transport-https ca-certificates software-properties-common curl

      Эти пакеты позволят вам загрузить сценарий установки DDEV из официального репозитория на GitHub.

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

      • curl -O https://raw.githubusercontent.com/drud/ddev/master/scripts/install_ddev.sh

      Перед запуском скрипта откройте его в nano или предпочитаемом текстовом редакторе и проверьте его содержимое:

      nano install_ddev.sh
      

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

      Используйте команду chmod, чтобы сделать скрипт исполняемым:

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

      В процессе установки вам может понадобиться подтвердить некоторые настройки или ввести пароль пользователя sudo. После завершения установки DDEV будет доступен в вашей операционной системе Linux.

      Запустите команду ddev version для проверки программного обеспечения:

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

      Output

      DDEV-Local version v1.15.0 commit v1.15.0 db drud/ddev-dbserver-mariadb-10.2:v1.15.0 dba phpmyadmin/phpmyadmin:5 ddev-ssh-agent drud/ddev-ssh-agent:v1.15.0 docker 19.03.8 docker-compose 1.25.5 os linux router drud/ddev-router:v1.15.0 web drud/ddev-webserver:v1.15.0

      DDEV — мощный инструмент CLI (интерфейс командной строки). Запустите команду ddev без опций, чтобы узнать о некоторых распространенных командах:

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

      Output

      Create and maintain a local web development environment. Docs: https://ddev.readthedocs.io Support: https://ddev.readthedocs.io/en/stable/#support Usage: ddev [command] Available Commands: auth A collection of authentication commands composer Executes a composer command within the web container config Create or modify a ddev project configuration in the current directory debug A collection of debugging commands delete Remove all project information (including database) for an existing project describe Get a detailed description of a running ddev project. exec Execute a shell command in the container for a service. Uses the web service by default. export-db Dump a database to a file or to stdout help Help about any command hostname Manage your hostfile entries. import-db Import a sql file into the project. import-files Pull the uploaded files directory of an existing project to the default public upload directory of your project. list List projects logs Get the logs from your running services. pause uses 'docker stop' to pause/stop the containers belonging to a project. poweroff Completely stop all projects and containers pull Pull files and database using a configured provider plugin. restart Restart a project or several projects. restore-snapshot Restore a project's database to the provided snapshot version. sequelpro This command is not available since sequel pro.app is not installed share Share project on the internet via ngrok. snapshot Create a database snapshot for one or more projects. ssh Starts a shell session in the container for a service. Uses web service by default. start Start a ddev project. stop Stop and remove the containers of a project. Does not lose or harm anything unless you add --remove-data. version print ddev version and component versions Flags: -h, --help help for ddev -j, --json-output If true, user-oriented output will be in JSON format. -v, --version version for ddev Use "ddev [command] --help" for more information about a command.

      Дополнительную информацию об использовании DDEV CLI можно найти в официальной документации по DDEV.

      После установки DDEV на локальном компьютере вы будете готовы установить Drupal 9 и начать разработку сайта.

      Шаг 2 — Развертывание нового сайта Drupal 9 с помощью DDEV

      Запустив DDEV, вы можете использовать его для создания файловой системы Drupal, установки Drupal 9 и создания стандартного проекта сайта.

      Вначале мы создадим корневой каталог проекта и перейдем в него. Все остальные команды мы будем запускать отсюда. В этом обучающем модуле мы будем использовать имя каталога d9test, но вы можете выбрать любое предпочитаемое имя. Обратите внимание, что DDEV не очень хорошо обрабатывает имена с дефисами. Желательно избегать использования таких имен каталогов, как my-project или drupal-site-1.

      Создайте корневой каталог проекта и перейдите в него:

      DDEV отлично создает деревья каталогов, соответствующие определенным платформам CMS. Используйте команду ddev config для создания специальной структуры каталогов для Drupal 9:

      • ddev config --project-type=drupal9 --docroot=web --create-docroot

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

      Output

      Creating a new ddev project config in the current directory (/Users/sammy/d9test) Once completed, your configuration will be written to /Users/sammy/d9test/.ddev/config.yaml Created docroot at /Users/sammy/d9test/web You have specified a project type of drupal9 but no project of that type is found in /Users/sammy/d9test/web Ensuring write permissions for d9new No settings.php file exists, creating one Existing settings.php file includes settings.ddev.php Configuration complete. You may now run 'ddev start'.

      Поскольку вы передали аргумент --project-type=drupal9 в команду ddev config, DDEV создаст несколько подкаталогов и файлов, используемых по умолчанию для организации сайта Drupal. Ваше дерево каталогов проекта теперь будет выглядеть так:

      A Drupal 9 directory tree

      .
      ├── .ddev
      │   ├── .gitignore
      │   ├── config.yaml
      │   ├── db-build
      │   │   └── Dockerfile.example
      │   └── web-build
      │       └── Dockerfile.example
      └── web
          └── sites
              └── default
                  ├── .gitignore
                  ├── settings.ddev.php
                  └── settings.php
      
      6 directories, 7 files
      

      .ddev/ будет главной папкой для конфигурации ddev. web/ будет корневым каталогом документов нового проекта и будет содержать определенные файлы settings. Вы подготовили структуру для создания нового проекта Drupal.

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

      Используйте команду ddev start для инициализации платформы:

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

      Output

      ... Successfully started d9test Project can be reached at http://d9test.ddev.site http://127.0.0.1:32773

      Примечание. Помните, что DDEV запускает контейнеры Docker скрыто. Если вы хотите просмотреть эти контейнеры или убедиться, что они работают, используйте команду docker ps:

      Вместе с другими запущенными контейнерами вы увидите четыре новых контейнера с отдельными образами: php-myadmin, ddev-webserver, ddev-router и ddev-dbserver-mariadb.

      Команда ddev start успешно построила контейнеры и вывела результаты с двумя URL. Хотя в выводимом сообщении говорится, что проект можно найти по URL-адресам http://d9test.ddev.site и http://127.0.0.1:32773, сейчас при попытке открыть любой из этих URL будет выведено сообщение об ошибке. Начиная с Drupal 8, ядро Drupal и модули contrib работают как зависимости. Поэтому нужно предварительно завершить установку Drupal с помощью диспетчера пакетов Composer для проектов PHP, прежде чем в браузере получится что-либо загрузить.

      Одна из самых полезных и элегантных возможностей DDEV — возможность передачи команд Composer через DDEV CLI и в среду с контейнерами. Это означает, что вы можете отделить конфигурацию вашего компьютера от среды разработки. Вам больше не нужно будет управлять путями к различным файлам и зависимостями или решать проблемы с версиями, часто возникающие при разработке PHP на локальном компьютере. Более того, вы сможете быстро и с минимумом трудозатрат переключаться между разными проектами, использующими разные структуры и технологические комплексы.

      Используйте команду ddev composer для загрузки проекта drupal/recommended-project. Она загрузит ядро Drupal, библиотеки и другие связанные ресурсы, а затем создаст проект по умолчанию:

      • ddev composer create "drupal/recommended-project"

      Загрузите заключительный компонент Drush (оболочка Drupal). В этом обучающем модуле мы будем использовать только одну команду drush и предложим альтернативный вариант, но в целом drush — это мощный интерфейс командной строки для разработки Drupal, который может повысить эффективность вашей работы.

      Используйте команду ddev composer для установки drush:

      • ddev composer require "drush/drush"

      Мы создали проект Drupal 9 по умолчанию и выполнили установку drush. Теперь мы откроем проект в браузере и настроим параметры сайта.

      Шаг 3 — Настройка проекта Drupal 9

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

      Откроется стандартный мастер установки Drupal.

      Установщик Drupal 9 из браузера

      Теперь у вас есть два варианта. Вы можете использовать этот пользовательский интерфейс и следовать указаниям мастера по установке, или вы можете вернуться в терминал и передать команду drush через ddev. Последний вариант автоматизирует процесс установки и задает admin как имя пользователя и пароль.

      Вариант 1 — Использование мастера

      Снова откройте мастер в браузере. В разделе Choose language (Выбор языка) выберите язык из выпадающего меню и нажмите Save and continue (Сохранить и продолжить). Выберите профиль установки. Вы можете выбрать вариант Standard (Стандартный), Minimal (Минимальный) или Demo (Демо). Сделайте выбор и нажмите Save and continue (Сохранить и продолжить). Drupal автоматически проверит ваши требования, настроит базу данных и установит ваш сайт. Последним шагом будет настройка нескольких конфигураций. Добавьте имя сайта и адрес электронной почты сайта с вашим доменом. Затем выберите имя пользователя и пароль. Выберите надежный пароль и сохраните ваши учетные данные в безопасном месте. В заключение добавьте частный адрес электронной почты, который вы регулярно проверяете, внесите региональные настройки и нажмите Save and continue (Сохранить и продолжить).

      Приветственное сообщение Drupal 9 с предупреждением о разрешениях

      Ваш новый сайт загрузится и выведет приветственное сообщение.

      Вариант 2 — Использование командной строки

      Запустите в корневом каталоге проекта следующую команду ddev exec для установки сайта Drupal по умолчанию с помощью drush:

      • ddev exec drush site:install --account-name=admin --account-pass=admin

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

      Запустите сайт для просмотра в браузере:

      Теперь вы готовы начать создание своего сайта, но предварительно рекомендуется проверить наличие правильных разрешений для каталога /sites/web/default. При локальной работе это не составляет существенной проблемы, но если вы перенесете эти разрешения на производственный сервер, возникнет риск для безопасности.

      Шаг 4 — Проверка разрешений

      Во время установки мастера или при первой загрузке приветственной страницы вы можете увидеть предупреждение о настройках разрешений для каталога /sites/web/default и одного файла внутри этого каталога: settings.php.

      После запуска установочного скрипта Drupal попробует установить для каталога web/sites/default разрешения на чтение и исполнение для всех групп: это параметр разрешений 555. Также он попытается установить для default/settings.php разрешение «только чтение» или 444. Если вы увидите это предупреждение, запустите эти две команды chmod из корневого каталога проекта. В противном случае возникнет риск для безопасности:

      • chmod 555 web/sites/default
      • chmod 444 web/sites/default/settings.php

      Чтобы убедиться в наличии правильных разрешений, запустите эту команду ls с аргументами a, l, h и d:

      • ls -alhd web/sites/default web/sites/default/settings.php

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

      Output

      dr-xr-xr-x 8 sammy staff 256 Jul 21 12:56 web/sites/default -r--r--r-- 1 sammy staff 249 Jul 21 12:12 web/sites/default/settings.php

      Вы готовы начать создание сайта Drupal 9 на локальном компьютере.

      Шаг 5 — Создание первого поста в Drupal

      Чтобы протестировать определенную функцию Drupal, вы создадите пост с помощью пользовательского веб-интерфейса.

      Нажмите на начальной странице сайта кнопку Content (Содержимое) в левом верхнем меню. Нажмите синюю кнопку Add content (Добавить содержимое). Откроется новая страница. Нажмите Article (Статья), и появится еще одна страница.

      Строка создания статьи в Drupal 9

      Добавьте любые предпочитаемые заголовок и содержимое. Также вы можете добавить изображение, например, одни из обоев DigitalOcean. Когда вы будете готовы, нажмите синюю кнопку Save (Сохранить).

      На вашем сайте появится ваш первый пост.

      Созданный пост в Drupal 9

      Теперь вы создаете сайт Drupal 9 на локальном компьютере без взаимодействия с сервером благодаря Docker и DDEV. На следующем шаге мы будем управлять контейнером DDEV с учетом вашего рабочего процесса.

      Шаг 6 — Управление контейнером DDEV

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

      Вы можете остановить DDEV в любое время для освобождения ресурсов. Запустите следующую команду в каталоге вашего проекта:

      DDEV доступен глобально, так что вы сможете запускать команды ddev из любого места, если вы зададите проект DDEV:

      Также вы можете просмотреть все проекты одновременно с помощью команды ddev list:

      В DDEV имеется много других полезных команд.

      Вы можете перезапустить DDEV и продолжить разработку на локальном компьютере в любое время.

      Заключение

      В этом обучающем модуле мы использовали Docker и возможности контейнеризации для разработки сайта Drupal на локальной системе с помощью DDEV. Также DDEV интегрируется с несколькими интегрированными средами разработки и имеет встроенную поддержку отладки PHP для Atom, PHPStorm и Visual Studio Code (vscode). Далее вы можете узнать больше о создании сред разработки для Drupal с DDEV или для разработки с использованием других структур PHP, таких как WordPress.



      Source link

      Скрейпинг веб-сайта с помощью 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