转 决策树

From: http://blog.csdn.net/heiyeshuwu/article/details/46991273

[原]【原创】机器学习算法之:决策树
Posted: July 21st, 2015, 8:12pm CEST

机器学习算法之:决策树

作者:jmz (360电商技术)

1 概览
决策树学习是一种逼近离散值目标函数的方法,在这种方法中学习到的函数被表示为一棵决策树:

1) 学习析取表达式,能再被表示为多个if-then的规则,以提高可读性。

2) 对噪声数据有很好的适应性。–统计特性

3) 决策树学习方法搜索完整表示的假设空间(一个有效的观点是机器学习问题经常归结于搜索问题,即对非常大的假设空间进行搜索、已确定最佳拟合到观察到的数据),从而避免了受限假设空间的不足。决策树学习的归纳偏置(有兴趣参考“归纳偏置”,“奥坎姆剃刀问题”相关问题更详细的描述)是优先选择较小的树。

2 决策树表示
决策树通过把实例从根结点排列(如何选择排列次序是决策树算法的核心)到某个叶子结点来分类实例,叶子结点即为实例所属的分类。树上的每一个结点指定了对实例的某个属性的测试,并且该结点的每一个后继分支对应于该属性的一个可能值。

如,一颗根据天气情况判断是否参加打网球的决策树如下:

0

例如,下面的实例:将被沿着这棵决策树的最左分支向下排列,因而被评定为反例(也就是这棵树预测PlayTennis=No) 。

通常决策树代表实例属性值约束的合取(conjunction)的析取式(disjunction)。从树根到树叶的每一条路径对应一组属性测试的合取,树本身对应这些合取的析取,该决策树对应于以下表达式:

(Outlook=Sunny ٨Humidity=Normal)

٧(Outlook=Overcast)

٧(Outlook=Rain٨ Wind=Weak)

3 适用场景
不同的决策树学习算法可能有这样或那样的不太一致的能力和要求,但根据以上分析描述不难看出、决策树学习最适合具有以下特征的问题:

1) 实例是由“属性-值”对(pair)表示的。最简单的决策树学习中,每一个属性取少数的分离的值(例如,Hot、Mild、Cold)。

2) 目标函数具有离散的输出值。

3) 可能需要析取的描述,如上面指出的,决策树很自然地代表了析取表达式。

4) 训练数据可以包含错误。决策树学习对错误有很好的适应性,无论是训练样例所属的分类错误还是描述这些样例的属性值错误。

5) 训练数据可以包含缺少属性值的实例。

已经发现很多实际的问题符合这些特征,所以决策树学习已经被应用到很多问题中。例如根据疾病分类患者;根据起因分类设备故障;根据拖欠支付的可能性分类贷款申请。对于这些问题,核心任务都是要把样例分类到各可能的离散值对应的类别中,因此经常被称为分类问题

4 基本的决策树学习算法
大多数决策树学习算法是一种核心算法的变体、该算法采用自顶向下的贪婪搜索遍历可能的决策树空间。决策树构造过程是从“哪一个属性将在树的根结点被测试?”这个问题开始的。

1) 使用统计测试来确定每分类能力最好的属性被选作树的根结点的测试。

2) 为根结点属性的每个可能值产生一个分支,并把训练样例排列到适当的分支(也就是,样例的该属性值对应的分支)之下。

3) 重复整个过程,用每个分支结点关联的训练样例来选取在该点被测试的最佳属性。

这形成了对决策树的贪婪搜索,也就是算法从不回溯重新考虑以前的选择。下图描述了该算法的一个简化版本:

1

1. 哪个属性是最佳的分类属性?
ID3 算法的核心问题是选取在树的每个结点要测试的属性,我们希望选择的是最有助于分类实例的属性。

1) 用熵度量样例的纯度

为了精确地定义信息增益,我们先定义信息论中广泛使用的一个度量标准,称为熵(entropy),它刻画了任意样例集的纯度(purity)。给定包含关于某个目标概念的正反样例的样例集S,那么S 相对这个布尔型分类的熵为:

Entropy(S) = -p⊕log2p⊕ -pΘlog2pΘ

其中p⊕是在S 中正例的比例,pΘ是在S 中负例的比例。在有关熵的所有计算中我们定义0log0 为0。

举例说明,假设S 是一个关于某布尔概念的有14 个样例的集合,它包括9 个正例和5 个反例(我们采用记号[9+,5-]来概括这样的数据样例)。那么S 相对于这个布尔分类的熵(Entropy)为:

Entropy ([9+, 5−]) = −(9 / 14) log 2 (9 /14) − (5 / 14) log 2(5 / 14) =0.940

注意,如果S 的所有成员属于同一类,那么S 的熵为0。例如,如果所有的成员是正的 ( p⊕=1 ) , 那么 pΘ 就是 0 , 于是 Entropy(S) =− 1 ⋅ log ( 1 ) − (0) ⋅log (0) = −1 ⋅ 0 − 0 ⋅ log 0 = 0 。

另外,当集合中正反样例的数量相等时熵为1。如果集合中正反例的数量不等时,熵介于0 和1 之间。下图显示了关于某布尔分类的熵函数随着p⊕从0 到1 变化的曲线。
2

信息论中熵的一种解释是,熵确定了要编码集合S 中任意成员(即以均匀的概率随机抽出的一个成员)的分类所需要的最少二进制位数。举例来说,如果p ⊕ 是1,接收者知道抽出的样例必为正,所以不必发任何消息,此时的熵为0。另一方面,如果是p⊕0.5,必须用一个二进制位来说明抽出的样例是正还是负。如果p⊕ 是0.8,那么对所需的消息编码方法是赋给正例集合较短的编码,可能性较小的反例集合较长的编码,平均每条消息的编码少于1 个二进制位。

更一般的,如果目标属性具有c个不同的值,那么S 相对于c 个状态(c-wise)的分类的熵定义为:

3

2) 用信息增益度量期望的熵降低
4

已经有了熵作为衡量训练样例集合纯度的标准,现在可以定义属性分类训练数据的效力的度量标准。这个标准被称为“信息增益”。简单的说,一个属性的信息增益就是由于使用这个属性分割样例而导致的期望熵降低。

其中Values(A)是属性A 所有可能值的集合。

Sv是S 中属性A 的值为v 的子集(也就是,S v ={s∈S|A(s)=v})。

请注意,等式的第一项就是原来集合S 的熵,第二项是用A 分类S 后熵的期望值。这个第二项描述的期望熵就是每个子集的熵的加权和。|Sv |权值为属于Sv 的样例占原始样例S 的比例。所以Gain(S,A)是由于知道属性A的|S |值而导致的期望熵减少。

