18

​ 在2011年的时候,浙大的一位博士生借助微博的开放平台为他实验室的一台饮水机弄了个微博,名唤@浙大CCNT实验室饮水机,俗称“饮水机娘“。当年这条新闻给自己留下了挺大的印象,也一直对这个微博账号可以自动发微博背后的机理感到十分憧憬。一晃,时间都来到了2019年了~~

​ 咳咳——在钻研不少技术文章以及对技术派网友的多多请教后,自己终于也捣弄了一个自动发微博的机器人( ̄︶ ̄)↗

​ 此微博机器人的功能如下:

  • 直接模拟登陆新浪微博;
  • 自动获取唐诗宋词文本;
  • 自动获取文艺主题图片;
  • 自动上传图片至微博图床;
  • 自动发送内容不同的图文微博;
  • 通过定时任务,实现周期性发微博任务。

​ 实际效果图:

​ GitHub仓库:

https://github.com/Leslie-Won...

​ 正所谓“前不见古人,后不见来者。念天地之悠悠,独怆然而涕下!“,咳咳——IT技术世界当然不是这样,我们的技术积累都是站在前人的基础上的,换言之,站在巨人的肩膀上。所以,还是先来啰嗦啰嗦当年的饮水机娘。

饮水机娘分析

​ 当年果壳网在饮水机娘爆红了的时候,采访了背后的开发者——浙江大学计算机科学与技术学院的一位陈姓博士生。文章标题是《揭开“饮水机娘”的神秘面纱》。在这篇文章中,阐述发微博原理的段落如下:

据陈同学介绍,饮水机本身并没有多加改造,只是饮水机上安装一个摄像头,镜头正对加热指示灯,作为传感器,实时监控加热状态。

发送微博的功能通过代码实现,利用了新浪微博开放平台提供的PHP语言软件开发工具包。在代码的设计中,主要有检测模块和反应模块两部分。检测模块处理摄像头的监控数据,捕捉加热指示灯“亮->不亮”与“不亮->亮”两个切换状态,然后调用反应模块及时发送微博。所以在“饮水机娘”自动发送的微博下方,会显示“来自未通过审核应用”。目前,完成这些功能,所需的代码量不足两百行。

​ 现在来分析分析这两段话,把整个流程弄成流程图的话是如下的效果:

​ 从“指示灯”到“视觉算法判断状态”这部分属于计算机视觉实现了,依本人目前的技术视野判断,可以借助openCV来构建。至于发送微博这一部分,则是纯粹的PHP代码实现。由于本文所要讨论的是构建一个发微博的机器人,而微博报文数据的获取可以有很多种方式,因此,openCV就点到为止了。(自己也不是太懂openCV)(。・_・)/~~~

​ 自己在查阅了不少技术文献后,通过这篇《新浪微博自动(模拟)登陆详解及实现》了解到饮水机娘发送的微博下方会出现“来自未通过审核应用”是由于用了新浪微博开放平台的接口的缘故,而且其会有几个比较致命的限制(调用次数限制和授权期限限制)。网上流传一种直接模拟登陆微博的解决方案,关键点就是利用php的curl功能,这也是本人所要阐述的微博机器人使用的登录原理。

​ 另外,翻了翻饮水机娘最早期发送的微博,报告饮水机水沸腾了的微博报文是这样子的——

1552649398204

​ 后来变成了这样子——

​ 而对应“亮->不亮”状态的微博报文最初是这样子的——

​ 不过,后来关注度上去之后,就很难判断饮水机娘发送的微博是不是根据饮水机状态自动发出去了的了,但是也不影响本文后续的叙述。OK,溯源的部分就到这里,接下来讲讲在机器人构建中占据不少分量的数据获取API——今日诗词API、文艺主题图片API、微博图床API。

今日诗词API

​ 今日诗词API是乱码开发的一个可以返回一句古诗词名句的接口。它可以通过图片和JSON格式调用。今日诗词API根据不同地点、时间、节日、季节、天气、景观、城市、事件进行智能推荐。

​ 官方文档地址是https://www.jinrishici.com/, 乱码大佬撰写的介绍文章则是https://luan.ma/post/jinrishici/。就本人所要构建的微博机器人而言,使用到的接口是https://v2.jinrishici.com/one...,而且是使用带token的调用方式。

文艺主题图片API

​ 这个图片API是九凌少子负责开发的,他的图源来自于360壁纸,主要功能就是根据调用需求,返回一张360壁纸的官方服务器上的图片URL。调用方式如下:

https://www.yuluoge.com/api/i...

