$
, $$
, $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);
}
})();
ss
https://www.tools4testing.com/contents/puppeteer/puppeteer-handling-checkbox
https://blog.daehwan.dev/2018/12/27/how-puppeteer-used-1-page-capture/
disabled 이 아닐때까지 대기
const outer_html = await page.$eval('.button.xlarge.width-max', element => element.outerHTML);
이름으로 가져오기
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}`);
}
}
puppetter.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']})
https://techozu.com/fix-running-as-root-without-no-sandbox-error-in-puppeteer/