Gone With the Wind

计算机眼中的数字

0.1 + 0.2 = ?

即使是没上过学的人,也知道答案是0.3,但是,在看似无所不能,计算速度远超人类的计算机来说,答案就不是0.3了。看看chrome下的结果:

1
2
3
0.1 + 0.2

0.30000000000000004

再看看python:

1
2
3
4
5
6
Python 2.7.10 (default, Jul 30 2016, 18:31:42) 
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.34)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> 0.1 + 0.2
0.30000000000000004
>>>

再试试C语言:

1
2
3
4
5
6
7
8
#include "stdio.h"

int main(){

double a = 0.1 + 0.2;
printf( "%.17lf", a );

}
1
0.30000000000000004

qustion

产生原因

对于数字的存储绝大多数语言遵循的都是IEEE 754规范,简单来说就是一些革命先烈告诉你当你在二进制计算机中想要一个浮点数的时候应该如何去表达。

首先,内存是有限的,所以一个数字,当它声明好了之后所占的位数是固定的,比如在C语言中,double类型所占的字节数是8,那么总共就是64位(可能某些奇葩编译器会有些差异),革命先烈们是怎么规定的呢?

double

有一个通用的计算公式:

float-cal

  1. (-1)^s表示符号位,当s=0,V为正数;当s=1,V为负数。
  2. M表示有效数字,大于等于1,小于2。
  3. 2^E表示指数位。

举例来说,十进制的5.0,写成二进制是101.0,相当于1.01×2^2。那么,按照上面V的格式,可以得出s=0,M=1.01,E=2。
十进制的-5.0,写成二进制是-101.0,相当于-1.01×2^2。那么,s=1,M=1.01,E=2。

有几点需要注意:

  • 因为有效数字M的第一位必然是1,所以存储的时候就把这一位舍去了,可以空出一位用来存储,在计算的时候加回来就行了。

  • 指数E没有符号位,无法表示负数,所以在计算的时候减去一个半数,比如:double是64位,11位用来存储指数,假设你要保存指数10,那么就必须存储1023 + 10,也就是10000001001。ps: 以上讨论的都是64位浮点数,如果是float,也就是32位的浮点数,指数位数是8,保存指数10就是:127 + 10

对于指数E,还有一下三条更细的规则:

1
2
3
(1)E不全为0或不全为1。这时,指数E的计算值减去127(或1023),得到真实值,再将有效数字M前加上第一位的1。
(2)E全为0。这时,浮点数的指数E等于1-127(或者1-1023),有效数字M不再加上第一位的1,而是还原为0.xxxxxx的小数。这样做是为了表示±0,以及接近于0的很小的数字。
(3)E全为1。这时,如果有效数字M全为0,表示±无穷大(正负取决于符号位s);如果有效数字M不全为0,表示这个数不是一个数(NaN)。

好了,说完了背景,那么就可以来看看到底是什么原因了。

整数的二进制和十进制之间的相互转换,相信大家都会算,小数部分,很多人可能已经忘记如何计算了。

总体来说就是 乘二取整,比如0.1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0.1 * 2
0.2 >> 0
0.2 * 2
0.4 >> 0
0.4 * 2
0.8 >> 0
0.8 * 2
1.6 >> 1
0.6 * 2
1.2 >> 1

0.2 * 2
0.4 >> 0
0.4 * 2
0.8 >> 0
0.8 * 2
1.6 >> 1
......

0.1 变成了 0.0001 1001 1001 1001 .... 1001无限循环
0.2 变成了 0.0011 0011 0011 0011 .... 0011无限循环

对于计算机来说,0.1和0.2的二进制就变成了一个无尽数,其实大部分小数转换成二进制都是无穷数。于是,二进制的0.1和0.2就变成了:

竖线后面代表存储位数之外

1
2
0.0001 1001 1001 ...... 1001 | 1001   
0.0011 0011 0011 ...... 0011 | 0011

二进制相加之后变成了:

1
...... 1100 | 1100

由于存储位数限制,必须舍去竖线后面的有效数字,按照舍0进1的方式,就变成了 .... 1101,所以整个二进制数字就变成了:

1
0.0100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1100 1101

转换成十进制就是:

1
0.30000000000000004440892098500626

就变成了我们最初所看到的结果。

至此,我们开头的问题已经有了答案,那么下面有几个引申问题:

  • JS中类型存储位是52位,那么可以准确表达最大的数字就是:Math.pow(2, 53), 也就是:9007199254740992,那么以下几个表达式的值是什么呢?

