CoderCastrov logo
CoderCastrov
Парсер

Как автоматизировать генерацию изображений с помощью Puppeteer

Как автоматизировать генерацию изображений с помощью Puppeteer
просмотров
7 мин чтение
#Парсер

Парсинг Discord

This image was generated by Midjourney, powered by my prompt skills.

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

Искусство искусственного интеллекта: использование ChatGPT и Midjourney для создания вдохновляющих визуальных образов

Исследование красоты случайности.

medium.com

Midjourney запрещает автоматизацию в своих условиях использования. Поэтому будьте осторожны и избегайте злоупотребления.

В статье предполагается, что у вас есть подписка на Midjourney, но код можно адаптировать и для бесплатной пробной версии.

Существуют разные способы автоматизации генерации изображений с помощью Midjourney.

Вот статья, описывающая, как это сделать с помощью pyautogui на Python, написанная Майклом Кингом. Есть способы сделать это в безголовом режиме, предоставив путь к виртуальному дисплею для pyautogui или использовав плагин Dummy display, который можно приобрести за несколько долларов на Amazon.

Это подходящее решение для pyautogui, работающего на Raspberry Pi. Однако в статье выше предполагается, что вы используете свой собственный компьютер и, возможно, оставляете скрипт работать на ночь, и не объясняется, как использовать pyautogui в безголовом режиме.

Мы попробуем другой подход.

Мы будем использовать Puppeteer, библиотеку Node.js, которая позволяет управлять Chromium или Chrome через протокол DevTools. Эта библиотека может помочь вам автоматизировать тестирование веб-сайтов, парсить веб-сайты, создавать скриншоты и PDF-файлы и выполнять другие задачи. Если вы не знакомы с Puppeteer, вы можете изучить его основы, обратившись к этому руководству.

Мы начинаем с создания нового проекта и импорта Puppeteer. Нам также нужно определить переменные среды, которые нам понадобятся.

import puppeteer from "puppeteer";
const password = process.env.DISCORD_PASSWORD;
const email = process.env.DISCORD_EMAIL;
const server_name = process.env.DISCORD_SERVER_NAME;

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

