十二 092014
 

查阅文档时看到一篇2013年12月3日写的文章,一直忘了贴出来,放在硬盘角落里厚厚一层灰了。
———————————————————————————
SQLite是一种嵌入式数据库,没有server进程,每个数据库均为单文件存储,因此在有多线程并发读写同一个数据库时,会因为文件读写锁造成并发写入或读取的失败率上升。
为了解决这个问题,基本的思路就是让存在多线程并发写入的情况收敛为顺序写入。在多线程这个策略无法更改的基础上,将数据库拆分成每个线程对应一个单独的文件就是比较适宜的解决方案。
我们的业务基本情况是有数量级为百或千的PC,这里假定为机器000到999,每30秒发送五组不同的数据,对应机器的五个不同指标,这里假定为指标A-E。为了方便归档和减少单个数据库文件的大小,需要按日期将数据库文件归类,每日、不同机器的数据存储到不到的数据库文件中。因此储存的目的目录结构为:
1111
对于每一项指标,都是通过一个异步队列到达存储层的,因此在数据到达的时间上并不一定按照指标实际产生的实际为序。这就使得在日期切换时,并不能保证在日期戳为20131203的第一个数据抵达后,不会再有日期戳为20131202的数据抵达。因此必须有一个缓冲时间段,在缓冲器内对同一机器的同一指标同时维护两个数据库连接,待旧日期戳数据包基本处理完毕后关闭旧的数据库连接。
为了达到这一效果,要在内存中维护一个数据结构,该结构保存了一个单调递增的日期戳和一个数据库连接池,连接池以日期哈希的形式保存数据库连接。程序启动时初始化该数据结构,每次有数据需要存储时,调用该结构的GetLink方法得到本数据包应该对应的数据库连接。在日期更替时,数据结构中的日期戳会指向最新的日期,防止切换过程中可能出现的连接池震荡。

type DbLink struct {
	Today string
	Changing bool
	Links map[string]*sql.DB
}

func NewDbLink(date string) (link *DbLink) {
	links := make(map[string]*sql.DB)
	link = &DbLink{
		date,
		false,
		links,
	}
	return
}

func (link *DbLink) GetLink(date string, hardware_addr string, indicator string) (dbLink *sql.DB, err error) {
	key := date + "_" + hardware_addr
	dbLink, ok := link.Links[key]
	// 如果已经存在,则认为没有日期变更,且数据库连接已经打开
	if ok {
		// fmt.Println("bingo!") //命中已经打开的数据库连接
		return dbLink, nil
	} else {
		// 否则为新的日期打开新的数据库连接,并延时关闭原有日期对应的数据库连接,且删除其在本结构体中的注册条目
		var dbPath, dbSourceName string
		dbPath = "../db/" + date + "/" + strings.Replace(hardware_addr, ":", "_", -1) + "/"
		dbSourceName = dbPath + indicator + ".db"
		os.MkdirAll(dbPath, 0666)
		link.Changing = true
		inComingDate, _ := strconv.Atoi(date)
		currentDate, _ := strconv.Atoi(link.Today)
		if inComingDate > currentDate {
			// 仅当后来的日期比保存的日期更晚时,更新结构体中的Today值
			link.Today = date
		}
		newLink, err := sql.Open("sqlite3", dbSourceName)
		link.Links[key] = newLink
		if err != nil {
			return nil, err
		}
		createTable(indicator, newLink)
		go func() {
			c := time.Tick(5 * time.Minute)
			for _ = range c {
				for k, v := range link.Links {
					// 如果缓存中有非Today的日期,表示已经过期,可以执行延时关闭
					if !strings.HasPrefix(k, link.Today) {
						v.Close()
						fmt.Println(k, "to be deleted")
						delete(link.Links, k)
						link.Changing = false
					}
				}
			}
		}()
		return newLink, nil
	}
	return nil, nil
}
162014
 

闭包使用的注意事项

for循环中使用闭包时,一定要显示向闭包函数传递某个循环变量,否则闭包只会使用第一次的值。如:

for _, conn := range conns {
    go func(c Conn) {
        select {
        case ch <- c.DoQuery(query):         default:         }     }(conn) } 而不能这样 for _, conn := range conns {     go func() {         select {         case ch <- conn.DoQuery(query):         default:         }     }() } 第二种错误的用法中,闭包中的conn是不会变的。 使用WaitGroup

package util

import (
    ”sync”
)

type WaitGroupWrapper struct {
    sync.WaitGroup
}

