См. исходный код

Делаем это простым способом

Я люблю простоту! кто не прав? поэтому один из способов решения проблемы:

  1. используя встроенный модуль https, чтобы сделать запрос непосредственно на заранее определенный URL-адрес.
  2. поскольку данные поступают порциями, нам нужно использовать поток для сбора данных, после того как они будут сохранены в html-файле.
  3. разобрать вещь.
  4. Отправьте локальное уведомление или запрос API, или RPC, или кого-то еще, кого вы хотите предупредить о новом задании.

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

const fs = require('fs');
const http = require('https');
const url = 'https://www.upwork.com/ab/jobs/search/?q=javascript&sort=recency'
http.get(url, (res) => {
    let data = '';
    res.on('data', (chunk) => {
        data += chunk;
    });
    res.on('end', () => {
        // save it as html file
        fs.writeFile('upwork.html', data, (err) => {
            if (err) throw err;
            // if a match (your jobs keyword matches ) found send a notification
            if (parse(data)) {
                sendNotification();
            }
        });
    });
}).on("error", (err) => {
    // handle error 
});

хммм ! не так быстро!!! на шаге 3 появляется облачная вспышка !!

Зал ожидания Cloudflare

Upwork — это клиент cloudflare, а cloudflare не любит ботов, как вы могли заметить!! если вы не знакомы с cloudflare, это один из крупнейших провайдеров CDN в мире, или, по крайней мере, это то, чем они наиболее известны, но они также предоставляют кучу других услуг, таких как балансировка нагрузки, брандмауэры и т. д. Служба, которая сейчас заблокировала нас, — это WAF (брандмауэр веб-приложений) — брандмауэр, защищающий веб-приложения от вредоносных программ (атаки ddos, межсайтовый скриптинг) и предоставляющий полезные функции, такие как оптимизация производительности, контроль кеша и многое другое. управление ботами — одна из фич. боты, если их не контролировать, могут нанести большой ущерб веб-ресурсам, потребляя ресурсы и потенциально вызывая атаку типа отказ в обслуживании. поэтому cloudflare управляет этими матричными существами, используя поведенческий анализ и машинное обучение. но ждать !! а как насчет хороших ботов? сканеры поисковых систем (google, bing), инструменты мониторинга производительности и другие вещи, необходимые для правильной работы Интернета! Итак, cloudflare различает хороших и плохих ботов, занося их в белый и черный список плохих. ты и я, как ты уже понял, плохие боты!!

Как обойти зал ожидания cloudflare

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

Puppeteer — это библиотека узлов, предоставляющая высокоуровневый API для управления Chrome или Chromium по протоколу DevTools. Его также можно настроить для использования полного (не безголового) Chrome или Chromium. дело в том, что вы можете управлять безголовым браузером и заставить его делать все, что может делать настоящий человек, например, щелкать ссылки, прокручивать страницы и заполнять формы. И если Cloudflare или любой менеджер ботов доставляет вам неприятности и думает, что вы бот, вы можете использовать Puppeteer, чтобы добавить немного случайности в свой скрипт, введя задержки и движения мыши, которые сделают ваш браузер менее роботизированным. Например, вы можете использовать Puppeteer, чтобы сделать паузу в браузере на произвольное время, прежде чем щелкнуть ссылку, или перемещать мышь в случайном порядке перед заполнением формы. Это затруднит обнаружение Cloudflare того, что вы используете бота.

так что мы собираемся использовать кукольник, чтобы солгать клаудфлэру и сказать ему, что мы настоящие люди, а не бот.

после установки puppeteer приступим к работе:

const puppeteer = require('puppeteer');
const fs = require('fs');
(async () => {
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    await page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/85.0.4183.121 Safari/537.36');
    await page.goto('https://www.upwork.com/ab/jobs/search/?q=javascript&sort=recency');
    await page.waitForTimeout(1000);
    // wait for the page to load
    const html = await page.content();
    fs.writeFile('upwork.html', html, (err) => {
        if (err) throw err;
        console.log('The file has been saved!');
    });
    await browser.close();
})();

наш код на самом деле не сильно изменился:

  • строка 3: мы определяем асинхронную функцию для выполнения нашей логики почему?
  • строка 4: затем мы создаем новый экземпляр безголового браузера, используя метод puppeteer.launch().
  • строка 5: затем мы создаем новую страницу в браузере, используя метод browser.newPage(), который будет использоваться для перехода на веб-сайт Upwork и очистки списков вакансий.

