用Python做单变量数据集的异常点分析

大数据时代,数据的异常分析被广泛的用于各个场合。 今天我们就来看一看其中的一种场景,对于单变量数据集的异常检测。
所谓单变量,就是指数据集中只有一个变化的值,下面我们来看看今天我们要分析的的数据,点击这里数据文件下载数据文件。
分析数据的第一步是要加载文件, 本文使用了numpy,pandas,scikit learn等常见的数据分析要用到的Python库。
import numpy as np
import pandas as pd
df = pd.read_csv("farequote.csv")
Pandas 是一个常用的数据分析的Python库,提供对数据的加载,清洗,抽取,变形等操作。Pandas依赖numpy,numpy提供了基于列/多维数组(List/N-D Array)的数据结构的操作。许多科学计算和数据分析的库都依赖于numpy。
df 是Pandas中常用的数据类型dataframe,dataframe类似与一个数据库的表,使用 df.head()可以得到数据的头几行,以便了解数据的概貌。
该数据结构中,第一列式Pandas添加的索引,第一行是每一列数据的名字,除了第一列,每一列数据可以看成是一个变量,所以该数据集共有三个变量,时间(_time)、航空公司名称(airline)、响应时间(responsetime)。我们可以这样理解,该数据集记录了一段时间内,各个航空公司飞机延误的时间。我们希望通过分析找出是否存在异常的情况。
注意,我们是要分析单变量,所以所有的分析都是基于某一个航空公司的数据,所以就需要对该数据集做一个查询,找出要分析的航空公司。首先要知道有哪些航空公司,使用np.unique(df.airline)可以找到所有的航空公司代码,类似SQL的Unique命令
array(['AAL', 'ACA', 'AMX', 'ASA', 'AWE', 'BAW', 'DAL', 'EGF', 'FFT',
       'JAL', 'JBU', 'JZA', 'KLM', 'NKS', 'SWA', 'SWR', 'TRS', 'UAL', 'VRD'], 
      dtype='|S3')
查询某个航空公司的数据使用dataframe的query方法,类似SQL的select。Query返回的结果仍然是一个dataframe对象。
dd = df.query('airline=="KLM"') ## 得到法航的数据
我们先了解一下数据的大致信息,使用describe方法
dd.responsetime.describe()
得到如下的结果:
count    1724.000000
mean     1500.613766
std       100.085320
min      1209.766800
25%      1434.084625
50%      1499.135000
75%      1567.831025
max      1818.774100
Name: responsetime, dtype: float64
该结果返回了数据集responsetime维度上的主要统计指标,个数,均值,方差,最大最小值等等,也可以调用单独的方法例如min(),mean()等来获得某一个指标。

基于标准差得异常检测

下面我们就可以开始异常点的分析了,对于单变量的异常点分析,最容易想到的就是基于标准差(Standard Deviation)的方法了。我们假定数据的正态分布的,利用概率密度函数,我们知道
  • 95.449974面积在平均数左右两个标准差的范围内
  • 99.730020%的面积在平均数左右三个标准差的范围内
  • 99.993666的面积在平均数左右三个标准差的范围内
所以我们95%也就是大概两个标准差为门限,凡是落在门限外的都认为是异常点。代码如下
def a1(dataframe, threshold=.95):
    d = dataframe['responsetime']
    dataframe['isAnomaly'] = d > d.quantile(threshold)  
    return dataframe
print a1(dd)
运行以上程序我们得到如下结果
                             _time airline  responsetime isAnomaly
20    2013-02-01T23:57:59.000-0700     KLM     1481.4945     False
76    2013-02-01T23:52:34.000-0700     KLM     1400.9050     False
124   2013-02-01T23:47:10.000-0700     KLM     1501.4313     False
203   2013-02-01T23:39:08.000-0700     KLM     1278.9509     False
281   2013-02-01T23:32:27.000-0700     KLM     1386.4157     False
336   2013-02-01T23:26:09.000-0700     KLM     1629.9589     False
364   2013-02-01T23:23:52.000-0700     KLM     1482.5900     False
448   2013-02-01T23:16:08.000-0700     KLM     1553.4988     False
511   2013-02-01T23:10:39.000-0700     KLM     1555.1894     False
516   2013-02-01T23:10:08.000-0700     KLM     1720.7862      True
553   2013-02-01T23:06:29.000-0700     KLM     1306.6489     False
593   2013-02-01T23:03:03.000-0700     KLM     1481.7081     False
609   2013-02-01T23:01:29.000-0700     KLM     1521.0253     False
666   2013-02-01T22:56:04.000-0700     KLM     1675.2222      True
...   ...   ...   ...
结果数据集上多了一列isAnomaly用来标记每一行记录是否是异常点,我们看到已经有一些点被标记为异常点了。
我们看看程序的详细内容:
  1. 方法a1定义了一个异常检测的函数
  2. dataframe['responsetime']等价于dataframe.responsetime,该操作取出responsetime这一列的值
  3. d.quantile(threshold)用正态分布假定返回位于95%的点的值,大于该值得点都落在正态分布95%之外
  4. d > d.quantile(threshold)是一个数组操作,返回的新数组是responsetime和threshold的比较结果,[False,False,True,... ... False]
  5. 然后通过dataframe的赋值操作增加一个新的列,标记所有的异常点。
数据可视化往往是数据分析的最后一步,我们看看结果如何:
import matplotlib.pyplot as plt
da = a1(dd)
fig = plt.figure()
ax1 = fig.add_subplot(2, 1, 1)
ax2 = fig.add_subplot(2, 1, 2)
ax1.plot(da['responsetime'])
ax2.plot(da['isAnomaly'])
这异常点也太多了,用99%在试试:
现在似乎好一点,然而我们知道,对于数据集的正态分布的假定往往是不成立的,假如数据分布在大小两头,那么这样的异常检测就很难奏效了。我们看看其他一些改进的方法。

基于ZSCORE的异常检测