9007199254740992 + 1
9007199254740992 + 2
9007199254740992 + 3
9007199254740992 + 4

  • 0.1, 0.2, 0.3, 0.4 转换成二进制小数都是无穷的,那么以下表达式的结果如何呢?

0.1 + 0.2 === 0.3
0.1 + 0.3 === 0.4
0.2 + 0.4 === 0.6
0.2 + 0.3 === 0.5

iOS开发--1天从入门到放弃--基础

写了一段时间的iOS,把iOS和javascript在各方面做一下对比,记录一些比较基础的东西。

iOS开发包含哪些东西?

  • 二门语言:Objective-C,Swift
  • 一个基础框架:Cocoa Touch
  • 一个IDE:XCode

我需要准备哪些东西?

  • Mac电脑,身残志坚的朋友可以选择黑苹果。
  • 最好有一台iPhone,身残志坚的朋友没得选了,通常在模拟器里面没问题的东西在真机里面会有一些诡异的问题,特别是涉及到摄像头以及麦克风等等硬件的时候。

Objective-c

从名字上可以见,Objective-c就是对象化的C语言,所以支持C语言,同时扩展了一些面向对象的内容,比如:Class

Objective-c与类C类的语言的一个比较大的区别是,Objective-c使用消息传递模型,从如下代码可以看出:

假设我要调用car实例的move方法,

C++

1
car.move();

Objective-c

1
[car move];

从语法上可以看出一二,C++中确实是调用了car实例的move方法,如果car实例没有这个方法,那么编译期间就会报错,但是Objective-c仅仅是向car实例发送了一个move消息,代码只会在运行时去检查car实例到底有没有能力对move消息进行回应,从中可以看出Objective-c是一门动态语言。

新建一个iOS应用

打开xcode,选择红框中的项目。

创建项目

然后选择Single View Application,顾名思义:单页应用。

取个名字:IOS-dev-demo,然后选择保存的地方,一个新的iOS应用就创建好了。

创建文件夹

xcode会自动帮我们创建四个group,分别是IOS-dev-demoIOS-dev-demoTestsIOS-dev-demoUITestsproducts,作用如下:

  • IOS-dev-demo 项目的主目录,所有的项目业务代码都保存在这个group下。
  • IOS-dev-demoTests 单元测试目录。
  • IOS-dev-demoUITests UI测试目录。
  • Products 相关编译文件保存目录。

基本我们用到最多的就是我们的项目主目录了。

运行我们的项目

运行按钮

CMD+R 或者点击左上方三角形的运行按钮,即可在模拟器上运行,刚创建好只是一个空的应用。

搭建我们的应用

为什么用搭建这个词呢,因为Cocoa Touch已经帮助我们封装好了很多内部组件,和安卓不同,Cocoa Touch的内部组件通常都有一个比较好的交互和外观,典型应用是微信,微信基本都使用了内部组件,内部组件也最符合用户使用习惯。

搭建界面有两种方式:

  1. storyboard,可视化组件拖拉,类似于Dreamweaver,这种方式不做介绍了,快速搭建应用首选,也存在些许弊端,有兴趣的可以看看这篇文章
  2. 纯代码,类似于手写HTML代码。

如何添加组件?

拿一个button组件来说,在ViewController类中的viewDidLoad方法中添加如下代码:

1
2
3
4
5
6
UIButton *button = [[UIButton alloc] init];
button.frame = CGRectMake(100, 100, 100, 50);
[button setTitle:@"按钮测试" forState:UIControlStateNormal];
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];

[self.view addSubview:button];

重新编译运行一下我们的代码,就可以看到一个黑色的按钮在界面上了。

下面用浏览器的方式展示如何添加一个按钮,相信大家一定都非常熟悉了。

1
2
3
4
5
6
7
8
9
const button = document.createElement( 'button' );
button.textContent = '按钮测试'
button.style.width = '100px';
button.style.height = '50px';
button.style[ 'margin-left' ] = '100px';
button.style[ 'margin-top' ] = '100px';
button.style.color = 'rgb(0, 0, 0)'

document.body.appendChild( button );

对比一下两段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 创建一个组件
UIButton *button = [[UIButton alloc] init]; ||| let button = document.createElement( 'button' );

// 设置组件的left, top, width, height属性
button.frame = CGRectMake(100, 100, 100, 50); ||| button.style.width = '100px';
||| button.style.height = '50px';
||| button.style.margin-left = '100px';
||| button.style.margin-top = '100px';

