首山钱包最近做了三轮的活动,每周一轮,每轮最多可以得到500元大红包。虽然不是专业羊毛党,但是本着为小伙伴们造福的初衷,我还是为了本次任务,写了[自动登录/选标/抢标]的PHP代码。目前代码运行良好,第一轮活动我们6个人都拿到了大红包。第二轮开始小伙伴队伍壮大了一倍,今天已是第二轮的最后一天,拿到大红包毫无压力。

下面针对本次的代码做个分享,有需要的小伙伴可以拿走自己使用。

技术关键词:

PHP / Pthreads / cURL

代码文件结构:

入口脚本 [run.php]
业务类库 [socian.php]

代码主要功能:

  1. 自动登录(使用设置的账号密码)
  2. 自动匹配标的(根据设置的标的规则)
  3. 自动提前抢标(提前一定时间开始投标,每个标投资成功后会加入黑名单,保证每账号每标的只投1次)
  4. 可以使用代理(防止请求太频繁IP被封)

代码待改进部分:

  1. 黑名单文件读写未加锁,如果同一个账号的投标进程有重叠可能会导致重复投标;
  2. CURL超时后可能造成投标成功但未成功写入黑名单,造成重复投标;

以上解决方案:

  1. 建议进程不要有重叠,设置好两次脚本调用的时间间隔;
  2. 设置合适的CURL超时时间,过短过长都不合适;
  3. 针对每次的任务,账号里面留满足任务的最小金额,防止多投;

1. 入口脚本 [run.php]

<?php
/**
 * [首山钱包]多线程版
 * @author <mail@phpha.com>
 * @date 2016年8月13日
 * @copyright www.phpha.com
 */

//脚本开始时间
$script_stime = getMicroTime();
//输出当前时间
echo PHP_EOL, '[', date('Y-m-d H:i:s'), ']', PHP_EOL;
//注册函数
register_shutdown_function('outputTimes');
//载入类库
require_once 'socian.php';

//活动规则
$rules = [
    '20160811' => [
        'type' => 1, //按期限
        'duration' => ['45天'], //期限数组
        'money' => 500, //投资金额
    ],
    '20160812' => [
        'type' => 2, //按标名
        'title' => ['首信通B'], //标名数组
        'money' => 100, //投资金额
    ],
];

//账号信息
$account = [
    [
        'username' => '185****7536',
        'password' => '******',
        'coupon_id' => 'S241C7',
    ],
];

//开启多线程
foreach($account as $k => $v){
    //实例化
    $pool[$k] = new Socian($v['username'], $v['password'], $v['coupon_id'], $rules);
    //启动线程
    $pool[$k]->start();
}

//线程池同步
foreach($pool as $work){
    //延时
    while($work->isRunning()){
        usleep(10);
    }
    //同步
    $work->join();
}

//GET_MICRO_TIME
function getMicroTime(){
    return round(microtime(true), 3);
}

//OPTPUT_EXECUTE_TIMES
function outputTimes(){
    global $script_stime;
    echo sprintf('[EXECUTE_TIMES: %.3fs]', getMicroTime() - $script_stime), PHP_EOL;
}

2. 核心类库 [socian.php]

<?php
/**
 * [首山钱包]多线程版
 * @author <mail@phpha.com>
 * @date 2016年8月13日
 * @copyright www.phpha.com
 */
class_exists('Thread') || exit('ERROR: CLASS_THREAD_NOT_EXIST'.PHP_EOL);

class Socian extends Thread{
    
    //TOKEN有效期[秒]
    const TOKEN_EXPIRES = 120;
    //每次投标次数
    const INVEST_TIMES = 30;
    //提前抢标时间[秒]
    const ADVANCE_TIME = 30;
    //标的列表地址
    const RSS_URL = 'http://www.socian.com.cn/rss';
    //登录页面地址
    const LOGIN_URL = 'http://www.socian.com.cn/user/login';
    //提交登录地址
    const DO_LOGIN_URL = 'http://www.socian.com.cn/user/doLogin';
    //提交投标地址
    const INVEST_URL = 'http://www.socian.com.cn/deal/dobid';
    
