DeepSeek 官方修正了 V3 的激活参数量说明

在之前的博客《DeepSeek V3 模型各子模块参数量精算》中,我计算的模型激活参数量跟官方 README_WEIGHT.md 中的说明对不上。之后有读者跟我说,官方更新了激活参数量的数字。我查了一下 commit history,具体修改如下:

DeepSeek V3 README_WEIGHTS.md commit

可以看到,V3 模型激活参数量从 36.7 改成了 36.6,并且去掉了包含 0.9B Embedding 的说明,那基本上跟我的计算完全对上了。MTP 激活参数量从 2.4B 改成了 1.5B,也去掉了 0.9B 的 Embedding,跟我的计算还是有 0.1B 的差异。

Anyway,这种总量统计只是为了揭示计算的大约规模,有点差异也不影响定性结论。真正有用的是你在拆分 TP、EP 等权重矩阵时,矩阵的形状是多大,要拆多少份,每份大概多大。

为了分析像 DeepSeek V3 这样的超大模型具体参数,我写了一个小脚本,可以将 safetensors 文件里面的权重 Shape 提取出来,并且可以按不同的层级做参数量的聚合计算:

https://github.com/solrex/solrex/blob/master/snippets/show_safetensors.py

#!/usr/bin/env python3
import os
import argparse
import torch

from safetensors import safe_open

def print_tensor_tsv(model_dir, depth):
    '''Print tensor info in .safetensors into tsv format'''
    TENSOR_CLASS = {
        'weight': 'weight',
        'e_score_correction_bias': 'weight',
        'weight_scale_inv': 'scale'
    }
    print('SafetensorsFile\tTensorKey\tTensorParams\tTensorType\tTensorShape')
    safetensor_files = sorted([f for f in os.listdir(model_dir) if f.endswith('.safetensors')])
    summary = {}
    for filename in safetensor_files:
        file_path = os.path.join(model_dir, filename)
        with safe_open(file_path, framework='pt') as f:
            for key in f.keys():
                tensor = f.get_tensor(key)
                print(f'{filename}\t{key}\t{tensor.numel()}\t{tensor.dtype}\t{tensor.shape}')
                lst = key.split('.')
                # Get suffix: .weight or .weight_scale_inv
                tclass = TENSOR_CLASS[lst[-1]]
                # Limit prefix to dep
                dep = min(len(lst), depth+1) if depth > 0 else len(lst)
                # Get summary of prefixes
                for prefix in ['.'.join(lst[:i]) for i in range(0, dep)]:
                    summary[f'{tclass}[{prefix}]'] = summary.get(f'{tclass}[{prefix}]', 0) + tensor.numel()
    for key in sorted(summary):
        print(f'Summary\t{key}\t{summary[key]}\t\t')

if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Print tensor shape and dtype of .safetensors file')
    parser.add_argument('model_dir', nargs='?', default='.', help='Model directory (default: $PWD)')
    parser.add_argument('--summary_depth', '-d', type=int, default=3, help='Summary depth of weights')
    args = parser.parse_args()
    print_tensor_tsv(args.model_dir, args.summary_depth)

在 HuggingFace 模型根目录下执行 ./show_safetensors.py ,即可获得当前模型的所有权重 Shape 和前 3 层的聚合权重规模。可以通过 “-d” 参数调整最大聚合的层级。输出的文件是 tsv 格式的,可以导入表格进行再计算。

以下是使用 show_safetensors.py 分析 DeepSeek V3 参数量的示例:

$ ./show_safetensors.py -d 2
SafetensorsFile TensorKey TensorParams TensorType TensorShape
model-00001-of-000163.safetensors model.embed_tokens.weight 926679040 torch.bfloat16 torch.Size([129280, 7168])
model-00001-of-000163.safetensors model.layers.0.input_layernorm.weight 7168 torch.bfloat16 torch.Size([7168])
...
model-00163-of-000163.safetensors model.layers.61.shared_head.head.weight 926679040 torch.bfloat16 torch.Size([129280, 7168])
model-00163-of-000163.safetensors model.layers.61.shared_head.norm.weight 7168 torch.bfloat16 torch.Size([7168])
Summary scale[] 41540496
Summary scale[model.layers] 41540496
Summary scale[model] 41540496
Summary weight[] 684489845504
Summary weight[lm_head] 926679040
Summary weight[model.embed_tokens] 926679040
Summary weight[model.layers] 682636480256
Summary weight[model.norm] 7168
Summary weight[model] 683563166464

可以看到第一列为文件名(像 model-00001-of-000163.safetensors)的行是该文件中的具体权重信息,包含 Shape 信息;第一列为 Summary 的行,是根据模型的 tensor key 名字结构, 例如 “model.layers.0.input_layernorm.weight”,按照 “.” 切成前缀,按照前缀聚合模型参数量的结果,不包含 Shape 信息。

寻找最快的Python字符串插入方式

在 MapReduce 分布式计算时有这样一种场景:mapper 输入来自多个不同的数据源,共同点是每行记录第一列是作为 key 的 id 列,reducer 需要根据数据源的不同,进行相应的处理。由于数据到 reducer 阶段已经无法区分来自什么文件,所以一般采取的方法是 mapper 为数据记录打一个 TAG。为了便于使用,我习惯于把这个 TAG 打到数据的第二列(第一列为 id 列,作为 reduce/join 的 key),所以有这样的 mapper 函数:

def mapper1(line):
    l = line.split('\t', 1)
    return "%s\t%s\t%s" % (l[0], 'TAG', l[1])

这样给定输入:

s = "3001	VALUE"

mapper1(s) 的结果就是:

s = "3001	TAG	VALUE"

这是一个潜意识就想到的很直白的函数,但是我今天忽然脑子转筋,陷入了“这是最快的吗”思维怪圈里。于是我就想,还有什么其它方法呢?哦,格式化的表达式可以用 string 的 + 运算来表示:

def mapper2(line):
    l = line.split('\t', 1)
    return l[0] + '\t' + 'TAG' + '\t' + l[1]

上面是故意将 '\t' 分开写,因为一般 TAG 是以变量方式传入的。还有,都说 join 比 + 快,那么也可以这样:

def mapper3(line):
    l = line.split('\t', 1)
    l.insert(1, 'TAG')
    return '\t'.join(l)

split 可能要消耗额外的空间,那就换 find:

def mapper4(line):
    pos = line.find('\t')
    return "%s\t%s\t%s" % (line[0:pos], 'TAG', line[pos+1:])

变态一点儿,第一个数是整数嘛,换成整型输出:

def mapper5(line):
    pos = line.find('\t')
    pid = long(line[0:pos])
    return "%d\t%s\t%s" % (pid, 'TAG', line[pos+1:])

再换个思路,split 可以换成 partition:

def mapper6(line):
    (h,s,t) = line.partition('\t')
    return "%s\t%s\t%s" % (h, 'TAG', t)

或者干脆 ticky 一点儿,用 replace 替换第一个找到的制表符:

def mapper7(line):
    return line.replace('\t', '\t'+'TAG'+'\t', 1)

哇,看一下,原来可选的方法还真不少,而且我相信这肯定没有列举到所有的方法。看到这里,就这几个有限的算法,你猜一下哪个最快?最快的比最慢的快多少?

先把计时方法贴一下:

for i in range(1,8):
    f = 'mapper%d(s)' % i
    su = "from __main__ import mapper%d,s" % i
    print f, ':', timeit.Timer(f, setup=su).timeit()

下面是答案:

mapper1(s) : 1.32489800453
mapper2(s) : 1.2933549881
mapper3(s) : 1.65229916573
mapper4(s) : 1.22059297562
mapper5(s) : 2.60358095169
mapper6(s) : 0.956777095795
mapper7(s) : 0.726199865341

最后胜出的是 mapper7 (tricky 的 replace 方法),最慢的是 mapper5 (蛋疼的 id 转数字方法),最慢的耗时是最慢的约 3.6 倍。最早想到的 mapper1 方法在 7 种方法中排名——第 5!耗时是最快方法的 1.8 倍。考虑到 mapper 足够简单,这个将近一倍的开销还是有一点点意义的。

最后,欢迎回复给出更快的方法!

用词典查找代替VLOOKUP

从上一篇《PYTHON操作EXCEL》可以看到,Python 操作 Excel 已非常自如方便。但是 Python 和相关库毕竟是一个额外的依赖,若能从 Excel 自身解决此类问题,自然是更为易用。

1. VBA 中的哈希表

用 Python 的着眼点主要是 VLOOKUP 公式太慢了,所以关键是要找到一种更高效的算法或数据结构定位数据。VLOOKUP 要求对列进行排序,内部应该是对列内数据进行二分查找,算法上不好再优化了,那就只好更换一种数据结构。搜索了一下,VBA 提供了 Scripting.Dictionary 这一词典结构,而且有文章说内部是哈希表实现,那就正是我要的东西了。

这样,VLOOKUP(lookup_value,table_array,col_index_num,range_lookup) 这一公式就转为下面的词典查找方式来实现:

  • 使用要从中进行查找的 table_array 内容构建词典。用 table_array 第一列作为 key,table_array 第 col_index_num 列作为 value,插入 Dictionary 中:Dictionary.Add key, value;
  • 查找时只需直接取 Dictionary 内的值 Dictionary.Item(lookup_value),即可完成查找;

若是仅仅 VLOOKUP 一次,倒也不必费劲先建立起一个词典。但当使用同样 VLOOKUP 公式的单元格很多时(比如几万个),就显得其必要了。因为 Dictionary 只需要建立一次,就可以用 O(1) 的复杂度进行多次查找了。

2. VLOOKUP 慢,主要问题不在算法上

从算法角度,词典查找的确快于二分查找,但优势并不是那么明显。所以在具体执行时,我发现使用词典查找的 VBA 宏运行速度并不比 VLOOKUP 快多少,运行时 Excel 仍然会导致系统假死几个小时。按说如此简单的程序不应该那么慢,问题究竟在哪里呢?

经过一段摸索,我才发现问题的根源所在:

  • VBA 往 Excel 表格中填内容时,会引发表格中已有公式的自动计算,非常耗时;
  • Excel 表格内容更新时,会触发屏幕显示内容的自动刷新,代价也很高;

所以提高 VBA 脚本执行性能的关键点,在于计算时关掉公式自动计算和屏幕刷新,这也是我始料未及的。在 VBA 中实现这两点很容易,但由于 VLOOKUP 本身即是公式,我没能想通直接调用 VLOOKUP 时如何避免这两点带来的性能损失。

3. 示例 VBA 代码

在做了上面提到的两次优化之后,原来 VLOOKUP N 个小时才能完成的任务,只用了 7 秒钟就执行结束了。

下面是我写的一段示例代码。我不熟悉 VBA 语言,只是照葫芦画瓢。代码规范程度相差甚远,但题意应是体现其中了。有心的朋友可以用作参考。

Sub 在机器表上生成一级分中心()
'
' 在机器表上生成一级分中心 Macro
'
Application.Calculation = xlCalculationManual
Application.ScreenUpdating = False

t0 = Timer
' 词典
Set map_dict = CreateObject("Scripting.Dictionary")

' 打开分中心映射表
Set map_sheet = Worksheets("分中心映射表")
map_nrows = map_sheet.Range("A300").End(xlUp).Row
Set my_rows = map_sheet.Range("A2:B" & map_nrows).Rows

' 遍历分中心映射表,获得 分中心 对应的一级分中心,插入词典
For Each my_row In my_rows
   center = my_row.Cells(1, 1).Value
   city = my_row.Cells(1, 2).Value
   If Not map_dict.Exists(center) Then
       map_dict.Add center, city
   End If
Next my_row

' 打开机器表
Set dispatch_sheet = Worksheets("机器表")
dispatch_nrows = dispatch_sheet.Range("G99999").End(xlUp).Row
Set my_rows = dispatch_sheet.Range("K2:L" & dispatch_nrows).Rows

' 遍历开通表,通过词典获得 machine_id 对应的一级分中心,插入开通表
For Each o_row In my_rows
   center = o_row.Cells(1, 1).Value
   o_row.Cells(1, 2).Value = map_dict.Item(center)
Next o_row

MsgBox "在机器表上生成一级分中心。共处理 " & dispatch_nrows & " 条记录,总耗时" & Timer - t0 & "秒。"

' 销毁建立的词典
Set map_dict = Nothing

' 打开自动计算和屏幕刷新
Application.Calculation = xlCalculationAutomatic
Application.ScreenUpdating = True
'
End Sub

最后补充一点:我先实现的词典查找,后发现性能问题根源,所以未能去比较 VLOOKUP 与词典查找两种方式的具体性能差异。我想如果差异可以忍受,那么直接在 VBA 中调用 VLOOKUP 公式或许是一种更为简单的实现。

Python操作Excel

老婆单位有时候有一些很大的 Excel 统计报表需要处理,其中最恶心的是跨表的 JOIN 查询。他们通常采取的做法是,把多个 Excel 工作簿合成一个工作簿的多个表格,然后再跑函数(VLOOKUP之类)去查。因为用的函数效率很低,在 CPU 打满的情况下还要跑几个小时。

然后我就看不过去了,我也不懂 Excel,不知道如何优化,但我想用 Python+SQLite 总归是能够实现的。于是就尝试了一把,效果还不错,一分钟以内完成统计很轻松,其中大部分时间主要花在读 Excel 内容上。

1. Python 操作 Excel 的函数库

我主要尝试了 3 种读写 Excel 的方法:

1> xlrd, xlwt, xlutils: 这三个库的好处是不需要其它支持,在任何操作系统上都可以使用。xlrd 可以读取 .xls, .xlsx 文件,非常好用;但因为 xlwt 不能直接修改 Excel 文档,必须得复制一份然后另存为其它文件,而且据说写复杂格式的 Excel 文件会出现问题,所以我没有选它来写 Excel 文件。

2> openpyxl: 这个库也是不需要其它支持的,而且据说对 Office 2007 格式支持得更好。遗憾地是,我经过测试,发现它加载 Excel 文件的效率比 xlrd 慢 3 倍以上,内存使用在 10 倍以上,于是就放弃了。

3> win32com: Python Win32 扩展,这个库需要运行环境为 Windows+Office 对应版本。由于 Python Win32 扩展只是把 COM 接口包装了一下,可以视为与 VBA 完全相同,不会有读写格式上的问题。尝试了一下用 win32com 读取 Excel 文件,效率还是比 xlrd 慢一些。

由于读取效率上 xlrd > win32com > openpyxl,所以我自然选择了 xlrd 用来读取统计报表;而最终输出的报表格式较复杂,所以选择了 win32com 直接操作 Excel 文件。

2. Python 里的关系型数据库

SQLite 是一个非常轻量级的关系型数据库,很多语言和平台都内置 SQLite 支持,也是 iOS 和 Android 上的默认数据库。Python 的标准库里也包含了 sqlite3 库,用起来非常方便。

3. 用 xlrd 读取 Excel 并插入数据库样例

如果数据量不大,直接用 Python 内部数据结构如 dict, list 就够了。但如果读取的几张表数据量都较大,增加个将数据插入数据库的预处理过程就有很大好处。一是避免每次调试都要进行耗时较长的 Excel 文件载入过程;二是能充分利用数据库的索引和 SQL 语句强大功能进行快速数据分析。

#!/usr/bin/python
# -*- coding: gbk -*-

import xlrd
import sqlite3

# 打开数据库文件
device_city_db = sqlite3.connect('device_city.db')
cursor = device_city_db.cursor()

# 建表
cursor.execute('DROP TABLE IF EXISTS device_city')
cursor.execute('CREATE TABLE device_city (device_id char(16) PRIMARY KEY, city varchar(16))')
 
# 打开 device 相关输入 Excel 文件
device_workbook = xlrd.open_workbook('输入.xlsx')
device_sheet = device_workbook.sheet_by_name('设备表')

# 逐行读取 device-城市 映射文件,并将指定的列插入数据库
for row in range(1, device_sheet.nrows):
   device_id = device_sheet.cell(row, 6).value
   if len(device_id) > 16:
       device_id = device_id[0:16]
   if len(device_id) == 0:
       continue
   city = device_sheet.cell(row, 10).value
   # 避免插入重复记录
   cursor.execute('SELECT * FROM device_city WHERE device_id=?', (device_id,))
   res = cursor.fetchone()
   if res == None:
       cursor.execute('INSERT INTO device_city (device_id, city) VALUES (?, ?)',
                      (device_id, city))
   else:
       if res[1] != city:
           print '%s, %s, %s, %s' % (device_id, city, res[0], res[1])
device_city_db.commit()

4. 将结果写入 Excel 文件样例

使用 win32com 写入 Excel 的时候要注意,一定要记得退出 Excel,否则下次运行会出错。这需要增加异常处理语句,我这里偷了个懒,出了异常后要手动杀死任务管理器中的 excel 进程。至于 win32com 中类的接口,可以从 MSDN 网站查阅。

import win32com.client as win32
import os
excel = win32.gencache.EnsureDispatch('Excel.Application')
excel.Visible = False
# 貌似这里只能接受全路径
workbook = excel.Workbooks.Open(os.path.join(os.getcwd(), '输出.xlsx'))
month_sheet = workbook.Worksheets(1)
# 计算文件中实际有内容的行数
nrows = month_sheet.Range('A65536').End(win32.constants.xlUp).Row
# 操作 Excel 单元格的值
for row in range(5, nrows-4):
   month_sheet.Cells(row, 1).Value += something
# 保存工作簿
workbook.Save()
# 退出 Excel
excel.Application.Quit()

Python JSON模块解码中文的BUG

很多语言或协议选择使用 ASCII 字符 “\”(backslash,0x5c) 作为字符串的转义符,包括 JSON 中的字符串。一般来说,使用 Python 中的 JSON 模块编码英文,不会存在转义符的问题。但如果使用 JSON 模块编解码中文,就可能面临着中文字符包含转义符带来的 bug。本篇文章给出了一个 badcase。

中文解码错误

测试用例文件里面包含繁体的“運動”二字,使用 GB18030 编码。使用 json 解码的错误如下:

$ cat decode.dat
{"a":"運動"}
$ python
>>> import json
>>> fp=open('decode.dat', 'r')
>>> json.load(fp, encoding='gb18030')
Traceback (most recent call last):
  File "", line 1, in 
  File "/home/yangwb/local/lib/python2.7/json/__init__.py", line 278, in load
    **kw)
  File "/home/yangwb/local/lib/python2.7/json/__init__.py", line 339, in loads
    return cls(encoding=encoding, **kw).decode(s)
  File "/home/yangwb/local/lib/python2.7/json/decoder.py", line 360, in decode
    obj, end = self.raw_decode(s, idx=_w(s, 0).end())
  File "/home/yangwb/local/lib/python2.7/json/decoder.py", line 376, in raw_decode
    obj, end = self.scan_once(s, idx)
UnicodeDecodeError: 'gb18030' codec can't decode byte 0xdf in position 0: incomplete
multibyte sequence

发生这个问题的原因,就存在于“運”字的编码之中。“運”的 GB18030 编码是 0xdf5c,由于第二个字符与转义符 “\” 编码相同,所以剩下的这个 0xdf 就被认为是一个 incomplete multibyte sequence。