zscore的计算如下
sd是标准差,X是均值。一般建议门限值取为3.5
代码如下:
def a2(dataframe, threshold=3.5):
    d = dataframe['responsetime']
    zscore = (d - d.mean())/d.std()
    dataframe['isAnomaly'] = zscore.abs() > threshold
    return dataframe
另外还有一种增强的zscore算法,基于MAD。MAD的定义是
其中X是中位数。
增强的zscore算法如下:
def a3(dataframe, threshold=3.5):
    dd = dataframe['responsetime']
    MAD = (dd - dd.median()).abs().median()
    zscore = ((dd - dd.median())* 0.6475 /MAD).abs()
    dataframe['isAnomaly'] = zscore > threshold
    return dataframe
用zscore算法得到:
调整门限为3得到
如果换一组数据AAL,结果会怎么样呢?
我们发现有一段时间,所有的响应都很慢,我们想要把这些点都标记为异常,可能么?

基于KMEAN聚集的异常检测

通常基于KMEAN的聚集算法并不适用于异常点检测,以为聚集算法总是试图平衡每一个聚集中的点的数目,所以对于少数的异常点,聚集非常不好用,但是我们这个例子中,异常点都聚在一起,所以应该可以使用。
首先,为了看清聚集,我们使用时间序列的常用分析方法,增加一个维度,该维度是每一个点得前一个点得响应时间。
preresponse = 0
newcol = []
newcol.append(0)
for index, row in dd.iterrows():
    if preresponse != 0:
        newcol.append(preresponse)
    preresponse = row.responsetime
dd["t0"] = newcol
plt.scatter(dd.t0,dd.responsetime)
我们利用iterrows来循环数据,把前一个点的响应时间增加到当前点,第一个点的该值为0,命名该列为t0。然后用scatter plot把它画出来。
上面是法航KLM的数据,其中最左边的点是一个无效的点,因为前一个点的响应时间不知道所以填了0,分析时应该过滤该店。
对于AAL,我们可以清楚的看到两个聚集:
其中右上方的聚集,也就是点数目比较少得聚集就是我们希望检测到的异常点得集合。
我们看看如何使用KMEAN算法来检测吧:
def a4(dataframe, threshold = .9):
    ## add one dimention of previous response
    preresponse = 0
    newcol = []
    newcol.append(0)
    for index, row in dataframe.iterrows():
        if preresponse != 0:
            newcol.append(preresponse)
        preresponse = row.responsetime
    dataframe["t0"] = newcol
    ## remove first row as there is no previous event for time
    dd = dataframe.drop(dataframe.head(1).index) 
    clf = cluster.KMeans(n_clusters=2)
    X=np.array(dd[['responsetime','t0']])
    cls = clf.fit_predict(X)
    freq = itemfreq(cls)
    (A,B) = (freq[0,1],freq[1,1])
    t = abs(A-B)/max(A,B)
    if t > threshold :
        ## "Anomaly Detected!"
        index = freq[0,0]
        if A > B :
            index = freq[1,0]
        dd['isAnomaly'] = (cls == index)
    else :
        ## "No Anomaly Point"
        dd['isAnomaly'] = False
    return dd
其核心代码是以下这几行:
clf = cluster.KMeans(n_clusters=2)
X=np.array(dd[['responsetime','t0']])
cls = clf.fit_predict(X)
cluster.KMeans返回一个预测模型,我们假定有两个聚集。你可以试着加大聚集的数量,结果没什么影响。
dd[['responsetime','t0']]返回一个2*n的数组,并赋值给X,用于聚集计算。
fit_pridict方法是对X做聚集运算,并计算每一个点对应的聚集编号。
freq = itemfreq(cls)
itemfreq返回聚集结果中每一个聚集的发生频率,如果其中一个比另一个显著地多,我们则认为那个少得是异常点聚集。
用该方法可以把所有聚集里的点标记为异常点。
这里我用红色标记结果让大家看的清楚一点,注意因为是line chart,连个竖线间的都是异常点。

总结

除了上述的算法,还有其它一些相关的算法,大家如果对背后的数据知识有兴趣的话,可以参考这篇相关介绍
单变量的异常检测算法相对比较简单,但是要做到精准检测就更难,因为掌握的信息更少。另外boxplot也经常被用于异常检测,他和基于方差的异常检测是一致的,只不过用图形让大家一目了然的获得结果,大家有兴趣可以了解一下。

发帖者Unknown 时间: 22:05 0 评论  

这些年,我穿过的那些队服

世界杯将近,晒一下毕业后穿过球衣,缅怀一下青春。
1997~
首先映入眼帘的这件阿根廷队服,这是仍然保存的年代最久的一件球衣了,依稀是研究生时的队服,15,6年了,现在我仍然有机会就会穿上这件球衣。
2003~2007
这是在SAP B1时代的队服,英格兰。从那时起,我更多的使用14号球衣
 
这是队友同事在欧洲带的米兰队服,卡卡不错,我更喜欢巴斯滕。
2003~
 
这是我在小区俱乐部队的球衣,那时我是17号,那支球队很不错,大家都很有热情,很多队员还来我的婚礼帮忙,然而后来不知为什么,活动少了,我和球队也失去了联系。至今,我还能在qq群上看到球队的消息,队伍似乎壮大了不少,可是物是人非,当年的小伙伴们,已经没有剩下几个了。
2011~
 
2011年重归SAP,加入BOBJ足球队,我仍然是14号。那一年我们选择了曼城;那一年,我们勇夺公司联赛冠军;曼城也随后夺得英超的冠军。后来我们继续蝉联了一届冠军。可惜两次夺冠,我的贡献都不多,只有一些替补登场,只记得两次替补登场,都是在第一脚触球打入进球。后来由于关键球星的离去,球队未能继续辉煌。
大家注意,贴的字不能机洗,字会掉!
这是夺冠后的奖励
2013~
 
去年,加入了一个新的球队-上海万里老男孩足球俱乐部,有组织真好,这是球队主要赞助商林特赞助的恒大队服。我们球队的气氛非常好,我很喜欢和大家一起踢球,赞!
 