换句话来讲,Gain(S,A)是由于给定属性A 的值而得到的关于目标函数值的信息。当对S 的一个任意成员的目标值编码时,Gain(S,A)的值是在知道属性A 的值后可以节省的二进制位数。

信息增益正是ID3 算法增长树的每一步中选取最佳属性的度量标准。下图概述了如何使用信息增益来评估属性的分类能力。在这个图中,计算了两个不同属性:湿度(Humidity)和风力(Wind)的信息增益:

5

2. 算法示例
为了演示ID3 算法的具体操作,训练样例入下图。这里,目标属性PlayTennis 对于不同的星期六上午具有yes 和no两个值,我们将根据其他属性来预测这个目标属性值。

先考虑这个算法的第一步,创建决策树的最顶端结点。哪一个属性该在树上第一个被测试呢?ID3算法计算每一个候选属性(也就是Outlook,Temperature,Humidity,和Wind)的信息增益,然后选择信息增益最高的一个。所有四个属性的信息增益为:

Gain(S,Outlook)=0.246

Gain(S,Humidity)=0.151

Gain(S,Wind)=0.048

Gain(S,Temperature)=0.029

其中S 表示来自下图的训练样例的集合。

6

根据信息增益标准,属性Outlook 在训练样例上提供了对目标属性PlayTennis 的最好预测。所以,Outlook 被选作根结点的决策属性,并为它的每一个可能值(也就是Sunny,Overcast 和Rain)在根结点下创建分支。

同时画出的还有被排列到每个新的后继结点的训练样例。注意到每一个Outlook=Overcast 的样例也都是PlayTennis 的正例。所以,树的这个结点成为一个叶子结点,它对目标属性的分类是PlayTennis=Yes。相反,对应Outlook=Sunny 和Outlook=Rain 的后继结点还有非0的熵,所以决策树会在这些结点下进一步展开。

对于非终端的后继结点,再重复前面的过程选择一个新的属性来分割训练样例,这一次仅使用与这个结点关联的训练样例。已经被收编入树的较高结点的属性被排除在外,以便任何给定的属性在树的任意路径上最多仅出现一次。对于每一个新的叶子结点继续这个过程,直到满足以下两个条件中的任一个:

1) 所有的属性已经被这条路径包括

2) 与这个结点关联的所有训练样例都具有同样的目标属性值(也就是它们的熵为0)

下图演示了算法的求解过程:

7

5 用搜索的观点看决策树学习
与其他的归纳学习算法一样,ID3算法可以被描述为从一个假设空间中搜索一个拟合训练样例的假设。被ID3 算法搜索的假设空间就是可能的决策树的集合。ID3算法以一种从简单到复杂的爬山算法遍历这个假设空间。从空的树开始,然后逐步考虑更加复杂的假设,目的是搜索到一个正确分类训练数据的决策树。引导这种爬山搜索的评估函数是信息增益度量。下图描述了这种搜索:

9

可以通过ID3算法的搜索空间和搜索策略深入认识这个算法的优势和不足。

1) ID3算法中的假设空间包含所有的决策树, 避免了搜索不完整假设空间(说明一下:有些算法是搜索不完整假设空间的、具体参考<<机器学习>>这本书)的一个主要风险:假设空间可能不包含目标函数。

2) 当遍历决策树空间时,ID3仅维护单一的当前假设。因为仅考虑单一的假设,ID3算法失去了表示所有一致假设所带来的优势。(说明一下:意思就是说它不能判断有没有其他的决策树也是与现有的训练数据一致的,或者使用新的实例查询来最优地区分这些竞争假设)

3) 在搜索中不进行回溯。每当在树的某一层次选择了一个属性进行测试,它不会再回溯重新考虑这个选择。所以,它易受无回溯的爬山搜索中常见风险影响:收敛到局部最优的答案,但不是全局最优的。

4) 搜索的每一步都使用当前的所有训练样例,以统计为基础决定怎样精化当前的假设。这与那些基于单独的训练样例递增作出决定的方法不同。使用所有样例的统计属性(例如,信息增益)的一个优点是大大减小了对个别训练样例错误的敏感性。

6 决策树学习的归纳偏置(参见归纳偏置相关论述)
1从观测到的训练数据泛化以分类未见实例的策略是什么呢?
换句话说,它的归纳偏置是什么?

如果给定一个训练样例的集合,那么通常有很多决策树与这些样例一致。所以,要描述ID3 算法的归纳偏置,应找到它从所有一致的假设中选择一个的根据。ID3从这些决策树中选择哪一个呢?它选择在使用简单到复杂的爬山算法遍历可能的树空间时遇到的第一个可接受的树。

概略地讲,ID3的搜索策略为

a) 优先选择较短的树而不是较长的

b) 选择那些信息增益高的属性离根结点较近的树。

在ID3 中使用的选择属性的启发式规则和它遇到的特定训练样例之间存在着微妙的相互作用,由于这一点。很难准确地刻划出ID3 的归纳偏置。然而我们可以近似地把它的归纳偏置描述为一种对短的决策树的偏好。

近似的ID3 算法归纳偏置:较短的树比较长的优先

事实上,我们可以想象一个类似于ID3的算法,它精确地具有这种归纳偏置。考虑一种算法,它从一个空的树开始广度优先搜索逐渐复杂的树,先考虑所有深度为1 的树,然后所有深度为2的,……。一旦它找到了一个与训练数据一致的决策树,它返回搜索深度的最小的一致树(例如,具有最少结点的树)。让我们称这种广度优先搜索算法为BFS-ID3。BFS-ID3寻找最短的决策树,因此精确地具有“较短的树比较长的得到优先”的偏置。ID3可被看作BFS-ID3的一个有效近似,它使用一种贪婪的启发式搜索企图发现最短的树,而不用进行完整的广度优先搜索来遍历假设空间。

因为ID3 使用信息增益启发式规则和“爬山”策略,它包含比BFS-ID3更复杂的偏置。尤其是,它并非总是找最短的一致树,而是倾向于那些信息增益高的属性更靠近根结点的树。ID3 归纳偏置的更贴切近似:

较短的树比较长的得到优先。那些信息增益高的属性更靠近根结点的树得到优先。

2. 为什么优先短的假设?
奥坎姆剃刀:优先选择拟合数据的最简单假设。

为什么应该优先选择较简单的假设呢?科学家们有时似乎也遵循这个归纳偏置。例如物理学家优先选择行星运动简单的解释,而不用复杂的解释。对这个问题并没有一个确定性的定论和证明、有兴趣的可以参考相关资料。

7 决策树常见问题
1. 避免过度拟合数据

对于一个假设,当存在其他的假设对训练样例的拟合比它差,但事实上在实例的整个分布(也就是包含训练集合以外的实例)上表现的却更好时,我们说这个假设过度拟合训练样例。