// 设置组件的'标题'属性
[button setTitle:@"按钮测试" forState:UIControlStateNormal]; ||| button.textContent = '按钮测试'

// 设置组件标题的颜色
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal]; ||| button.style.color = 'rgb(0, 0, 0)'

// 将组件添加到当前画布上
[self.view addSubview:button]; ||| document.body.appendChild( button );

其实从逻辑上来看两段代码非常相似,唯一不同的仅仅是语法而已。

那么从系统层面看,两者如何?

首先来看看html

htmlc

再看看iOS

cocoac

响应UI事件

通常在页面上放置了一个按钮并不是为了展示信息,而是为了能让用户进行交互,那么来看看两者如何进行UI响应。

Javascript

1
2
3
button.addEventListener( 'click', function( event ) {
alert( 'clicked' );
} );

拿到button对象,然后给这个button注册一个名字叫click的事件,再给这个注册方法一个回调函数,然后用户在这个按钮上单击的话,就能够触发这个回调函数了。

Objective-c

1
[button addTarget:self action:@selector(clickHandler:) forControlEvents:UIControlEventTouchUpInside];

调用button对象的addTarget方法,为UIControlEventTouchUpInside事件,也就是单击事件添加self也就是当前实例下的clickHandler作为事件处理函数。这里也可以选择别的类的方法,只要selector中的方法名能在addTarget对象中找到就行。
iOS中只能使用类的方法作为事件处理函数,没法使用类似匿名函数的block来进行事件处理。

回调

Javascript中发送一个http请求

1
2
3
4
5
6
7
let xhr = new XMLHttpRequest();
xhr.open( 'GET', 'http://xxxx.com' );
xhr.send();

xhr.onreadystatechange = function(){
// do something
}

Javascript基于事件驱动,通常使用一个匿名函数来作为请求结束时候的回调函数。

Objective-c