​ 不同的cid值对应不同的分类,根据他的解释及本人测试,分类如下——

  • cid=0 —— 默认图片,不分类型
  • cid=1 —— 美女
  • cid=2 —— 动漫
  • cid=3 —— 风景
  • cid=4 —— 游戏
  • cid=5 —— 文艺
  • cid=6 —— 文字控
  • cid=7 —— 动物
  • cid=8 —— 爱情

​ 此外,这篇文章最后贴出来的源代码是基于他在今日诗词的Q群里分享的发微博源码改造而来的,在此感谢他的贡献。

微博图床API

​ 对于微博图床API的理解得力于这篇文章——《利用微博当图床-php语言实现》

​ 使用到的微博图片上传接口为

http://picupload.service.weib...

​ 本文所构建机器人略有改动地使用了这篇文章里的获取新浪图床图片pid的PHP源码。源码如下:

/**
 * 上传图片到微博图床
 * @author mengkun  http://mkblog.cn
 * @param $file 图片文件/图片url
 * @param $multipart 是否采用multipart方式上传
 * @return 返回的json数据
 */
function upload($file, $multipart = true) {
    $cookie = '';    // 微博cookie
    $url = 'http://picupload.service.weibo.com/interface/pic_upload.php'
    .'?mime=image%2Fjpeg&data=base64&url=0&markpos=1&logo=&nick=0&marks=1&app=miniblog';
    if($multipart) {
        $url .= '&cb=http://weibo.com/aj/static/upimgback.html?_wv=5&callback=STK_ijax_'.time();
        if (class_exists('CURLFile')) {     // php 5.5
            $post['pic1'] = new CURLFile(realpath($file));
        } else {
            $post['pic1'] = '@'.realpath($file);
        }
    } else {
        $post['b64_data'] = base64_encode(file_get_contents($file));
    }
    // Curl提交
    $ch = curl_init($url);
    curl_setopt_array($ch, array(
        CURLOPT_POST => true,
        CURLOPT_VERBOSE => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => array("Cookie: $cookie"),
        CURLOPT_POSTFIELDS => $post,
    ));
    $output = curl_exec($ch);
    curl_close($ch);
    // 正则表达式提取返回结果中的json数据
    preg_match('/({.*)/i', $output, $match);
    if(!isset($match[1])) return '';
    return $match[1];
}

微博机器人源码

​ 列举了所要用到的几个重要API,最后还是贴一下机器人的源码吧。当然,也有相对应的GitHub仓库https://github.com/Leslie-Won...

主模块

//weibo.php

<?php
require_once './weiboLogin.php';
header("Content-type: text/html; charset=utf-8");
header("Access-Control-Allow-Origin:*");
header('Content-type: application/json');
error_reporting(0);

/**
发送微博
**/
function curl($url,$post=0,$header=0,$cookie=0,$referer=0,$ua=0,$nobody=0){
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL,$url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        $httpheader[] = "Accept:*/*";
        $httpheader[] = "Accept-Encoding:gzip,deflate,sdch";
        $httpheader[] = "Accept-Language:zh-CN,zh;q=0.8";
        $httpheader[] = "Connection:close";
        curl_setopt($ch, CURLOPT_HTTPHEADER, $httpheader);
        if($post){
            curl_setopt($ch, CURLOPT_POST, 1);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post);
        }
        if($header){
            curl_setopt($ch, CURLOPT_HEADER, TRUE);
        }
        if($cookie){
            curl_setopt($ch, CURLOPT_COOKIE, $cookie);
        }
        if($referer){
            curl_setopt($ch, CURLOPT_REFERER, $referer);
        }
        if($ua){
            curl_setopt($ch, CURLOPT_USERAGENT,$ua);
        }else{
            curl_setopt($ch, CURLOPT_USERAGENT,'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.167 Safari/537.36');
        }
        if($nobody){
            curl_setopt($ch, CURLOPT_NOBODY,1);
        }
        curl_setopt($ch, CURLOPT_ENCODING, "gzip");
        curl_setopt($ch, CURLOPT_RETURNTRANSFER,1);
        $ret = curl_exec($ch);
        curl_close($ch);
        return $ret;
    }




/**
 * 上传图片到微博图床
 * @author mengkun http://mkblog.cn
 * @param $file 图片文件/图片url
 * @param $multipart 是否采用multipart方式上传
 * return 返回的json数据
 */