func (w *WaitGroupWrapper) Wrap(cb func()) {
    w.Add(1)
    go func() {
        cb()
        w.Done()
    }()
}
这种方法将WaitGroup包装起来,其他的struct中需要等待一系列的goroutine返回时,先生成一个WaitGroupWrapper对象,再用该对象的Wrap方法包裹要并行且等待的函数,一般是匿名函数的形式。最后调用者通过该对象的Wait()方法等待所有函数返回。

map类型不是线程安全的。对map数据并发读写时需要加锁。

var counter = struct {
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

counter.RLock()
n := counter.m["some_key"]
counter.Unlock()
fmt.Println(n)

实现类似try/catch的错误捕捉

用一个函数包装另一个,当内部程序panic时,外部程序通过recover收集错误,使整个进程得以继续。

package main

import ”fmt”

func safeHandler(cb func() int) {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println(r)
        }
    }()
    cb()
}

func worker() int {
    b := []int{1,2}
    b[3] = 0
    return 1
}

func main() {
    safeHandler(worker)
    fmt.Println(“ok, we are safe to continue.”)
}

golang中的yield方式。

可以采用在生成器函数中向无缓冲管道写入,写入完毕后close管道。消费者for循环中读取管道,当管道close时自动退出循环。
package main

import ”fmt”

// 生成器
func generator(num …int) <-chan int {     c := make(chan int)     go func() {         for _, v := range num {             c <- v         }         close(c)     }()     return c } //消费者 func main() {     gen := generator(1, 1, 2, 3, 5, 8)     for n := range gen {         fmt.Println(n)     } } 这种生成方式要求消费者必须消费掉所有生成的数据,否则生成器一直阻塞并占用资源,形成了内存的泄露。为了避免这种情况的发生,可以在传递给生成器一个done的通道,当通道激活时生成器退出。利用channel的一个特性,即close执行时,在这个channel上的读取会立刻返回。我们不用手动向channel发送done信号,只需在defer函数中关闭它既可。因此上面的程序改为: package main import "fmt" // 生成器 func generator(done chan int, num ...int) <-chan int {     c := make(chan int)     go func() {         defer close(c)         for _, v := range num {             select {             case c <- v:             case <- done:                 return             }         }     }()     return c } //消费者 func main() {     done := make(chan int)     defer close(done)     gen := generator(done, 1, 1, 2, 3, 5, 8)     for n := range gen {         fmt.Println(n)     } } 这样,即使消费者没有完全耗尽所有要生产的数据,生产者也能在接到信号后自行退出。 使用什么样的import方式引入自定义的包?

应该使用唯一的路径来引入包,一般代码应该提交到google code或者github,这样使用

import ”github.com/seanluo/my/package”
引入的包,可以用go get的形式获取到。不宜采用本地的相对路径的形式,如import my/package,因为这种形式的引入必须手动将包路径添加到$GOPATH中去。

 Posted by at 10:22
072013
 

前一阵子因为工作需要,接触了一下Go语言。
起初看来这个语言的语法实在是非常的怪异,尤其是变量声明的方式,让从C语言之类转过来的人们非常不习惯。但是Golang的开发团队与给出了这些写变量声明的原因,而我看了之后觉得他们说的是对的:这种方式的声明在出现一大堆关于函数指针的嵌套定义的情况下,能够展示的比C语言更清晰。
另一方面,这是一种静态编译的语言,所以在移植性上非常优秀,基本上不会看到类似“缺少xxx.so”或者“找不到yyy.dll”之类的错误,脱离一堆依赖的感觉真的很好。
Golang的defer处理机制是把双刃剑。用得好,可以避免文件、Socket之类的不关闭造成的问题,但是新手用得不好,很有可能出现一些搞不清楚状况的panic。如一个file没有被真正打开过,却在defer中进行了关闭,就会出现关闭一个nil的调用。果断panic,报错的还不是defer这一行,而是return语句那行——defer里的东西正是这个时候被调用的。

 Posted by at 16:57
282013
 

转帖:http://www.blogfodder.co.uk/2012/4/20/win-2008-task-scheduler-with-return-code-1-0×1

今天在一台Windows服务器上部署一个计划任务的时候,遇到了这么一个问题。
要执行的任务很简单,即运行一个Python脚本,每小时执行一次。这种任务以前已经创建过很多个,一直运行良好。但是这次不同,任务计划程序每次都会迅速返回一个0×1的值然后退出任务,并且不认为任务失败。
查看脚本的执行log文件,发现直接就没有被执行过,而且手动执行可以得到完全正确的结果。由此可见,问题就出在了任务计划程序的配置上。
但是这个配置和以前都是完全一致的,可是就是始终不能执行。放Google搜,似乎中文的结果里面没有见到合适的解决方案,又换成英文再搜,终于有了答案。
原来,只需要在创建任务中的“操作”选项卡里面,新建操作,“程序或脚本”中只填脚本名称,在“起始于”里面填写脚本所在的路径。保存,生效!
虽然问题解决了,但是只能算是知其然,依旧不知其所以然。