export default async (prompt) => {
  const browser = await puppeteer.launch({
    headless: true,
    ignoreHTTPSErrors: true,
    slowMo: 0,
    args: [
      "--disable-gpu",
      "--disable-dev-shm-usage",
      "--disable-setuid-sandbox",
      "--no-first-run",
      "--no-sandbox",
      "--no-zygote",
    ],
  });
  const page = await browser.newPage();
  await page.setUserAgent(
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36"
  );

Вы можете заменить user agent на любой другой. Обратите внимание, что headless установлен в true. В фазе тестирования, локально, я бы предложил установить его в false.

Давайте определим функцию wait. Она позволит нам ждать выполнения JavaScript. Даже если элемент может появиться в дереве DOM, он все равно может быть нереагирующим.

const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

Затем мы попытаемся войти в систему.

await page.goto("https://discord.com/login");
  // Set screen size
  await page.setViewport({ width: 1080, height: 1024 });
  await page.waitForSelector('input[name="email"]');
  //wait for js
  await wait(1000);
  await page.type('input[name="email"]', email);
  await wait(1000);
  await page.type('input[name="password"]', password);
  await wait(2000);

Если вы запускаете скрипт на своем персональном компьютере или на близкой машине, у вас не возникнет проблем. Однако, если вы используете удаленный сервер с новым IP-адресом, механизм защиты от ботов Discord вступит в действие и потребует от пользователей решить hCaptcha-задачи, такие как головоломки и флажки.

hCaptcha

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

Я потратил 3 фунта стерлингов, что позволит получить около 1000 решений. Мы передаем им URL и site-key, который является уникальным UUID, найденным где-то в HTML. После предоставления этих входных данных 2captcha возвращает хэш решения.

import captcha from "2captcha";
const solver = new captcha.Solver(process.env.CAPTCHA_API_KEY);
export default async (siteKey, url) => {
  const obj = await solver.hcaptcha(siteKey, url);
  const { data } = obj;
  return data;
};

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

try {
    await page.click('button[type="submit"]');
    let foundElement = await page.waitForSelector(
      `iframe[data-hcaptcha-widget-id], div[data-dnd-name="${server_name}"]`
    );
    //let's determine what we found
    const tagName = await foundElement.evaluate((el) => el.tagName);
    if (tagName === "IFRAME") {
      const srcString = await foundElement.evaluate((el) => el.src);
      const siteKey = srcString.split("sitekey=")[1].split("&")[0];
      const data = await solver(siteKey, "https://discord.com/login");
      await page.evaluate((token) => {
        const node =
          document.querySelector("iframe").parentElement.parentElement;
        const properties = Object.getOwnPropertyDescriptors(node);
        const keys = Object.keys(properties);
        const reactProp = keys[1];
        document
          .querySelector("iframe")
          .parentElement.parentElement[reactProp].children.props.onVerify(
            token
          );
      }, data);
      foundElement = await page.waitForSelector(
        `div[data-dnd-name="${server_name}"]`
      );
    }

Позвольте мне рассказать подробнее о коде выше. Мы нажимаем submit и ждем, пока Discord загрузится, затем где-то слева появится div с именем вашего сервера, или будет запущен hCaptcha. В последнем случае мы ожидаем увидеть iframe.

await page.click('button[type="submit"]');
    let foundElement = await page.waitForSelector(
      `iframe[data-hcaptcha-widget-id], div[data-dnd-name="${server_name}"]`
    );

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

const tagName = await foundElement.evaluate((el) => el.tagName);

Если это iframe, мы получаем site-key, который является одним из свойств iframe.

const srcString = await foundElement.evaluate((el) => el.src);
const siteKey = srcString.split("sitekey=")[1].split("&")[0];

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

const data = await solver(siteKey, "https://discord.com/login");

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

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

//__reactProps + случайная строка
// будет отличаться каждый раз, когда мы перезагружаем страницу
let react_p = "__reactProps$ksy66ebrux";
const search = (el) => {
  if (!el) return;
  if (el[react_p]?.children?.props) {
    if (Object.keys(el[react_p].children.props).find((el) => el === "onVerify"))
      console.log("Got it!", el.className);
  }
  if (el.children) {
    if (HTMLCollection.prototype.isPrototypeOf(el.children)) {
      for (let i = 0; i < el.children.length; i++) {
        if (el.children[i]) search(el.children[i]);
      }
    } else {
      search(el.children);
    }
  }
};
search(document);

Мы получаем доступ к виртуальному DOM React с помощью react_p и к свойствам узла с помощью .children.props. Как только мы получили имя класса, мы можем напрямую нацеливаться на него. Вернемся к скрипту Puppeteer.

if (tagName === "iframe") {
      const srcString = await foundElement.evaluate((el) => el.src);
      const siteKey = srcString.split("sitekey=")[1].split("&")[0];
      const data = await solver(siteKey, "https://discord.com/login");
      await page.evaluate((token) => {
        const node =
          document.querySelector("iframe").parentElement.parentElement;
        const properties = Object.getOwnPropertyDescriptors(node);
        const keys = Object.keys(properties);
        const reactProp = keys[1];
        document
          .querySelector("iframe")
          .parentElement.parentElement[reactProp].children.props.onVerify(
            token
          );
      }, data);
      foundElement = await page.waitForSelector(
        `div[data-dnd-name="${server_name}"]`
      );
    }

После того, как мы победили hCaptcha, остальное дело техники.

//wait for js
    await wait(1000);
    await foundElement.click();
    await page.waitForSelector("form");
    await page.type("form", `/imagine`);
    await wait(1000);
    await page.type("form", ` `);
    await wait(1000);
    await page.type("form", `${prompt}`);
    await page.keyboard.press("Enter");
    // wait for js to update html tree
    await wait(3000);
    // waiting for the 4ximage to load
    await page.waitForSelector('ol li:last-of-type img[alt="🔄"]', {
      timeout: 1000 * 60 * 4,
    });
    const button = await page.waitForSelector("ol li:last-of-type button");
    //wait for javascript, buttons are not responsive at first
    await wait(3000);
    await button.click();
    //wait for upscale, upscaling the first image
    await page.waitForSelector('ol li:last-of-type img[alt="❤️"]', {
      timeout: 1000 * 60 * 4,
    });
    await wait(1000);
    const image = await page.$('ol li:last-of-type img[alt="Image"]');
    const imageSrc = await image.evaluate((img) => img.src);
    await browser.close();

Код выше также увеличивает первое изображение.

Время ожидания для селекторов может быть значительным, до 5 минут или даже больше. Если вы планируете развернуть его в облаке и активно использовать скрипт, имеет смысл разделить его на три части: запись запроса, увеличение и извлечение URL. Таким образом, вы можете избежать оплаты за время ожидания между процессами и можете вызывать каждый скрипт отдельно. Это ваше решение.

Полный исходный код находится здесь.

rasbign/getImageUrl.js в основной ветке · alekslario/rasbign

Внесите свой вклад в разработку alekslario/rasbign, создав учетную запись на GitHub.

github.com

Проверьте свою электронную почту

Если вы запускаете скрипт с нового IP-адреса, Discord отправит вам письмо с подтверждением, которое вы должны будете немедленно подтвердить.

Есть несколько способов автоматизировать это. Я расскажу об этом в будущих статьях.


Вот и все.

В следующей статье будет рассмотрено, как развернуть его в облаке и настроить Raspberry Pi с дисплеем E-Ink для вызова конечной точки на регулярной основе.

До свидания, друзья.