1
2
3
4
5
6
7
8
9
NSURL *url = [NSURL URLWithString:@"http://xxx.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];

NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *task = [session dataTaskWithRequest: request completionHandler: ^(NSData *data, NSURLResponse *response, NSError *error) {
NSString *result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
// ...
}];
[task resume];

Objective-c 相对来说发送一个请求的方式稍微有一些复杂,看到completionHandler这个参数,这在Objective-c中被称为block,是一种类似于Javascript中的回调函数,当然也可以使用typedef声明一个block在多个场景下使用。

关于block更多的使用奥秘,可以点击查看这篇文章

动画

数据请求回来了,那么就要开始渲染页面了,iOS有良好的动画运行机制,可以比安卓更流畅地运行动画,良好的动画效果可以带来更好的用户体验。

Javascript

JS中动画效果大致有如下几种:

纯粹的JS实现的动画效果应该就只有如下一种了

  • setTimeout,定时更新形变达到每秒24次即可实现基本的动画效果。

示例代码:

1
2
3
4
5
let width = 100;
setInterval( function() {
width++;
button.style.width = `${width}px`;
}, 16 );

更多更高效的动画效果可以借助HTML5CSS3来实现

  • requestAnimationFrame,保持和系统相同的刷新率,得到更好体验的同时,有更高的效率。
  • css中的transition-duration,效率非常高,可以直接调用GPU来进行渲染。

Objective-c

iOS中的动画主要是指Core Animation框架,这里是官方的文档

调用方式主要有以下三种:

  1. UIView 代码块调用
  2. UIView [begin commit]模式
  3. CoreAnimation中的类

简单贴一下最简单也最常用的 UIView 代码块调用的方式,其他方式的详细调用可以参考这里

1
2
3
4
CGRect newFrame = CGRect( 200, 200, 100, 50 );
[UIView animateWithDuration:1 animations:^{
button.frame = newFrame;
}];

修改UI只能在主线程中执行,如果说要在回调函数中执行以上代码,需要嵌套在下面的代码中进行:

1
2
3
dispatch_async(dispatch_get_main_queue(), ^{

});

总结

有了以上这些东西,你能已经能把一个小功能呢给串起来了,但是iOS还有更加强大实用的各种组件,UINavigationControllerUITableViewControllerNSUrlSession,还有各种全局事件派发,多线程GCD,以及处理音视频的AVFoundation,更多功能,请看进阶篇。

数加Pai(机器学习平台)的上手体验

数加pai

数加pai是一个机器学习平台,提供可视化界面来操作算法,无需自己编写任何代码即可计算自己的数据。
如果你没有那么多数据也没有这种场景,那么选择一个外部算法平台也是一个不错的选择。

kaggle

一个类似阿里云天池的算法开发大赛平台,会定时发布各种任务,前几名有奖金,第一名$10W。

我们用这个平台上练手的项目来体验一下PAI。

找到首页中标着101的那一组,这些题目都是给你练手的,相对来说会比较简单一点,就选第一个,Digit Recognizer

题目分析

这个题目的主要任务的数字识别,很多手写的数字,左边导航栏home-data,进去可以下载本次的训练数据: train.csv和测试数据: test.csv

打开train.csv文件,主办方已经把需要识别的图片提取成像素值,直接训练就行了。

其中,一行代表一个数字的图片,第一列lable代表的是已经识别出来的具体的数字。

label pixel0 pixel1 …..
1 0 186 0
0 0 186 100
….. …… …… …..

寻找合适算法

这里我采用了kNN算法,具体可以点击这里查看算法详细介绍。

选择kNN是因为kNN算法比较好理解,对于我这种新手也比较容易看懂,缺点是kNN算法是惰性算法,计算时间会比较长。

开始解题

一开始我使用python的numpy包来进行解题,在本机跑了将近一个半小时之后,终于输出了结果,这里有两个缺点,一个是我要自己处理CSV文件,提取成数组给kNN算法组件,二来是笔记本性能捉襟见肘,想要调试算法参数每次都要等这么久简直不能忍,于是我想到数加机器学习平台

上传数据

pai依赖于MaxCompute,所以要先开通MaxCompute,在数加里面可以开通,开通后建两张表pai_test_trainpai_test_data,分别对应训练数据和测试数据。

首先下载MaxCompute CMD,这里点击新版客户端下载,下载完成后,配置AK以及运行CMD,关于如何建表,请查看帮助文档,注意要把所有字段也建了,和CSV文件相对应,这里字段有段多,可以用一个脚本生成sql. 然后使用 MaxCompute CMDtunnel命令上传CSV文件建表odps@ my_test_project> tunnel upload train.csv pai_test_train;,还有测试数据表odps@ my_test_project> tunnel upload test.csv pai_test_data;

使用pai来解题

从数加的管理控制台进入pai,然后按照以下步骤操作。

  1. 新建一个实验,取名pai_test,这时候我们会得到一个空的实验,接下来就是往实验上拖组件。
  2. 首先选择数据源,在左侧表列表中选择刚刚新建的pai_test_trainpai_test_data,分别拖到右侧的实验图上。这里表同步机制有延迟,如果在所有表中找不到新建的表,可以尝试一下搜索。
  3. 这时候我们的训练表和测试表已经在实验中ready了,就需要去找我们的算法组件了,在组件-机器学习-多分类中,K近邻就是我们需要的算法组件,同样拖拉到实验中。
  4. 组件都已经准备好了,那么就需要告诉pai如果来运行你的组件了,同样采用拖拉的方式,在pai_test_train表的组件下方有个小白点,鼠标拖拉一下可以拉出一根线,把这个线连接到K近邻算法组件上方的右边白点上,两个组件已经关联完成,同理另外一个表连接到右边白点上。
  5. 接下来就是配置算法组件了,要告诉算法,哪些是我的标签列,哪些是我的特征列,点击K近邻算法组件,在右侧的算法配置界面中,选择训练表特征列,这里把除了label字段外的所有字段都勾上,因为lable列是我们的标签列。
  6. 选择训练表的标签列中选择label字段,下面两个选项都是可选项,可以选择预测表的哪些特征别参与计算,还有产出表选择产出哪些字段,这些都可以选择默认,同理还有参数设置tab,如果对算法没什么研究,选择默认就可以了。
  7. 全部选择好了,点击下方的运行来进行计算。

产出数据

运行成功后,会产出一张新表,名字带有随机性,通常格式是pai_temp_5052_75767_1,因为kaggle没有和阿里云的产品进行打通,这里我们需要把数据下载成CSV文件,然后进行上传。

  1. 首先依然是通过tunnel命令进行下载: odps@ my_test_project> tunnel download pai_temp_5052_75767_1 -h true result.csv;,-h true命令表示带上表头。
  2. 打开CSV文件,找到prediction字段,这个就是我们的预测结果了,然后就是上传kaggle了。
  3. 因为kaggle对上传的字段有要求,这样他才能统计出分数,需要一个csv文件,有两列imageIdlabelimageId就是一列自增列,从1开始,label列就是我们的结果列。
  4. 打开101那个项目,找到Make a submission那一项,上传我们的CSV文件,就能得到分数了。
  5. 后续可以调整算法参数,慢慢改善算法,提高分数,提高排名,也可以尝试一下别的算法。

TCP的粘包和半包

什么是粘包

首先请执行以下以下nodejs代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const net = require( 'net' );

const app = net.createServer( connect => {

connect.on( 'data', function ( data ) {
console.log( data.toString() );
} );

} );

app.on( 'error', error => {} );

app.listen( 8080, '0.0.0.0', function () {

let client = net.connect( { port: 8080, host: '127.0.0.1' } );

client.write( 'Hi, server!' );

client.write( 'Hi, server!' );

} );

你看到的输出一定是 Hi, server!Hi, server!,并且data事件的回调函数只执行了一次,WTF.

将代码稍作修改, 客户端write的时候加一个间隔时间

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const net = require( 'net' );

const app = net.createServer( connect => {

connect.on( 'data', function ( data ) {
console.log( data.toString() );
} );

} );

app.on( 'error', error => {} );

app.listen( 8080, '0.0.0.0', function () {

let client = net.connect( { port: 8080, host: '127.0.0.1' } );

client.write( 'Hi, server!' );

setTimeout( () => {
client.write( 'Hi, server!' );
}, 1000 );

} );

你会看到输出分了两次执行,回调函数也执行了两次,符合了我们的预期,但是,总不能我们的所有代码都加一个延时吧?

为什么会出现粘包?

网上的解释已经很明确,这里引用一段,出处已经无法考证了

1
2
3
4
5
6
7
8
9
10
TCP是个"流"协议,所谓流,就是没有界限的一串数据.大家可以想想河里的流水,是连成一片的,其间是没有分界线的.但一般通讯程序开发是需要定义一个个相互独立的数据包的,比如用于登陆的数据包,用于注销的数据包.由于TCP"流"的特性以及网络状况,在进行数据传输时会出现以下几种情况.
假设我们连续调用两次send分别发送两段数据data1和data2,在接收端有以下几种接收情况(当然不止这几种情况,这里只列出了有代表性的情况).
A.先接收到data1,然后接收到data2.
B.先接收到data1的部分数据,然后接收到data1余下的部分以及data2的全部.
C.先接收到了data1的全部数据和data2的部分数据,然后接收到了data2的余下的数据.
D.一次性接收到了data1和data2的全部数据.

对于A这种情况正是我们需要的,不再做讨论.对于B,C,D的情况就是大家经常说的"粘包",就需要我们把接收到的数据进行拆包,拆成一个个独立的数据包.为了拆包就必须在发送端进行封包.

另:对于UDP来说就不存在拆包的问题,因为UDP是个"数据包"协议,也就是两段数据间是有界限的,在接收端要么接收不到数据要么就是接收一个完整的一段数据,不会少接收也不会多接收.

如何处理粘包或者半包的情况?

这里采用了一个类似HTTP的方法,定义一个HTTP头:Content-Length: 100,然后这个数据包的大小就是100,我只需要在读到头数据之后,再读100,就是我这个数据包的带下了,如果数据还有剩下,那么就是下一个包的内容,如果数据不足100,那就说明这里只传了半个包,等待下次data事件触发,后半部分的数据就是这个包里面。

举个例子

假设我要传送一个数据: hello world!,首先我需要进行封包,一个数据包由头部和身体组成,其中,头部组成如下:

1
||||00000012

||||这个是头部开始的标示,由服务端和客户端互相约定,00000012,这个是数据体的大小,前面的0为了补全大小位,这里采用了8位,因为默认情况下,TCP的缓冲区大小是8K,8K换成byte就是: 8388608。

所以我们的头部其实固定为12位,那么接下来的就都是数据体,hello world!组成一个数据包之后的样子就是: ||||00000012hello world!,服务端的解包就不说了,就是自己维护一个缓冲区,从头开始解就行了。

better-packet

better-packet,提供了一个封包的解包的方法,专门为了用来解决粘包问题,看看使用了better-package之后会怎么样.

server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const net = require( 'net' );

const betterPacket = require( './index' );

const app = net.createServer( ( connect ) => {

let unpackager = new betterPacket.UnPackager();

connect.on( 'data', unpackager.addBuffer.bind( unpackager ) );

connect.on( 'error', () => {} );

unpackager.on( 'package', function ( data ) {

// Hi, server!
// Hi, server!
console.log( data.toString() );

} );

} );

app.on( 'error', error => {} );

app.listen( '8080', '0.0.0.0' );

client.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const net = require( 'net' );

const betterPacket = require( './index' );

const client = net.connect( { port: 8080, host: '127.0.0.1' } );

const packager = new betterPacket.Packager();

let sendData = packager.packageData( 'Hi, server!' );

client.write( sendData );

client.write( sendData );

这里服务端的数据就变得正常了,而且data事件的回调函数也被调了两次。

丧心病狂的adblock

有天在开发一个页面,里面用到了一张图片,图片对应的是一个名字叫ads的数据服务,全称大概是analyse data service,分析型数据服务,图片名字就取了一个ads.png,一开始也觉得没啥,就这么开发了,但是测试的时候发现,这张图片死活加载不出来,控制台提示net::ERR_BLOCKED_BY_CLIENT

一开始以为是图片格式有问题,于是重新转了一下图片,发现问题依旧,单独在新TAB打开这个图片又能正常显示,所以把图片格式问题排除了。

于是就把问题放到这个报错信息上net::ERR_BLOCKED_BY_CLIENT,blocked by client,被客户端,也就是浏览器给拒绝了,难道是返回的头信息有什么问题?server端是webpack起的一个静态文件服务,看了看浏览器的请求记录,发现只有request,都没有response,看来请求都没有走出浏览器。

为了验证上述问题,我把服务停了,然后然后手动更改图片的URL链接,使得重新加载,发现任然报错了,那么就可以确定问题在浏览器端。

各种google,stackoverflow,baidu,都没有找到一个合适的解答,而且其他图片全都没问题,唯独这张图片加载不出来,尝试用命令行起chrome,然后看看控制台输出,也没看到什么有价值的log。心如死灰之际,起身去了趟厕所,尿尿过程中,想到,不如把图片上传到CDN或者一些云服务试试看。

回来立马就传到了OTS上,因为传上去之后文件名会被改变,变成了一些自动生成的信息,然后把网址拿过来,放到了页面上,居然神奇得发现图片能正常显示了,看来找对了方向,和之前的一对比,马上就发现了文件名上有一些不一样,再一看之前的文件名ads,不就是广告么,一下就想到了adblock,至此,折腾了这么久,总算是找到原因了,也找到元凶: adblock,不过这也算是adblock的一个策略吧,从文件名上入手,去搜索了一下adblock的一些别的策略,谨防再次入坑。

nodejs在世界范围内的大型应用

paypal

实现相同的功能,2个人用nodejs干了5个人以前用java干的活,性能提高一倍,代码量减少33%,文件减少了40%。

nodejs 基金会主席:Danese Cooper,2014年加入paypal。

Microsoft

微软自家研发的JS引擎:chakra,主要用来解决在跑在ARM上的windows系统不支持nodejs的问题,微软已经有意向提PR到nodejs主分支上去。

Uber

整个Uber的分布式调度系统都是基于nodejs和redis开发。

API层的业务逻辑基于python,mysql和mongo。

Groupon

原先使用Ruby on Rails,有一次大家PK完之后,发现了nodejs的带来的好处,所以转换成nodejs开发,2013年宣布成功从ROR迁移至nodejs。

目前现在Groupon的技术架构:web front-end mvc / mobile client <–> json <–> nodejs <–> service server

service server可能也是nodejs。

New York Times

移动端核心业务使用nodejs, Express作为web框架,使用dust.js作为模板渲染,所有node代码用coffeeScript,同时使用redis作为node层和核心业务层之间的缓存。

GitHub

找到的资料不多,只找到一条Twitter,是GitHub官网账号在2010年12月20日发布的.

1
Git tag/branch downloads aren't working current.  We're upgrading Node.js.

NASA

因为其背景,找到的资料也不多,2008年5月份的时候,nodesource.com的CEO发布了一条Twitter

1
Welcome to #Nodejs at NASA。

沃尔玛

沃尔玛实验室,在Github上有自己的主页,发布了很多项目,大部分都跟nodejs还有javascript有关,有自己的nodejs框架:hapi,提供restful的API服务,以配置为核心,提供鉴权需求、输入验证、数据缓存和预加载等功能。
另外还有hoekflodloutMUPD8Lumbar等很多服务于nodejs的工具。

并发场景下,mysql的重复插入问题

背景

有如下一张用户表

id uid name is_deleted
1 111 xiaoming 0
2 222 xiaohong 0

为了更好地做记录,同时保证安全,代码不做物理删除,只做逻辑删除,将is_deleted字段置为1,即表示删除。

登陆依赖别人的SSO,如果登陆了之后访问过我的网站的话,那么我就会把这个人记录到自己的系统里面,同时做一些别的操作,uid字段是由SSO系统生成的,每个人唯一。

问题

当两个请求同时访问我的系统时,同时去查询数据库,两次查询都没有查到当前用户,于是这两个请求的逻辑就都走到插入新用户逻辑,同时,因为增加了is_deleted字段,我无法对uid字段加唯一索引,一个用户可以被删除多次,也无法对uidis_deleted两个字段做复合唯一索引,所以这两个请求的插入动作都能成功执行。这时,表里面就有了两条完全相同的用户记录了,问题发生。

问题原因

如果没有is_deleted字段,那么其实不存在这个问题,给uid字段加一个唯一索引,第二次插入的时候必然会报错,所以究其原因,我们需要一把锁,一把在这张表之外的锁。

解决办法

  1. 新建一张表,以uid为主键或者唯一索引,在需要插入用户表之前,先插入这张新建的表,如果插入成功了,那么再往业务表里面插入用户信息。
  2. 使用tair``redis等,在准备插入用户表前,先使用uid作为key插一条记录并加上锁,就能够保证只有一个请求能够抢到锁,抢到后再进行用户信息的插入。

缺陷

  1. 几种方法都比较重,都需要新建一张表,甚至引入一种新的存储。
  2. 假设同时两个请求抢锁,没抢到锁的请求处理会比较麻烦,可能需要定时隔一段时间去检测用户信息是否已经插入,逻辑较重。

uplodify中的大坑

项目中有使用到需要上传图片的地方, 项目本身之前是由别的同事开发的, 上传使用了uploadify来上传图片, 我们这次准备重构, 因为时间较紧, 所以也就沿用的之间的上传方法, 第一次使用uploadify, 觉得用的人还挺多, 不会有什么问题, 但是问题接踵而来。

自动发起的http请求.

因为没有影响功能, 一开始没有特别关注, 后来看控制台, 发现老是会多发出去一个请求, URL是当前域名, 因为URL有问题, 所以老是404, 查遍了所有代码都没有找到发送这个请求的地方, 后来把目光锁定到uploadify上面去, 注释了uploadify, 就发现没有这个请求了, 搜了一下, 遇到这个问题的还挺多, 网上随便找个解决办法就行了, 具体就是当你没有设置image_url的时候, 也会发送一个请求去拿这张图片, 下面有个大坑。

flash版本不会带上二级域名的cookie.

说先说下业务场景, 我们使用oauth的方式登录, 域名是: a.test.com, 登录后, 登录系统会将cookie写入到test.com域名下去, 子系统访问的时候拿到test.com下的cookie, 到登录系统中获取用户信息, 完成用户认证流程, 一个很常规的登录流程.

那么问题来了, 我们子系统的域名是a.test.com, 上传功能我们并不想开放给所有用户使用, 会有一些acl鉴权, 所以需要检测登陆态, 在chromeie下一切正常, 当使用safarifirefox的时候, 后端会提示无法获取用户信息的错, 打印了一下后端拿到的headers, 发现只有a.test.com域名下的cookie, test.com域名下的cookie全都没有获取到, 导致拿不到用户信息. 到这里就很清晰了, 一开始我以为是flash的问题, 对flash也不是很熟, 但是想想flash应该不会这么坑, 就下载了一下编译器自己写了一个测试了一下, 果然在所有浏览器下都是正常的, 那么就只能确定是uploadify的问题了, 找了个反编译工具看了下uploadify的代码, 有点多, 反编译后也不大好看, 没有找到具体原因.

最后的解决办法是, 先将上传的鉴权去掉了, 赶项目时间点, 回头马上迁移到html5去.

如何爬带有reCAPTCHA的页面

reCAPTCHA简介

reCAPTCHA项目是由卡内基梅隆大学所发展的系统,主要目的是利用CAPTCHA技术来帮助典籍数字化的进行,这个项目将由书本扫描下来无法准确的被光学文字辨识技术(OCR, Optical Character Recognition)识别的文字显示在CAPTCHA问题中,让人类在回答CAPTCHA问题时用人脑加以识别[1]。reCAPTCHA正数字化《纽约时报》(New York Times)的扫描存文件,目前已经完成20年份的数据,并希望在2010年完成110年份的数据。2009年9月17日,Google宣布收购reCAPTCHA。这是一个伟大的项目,在发挥验证码作用的同时,使得输入验证码的人对全人类做出了贡献。

项目初衷

有江湖的地方就有爬虫,为什么我会去爬一个带有验证码的网站呢?前段时间刚买了车,但是运到店里面需要一些时间,从销售那问了车架号就一直想蠢蠢欲动,看到车托之家有人可以用车架号查询车辆生产以及配置信息,那也是一个别人开发的系统,但是不知道为什么最近一直处于瘫痪状态,好在还有英文网站可以查询。但是因为英文网站带有reCAPTCHA,而天朝又因为某些不可抗力因素导致reCAPTCHA无法使用,除非挂VPN,一般人查不了配置,那么,为什么不自己开发一个呢?

实现过程

其他爬页面的细节就不说了,主要还是说说reCAPTCHA如何爬,验证码也没想着自动识别,那太复杂了,就把验证码爬下来让用户自己输入吧,reCAPTCHA使用很简单,在页面上嵌入一段JS,然后去问谷歌这个验证码的输入正确与否就OK了,所有遇到的问题如下:

  • script标签发出的请求带有referer头,显然申请reCAPTCHA服务的时候会生成一个唯一性的token,带着token才能请求到验证码,而发送请求的时候会验证`referer头,如果不是127.0.0.1或者localhost或者申请时候填写的网址,那么谷歌就拒绝提供服务。解决办法:本身因为天朝地址无法访问谷歌服务,就准备在服务端对这些请求做一次转发,转发时候不要带上referer头即可。

  • reCAPTCHA的运行原理很简单,首先页面加载一个谷歌JS文件:http://www.google.com/recaptcha/api/challenge?k=6Ldlev8SAAAAAF4fPVvI5c4IPSfhuDZp6_HR-APV,这个k参数就是上面提到申请reCAPTCHA服务的时候给你一个唯一性的token,当然要经过服务器转发,不然天朝子民无法获得这个请求,这个JS的返回格式如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    var RecaptchaState = {
    challenge : '03AHJ_VutQSbh3e1Zy9JyHmt_Zmt83Gl7r0UpTa7Hq8KHlT7XGb1zxpMDyON6W70ZB3K2rY1kxWj31xkzVCJEy4SI-KsxOac-Rvh32JMoIT2-IX6gbAgj24p-SiYSYhHyYu00OAcePod4nf5QZuFdOWNRwZfcCkemd55F6dfJOqDTmPp-i2ySI2lId8Eo5d557NGZvpYYBmLLwFtyewzBfhpEwYJgR2L0lSA',
    timeout : 1800,
    lang : 'zh-CN',
    server : 'https://www.google.com',
    site : '6Ldlev8SAAAAAF4fPVvI5c4IPSfhuDZp6_HR-APV',
    error_message : '',
    programming_error : '',
    is_incorrect : false,
    rtl : false,
    t1 : 'Ly93d3cuZ29vZ2xlLmNvbS9qcy90aC9TMFJ2cEhDbHY2a29udUt6cUtXYkxMUnY0WHM0bEVSblphTGlucDNfelo4Lmpz',
    t2 : '',
    t3 : 'OXVBWHdpOTZlcEp....(这里是一大串类似密文的东西)'
    };

    document.write('<scr'+'ipt type="text/javascript" s'+'rc="' + RecaptchaState.server + 'js/recaptcha.js"></scr'+'ipt>');

注意这个地方有个server属性,谷歌还是挺良心的,后续发的所有请求,比如获取验证码图片或者语音的请求,host都是这个server属性所带的值,同样,这些请求需要转发,不然请求不到,在服务端获取这个JS文件时,直接把server属性替换掉,替换你自己服务器转发地址,后续的请求就都往你自己服务器上发了,既能让天朝子民访问reCAPTCHA,又能在自己的域名上爬到别人域名的reCAPTCHA验证码。

  • 谷歌毕竟是谷歌,即使爬到了验证码,他也不会让你好用,谷歌的识别能力还是挺强的,原本想带上一些随机agentx-forwarded-for之类的头来模拟我是一个代理而已,但是无一例外,都被识别为了爬虫而拒绝服务,最后采用的还是没有带上任何头,直接请求,至少服务能用,但是等到用的人多了之后,验证码就会变得非常复杂,好在还有语音验证码可以用。

总结

这次爬得不算完美,当PV超过2 300的时候,验证码就变得非常恶心了,常人一般难以识别,这块还在想办法。最后贴出这个应用实例吧:宝马中文车架号查询系统

Hello World

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment