puppeteer

spring·2022년 5월 24일
0

기본

$ , $$ , $eval , $$eval, evaluate 가 있다.

$: 지정된 selector로 하나 의 원소를 찾는다. (waitForSelector 써야함)
$$: 지정된 selector로 여러개 의 원소를 찾는다. (waitForSelector 써야함)
$eval: 지정된 selector로 선택된 하나의 원소에 대해 script를 실행한다.

evaluate: 그냥 web에서 javascript를 실행한다.

import puppeteer from 'puppeteer-extra';
import pluginStealth from "puppeteer-extra-plugin-stealth";
import fs from "fs"
import os from "os"
import array_shuffle from "array-shuffle";
import node_schedule from "node-schedule";
import {
    safe_click,
    delay,
    logger,
    get_process_time,
    VERIFY,
    BEGIN_DEBUG,
    DEFAULT_TIMEOUT,
    DEFAULT_PATIENT,
    DEFAULT_DELAY,
    PAYMENT_TIMEOUT,
    REFRESH_DELAY,
    REFRESH_PATIENT,
    MINIMUM_TIMEOUT
} from "./utils.js";
import {naver_pay} from "./naver_pay.js";
import {kakao_pay} from "./kakao_pay.js";
import {payco_pay} from "./payco_pay.js";
import {to_date} from "./server_time.js";
import {ERROR} from "./errors.js";
import {nike_login, nike_size_select_ko_kr, nike_size_select_launch} from "./nike_utils.js";

puppeteer.use(pluginStealth());

let bot_option = {
    "headless": false,
    "debug": true,
};


let chrome_path = '';
let platform = os.platform();
if (platform === "win32") {
    chrome_path = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
} else if (platform === "linux") {
    bot_option.headless = true;
    chrome_path = "/usr/bin/google-chrome";
}


let config_path = "./config/config2.json";
let payment_method = "naver";
let product_url = "https://www.nike.com/kr/launch/t/men/fw/nike-sportswear/DV0743-100/txgm67/nike-waffle-debut";
let product_size = 300;
let launch_date = undefined;                 // YYYYMMDDhhmmss
let adjustment_milliseconds = undefined;    // -1000 ~ 1000


async function wait_for_product_launch(page, product_url_fixed) {
    /**
     * 너무 빠르게 접속하여 제품이 아직 출시되지 않을때 처리
     * @returns 성공시 true, 출시전이면 false, NoAccess 면 undefined
     */
    let is_product_launch = undefined;
    await Promise.any([
        // 제품이 출시 됨.
        Promise.any([
            page.waitForSelector(/*launch*/'.select-head', {timeout: DEFAULT_TIMEOUT, visible: true}),
            page.waitForSelector(/*ko_kr*/'.size-grid-type', {timeout: DEFAULT_TIMEOUT, visible: true}),
        ]).then((btn) => {
            is_product_launch = true;
        }),
        // 제품이 출시되지 않았거나, 품절 됨
        Promise.any([
            Promise.any([
                page.waitForSelector(/*launch*/'.btn-coming-soon', {timeout: DEFAULT_TIMEOUT}),
                page.waitForSelector(/*ko_kr*/'.product-comming', {timeout: DEFAULT_TIMEOUT}),
            ]).then(async () => {
                logger.warn("Product has not been released.")
                await page.reload();
                is_product_launch = false;
            }),
            page.waitForSelector('.contens_wrap', {timeout: DEFAULT_TIMEOUT}).then(() => {
                logger.warn("Browser enters NoAccess")
                is_product_launch = undefined;
            }),
            page.waitForSelector(/*launch, ko_kr*/'.product-soldout', {timeout: DEFAULT_TIMEOUT}).then(() => {
                logger.fatal("[FATAL] 품절입니다.");
                process.exit(0);
            }),
        ]),
    ]).catch((err) => {
        logger.error(ERROR(29000));
    });
    return is_product_launch;
}

async function nike_wait_page(page, product_size) {
    /**
     *
     * @return {boolean}, true: 성공, undefined: NoAccess
     */
    let product_url_fixed = product_url;
    if (product_url.includes('/ko_kr/')) {
        product_url_fixed += '?tftest=true&size=' + product_size.toString();
    }
    let patient4refresh = REFRESH_PATIENT;
    let is_launch = false;
    // TODO: 왜 5번만 시도해? -> 브라우저 차단 박히면 어떡할라고?
    while (!is_launch && patient4refresh !== 0) {
        patient4refresh -= 1;
        is_launch = await wait_for_product_launch(page, product_url_fixed);
        if (is_launch) {
            break;
        }
        await delay(DEFAULT_DELAY);
    }
    if (is_launch === false) {   // 출시하지 않아서 실패
        logger.fatal('상품 구매 대기 실패');
        process.exit(1);
    }
    return is_launch;
}


async function size_select_except_callback(page) {
    let pass = false;
    await Promise.any([
        page.waitForFunction("window.location.pathname == '/kr/launch/checkout'", {timeout: DEFAULT_TIMEOUT}).then(() => {
            pass = true;
        }),
        page.waitForSelector('.uk-modal-close.uk-close', {visible: true, hidden: false, timeout: DEFAULT_TIMEOUT}).then((btn) => {
            pass = false;
            btn.click();
        }),
    ]).catch(async (err) => {

    });
    return pass;
}

async function nike_size_select(page) {
    /**
     * @returns 성공시 true, 실패(사이즈 획득 실패)시 undefined
     */
    if (product_url.includes('/ko_kr/')) {
        return nike_size_select_ko_kr(page);
    } else {
        return nike_size_select_launch(page, size_select_except_callback);
    }
}

async function nike_checkout_step1(page) {
    /**
     * checkout step1을 수행합니다.
     * 여기서 처리하는 NoAccess는 이전 페이지(상품 페이지)에서 버튼을 눌렀는데 NoAccess가 뜨는 경우를 처리합니다.
     * @return {boolean}, true: 성공, undefined: NoAccess
     */
    let ret = undefined;
    await Promise.any([
        Promise.all([
            page.on('dialog', async (dialog) => {
                // 접속자가 많아 대기하라는 alert이 뜨는 경우가 있음. alert이 떠도 무시하고 버튼을 눌러야 하기 떄문에 Promise.all을 사용함.
                logger.info("Dialog was detected");
                await dialog.dismiss();
            }),
            safe_click(page, '#btn-next:not(.disabled)'),
        ]).then(() => {
            ret = true;
        }),
        page.waitForSelector('.contens_wrap', {timeout: DEFAULT_TIMEOUT}).then(() => {
            logger.warn("NoAccess catch");
            ret = undefined;
        }),
    ]);

    return ret;
}


async function nike_checkout_step2(page) {
    /**
     * 이 함수는 checkout 2단계(최종단계)에서 발생하는 이벤트를 처리합니다.
     * 여기서 처리하는 NoAccess는 이전 페이지(checkout step1)에서 버튼을 눌렀는데 NoAccess가 뜨는 경우를 처리합니다.
     * 성공시: 지정한 결제 방식으로 결제를 시도합니다.
     * 실패 case01: 선택된 상품의 재고가 없습니다. 해당상품의 수량변경 또는 삭제하여야 주문이 가능합니다. 장바구니로 이동하시겠습니까?
     *           => 첫 페이지부터 다시 시도
     * @returns 성공시: 마지막 결제버튼, 실패시: undefined
     */
    let payment_index = {
        "naver": 1,
        "kakao": 0,
        "payco": 3, // deprecated
        "card": 2,  // unavailable
        "bank": 4,  // unavailable
        "null": 100,
    }
    let find_paybtn = async () => {
        let ret = await Promise.all([
            // 결제수단 버튼 클릭
            page.waitForSelector('.payment-method-item', {timeout: DEFAULT_TIMEOUT}).then(async () => {
                const payment_list = await page.$$('.payment-method-item');
                if (payment_index[payment_method] < 5) {
                    await payment_list[payment_index[payment_method]].click();
                }
            }).catch(async (err) => {
                logger.error('[ERROR] .payment-method-item not found.');
            }),
            // 체크박스 체크
            page.waitForSelector('#isCheckoutAgree', {timeout: DEFAULT_TIMEOUT}).then(async () => {
                await page.$eval('#isCheckoutAgree', check => check.checked = true);
            }).catch((err) => {
                logger.error('[ERROR] Failed to click checkbox.');
            }),
            // 결제 버튼 획득
            page.waitForSelector('.button.xlarge.width-max:not(.disabled)', {timeout: PAYMENT_TIMEOUT}).catch((err) => {
                // CASE01: timeout > 일단 10초로 해놨음.
                logger.error('[ERROR] Failed to find payment button.');
            }),
        ]);
        return ret.at(-1);
    };
    return await Promise.any([
        find_paybtn(),
        page.waitForSelector('.uk-modal-close', {visible: true, hidden: false, timeout: DEFAULT_TIMEOUT}).then(async (btn) => {
            logger.error(ERROR(40000));
            await btn.click();
            return undefined;
        }),
        page.waitForSelector('.contens_wrap', {timeout: DEFAULT_TIMEOUT}).then(() => {
            logger.error("NoAccess catch");
            return undefined;
        }),
    ]);
}


async function run_nike_bot(browser, page, config) {
    let naver_info = config["naver"];
    let kakao_info = config["kakao"];
    let payco_info = config["payco"];
    logger.info('Attempt to purchase a product.');
    logger.info(get_process_time());
    let t_beg = new Date();
    let patient4product = 10;
    while (patient4product !== 0) {
        logger.info('[STEP 01] Wait for page loading.');
        patient4product -= 1;
        if (await nike_wait_page(page, product_size) === undefined) {
            logger.info(get_process_time());
            await delay(DEFAULT_DELAY);
            await page.goto(product_url);
            continue;
        }
        logger.info('[STEP 02] Select shoe size.');
        if (await nike_size_select(page) === undefined) {
            await page.goto(product_url);
            continue;
        }
        logger.info('[STEP 03] Pass through checkout step 1.');
        if (await nike_checkout_step1(page) === undefined) {
            await page.goto(product_url);
            continue;
        }
        logger.info('[STEP 04] Pass through checkout step 2.');
        const btn = await nike_checkout_step2(page);
        if (btn === undefined) {
            await page.goto(product_url);
            continue;
        }
        logger.info("[ELAPSED TIME]: " + (new Date() - t_beg) / 1000);
        let param = {
            "browser": browser,
            "page": page,
            "btn": btn,
            "bot_option": bot_option,
            "info": undefined,
        };
        if (payment_method === "naver") {
            logger.info('[STEP 04] PAY with naver_pay.');
            param["info"] = naver_info;
            await naver_pay(param);
        } else if (payment_method === "kakao") {
            logger.info('[STEP 04] PAY with kaako_pay.');
            param["info"] = kakao_info;
            await kakao_pay(param);
        } else if (payment_method === "payco") {
            logger.info('[STEP 04] PAYCO is currently unsupported.');
            await process.exit(1);
            param["info"] = payco_info;
            await payco_pay(param);
        } else if (payment_method === "null") {
            logger.info('[STEP 04] payment_method is null.');
            process.exit(0);
        }
        await Promise.any([
            page.waitForFunction("window.location.pathname == '/kr/ko_kr/confirmation'"),
            page.waitForFunction("window.location.pathname == '/kr/launch/confirmation'"),
        ]);
        logger.info('[SUCCESS] Success to purchase a product.');
        break;  // 여러개 살 수 없음.
    }
    await browser.close();
}

