十一 192012
 

最近做一个小东西,涉及到Flex中的WebService传输。完全使用Flex框架太过臃肿,想试一下能不能直接解析AMF。

AMF是Action Message Format协议的简称,AMF协议是Adobe公司自己的协议,主要用于数据交互和远程过程调用,在功能上相当于WebService,但是AMF与WebService中的XML不同的是AMF是二进制数据,而XML是文本数据,AMF的传输效率比XML高。AMF使用HTTP方式传输,目前主要是用于ActionScript中,即实现Flex和Server之间的通信。
AMF目前有两种版本,AMF0和AMF3,他们在数据类型的定义上有细微不同。关于AMF的官方文档参见这里

Type
Byte code
Notes

Number
0×00

Boolean
0×01

String
0×02

Object
0×03

MovieClip
0×04
Not available in Remoting

Null
0×05

Undefined
0×06

Reference
0×07

MixedArray
0×08

EndOfObject
0×09
See Object

Array
0x0a

Date
0x0b

LongString
0x0c

Unsupported
0x0d

Recordset
0x0e
Remoting, server-to-client only

XML
0x0f

TypedObject (Class instance)
0×10

AMF3 data
0×11
Sent by Flash player 9+

对应的枚举就是

public enum DataType
{
   Number = 0,
   Boolean = 1,
   String = 2,
   UntypedObject = 3,
   MovieClip = 4,
   Null = 5,
   Undefined = 6,
   ReferencedObject = 7,
   MixedArray = 8,
   End = 9,
   Array = 10,//0x0A
   Date = 11,//0x0B
   LongString = 12,//0x0C
   TypeAsObject = 13,//0x0D
   Recordset = 14,//0x0E
   Xml = 15,//0x0F
   TypedObject = 16,//0×10
   AMF3data=17//0×11
}

以上表列出了每种数据类型的表示方法,这样看并不容易理解,下面我就主要讲解一下常用的一些格式:
0.Number这里指的是double类型,数据用8字节表示,比如十六进制00 40 10 00 00 00 00 00 00就表示的是一个double数4.0,在C#中可以使用如下代码读取该数据:

byte[] d=new byte[]{0,0,0,0,0,0,0×10,0×40};//这里的顺序是和amf文件中的顺序正好相反,不要忘记了
double num=BitConverter.ToDouble(d,0);

1.Boolean对应的是.net中的bool类型,数据使用1字节表示,和C语言差不多,使用00表示false,使用01表示true。比如十六进制01 01就表示true。
2.String相当于.net中的string类型,String所占用的空间有1个类型标识字节和2个表示字符串UTF8长度的字节加上字符串UTF8格式的内容组成。比如十六进制03 00 08 73 68 61 6E 67 67 75 61表示的就是字符串,该字符串长8字节,字符串内容为73 68 61 6E 67 67 75 61,对应的就是“shanggua”。在C#中要读取字符串则使用:

byte[] buffer=new byte[]{0×73,0×68,0×61,0x6E,0×67,0×67,0×75,0×61};//03 00 08 73 68 61 6E 67 67 75 61
string str=System.Text.Encoding.UTF8.GetString(buffer);

3.Object在.net中对应的就是Hashtable,内容由UTF8字符串作为Key,其他AMF类型作为Value,该对象由3个字节:00 00 09来表示结束。C#中读取该对象使用如下方法:

private Hashtable ReadUntypedObject()
      {
         Hashtable hash = new Hashtable();
         string key = ReadShortString();
         for (byte type = ReadByte(); type != 9; type = ReadByte())
         {
            hash.Add(key, ReadData(type));
            key = ReadShortString();
         }
         return hash;
      }

4.Null就是空对象,该对象只占用一个字节,那就是Null对象标识0×05。
5. Undefined 也是只占用一个字节0×06。
6.MixedArray相当于Hashtable,与3不同的是该对象定义了Hashtable的大小。读取该对象的C#代码是:

private Hashtable ReadDictionary()
      {
         int size = ReadInt32();
         Hashtable hash = new Hashtable(size);
         string key = ReadShortString();
         for (byte type = ReadByte(); type != 9; type = ReadByte())
         {
            object value = ReadData(type);
            hash.Add(key, value);
            key = ReadShortString();
         }
         return hash;
      }

7.Array对应的就是.net中的ArrayList对象,该对象首先使用32位整数定义了ArralyList的长度,然后是密集的跟着ArrayList中的对象,读取该对象使用如下函数:

private ArrayList ReadArray()
      {
         int size = ReadInt32();
         ArrayList arr = new ArrayList(size);
         for (int i = 0; i < size; ++i)
         {
            arr.Add(ReadData(ReadByte()));
         }
         return arr;
      }

8.Date对应.net中的DateTime数据类型,Date在类型标识符0x0B后使用double来表示从1970/1/1到表示的时间所经过的毫秒数,然后再跟一个ushort的16位无符号整数表示时区。读取Date类型的C#代码为:  

private DateTime ReadDate()
     {
        double ms = ReadDouble();
        DateTime BaseDate = new DateTime(1970, 1, 1);
        DateTime date = BaseDate.AddMilliseconds(ms);       
        ReadUInt16(); //get’s the timezone       
        return date;
     }

9.LongString对应的也是string类型,不过和2对应的String不同的是这里使用32位整数来表示字符串的UTF8长度,而String使用的是16位。
10.XML是使用类型标识符0x0F后直接跟LongString类型的字符串表示。
这里大部分代码我都是摘自AMF.net 一个开源的.net AMF序列化和反序列化的库,大家若有兴趣可以到http://sourceforge.net/project/showfiles.php?group_id=159742 去下载。另外http://osflash.org/documentation/amf/astypes 这个英文网站也对AMF数据类型作了比较详细的介绍。

AMF文件总体来说分为4部分:前言(Preamble)、AMF头、AMF主体和主体的响应。
前言的前2字节用于说明AMF的版本,目前AMF有2个版本AMF0和AMF3.如使用AMF0则是:00 00
第3和第4字节用16位整数表示AMF头的数量。
每一个AMF头是由以下四部分组成:

引用

UTF string 表示Header的名字
Boolean 表示该Header是否是必须的
Int32表示Header的长度,但是好像很多情况下该值为FF FF FF FF,似乎这个字段没有意义。
Variable变量是某种AMF数据类型。

在Header表示完后,接下来是一个16位的整数用来表示AMF主体的数量,在这个数量之后才是AMF主体。
AMF主体主要由以下四部分组成:

引用

UTF String – Response表示请求的类和方法或响应的结果。
UTF String – Target是一个标识,其作用就是为了实现请求和响应的对应,通过Target找到该响应对应的请求。一般使用自增整数。
Int32- 表示主体的长度,该字段一般没有什么用
Variable变量表示主体的数据。

主体响应是客户端向服务器发送一个AMF请求以后服务器做出的和请求的主体格式相同的AMF响应,但是主体响应中的内容有所不同:
Response: 被设置为字符串‘null’.
Target: 是请求的Target值再加上“/onStatus”, “onResult”, 或者 “/onDebugEvents”组成. “/onStatus” 是为运行时错误而准备的我们一般不关心这个. “/onResult” 表示该请求被正确调用. “/onDebugEvents” 是在调试时使用的,这里也不用关心. 如果请求的Target是‘/1’, 那么被成功调用以后的主体响应应该是: ‘/1/onResult’ 。
Data:就是响应后返回的AMF对象。
说了这么多估计还是感觉比较抽象,下面给出个实例:
AMF 16进制内容

00000000h: 00 00 00 00 00 01 00 1B 7A 68 2E 66 6C 65 65 74 ; ……..zh.fleet
00000010h: 53 65 72 76 69 63 65 2E 67 65 74 46 6C 65 65 74 ; Service.getFleet
00000020h: 52 6F 77 00 03 2F 37 39 00 00 00 13 0A 00 00 00 ; Row../79……..
00000030h: 03 02 00 01 35 02 00 03 38 34 35 02 00 01 35      ; ….5…845…5

以上是客户端向服务器发送的一个AMF请求。我们可以按照前面说的封装方式将该amf解析如下:
00 00(AMF0版本)00 00(Header个数为0)00 01(AMF主体有1个)
00 1B(请求的方法的字符串长度为27个字节)
7A ……77(这27个直接就是调用的类和方法:“zh.fleetService.getFleetRow”)
00 03(请求的Target字符串长3字节) 2F 37 39(Target的内容:“/79”)
00 00 00 13(主体的长度为19)
0A(传入的变量是一个Array)00 00 00 03(该Array的长度为3)02 00 01 35(Array的第一个值是字符串“5”)02 00 03 38 34 35(Array的第二个值是字符串“845”)02 00 01 35(Array的第三个值是字符串“5”)
现在整个AMF对象都解析出来了,我们可以认为是客户端调用了服务器的方法:zh.fleetService.getFleetRow(“5″, “845″, “5″)
服务器返回的AMF文件的内容的解析方式相同,这里我就不再重复了。
现在我们已经对AMF文件有了一个清晰的认识了。那么接下来就是要抓包,看某些在Flex上的操作对应的发送了什么AMF文件,服务器返回了什么AMF文件。将这些AMF文件解析出来然后就可以看到调用了API了。

262012
 

原创文章,转载请注明来自Sean的技术博客

经过之前的需求分析,整个软件的架构基本可以确定下来。考虑到用户对于存储方式的需求可能再次发生变化,在实现过程中,把整个程序分成若干模块,主程序仅提供互操作的接口。一旦需求变动,只需改动部分模块或者增删部分模块,而不必影响其他部分的代码。使得各个模块间的耦合度降低。