球队另一主要赞助商味它赞助的意大利队服。
同时,还联系上了大学的校友会,在我们的世界杯抽签,我们签到了伊朗,不过太忙,还么来得及和队友合练。
 
23是我和我女儿的学号 :)
容颜易老,青春永恒~

发帖者Unknown 时间: 17:32 0 评论  

使用Python抓取欧洲足球联赛数据

背景

Web Scraping

在大数据时代,一切都要用数据来说话,大数据处理的过程一般需要经过以下的几个步骤
  • 数据的采集和获取
  • 数据的清洗,抽取,变形和装载
  • 数据的分析,探索和预测
  • 数据的展现
其中首先要做的就是获取数据,并提炼出有效地数据,为下一步的分析做好准备。
数据的来源多种多样,以为我本身是足球爱好者,而世界杯就要来了,所以我就想提取欧洲联赛的数据来做一个分析。许多的网站都提供了详细的足球数据,例如:
这些网站都提供了详细的足球数据,然而为了进一步的分析,我们希望数据以格式化的形式存储,那么如何把这些网站提供的网页数据转换成格式化的数据呢?这就要用到Web scraping的技术了。简单地说,Web Scraping就是从网站抽取信息, 通常利用程序来模拟人浏览网页的过程,发送http请求,从http响应中获得结果。

Web Scraping 注意事项

在抓取数据之前,要注意以下几点:
  • 阅读网站有关数据的条款和约束条件,搞清楚数据的拥有权和使用限制
  • 友好而礼貌,使用计算机发送请求的速度飞人类阅读可比,不要发送非常密集的大量请求以免造成服务器压力过大
  • 因为网站经常会调整网页的结构,所以你之前写的Scraping代码,并不总是能够工作,可能需要经常调整
  • 因为从网站抓取的数据可能存在不一致的情况,所以很有可能需要手工调整

Python Web Scraping 相关的库

Python提供了很便利的Web Scraping基础,有很多支持的库。这里列出一小部分
当然也不一定要用Python或者不一定要自己写代码,推荐关注import.io

Web Scraping 代码

下面,我们就一步步地用Python,从腾讯体育来抓取欧洲联赛13/14赛季的数据。
首先要安装Beautifulsoup
pip install beautifulsoup4
我们先从球员的数据开始抓取。
球员数据的Web请求是http://soccerdata.sports.qq.com/playerSearch.aspx?lega=epl&pn=2 ,返回的内容如下图所示:
该web服务有两个参数,lega表示是哪一个联赛,pn表示的是分页的页数。
首先我们先做一些初始化的准备工作
from urllib2 import urlopen
import urlparse
import bs4

BASE_URL = "http://soccerdata.sports.qq.com"
PLAYER_LIST_QUERY = "/playerSearch.aspx?lega=%s&pn=%d"
league = ['epl','seri','bund','liga','fran','scot','holl','belg']
page_number_limit = 100
player_fields = ['league_cn','img','name_cn','name','team','age','position_cn','nation','birth','query','id','teamid','league']
urlopen,urlparse,bs4是我们将要使用的Python库。
BASE_URL,PLAYER_LIST_QUERY,league,page_number_limit和player_fields是我们会用到的一些常量。
下面是抓取球员数据的具体代码:
def get_players(baseurl):
    html = urlopen(baseurl).read()
    soup = bs4.BeautifulSoup(html, "lxml")
    players = [ dd for dd in soup.select('.searchResult tr') if dd.contents[1].name != 'th']
    result = []
    for player in players:
        record = []
        link = ''
        query = []
        for item in player.contents:
            if type(item) is bs4.element.Tag:
                if not item.string and item.img:
                    record.append(item.img['src'])
                else :
                    record.append(item.string and item.string.strip() or 'na')
                try:
                    o = urlparse.urlparse(item.a['href']).query
                    if len(link) == 0:
                        link = o
                        query = dict([(k,v[0]) for k,v in urlparse.parse_qs(o).items()])
                except:
                    pass
             
        if len(record) != 10:
            for i in range(0, 10 - len(record)):
                record.append('na')
        record.append(unicode(link,'utf-8'))
        record.append(unicode(query["id"],'utf-8'))
        record.append(unicode(query["teamid"],'utf-8'))
        record.append(unicode(query["lega"],'utf-8'))
        result.append(record)
    return result
    
result = []
for url in [ BASE_URL + PLAYER_LIST_QUERY % (l,n) for l in league for n in range(page_number_limit) ]:
    result = result +  get_players(url)
我们来看看抓取球员数据的详细过程:
首先我们定义了一个get_players方法,该方法会返回某一请求页面上所有球员的数据。为了得到所有的数据,我们通过一个for循环,因为要循环各个联赛,每个联赛又有多个分页,一般情况下是需要一个双重循环的:
for i in league:
    for j in range(0, 100):
        url = BASE_URL + PLAYER_LIST_QUERY % (l,n)
        ## send request to url and do scraping
Python的list comprehension可以很方便的通过构造一个列表的方式来减少循环的层次。
另外Python还有一个很方便的语法来合并连个列表: list = list1 + list2
好我们再看看如何使用BeautifulSoup来抓取网页中我们需要的内容。
首先调用urlopen读取对应url的内容,通常是一个html,用该html构造一个beautifulsoup对象。
beautifulsoup对象支持很多查找功能,也支持类似css的selector。通常如果有一个DOM对象是<xx class='cc'>,我们使用以下方式来查找:
obj = soup.find("xx","cc")
另外一种常见的方式就是通过CSS的selector方式,在上述代码中,我们选择class=searchResult元素里面,所有的tr元素,过滤掉th也就是表头元素。
for dd in soup.select('.searchResult tr') if dd.contents[1].name != 'th'
对于每一行记录tr,生成一条球员记录,并存放在一个列表中。所以我们就循环tr的内容tr.contents,获得对应的field内容。
对于每一个tr的content,我们先检查其类型是不是一个Tag,对于Tag类型有几种情况,一种是包含img的情况,我们需要取出球员的头像图片的网址。
另一种是包含了一个链接,指向其他数据内容
所以在代码中要分别处理这些不同的情况。
对于一个Tag对象,Tag.x可以获得他的子对象,Tag['x']可以获得Tag的attribute的值。
所以用item.img['src']可以获得item的子元素img的src属性。
对已包含链接的情况,我们通过urlparse来获取查询url中的参数。这里我们利用了dict comprehension的把查询参数放入一个dict中,然后添加到列表中。
dict([(k,v[0]) for k,v in urlparse.parse_qs(o).items()])
对于其它情况,我们使用Python 的and or表达式以确保当Tag的内容为空时,我们写入‘na’,该表达式类似C/C++或Java中的三元操作符 X ? A : B
然后有一段代码判断当前记录的长度是否大于10,不大于10则用空值填充,目的是避免一些不一致的地方。
if len(record) != 10:
    for i in range(0, 10 - len(record)):
        record.append('na')