我本来认为,既然已经提供了编码,json 模块就能够区分汉字与转义符(所以我觉得这应该是 json 的一个 bug)。但从实验来看,并非如此。对于一些不需提供字符编码的 JSON 解码器来说,我们倒可以用一种比较 tricky 的方法绕过上面这个问题,即在“運”字后面加一个额外的转义符:

{"a":"運\動"}

遗憾的是,这种方法对 Python 的 json 模块不适用。我仍不知道该如何解决这个解码问题。

中文编码——没错误!

对于相同的 case,Python 倒是能够编码成功:

$ cat in.dat
運動
$ python
>>> import json
>>> in_str = open('in.dat', 'r').read()
>>> out_f = open('out.dat', 'w', 0)
>>> dump_str = json.dumps({'a': in_str}, ensure_ascii=False, encoding='gb18030')
>>> out_f.write(dump_str.encode('gb18030'))
$ cat out.dat
{"a": "運動"}

所以这件事情就把我给搞糊涂了,Python 的 json 模块不能解码自己编码的 json 串。所以我觉得这可能是一个 bug,或者至少是 2.7.1 版本的 bug。

PS: 要仔细看文档

20120516:经网友 TreapDB 提醒,加载字符串时自己做 Unicode 转换,貌似能够解决这个问题。

$ cat decode.dat
{"a":"運動"}
$ python
>>> import json
>>> in_str = open('decode.dat', 'r').read().decode('gb18030')
>>> json.loads(in_str)

回头仔细看了一下 json 的文档,其中有这么一段:

Encodings that are not ASCII based (such as UCS-2) are not allowed, and should be wrapped with codecs.getreader(encoding)(fp), or simply decoded to a unicode object and passed to loads().

已经注明了 encoding 不支持非 ASCII-based 编码的参数,所以应该使用 getreader 进行转码,而不是让 json 模块去转码。看来是我没读懂文档,大惊小怪了,回家面壁去!

>>> json.load(codecs.getreader('gb18030')(fp))

An IPv6 Enabled NTP Client for Windows in Python

Python NTP library (ntplib) offers a simple interface to query NTP servers from Python. But it does not support IPv6 NTP servers. I wrote a patch for ntplib to support IPv6 connections. You can download the patch file here and the patched library here.

The code bellow is a simple IPv6 enabled NTP client (ntpdate.py) in Python for Windows, using the patched ntplib. It doesn't (and won't) support Linux because the official NTP release offers IPv6 support on that platform.

#!/usr/bin/env python
# ntpdate.py - set the date and time via NTP
# An IPv6 enabled ntp client, for Windows ONLY.

import ntplib, time
from os import system
from sys import argv

def usage():
  print '''Usage: ntpdate.py  [-qh] server
Example:
  ntpdate.py 210.72.145.44      # IPv4
  ntpdate.py ntp6.remco.org     # IPv6
Options:

  -q     Query only - don't set the clock.
  -h     Print this message.

IPv6 NTP Server List:
  ntp6.remco.org               [2001:888:1031::2]
  ntp6.space.net               [2001:608:0:dff::2]
  time.buptnet.edu.cn          [2001:da8:202:10::60]
  time.join.uni-muenster.de    [2001:638:500:717:2e0:4bff:fe04:bc5f]
  ntp.sixxs.net                [2001:1291:2::b]
  ntp.eu.sixxs.net             [2001:808::66]
  ntp.us.sixxs.net             [2001:1291:2::b]
  ntp.rhrk.uni-kl.de           [2001:638:208:9::116]
  ntp.ipv6.uni-leipzig.de      [2001:638:902:1::10]
  ntp.hexago.com               [2001:5c0:0:2::25]
  ntp1.bit.nl                  [2001:7b8:3:2c::123]

Report bugs to http://solrex.org.'''
  sys.exit()

def main():
  ntp_svr = ''
  query = False

  for a in argv[1:]:
    if a == '-q':
      query = True
    elif a == '-h':
      usage()
    else:
      ntp_svr = a
  if ntp_svr == '':
    usage()

  c = ntplib.NTPClient()
  res = c.request(ntp_svr, version=3)
  t_epoch = res.offset + res.delay + time.time()
  t = time.localtime(t_epoch)
  centi_sec = t_epoch%1 * 100
  time_str = time.strftime('%H:%M:%S', t)
  if not query:
    system('time %s.%2.0f' % (time_str, centi_sec))
    date_str = time.strftime('%Y-%m-%d', t)
    system('date %s' % date_str)
  if query:
    print 'server %s, stratum %d, offset %f, delay %f' % (
           ntp_svr, res.stratum, res.offset, res.delay)
  print '%s %s ntpdate.py: time server %s offset %f sec' % (
         time.strftime('%d %b', t), time_str, ntp_svr, res.offset)

if __name__ == '__main__':
  main()

一个 Windows 对时小工具

由于在 CERNET 内,我经常需要用代理上网,没办法直连到 NTP 服务器,因此不能使用 Windows 时间服务对时。偶尔维修电脑或者不小心调整错时间,再加上电脑时钟本身就有一定的漂移,对时就变成了件麻烦的事情。

手动调时也没个参照,误差往往比较大。IPv6 网络上存在一些 NTP 服务器,Linux 下有 ntpdate 是支持 IPv6 NTP 服务器的,但是我搜索了半天,才在一篇文章上看到有人评论说 Windows 下只有一款 NTP 客户端支持 IPv6,还是收费软件——可他也没给出名字。

无奈之下想到 Python 的 httplib 是支持 IPv6 连接的,于是我就仿照 htpdate 写了一个利用 Google 的 IPv6 Web 服务器进行对时的 Python 小工具 htpdate.py。虽然误差比 NTP 大不少,但是还是在可接受范围内(不到 1 秒),而且比较方便,连日期也一块更新了。下面是代码,比较粗糙。

#!/usr/bin/env python
import httplib, time
from os import system

def main():
  conn = httplib.HTTPConnection('google.com')
  time.clock()
  conn.request('HEAD', '')
  t_rtt = time.clock()
  res_time = conn.getresponse().getheader('date')
  t = time.localtime(time.mktime(time.strptime(res_time,
                                 '%a, %d %b %Y %H:%M:%S %Z')) - time.timezone)
  time_str = time.strftime('%H:%M:%S', t)
  local_time = time.asctime()
  t_exe = time.clock()
  centi_sec = (t_exe - t_rtt/2)*100
  if centi_sec > 99:
    centi_sec = 99
  system('time %s.%2.0f' % (time_str, centi_sec))
  date_str = time.strftime('%Y-%m-%d', t)
  system('date %s' % date_str)
  print 'LOCAL  TIME: ' + local_time
  print 'SERVER TIME: ' + time.asctime(t)
  print 'LOCAL  TIME: ' + time.asctime()
  if (t_exe - t_rtt/2) >= 1:
    print 'Round trip time is too long. Time error might be larger than 1 sec.'