image

整个程序的流程图如上图,需要被加载的模块信息存储在一个配置文件中,每次执行数据抓取和存储之前,先通过配置文件获得要加载的模块。这些模块包括:

  1. mod_utils:通过基本库实现日志功能,网址生成,邮件发送,单位转换和配置文件读取。
  2. mod_rawdata:定义了数据获取,处理和存储的接口。由于本次需求对抓取的来源非常的单一,所以在内部实现了获取和处理两个接口,解析得到的JSON数据,过滤出有价值的信息,并把它们处理成便于存储的格式放在内存中。同时留出存储接口供下面的模块使用。
  3. mod_text:将内存中的数据以txt文件的形式存储。
  4. mod_excel:将内存中的数据以xls文件的形式存储。
  5. mod_sqlite:将内存中的数据存储到SQLite数据库中。

配置文件包括:

  1. zone.xml:储存区域的信息,不同的区域对应的URL是不同的,mod_utils中提供一个函数,将每个Zone映射到一个URL上。
  2. config.ini:里面包括三个字段,module字段指出哪些存储方式将被使用;interval字段指定了在非单次执行模式中,两次抓取之间的时间间隔(秒);email字段指定一个或多个电子邮件地址,将在程序的log模块中记录到异常时候发送邮件通知。

全局的模块就是Log模块,它在程序启动的第一时间加载,并全程记录信息抓取、数据存储的过程。如果过程顺利,则每次仅简单的指出Success信息;如果出错,将按照调用栈反向输出错误信息,便于分析问题的出处。

目录结构如下:

捕获

252012
 

原创文章,转载请注明来自Sean的技术博客

去年的十一月份,结束了在某杀毒软件公司中国研发中心为期半年的实习后回到学校。论文开题已经结束,手头事情不太多。刚好有位国内名校的教授需要做一个从Web站点抓取每小时滚动的数据,并保存成Excel表格形式的文件这样一个程序。凑巧找到了我,于是就有了这么一个项目。

整个项目从需求分析,到软件架构设计,再到编码、测试以及最后的部署,都是自己一个人独立完成。也算是一个小小的锻炼,让我知道了为何对需求的理解是如此重要,因为这是客户唯一需要的东西……因为项目还在运行之中,所有有些设计项目具体内容的部分就只能隐去了。这一篇博文和后续的几篇用来讲一下项目中碰到的一些问题和解决思路。

需求概述

从某Web站点抓取数据,每小时一次。数据分两类,一类是“区域”的小时平均值(3个);第二类是“区域”中“点”的每小时取值(3个)。区域共有约15个,每个区域包含2个到10个不等的点,每个点提供3个数值。数据最终格式应为Excel表格,表格包含上述数据在一年内的所有值。

需求分析

  1. 从数据发布网站每小时抓取xxx数据,并以合适的形式保存。发布网站使用的发布方式为网页嵌入Silverlight控件展示,控件无法以HTML的分析方式得到数据,必须进行抓包,如果数据包被加密,则还需要反编译控件以破解其加密算法,从而解密数据。幸而该网站使用的是明文传输,HTTP GET形式向数据服务器发起请求,返回UTF-8编码的明文字符串。因此,抓取的重点在于研究其GET命令的形式和返回字符串的结构。
  2. 使用WireShark抓包分析,忽略TCP连接的三次握手,由第一次HTTP GET请求开始追踪每一个TCP segment of a reassembled PDU。得到了:
    • 区域均值的GET请求字符串,及相应的返回值;
    • 每个点数据的GET请求字符串及相应的返回值;
    • 返回值的格式是UTF-8文本,组织方式为JSON,除了所需的三个值之外还有其他附加数据。
  3. 存储方式:根据上面的需求,使用数据库存储无疑是性能和伸缩性最佳的,然而与客户要求的Excel文件有出入。再考虑使用Plain Text存储,通过文件系统中的树形目录结构区分区域、点、日期、时间等,因为源数据格式就是普通文本,所以这种方式的优点是可以最大限度的保留原始信息,但是显然不利于数据的检索和使用等,只能是作为备份手段使用。第三种方法是直接按需求的格式保存为Excel文件,其优点不必多言,缺点却也非常明显,性能实在是低下。因此,本系统考虑用数据库存储信息,再通过额外的附加程序将数据库生成目标xls格式,同时通过txt进行数据的备份,以防原始信息的丢失。

经过上面的分析,就可以开始设计整个程序的架构了。在实际开发过程中,并非自顶向下的进行设计的,而是先建立了多个快速的程序原型,分步实现数据的①下载②分析③有效信息提取④存储为文本。基于这些小的程序片段最终合并为完整的程序,然后重构和优化和测试。

整个程序使用Python开发,用到了xlrd, xlwt和xlutils三个第三方库。