首山钱包最近做了三轮的活动,每周一轮,每轮最多可以得到500元大红包。虽然不是专业羊毛党,但是本着为小伙伴们造福的初衷,我还是为了本次任务,写了[自动登录/选标/抢标]的PHP代码。目前代码运行良好,第一轮活动我们6个人都拿到了大红包。第二轮开始小伙伴队伍壮大了一倍,今天已是第二轮的最后一天,拿到大红包毫无压力。
下面针对本次的代码做个分享,有需要的小伙伴可以拿走自己使用。
技术关键词:
PHP / Pthreads / cURL
代码文件结构:
入口脚本 [run.php]
业务类库 [socian.php]
代码主要功能:
- 自动登录(使用设置的账号密码)
- 自动匹配标的(根据设置的标的规则)
- 自动提前抢标(提前一定时间开始投标,每个标投资成功后会加入黑名单,保证每账号每标的只投1次)
- 可以使用代理(防止请求太频繁IP被封)
代码待改进部分:
- 黑名单文件读写未加锁,如果同一个账号的投标进程有重叠可能会导致重复投标;
- CURL超时后可能造成投标成功但未成功写入黑名单,造成重复投标;
以上解决方案:
- 建议进程不要有重叠,设置好两次脚本调用的时间间隔;
- 设置合适的CURL超时时间,过短过长都不合适;
- 针对每次的任务,账号里面留满足任务的最小金额,防止多投;
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代码。另外想说的是,代码实现的是功能,至于代码怎么使用,计划任务频率怎么设定,都需要自己去观察调整,因为每台服务器的硬件情况不一样,每次脚本处理的时间也会有差别。有问题欢迎留言沟通。