function upload($file, $cookie, $multipart = true){
    $url = 'http://picupload.service.weibo.com/interface/pic_upload.php'.'?mime=image%2Fjpeg&data=base64&url=0&markpos=1&logo=&nick=0&marks=1&app=miniblog';
    if($multipart){
        $url .= '&cb=http://weibo.com/aj/static/upimgback.html?_wv=5&callback=STK_ijax_'.time();
        if(class_exists('CURLFile')){    //php 5.5
            $post['pic1'] = new CURLFile(realpath($file));
        }
        else {
            $post['pic1'] = '@'.realpath($file);
        }
    }
    else {
        $post['b64_data'] = base64_encode(file_get_contents($file));
    }

    // echo $post['b64_data'];

    //Curl 提交
    $ch = curl_init($url);
    curl_setopt_array($ch, array(
        CURLOPT_POST => true,
        CURLOPT_VERBOSE => true,
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_HTTPHEADER => array("Cookie: $cookie"),
        CURLOPT_POSTFIELDS => $post,
    ));

    $output = curl_exec($ch);
    curl_close($ch);
    // 正则表达式提取返回结果中的json数据

    preg_match('/({.*)/i', $output, $match);
    if(!isset($match[1])) return '';
    return $match[1];
}

 /**
  通过今日诗词API获取诗词内容
**/

function jinrishici(){

    $opts = array(
        'http'=>array(
        'method'=>"GET",
        'header'=>"Accept-language: en\r\n"."X-User-Token: k4z4CMgTyl3JN6s+y2iWWiHN6we+0J9V\r\n"
        )
    );
    $context = stream_context_create($opts);
    // Open the file using the HTTP headers set above
    $tangshi_pailie = json_decode(file_get_contents('https://v2.jinrishici.com/one.json', false, $context),true);          //今日诗词API,带token版本
   
    // $tangshi_pailie = json_decode(file_get_contents('https://v2.jinrishici.com/one.json'), true);                  //今日诗词api,不带token版本

    $tangshi_title = $tangshi_pailie['data']['origin']['title'];              //标题
    $tangshi_dynasty = $tangshi_pailie['data']['origin']['dynasty'];          //朝代
    $tangshi_author = $tangshi_pailie['data']['origin']['author'];            //诗人
    
    $tangshi_line_numbers = count($tangshi_pailie['data']['origin']['content']);
    $tangshi_content = $tangshi_pailie['data']['origin']['content'][0];  
    for ($i=1; $i < $tangshi_line_numbers; $i++) { 
      $tangshi_temp_line = $tangshi_pailie['data']['origin']['content'][$i];  
        $tangshi_content = $tangshi_content."\n".$tangshi_temp_line;      
    }                                 //拼接全诗

    $post_Poem = "《".$tangshi_title."》"."\n".$tangshi_dynasty."·".$tangshi_author."\n"."\n".$tangshi_content;
  
    return "$post_Poem";
}  
    
    include './wbcookie.php';
    $cookie = $config['cookie'];

    //通过图片api获取图片,并转存微博图床
    $bing_img = json_decode(upload('https://www.yuluoge.com/api/index.php?cid=5', $cookie, false),true);
    $bing_img_pid = $bing_img['data']['pics']['pic_1']['pid'];

    echo "$bing_img_pid\n";

    $tangshi = jinrishici();

    echo "$tangshi\n";

    $post=[
    'title' =>'今日要说什么?',
    'location' => 'v6_content_home',
    'text' => "#诗词[超话]# #中华好诗词# #中国诗词大会#"."\n".$tangshi."\n"."\n",//需要发送微博的内容
    'pic_id' =>  "$bing_img_pid",
    // '007CcEyfly1g042kquhztj31ns0u0tdu',//微博图片id,需事先上传好 
    'isReEdit' => false,
    'pub_source' => 'page_2',
    'topic_id' => '1022%3A',
    'pub_type' => 'dialog',
    '_t' => 0,
    'style_type' => 1,
    ];
    $url='https://weibo.com/aj/mblog/add?ajwvr=6&__rnd=2918942797035';//不需要改变
    $referer='https://weibo.com/liufengshishe/home?topnav=1&wvr=6';//你的微博用户名(首页链接)

    $response = curl($url,$post,'',$cookie,$referer);


    echo "$response\n发送成功";
    

微博登录模块