    //登录账号
    private $username;
    //登录密码
    private $password;
    //邀请码
    private $coupon_id;
    //标的ID
    private $borrow_id;
    //投资金额
    private $money;
    //活动规则
    private $act_rules;
    //代理配置
    private $proxy;
    //TOKEN文件
    private $token_file;
    //COOKIE文件
    private $cookie_file;
    //标的黑名单文件
    private $black_file;
    //错误配置
    private $error_conf = [
        0   => 'SUCCESS',
        100 => 'SYSTEM_ERROR',
        101 => 'REQUIRED_PARAM_ERROR',
        102 => 'GET_TOKEN_FAILED',
        103 => 'WRITE_TOKEN_FILE_FAILED',
        104 => 'AUTO_LOGIN_FAILED',
        105 => 'TOKEN_FILE_NOT_EXIST',
        106 => 'GET_TOKEN_FROM_FILE_FAILED',
        107 => 'INVEST_REQUEST_FAILED',
        108 => 'DO_INVEST_FAILED',
        109 => 'SELECT_RULE_FAILED',
        110 => 'RULE_TYPE_FIELD_ERROR',
        111 => 'RULE_DURATION_FIELD_ERROR',
        112 => 'RULE_TITLE_FIELD_ERROR',
        113 => 'RULE_MONEY_FIELD_ERROR',
        114 => 'RSS_REQUEST_FAILED',
        115 => 'GET_BORROW_LIST_FAILED',
        116 => 'SELECT_BORROW_ITEM_FAILED',
        117 => 'GET_INVEST_START_TIME_FAILED',
    ];
    
    /**
     * 构造函数
     * @param string $username
     * @param string $password
     * @param string $coupon_id
     * @param array $act_rules
     * @param string $proxy
     */
    public function __construct($username = '', $password = '', $coupon_id = '', $act_rules = [], $proxy = ''){
        //属性赋值
        $this->username = trim($username);
        $this->password = trim($password);
        $this->coupon_id = trim($coupon_id);
        $this->act_rules = $act_rules;
        $this->proxy = trim($proxy);
        $this->token_file = dirname(__FILE__).'/'.$username.'.token';
        $this->cookie_file = dirname(__FILE__).'/'.$username.'.cookie';
        $this->black_file = dirname(__FILE__).'/'.$username.'.black';
    }
    
    /**
     * Thread::run()
     * 开始执行业务流
     */
    public function run(){
        //[1]选取规则
        $result = $this->selectRule(date('Ymd'));
        if(0 != $result['err_no']){
            return $this->outputLogs($result);
        }
        $curr_rule = $result['data'];
        //[2]获取标的列表
        $result = $this->getBorrowList(6);
        if(0 != $result['err_no']){
            return $this->outputLogs($result);
        }
        $borrow_list = $result['data'];
        //[3]匹配标的
        $result = $this->selectBorrow($borrow_list, $curr_rule);
        if(0 != $result['err_no']){
            return $this->outputLogs($result);
        }
        $this->borrow_id = intval($result['data']['id']);
        $this->money = floatval($result['data']['money']);
        //[4]自动登录
        $result = $this->autoLogin();
        if(0 != $result['err_no']){
            //二次登录
            $result = $this->autoLogin();
            //二次失败
            if(0 != $result['err_no']){
                return $this->outputLogs($result);
            }
        }
        //[5]执行投标
        for($i = 1; $i <= self::INVEST_TIMES; $i++){
            //循环执行
            $result = $this->doInvest($this->borrow_id, $this->money);
            //输出日志
            $logs['id'] = $i;
            $logs = array_merge($logs, $result);
            $this->outputLogs($logs);
            //投标成功
            if(0 == $result['err_no']){
                break;
            }
        }
    }
    
    /**
     * 自动登录
     * @return array
     */
    public function autoLogin(){
        //参数校验
        if(empty($this->username) || empty($this->password)){
            return $this->returnError(101);
        }
        //有效期校验
        if(file_exists($this->token_file)){
            $curr_time = time();
            $file_time = filemtime($this->token_file);
            $token = file_get_contents($this->token_file);
            if( ! empty($token) && ($curr_time - $file_time < self::TOKEN_EXPIRES)){
                //无需重复登录
                return $this->returnError(0);
            }
        }
        //登录页面源码
        $html = $this->curlData(self::LOGIN_URL, 'GET', [], 'SET', $this->proxy);
        //正则匹配
        $pattern = "/<input type='hidden' id='token_id' name='token_id' value='(\\d+)' ><input type='hidden' id='token' name='token' value='([0-9a-z]{32})' >/";
        $result_match = preg_match($pattern, $html['data'], $matches);
        //匹配失败
        if(1 !== $result_match){
            return $this->returnError(102);
        }
        //匹配成功
        $token_id = $matches[1];
        $token = $matches[2];
        //写入文件
        $result_write = file_put_contents($this->token_file, $token_id.'|'.$token);
        //写入失败
        if(false === $result_write){
            return $this->returnError(103);
        }
        //提交登录
        $param = [
            'login' => 'true',
            'username' => $this->username,
            'password' => $this->password,
            'token_id' => $token_id,
            'token' => $token,
        ];
        $result = $this->curlData(self::DO_LOGIN_URL, 'POST', $param, 'USE', $this->proxy);
        //登录失败
        if(false === $result || intval($result['code']) != 302){
            //清空TOKEN文件
            file_put_contents($this->token_file, '');
            //记录日志
            $logs = [
                'mobile' => $this->username,
            ];
            return $this->returnError(104, '', $logs);
        }
        //登录成功
        return $this->returnError(0);
    }
    
