看到帖子有网友问如何使用C#程序访问百度指数(网站的站长对这个指标一定不陌生)。如下图,显示了CSDN的用户关注度和媒体关注度。但很可惜的是,用户只能通过浏览的方式得到某天的指数,如果想批量检查多个关键词的百度指数,就很麻烦了。而且如果想得到具体每天的数值也不是那么容易。

正好自己也有需要,所以研究了下,写了个简单的Demo实现此功能,欢迎各位朋友指正。

 

由于百度指数是以Flash的形式在客户端展现出来的,不方便直接取到,开始我以为要用C#程序和Flash客户端交互。但按常理,我们需要分析发送客户度请求是发送到服务器的数据和返回的响应结果(其实基本所有的外挂都是这样做的)。

 

我使用的抓包工具是HTTP Analyzer,通过查看Post Data,可以看到客户端是发送了一个Post请求到http://index.baidu.com/gateway.php

发送的数据是一个AMF Message

那么我们需要研究下我们如何能让我们的程序发送出相同的请求。

再此之前我们需要了解下AMF的概念,AMF是Adobe公司开发的数据交互和远程过程调用的协议,全称为Action Message Format,很类似于WebService。但WebService传递的是XML文本数据,而AMF使用Http传输的是二进制数据。

AMF中主要使用的数据类型如下:

[c-sharp] view plaincopyprint?
  1. public enum DataType  
  2. {  
  3.     Number = 0,  
  4.     Boolean = 1,  
  5.     String = 2,  
  6.     UntypedObject = 3,  
  7.     MovieClip = 4,  
  8.     Null = 5,  
  9.     Undefined = 6,  
  10.     ReferencedObject = 7,  
  11.     MixedArray = 8,  
  12.     End = 9,  
  13.     Array = 10,//0x0A      
  14.     Date = 11,//0x0B      
  15.     LongString = 12,//0x0C      
  16.     TypeAsObject = 13,//0x0D      
  17.     Recordset = 14,//0x0E      
  18.     Xml = 15,//0x0F      
  19.     TypedObject = 16,//0x10      
  20.     AMF3data = 17//0x11      
  21. }  

我们看下向服务器发送的AMF Message信息,HEX显示如下

这些二进制数据的分析如下
0003 说明使用的AMF版本,目前AMF有两个版本,0000 表示 AMF0,0003表示AMF3
0000 表示AMF Header的数量0
0001 表示AMF Body的数量1
0017 表示调用的方法的长度占23个字节(17的16进制就是23)
接下来
4461 7461 4163 6365 7373 6f72 2e67 6574 496e 6465 7865 73
这23个字节表示调用服务器端的方法是DataAccessor.getIndexes(44的16进制就是68,也就是D,后面不再做类似解释)
接下来的 00 022f 31 表示target作为标识实现请求和响应的联系,一般是自增整数。00表示数据类型为Number,02表示长度为两个字节,2f 31其实就是/1

00 0000 2d Body的长度为Number类型,长度为45