这种情况发生的一种可能原因是训练样例含有随机错误或噪声。事实上,当训练数据没有噪声时,过度拟合也有可能发生,这种情况下,很可能出现巧合的规律性。

有几种途径用来避免决策树学习中的过度拟合。它们可被分为两类:

a) 及早停止增长树法,在ID3 算法完美分类训练数据之前停止增长树;

b) 后修剪法,即允许树过度拟合数据,然后对这个树后修剪。

一个常见的做法是错误率降低修剪:考虑将树上的每一个结点作为修剪的候选对象。修剪一个结点由以下步骤组成:删除以此结点为根的子树;使它成为叶子结点;把和该结点关联的训练样例的最常见分类赋给它。仅当修剪后的树对于验证集合的性能不差于原来的树时才删除该结点。这样便使因为训练集合的巧合规律性而加入的结点很可能被删除,因为同样的巧合不大会发生在验证集合中。反复地修剪结点,每次总是选取它的删除可以最大提高决策树在验证集合上的精度的结点。继续修剪结点直到进一步的修剪是有害的。

2. 合并连续值属性

把连续值属性的值域分割为离散的区间集合。其实本质还是处理离散值、只是将连续值划分为离散值。

3. 属性选择的其他度量标准

信息增益度量存在一个内在偏置,它偏袒具有较多值的属性。举一个极端的例子,考虑属性Date,它有大量的可能值(例如March 4,1979)。要是我们把这个属性加到数据中,它会在所有属性中有最大的信息增益。

这是因为单独Date就可以完全预测训练数据的目标属性。于是这个属性会被选作树的根结点的决策属性并形成一棵深度为一级但却非常宽的树,这棵树可以理想地分类训练数据。当然,这个决策树对于后来数据的性能会相当差,因为尽管它完美地分割了训练数据,但它不是一个好的预测器(predicator)。

属性Date出了什么问题了呢?简单地讲,是因为它太多的可能值必然把训练样例分割成非常小的空间。因此,相对训练样例,它会有非常高的信息增益,尽管对于未见实例它是一个非常差的目标函数预测器。避免这个不足的一种方法是用其他度量,而不是信息增益,来选择决策属性。关于选取其他度量属性度量标准、参见参考资料。

4. 处理缺少属性值的训练样例

在某些情况下,可供使用的数据可能缺少某些属性的值。例如,在医学领域我们希望根据多项化验指标预测患者的结果,然而可能仅有部分患者具有验血结果。在这种情况下,经常需要根据此属性值已知的其他实例,来估计这个缺少的属性值。

处理缺少属性值的一种策略是赋给它结点n 的训练样例中该属性的最常见值。另一种策略是可以赋给它结点n 的被分类为c(x)的训练样例中该属性的最常见值。

5. 处理代价不同的属性

在某些学习任务中,实例的属性可能与代价相关。例如,在学习分类疾病时我们可能以这些属性来描述患者:体温、活组织切片检查、脉搏、血液化验结果等。这些属性在代价方面差别非常大,不论是所需的费用还是患者要承受的不适。对于这样的任务,我们将优先选择尽可能使用低代价属性的决策树,仅当需要产生可靠的分类时才依赖高代价属性。考虑代价的相关算法参见参考资料。

参考资料:

1<<机器学习>> Tom M. Mitchell

2<> Andrew Ng

课程翻译:

http://v.163.com/special/opencourse/machinelearning.html

讲义下载:

http://openclassroom.stanford.edu/MainFolder/CoursePage.php?course=MachineLearning

<完>

————————————————————————————-

黑夜路人,一个关注开源技术、乐于学习、喜欢分享的程序员

博客�� [blog.csdn.net]

微博�� [weibo.com]

微信:heiyeluren2012

想获取更多IT开源技术相关信息,欢迎关注微信!

微信二维码扫描快速关注本号码:

作者:heiyeshuwu 发表于2015/7/21 20:12:43 原文链接 阅读:35 评论:0 查看评论

firefox 中 hash会被预先urldecode

fiefox中如果直接使用 window.location.hash 返回的是已经 urldecode的字符串。chrome则不是。这种情况下,如果接着使用 decodeURIComponent  就很容易遇到错误。

解决办法是使用

location.href.split("#")[1].substring(1)

比如
var hash = window.location.hash.substring(1);
改为

var hash= (location.href.splice("#")[1] || "").substring(1);

[转]RESTful API 设计最佳实践

来源: http://blog.jobbole.com/41233/

RESTful API 设计最佳实践

背景

目前互联网上充斥着大量的关于RESTful API(为方便,下文中“RESTful API ”简写为“API”)如何设计的文章,然而却没有一个”万能“的设计标准:如何鉴权?API 格式如何?你的API是否应该加入版本信息?当你开始写一个app的时候,特别是后端模型部分已经写完的时候,你不得不殚精竭虑的设计和实现自己app的public API部分。因为一旦发布,对外发布的API将会很难改变。

在给SupportedFu设计API的时候,我试图以实用的角度来解决上面提到的问题。我希望可以设计出容易使用,容易部署,并且足够灵活的API,本文因此而生。

 

API设计的基本要求

网上的很多关于API设计的观点都十分”学院派“,它们也许更有理论基础,但是有时却和现实世界脱轨(因此我是自由派)。所以我这篇文章的目标是从实践的角度出发,给出当前网络应用的API设计最佳实践(当然,是我认为的最佳了~),如果觉得不合适,我不会遵从标准。当然作为设计的基础,几个必须的原则还是要遵守的:

  1. 当标准合理的时候遵守标准。
  2. API应该对程序员友好,并且在浏览器地址栏容易输入。
  3. API应该简单,直观,容易使用的同时优雅。
  4. API应该具有足够的灵活性来支持上层ui。
  5. API设计权衡上述几个原则。

需要强调的是:API的就是程序员的UI,和其他UI一样,你必须仔细考虑它的用户体验!

 

使用RESTful URLs 和action.

虽然前面我说没有一个万能的API设计标准。但确实有一个被普遍承认和遵守:RESTfu设计原则。它被Roy Felding提出(在他的”基于网络的软件架构“论文中第五章)。而REST的核心原则是将你的API拆分为逻辑上的资源。这些资源通过http被操作(GET ,POST,PUT,DELETE)。

 

那么我应该如何拆分出这些资源呢?

显然从API用户的角度来看,”资源“应该是个名词。即使你的内部数据模型和资源已经有了很好的对应,API设计的时候你仍然不需要把它们一对一的都暴露出来。这里的关键是隐藏内部资源,暴露必需的外部资源。

在SupportFu里,资源是 ticket、user、group。