Важная часть:

  • строка 7: мы устанавливаем пользовательский агент для страницы с помощью метода page.setUserAgent(), чтобы браузер выглядел как настоящий веб-браузер, а не бот.

Пользовательский агент — это строка текста, отправляемая веб-браузером для идентификации себя и своих возможностей. Эта информация используется сервером для определения того, какой контент и функции должны предоставляться в браузере, а также для отслеживания трафика из разных браузеров. В этом коде пользовательский агент устанавливается с помощью метода page.setUserAgent(). Этот метод принимает в качестве аргумента строку, указывающую пользовательский агент, который будет отправлен на сервер. В этом случае пользовательский агент настроен на строку, которая идентифицирует браузер как последнюю версию Google Chrome в операционной системе Linux. Это делается для того, чтобы безголовый браузер выглядел как настоящий веб-браузер, а не бот, что может помочь обойти любые меры по борьбе с ботами, которые есть на веб-сайте.

  • строка 8..9: код переходит на веб-сайт Upwork с помощью метода page.goto() и ожидает загрузки страницы с помощью метода page.waitForTimeout(). имитировать реальное человеческое поведение. (не так по-человечески, но все же).
  • rest..: После загрузки страницы мы используем метод `page.content()` для получения HTML-содержимого страницы, а затем записываем HTML-код в файл, используя метод fs.writeFile(). Наконец, мы закрываем браузер, используя метод browser.close(). Это завершит скрипт и остановит запуск безголового браузера.

То, что вы видите, это новая html-страница, которую мы получили в ответ на URL-адрес. мы фактически успешно обошли комнату ожидания cloudflare. как вы можете заметить, мы получили страницу со списком вакансий, а затем мгновенно страницу 404. Таким образом, помимо комнаты ожидания cloudflare, у upworks, похоже, есть какой-то другой механизм обнаружения ботов. эти ребята действительно серьезно относятся к ботам. хммм ! я думаю, что мы должны быть более серьезными об этом тоже.

Немного повозившись здесь и там, я придумал следующий скрипт:

(async () => {
    // reading keywords from keywords.txt file
    let keywords = fs.readFileSync('/Users/wa5ina/Porn/automation/upwork-bot/keywords.txt', 'utf-8');
    keywords = keywords.split('\n');
    for (let i = 0; i < keywords.length; i++) {
        keywords[i] = keywords[i].trim();
    }
    // Launch the browser in non-headless mode
    const browser = await puppeteer.launch();
    const page = await browser.newPage();
    // Set a realistic user agent string
    await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36');
    // set the viewport to 1920x1080 to avoid the cookie banner 
    await page.setViewport({
        width: 1920,
        height: 1080
    });
    await page.goto('https://www.upwork.com/ab/account-security/login');
    // Wait for the page to load 
    await page.waitForTimeout(1000);
    // getting the email and password from the .env file
    const email = process.env.EMAIL;
    const password = process.env.PASSWORD;
    // enter the email and password
    await page.type('#login_username', email);
    // click the "continue with email" button
    await page.click('#login_password_continue');
    // some randomness to the mouse movement
    for (let i = 0; i < 10; i++) {
        await page.mouse.move(getRndm(0, 10000), getRndm(0, 1000));
        await page.waitForTimeout(1000);
    }
    // password
    await page.type('#login_password', password);
    await page.click('#login_control_continue');
    // move the mouse randomly to be more human 🤡
    for (let i = 0; i < 10; i++) {
        await page.mouse.move(getRndm(0, 20000), getRndm(0, 10000));
        await page.waitForTimeout(1000);
    }
    // wait for the page to load
    // wait for the search input to load 
    let allJobs = [];
    // wait for search input to load
    // await page.waitForSelector('input[placeholder="Search for job"]', { visible: true });
    for (let i = 0; i < keywords.length; i++) {
        // console.log('searching for ' + keywords[i]);
        for (let j = 0; j < 5; j++) {
            // scrolling throught 5 pages 
            await page.goto('https://www.upwork.com/ab/jobs/search/?q=' + keywords[i] + '&page=' + j + '&sort=recency');
            await page.waitForTimeout(3000);
            await page.waitForSelector('div[data-test="main-tabs-index"]', { visible: true });
            // get all sections with data-test="JobTile"
            const listings = await page.$$('section[data-test="JobTile"]');
            // change the page number of jobs
            let jobs = await Promise.all(listings.map(async (listing) => {
                // get the title of the job which in <h4 class="job-tile-title"> <a> </a> </h4>
                let posted = await getTime(listing);
                // if it's too old, then skip it
                if (tooOld(posted) === true)
                    return;
                // get title of the job
                let title = await getTitle(listing);
                // get the link of the job 
                let link = await getLink(listing);
                // get the description of the job 
                let description = await getDescription(listing);
                // get type of job {type, budget}
                let typeOfJob = await getTypeOfJob(listing);
                if (tooCheap(typeOfJob) === true)
                    return;
                // // is client's payment verified (true or false)
                let paymentverified = await isVerified(listing);
                return { posted, title, link, description, typeOfJob, paymentverified };
            }
            ));
            // filter out the undefined jobs
            jobs = jobs.filter((job) => job !== undefined);
            // push jobs to alljobs
            allJobs.push(…jobs);
        }
    }
    // Add some randomness to the requests
    const randomDelay = Math.random() * 2000;
    await page.waitForTimeout(randomDelay);
    // Close the browser
    await browser.close();
    // write to json file by overriding the file
    fs.writeFileSync('/Users/wa5ina/Porn/automation/upwork-bot/jobs.json', JSON.stringify(allJobs, null, 2));
})();