最后,我们把query中的一些相关的参数如球员的id,球队的id,所在的联赛代码等加入到列表。
record.append(unicode(link,'utf-8'))
record.append(unicode(query["id"],'utf-8'))
record.append(unicode(query["teamid"],'utf-8'))
record.append(unicode(query["lega"],'utf-8'))
最后我们把本页面所有球员的列表放入一个列表返回。
好了,现在我们拥有了一个包含所有球员的信息的列表,我们需要把它存下来,以进一步的处理,分析。通常,csv格式是一个常见的选择。
import csv
def write_csv(filename, content, header = None): 
    file = open(filename, "wb")
    file.write('\xEF\xBB\xBF')
    writer = csv.writer(file, delimiter=',')
    if header:
        writer.writerow(header)
    for row in content:
        encoderow = [dd.encode('utf8') for dd in row]
        writer.writerow(encoderow)

write_csv('players.csv',result,player_fields)
这里需要注意的就是关于encode的问题。因为我们使用的时utf-8的编码方式,在csv的文件头,需要写入\xEF\xBB\xBF,详见这篇文章
好了现在大功告成,抓取的csv如下图:
因为之前我们还抓取了球员本赛季的比赛详情,所以我们可以进一步的抓取所有球员每一场比赛的记录
抓取的代码如下
def get_player_match(url):
    html = urlopen(url).read()
    soup = bs4.BeautifulSoup(html, "lxml")
    matches = [ dd for dd in soup.select('.shtdm tr') if dd.contents[1].name != 'th']
    records = []
    for item in [ dd for dd in matches if len(dd.contents) > 11]: ## filter out the personal part
        record = []
        for match in [ dd for dd in item.contents if type(dd) is bs4.element.Tag]:
            if match.string:
                record.append(match.string)
            else:
                for d in [ dd for dd in match.contents if type(dd) is bs4.element.Tag]:
                    query = dict([(k,v[0]) for k,v in urlparse.parse_qs(d['href']).items()])
                    record.append('teamid' in query and query['teamid'] or query['id'])   
                    record.append(d.string and d.string or 'na')                    
        records.append(record)
    return records[1:]  ##remove the first record as the header

def get_players_match(playerlist, baseurl = BASE_URL + '/player.aspx?'):
    result = []
    for item in playerlist:
        url =  baseurl + item[10]
        print url
        result = result + get_player_match(url)
    return result
match_fields = ['date_cn','homeid','homename_cn','matchid','score','awayid','awayname_cn','league_cn','firstteam','playtime','goal','assist','shoot','run','corner','offside','foul','violation','yellowcard','redcard','save']    
write_csv('m.csv',get_players_match(result),match_fields)
抓取的过程和之前类似。

下一步做什么

现在我们拥有了详细的欧洲联赛的数据,那么下一步要怎么做呢,我推荐大家把数据导入BI工具来做进一步的分析。有两个比较好的选择:
Tableau在数据可视化领域可谓无出其右,Tableau Public完全免费,用数据可视化来驱动数据的探索和分析,拥有非常好的用户体验
Splunk提供一个大数据的平台,主要面向机器数据。支持每天免费导入500M的数据,如果是个人学习,应该足够了。
当然你也可以用Excel。 另外大家如果有什么好的免费的数据分析的平台,欢迎交流。

发帖者Unknown 时间: 17:30 0 评论  

探索Javascript异步编程

笔者在之前的一片博客中简单的讨论了Python和Javascript的异同,其实作为一种编程语言Javascript的异步编程是一个非常值得讨论的有趣话题。

JavaScript 异步编程简介

回调函数和异步执行

所谓的异步指的是函数的调用并不直接返回执行的结果,而往往是通过回调函数异步的执行。
我们先看看回调函数是什么:
var fn = function(callback) {
    // do something here
    ...
    callback.apply(this, para);
};

var mycallback = function(parameter) {
    // do someting in customer callback
};

// call the fn with callback as parameter
fn(mycallback);
回调函数,其实就是调用用户提供的函数,该函数往往是以参数的形式提供的。回调函数并不一定是异步执行的。比如上述的例子中,回调函数是被同步执行的。大部分语言都支持回调,C++可用通过函数指针或者回调对象,Java一般也是使用回调对象。
在Javascript中有很多通过回调函数来执行的异步调用,例如setTimeout()或者setInterval()。
setTimeout(function(){
    console.log("this will be exectued after 1 second!");
},1000);
在以上的例子中,setTimeout直接返回,匿名函数会在1000毫秒(不一定能保证是1000毫秒)后异步触发并执行,完成打印控制台的操作。也就是说在异步操作的情境下,函数直接返回,把控制权交给回调函数,回调函数会在以后的某一个时间片被调度执行。那么为什么需要异步呢?为什么不能直接在当前函数中完成操作呢?这就需要了解Javascript的线程模型了。

Javascript线程模型和事件驱动