一旦定义好了要暴露的资源,你可以定义资源上允许的操作,以及这些操作和你的API的对应关系:

  • GET /tickets # 获取ticket列表
  • GET /tickets/12 # 查看某个具体的ticket
  • POST /tickets # 新建一个ticket
  • PUT /tickets/12 # 更新ticket 12.
  • DELETE /tickets/12 #删除ticekt 12

可以看出使用REST的好处在于可以充分利用http的强大实现对资源的CURD功能。而这里你只需要一个endpoint:/tickets,再没有其他什么命名规则和url规则了,cool!

 

这个endpoint的单数复数

一个可以遵从的规则是:虽然看起来使用复数来描述某一个资源实例看起来别扭,但是统一所有的endpoint,使用复数使得你的URL更加规整。这让API使用者更加容易理解,对开发者来说也更容易实现。

如何处理关联?关于如何处理资源之间的管理REST原则也有相关的描述:

  • GET /tickets/12/messages- Retrieves list of messages for ticket #12
  • GET /tickets/12/messages/5- Retrieves message #5 for ticket #12
  • POST /tickets/12/messages- Creates a new message in ticket #12
  • PUT /tickets/12/messages/5- Updates message #5 for ticket #12
  • PATCH /tickets/12/messages/5- Partially updates message #5 for ticket #12
  • DELETE /tickets/12/messages/5- Deletes message #5 for ticket #12

其中,如果这种关联和资源独立,那么我们可以在资源的输出表示中保存相应资源的endpoint。然后API的使用者就可以通过点击链接找到相关的资源。如果关联和资源联系紧密。资源的输出表示就应该直接保存相应资源信息。(例如这里如果message资源是独立存在的,那么上面 GET /tickets/12/messages就会返回相应message的链接;相反的如果message不独立存在,他和ticket依附存在,则上面的API调用返回直接返回message信息)

不符合CURD的操作

对这个令人困惑的问题,下面是一些解决方法:

  1. 重构你的行为action。当你的行为不需要参数的时候,你可以把active对应到activated这个资源,(更新使用patch).
  2. 以子资源对待。例如:github上,对一个gists加星操作:PUT /gists/:id/star 并且取消星操作:DELETE /gists/:id/star.
  3. 有时候action实在没有难以和某个资源对应上例如search。那就这么办吧。我认为API的使用者对于/search这种url也不会有太大意见的(毕竟他很容易理解)。只要注意在文档中写清楚就可以了。

永远使用SSL

毫无例外,永远都要使用SSL。你的应用不知道要被谁,以及什么情况访问。有些是安全的,有些不是。使用SSL可以减少鉴权的成本:你只需要一个简单的令牌(token)就可以鉴权了,而不是每次让用户对每次请求签名。

值得注意的是:不要让非SSL的url访问重定向到SSL的url。

 

文档

文档和API本身一样重要。文档应该容易找到,并且公开(把它们藏到pdf里面或者存到需要登录的地方都不太好)。文档应该有展示请求和输出的例子:或者以点击链接的方式或者通过curl的方式(请见openstack的文档)。如果有更新(特别是公开的API),应该及时更新文档。文档中应该有关于何时弃用某个API的时间表以及详情。使用邮件列表或者博客记录是好方法。

 

版本化

在API上加入版本信息可以有效的防止用户访问已经更新了的API,同时也能让不同主要版本之间平稳过渡。关于是否将版本信息放入url还是放入请求头有过争论:API version should be included in the URL or in a header. 学术界说它应该放到header里面去,但是如果放到url里面我们就可以跨版本的访问资源了。。(参考openstack)。

strip使用的方法就很好:它的url里面有主版本信息,同时请求头俩面有子版本信息。这样在子版本变化过程中url的稳定的。变化有时是不可避免的,关键是如何管理变化。完整的文档和合理的时间表都会使得API使用者使用的更加轻松。

 

结果过滤,排序,搜索:

url最好越简短越好,和结果过滤,排序,搜索相关的功能都应该通过参数实现(并且也很容易实现)。

过滤:为所有提供过滤功能的接口提供统一的参数。例如:你想限制get /tickets 的返回结果:只返回那些open状态的ticket–get /tickektsstate=open这里的state就是过滤参数。

排序:和过滤一样,一个好的排序参数应该能够描述排序规则,而不业务相关。复杂的排序规则应该通过组合实现:

  • GET /ticketssort=-priority- Retrieves a list of tickets in descending order of priority
  • GET /ticketssort=-priority,created_at- Retrieves a list of tickets in descending order of priority. Within a specific priority, older tickets are ordered first

这里第二条查询中,排序规则有多个rule以逗号间隔组合而成。

搜索:有些时候简单的排序是不够的。我们可以使用搜索技术(ElasticSearch和Lucene)来实现(依旧可以作为url的参数)。

  • GET /ticketsq=return&state=open&sort=-priority,created_at- Retrieve the highest priority open tickets mentioning the word ‘return’

对于经常使用的搜索查询,我们可以为他们设立别名,这样会让API更加优雅。例如:
get /ticketsq=recently_closed -> get /tickets/recently_closed.

 

限制API返回值的域

有时候API使用者不需要所有的结果,在进行横向限制的时候(例如值返回API结果的前十项)还应该可以进行纵向限制。并且这个功能能有效的提高网络带宽使用率和速度。可以使用fields查询参数来限制返回的域例如:
GET /ticketsfields=id,subject,customer_name,updated_at&state=open&sort=-updated_at

 

更新和创建操作应该返回资源

PUT、POST、PATCH 操作在对资源进行操作的时候常常有一些副作用:例如created_at,updated_at 时间戳。为了防止用户多次的API调用(为了进行此次的更新操作),我们应该会返回更新的资源(updated representation.)例如:在POST操作以后,返回201 created 状态码,并且包含一个指向新资源的url作为返回头

 

是否需要 “HATEOAS

网上关于是否允许用户创建新的url有很大的异议(注意不是创建资源产生的url)。为此REST制定了HATEOAS来描述了和endpoint进行交互的时候,行为应该在资源的metadata返回值里面进行定义。

(译注:作者这里认为HATEOAS还不算成熟,我也不怎么理解这段就算了,读者感兴趣可以自己去原文查看)

 

只提供json作为返回格式

现在开始比较一下XML和json了。XML即冗长,难以阅读,又不适合各种编程语言解析。当然XML有扩展性的优势,但是如果你只是将它来对内部资源串行化,那么他的扩展优势也发挥不出来。很多应用(youtube,twitter,box)都已经开始抛弃XML了,我也不想多费口舌。给了google上的趋势图吧:

当然如果的你使用用户里面企业用户居多,那么可能需要支持XML。如果是这样的话你还有另外一个问题:你的http请求中的media类型是应该和accept 头同步还是和url?为了方便(browser explorability),应该是在url中(用户只要自己拼url就好了)。如果这样的话最好的方法是使用.xml或者.json的后缀。

 

