feng 发布的文章

微信自动发送消息接口开发(一)

一直有需求想做一个微信自动发送消息的功能,然而官方没有提供公开的接口,趁着下午有空简单的研究了一下.
大概有以下几种方法可以实现:
1:通过电脑版微信去模拟发送消息
这种方式相当于服务器开启一个微信客户端,通过程序获取客户端的句柄去模拟手动操作各种功能,这种方法优势是程序开发完后维护较少,缺点是可能存在技术难点,比方说如何定位到要发送的群,发送图片是否存在难题等.
2:通过网页版微信抓包获取接口,调用接口开发功能.
这种方式占用资源小,效率较高,延迟低,缺点是人家万一重构连接口都改了,就的重新开发了.
3:通过桌面程序内嵌一个web容器,通过web容器模拟操作各种动作.这个相对第一个简单一点,但是也存在服务器开销大,延迟高等问题.
4:通过电脑版微信抓包获取接口,模拟调用接口开发功能.
这个有点是性能达到最高,但是对我而言比较难,TCP协议抓包功夫不到家,万一中途碰到坑爬都爬不出来.

综合考虑觉得还是通过网页版微信抓包先观察观察具体情况再说.
(本人C#网抓喜欢用苏飞的HttpHelper,的确挺好用的.网址:http://www.sufeinet.com/thread-3-1-1.html)

首先访问微信网页版网址: https://wx.qq.com/

观察了一下数据包请求.
登录页面

发现微信并没有用webSocket连接进行通讯,而是采用长轮询的方式进行通讯,感觉有戏.第一次赞美IE8等浏览器的存在.决定就通过网页版微信开发自己想要的接口吧!

首先发现微信会post提交一个 https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxstatreport?fun=new 的接口,
然而一直没有什么返回消息,感觉像是等待消息的接口,先挖一个坑,放在这里.以后再来看.

接下去又会Get请求一个https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=1515506233792
的接口.
里面有一个appid参数,不知道是怎么获取来的,通过我不懈的努力,发现...是....js里面写死的....
appiid

好吧,你牛B,姑且认为你写死吧,就当写死的用,大不了以后改版了再去你js里面取.

private HttpResult GetLoginHtml()
    {
        Item.URL = "https://login.wx.qq.com/jslogin?appid=wx782c26e4c19acffb&redirect_uri=https%3A%2F%2Fwx.qq.com%2Fcgi-bin%2Fmmwebwx-bin%2Fwebwxnewloginpage&fun=new&lang=zh_CN&_=" + DateTime.Now.Ticks;
        var rst = Http.GetHtml(Item);
        return rst;
    }

恩,不需要任何cookie,直接得到uuid
window.QRLogin.code = 200; window.QRLogin.uuid = "QY1SL_Lv_Q==";

public string Get_uuid()
    {
        try
        {
            var loginRst = GetLoginHtml();
            string html = loginRst.Html;
            var uuid = Regex.Match(html, @"""[^""]+").Value.Substring(1);
            return uuid;
        }
        catch (Exception)
        {
            return null;
        }
    }

紧接着后面又是一个接口就用到uuid了.
https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=QY1SL_Lv_Q==&tip=1&r=616617253&_=1515506837549

这个接口是一个长轮询,应该就是在等待我们用手机扫描二维码了.赶紧看一看这图片是怎么生成的.

微信二维码

太棒了,这图片后面跟着的就是一个uuid(微信扫码登录有时间限制,超过多少时间就会重置,所以没法取到上面的uuid了,其实都一样,就是uuid)

OK,写一个登录自动发送图片到邮箱的方法.(邮箱发送不是本次重点,略过不谈)

public static bool Login(WXService wx, string sendMail)
    {
        string uuid = wx.Get_uuid();
        wx.uuid = uuid;
        if (string.IsNullOrEmpty(uuid))
        {
            Console.WriteLine("获取uuid失败");
            return false;
        }
        //二维码链接
        string url = "https://login.weixin.qq.com/qrcode/" + uuid;
        //发送邮件.
        MailHelper mail = new MailHelper();
        MailItem item = new MailItem();

        item.SendMailNumbers.Add(sendMail);     //发送方邮箱
        item.MailSubJect = "微信登录服务程序";
        item.IsBodyHtml = true;
        item.MailBody = $"<img src='{url}' />";

        return mail.Send(item);

    }

邮箱二维码
完美收到需要扫描的二维码,为将来自动化登录做好基础.

接下来就是长轮询请求等待扫码登录了.
上面已经说过,请求接口是https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?loginicon=true&uuid=QY1SL_Lv_Q==&tip=1&r=616617253&_=1515506837549

值得注意的是这里的tip参数,第一次请求是1,但是如果超时后,接下来的请求都会提交0
tip

虽然说不知道具体是为了干嘛,不过做网抓的,必须要心细,任何疑点都不要放过,先记录一下留一个坑位,将来有问题了好回来思考.

private HttpResult GetTicket(string uuid)
    {
        var obj = new
        {
            loginicon = true,
            uuid = uuid,
            tip = 0,
            r = DateTime.Now.Ticks,
            _ = DateTime.Now.Ticks
        };
        string url = "https://login.wx.qq.com/cgi-bin/mmwebwx-bin/login?" + HttpTool.UrlEncodeObj(obj);
        Item.URL = url;

        var rst = Http.GetHtml(Item);

        return rst;
    }

HttpTool是我封装的一些网抓小功能,UrlEncodeObj就是把object对象解析成url的字符串参数.辅助函数不做详细讲解,有疑问的话可以联系我的邮箱.
如果一直没有扫描,最终接口会返回window.code=408;同时再次发出一个相同的请求.
手机扫描二维码后,接口会返回一个window.code=201;同时会将微信的头像以base64的形式返回回来.(通过测试发现,loginicon参数给false就没有头像返回回来,我的服务程序不需要图像,在后面完善时改成了false)
成功登陆后,该接口会返回

window.code=200;
window.redirect_uri="https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=ASDBS3liXbgbLZiGetuv9Y6a@qrticket_0&uuid=wYcsrOLhog==&lang=zh_CN&scan=1515508420";

通过验证后.这个地方又得到一个非常重要的值:ticket
根据redirect_uri接口,微信网页版在后面又加上了&fun=new&version=v2,再次发起请求
https://wx.qq.com/cgi-bin/mmwebwx-bin/webwxnewloginpage?ticket=ASDBS3liXbgbLZiGetuv9Y6a@qrticket_0&uuid=wYcsrOLhog==&lang=zh_CN&scan=1515508420&fun=new&version=v2
可以拿到一个xml数据.里面有用的参数应该有:skey,wxsid,wxuin,pass_ticket,isgrayscale
当然还有很多才cookice
cookice

public WXModel GetOAuthWXModel(string uuid)
    {
        bool authFlag = false;
        WXModel result = new WXModel();
        int c = 0;
        while (!authFlag)
        {
            var rst = this.GetTicket(uuid);
            if (Regex.IsMatch(rst.Html, @"window\.code=200"))
            {   //成功.
                var d = HttpTool.GetUrlCode(rst.Html);
                result.uuid = d["uuid"];
                result.ticket = d["ticket"];

                var oResult = this.GetOAuth(result.uuid, result.ticket, d["scan"]);
                string xml = oResult.Html;
                result.skey = GetXMLValue(xml, "skey");
                result.wxsid = GetXMLValue(xml, "wxsid");
                result.wxuin = GetXMLValue(xml, "wxuin");
                result.pass_ticket = GetXMLValue(xml, "pass_ticket");
                result.isgrayscale = GetXMLValue(xml, "isgrayscale");

                return result;
            }
            c++;

            if (c > 10) break;      //超时.
        }
        return null;
    }

xml

完美拿到xml数据,登录也差不多结束了,接下去要做的应该就是获取人员列表以及发送消息了.
写到这里觉得网页版微信接口抓取还算顺利,后面的数据读取希望也如现在这样一帆风顺!^_^

C#快速解析json技巧

json作为轻量级数据格式,在如今webapi上应用的越来越广了.很多厂商在提供的接口中都用json来输出.

在解析json方面,javascript和python有天生的优势,作为弱类型语言,直接解析了就能当对象用了.在C#里面我们想要解析json,大部分情况下都是先定义一个实体类,然后通过Newtonsoft.Json中的Deserialize方法去反序列化为成这个类对象.

首先引入命名空间using Newtonsoft.Json;

//解析JSON字符串生成对象实体
public static T DeserializeJsonToObject<T>(string json) where T : class
    {
        JsonSerializer serializer = new JsonSerializer();
        StringReader sr = new StringReader(json);
        object o = serializer.Deserialize(new JsonTextReader(sr), typeof(T));
        T t = o as T;
        return t;
    }

那当我们碰到好多个接口,json格式多种多样的时候,就要定义N个类,那岂不是非常麻烦....
后来发现,Newtonsoft.Json的静态对象JsonConvert中给我们提供了反序列化的静态方法:

public static T DeserializeAnonymousType<T>(string value, T anonymousTypeObject);
public static T DeserializeAnonymousType<T>(string value, T anonymousTypeObject, JsonSerializerSettings settings);

那就好办一点了,很多只用到一次的格式我们可以通过定义一个匿名对象,这样可以让我们少定义不少的实体类仅仅只是为了解析json


到了这一步,虽然我们对象定义可以免了,但是对比python和js,还是多了一步定义匿名对象啊,还是比较麻烦不是么?
那我们会说没办法嘛,人家弱类型对象天生就自带光环,天生就是有这优势,本身就不需要实体类,强类型语言怎么跟他们比呢,能有什么办法嘛.
想想也是,可是关键是,我以前用VB编程做网抓的时候,可是吃尽了json的苦头,只能用VBScript去解析json,麻烦的要命.后来发现有老外专门为vb写了一个json解析.真是牛的一B.
附上github链接: https://github.com/greatbody/VB6_JSON_Parse

他里面是把json解析成了两种对象,dictionary对应弱类型里面的对象,collection对应弱类型里面的数组,因为vb里面dictionary和collection的item都可以是object类型,这样就完美的解决了json的解析值存储.
那其实C#完全也能做到哇,C#也可以拿Dictionary和List去存嘛.于是我就本着自己照着人家老外写的一套VB写一套C#用的去网上搜索一下资料.这个时候总算被我找到我想要的了.原来人家Newtonsoft.Json老早就考虑到这点了.(我就想嘛,连我都觉得麻烦人家肯定想到了)
Newtonsoft.Json有封装一个类: Newtonsoft.Json.Linq.JObject 他下面有一个静态方法,不管怎么样的json结构,调用Parse方法,就能得到JObject这个类对象.然后就想怎么用就怎么用了,多的不说了,代码截图放出来大家就都明白了.
ps:我们还可以通过JObject类,像弱类型一样添加属性,最后ToString输出json

        
        string json = "{\"name\":\"BeJson\",\"url\":\"http://www.bejson.com\",\"page\":88,\"isNonProfit\":true,\"address\":{\"street\":\"科技园路.\",\"city\":\"江苏苏州\",\"country\":\"中国\"},\"links\":[{\"name\":\"Google\",\"url\":\"http://www.google.com\"},{\"name\":\"Baidu\",\"url\":\"http://www.baidu.com\"},{\"name\":\"SoSo\",\"url\":\"http://www.SoSo.com\"}]}";
        var jsonObj = Newtonsoft.Json.Linq.JObject.Parse(json);

        string url = jsonObj["url"].ToString();

        var address = jsonObj["address"];
        var street = address["street"].ToString();

JObject使用


注意:如果json最外面是数组格式的话改成Newtonsoft.Json.Linq.JArray.Parse(json);

如果不确定是object或者是array类型,可以用通用的JToken.Parse,只是比前面两个少了对应的部分方法而已

C#程序断言运用

后台接口程序特别是写流程的时候,非常容易出现不易察觉的bug,
很多时候,我们一整天的功能可能就是找出某一个很诡异的bug,最终发现只是改了一句代码而已...

类似的教训告诉我们,平时写代码的时候,要注意暴露错误..
那么断言及抛异常就派上用处了.

C#中断言主要方法:System.Diagnostics.Debug.Assert

一般来讲,对外界传参规范性一般用抛异常去抓取到.
断言则用来规避程序内部流转的变量值有错.特别是调试的时候,如果断言返回false,程序会跳出一个弹窗让你确认,点击重试后,你可以直接跳转到那条语句进行调试.
这个小技巧在平时写代码的时候,特别是循环之类的,可以通过System.Diagnostics.Debug.Assert(i!=8);来很方便的跳转调试

注意:断言处必须尽量写清注释,一般一开始断言都不可能报错,都是后面隐蔽处才会跳出错误.
当在你的程序里存在着各种断言语句时,那么你的程序错误将无处可藏.

倒计时超期图标制作

前端时间美工有出图做一个事件还剩时间的动态图标.
因为页面是在首页,希望可以动态改变剩余的时间,效果如下图:

倒计时图片

这功能的确挺让人眼前一亮的,于是想分享一下当时自己写的框架.
框架是基于canvas去做的.

先给个源码demo链接:
动态倒计时图标

demo效果:

demo中的common.js是一些简单的时间处理函数,其他地方抄来的,我很多地方都会用common.js就不细说了.

框架源码里有几个常量说一下:

  • var CANVAS_WIDTH = 64; 定义绘制图片的canvas的宽高,最终图片会是拉伸状态,最好是和最后生成的图片宽高相同.
  • var FONT_SIZE = 13; 绘制图片上的文字字体大小,这个是和宽高联合调整的,宽高大了,字当然也要相对调整大.
  • var CLOCK_FONT_SIZE = 10; 这个是画时钟里面那个字的大小(本人写的是 "超" 字,时间超了后会显示)
  • var IMAGE_COLOR_ARRAY = [{overValue:,color:},...] 这个数组是定义剩余不同时间,时钟所显示的颜色.要按照overValue降序排列,第一个必然是overValue=null,
    表示默认显示颜色.

程序框架默认剩余分秒状态下会自动更新,超过1天的状态就不更新了,如果有需要也可以把超过1天的状态的也加入更新,更新位置在下面函数过程中,比较简单,就不多做说明.
程序返回一个毫秒数,在setTimeout函数中会调用.

//下一次更新图片的时间.
function nextUpdateTime(timeObject) {
    var obj = timeObject;
    var sDate;
    var num;
    if(obj.day != 0) return 0; //不用更新了.

    if(obj.hour != 0) {
        //未验证,应该没问题,如果有问题改成1分钟刷一次好了
        var s = obj.minute % 3600;
        if(s < 0) s = 3600 + s;
        if(s == 0) {
            s = Math.abs(obj.hour) > 1 ? 3600 : 60;
        }
        num = s * 1000;

    } else if(obj.minute != 0) {

        var s = obj.second % 60;
        if(s < 0) s = 60 + s;
        if(s == 0) {
            s = Math.abs(obj.minute) > 1 ? 60 : 1;
        }
        num = s * 1000;

    } else {
        num = 1000;
    }
    return num;

}

发现什么问题的,欢迎联系作者邮箱^_^

原生canvas制作刮刮乐效果

商城网站很多时候会有类似刮刮乐的功能,网上看到的实现方式是通过两个canvas实现.
总觉得比较low.于是自己封装了一个纯canvas底层实现刮刮乐模型
当然功能没有封装的很全,比方说加个背景图/奖品为图片等等..
当然这些功能都可以很简单的修改源码去实现了.
下面提供一下制作思路
在canvas绘制顺序应该是:
绘制背景图片(这一步我没有做)
->绘制蒙版
->destination-out模式绘制擦除蒙版的线段
->destination-over模式绘制奖品内容

照例给个demo地址:
刮刮乐demo
效果:

调用代码:

/**
     * 刮刮卡
     * @param {Object} opt
     * el: canvas id
     * text: 显示文字
     * textColor: 文字颜色 默认 #000
     * maskColor: 蒙版颜色 默认 #000
     * lineWidth: 擦除线条宽度 默认 20
     * fontStyle: 文字样式 默认 30px Georgia
     * callback: 用户第一次擦除回调(奖品在第一次擦除的时候获取.)
     * method
     *     setValue: 奖品赋值
     */
    var mask = maskShape({
        el: 'canvas',
        text: null,
        maskColor: "#ccc",
        textColor: '#000',
        lineWidth: 20,
        fontStyle: '30px Georgia',
        callback: function() { //第一次擦除回调函数. this指向对象本身
            //return 如果奖品是第一次擦除时加载进来,在callback中写ajax取值
            //如果超过200毫秒延迟体验会比较差,可以预先做一个生产队列
            var self = this;
            setTimeout(function() {
                var arr = ['一等奖', '二等奖', '三等奖', '谢谢惠顾'];
                var z = Math.floor(Math.random() * arr.length);
                self.setText(arr[z]);
            }, 200);
        }
    });

    //修改刮刮卡中值.
    //mask.setText('aaaa');