Javascript最初是被设计成在浏览器中辅助提供HTML的交互功能。在浏览器中都包含一个Javascript引擎,Javscript程序就运行在这个引擎之中,并且只有一个线程。单线程能都带来很多优点,程序员们可以很开心的不用去考虑诸如资源同步,死锁等多线程阻塞式编程所需要面对的恼人的问题。但是很多人会问,既然Javascript是单线程的,那它又如何能够异步的执行呢?
这就需要了解到Javascript在浏览器中的事件驱动(event driven)机制。事件驱动一般通过事件循环(event loop)和事件队列(event queue)来实现的。假定浏览器中有一个专门用于事件调度的实例(该实例可以是一个线程,我们可以称之为事件分发线程event dispatch thread),该实例的工作就是一个不结束的循环,从事件队列中取出事件,处理所有很事件关联的回调函数(event handler)。注意回调函数是在Javascript的主线程中运行的,而非事件分发线程中,以保证事件处理不会发生阻塞。
Event Loop Code:
while(true) {
 var event = eventQueue.pop();
 if(event && event.handler) {
     event.handler.execute(); // execute the callback in Javascript thread
 } else {
     sleep(); //sleep some time to release the CPU do other stuff
 }
}
通过事件驱动机制,我们可以想象Javascript的编程模型就是响应一系列的事件,执行对应的回调函数。很多UI框架都采用这样的模型(例如Java Swing)。
那为什要异步呢,同步不是很好么?
异步的主要目的是处理非阻塞,在和HTML交互的过程中,会需要一些IO操作(典型的就是Ajax请求,脚本文件加载),如果这些操作是同步的,就会阻塞其它操作,用户的体验就是页面失去了响应。
综上所述Javascript通过事件驱动机制,在单线程模型下,以异步回调函数的形式来实现非阻塞的IO操作。

Javascript异步编程带来的挑战

Javascript的单线程模型有很多好处,但同时也带来了很多挑战。
代码可读性
想象一下,如果某个操作需要经过多个非阻塞的IO操作,每一个结果都是通过回调,程序有可能会看上去像这个样子。
operation1(function(err, result) {
    operation2(function(err, result) {
        operation3(function(err, result) {
            operation4(function(err, result) {
                operation5(function(err, result) {
                    // do something useful
                })
            })
        })
    })
})
我们称之为意大利面条式(spaghetti)的代码。这样的代码很难维护。这样的情况更多的会发生在server side的情况下。
流程控制
异步带来的另一个问题是流程控制,举个例子,我要访问三个网站的内容,当三个网站的内容都得到后,合并处理,然后发给后台。代码可以这样写:
var urls = ['url1','url2','url3'];
var result = [];

for (var i = 0, len = urls.length(); i < len; i++ ) {
    $.ajax({
        url: urls[i],
        context: document.body,
        success: function(){
          //do something on success
          result.push("one of the request done successfully");
          if (result.length === urls.length()) {
              //do something when all the request is completed successfully
          }
        }});
}
上述代码通过检查result的长度的方式来决定是否所有的请求都处理完成,这是一个很丑陋方法,也很不可靠。
异常和错误处理
通过上一个例子,我们还可以看出,为了使程序更健壮,我们还需要加入异常处理。 在异步的方式下,异常处理分布在不同的回调函数中,我们无法在调用的时候通过try...catch的方式来处理异常, 所以很难做到有效,清楚。

更好的Javascript异步编程方式

“这是最好的时代,也是最糟糕的时代”
为了解决Javascript异步编程带来的问题,很多的开发者做出了不同程度的努力,提供了很多不同的解决方案。然而面对如此众多的方案应该如何选择呢?我们这就来看看都有哪些可供选择的方案吧。

Promise

Promise 对象曾经以多种形式存在于很多语言中。这个词最先由C++工程师用在Xanadu 项目中,Xanadu 项目是Web 应用项目的先驱。随后Promise 被用在E编程语言中,这又激发了Python 开发人员的灵感,将它实现成了Twisted 框架的Deferred 对象。
2007 年,Promise 赶上了JavaScript 大潮,那时Dojo 框架刚从Twisted框架汲取灵感,新增了一个叫做dojo.Deferred 的对象。也就在那个时候,相对成熟的Dojo 框架与初出茅庐的jQuery 框架激烈地争夺着人气和名望。2009 年,Kris Zyp 有感于dojo.Deferred 的影响力提出了CommonJS 之Promises/A 规范。同年,Node.js 首次亮相。
在编程的概念中,future,promise,和delay表示同一个概念。Promise翻译成中文是“承诺”,也就是说给你一个东西,我保证未来能够做到,但现在什么都没有。它用来表示异步操作返回的一个对象,该对象是用来获取未来的执行结果的一个代理,初始值不确定。许多语言都有对Promise的支持。
Promise的核心是它的then方法,我们可以使用这个方法从异步操作中得到返回值,或者是异常。then有两个可选参数(有的实现是三个),分别处理成功和失败的情景。
var promise = doSomethingAync()
promise.then(onFulfilled, onRejected)
异步调用doSomethingAync返回一个Promise对象promise,调用promise的then方法来处理成功和失败。这看上去似乎并没有很大的改进。仍然需要回调。但是和以前的区别在于,首先异步操作有了返回值,虽然该值只是一个对未来的承诺;其次通过使用then,程序员可以有效的控制流程异常处理,决定如何使用这个来自未来的值。
对于嵌套的异步操作,有了Promise的支持,可以写成这样的链式操作:
operation1().then(function (result1) {
    return operation2(result1)
}).then(function (result2) {
    return operation3(result2);
}).then(function (result3) {
    return operation4(result3);
}).then(function (result4) {
    return operation5(result4)
}).then(function (result5) {
    //And so on
});
Promise提供更便捷的流程控制,例如Promise.all()可以解决需要并发的执行若干个异步操作,等所有操作完成后进行处理。
var p1 = async1();
var p2 = async2();
var p3 = async3();
Promise.all([p1,p2,p3]).then(function(){
    // do something when all three asychronized operation finished
});
对于异常处理,
doA()
  .then(doB)
  .then(null,function(error){
      // error handling here
  })