附网贴原文:
Friday, April 20, 2012
I recently had the most infuriating issue with a scheduled task on a Win 2008 R2 server.

All I wanted it to do was run a .bat file once a day which fired a WGET script. If I logged into the server and clicked the bat file, it fired and triggered the WGET script and everything worked.

But whenever I tried to run it from the Task Scheduler (Either manually pressing run or letting it fire off the schedule), it failed and returned the code 0×1

I checked the history and log and it said it successfully completed with the following:

action “C:\Windows\SYSTEM32\cmd.exe” with return code 1

Great bit of information! Anyway, I spent quite some time changing permissions and users to no avail. I managed to make it work in the end, but its still not completely obvious to me why this makes a difference.

Instead of putting the full file path in the program/script textbox, use the Start in (Optional) field to put the folder that the .bat file is actually in – Like so:

Once you have done this, make sure you tick the ‘Run with highest privileges’ tick box

And that’s it. Its now returning the correct result code 0×0 and my script is running! Finally.

052013
 

装了wxPython用于GUI程序的开发,照例打开Eclipse+Pydev的开发环境,发现输进去示例程序也是祖国山河一片红。

什么wx.App,什么wx.Frame之类的,全被标注了红色提示下滑波浪线。报错“undefined variable from import: App”之类的。但是直接运行,完全木有问题,无语凝咽了我就。

请出StackOverflow大神,想必我遇到的任何问题大神们都遇到过了。果然,找到了一篇问答。

Undefined variable from import when using wxPython in pydev

其中的前面几个试了一下,没有立竿见影的效果。而倒数第二条则很有趣:

Try
wx = wx
Don’t ask why. This approach (that I found when trying to break the problem in smaller parts) just seems to remove the wx undefined variables problem.

看上去很没意义,结果一试就灵,红色提示立马就没了。回头来删掉这句,错误提示没再出现。问题解决。

十一 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了。

272012
 

回头看一下,发现已经一个月没有发博客文章了。最近一个月被导师的项目弄得焦头烂额的,终于明白做技术和做工程的差异。导师的项目大多数与计算机只是有所关联,但是侧重点是在于完成一整个的工程。

这次的项目从最初的需求分析,准备标书,投递标书,到后面的硬件开发和软件开发主要都是我在做,所以必须考虑很多的问题。而这些问题导师是只求结果不管过程的,所以我很辛苦的忙活了好久好久。

软硬件的设计和开发并不是大问题,真的问题是购买很多项目需要的设备的过程,比较麻烦,涉及到货物的的进口和教学仪器免税的办理,涉及到性能的要求和仪器向最终用户交付时的罗嗦。到现在这个罗嗦的交付还没有完成,原因在于用户根本搞不清楚他们的需求……

果然需求是大敌。

购买一些大件的设备的时候更复杂,从寻找厂商到谈需求,谈价格,谈合同,谈付款,去验货,到货验收,没有一项是顺利的,充满了各种的纠结,不容易。最近的一个月就纠缠在和用户的沟通和某个最后购买的大型部件上面了,炎热的季节,宿舍木有空调,导师火气也不小,唉……

好不容易放暑假啦,回来休整一下,处理一些私人问题。还要看看书,复习和总结一下各种知识。再过一个多月就是找工作的时候了,人生的一件大事,不可儿戏,得好好准备一下。很羡慕已经找好工作的几个朋友,都是说出来让人眼馋的公司;不过貌似他们也在羡慕我,还有两个假期可以过。哈哈。

 Posted by at 09:25
052012
 

学校已经放假了,天气也热的让人无法忍受,可是一时半会儿还是不能回家。还要给导师的一个项目收尾。编写代码什么的事情做做还是挺顺手的,可是这项目剩下的是采购一样东西,所以很繁琐。我向来在买东西上面是很白痴的,何况又有这么一个思维天马行空的老板,痛苦啊。

前面的两篇博文那个系列还没有完成,最近也没有精力去写,可能还得再一阵子了。同时关于数据抓取的事情,那位老师又找我开始了一个新的任务,简单的分析了一下,也是比较快的搞定了。那是个Flex的前端显示,和之前这个Silverlight的不同之处还是挺多的,因此完成任务的方式也和这个完全不同了,有机会再写吧。

好想回家啊,这天气不能忍啊,赶紧毕业吧~

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三个第三方库。