/**
 * 入口
 */ import puppeteer from 'puppeteer' ; import qs from 'querystring' ; import os from 'os' ; import child_process from 'child_process' ; import iconv from 'iconv-lite' ; import http from 'http' ; import cluster from 'cluster' ; import sendMail from './mail' ; import fs from 'fs' ; import path from 'path' ; import { EventEmitter } from 'events' ; import imageBytes from './defaultimg' ; import Log from './log' ; let products = require ( './products.json' ) ; const cpulength = os . cpus ( ) . length ; const step = Math . ceil ( products . length / cpulength ) ; let userInfo = { } ; let uuid = '' ; let isOrder = false ; let isLogin = false ; const event = new EventEmitter ( ) ; let brower = null ; let pages = [ ] ; if ( cluster . isMaster ) { ; ( async ( ) => { const productSuccess = new Set ( [ ] ) ; const works = [ ] ; brower = await puppeteer . launch ( { headless : true } ) ; const page = await brower . newPage ( ) ; pages = await brower . pages ( ) ; page . setViewport ( { width : 1200 , height : 1200 } ) ; await page . setRequestInterception ( true ) ; page . on ( 'request' , ( req ) => { if ( req . resourceType ( ) . match ( /image|stylesheet|font/ ) && ! isLogin ) { req . respond ( { status : 200 , body : Buffer . from ( imageBytes ) } ) ; return ; } req . continue ( ) ; } ) ; page . on ( 'response' , async ( res ) => { const myUrl = new URL ( res . url ( ) ) ; const params = qs . parse ( myUrl . search . slice ( 1 ) ) ; event . emit ( myUrl . pathname , res ) ; if ( myUrl . pathname === '/user/petName/getUserInfoForMiniJd.action' && ! userInfo ) { const text = await res . text ( ) ; userInfo = getJsonp ( params . callback , text ) ; console . log ( '更新登录用户个人数据' ) ; } } ) ; await login ( page ) ; console . log ( `正在收集产品依赖数据中，共${ products . length }个产品` ) ; for ( const product of products ) { console . log ( `产品ID:${ product . skuId } 收集中...` ) ; try { await getdepData ( page , product ) ; } catch ( e1 ) { console . log ( '重新尝试获取数据' , `产品ID:${ product . skuId }` , e1 . message ) ; try { await getdepData ( page , product ) ; } catch ( e2 ) { } } } products = products . filter ( product => product . skuId && product . name && product . venderId && product . cat && product . buyNum ) ; console . log ( '收集产品依赖数据结束' , `共收集${ products . length }个产品数据` ) ; await clearCart ( page ) ; for ( let index = 0 ; index < cpulength ; index ++ ) { const work = cluster . fork ( ) ; works . push ( work ) ; work . send ( { productList : products . slice ( index * step , step * ( index + 1 ) ) , index , userInfo , uuid } ) ; work . process . on ( 'message' , ( data ) => { if ( typeof data === 'object' ) { let { msg , product , type } = data ; product = products . find ( p => p . skuId === product . skuId ) ; if ( type === 'requestGoodsById' ) { productSuccess . add ( msg ) ; if ( product . needs > 0 ) { product . needs -- ; try { addCart ( page , product , ( ) => { console . log ( '下单结束' ) ; } ) ; } catch ( error ) { Log . write ( '系统出现错误' , error . message ) ; } } else { Log . write ( product . name , '不需要货物了' ) ; } fs . writeFile ( path . join ( __dirname , './productSuccess.data' ) , Array . from ( productSuccess ) . join ( '\n' ) , ( err ) => { if ( err ) return console . log ( err ) ; } ) ; } else if ( type === 'log' ) { Log . write ( msg ) ; } } } ) ; } } ) ( ) ; } else { process . on ( 'message' , async ( data ) => { userInfo = data . userInfo ; uuid = data . uuid ; if ( data . productList . length ) { getProduct ( data . productList ) ; } } ) ; console . log ( `工作进程 ${ process . pid } 准备爬取` ) ; } param page async function clearCart ( page ) { if ( isOrder ) return ; try { isOrder = true ; const old = Date . now ( ) ; await page . goto ( `https://cart.jd.com/` ) ; console . log ( '[防止误下单]正在清空购物车...' ) ; const clearAll = await page . $ ( '.J_clr_all' ) ; if ( clearAll ) { await clearAll . click ( ) ; await sleep ( 100 ) ; const cartcItems = await page . $$ ( '.cartc-item' ) ; for ( const cartItem of cartcItems ) { await cartItem . click ( ) ; } const del = await page . $ ( '#cart-cleaner-continer-3vwezo-opt-del' ) ; await del . click ( ) ; console . log ( '清空购物车结束' , Date . now ( ) - old ) ; } else { console . log ( '购物车内没东西' ) ; } } catch ( error ) { console . log ( '清空购物车失败' , error . message ) ; } finally { isOrder = false ; } } /**
 * 加入到购物车
 */ async function addCart ( page , product , cb ) { console . log ( 'isOrder' , isOrder ) ; if ( isOrder ) { product . needs ++ ; return ; } const oldTimes = Date . now ( ) ; let submitClickTime = 0 ; let hasClick = true ; function pageLoad ( ) { if ( hasClick ) { Log . write ( product . name , '下单成功，需要的数量-1' , product . needs , `下单耗时:${ submitClickTime - oldTimes }` ) ; sendMail ( `${ product . name } https://www.jd.com/` ) ; } else { product . needs += 1 ; } event . off ( '/shopping/order/submitOrder.action' , eventFireFn ) ; isOrder = false ; clearCart ( page ) . finally ( ( ) => { cb && cb ( ) ; } ) ; } async function eventFireFn ( res ) { let result = '' ; try { result = await res . text ( ) ; } catch ( error ) { result = '' ; } if ( result ) { Log . write ( product . name , '下单失败' , `下单耗时:${ submitClickTime - oldTimes }` , `\n${ result }` ) ; isOrder = false ; clearCart ( page ) . finally ( ( ) => { cb && cb ( ) ; } ) ; product . needs += 1 ; page . off ( 'load' , pageLoad ) ; } } try { isOrder = true ; pages [ 0 ] . goto ( `https://cart.jd.com/gate.action?pid=${ product . skuId }&pcount=${ product . buyNum }&ptype=1` ) ; await sleep ( 100 ) ; await page . goto ( `https://trade.jd.com/shopping/order/getOrderInfo.action` ) ; event . once ( '/shopping/order/submitOrder.action' , eventFireFn ) ; page . once ( 'load' , pageLoad ) ; try { ; ( await page . $ ( '#order-submit' ) ) . click ( ) ; } catch ( error ) { hasClick = false ; throw error ; } submitClickTime = Date . now ( ) ; await page . screenshot ( { path : './order.png' } ) ; } catch ( error ) { isOrder = false ; Log . write ( product . skuId , product . name , '下单失败' , error . message ) ; event . off ( '/shopping/order/submitOrder.action' , eventFireFn ) ; page . off ( 'load' , pageLoad ) ; product . needs += 1 ; clearCart ( page ) . finally ( ( ) => { cb && cb ( ) ; } ) ; } } param 产品id async function getdepData ( page , product ) { try { await page . goto ( `https://item.jd.com/${ product . skuId }.html` ) ; await page . screenshot ( { path : `./${ product . skuId }.png` } ) ; const aHandle = await page . evaluateHandle ( ( ) => ( window as any ) . pageConfig ) ; const resultHandle = await page . evaluateHandle ( pageConfig => ( { cat : pageConfig . product . cat , venderId : pageConfig . product . venderId } ) , aHandle ) ; Object . assign ( product , await resultHandle . jsonValue ( ) ) ; product . buyNum = ~ ~ await page . $eval ( '#buy-num' , node => ( node as any ) . value ) ; product . name = ( await page . $eval ( '.sku-name' , node => node . innerHTML ) || '' ) . replace ( /\<\w+(.| )*\/?>/g , '' ) . replace ( /\n| /g , '' ) ; if ( ! uuid ) { const jdu = ( await page . cookies ( ) ) . find ( ( val ) => val . name === '__jdu' ) || { value : '' } ; uuid = jdu . value ; } await aHandle . dispose ( ) ; } catch ( error ) { Log . write ( '获取依赖数据失败' , product . skuId ) ; throw new Error ( error ) ; } } param page async function login ( page ) { console . log ( '准备登录中...' ) ; isLogin = true ; try { await page . goto ( 'https://passport.jd.com/new/login.aspx' ) ; } catch ( error ) { return await login ( page ) ; } await getQrcode ( page ) ; await new Promise ( ( resolve ) => { page . on ( 'response' , async function qrcodeOn ( res ) { if ( res . ok ( ) ) { const url = await res . request ( ) . url ( ) ; const myUrl = new URL ( url ) ; if ( myUrl . pathname === '/check' && myUrl . hostname === 'qr.m.jd.com' ) { const params = qs . parse ( myUrl . search . slice ( 1 ) ) ; const callback = params . callback ; const data = new Function ( callback , `return ${ await res . text ( ) }` ) ( callbackFn ) ; if ( data . code !== 200 ) console . log ( data . msg ) ; if ( data . code === 203 ) { getQrcode ( page , true ) ; } else if ( data . code === 201 ) { } else if ( data . code === 202 ) { } else if ( data . code === 200 ) { const ticket = data . ticket ; isLogin = false ; page . once ( 'load' , async ( e ) => { page . off ( 'response' , qrcodeOn ) ; console . log ( '登录成功' ) ; await page . screenshot ( { path : './login.png' } ) ; resolve ( ) ; } ) ; } } } } ) ; } ) ; } param data function callbackFn ( data ) { return data ; } param cb param data function getJsonp ( cb , data ) { return new Function ( cb , `return ${ data }` ) ( callbackFn ) ; } param page async function getQrcode ( page , refresh ? ) { const qrcode = await page . $ ( '.qrcode-img' ) ; if ( refresh ) { const refreshBtn = await page . $ ( '.refresh-btn' ) ; await refreshBtn . click ( ) ; await sleep ( 2000 ) ; } console . log ( '生成二维码中...' ) ; await qrcode . screenshot ( { path : './qr.png' } ) ; child_process . exec ( 'qr.png' , ( error ) => { if ( error ) console . log ( '打开图片失败' ) ; } ) ; } param times async function sleep ( times ) { await new Promise ( ( resolve ) => setTimeout ( resolve , times ) ) ; } param data async function getProduct ( data ) { for ( const product of data ) { if ( isOrder ) { await sleep ( 2000 ) ; continue ; } let result ; try { result = await requestGoodsById ( product ) ; } catch ( error ) { result = await requestGoodsById ( product ) ; } if ( result ) process . send ( { type : 'requestGoodsById' , product , msg : `${ result } https://item.jd.com/${ product . skuId }.html` } ) ; } getProduct ( data ) ; } async function requestGoodsById ( product ) { const now = new Date ( ) ; const hour = now . getHours ( ) ; const minutes = now . getMinutes ( ) ; const second = now . getSeconds ( ) ; try { const res = await request ( { url : 'http://c0.3.cn/stock' , params : { skuId : product . skuId , area : '12_988_40034_58079' , venderId : product . venderId , buyNum : product . buyNum , choseSuitSkuIds : null , cat : product . cat . join ( ',' ) , extraParam : '{\"originid\":\"1\"}' , fqsp : 0 , pdpin : userInfo . realName , pduid : uuid , ch : 1 , callback : 'jQuery1088082' } , method : 'get' } ) ; if ( res && res . stock && res . stock . StockState !== 34 ) { console . log ( '\x1B[32m[' , `${ hour }:${ minutes }:${ second }` , ']' , product . name , ' 有货\x1b[0m' ) ; return `[ ${ hour }:${ minutes }:${ second } ] ${ product . name } 有货` ; } else { console . log ( '\x1b[31m[' , `${ hour }:${ minutes }:${ second }` , ']' , product . name , ' 无货\x1b[0m' ) ; return false ; } } catch ( error ) { console . log ( '\x1B[33m[' , `${ hour }:${ minutes }:${ second }` , ']' , product . skuId , product . name , '-' , error . message , '\x1b[0m' ) ; } } interface IRequestOptions { method ? : string ; url : string ; params : Record < string , any > ; } function request ( options ) { options . method = options . method . toUpperCase ( ) || 'GET' ; const myUrl = new URL ( options . url ) ; if ( myUrl . search ) options . params = Object . assign ( { } , qs . parse ( myUrl . search . slice ( 1 ) ) , options . params ) ; const search = '?' + decodeURIComponent ( qs . stringify ( options . params ) ) ; return new Promise ( ( resolve , reject ) => { http . request ( { timeout : 300 , method : options . method , hostname : myUrl . hostname , path : myUrl . pathname + search , headers : { 'User-Agent' : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36' } } , ( res ) => { let data = [ ] ; res . on ( 'data' , ( chunk ) => { data . push ( chunk ) ; } ) ; res . on ( 'end' , ( ) => { const headers = res . headers ; let charset ; try { charset = headers [ 'content-type' ] . match ( /(?:charset=)(\w+)/ ) [ 1 ] || 'utf8' ; } catch ( error ) { charset = 'utf8' ; } let body = iconv . decode ( Buffer . concat ( data ) , charset ) ; const result = getJsonp ( options . params . callback , body ) ; resolve ( result ) ; } ) ; } ) . on ( 'error' , ( err ) => { reject ( err ) ; } ) . on ( 'timeout' , ( ) => { reject ( new Error ( '链接超时' ) ) ; } ) . end ( ) ; } ) ; }