if __name__ == '__main__':
  main()

关于 eYouIPB 和 CASNET

我不知道为什么在干正事的时候想写这个东西,大概也算是一种强迫症吧,想起来某件事情就总觉得有义务马上去做。那么就赶快写完吧。

这篇文章是写给有心人看的,如果您不知道 eYouIPB 和 CASNET 是什么东西,那么您可以无视以下内容。

eYouIPB 是中科院研究生院目前使用的网关登录客户端,但只能在 Windows 下使用。我用 Python 写的一个小软件 CASNET,就是一个 Linux 下的登录替代品,不过目前它也可以支持 Windows。昨天有同学发信问我有关科苑网关登录的问题,我顺便在这里做个笔记吧。

1. 有关流量统计。目前 CASNET 不支持像 eYouIPB 那样的实时的流量统计功能,因为我觉得没有必要。科苑上网是按照流量收费的,而这个流量是指网关服务器统计的流量,而不是客户端统计的流量,所以客户端的实时精确流量统计没有太大意义。

eYouIPB 带有实时的统计流量功能,但这却造成了一些问题。eYouIPB 使用 WinPcap 库做流量统计,而且这个库的版本比较低。假如系统里安装了新的 WinPcap 库,例如 Wireshark(Ethereal) 网络监听程序就会安装 WinPcap 库,那么 eYouIPB 就无法工作了。WinPcap 是一个非常强大的抓包库,而 eYouIPB 仅仅使用了其中的流量统计功能,实在很让人费解它为什么要使用 WinPcap 库并且将它的 dll 包含在自己的软件中。我想应该有更简单的办法做流量统计——如果非做不可的话。

2. 有关登录方式。科苑上网有两种登录方式:一种是 eYouIPB,使用私有的协议;一种是网页登录,使用 https 表单交互。由于 eYouIPB 协议私有,CASNET 只能模拟网页登录的方式,所以您会发现 CASNET 登录速度会比 eYouIPB 慢一点儿,因为它有一个完整的 https 安全协商和交互过程,而 eYouIPB 只需要几次简单 tcp 数据包交换就可以了。不过因为同在一个局域网内,这个速度还是在可以忍受的范围之内。

3. 有关安全性。去年我曾经研究过 eYouIPB 的协议,发现还是相对简单的。我记录的笔记已经丢失,但大概是这个样子的(应用层):“首先 eYouIPB 向服务器发起一个连接,服务器应答可以连接,然后 eYouIPB 将用户名和域发送给服务器,服务器看上去会返回一个密钥,然后 eYouIPB 使用该密钥用某种加密算法加密用户口令(或者还有其它信息)发送给服务器,服务器验证口令,回应是否可以登入。”

从该协议的实现来看,安全性有限。一个是用户可以通过不停地登入登出收集明密文对,另外反汇编 eYouIPB 看起来也不是那么困难,尤其是可以使用 Winsock 32 的 API 来定位负责加密的代码段。一旦加密算法被了解,窃听 eYouIPB 的数据包就能获得用户的口令,那么帐户就可以被窃取。所以相比而言,https 的登录方式更值得信任。

PS: 今天顺便搜索了一下,有个朋友曾经对 1.03 版本的 eYouIPB 协议进行过分析,我感觉和 2.0 版本差距不大。而且这个人完全用 C 实现了 eYouIPB 1.03 的加解密函数如果他仅仅凭逆向工程做出这个结果的话,我是相当地佩服的(因为我花了一整天的工夫也没有做出来一点儿东西)。如果当初我能看到这个结果的话,说不定就有 clue 去模拟 eYouIPB 的协议而不是使用 https 方式了,现在是懒得再去钻研这个东西了。只是不知道 2.0 是否还在使用同样的加解密函数,如果仍在使用的话,那么对有心人来说这个口令保护措施基本上算是不存在了。

4. 有关软件运行速度。这个比较主要是 Windows 平台下,CASNET 是用 Python 脚本写成的,而看起来 eYouIPB 使用 VC++ 写的,在软件的大小和运行速度上 eYouIPB 要显然优于 CASNET。由于 CASNET 的 Windows GUI 版需要额外的 GTK 运行时库和 Python 库支持,大概会加载到内存中 16M 左右的数据,不过这些东西运行时真正用到的不多,所以有些部分除了初始化时,其它时候很可能是驻留在交换区中,所以还是可以接受的。

在 Linux 平台下,因为 GTK 和 Python 库是被非常广泛使用的,所以 CASNET 并不需要额外占用很多内存。

之所以会发布 Windows 版,是因为相比 eYouIPB 而言,Wireshark 对我更重要,所以我无法使用 eYouIPB,而又讨厌网页登录时每次都要选择下拉菜单。再加上顺便看看 PyGtk 在 Windows 下的表现如何。但比较讽刺的是,Windows GUI 版的 CASNET 下载量要超过其它版本加一起的下载量,看来它的存在还是对某些用户有一些帮助。

5. 有关软件功能。eYouIPB 的功能已经被锁定了很多年,但是 CASNET 一直在根据用户的需求增加或者删减某些功能,例如余额不足提醒,断线自动重连,一键切换登录模式等。还有一个未发布的功能是关机自动下线。这是一个我一直想实现的功能,因为当用户关机忘记离线时,恰好分配到原 IP 的用户就可以使用该帐号,造成流量被窃取。这个功能在 Linux 版本上已经基本实现,但是 Windows 版本仍然没有找到可用的方法实现。

GUCAS IP 网关登录客户端 1.3 发布

如果您不知道这软件是干嘛的,那您就不用往下看了。这个软件是中科院研究生院师生使用的,更新公告发表在这里只是为了记录一下版本发布历史。

软件主页:http://share.solrex.org/casnet

最新版本 1.3-1(2009年2月11日发布) 更新

  1. 增加了单击更换登录模式功能。
  2. 增加了自动断线充连功能。
  3. 增加了余额不足提醒功能。
  4. 解决了以前版本的一些 BUG。

尽量别使用 Py2exe for Python 2.6

Py2exe 是用来将 Python 程序打包成 Windows 下可执行 exe 程序的工具。这样那些未安装 Python 开发环境的用户就可以直接使用 Python 写的软件了。