如果doA失败,它的Promise会被拒绝,处理链上的下一个onRejected会被调用,在这个例子中就是匿名函数function(error){}。比起原始的回调方式,不需要在每一步都对异常进行处理。这生了不少事。
以上只是对于Promise概念的简单陈述,Promise拥有许多不同规范建议(A,A+,B,KISS,C,D等),名字(Future,Promise,Defer),和开源实现。大家可以参考一下的这些链接。

如果你有选择困难综合症,面对这么多的开源库不知道如何决断,先不要急,这还只是一部分,还有一些库没有或者不完全采用Promise的概念

Non-Promise

下面列出了其它的一些开源的库,也可以帮助解决Javascript中异步编程所遇到的诸多问题,它们的解决方案各不相同,我这里就不一一介绍了。大家有兴趣可以去看看或者试用一下。

Non-3rd Party

其实,为了解决Javascript异步编程带来的问题,不一定非要使用Promise或者其它的开源库,这些库提供了很好的模式,但是你也可以通过有针对性的设计来解决。
比如,对于层层回调的模式,可以利用消息机制来改写,假定你的系统中已经实现了消息机制,你的code可以写成这样:
eventbus.on("init", function(){
    operationA(function(err,result){
        eventbus.dispatch("ACompleted");
    });
});

eventbus.on("ACompleted", function(){
    operationB(function(err,result){
        eventbus.dispatch("BCompleted");
    });
});

eventbus.on("BCompleted", function(){
    operationC(function(err,result){
        eventbus.dispatch("CCompleted");
    });
});

eventbus.on("CCompleted", function(){
    // do something when all operation completed
});
这样我们就把嵌套的异步调用,改写成了顺序执行的事件处理。
更多的方式,请大家参考这篇文章,它提出了解决异步的五种模式:回调、观察者模式(事件)、消息、Promise和有限状态机(FSM)。

下一代Javscript对异步编程的增强

ECMAScript6

下一代的Javascript标准Harmony,也就是ECMAScript6正在酝酿中,它提出了许多新的语言特性,比如箭头函数、类(Class)、生成器(Generator)、Promise等等。其中Generator和Promise都可以被用于对异步调用的增强。
Nodejs的开发版V0.11已经可以支持ES6的一些新的特性,使用node --harmony命令来运行对ES6的支持。

co、Thunk、Koa

koa是由Express原班人马(主要是TJ)打造,希望提供一个更精简健壮的nodejs框架。koa依赖ES6中的Generator等新特性,所以必须运行在相应的Nodejs版本上。
利用Generator、coThunk,可以在Koa中有效的解决Javascript异步调用的各种问题。
co是一个异步流程简化的工具,它利用Generator把一层层嵌套的调用变成同步的写法。
var co = require('co');
var fs = require('fs');

var stat = function(path) {
  return function(cb){
    fs.stat(path,cb);
  }
};

var readFile = function(filename) {
  return function(cb){
    fs.readFile(filename,cb);
  }
};

co(function *() {
  var stat = yield stat('./README.md');
  var content = yield readFile('./README.md');
})();
通过co可以把异步的fs.readFile当成同步一样调用,只需要把异步函数fs.readFile用闭包的方式封装。
利用Thunk可以进一步简化为如下的code, 这里Thunk的作用就是用闭包封装异步函数,返回一个生成函数的函数,供生成器来调用。
var thunkify = require('thunkify');
var co = require('co');
var fs = require('fs');

var stat = thunkify(fs.stat);
var readFile = thunkify(fs.readFile);

co(function *() {
  var stat = yield stat('./README.md');
  var content = yield readFile('./README.md');
})();
利用co可以串行或者并行的执行异步调用。
串行
co(function *() {
  var a = yield request(a);
  var b = yield request(b);
})();
并行
co(function *() {
 var res = yield [request(a), request(b)];
})();
更多详细的内容,大家可以参考这两篇文章12

总结

异步编程带来的问题在客户端Javascript中并不明显,但随着服务器端Javascript越来越广的被使用,大量的异步IO操作使得该问题变得明显。许多不同的方法都可以解决这个问题,本文讨论了一些方法,但并不深入。大家需要根据自己的情况选择一个适于自己的方法。
同时,随着ES6的定义,Javascript的语法变得越来越丰富,更多的功能带来了很多便利,然而原本简洁,单一目的的Javascript变得复杂,也要承担更多的任务。Javascript何去何从,让我们拭目以待。

发帖者Unknown 时间: 17:28 0 评论  

Python 与 Javascript 之比较

最近由于工作的需要开始开发一些Python的东西,由于之前一直在使用Javascript,所以会不自觉的使用一些Javascript的概念,语法什么的,经常掉到坑里。我觉得对于从Javascript转到Python,有必要总结一下它们之间的差异。

基本概念

PythonJavascript都是脚本语言,所以它们有很多共同的特性,都需要解释器来运行,都是动态类型,都支持自动内存管理,都可以调用eval()来执行脚本等等脚本语言所共有的特性。

然而它们也有很大的区别,Javascript这设计之初是一种客户端的脚本语言,主要应用于浏览器,它的语法主要借鉴了C,而Python由于其“优雅”,“明确”,“简单”的设计而广受欢迎,被应用于教育,科学计算,web开发等不同的场景中。

编程范式

Python和Javascript都支持多种不同的编程范式,在面向对象的编程上面,它们有很大的区别。Javascript的面向对象是基于原型(prototype)的, 对象的继承是由原型(也是对象)创建出来的,由原型对象创建出来的对象继承了原型链上的方法。而Python则是中规中矩的基于类(class)的继承,并天然的支持多态(polymophine)。

OO in Pyhton 

class Employee:
   'Common base class for all employees'
   empCount = 0 ##类成员

   def __init__(self, name, salary):
      self.name = name
      self.salary = salary
      Employee.empCount += 1
   
   def displayCount(self):
     print "Total Employee %d" % Employee.empCount

   def displayEmployee(self):
      print "Name : ", self.name,  ", Salary: ", self.salary