0a 0000 0003 0a表示数组类型(类似于C#中的Dictionary,键值对,键总是string类型),长度为3

数组的第一部分
0200 0ce5 a881 e8bf 85e6 9599 e882 b2 02表示字符串类型,0c表示此字符串长度为12,后面的e5 a881 e8bf 85e6 9599 e882 b2
表示传递的数据,可以用下段代码查看

[c-sharp] view plaincopyprint?
  1. byte[] buffer = new byte[] { 0xe5, 0xa8, 0x81, 0xe8, 0xbf, 0x85, 0xe6, 0x95, 0x99, 0xe8, 0x82, 0xb2 };  
  2. string str = System.Text.Encoding.UTF8.GetString(buffer);  
  3. Console.Write(str); //广告  

数组的第二部分
02 0001 30 字符串0,不知道有什么意义

数组的第三部分
02 0012 3230 3130 2d36 2d31 7c32 3031 302d 362d 3131
长度为18的字符串,内容为2010-6-1|2010-6-11

至此,请求的数据我们已经分析完毕,再简单说下返回的数据(Response Content)


返回的AMF Message当然也包括Header和Body(数组类型)
Body的Target为/1/onResult
Body的Content包括
key String类型 就是我们传进去的关键词
area Number类型 0
areaName String类型 可以用上面的代码看到内容,就是全国
userIndexes String类型 这里都是百度指数(用户关注度),用逗号分隔开
mediaIndexes String类型 这里都是媒体指数(媒体关注度),用逗号分隔开(由于图中userIndexes 占位较多,所以未显示出来,在userIndexes数据下方)

如果想得到Z后一天的百度指数,只要找到mediaIndexes前面Z后一个逗号后跟的数字即可(当然要排除mediaIndexes本身数据类型及长度的占位字节)

前面我们已经分析了发送给服务器的数据,下面就是我们怎样把这些数据post到服务器了,我们看到对AMF的二进制数据处理(序列化及反序列化)很麻烦,有兴趣的朋友可以使

用FluroineFx
FluroineFx官方网站:http://www.fluorinefx.com/   
FluroineFx下载地址:http://www.fluorinefx.com/download.html
FluroineFx在线文档:http://www.fluorinefx.com/docs/fluorine/index.html

但由于我们的应用非常简单,所以使用网上流传的一个模拟AMF Post的类,做了些修改,主要是我们现在应用的编码为UTF8

[c-sharp] view plaincopyprint?
  1. class AMFPostData  
  2. {  
  3.     public List<byte> Message;  
  4.   
  5.     /// <summary>      
  6.     /// 初始化Message      
  7.     /// </summary>      
  8.     /// <param name="amftype"></param>      
  9.     /// <param name="headers"></param>      
  10.     /// <param name="bodies"></param>      
  11.     public AMFPostData(AMFType amftype, int headers, int bodies)  
  12.     {  
  13.         //AMF版本      
  14.         if (amftype == AMFType.AMF0)  
  15.         {  
  16.             Message = new List<byte>(new byte[] { 0x00, 0x00 });  
  17.         }  
  18.         else if (amftype == AMFType.AMF3)  
  19.         {  
  20.             Message = new List<byte>(new byte[] { 0x00, 0x03 });  
  21.         }  
  22.   
  23.         //header数量      
  24.         Message.Add(BitConverter.GetBytes(headers)[1]);  
  25.         Message.Add(BitConverter.GetBytes(headers)[0]);  
  26.         //body数量      
  27.         Message.Add(BitConverter.GetBytes(bodies)[1]);  
  28.         Message.Add(BitConverter.GetBytes(bodies)[0]);  
  29.     }  
  30.   
  31.     /// <summary>      
  32.     /// 添加Target      
  33.     /// </summary>      
  34.     /// <param name="target"></param>      
  35.     /// <param name="response"></param>      
  36.     public void AddTargetAndResponse(string target, string response)  
  37.     {  
  38.         //添加Target长度      
  39.         Message.Add(BitConverter.GetBytes(target.Length)[1]);  
  40.         Message.Add(BitConverter.GetBytes(target.Length)[0]);  
  41.         //添加Target内容      
  42.         Message.AddRange(Encoding.UTF8.GetBytes(target));  
  43.   
  44.         //添加Response长度      
  45.         Message.Add(BitConverter.GetBytes(response.Length)[1]);  
  46.         Message.Add(BitConverter.GetBytes(response.Length)[0]);  
  47.         //添加Response内容      
  48.         Message.AddRange(Encoding.UTF8.GetBytes(response));  
  49.     }  
  50.   
  51.     /// <summary>      
  52.     /// 添加Body      
  53.     /// </summary>      
  54.     /// <param name="length"></param>      
  55.     /// <param name="Content"></param>      
  56.     public void AddBody(AMFPostDataBody amfpostdatabody)  
  57.     {  
  58.         Message.AddRange(amfpostdatabody.GetLength());  
  59.         Message.AddRange(amfpostdatabody.Content.ToArray());  
  60.     }  
  61. }  
  62.   
  63. class AMFPostDataBody  
  64. {  
  65.     private byte[] length = new byte[4];  
  66.     public List<byte> Content = new List<byte>();  
  67.   
  68.     /// <summary>      
  69.     /// 初始化Body      
  70.     /// </summary>      
  71.     /// <param name="datatype"></param>      
  72.     /// <param name="arraylength"></param>      
  73.     public AMFPostDataBody(DataType datatype, int arraylength)  
  74.     {  
  75.         //添加类型标识      
  76.         Content.Add((byte)datatype);  
  77.   
  78.         //数组的话添加长度      
  79.         if (datatype == DataType.Array)  
  80.         {  
  81.             Content.Add(BitConverter.GetBytes(arraylength)[3]);  
  82.             Content.Add(BitConverter.GetBytes(arraylength)[2]);  
  83.             Content.Add(BitConverter.GetBytes(arraylength)[1]);  
  84.             Content.Add(BitConverter.GetBytes(arraylength)[0]);  
  85.         }  
  86.     }  
  87.   
  88.     public void AddData(DataType datatype, string value)  
  89.     {  
  90.         //添加类型标识      
  91.         Content.Add((byte)datatype);  
  92.   
  93.         switch (datatype)  
  94.         {  
  95.             case DataType.Number:  
  96.                 AddData(double.Parse(value));  
  97.                 break;  
  98.             case DataType.String:  
  99.                 AddData(value);  
  100.                 break;  
  101.             case DataType.Boolean:  
  102.                 AddData(Boolean.Parse(value));  
  103.                 break;  
  104.         }  
  105.     }  
  106.     #region 各种类型处理方法   
  107.     /// <summary>      
  108.     /// Boolean      
  109.     /// </summary>      
  110.     /// <param name="flag"></param>      
  111.     private void AddData(bool flag)  
  112.     {  
  113.         if (flag)  
  114.             Content.Add(0x01);  
  115.         else  
  116.             Content.Add(0x00);  
  117.     }  
  118.     /// <summary>      
  119.     /// String      
  120.     /// </summary>      
  121.     /// <param name="value"></param>      
  122.     private void AddData(string value)  
  123.     {  
  124.         //添加长度      
  125.         Content.Add(BitConverter.GetBytes(Encoding.UTF8.GetBytes(value).Length)[1]);  
  126.         Content.Add(BitConverter.GetBytes(Encoding.UTF8.GetBytes(value).Length)[0]);  
  127.         //添加内容      
  128.         Content.AddRange(Encoding.UTF8.GetBytes(value));  
  129.     }  
  130.     /// <summary>      
  131.     /// Number      
  132.     /// </summary>      
  133.     /// <param name="number"></param>      
  134.     private void AddData(double number)  
  135.     {  
  136.         byte[] b = new byte[8];  
  137.         b = BitConverter.GetBytes(number);  
  138.         for (int i = 7; i > -1; i--)  
  139.         {  
  140.             Content.Add(b[i]);  
  141.         }  
  142.     }  
  143.     #endregion   
  144.   
  145.     public byte[] GetLength()  
  146.     {  
  147.         length[0] = BitConverter.GetBytes(Content.Count)[3];  
  148.         length[1] = BitConverter.GetBytes(Content.Count)[2];  
  149.         length[2] = BitConverter.GetBytes(Content.Count)[1];  
  150.         length[3] = BitConverter.GetBytes(Content.Count)[0];  
  151.   
  152.         return length;  
  153.     }  
  154. }  
  155.   
  156. public enum AMFType  
  157. {  
  158.     AMF0,  
  159.     AMF3  
  160. }  
  161.   
  162. public enum DataType  
  163. {  
  164.     Number = 0,  
  165.     Boolean = 1,  
  166.     String = 2,  
  167.     UntypedObject = 3,  
  168.     MovieClip = 4,  
  169.     Null = 5,  
  170.     Undefined = 6,  
  171.     ReferencedObject = 7,  
  172.     MixedArray = 8,  
  173.     End = 9,  
  174.     Array = 10,//0x0A      
  175.     Date = 11,//0x0B      
  176.     LongString = 12,//0x0C      
  177.     TypeAsObject = 13,//0x0D      
  178.     Recordset = 14,//0x0E      
  179.     Xml = 15,//0x0F      
  180.     TypedObject = 16,//0x10      
  181.     AMF3data = 17//0x11      
  182. }  

根据前文分析结果,我们生成数据的方法如下

[c-sharp] view plaincopyprint?
  1. private static byte [] GetData(string key, string startdate, string enddate)  
  2. {  
  3.     AMFPostData amfpostdata = new AMFPostData(AMFType.AMF3, 0, 1);  
  4.     amfpostdata.AddTargetAndResponse("DataAccessor.getIndexes""/1");  
  5.   
  6.     AMFPostDataBody amfpostdatabody = new AMFPostDataBody(DataType.Array, 3);  
  7.     amfpostdatabody.AddData(DataType.String, key);  
  8.     amfpostdatabody.AddData(DataType.String, "0");  
  9.     amfpostdatabody.AddData(DataType.String, startdate + "|" + enddate);  
  10.   
  11.     amfpostdata.AddBody(amfpostdatabody);  
  12.   
  13.     byte[] data = amfpostdata.Message.ToArray();  
  14.   
  15.     return data;  
  16. }  

可以使用以下方法发送数据及得到返回响应的二进制数据

[c-sharp] view plaincopyprint?
  1. public static byte[] GetFlashData(string gateway, byte[] data)  
  2. {  
  3.     HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(gateway);  
  4.     request.Method = "Post";  
  5.     request.ContentType = "application/x-amf";  
  6.     CookieContainer cookie = new CookieContainer();  
  7.     request.CookieContainer = cookie;  
  8.   
  9.     byte[] requestData = data;  
  10.     request.ContentLength = requestData.Length;  
  11.   
  12.     byte[] responseData = null;  
  13.   
  14.     try  
  15.     {  
  16.         using (Stream requestStream = request.GetRequestStream())  
  17.         {  
  18.             requestStream.Write(requestData, 0, requestData.Length);  
  19.         }  
  20.   
  21.         HttpWebResponse response = (HttpWebResponse)request.GetResponse();  
  22.         using (Stream responseStream = response.GetResponseStream())  
  23.         {  
  24.      //StreamToBytes是将流转换为二进制字节数组的方法,下文补上   
  25.             responseData = StreamToBytes(responseStream);  
  26.         }  
  27.     }  
  28.     catch (Exception ex)  
  29.     {  
  30.         Console.WriteLine(DateTime.Now.ToString() + "," + ex.Message);  
  31.         //Log.WriteLine(DateTime.Now.ToString() + "," + ex.Message);   
  32.     }  
  33.     return responseData;  
  34. }  

    由于返回的Stream只能Read,所以转换为二进制流麻烦一些,并且考虑到AMF中流结束的标记为00 00 09,所以写了如下方法得到返回的二进制流

    [c-sharp] view plaincopyprint?
    1. /// <summary>    
    2. /// 将 Stream 转成 byte[]    
    3. /// </summary>    
    4. public static byte[] StreamToBytes(Stream stream)  
    5. {  
    6.     List<byte> bytes = new List<byte>();  
    7.     byte b = (byte)stream.ReadByte();  
    8.     byte b1 = 0, b2 = 0, b3 = 0;  
    9.     while (true)  
    10.     {  
    11.         if (b1 == 0 && b2 == 0 && b3 == 9)  
    12.         {  
    13.             break;  
    14.         }  
    15.         b1 = b2;  
    16.         b2 = b3;  
    17.         b3 = b;  
    18.         bytes.Add(b3);  
    19.         b = (byte)stream.ReadByte();  
    20.     }  
    21.   
    22.     return bytes.ToArray();  
    23. }  

    对返回数据的分析第一篇已经说过了,考虑到分析二进制数据比较麻烦,而实际上我们需要的数据就在userIndexes和mediaIndexes之间,所以我用了如下的方法返回Z后7天的百度指数

    [c-sharp] view plaincopyprint?
    1. private static List<int> GetIndexs(byte[] data)  
    2. {  
    3.     if (data == null)  
    4.     {  
    5.         return null;  
    6.     }  
    7.     List<int> result = new List<int>();  
    8.     string str = Encoding.UTF8.GetString(data);  
    9.   
    10.     string start = "userIndexes";  
    11.     string end = "mediaIndexes";  
    12.     int startIndex = str.IndexOf(start) + start.Length;  
    13.     int endIndex = str.IndexOf(end);  
    14.   
    15.     string[] temp = str.Substring(startIndex, endIndex - startIndex).Split(',');  
    16.   
    17.     result.Add(StringToInt(temp[temp.Length - 1]));  
    18.   
    19.     int number = 0;  
    20.     for (int index = temp.Length - 2; index > temp.Length - 8; index--)  
    21.     {  
    22.         int.TryParse(temp[index], out number);  
    23.         result.Add(number);  
    24.     }  
    25.   
    26.     return result;  
    27.   
    28.     //以下为测试代码   
    29.     //int count = 0;   
    30.     //foreach (byte item in data)   
    31.     //{   
    32.     //    Log.Write(item.ToString("X2"));   
    33.     //    Log.Write(" ");   
    34.     //    count++;   
    35.     //    if (count == 16)   
    36.     //    {   
    37.     //        count = 0;   
    38.     //        Log.Write(System.Environment.NewLine);   
    39.     //    }   
    40.     //}   
    41. }  
    42. //Z后一天的数据由于和mediaIndexes中有包含数据类型长度的字节,特殊处理下   
    43. public static int StringToInt(string str)  
    44. {  
    45.     int result = 0;  
    46.     int number = 0;  
    47.     for (int i = 0; i < str.Length; i++)  
    48.     {  
    49.         number = str[i] - '0';  
    50.         if (number >= 0 && number <= 9)  
    51.         {  
    52.             result = result * 10 + number;  
    53.         }  
    54.         else  
    55.         {  
    56.             break;  
    57.         }  
    58.     }  
    59.     return result;  
    60. }  

    下面是主函数的调用

    [c-sharp] view plaincopyprint?
    1. static void Main(string[] args)  
    2. {  
    3.     string keyword = "威迅教育";  
    4.     string start = "2010-6-1";  
    5.     string end = "2010-6-11";  
    6.   
    7.     List<int> result = Run(keyword, start, end);  
    8.     foreach (int item in result)  
    9.     {  
    10.         Console.Write(item + " ");  
    11.     }  
    12. }  
    13. public static List<int> Run(string keyword, string start, string end)  
    14. {  
    15.     byte[] data = GetData(keyword, start, end);  
    16.             string gateway = "http://index.baidu.com/gateway.php";  
    17.   
    18.     byte[] responseData = WebFunc.GetFlashData(gateway, data);  
    19.       
    20.     if (responseData == null)  
    21.     {  
    22.         return null;  
    23.     }  
    24.   
    25.     List<int> result = GetIndexs(responseData);  
    26.     return result;  
    27. }  

     

    如果还是无法得到的,可以留言我会提供代码样例

    Z后,实际上,以上的算法效率并不高,百度指数可以同时查询三个关键词,有兴趣的朋友可以研究下处理的方式。