Py2exe 并不是把 Python 程序编译成 Windows 的原生程序,而是将运行 Python 所需的 dll, lib 等打包到一起供 Python 程序使用。一个很短的 Python 程序往往会生成几兆的软件包。因此 Py2exe 打包程序的执行效率并不会有提升,只是方便初级 Windows 用户使用罢了。

除了 Py2exe 之外,还有一些其它的 Python 到 exe 的打包程序,比如 Pyinstaller、cx_Freeze 等。它们在某些情况下表现比 Py2exe 要好,但是在兼容性和用户群支持上不如 Py2exe(一家之见)。

前两天我图新鲜,把 Windows 中的 Python 升级到了 2.6,PyGtk 和 Py2exe 也随之升级到了支持 2.6 的版本。然后问题就来了,用 Py2exe for Python 2.6 打包的 PyGtk 程序在打包的机器上运行正常,但拷贝到部分人的 Windows 中后却无法运行,点击就出现

“由于应用程序配置不正确,应用程序未能启动。重新安装应用程序可能会纠正这个问题。”

刚开始我以为是 Win 下 GTK 库 dll 的问题,反复地验证几次觉得应该问题不在 GTK 上。如果缺少外部库,Python 应该报缺少 dll 问题,而不是应用程序出错。怀疑是缺少微软的库,用 PE Explorer 试用版在出问题的电脑上扫描一下应用程序的依赖关系,发现缺少 msvcr90.dll,将 msvcr90.dll 拷贝到程序包中,再扫描依赖关系,所有的 dll 已经都满足了,仍然报出同样的错误。

后来基本确定是 Python 2.6 的问题。因为 Python 2.6 是使用 Microsoft Visual C++ 2008 编译的,所以要想 py2exe for 2.6 打包的程序运行,目标机器上必须装有 Python 2.6 或者 Microsoft Visual C++ 2008 Redistributable Package。否则系统就无法识别 exe 程序的 CRT, 因而它就成为无法运行的程序。

之所以程序在一部分人的机器上运行正常,是因为这些人 Windows 中安装了 VC2008 开发套件,自然也就包括了 VC2008 运行时库。

因为我们发布程序时无法强制每个人都去安装 Microsoft Visual C++ 2008 Redistributable Package,所以需要发布 exe 程序时,还是使用老版本的 Python 2.5 和 Py2exe for Python 2.5,别使用 Python 2.6 为好。

Python 不支持杀死子线程

昨天为我的 casnet 程序添加新功能。其中一个功能是断线自动重连,本来是单线程的程序,添加这个功能就需要后台有一个线程定时地查询当前状态,如果掉线就自动重连。因之遇到了一个如何设计这个守护线程的问题。

我刚开始的想法是后台线程每次运行查询后 sleep 一段时间,然后再运行查询。但是我马上遇到了一个问题:当主程序退出时,后台线程仍在运行,主窗口无法退出。

在使用其它的库时,比如 POSIX 的 pthread,可以使用 ptread_cancel(tid) 在主线程中结束子线程。但是 Python 的线程库不支持这样做,理由是我们不应该强制地结束一个线程,这样会带来很多隐患,应该让该线程自己结束自己。所以在 Python 中,推荐的一种方法是在子线程中循环判断一个标志位,在主线程中改变该标志位,子线程读到标志位改变,就结束自己。

import threading

class X(threading.Thread):
  def __init__(self):
    threading.Thread.__init__(self)
    self.flag = 1

  def run(self):
    while self.flag == 1:
      sleep(300)
      ...

如果直接使用这种方法,那么我前面的设计就会出现问题。因为线程会被 sleep 阻塞一段时间,那么只有在 sleep 的间隙,才有可能去读取标志位。这样主线程需要等待当前 sleep 结束才能使子线程退出,进而整个程序才能退出。这种做法是行不通的,你不可能指望用户点击“关闭窗口”后等待几百秒程序才能退出。

当然,也可以使用系统命令 kill 来杀死整个进程。但问题是这样做既不 graceful,又不能保证代码对不同系统的兼容性。

只好换个思路,从原来后台进程的设计改起。定时执行未必非得使用 sleep,也可以像 crontab 那样判断当前时间能不能整除某个值,但这样做不能保证任务在某个时间间隔内只执行一次,因为除数的精度和任务的执行时间不好把握;或者使用 timer,但是 timer 会带来更多线程,增加了复杂度。

于是最后决定使用解决 Feedbuner 图标定时抓取问题的方法。在线程中保存上次查询时间,比较当前时间与上次查询时间的差,若大于某个值,就进行查询并更新保存的时间。

  def run(self):
    self.last = time.time()
    while self.flag == 1:
      Now = time.time()
      if Now - self.last > 300:
         self.last = Now
         ...

这样就既能保证子线程在 flag 改变之后尽快退出,又能保证在指定时间间隔内任务只运行一次。但是网友 earthengine 兄指出这种方法并不妥,代码中不用 sleep 就变成了忙循环,这样会造成 CPU 使用率过高的问题,仅仅在循环中间添加一个 sleep(0~1) 就能大幅度地降低 CPU 使用,而且关闭程序时 1 秒钟以内的延迟对于用户来说一般还是可以接受的。

  def run(self):
    self.last = time.time()
    while self.flag == 1:
      sleep(1)
      Now = time.time()
      if Now - self.last > 300:
         self.last = Now
         ...

再深入思考一下,虽然本文中的后台线程从功能上来看似乎用不着考虑太多同步的问题,但最后的退出过程可视为一个线程同步的过程。因此可以采用线程同步的思想来设计后台线程:在正常工作时,后台线程进行带超时的等待,超时后就执行工作;退出时主线程给后台线程发送一个信号,由于后台线程在超时等待,因此接收信号后就终止退出。这样,在用户结束程序时,就不用等待 sleep 到时了。

import threading

class X(threading.Thread):
  def __init__(self):
    threading.Thread.__init__(self)
    self.flag = 1
    self.cond = threading.Condition()

  def run(self):
    self.cond.acquire()
    self.condition.wait(300)
    while self.flag == 1:
      ...
      self.cond.release()
      self.cond.acquire()
      self.condition.wait(300)

...
x.flag = 0
x.cond.acquire()
x.cond.notify()
x.cond.release()

最后,非常感谢 earthengine 兄的精彩评论,小弟受益良多。

定制自己的免费天气预报短信

摘要:这篇博客介绍了一种在 Linux 下使用飞信(libfetion 库)来定时发送天气预报短信的方法。本文的主要贡献是:一、提供了一个 Linux 下发送飞信的命令行程序;二、提供了一个到中国气象网抓取、过滤天气信息并发送短信的脚本。