Он начинается со чтения списка ключевых слов из файла keywords.txt. Это наши волшебные слова (разделенные переводом строки «\n»), которые код использует для поиска вакансий.

Затем код запускает свой надежный веб-браузер и создает новую страницу. Он устанавливает строку пользовательского агента, чтобы она притворялась настоящим веб-браузером. Он также устанавливает область просмотра на 1920x1080, чтобы избежать раздражающего баннера cookie.

Затем код переходит на страницу входа в систему Upwork и терпеливо ждет ее загрузки. Он извлекает ваш адрес электронной почты и пароль из файла .env и вводит их в соответствующие поля на странице входа. (Причина, по которой я решил войти в систему перед очисткой, заключается в том, что я заметил, что качество заданий при аутентификации и без аутентификации заметно отличается), затем он нажимает кнопку «Продолжить», чтобы продолжить процесс входа в систему.

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

После загрузки страницы код переходит на страницу поиска вакансий Upwork и начинает поиск вакансий, используя каждое ключевое слово в массиве ключевых слов. Это похоже на охоту за сокровищами для проектов нашей мечты! Для каждого ключевого слова он просматривает до пяти страниц результатов поиска.

Для каждой вакансии, указанной на странице результатов поиска, код извлекает название вакансии, ссылку, описание и тип (например, почасовая или с фиксированной оплатой) и бюджет. Он хранит всю эту сочную информацию в массиве с именем allJobs.

После поиска всех ключевых слов и очистки всех заданий код закрывает веб-браузер, записывает это в файл jobs.json и завершает работу.