    /**
     * 执行投标
     * @param integer $id
     * @param float $money
     * @return array
     */
    public function doInvest($id = 0, $money = 0){
        //参数校验
        if(intval($id) < 1 || floatval($money) < 100){
            return $this->returnError(101);
        }
        //TOKEN文件不存在
        if( ! file_exists($this->token_file)){
            return $this->returnError(105);
        }
        //获取TOKEN失败
        $token = file_get_contents($this->token_file);
        if(empty($token)){
            return $this->returnError(106);
        }
        //设置TOKEN
        list($token_id, $token) = explode('|', $token);
        //邀请码绑定关系
        $coupon_fixed = empty($this->coupon_id) ? 0 : 1;
        //执行投标
        $param = [
            'id' => intval($id),
            'token_id' => $token_id,
            'token' => $token,
            'bid_money' => floatval($money),
            'coupon_id' => $this->coupon_id,
            'coupon_is_fixed' => $coupon_fixed,
        ];
        $result = $this->curlData(self::INVEST_URL, 'POST', $param, 'USE', $this->proxy);
        //请求失败
        if(false === $result){
            return $this->returnError(107);
        }
        //请求成功
        $result = json_decode($result['data'], true);
        //记录日志
        $logs = [
            'status' => $result['status'],
            'mobile' => $this->username,
            'result' => $result['info'],
        ];
        //投标失败
        if($result['status'] != 1){
            return $this->returnError(108, '', $logs);
        }
        //加入黑名单
        $this->addBlackList($id);
        //投标成功
        return $this->returnError(0, '', $logs);
    }
    
    /**
     * 获取规则
     * @param string $rule_key
     * @return array
     */
    public function selectRule($rule_key = ''){
        //参数校验
        if(empty($this->act_rules) || ! is_array($this->act_rules) || empty($rule_key)){
            return $this->returnError(101);
        }
        //选取规则失败
        if(empty($this->act_rules[$rule_key])){
            return $this->returnError(109);
        }
        //当前规则
        $curr_rule = $this->act_rules[$rule_key];
        //规则类型校验
        if( ! in_array($curr_rule['type'], [1,2])){
            //规则类型错误
            return $this->returnError(110);
        }
        //规则期限校验
        if(1 == $curr_rule['type'] && (empty($curr_rule['duration']) || ! is_array($curr_rule['duration']))){
            //规则期限错误
            return $this->returnError(111);
        }
        //规则标题校验
        if(2 == $curr_rule['type'] && (empty($curr_rule['title']) || ! is_array($curr_rule['title']))){
            //规则标题错误
            return $this->returnError(112);
        }
        //规则金额校验
        if(empty($curr_rule['money']) || floatval($curr_rule['money']) < 100){
            //规则金额错误
            return $this->returnError(113);
        }
        //返回
        return $this->returnError(0, '', $curr_rule);
    }
    
    /**
     * 获取标的列表
     * @param integer $limit
     * @return array
     */
    public function getBorrowList($limit = 6){
        //标的列表
        $result = $this->curlData(self::RSS_URL, 'GET', ['limit'=>$limit], '', $this->proxy);
        //请求失败
        if(false === $result){
            return $this->returnError(114);
        }
        //解析XML
        $data = simplexml_load_string($result['data'], 'SimpleXMLElement', LIBXML_NOCDATA);
        $data = json_decode(json_encode($data), true);
        //获取标的列表失败
        if(empty($data['channel']['item'])){
            return $this->returnError(115);
        }
        //获取标的列表成功
        return $this->returnError(0, '', $data['channel']['item']);
    }
    