命名方式?

是蛇形命令(下划线和小写)还是驼峰命名?如果使用json那么最好的应该是遵守JAVASCRIPT的命名方法-也就是说骆驼命名法。如果你正在使用多种语言写一个库,那么最好按照那些语言所推荐的,java,c#使用骆驼,python,ruby使用snake。

个人意见:我总觉得蛇形命令更好使一些,当然这没有什么理论的依据。有人说蛇形命名读起来更快,能达到20%,也不知道真假http://ieeexplore.ieee.org/xpl/articleDetails.jsptp=&arnumber=5521745

 

默认使用pretty print格式,使用gzip

只是使用空格的返回结果从浏览器上看总是觉得很恶心(一大坨有没有?~)。当然你可以提供url上的参数来控制使用“pretty print”,但是默认开启这个选项还是更加友好。格外的传输上的损失不会太大。相反你如果忘了使用gzip那么传输效率将会大大减少,损失大大增加。想象一个用户正在debug那么默认的输出就是可读的-而不用将结果拷贝到其他什么软件中在格式化-是想起来就很爽的事,不是么?

下面是一个例子:

$ curl https://API.github.com/users/veesahni > with-whitespace.txt
$ ruby -r json -e 'puts JSON JSON.parse(STDIN.read)' < with-whitespace.txt > without-whitespace.txt
$ gzip -c with-whitespace.txt > with-whitespace.txt.gz
$ gzip -c without-whitespace.txt > without-whitespace.txt.gz

输出如下:

  • without-whitespace.txt- 1252 bytes
  • with-whitespace.txt- 1369 bytes
  • without-whitespace.txt.gz- 496 bytes
  • with-whitespace.txt.gz- 509 bytes