## 创建实例
ea = Employee("a",1000)
eb = Employee("b",2000)

OO in Javascript

var empCount = 0;
//构造函数
function Employee(name, salary){
    this.name = name;
    this.salary = salary;   
    this.empCount += 1;
}

Employee.prototype.displayCount = function(){
    console.log("Total Employee " + empCount );
}

Employee.prototype.displayEmployee = function(){
    console.log("Name " + this.name + ", Salary " + this.salary );
}
//创建实例
var ea = new Employee("a",1000);
var eb = new Employee("b",2000);

因为是基于对象的继承,在Javascript中,我们没有办法使用类成员empCount,只好声明了一个全局变量,当然实际开发中我们会用更合适的scope。注意Javascript创建对象需要使用new关键字,而Python不需要。

除了原生的基于原型的继承,还有很多利用闭包或者原型来模拟类继承的Javascript OO工具,因为不是语言本身的属性,我们就不讨论了。

线程模型

在Javascript的世界中是没有多线程的概念的,并发使用过使用事件驱动的方式来进行的, 所有的JavaScript程序都运行在一个线程中。在HTML5中引入web worker可以并发的处理任务,但没有改变Javascript单线程的限制。

Python通过thread包支持多线程。

不可改变类型 (immutable type)

在Python中,有的数据类型是不可改变的,也就意味着这种类型的数据不能被修改,所有的修改都会返回新的对象。而在Javascript中所有的数据类型都是可以改变的。Python引入不可改变类型我认为是为了支持线程安全,而因为Javascript是单线程模型,所以没有必要引入不可改变类型。

当然在Javascript可以定义一个对象的属性为只读。

var obj = {};Object.defineProperty(obj, "prop", {
    value: "test",
    writable: false});

在ECMAScript5的支持中,也可以调用Object的freeze方法来是对象变得不可修改。

Object.freeze(obj)


数据类型

Javascript的数据类型比较简单,有object、string、boolean、number、null和undefined,总共六种

Python中一切均为对象,像module、function、class等等都是。

Python有五个内置的简单数据类型bool、int、long、float和complex,另外还有容器类型,代码类型,内部类型等等。

布尔

Javascript有true和false。Python有True和False。它们除了大小写没有什么区别。

字符串

Javascript采用UTF16编码。

Python使用ASCII码。需要调用encode、decode来进行编码转换。使用u作为前缀可以指定字符串使用Unicode编码。

数值

Javascript中所有的数值类型都是实现为64位浮点数。支持NaN(Not a number),正负无穷大(+/-Infiity)。

Python拥有诸多的数值类型,其中的复数类型非常方便,所以在Python在科研和教育领域很受欢迎。这应该也是其中一个原因吧。Python中没有定义NaN,除零操作会引发异常。

列表

Javascript内置了array类型(array也是object)

Python的列表(List)和Javascript的Array比较接近,而元组(Tuple)可以理解为不可改变的列表。

除了求长度在Python中是使用内置方法len外,基本上Javascript和Python都提供了类似的方法来操作列表。Python中对列表下标的操作非常灵活也非常方便,这是Javascript所没有的。例如l[5:-1],l[:6]等等。

字典、哈希表、对象

Javascript中大量的使用{}来创建对象,这些对象和字典没有什么区别,可以使用[]或者.来访问对象的成员。可以动态的添加,修改和删除成员。可以认为对象就是Javascript的字典或者哈希表。对象的key必须是字符串。

Python内置了哈希表(dictS),和Javascript不同的是,dictS可以有各种类型的key值。

空值

Javascript定义了两种空值。 undefined表示变量没有被初始化,null表示变量已经初始化但是值为空。

Python中不存在未初始化的值,如果一个变量值为空,Python使用None来表示。

Javascript中变量的声明和初始化

v1;
v2 = null;
var v3;
var v4 = null;
var v5 = 'something';

在如上的代码中v1是全局变量,未初始化,值为undefined;v2是全局变量,初始化为空值;v3为局部未初始化变量,v4是局部初始化为空值的变量;v5是局部已初始化为一个字符处的变量。

Python中变量的声明和初始化

v1 = None
v2 = 'someting'

Python中的变量声明和初始化就简单了许多。当在Python中访问一个不存在的变量时,会抛出NameError的异常。当访问对象或者字典的值不存在的时候,会抛出AttributeError或者KeyError。因此判断一个值是否存在在Javascript和Python中需要不一样的方式。

Javascript中检查某变量的存在性:

if (!v ) {
    // do something if v does not exist or is null or is false
}

if (v === undefined) {
    // do something if v does not initialized
}

注意使用!v来检查v是否初始化是有歧义的因为有许多种情况!v都会返回true

Python中检查某变量的存在性:

try:
    v
except NameError
    ## do something if v does not exist

在Python中也可以通过检查变量是不是存在于局部locals()或者全局globals()来判断是否存在该变量。

类型检查

Javascript可以通过typeof来获得某个变量的类型:

typeof in Javascript 的例子:

typeof 3 // "number"
typeof "abc" // "string"
typeof {} // "object"
typeof true // "boolean"
typeof undefined // "undefined"
typeof function(){} // "function"
typeof [] // "object"
typeof null // "object"

要非常小心的使用typeof,从上面的例子你可以看到,typeof null居然是object。因为javscript的弱类型特性,想要获得更实际的类型,还需要结合使用instanceof,constructor等概念。具体请参考这篇文章

Python提供内置方法type来获得数据的类型。

>>> type([]) is list
True
>>> type({}) is dict
True
>>> type('') is str
True
>>> type(0) is int
True

同时也可以通过isinstance()来判断类的类型

class A:
    pass
class B(A):
    pass
isinstance(A(), A)  # returns True
type(A()) == A      # returns True
isinstance(B(), A)    # returns True
type(B()) == A        # returns False