(обратите внимание, что я не добавлял явно код для некоторых конкретных функций, которые извлекают данные, такие как getTitle(), getLink() и т. д., если бы я это сделал, мы, вероятно, получили бы уродливое нечитаемое сообщение в блоге, предполагая, что его еще нет, но, конечно, вы можете найти весь код в моем репозитории github.

Но я кратко объясню функции tooOld() и tooCheap(). так как они немного важны. Я решил отфильтровать вакансии, которые слишком стары или слишком дешевы.
слишком старый означает, что вакансия была опубликована более 20 минут назад. а слишком дешево означает, что работа стоит менее 500 долларов при фиксированной цене или менее 15 долларов при почасовой оплате. если вы собираетесь использовать скрипт, вы можете отредактировать эти функции, чтобы они соответствовали вашим слишком дешевым или слишком старым.

После запуска скрипта мы получим файл jobs.json, который выглядит примерно так:

[
    {
        "posted": "5 minutes ago",
        "title": "Senior Software and App Engineer",
        "link": "https://www.upwork.com/jobs/Senior-Software-and-App-Engineer_~012d16e9ca1001988c/",
        "description": "We need an absolute ninja to go through and clean up our entire platform, and our mobile apps to perform at their highest levels possible to increase our satisfaction and functionality in the field. We need a perfectionist, and someone who works efficiently because of their superior abilities. We have several integrations that need fine-tuned and more in the pipeline that will need done. So intimate knowledge of Api and SDK integrations will be necessary. He will also assist an IT support and be available for emergency service in the case of complete failure. We do not see this being the case as we are hiring you to make the system fail proof. We are young growing, high definition, video intercom system, and there is long-term potential be on this contract term.",
        "typeOfJob": {
            "type": "Hourly: ",
            "budget": "$35.00-$46.00"
        },
        "paymentverified": true
    },
    {
        "posted": "7 minutes ago",
        "title": "Build a web and mobile application",
        "link": "https://www.upwork.com/jobs/Build-span-web-span-and-mobile-application_~015ad09536ea065d5d/",
        "description": "You can read the specification document attached. This document contains all what you need.",
        "typeOfJob": {
            "type": "Fixed-price",
            "budget": "$1000 "
        },
        "paymentverified": false
    },
    {
        "posted": "7 minutes ago",
        "title": "Order platform",
        "link": "https://www.upwork.com/jobs/Order-platform_~0185808b1d24763136/",
        "description": "Looking to get a orders platform for a remittance company. Using Laravel Customer must be able to sign up, login, get live rate, book transaction and manage beneficiary and linked attributes. What would be the time line and approx cost",
        "typeOfJob": {
            "type": "Hourly",
            "budget": "not specified"
        },
        "paymentverified": true
    },
]

Простой массив объектов. каждый объект представляет собой работу. Теперь, когда у нас есть наш список, мы можем делать с ним все, что захотим, использовать discord.js, чтобы отправить его на канал раздора, или использовать node-mailer, чтобы отправить его на вашу электронную почту, или я не знаю? отправьте его своей бабушке телепатически 👵 выбор за вами.
Я решил отображать его в виде уведомлений на моем прекрасном Mac.

Выбор конечной точки

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

Код довольно прост, он просто читает файл jobs.json и отображает уведомление для каждого задания.

Я использовал какую-то не очень причудливую утилиту под названием jq, чтобы проанализировать json и извлечь все пикантные подробности о вакансиях, такие как название, дата публикации, тип, бюджет и ссылка.

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

Уведомление выглядит примерно так:

и если вам нравится предложение, вы можете нажать кнопку «Открыть», чтобы открыть ссылку на вакансию в своем браузере.

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

Теперь, когда у нас настроены бот и сценарии уведомлений, пришло время автоматизировать процесс, чтобы мы могли расслабиться и позволить боту сделать всю работу за нас. Для этого мы будем использовать что-то под названием cronjobs. Это как маленькие роботы, которые запускают сценарии по расписанию. Это как нанять личного помощника для вашего компьютера!

Во-первых, давайте добавим bot.js в качестве cronjob. Откройте терминал и введите crontab -e Это откроет редактор crontab. поэтому, поскольку наш скрипт будет очищать веб-сайт Upwork и заполнять файл jobs.json, я буду запускать его каждые 7 минут, чтобы убедиться, что я получаю последние вакансии. поэтому я добавлю следующую строку в файл crontab:

*/7 * * * * node /path/to/bot.js

Это говорит cronjob запускать скрипт bot.js каждые 7 минут. Вы можете настроить расписание по своему вкусу — воспользуйтесь этим удобным инструментом, который поможет вам разобраться с синтаксисом.

Далее, давайте запланируем наш bash-скрипт notifyjobs.sh, который будет читать из jobs.json и отображать уведомления.

поскольку он должен запускаться после сценария bot.js, мы запланируем его запуск через 3 минуты после сценария. поэтому мы добавим следующую строку в файл crontab:

*/10 * * * * /path/to/notifyjobs.sh

И с этим мы закончили! Мы успешно создали бота для извлечения списков вакансий из Upwork и отображения их в виде уведомлений macOS. Теперь мы можем получать последние обновления вакансий, не выходя из собственного рабочего стола. Больше не нужно бесконечно пролистывать списки вакансий — всю тяжелую работу за нас сделает бот! Так что вперед, расслабьтесь, расслабьтесь и позвольте боту охотиться за вами. Удачной охоты за работой!

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