在上面的例子中,多余的空格使得结果大小多出了8.5%(没有使用gzip),相反只多出了2.6%。据说:twitter使用gzip之后它的streaming API传输减少了80%(link:https://dev.twitter.com/blog/announcing-gzip-compression-streaming-APIs).

 

只在需要的时候使用“envelope”

很多API象下面这样返回结果:

1
2
3
4
5
6
{
  "data" : {
    "id" : 123,
    "name" : "John"
  }
}

理由很简单:这样做可以很容易扩展返回结果,你可以加入一些分页信息,一些数据的元信息等-这对于那些不容易访问到返回头的API使用者来说确实有用,但是随着“标准”的发展(cors和http://tools.ietf.org/html/rfc5988#page-6都开始被加入到标准中了),我个人推荐不要那么做。

何时使用envelope?

有两种情况是应该使用envelope的。如果API使用者确实无法访问返回头,或者API需要支持交叉域请求(通过jsonp)。
jsonp请求在请求的url中包含了一个callback函数参数。如果给出了这个参数,那么API应该返回200,并且把真正的状态码放到返回值里面(包装在信封里),例如:

1
2
3
4
5
6
7
callback_function({
  status_code: 200,
  next_page: "https://..",
  response: {
    ... actual JSON response body ...
  }
})

同样为了支持无法方法返回头的API使用者,可以允许envelope=true这样的参数。

在post,put,patch上使用json作为输入

如果你认同我上面说的,那么你应该决定使用json作为所有的API输出格式,那么我们接下来考虑考虑API的输入数据格式。
很多的API使用url编码格式:就像是url查询参数的格式一样:单纯的键值对。这种方法简单有效,但是也有自己的问题:它没有数据类型的概念。这使得程序不得不根据字符串解析出布尔和整数,而且还没有层次结构–虽然有一些关于层次结构信息的约定存在可是和本身就支持层次结构的json比较一下还是不很好用。

当然如果API本身就很简单,那么使用url格式的输入没什么问题。但对于复杂的API你应该使用json。或者干脆统一使用json。
注意使用json传输的时候,要求请求头里面加入:Content-Type:application/json.,否则抛出415异常(unsupported media type)。

 

分页

分页数据可以放到“信封”里面,但随着标准的改进,现在我推荐将分页信息放到link header里面:http://tools.ietf.org/html/rfc5988#page-6。

使用link header的API应该返回一系列组合好了的url而不是让用户自己再去拼。这点在基于游标的分页中尤为重要。例如下面,来自github的文档

1
2
Link: <https://api.github.com/user/repos?page=3&per_page=100>; rel="next",
<https://api.github.com/user/repos?page=50&per_page=100>; rel="last"

自动加载相关的资源

很多时候,自动加载相关资源非常有用,可以很大的提高效率。但是这却和RESTful的原则相背。为了如此,我们可以在url中添加参数:embed(或者expend)。embed可以是一个逗号分隔的串,例如:

1
GET /ticket/12embed=customer.name,assigned_user

对应的API返回值如下:

1
2
3
4
5
6
7
8
9
10
11
12
{
  "id" : 12,
  "subject" : "I have a question!",
  "summary" : "Hi, ....",
  "customer" : {
    "name" : "Bob"
  },
  assigned_user: {
   "id" : 42,
   "name" : "Jim",
  }
}

值得提醒的是,这个功能有时候会很复杂,并且可能导致N+1 SELECT 问题

 

重写HTTP方法

有的客户端只能发出简单的GET 和POST请求。为了照顾他们,我们可以重写HTTP请求。这里没有什么标准,但是一个普遍的方式是接受X-HTTP-Method-Override请求头。

 

速度限制

为了避免请求泛滥,给API设置速度限制很重要。为此 RFC 6585 引入了HTTP状态码429(too many requests)。加入速度设置之后,应该提示用户,至于如何提示标准上没有说明,不过流行的方法是使用HTTP的返回头。

下面是几个必须的返回头(依照twitter的命名规则):

  • X-Rate-Limit-Limit :当前时间段允许的并发请求数
  • X-Rate-Limit-Remaining:当前时间段保留的请求数。
  • X-Rate-Limit-Reset:当前时间段剩余秒数

为什么使用当前时间段剩余秒数而不是时间戳?

时间戳保存的信息很多,但是也包含了很多不必要的信息,用户只需要知道还剩几秒就可以再发请求了这样也避免了clock skew问题

有些API使用UNIX格式时间戳,我建议不要那么干。为什么?HTTP 已经规定了使用 RFC 1123 时间格式

 

鉴权 Authentication

restful API是无状态的也就是说用户请求的鉴权和cookie以及session无关,每一次请求都应该包含鉴权证明。

通过使用ssl我们可以不用每次都提供用户名和密码:我们可以给用户返回一个随机产生的token。这样可以极大的方便使用浏览器访问API的用户。这种方法适用于用户可以首先通过一次用户名-密码的验证并得到token,并且可以拷贝返回的token到以后的请求中。如果不方便,可以使用OAuth 2来进行token的安全传输。

支持jsonp的API需要额外的鉴权方法,因为jsonp请求无法发送普通的credential。这种情况下可以在查询url中添加参数:access_token。注意使用url参数的问题是:目前大部分的网络服务器都会讲query参数保存到服务器日志中,这可能会成为大的安全风险。

注意上面说到的只是三种传输token的方法,实际传输的token可能是一样的。

 

缓存

HTTP提供了自带的缓存框架。你需要做的是在返回的时候加入一些返回头信息,在接受输入的时候加入输入验证。基本两种方法:

ETag:当生成请求的时候,在HTTP头里面加入ETag,其中包含请求的校验和和哈希值,这个值和在输入变化的时候也应该变化。如果输入的HTTP请求包含IF-NONE-MATCH头以及一个ETag值,那么API应该返回304 not modified状态码,而不是常规的输出结果。

Last-Modified:和etag一样,只是多了一个时间戳。返回头里的Last-Modified:包含了 RFC 1123 时间戳,它和IF-MODIFIED-SINCE一致。HTTP规范里面有三种date格式,服务器应该都能处理。

出错处理

就像html错误页面能够显示错误信息一样,API 也应该能返回可读的错误信息–它应该和一般的资源格式一致。API应该始终返回相应的状态码,以反映服务器或者请求的状态。API的错误码可以分为两部分,400系列和500系列,400系列表明客户端错误:如错误的请求格式等。500系列表示服务器错误。API应该至少将所有的400系列的错误以json形式返回。如果可能500系列的错误也应该如此。json格式的错误应该包含以下信息:一个有用的错误信息,一个唯一的错误码,以及任何可能的详细错误描述。如下:

1
2
3
4
5
{
  "code" : 1234,
  "message" : "Something bad happened :-(",
  "description" : "More details about the error here"
}

对PUT,POST,PATCH的输入的校验也应该返回相应的错误信息,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
  "code" : 1024,
  "message" : "Validation Failed",
  "errors" : [
    {
      "code" : 5432,
      "field" : "first_name",
      "message" : "First name cannot have fancy characters"
    },
    {
       "code" : 5622,
       "field" : "password",
       "message" : "Password cannot be blank"
    }
  ]
}

 

HTTP 状态码

1
2
3
4
5
6
7
8
9
10
11
12
200 ok  - 成功返回状态,对应,GET,PUT,PATCH,DELETE.
 201 created  - 成功创建。
 304 not modified   - HTTP缓存有效。
 400 bad request   - 请求格式错误。
 401 unauthorized   - 未授权。
 403 forbidden   - 鉴权成功,但是该用户没有权限。
 404 not found - 请求的资源不存在
 405 method not allowed - 该http方法不被允许。
 410 gone - 这个url对应的资源现在不可用。
 415 unsupported media type - 请求类型错误。
 422 unprocessable entity - 校验错误时用。
 429 too many request - 请求过多。

 

js trim 比较

trim 在ie8内是没有的。所以有了这个讨论:

http://stackoverflow.com/questions/11219731/trim-function-doesnt-work-in-ie8

但是,是原生的trim还是使用正则表达式的快呢?

有人跑了几个测试

http://jsperf.com/javascript-trim-string/2

http://jsperf.com/jquery-trim-vs-string-prototype-trim/5

证明还是正则快点。

 

mysql实现一些安全和版本控制的思路

1. 什么是防删除,防撰改

禁止数据删除,数据一旦增加不允许数据被任何人删除

禁止数据修改,数据一旦建立不允许对数据做修改操作

2. 为什么要做防删除,防撰改限制

很多时候我们的数据是只增加,不会删除数据。有些敏感子段一旦数据家里是不允许再修改的,例如银行账户表中的资金子段。

另一个原因是我们防止误操作

3. 何时做防删除,防撰改限制

我认为在数据库设计时就应该考虑倒这些问题,如果发现数据被删除或者被撰改,亡羊补牢也不晚,我们不能允许再次发生。

你可以取消用户的 DELETE 权限,使之只能做查询操作,但修改(UPDATE)呢?你就无能为力!如果取消UPDATE程序将不能正常运行。

4. 在哪里做防删除,防撰改限制

程序设计之初你就应该想到这些问题,如果没有考虑倒,你只能修改现有逻辑。通常的做法是所有表增加一个删除状态子段,删除操作即是更新状态。这种方式也有弊端就是垃圾数据会不停地膨胀。

5. 谁去做防删除,防撰改限制

我认为可以分为两种人,一种是DBA,一种是开发者。这里主要将数据库部分。

6. 怎样实现防删除,防撰改限制

6.1. 限制删除

CREATE DEFINER=`dba`@`192.168.%` TRIGGER `account_before_delete` BEFORE DELETE ON `account` FOR EACH ROW BEGIN
	SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Permission denied', MYSQL_ERRNO = 1001;
END

对account表中的记录做删除操作,数据库抛出异常 Permission denied

6.2. 限制修改

禁止所有修改操作

DELIMITER $$
CREATE DEFINER=`dba`@`192.168.%` TRIGGER `logging_before_update` BEFORE UPDATE ON `logging` FOR EACH ROW BEGIN
	SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Permission denied', MYSQL_ERRNO = 1001;
END			

限制部分子段修改,其他子段扔允许修改

CREATE DEFINER=`dba`@`192.168.%` TRIGGER `members_before_update` BEFORE UPDATE ON `members` FOR EACH ROW BEGIN
	SET NEW.`id` = OLD.id;
	SET NEW.`name` = OLD.name;
	SET NEW.`chinese_name` = OLD.chinese_name;
	SET NEW.`english_name` = OLD.english_name;
	SET NEW.`sex` = OLD.sex;
	SET NEW.`address` = OLD.address;
	SET NEW.`zipcode` = OLD.zipcode;
	SET NEW.`country_code` = OLD.country_code;
	SET NEW.`mobile` = OLD.mobile;
	SET NEW.`email` = OLD.email;
	SET NEW.`qq` = OLD.qq;
	SET NEW.`question` = OLD.question;
	SET NEW.`answer` = OLD.answer;
	SET NEW.`ctime` = OLD.ctime;
END

在数据库修改前我们覆盖掉修改的数据,使之更新后数据保持不变。

6.3. 为数据安全而分库

我们通常使用一个数据库开发,该数据库包含了前后台所有的功能,我建议将前后台等等功能进行分库然后对应各种平台分配用户权限,例如

我们创建三个数据库cms,frontend,backend 同时对应创建三个用户 cms,frontend,backend 三个用户只能分别访问自己的数据库,注意在系统的设计之初你要考虑好这样的划分随之系统需要做相应的调整。

CREATE DATABASE `cms` /*!40100 COLLATE 'utf8_general_ci' */;
CREATE DATABASE `frontend` /*!40100 COLLATE 'utf8_general_ci' */;
CREATE DATABASE `backend` /*!40100 COLLATE 'utf8_general_ci' */;

backend 负责后台,权限最高

mysql> SHOW GRANTS FOR 'backend'@'localhost';
+--------------------------------------------------------------------------------------+
| Grants for backend@localhost                                                         |
+--------------------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'backend'@'localhost'                                          |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `cms`.* TO 'backend'@'localhost'             |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `frontend`.* TO 'backend'@'localhost'        |
| GRANT SELECT, INSERT, UPDATE, DELETE, CREATE ON `backend`.* TO 'backend'@'localhost' |
+--------------------------------------------------------------------------------------+
4 rows in set (0.04 sec)		

frontend 是前台权限,主要是用户用户中心,用户注册,登录,用户信息资料编辑,查看新闻等等

mysql> SHOW GRANTS FOR 'frontend'@'localhost';
+------------------------------------------------------------------------+
| Grants for frontend@localhost                                          |
+------------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'frontend'@'localhost'                           |
| GRANT SELECT, INSERT, UPDATE ON `frontend`.* TO 'frontend'@'localhost' |
| GRANT SELECT ON `cms`.`news` TO 'frontend'@'localhost'                 |
+------------------------------------------------------------------------+
3 rows in set (0.00 sec)		

cms 用户是网站内容管理,主要负责内容更新,但登陆CMS后台需要`backend`.`Employees`表用户认证,所以他需要读取权限,但不允许修改其中的数据。

mysql> SHOW GRANTS FOR 'cms'@'localhost';
+----------------------------------------------------------------------+
| Grants for cms@localhost                                             |
+----------------------------------------------------------------------+
| GRANT USAGE ON *.* TO 'cms'@'localhost'                              |
| GRANT SELECT, INSERT, UPDATE, DELETE ON `cms`.* TO 'cms'@'localhost' |
| GRANT SELECT ON `backend`.`Employees` TO 'cms'@'localhost'           |
+----------------------------------------------------------------------+
3 rows in set (0.00 sec)		

cms与backend 通常我们会限制IP地址来源,安全相对好控制。

frontend 主要对外提供服务,我们假设一旦被骇客入侵,所波及的范围被限制在frontend权限下,至少`backend`.`Employees`不会被撰改,CMS内容也得到了保护。

想100%解决数据的安全是非常空难的,但我们至少保护了一部份数据的安全。使其安全不会进一步扩散影响。

7. 怎样实现数据修改留痕

数据记录每一次修改我们都需要保留之前的数据,这样可以随时调出历史数据,用户审计等等。

7.1. 版本控制

主表

CREATE TABLE `article` (
	`article_id` MEDIUMINT(8) UNSIGNED NOT NULL AUTO_INCREMENT,
	`cat_id` SMALLINT(5) NOT NULL DEFAULT '0',
	`title` VARCHAR(150) NOT NULL DEFAULT '',
	`content` LONGTEXT NOT NULL,
	`author` VARCHAR(30) NOT NULL DEFAULT '',
	`keywords` VARCHAR(255) NOT NULL DEFAULT '',
	PRIMARY KEY (`article_id`),
	INDEX `cat_id` (`cat_id`)
)
ENGINE=MyISAM
ROW_FORMAT=DEFAULT
AUTO_INCREMENT=1			

本版控制表,用于记录每次变动

CREATE TABLE `article_history` (
	`id` MEDIUMINT(8) UNSIGNED NOT NULL AUTO_INCREMENT,
	`article_id` MEDIUMINT(8) UNSIGNED NOT NULL,
	`cat_id` SMALLINT(5) NOT NULL DEFAULT '0',
	`title` VARCHAR(150) NOT NULL DEFAULT '',
	`content` LONGTEXT NOT NULL,
	`author` VARCHAR(30) NOT NULL DEFAULT '',
	`keywords` VARCHAR(255) NOT NULL DEFAULT '',
	PRIMARY KEY (`id`),
	INDEX `article_id` (`article_id`)
)
ENGINE=MyISAM
ROW_FORMAT=DEFAULT
AUTO_INCREMENT=1

版本控制触发器

DROP TRIGGER article_history;

DELIMITER //
CREATE TRIGGER article_history BEFORE update ON article FOR EACH ROW
BEGIN
	INSERT INTO article_history SELECT * FROM article WHERE article_id = OLD.article_id;
END; //
DELIMITER;					

任何数据的变化都会复制一份到历史表,我们可以随时比较两个版本数据的变化,我还为此开发了一个类似diff的工具,可以逐行比较,通过色彩变化现实数据的不同。

7.2. 一张表实现历史日志记录

我有一个表,里面只有固定行数的行记录,这些数据就是配置参数,我们将配置文件保存在数据库中,因为需要做负载均衡而不能使用文件配置文件。

有这样一个需求,这个记录每次修改都要保存历史记录,用于审计等等。我是这样设计该表的

CREATE TABLE `config_fee` (
	`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
	`level` INT(11) NULL DEFAULT NULL COMMENT '层级',
	`type` ENUM('Deposit','Withdrawing') NOT NULL DEFAULT 'Withdrawing' COMMENT '类型,存款,取款',
	`min_fee` FLOAT(10,2) NOT NULL COMMENT '最低手续费',
	`max_fee` FLOAT(10,2) NOT NULL COMMENT '最高手续费',
	`ratio` FLOAT(10,2) NOT NULL COMMENT '手续费比例',
	`operator` VARCHAR(10) NOT NULL COMMENT '操作者',
	`status` ENUM('Current','Trash') NOT NULL DEFAULT 'Current',
	`ctime` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
	`mtime` TIMESTAMP NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
	PRIMARY KEY (`id`)
)
COMMENT='手续费管理'
COLLATE='utf8_general_ci'
ENGINE=InnoDB;			

数据记录的形态

mysql> select type,operator,status,ctime,mtime from config_mtf_fee;
+---------+----------+---------+---------------------+---------------------+
| type    | operator | status  | ctime               | mtime               |
+---------+----------+---------+---------------------+---------------------+
| Deposit | jam      | Trash   | 2014-07-20 11:10:17 | 2014-07-20 11:10:57 |
| Deposit | lucy     | Trash   | 2014-08-24 11:10:17 | 2014-08-24 11:10:57 |
| Deposit | lily     | Trash   | 2014-08-25 11:10:17 | 2014-08-25 11:10:57 |
| Deposit | kitty    | Trash   | 2014-08-27 11:10:17 | 2014-08-27 11:10:57 |
| Deposit | neo      | Current | 2014-08-28 11:10:54 | 2014-08-28 11:10:59 |
+---------+----------+---------+---------------------+---------------------+
2 rows in set (0.00 sec)			

如上图所示,状态 Current 是当前记录,而Trash是废弃的历史记录。

每次修改数据,首先将Current改为Trash,然后插入一条新数据状态为Current,我们只会使用最后一条状态为current的数据。

我们使用更新触发器控制除了status,mtime意外的字段修改

CREATE DEFINER=`root`@`%` TRIGGER `config_fee_before_update` BEFORE UPDATE ON `config_fee` FOR EACH ROW BEGIN
	SET NEW.`id` = OLD.id;
	SET NEW.`level` = OLD.level;
	SET NEW.`type` = OLD.type;
	SET NEW.`min_amount` = OLD.min_amount;
	SET NEW.`min_fee` = OLD.min_fee;
	SET NEW.`max_fee` = OLD.max_fee;
	SET NEW.`ratio` = OLD.ratio;
	SET NEW.`operator` = OLD.operator;
	SET NEW.`ctime` = OLD.ctime;
END;			

限制删除的触发器

CREATE DEFINER=`dba`@`192.168.%` TRIGGER `config_fee_before_delete` BEFORE DELETE ON `config_fee` FOR EACH ROW BEGIN
	SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Permission denied', MYSQL_ERRNO = 1001;
END			

 

[转]受骗的快乐

[原文连接]http://blog.6park.com/article.php?type=blog&itemid=190323

 

受骗的快乐

 

李公尚

 

在洛杉矶的一个商业中心,有家中国自助餐厅,导游每天带领大量中国游客前往就餐。旅游旺季,前去就餐的中国游客要排很长的队,在餐厅外候餐。

 

比尔先生开的化妆品商店,紧靠这家中国餐厅。他注意到:中国游客吃完饭,毫无例外地都爱到周围的各个商店转。甚至中国游客排队候餐时,也只有少数人排队,大多数人都去逛商店。他很奇怪,这周围很多商店,除了一家中国人开的卖化妆品和保健品的商店外,其他都很难做中国游客的生意。

 

很多商店把无法做中国人的生意,归结为语言不通,但比尔不以为然。中国游客进了他的商店,大都转转看看,最多问问价钱,就推门离去。旁边的中国商店和他卖同样的商品,标价比他商店的高,中国游客却购买踊跃,让他觉得不合情理。为了弄清原因,他经常跟着中国游客,进入中国商店观察。

 

经过一段时间的调查研究,他发现中国游客看到新奇或名牌商品,总是先问价钱,即便明码标价,也明知故问。一旦听说可以打折,不管有用没用,都抢着买。很多人说自己没用,可以带回去送人。对于早就打算要买的目标商品,找到后看了标价,照样问价,然后讨价还价。如砍价成功,就大量购买。一旦买到了“便宜货”,就向同伴炫耀,显示自己精明。于是一传十,十传百,没买那种商品的人就感觉自己吃亏了,于是也去买。有时那种商品甚至会卖到脱销。

 

比尔想了很久,终于明白了。中国游客所以这样做,是因为相信在美国买的商品,质量全都合格,所以根本不问质量,只问价格。美国人做不了中国人的生意,是因为中国游客喜爱讨价还价,而美国商店却无权随便打折。

 

他了解到,中国游客到中国商店去,是因为导游和商店有利益关系,把商店的价格说成全美国最低,而商店却趁机标高价格,当中国游客津津乐道讨价还价时,商店再逐步降低价格。

 

这是个了不起的发现。比尔惊喜自己想出了吸引中国游客的办法。一天,一位中国游客进门,他热情接待,耐心细致地介绍产品特点和功效。他知道那位游客根本听不懂他的话,只是出于礼貌不好拂袖而去。果然,想尽快离去的游客打断他,指着感兴趣的商品询问价钱。他装作老眼昏花,戴上花镜看了半天,又摘掉花镜,似乎不放心,再戴上,最后拿过计算器,按下数字:63.33元。

 

游客看了,瞪大眼睛看着他,再看看商品下面的标价,明明写着89.99元。便问商品是不是打折。比尔耸耸肩帮,双手一摊,坚定地表示决不打折。说打折损害厂商和店铺的信誉,行业规定正品不许打折。游客狡诘地指着计算器,比划着手势,用生硬的英语问:“真的?这上面的价格没错?”比尔坚定地点点头。

 

于是游客立即把那款商品拿到柜台,急急忙忙付款。比尔给他结帐时,他不失时机又尽可能多地去拿一些,一起付款。他出门时,比尔见他沾沾自喜,毫无半点愧疚。

 

游客走后不久,商店里一下进来好几位中国游客,点名要买这款商品。这几位游客尽自己的最大能量,静悄悄地取了商品,大气不敢出地走到柜台付款。一出门,哈哈大笑,议论纷纷。比尔知道,他们在笑他眼老昏花,自豪于“骗了老外”。

 

很快,他的商店一下涌进了几十名中国游客。很多顾客在抢购那款商品时,还询问其它商品价格,比尔拿着计算器,有的按出正确价格,有的故意按错。按错价格的商品,多被抢购一空。

 

那天,最后几位旅客离去时,比尔故意问:“你们为什么喜欢这款商品?”几位顾客听了,互使眼色都不说话。一名想练说英语的游客自以为是地说:“美国人开的商店一般不会骗人。旁边那家中国店卖的东西可能是假货,正品哪有打折的?”比尔问:“既然知道他们卖假货,为什么还买?”游客答:“图个便宜,回去送人。我们从美国商店买的东西,一般都留着自己用。”

 

比尔的这个小招数,屡试不爽,让他的生意大有起色。一次,他故意离开商店,让他女儿替他照看一会儿。她女儿为游客结帐时,按照标价结帐。几名顾客付款后,发现和前面游客付的钱不一样,就质问为什么多收他们的钱。前面几位付了钱的游客听了,赶紧悄悄离开商店。比尔的女儿拿着标价告诉他们并没有错,让他们把前面那些付了款的人找回来问问。这几名游客知道前面的顾客不会回来,为了面子,他们不好退货,只好悻悻离去。出了商店不久,他们和那些等在门外,取笑奚落他们的游客吵骂起来。

 

比尔和中国游客从此建立了双赢的情谊,共享骗人和受骗的快乐。看到洋洋自得的中国游客从他的店里乘兴而去,他常对人说:中国人真像一群顽皮的孩子,非常可爱。即便受了骗,也欢天喜地。

 

2015322

 

于美国加利福尼亚

PHP Tratis

关于Tratis是好是坏的讨论已经很多。

http://www.sitepoint.com/php-traits-good-or-bad/

http://www.whitewashing.de/2013/04/12/traits_are_static_access.html

我的意见是,必要的时候就用,但不要滥用。