但是注意Python的class style发生过一次变化,不是每个版本的Python运行上述代码的行为都一样,在old style中,所有的实例的type都是‘instance’,所以用type方法来检查也不是一个好的方法。这一点和Javascript很类似。

自动类型转换

当操作不同类型一起进行运算的时候,Javascript总是尽可能的进行自动的类型转换,这很方便,当然也很容易出错。尤其是在进行数值和字符串操作的时候,一不小心就会出错。我以前经常会计算SVG中的各种数值属性,诸如x,y坐标之类的,当你一不小心把一个字符串加到数值上的时候,Javascript会自动转换出一个数值,往往是NaN,这样SVG就完全画不出来啦,因为自动转化是合法的,找到出错的地方也非常困难。

Python在这一点上就非常的谨慎,一般不会在不同的类型之间做自动的转换。

语法

风格

Python使用缩进来决定逻辑行的结束非常具有创造性,这也许是Python最独特的属性了,当然也有人对此颇具微词,尤其是需要修改重构代码的时候,修改缩进往往会引起不小的麻烦。

Javascript虽然名字里有Java,它的风格也有那么一点像Java,可是它和Java就好比雷峰塔和雷锋一样,真的没有半毛钱的关系。到时语法上和C比较类似。这里必须要提到的是coffeescript作为构建与Javascript之上的一种语言,采用了类似Python的语法风格,也是用缩进来决定逻辑行。

Python风格

def func(list):
    for i in range(0,len(list)):
        print list[i]

Javascript风格

function funcs(list) {
    for(var i=0, len = list.length(); i < len; i++) {
        console.log(list[i]);
    }
}

从以上的两个代码的例子可以看出,Python确实非常简洁。

作用范围和包管理

Javascript的作用域是由方法function来定义的,也就是说同一个方法内部拥有相同的作用域。这个严重区别与C语言使用{}来定义的作用域。Closure是Javascript最有用的一个特性。

Python的作用域是由module,function,class来定义的。

Python的import可以很好的管理依赖和作用域,而Javascript没有原生的包管理机制,需要借助AMD来异步的加载依赖的js文件,requirejs是一个常用的工具。

赋值逻辑操作符

Javascript使用=赋值,拥有判断相等(==)和全等(===)两种相等的判断。其它的逻辑运算符有&& 和||,和C语言类似。

Python中没有全等,或和与使用的时and 和 or,更接近自然语言。Python中没有三元运算符 A :B ?C,通常的写法是

(A and B) or C

因为这样写有一定的缺陷,也可以写作

 B if A else C

Python对赋值操作的一个重要的改进是不允许赋值操作返回赋值的结果,这样做的好处是避免出现在应该使用相等判断的时候错误的使用了赋值操作。因为这两个操作符实在太像了,而且从自然语言上来说它们也没有区别。

++运算符

Python不支持++运算符,没错你再也不需要根据++符号在变量的左右位置来思考到底是先加一再赋值呢还是先赋值再加一。

连续赋值

利用元组(tuple),Python可以一次性的给多个变量赋值

(MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY) = range(7)

函数参数

Python的函数参数支持命名参数和可选参数(提供默认值),使用起来很方便,Javascript不支持可选参数和默认值(可以通过对arguments的解析来支持)

def info(object, spacing=10, collapse=1):
    ... ...

其它

立即调用函数表达式 (IIFE

Javascript的一个方便的特性是可以立即调用一个刚刚声明的匿名函数。也有人称之为自调用匿名函数。

下面的代码是一个module模式的例子,使用闭包来保存状态实现良好的封装。这样的代码可以用在无需重用的场合。

var counter = (function(){
    var i = 0;
    return {
        get: function(){
            return i;
            },
        set: function( val ){
            i = val;
            },
        increment: function() {
            return ++i;
            }
        };
    }());

Python没有相应的支持。

生成器和迭代器(Generators & Iterator)

在我接触到的Python代码中,大量的使用这样的生成器的模式。

Python生成器的例子

# a generator that yields items instead of returning a list
def firstn(n):
    num = 0
    while num < n:
        yield num
        num += 1
  
sum_of_first_n = sum(firstn(1000000))

Javascript1.7中引入了一些列的新特性,其中就包括生成器和迭代器。然而大部分的浏览器除了Mozilla(Mozilla基本上是在自己玩,下一代的Javascript标准应该是ECMAScript5)都不支持这些特性

Javascript1.7 迭代器和生成器的例子。

function fib() {
  var i = 0, j = 1;
  while (true) {
    yield i;
    var t = i;
    i = j;
    j += t;
  }
};

var g = fib();
for (var i = 0; i < 10; i++) {
  console.log(g.next());
}

列表(字典、集合)映射表达式 (List、Dict、Set Comprehension)

Python的映射表达式可以非常方便的帮助用户构造列表、字典、集合等内置数据类型。

下面是列表映射表达式使用的例子:

>>> [x + 3 for x in range(4)]
[3, 4, 5, 6]
>>> {x + 3 for x in range(4)}
{3, 4, 5, 6}
>>> {x: x + 3 for x in range(4)}
{0: 3, 1: 4, 2: 5, 3: 6}

Javascript1.7开始也引入了Array Comprehension

var numbers = [1, 2, 3, 4];
var doubled = [i * 2 for (i of numbers)];

Lamda表达式 (Lamda Expression )

Lamda表达式是一种匿名函数,基于著名的λ演算。许多语言诸如C#,Java都提供了对lamda的支持。Pyhton就是其中之一。Javascript没有提供原生的Lamda支持。但是有第三方的Lamda包。

g = lambda x : x*3

装饰器(Decorators)

Decorator是一种设计模式,大部分语言都可以支持这样的模式,Python提供了原生的对该模式的支持,算是一种对程序员的便利把。

Decorator的用法如下。

@classmethod
def foo (arg1, arg2):
    ....

更多decorator的内容,请参考https://wiki.python.org/moin/PythonDecorators

本人对Javascript和Python的认识有限,欢迎大家提出宝贵意见。


发帖者Unknown 时间: 20:30 0 评论