    /**
     * 选取标的
     * @param array $borrow_list
     * @param array $curr_rule
     * @return array
     */
    public function selectBorrow($borrow_list = [], $curr_rule = []){
        //参数校验
        if(empty($borrow_list) || empty($curr_rule)){
            return $this->returnError(101);
        }
        //待投标的
        $borrow = [];
        //黑名单列表
        $black_list = $this->getBlackList();
        //循环匹配标的
        foreach($borrow_list as $v){
            //排除黑名单
            if(in_array($v['deal_id'], $black_list)){
                continue;
            }
            //按状态排除
            if( ! in_array(trim($v['dealStatus']), ['查看','投资'])){
                continue;
            }
            //倒计时标的|优先级最高
            if(trim($v['dealStatus']) == '查看'){
                //当前时间
                $curr_time = time();
                //开始投标时间
                $invest_stime = trim($v['dealFlow_2']);
                $invest_stime = substr($invest_stime, -5);
                $invest_stime = strtotime(date('Y-m-d ').$invest_stime);
                //获取时间失败
                if(false === $invest_stime){
                    return $this->returnError(117);
                }
                //提前开始投标
                if($invest_stime - $curr_time > self::ADVANCE_TIME){
                    //未到设定时间
                    continue;
                }
            }
            //按条件查找
            if(1 == $curr_rule['type']){
                //按期限
                foreach($curr_rule['duration'] as $duration){
                    if(trim($v['repayTime']) == $duration){
                        //待投标的
                        $borrow['id'] = $v['deal_id'];
                        $borrow['money'] = $curr_rule['money'];
                        break 2;
                    }
                }
            }elseif(2 == $curr_rule['type']){
                //按标名
                foreach($curr_rule['title'] as $title){
                    if(false !== strstr($v['title'], $title)){
                        //待投标的
                        $borrow['id'] = $v['deal_id'];
                        $borrow['money'] = $curr_rule['money'];
                        break 2;
                    }
                }
            }
        }
        //匹配失败
        if(empty($borrow)){
            return $this->returnError(116);
        }
        //匹配成功
        return $this->returnError(0, '', $borrow);
    }
    
    /**
     * 加入黑名单
     * @param integer $id
     * @return boolean
     */
    protected function addBlackList($id = 0){
        //写入黑名单
        $result = file_put_contents($this->black_file, "{$id},", FILE_APPEND);
        //返回
        return $result === false ?: true;
    }
    
    /**
     * 读取黑名单
     * @return array
     */
    protected function getBlackList(){
        //初始化
        $data = [];
        //文件存在
        if(file_exists($this->black_file)){
            $list = file_get_contents($this->black_file);
            $data = explode(',', $list);
        }
        //返回
        return $data;
    }
    
    /**
     * 返回错误
     * @param integer $err_no
     * @param string $err_msg
     * @param array $data
     * @return array
     */
    protected function returnError($err_no = 0, $err_msg = '', $data = []){
        //格式化
        $error['err_no'] = $err_no;
        $error['err_msg'] = empty($err_msg) ? $this->error_conf[$err_no] : $err_msg;
        empty($data) || $error['data'] = $data;
        //返回
        return $error;
    }
    
    /**
     * 输入日志
     * @param array $logs
     * @return null
     */
    protected function outputLogs($logs = []){
        echo var_export($logs), PHP_EOL;
        return null;
    }
    
    /**
     * CURL
     * @param string $url
     * @param string $type [GET|POST]
     * @param array $data
     * @param string $cookie [SET|USE]
     * @param string $proxy
     * @return boolean|array
     */
    protected function curlData($url = '', $type = 'GET', $data = [], $cookie = '', $proxy = ''){
        //参数校验
        if(
            empty($url) || ! in_array(strtoupper($type), ['GET','POST'])
            || ! in_array(strtoupper($cookie), ['','SET','USE'])
        ){
            return false;
        }
        //初始化
        $ch = curl_init();
        if( ! $ch){
            return false;
        }
        //请求方式
        if(strtoupper($type) == 'POST'){
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data));
        }else{
            empty($data) || $url .= '?' . http_build_query($data);
        }
        //通用配置
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
        curl_setopt($ch, CURLOPT_HEADER, 0);
        curl_setopt($ch, CURLOPT_TIMEOUT, 1);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:48.0)');
        //代理配置
        if( ! empty($proxy)){
            curl_setopt($ch, CURLOPT_PROXY, $proxy);
        }
        //COOKIE
        if(strtoupper($cookie) == 'SET'){
            curl_setopt($ch, CURLOPT_COOKIEJAR, $this->cookie_file);
        }elseif(strtoupper($cookie) == 'USE'){
            curl_setopt($ch, CURLOPT_COOKIEFILE, $this->cookie_file);
        }
        //执行请求
        $result['data'] = curl_exec($ch);
        $result['code'] = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        //错误信息
        //$err_no = curl_errno($ch);
        //$err_info = curl_getinfo($ch);
        //关闭资源
        curl_close($ch);
        //返回
        return $result;
    }
}

3. 总结

以上就是所有的PHP代码。另外想说的是,代码实现的是功能,至于代码怎么使用,计划任务频率怎么设定,都需要自己去观察调整,因为每台服务器的硬件情况不一样,每次脚本处理的时间也会有差别。有问题欢迎留言沟通。

标签:Pthreads