<?php

  if (!is_file('./wbcookie.php')) {
    CookieSet('SUB;','0');
  }

  include './wbcookie.php';
  require_once './weiboAccount.php';

  if (time() - $config['time'] >20*3600||$config['cookie']=='SUB;') {
    $cookie = login($sinauser,$sinapwd);
    if($cookie&&$cookie!='SUB;')
    {
      CookieSet($cookie,$time = time());
    }
    else
    {
      return error('203','获取cookie出现错误,请检查账号状态或者重新获取cookie');
    }
  }
  
  /**
       * 新浪微博登录(无加密接口版本)
       * @param  string $u 用户名
       * @param  string $p 密码
       * @return string    返回最有用最精简的cookie
       */
  function login($u,$p){
    $loginUrl = 'https://login.sina.com.cn/sso/login.php?client=ssologin.js(v1.4.15)&_=1403138799543';
    $loginData['entry'] = 'sso';
    $loginData['gateway'] = '1';
    $loginData['from'] = 'null';
    $loginData['savestate'] = '30';
    $loginData['useticket'] = '0';
    $loginData['pagerefer'] = '';
    $loginData['vsnf'] = '1';
    $loginData['su'] = base64_encode($u);
    $loginData['service'] = 'sso';
    $loginData['sp'] = $p;
    $loginData['sr'] = '1920*1080';
    $loginData['encoding'] = 'UTF-8';
    $loginData['cdult'] = '3';
    $loginData['domain'] = 'sina.com.cn';
    $loginData['prelt'] = '0';
    $loginData['returntype'] = 'TEXT';
    return loginPost($loginUrl,$loginData); 
  }

  /**
       * 发送微博登录请求
       * @param  string $url  接口地址
       * @param  array  $data 数据
       * @return json         算了,还是返回cookie吧//返回登录成功后的用户信息json
       */
  function loginPost($url,$data){
    $tmp = '';
    if(is_array($data)){
      foreach($data as $key =>$value){
        $tmp .= $key."=".$value."&";
      }
      $post = trim($tmp,"&");
    }else{
      $post = $data;
    }
    $ch = curl_init();
    curl_setopt($ch,CURLOPT_URL,$url); 
    curl_setopt($ch,CURLOPT_RETURNTRANSFER,1); 
    curl_setopt($ch,CURLOPT_HEADER,1);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
    curl_setopt($ch,CURLOPT_POST,1);
    curl_setopt($ch,CURLOPT_POSTFIELDS,$post);
    $return = curl_exec($ch);
    curl_close($ch);
    return 'SUB' . getSubstr($return,"Set-Cookie: SUB",'; ') . ';';
  }

  /**
   * 取本文中间
   */
  function getSubstr($str,$leftStr,$rightStr){
    $left = strpos($str, $leftStr);
    echo '左边:'.$left;
    $right = strpos($str, $rightStr,$left);
    echo '<br>右边:'.$right;
    if($left <= 0 or $right < $left) return '';
    return substr($str, $left + strlen($leftStr), $right-$left-strlen($leftStr));
  }

  /**
    设置cookie文件
  */

  function CookieSet($cookie,$time){
    $newConfig = '<?php 
    $config = array(
      "cookie" => "'.$cookie.'",
      "time" => "'.$time.'",
    );';
    @file_put_contents('./wbcookie.php', $newConfig);
  }

  /**
    错误反馈
  */

  function error($code,$msg){
    $arr = array('code'=>$code,'msg'=>$msg);
    echo json_encode($arr);
  }
  

微博账号模块

<?php

    $sinauser = 'example@email.com';//你的微博账号
    $sinapwd = '123456789';//你的微博密码
    

关于如何使用

​ 本地搭建了lamp环境的话,开启lamp环境后,直接在浏览器地址栏输入localhost及主入口文件对应的路径就可以运行了(本人使用xampp)。

​ 云服务器的话,本人的方案是使用宝塔服务器面板安装lamp环境后,使用xftp将文件传到apache服务器网站根目录上,开启lamp环境就可以了的。

关于安全性问题

​ 实不相瞒,如果是在云服务器上直接跑这些php文件的话,是不太安全的。因为网站的公共用户具有可以访问微博账号文件的权限。所以,推荐对微博账号文件进行.htaccess设置,也推荐申请个小号来搭建。

​ 具体操作有点复杂,可以参考这篇文章——《apache .htaccess文件详解和配置技巧总结》

关于定时任务

​ 设置定时任务的话可以使用linux主机的crontab命令。

  1. 远程连接主机,连接成功后,输入命令crontab -e;
  2. 会打开一个文件,按照格式输入需要执行的脚本;
  3. 保存退出后,重启crontab服务。

语法解释:

“*” 代表取值范围内的数字,
“/” 代表”每”,
“-” 代表从某个数字到某个数字,
“,” 分开几个离散的数字

参考文献

《揭开“饮水机娘”的神秘面纱》

《新浪微博自动(模拟)登陆详解及实现》

《今日诗词开放接口-调用文档》

《利用微博当图床-php语言实现》

《apache .htaccess文件详解和配置技巧总结》

《linux下crontab定时访问指定url》


特别致谢

九凌少子


冒泡的马树
194 声望14 粉丝

曾痴迷于赛博朋克世界的代码玩家一枚,