Libfetion修改了调用接口,而且中国移动现在换IP登录就需要使用验证码。除非我哪天闲得蛋疼,搞一个验证码识别模块出来,否则本项目将不再维护,很抱歉!

天气预报短信一直是移动通信公司提供的一种收费服务,Google 免费天气预报服务打破了这个僵局。但是Google 的服务很不稳定,经常收不到短信,而且天气预报内容的定制性差。我家 xixi 一直有看天气预报的习惯,我就告诉她说我能写个程序每天给你发天气预报消息,她不相信,然后我就写了下面的程序。

首先感谢一下 mirth@bbs.nju.edu.cn,本文的主要内容是基于他在小百合 BBS 上发表的如何用飞信定时给自己发免费天气预报一文做的少许改进。

1. 发送飞信的命令行程序[1, 2, 3, 4, 5]

这个程序主要基于邓东东开发的 libfetion 库。这个库不是开源的,但是作者提供了头文件和库文件(在GUI源代码中),所以我们可以使用它的 API 来写一些自己的程序。下面的程序内容很简单,注释也不少,我就只贴源码,不再解释了(注意,编译时需要 curl 的 dev 库)。你可以在这里下载到我的 sendsms 小程序的源代码

sendsms
|-- Makefile
|-- include
|   |-- common.h
|   |-- datastruct.h
|   |-- event.h
|   |-- fxconfig.h
|   `-- libfetion.h
|-- lib
|   |-- libfetion_32.a
|   `-- libfetion_64.a
|-- sendsms
`-- sendsms.cpp

2. 到中国气象网抓取、过滤天气信息并发送短信的 bash 脚本

你可以从这里下载到下面的 bash 脚本,或者到这里下载几乎同样功能的 python 脚本。脚本就不多做解释了,没几行代码,相信稍微研究一下就能看懂。

天气网经常更新,新的脚本我就不再贴到博客里了。如果您发现天气预报脚本不好用了,就请关注脚本下载的地址,我一般会尽快更新的。

$ more weatherman.sh
#!/bin/bash
# This script fetch user specified citys' weather forecast from
# http://weather.com.cn, and send them using a CLI SMS sender "sendsms"
# which you can get from http://share.solrex.org/dcount/click.php?id=5.
#
# You can look for new or bug fix version
# @ http://share.solrex.org/scripts/weatherman.sh.
# Copyright (C) Solrex Yang <http://solrex.org> with GPL license.
#
# Usage: You should add it to crontab by "crontab -e", and then add a line
# such as:
# 00 20 * * * /usr/bin/weatherman.sh >> ~/bin/log/weatherman.log 2>&1
# which will send weather forecast to your fetion friends at every 8pm.

3. 将脚本设置为定时执行

安装好 sendsms 到 /usr/bin 之后,将上面脚本放到 YOURPATH 下,然后在命令行执行:crontab -e,将下面一行添加进去:

50 19 * * * /YOURPATH/weatherman.sh 1> /tmp/weatherman.out 2> /tmp/weatherman.err

就设置为每天下午 7 点 50 发送天气预报短信。

[1] 应大家要求,在程序中加入了读取 http_proxy 代理服务器环境变量的部分,其它类型的代理服务器可以自行添加(毕竟源代码给你了,随便改),增加了重试登录和发送的代码。

[2] 2008 年 11 月 30 日:增加了群发短信功能(多个接收者用','分隔)。

[3] 2009 年 01 月 11 日:增加从标准输入读入信息支持,可使用管道和输入重定向。这篇博客中的代码就不更新了,请到给出的链接去下载新版本。

[4] 2009 年 4 月 17 日:添加了"-l"选项,支持长短信发送,最长可到 1024 字节。解决了一个从标准输入读取短信的 bug。

[5] 2009 年 12 月 08 日:根据中国天气网的改版,更新抓取页面的脚本。

用 Linux 命令行工具自动追踪车票信息

前一篇博客中说到我买票失败的经历,也充分表达了我想买一张二手座票的意愿。怎么办呢?只好到网上各二手火车票信息平台去找了。心肠不好的人肯定幸灾了祸地在想:“哈哈,这个倒霉的小伙儿该对着浏览器不停地按 F5 了!” 你才 F5 呢,你们全家都 F5。那是典型的 Windows 用户的想法,不要以为 Linux User 跟你一样傻。

前面都是玩笑话 :),本文只是想介绍一下在 Linux 下有什么更方便的方法来追踪网页发布的信息,以展示 Linux 的命令行工具有多强大(也响应一下 Eric 师兄的文章:完全用键盘工作-3:常用的命令行工具)。

我们就拿火车网为例,通常情况下 Windows 用户为了在火车网上找一张二手火车票信息,会不断地到查询页面刷新,看有没有自己需要的车票。而一个 Linux 用户的做法会有何不同呢?一般来讲他会用工具来做这件事情,而不是在那傻刷,浪费时间。

怎么做呢?有很多种方法,我这里来介绍一种比较好玩的方法,用脚本自动跟踪信息,如果有结果就发送一个 Gtalk 消息给自己。

首先,写一个命令行发送 Gtalk 消息的 Python 脚本。其实我本打算用 freetalk 来做这件事的,奈何咱学识浅薄,不懂 freetalk 脚本该怎么写,也不知道 scheme 语言为何物。没办法,只好用 Python 来做了。下面内容就是用 Python 发送 gtalk 消息的脚本(需要 Linux 上装有 python-xmpp):

#!/usr/bin/env python
# -*- encoding: utf-8 -*-
# Usage: gtsent.py "SOMEBODY@gmail.com" "Message"

import xmpp
import sys

login = 'USERNAME' # without @gmail.com
pwd   = 'PASSWORD'

cnx = xmpp.Client('gmail.com', debug=[])
cnx.connect( server=('talk.google.com', 5223) )
cnx.auth(login, pwd, 'python')

cnx.send(xmpp.Message(sys.argv[1], sys.argv[2]))

将以上内容保存为 gtsend.py 文件,chmod u+x gtsend.py,修改 USERNAME 和 PASSWORD 为你的另一个非[常用] gmail 帐户名和密码。这样执行 ./gtsend.py SOMEBODY@gmail.com "Message" 就可以给 SOMEBODY@gmail.com 发送消息了(当然了,前提是 SOMEBODY@gmail.com 好友列表中有 USERNAME@gmail.com,请注意这里大写只是为了方便阅读)。

其次,写一个 Shell 脚本,用来追踪网页,过滤信息并发送 gtalk 消息。这个就更简单了,使用火车网提供的查询表单接口,用 wget 抓下来,再 grep 一下即可,bash 脚本如下:

#!/bin/bash
URL="http://www.huoche.com.cn/piao/2piaoserach.asp?ICheci=T65&type=0"
RESULT=`wget -O - $URL | iconv -f gbk -t utf8 | grep -i -e "t65.*南京.*硬座\
.*2008-9-28"`
if [ $(echo $RESULT | wc -c) -ge 5 ]
then
  /home/solrex/gtsend.py "YOURSELF@gmail.com" "$RESULT $URL"
fi

将该脚本保存为 get_tickt.sh,chmod u+x get_tickt.sh。这个脚本的工作流程是:wget 以 GET 方式提交对 T65 转让车票的查询,得到的结果输出到标准输出,然后将 GBK 编码转换为 UTF8 编码,再 grep 看是否含有“T65 南京 硬座 2008-9-28“关键词。如果有的话,用 gtsend.py 发送一个提醒消息给自己 gtalk 帐户;如果没有结果,什么都不做。

最后,将上面脚本加入 cron 列表每 10 分钟定时执行一次。 执行 crontab -e,添加下面一行即可(注意需要修改到该脚本的路径):

*/10 * * * * /home/solrex/get_ticket.sh

然后呢,你就可以高枕无忧,开着 Gtalk 等消息吧。当然,不一定能等得到 :(,唉,对我们来说, No news is BAD news!

当然,根据不同情况,你可以把追踪的信息换成别的东西。比如追女孩子的时候,可以用上面方法来实时跟踪她的最新博客,实时跟踪她在 BBS 上的留言,永远保持自己沙发的地位,说不定人家就感受到了你的关心,然后...具体方法我就不教了哈...

GUCAS IP 网关登录客户端版本1.2 发布

CASNET 是中科院内部 IP 控制网关登录客户端,支持 Linux 和 Windows 双系统。此软件完全使用 Python 语言写成,同时拥有命令行和图形界面,使用简单,安装方便,实乃中国科学院 IP 网关用户居家旅行必备之良品 :)。

CASNET 的官方主页:http://share.solrex.org/casnet

软件特性

  1. 同时支持 Linux 和 Windows 操作系统!
  2. 提供各种格式的安装包,方便安装过程。
  3. 客户端同时具有命令行和图形界面,满足不同用户需要。
  4. 可设置选项多,可保存用户设置,登录简单快捷。
  5. 纯 Python 编程,修改简单,扩展性强。
  6. 开放源代码,确保程序无后门。

最新版本 1.2-1(2008年6月7日发布) 更新

  1. 增加 Windows XP 系统支持。
  2. 解决了一些 BUG.

如何选择合适自己的安装包

if 您是 Linux 用户并且拥有 Python 和 PyGtk? 支持(一般的 Linux 发行版都有):
  if 您使用 RedHat系列的 Linux 系统(Fedora, RHEL, CentOS,...):
    请下载 casnet-1.2-1.i386.rpm
  elif 您使用 Debian 系列的 Linux 系统(Debian, Ubuntu,...):
    请下载 casnet-1.2-1_i386.deb
  else:
    请下载 casnet-1.2-1_i386.tar.gz
elif 您是 Windows 用户:
  if 您是 Python 开发者并确信您 Windows 系统里已经安装 Python 和 PyGtk:
    请下载 casnet-1.2-1_win32_Pygtk_Installed.zip
  else:
    请下载 casnet-1.2-1_win32.zip

GUCAS IP 网关 Linux 登录客户端版本 1.1 发布

CAS NET 是中科院内部 IP 控制网关的 Linux 登录客户端,此软件完全使用 Python 语言写成,同时支持命令行和图形界面,使用简单,安装方便,实乃中国科学院 Linux 使用者居家旅行必备之良品 :)。

官方主页

软件特性

  1. 同时提供源代码, .deb 和 .rpm 安装包,方便安装过程。
  2. 客户端同时具有命令行和图形界面,满足不同用户需要。
  3. 可设置选项多,可保存用户设置,登录简单快捷。
  4. 纯 Python 编程,修改简单,扩展性强,可移植到不同操作系统平台。
  5. 开放源代码,确保程序无后门。

最新版本 1.1-1(2008年5月9日发布) 更新

  1. 添加了窗口关闭按钮,关闭时最小化到 System Notification Area。
  2. 添加了 Status Icon 的右键菜单。
  3. 用户登录时自动强制退出在其它 IP 的连线。

中科院 IP 网关 Linux 登录客户端版本 1.0 发布

愚人节和大家开了个 小玩笑,不过这次可不是玩笑了。CAS NET 正式发布版本 1.0,官方主页:http://share.solrex.org/casnet/

CAS Net 是中科院内部 IP 控制网关的 Linux 登录客户端,此软件完全使用 Python 语言写成,同时支持命令行和图形界面,使用简单,安装方便,实乃中国科学院 Linux 使用者居家旅行必备之良品 :)。

最新版本(1.0)特性:

1. 客户端同时具有命令行和图形界面,满足不同用户需要。
2. 可设置选项多,拥有较高扩展性。
3. 可保存用户设置,登录简单快捷。
4. 纯 Python 编程,修改简单,扩展性强,可移植到不同操作系统平台。
5. 开放源代码,确保程序无后门。

软件效果截图:

Ubuntu 7.10 系统下截图
Ubuntu 7.10 系统下截图

中科院 IP 网关 Linux 登录客户端版本 1pre 发布-注意日期

更多请访问官方主页:http://share.solrex.org/casnet/

CAS Net 是中科院内部 IP 控制网关的 Linux 登录客户端,此软件完全使用 Python 语言写成,同时支持命令行和图形界面,使用简单,安装方便,实乃中国科学院 Linux 使用者居家旅行必备之良品 :)。

最新版本(1 pre)特性:

  1. 客户端同时具有命令行和图形界面,满足不同用户需要。
  2. 可设置选项多,拥有较高扩展性。
  3. 可保存用户设置,登录简单快捷。
  4. 纯 Python 编程,修改简单,可移植到不同 Linux 平台。

感谢列表:

  • giv<goldolphin[at]163.com>: 命令行客户端脚本的原型作者

版权声明:

Copyright (C) 2008 Wenbo Yang<http://solrex.org> 祝大家节日快乐!

本软件遵从 GPL 协议<http://www.gnu.org/licenses/gpl.txt>,在此协议保护之下,您可以自由地使用、修改或分发本软件。

Ubuntu 7.10 系统下截图CentOS 5.1 系统下截图