async function run_puppeteer(config) {
    let nike_id = config["nike_id"];
    let nike_pw = config["nike_pw"];

    const browser = await puppeteer.launch({
        headless: bot_option["headless"],
        executablePath: chrome_path,
        args: ["--window-size=1920,1500", '--no-sandbox', '--disable-setuid-sandbox'],
        dumpio: false,
    });
    const page = await browser.newPage();
    logger.info('Browser Launched.');
    // const client = await page.target().createCDPSession();
    // await client.send('Network.emulateNetworkConditions', {
    //     'offline': false,
    //     'downloadThroughput': 1 * 1024 * 1024,   // Simulated download speed (bytes/s)
    //     'uploadThroughput': 1 * 1024 * 1024,     // Simulated upload speed (bytes/s)
    //     'latency': 50                           // Simulated latency (ms)
    // });

    await page.setViewport({
        width: 1920, height: 1500
    });

    logger.info('Attempt to login.');
    await nike_login(page, nike_id, nike_pw, product_url);
    logger.info('Login successful.');
    // 지정한 시간(제품 발매 시간) 까지 대기한다.
    if (launch_date !== undefined) {
        logger.info('Wait for launch date.');
        let date_buy = to_date(launch_date, undefined, undefined, -500);   // 3분전에 브라우저 구동
        node_schedule.scheduleJob(date_buy, run_nike_bot.bind(null, browser, page, config));
    } else {
        await run_nike_bot(browser, page, config);
    }
}

(async () => {
    // args : config path , payment method , url , size
    if (process.argv.length > 2) {
        config_path = process.argv[2];
    }
    if (process.argv.length > 3) {
        payment_method = process.argv[3];
    }
    if (process.argv.length > 4) {
        product_url = process.argv[4];
    }
    if (process.argv.length > 5) {
        product_size = parseInt(process.argv[5]);
    }
    if (process.argv.length > 7) {
        launch_date = process.argv[6];
        adjustment_milliseconds = parseInt(process.argv[7]);
    }
    let fp = fs.readFileSync(config_path);
    let config = JSON.parse(fp);

    if (bot_option['debug']) {
        BEGIN_DEBUG(config_path);
    }
    logger.info(`${config_path} ${payment_method}`);
    logger.info(`${product_url} ${product_size}`);
    logger.info(`${launch_date} ${adjustment_milliseconds}`)
    if (launch_date !== undefined) {
        logger.info('Wait for launch browser.');
        let date_execute = to_date(launch_date, -3, undefined, undefined);   // 3분전에 브라우저 구동
        node_schedule.scheduleJob(date_execute, run_puppeteer.bind(null, config));
    } else {
        await run_puppeteer(config);
    }
})();

reference

ss

https://stackoverflow.com/questions/59882543/how-to-wait-for-a-button-to-be-enabled-and-click-with-puppeteer

https://www.tools4testing.com/contents/puppeteer/puppeteer-handling-checkbox

https://blog.daehwan.dev/2018/12/27/how-puppeteer-used-1-page-capture/

disabled 이 아닐때까지 대기

https://stackoverflow.com/questions/59882543/how-to-wait-for-a-button-to-be-enabled-and-click-with-puppeteer

html 가져오기

const outer_html = await page.$eval('.button.xlarge.width-max', element => element.outerHTML);

iframe 가져오기

이름으로 가져오기

const frame = page.frames().find(frame => frame.name() === 'frameName');

url로 가져오기

const page = await browser.newPage();
for (const frame of page.mainFrame().childFrames()) {
  if (frame.url().includes('partialFrameName')) {
    console.log(`frameName: ${frame}`);
  }
}

running as root without –no-sandbox

 puppetter.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']})

https://techozu.com/fix-running-as-root-without-no-sandbox-error-in-puppeteer/

profile
Researcher & Developer @ NAVER Corp | Designer @ HONGIK Univ.

0개의 댓글