别慌,不就是跨域么

什么是跨域

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。

同源策略限制了一下行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 和 JS 对象无法获取
  • Ajax请求发送不出去

常见的跨域场景

所谓的同源是指,域名、协议、端口均为相同。

http://www.nealyang.cn/index.html 调用 http://www.nealyang.cn/server.php 非跨

http://www.nealyang.cn/index.html 调用 http://www.neal.cn/server.php 跨域,主域不同

http://abc.nealyang.cn/index.html 调用 http://def.neal.cn/server.php 跨域,子域名不同

http://www.nealyang.cn:8080/index.html 调用 http://www.nealyang.cn/server.php 跨域,端口不同

https://www.nealyang.cn/index.html 调用 http://www.nealyang.cn/server.php 跨域,协议不同

localhost 调用 127.0.0.1 跨域

跨域的解决办法

jsonp跨域

jsonp跨域其实也是JavaScript设计模式中的一种代理模式。在html页面中通过相应的标签从不同域名下加载静态资源文件是被浏览器允许的,所以我们可以通过这个“犯罪漏洞”来进行跨域。一般,我们可以动态的创建script标签,再去请求一个带参网址来实现跨域通信

//原生的实现方式

let script = document.createElement(‘script’);

script.src = ‘http://www.nealyang.cn/login?username=Nealyang&callback=callback’;

document.body.appendChild(script);

function callback(res) {

console.log(res);

}

当然,jquery也支持jsonp的实现方式

$.ajax({

url:’http://www.nealyang.cn/login’,

type:’GET’,

dataType:’jsonp’,//请求方式为jsonp

jsonpCallback:’callback’,

data:{

“username”:”Nealyang”

}

})

虽然这种方式非常好用,但是一个最大的缺陷是,只能够实现get请求

document.domain + iframe 跨域

这种跨域的方式最主要的是要求主域名相同。什么是主域名相同呢? www.nealyang.cn aaa.nealyang.cn ba.ad.nealyang.cn 这三个主域名都是nealyang.cn,而主域名不同的就不能用此方法。

假设目前a.nealyang.cn 和 b.nealyang.cn 分别对应指向不同ip的服务器。

a.nealyang.cn 下有一个test.html文件

<!DOCTYPE html>

<html lang=”en”>

<head>

<meta charset=”UTF-8″>

<title>html</title>

<script type=”text/javascript” src = “jquery-1.12.1.js”></script>

</head>

<body>

<div>A页面</div>

<iframe

style = “display : none”

name = “iframe1”

id = “iframe”

src=”http://b.nealyang.cn/1.html” frameborder=”0″></iframe>

<script type=”text/javascript”>

$(function(){

try{

document.domain = “nealyang.cn”

}catch(e){}

$(“#iframe”).load(function(){

var jq = document.getElementById(‘iframe’).contentWindow.$

jq.get(“http://nealyang.cn/test.json”,function(data){

console.log(data);

});

})

})

</script>

</body>

</html>

利用 iframe 加载 其他域下的文件(nealyang.cn/1.html), 同时 document.domain 设置成 nealyang.cn ,当 iframe 加载完毕后就可以获取 nealyang.cn 域下的全局对象, 此时尝试着去请求 nealyang.cn 域名下的 test.json (此时可以请求接口),就会发现数据请求失败了~~ 惊不惊喜,意不意外!!!!!!!

数据请求失败,目的没有达到,自然是还少一步:

<!DOCTYPE html>

<html lang=”en”>

<head>

<meta charset=”UTF-8″>

<title>html</title>

<script type=”text/javascript” src = “jquery-1.12.1.js”></script>

<script type=”text/javascript”>

$(function(){

try{

document.domain = “nealyang.com”

}catch(e){}

})

</script>

</head>

<body>

<div id = “div1”>B页面</div>

</body>

</html>

此时在进行刷新浏览器,就会发现数据这次真的是成功了

window.name + iframe 跨域

window.name属性可设置或者返回存放窗口名称的一个字符串。他的神器之处在于name值在不同页面或者不同域下加载后依旧存在,没有修改就不会发生变化,并且可以存储非常长的name(2MB)

假设index页面请求远端服务器上的数据,我们在该页面下创建iframe标签,该iframe的src指向服务器文件的地址(iframe标签src可以跨域),服务器文件里设置好window.name的值,然后再在index.html里面读取改iframe中的window.name的值。完美~

<body>

<script type=”text/javascript”>

iframe = document.createElement(‘iframe’),

iframe.src = ‘http://localhost:8080/data.php’;

document.body.appendChild(iframe);

iframe.onload = function() {

console.log(iframe.contentWindow.name)

};

</script>

</body>

当然,这样还是不够的。

因为规定如果index.html页面和和该页面里的iframe框架的src如果不同源,则也无法操作框架里的任何东西,所以就取不到iframe框架的name值了,告诉你我们不是一家的,你也休想得到我这里的数据。 既然要同源,那就换个src去指,前面说了无论怎样加载window.name值都不会变化,于是我们在index.html相同目录下,新建了个proxy.html的空页面,修改代码如下:

<body>

<script type=”text/javascript”>

iframe = document.createElement(‘iframe’),

iframe.src = ‘http://localhost:8080/data.php’;

document.body.appendChild(iframe);

iframe.onload = function() {

iframe.src = ‘http://localhost:81/cross-domain/proxy.html’;

console.log(iframe.contentWindow.name)

};

</script>

</body>

理想似乎很美好,在iframe载入过程中,迅速重置iframe.src的指向,使之与index.html同源,那么index页面就能去获取它的name值了!但是现实是残酷的,iframe在现实中的表现是一直不停地刷新, 也很好理解,每次触发onload时间后,重置src,相当于重新载入页面,又触发onload事件,于是就不停地刷新了(但是需要的数据还是能输出的)。修改后代码如下:

<body>

<script type=”text/javascript”>

iframe = document.createElement(‘iframe’);

iframe.style.display = ‘none’;

var state = 0;

iframe.onload = function() {

if(state === 1) {

var data = JSON.parse(iframe.contentWindow.name);

console.log(data);

iframe.contentWindow.document.write(”);

iframe.contentWindow.close();

document.body.removeChild(iframe);

} else if(state === 0) {

state = 1;

iframe.contentWindow.location = ‘http://localhost:81/cross-domain/proxy.html’;

}

};

iframe.src = ‘http://localhost:8080/data.php’;

document.body.appendChild(iframe);

</script>

</body>

所以如上,我们就拿到了服务器返回的数据,但是有几个条件是必不可少的:

  • iframe标签的跨域能力
  • window.names属性值在文档刷新后依然存在的能力

location.hash + iframe 跨域

此跨域方法和上面介绍的比较类似,一样是动态插入一个iframe然后设置其src为服务端地址,而服务端同样输出一端js代码,也同时通过与子窗口之间的通信来完成数据的传输。

关于锚点相信大家都已经知道了,其实就是设置锚点,让文档指定的相应的位置。锚点的设置用a标签,然后href指向要跳转到的id,当然,前提是你得有个滚动条,不然也不好滚动嘛是吧。

而location.hash其实就是url的锚点。比如http://www.nealyang.cn#Nealyang的网址打开后,在控制台输入location.hash就会返回#Nealyang的字段。

基础知识补充完毕,下面我们来说下如何实现跨域

如果index页面要获取远端服务器的数据,动态的插入一个iframe,将iframe的src执行服务器的地址,这时候的top window 和包裹这个iframe的子窗口是不能通信的,因为同源策略,所以改变子窗口的路径就可以了,将数据当做改变后的路径的hash值加载路径上,然后就可以通信了。将数据加在index页面地址的hash上, index页面监听hash的变化,h5的hashchange方法

<body>

<script type=”text/javascript”>

function getData(url, fn) {

var iframe = document.createElement(‘iframe’);

iframe.style.display = ‘none’;

iframe.src = url;

iframe.onload = function() {

fn(iframe.contentWindow.location.hash.substring(1));

window.location.hash = ”;

document.body.removeChild(iframe);

};

document.body.appendChild(iframe);

}

// get data from server

var url = ‘http://localhost:8080/data.php’;

getData(url, function(data) {

var jsondata = JSON.parse(data);

console.log(jsondata.name + ‘ ‘ + jsondata.age);

});

</script>

</body>

补充说明:其实location.hash和window.name都是差不多的,都是利用全局对象属性的方法,然后这两种方法和jsonp也是一样的,就是只能够实现get请求

postMessage跨域

这是由H5提出来的一个炫酷的API,IE8+,chrome,ff都已经支持实现了这个功能。这个功能也是非常的简单,其中包括接受信息的Message时间,和发送信息的postMessage方法。

发送信息的postMessage方法是向外界窗口发送信息

otherWindow.postMessage(message,targetOrigin);

otherWindow指的是目标窗口,也就是要给哪一个window发送消息,是window.frames属性的成员或者是window.open方法创建的窗口。 Message是要发送的消息,类型为String,Object(IE8、9不支持Obj),targetOrigin是限定消息接受范围,不限制就用星号 *

接受信息的message事件

var onmessage = function(event) {

var data = event.data;

var origin = event.origin;

}

if(typeof window.addEventListener != ‘undefined’){

window.addEventListener(‘message’,onmessage,false);

}else if(typeof window.attachEvent != ‘undefined’){

window.attachEvent(‘onmessage’, onmessage);

}

举个栗子

a.html(http://www.nealyang.cn/a.html)

<iframe id=”iframe” src=”http://www.neal.cn/b.html” style=”display:none;”></iframe>

<script>

var iframe = document.getElementById(‘iframe’);

iframe.onload = function() {

var data = {

name: ‘aym’

};

// 向neal传送跨域数据

iframe.contentWindow.postMessage(JSON.stringify(data), ‘http://www.neal.cn’);

};

// 接受domain2返回数据

window.addEventListener(‘message’, function(e) {

alert(‘data from neal —> ‘ + e.data);

}, false);

</script>

b.html(http://www.neal.cn/b.html)

<script>

// 接收domain1的数据

window.addEventListener(‘message’, function(e) {

alert(‘data from nealyang —> ‘ + e.data);

var data = JSON.parse(e.data);

if (data) {

data.number = 16;

// 处理后再发回nealyang

window.parent.postMessage(JSON.stringify(data), ‘http://www.nealyang.cn’);

}

}, false);

</script>

跨域资源共享 CORS

因为是目前主流的跨域解决方案。所以这里多介绍点。

简介

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)。 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。IE8+:IE8/9需要使用XDomainRequest对象来支持CORS。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

两种请求

说起来很搞笑,分为两种请求,一种是简单请求,另一种是非简单请求。只要满足下面条件就是简单请求

  • 请求方式为HEAD、POST 或者 GET
  • http头信息不超出一下字段:Accept、Accept-Language 、 Content-Language、 Last-Event-ID、 Content-Type(限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain)

为什么要分为简单请求和非简单请求,因为浏览器对这两种请求方式的处理方式是不同的。

简单请求

基本流程

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。 下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1

Origin: http://api.bob.com

Host: api.alice.com

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0

Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。 浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。

注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com

Access-Control-Allow-Credentials: true

Access-Control-Expose-Headers: FooBar

Content-Type: text/html; charset=utf-8

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头

  • Access-Control-Allow-Origin :该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求
  • Access-Control-Allow-Credentials: 该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
  • Access-Control-Expose-Headers:该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
withCredentials 属性

上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

另一方面,开发者必须在AJAX请求中打开withCredentials属性。

var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie

xhr.withCredentials = true;

xhr.open(‘post’, ‘http://www.domain2.com:8080/login’, true);

xhr.setRequestHeader(‘Content-Type’, ‘application/x-www-form-urlencoded’);

xhr.send(‘user=admin’);

xhr.onreadystatechange = function() {

if (xhr.readyState == 4 && xhr.status == 200) {

alert(xhr.responseText);

}

};

// jquery

$.ajax({

xhrFields: {

withCredentials: true // 前端设置是否带cookie

},

crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie

});

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。 但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

非简单请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json。

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

var url = ‘http://api.alice.com/cors’;

var xhr = new XMLHttpRequest();

xhr.open(‘PUT’, url, true);

xhr.setRequestHeader(‘X-Custom-Header’, ‘value’);

xhr.send();

浏览器发现,这是一个非简单请求,就自动发出一个”预检”请求,要求服务器确认可以这样请求。下面是这个”预检”请求的HTTP头信息。

OPTIONS /cors HTTP/1.1

Origin: http://api.bob.com

Access-Control-Request-Method: PUT

Access-Control-Request-Headers: X-Custom-Header

Host: api.alice.com

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0…

“预检”请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。

除了Origin字段,”预检”请求的头信息包括两个特殊字段。

  • Access-Control-Request-Method:该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT。
  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
预检请求的回应

服务器收到”预检”请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应

HTTP/1.1 200 OK

Date: Mon, 01 Dec 2008 01:15:39 GMT

Server: Apache/2.0.61 (Unix)

Access-Control-Allow-Origin: http://api.bob.com

Access-Control-Allow-Methods: GET, POST, PUT

Access-Control-Allow-Headers: X-Custom-Header

Content-Type: text/html; charset=utf-8

Content-Encoding: gzip

Content-Length: 0

Keep-Alive: timeout=2, max=100

Connection: Keep-Alive

Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

如果浏览器否定了”预检”请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

服务器回应的其他CORS相关字段如下:

Access-Control-Allow-Methods: GET, POST, PUT

Access-Control-Allow-Headers: X-Custom-Header

Access-Control-Allow-Credentials: true

Access-Control-Max-Age: 1728000

  • Access-Control-Allow-Methods:该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次”预检”请求。
  • Access-Control-Allow-Headers:如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在”预检”中请求的字段。
  • Access-Control-Allow-Credentials: 该字段与简单请求时的含义相同。
  • Access-Control-Max-Age: 该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
浏览器正常请求回应

一旦服务器通过了”预检”请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。

PUT /cors HTTP/1.1

Origin: http://api.bob.com

Host: api.alice.com

X-Custom-Header: value

Accept-Language: en-US

Connection: keep-alive

User-Agent: Mozilla/5.0…

浏览器的正常CORS请求。上面头信息的Origin字段是浏览器自动添加的。下面是服务器正常的回应。

Access-Control-Allow-Origin: http://api.bob.com

Content-Type: text/html; charset=utf-8

Access-Control-Allow-Origin字段是每次回应都必定包含的

结束语

CORS与JSONP的使用目的相同,但是比JSONP更强大。JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。

原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

前端代码:

<div>user input:<input type=”text”></div>

<script src=”./socket.io.js”></script>

<script>

var socket = io(‘http://www.domain2.com:8080’);

// 连接成功处理

socket.on(‘connect’, function() {

// 监听服务端消息

socket.on(‘message’, function(msg) {

console.log(‘data from server: —> ‘ + msg);

});

// 监听服务端关闭

socket.on(‘disconnect’, function() {

console.log(‘Server socket has closed.’);

});

});

document.getElementsByTagName(‘input’)[0].onblur = function() {

socket.send(this.value);

};

</script>

node Server

var http = require(‘http’);

var socket = require(‘socket.io’);

// 启http服务

var server = http.createServer(function(req, res) {

res.writeHead(200, {

‘Content-type’: ‘text/html’

});

res.end();

});

server.listen(‘8080’);

console.log(‘Server is running at port 8080…’);

// 监听socket连接

socket.listen(server).on(‘connection’, function(client) {

// 接收信息

client.on(‘message’, function(msg) {

client.send(‘hello:’ + msg);

console.log(‘data from client: —> ‘ + msg);

});

// 断开处理

client.on(‘disconnect’, function() {

console.log(‘Client socket has closed.’);

});

});

node代理跨域

node中间件实现跨域代理,是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

利用node + express + http-proxy-middleware搭建一个proxy服务器

前端代码

var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie

xhr.withCredentials = true;

// 访问http-proxy-middleware代理服务器

xhr.open(‘get’, ‘http://www.domain1.com:3000/login?user=admin’, true);

xhr.send();

后端代码

var express = require(‘express’);

var proxy = require(‘http-proxy-middleware’);

var app = express();

app.use(‘/’, proxy({

// 代理跨域目标接口

target: ‘http://www.domain2.com:8080’,

changeOrigin: true,

// 修改响应头信息,实现跨域并允许带cookie

onProxyRes: function(proxyRes, req, res) {

res.header(‘Access-Control-Allow-Origin’, ‘http://www.domain1.com’);

res.header(‘Access-Control-Allow-Credentials’, ‘true’);

},

// 修改响应信息中的cookie域名

cookieDomainRewrite: ‘www.domain1.com’ // 可以为false,表示不修改

}));

app.listen(3000);

console.log(‘Proxy server is listen at port 3000…’);

nginx代理跨域

NGINX其实个人没有怎么玩过,所以暂且也就不能误人子弟了,原谅笔者才疏尚浅~ 有机会学习研究再回来补充~~

参考文档

http://www.ruanyifeng.com/blog/2016/04/cors.html

https://segmentfault.com/a/1190000011145364

via  github.com/Nealyang/YOU-SHOULD-KNOW-JS/blob/master/doc/basic_js/JavaScript中的跨域总结.md

Net Core中数据库事务隔离详解——以Dapper和Mysql为例

事务隔离级别

.NET Core中的IDbConnection接口提供了BeginTransaction方法作为执行事务,BeginTransaction方法提供了两个重载,一个不需要参数BeginTransaction()默认事务隔离级别为RepeatableRead;另一个BeginTransaction(IsolationLevel il)可以根据业务需求来修改事务隔离级别。由于Dapper是对IDbConnection的扩展,所以Dapper在执行增删除改查时所有用到的事务需要由外部来定义。事务执行时与数据库之间的交互如下:

2017-12-19-22-43-23

从WireShark抓取的数据包来看程序和数据交互步骤依次是:建立连接-->设置数据库隔离级别-->告诉数据库一个事务开始-->执行数据增删查改-->提交事务-->断开连接

准备工作

准备数据库:Mysql (笔者这里是:MySql 5.7.20 社区版)

创建数据库并创建数据表,创建数据表的脚本如下:


CREATE TABLE `posts` (
  `Id` varchar(255) NOT NULL ,
  `Text` longtext NOT NULL,
  `CreationDate` datetime NOT NULL,
  `LastChangeDate` datetime NOT NULL,
  `Counter1` int(11) DEFAULT NULL,
  `Counter2` int(11) DEFAULT NULL,
  `Counter3` int(11) DEFAULT NULL,
  `Counter4` int(11) DEFAULT NULL,
  `Counter5` int(11) DEFAULT NULL,
  `Counter6` int(11) DEFAULT NULL,
  `Counter7` int(11) DEFAULT NULL,
  `Counter8` int(11) DEFAULT NULL,
  `Counter9` int(11) DEFAULT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8;

创建.NET Core Domain类:


[Table("Posts")]
public class Post
{
    [Key]
    public string Id { get; set; }
    public string Text { get; set; }
    public DateTime CreationDate { get; set; }
    public DateTime LastChangeDate { get; set; }
    public int? Counter1 { get; set; }
    public int? Counter2 { get; set; }
    public int? Counter3 { get; set; }
    public int? Counter4 { get; set; }
    public int? Counter5 { get; set; }
    public int? Counter6 { get; set; }
    public int? Counter7 { get; set; }
    public int? Counter8 { get; set; }
    public int? Counter9 { get; set; }

}

具体怎样使用Dapper,请看上篇

Read uncommitted 读未提交

允许脏读,即不发布共享锁,也不接受独占锁。意思是:事务A可以读取事务B未提交的数据。

优点:查询速度快

缺点:容易造成脏读,如果事务A在中途回滚

以下为执行脏读的测试代码片断:


public static void RunDirtyRead(IsolationLevel transaction1Level,IsolationLevel transaction2Level)
{
    var id = Guid.NewGuid().ToString();
    using (var connection1 = new MySqlConnection(connStr))
    {
        connection1.Open();
        Console.WriteLine("transaction1 {0} Start",transaction1Level);
        var transaction1 = connection1.BeginTransaction(transaction1Level);
        Console.WriteLine("transaction1 插入数据 Start");
        var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)";
        var detail1 = connection1.Execute(sql,
        new Post
        {
            Id = id,
            Text = Guid.NewGuid().ToString(),
            CreationDate = DateTime.Now,
            LastChangeDate = DateTime.Now
        },
            transaction1);
        Console.WriteLine("transaction1 插入End 返回受影响的行:{0}", detail1);
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2 {0} Start",transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            Console.WriteLine("transaction2 查询数据 Start");
            var result = connection2.QueryFirstOrDefault<Post>("select * from posts where id=@Id", new { id = id }, transaction2);
            //如果result为Null 则程序会报异常
            Console.WriteLine("transaction2 查询结事 返回结果:Id={0},Text={1}", result.Id, result.Text);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End",transaction2Level);
        }
        transaction1.Rollback();
        Console.WriteLine("transaction1 {0} Rollback ",transaction1Level);
    }

}

1、当执行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadUncommitted),即事务1和事务2都设置为ReadUncommitted时结果如下:

2017-12-22-22-06-49

当事务1回滚以后,数据库并没有事务1添加的数据,所以事务2获取的数据是脏数据。

2、当执行RunDirtyRead(IsolationLevel.Serializable,IsolationLevel.ReadUncommitted),即事务1隔离级别为Serializble,事务2的隔离级别设置为ReadUncommitted,结果如下:

2017-12-22-22-07-28

3、当执行RunDirtyRead(IsolationLevel.ReadUncommitted,IsolationLevel.ReadCommitted);,即事务1隔离级别为ReadUncommitted,事务2的隔离级别为Readcommitted,结果如下:

2017-12-22-22-08-13

结论:当事务2(即取数据事务)隔离级别设置为ReadUncommitted,那么不管事务1隔离级别为哪一种,事务2都能将事务1未提交的数据得到;但是测试结果可以看出当事务2为ReadCommitted则获取不到事务1未提交的数据从而导致程序异常。

Read committed 读取提交内容

这是大多数数据库默认的隔离级别,但是,不是MySQL的默认隔离级别。读取数据时保持共享锁,以避免脏读,但是在事务结束前可以更改数据。

优点:解决了脏读的问题

缺点:一个事务未结束被另一个事务把数据修改后导致两次请求的数据不一致

测试重复读代码片断:


public static void RunRepeatableRead(IsolationLevel transaction1Level, IsolationLevel transaction2Level)
{
    using (var connection1 = new MySqlConnection(connStr))
    {
        connection1.Open();
        var id = "c8de065a-3c71-4273-9a12-98c8955a558d";
        Console.WriteLine("transaction1 {0} Start", transaction1Level);
        var transaction1 = connection1.BeginTransaction(transaction1Level);
        Console.WriteLine("transaction1 第一次查询开始");
        var sql = "select * from posts where id=@Id";
        var detail1 = connection1.QueryFirstOrDefault<Post>(sql, new { Id = id }, transaction1);
        Console.WriteLine("transaction1 第一次查询结束,结果:Id={0},Counter1={1}", detail1.Id, detail1.Counter1);
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2  {0} Start", transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            var updateCounter1=(detail1.Counter1 ?? 0) + 1;
            Console.WriteLine("transaction2  开始修改Id={0}中Counter1的值修改为:{1}", id,updateCounter1);
            var result = connection2.Execute(
                "update posts set Counter1=@Counter1 where id=@Id",
                new { Id = id, Counter1 = updateCounter1 },
                transaction2);
            Console.WriteLine("transaction2 修改完成 返回受影响行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
        Console.WriteLine("transaction1 第二次查询 Start");
        var detail2 = connection1.QueryFirstOrDefault<Post>(sql, new { Id = id }, transaction1);
        Console.WriteLine("transaction1 第二次查询 End 结果:Id={0},Counter1={1}", detail2.Id, detail2.Counter1);
        transaction1.Commit();
        Console.WriteLine("transaction1 {0} End", transaction1Level);
    }
}

在事务1中detail1中得到的Counter1为1,事务2中将Counter1的值修改为2,事务1中detail2得到的Counter1的值也会变为2

下面分几种情况来测试:

1、当事务1和事务2都为ReadCommitted时,结果如下:

2017-12-21-21-29-41

2017-12-22-22-08-38

2、当事务1和事务2隔离级别都为RepeatableRead时,执行结果如下:

2017-12-22-22-08-53

3、当事务1隔离级别为RepeatableRead,事务2隔离级别为ReadCommitted时执行结果如下:

2017-12-22-22-09-09

4、当事务1隔离级别为ReadCommitted,事务2隔离级别为RepeatableRead时执行结果如下:

2017-12-22-22-09-30

结论:当事务1隔离级别为ReadCommitted时数据可重复读,当事务1隔离级别为RepeatableRead时可以不可重复读,不管事务2隔离级别为哪一种不受影响。

注:在RepeatableRead隔离级别下虽然事务1两次获取的数据一致,但是事务2已经是将数据库中的数据进行了修改,如果事务1对该条数据进行修改则会对事务2的数据进行覆盖。

Repeatable read (可重读)

这是MySQL默认的隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行(目标数据行不会被修改)。

优点:解决了不可重复读和脏读问题

缺点:幻读

测试幻读代码

 
public static void RunPhantomRead(IsolationLevel transaction1Level, IsolationLevel transaction2Level)
{
    using (var connection1 = new MySqlConnection(connStr))
    {
        connection1.Open();
        Console.WriteLine("transaction1 {0} Start", transaction1Level);
        var transaction1 = connection1.BeginTransaction(transaction1Level);
        Console.WriteLine("transaction1 第一次查询数据库 Start");
        var detail1 = connection1.Query<Post>("select * from posts").ToList();
        Console.WriteLine("transaction1 第一次查询数据库 End 查询条数:{0}", detail1.Count);
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2 {0} Start", transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            Console.WriteLine("transaction2 执行插入数据 Start");
            var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)";
            var entity = new Post
            {
                Id = Guid.NewGuid().ToString(),
                Text = Guid.NewGuid().ToString(),
                CreationDate = DateTime.Now,
                LastChangeDate = DateTime.Now
            };
            var result = connection2.Execute(sql, entity, transaction2);
            Console.WriteLine("transaction2 执行插入数据 End 返回受影响行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
        Console.WriteLine("transaction1 第二次查询数据库 Start");
        var detail2 = connection1.Query<Post>("select * from posts").ToList();
        Console.WriteLine("transaction1 第二次查询数据库 End 查询条数:{0}", detail2.Count);
        transaction1.Commit();
        Console.WriteLine("transaction1 {0} End", transaction1Level);
    }
}

分别对几种情况进行测试:

1、事务1和事务2隔离级别都为RepeatableRead,结果如下:

2017-12-22-22-09-46

2、事务1和事务2隔离级别都为Serializable,结果如下:

2017-12-22-22-10-02

3、当事务1的隔离级别为Serializable,事务2的隔离级别为RepeatableRead时,执行结果如下:

2017-12-22-22-10-18

4、当事务1的隔离级别为RepeatableRead,事务2的隔离级别为Serializable时,执行结果如下:

2017-12-22-22-10-32

结论:当事务隔离级别为RepeatableRead时虽然两次获取数据条数相同,但是事务2是正常将数据插入到数据库当中的。当事务1隔离级别为Serializable程序异常,原因接下来将会讲到。

Serializable 序列化

这是最高的事务隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。

优点:解决幻读

缺点:在每个读的数据行上都加了共享锁,可能导致大量的超时和锁竞争

当执行RunPhantomRead(IsolationLevel.Serializable, IsolationLevel.Serializable)或执行RunPhantomRead(IsolationLevel.Serializable, IsolationLevel.RepeatableRead)时代码都会报异常,是因为Serializable隔离级别下强制事务以串行方式执行,由于这里是一个主线程上第一个事务未完时执行了第二个事务,但是第二个事务必须等到第一个事务执行完成后才参执行,所以就会导致程序报超时异常。这里将代码作如下修改:


using (var connection1 = new MySqlConnection(connStr))
{
    connection1.Open();
    Console.WriteLine("transaction1 {0} Start", transaction1Level);
    var transaction1 = connection1.BeginTransaction(transaction1Level);
    Console.WriteLine("transaction1 第一次查询数据库 Start");
    var detail1 = connection1.Query<Post>("select * from posts").ToList();
    Console.WriteLine("transaction1 第一次查询数据库 End 查询条数:{0}", detail1.Count);
    Thread thread = new Thread(new ThreadStart(() =>
    {
        using (var connection2 = new MySqlConnection(connStr))
        {
            connection2.Open();
            Console.WriteLine("transaction2 {0} Start", transaction2Level);
            var transaction2 = connection2.BeginTransaction(transaction2Level);
            Console.WriteLine("transaction2 执行插入数据 Start");
            var sql = "insert into posts (id,text,CreationDate,LastChangeDate) values(@Id,@Text,@CreationDate,@LastChangeDate)";
            var entity = new Post
            {
                Id = Guid.NewGuid().ToString(),
                Text = Guid.NewGuid().ToString(),
                CreationDate = DateTime.Now,
                LastChangeDate = DateTime.Now
            };
            var result = connection2.Execute(sql, entity, transaction2);
            Console.WriteLine("transaction2 执行插入数据 End 返回受影响行:{0}", result);
            transaction2.Commit();
            Console.WriteLine("transaction2 {0} End", transaction2Level);
        }
    }));
    thread.Start();
    //为了证明两个事务是串行执行的,这里让主线程睡5秒
    Thread.Sleep(5000);
    Console.WriteLine("transaction1 第二次查询数据库 Start");
    var detail2 = connection1.Query<Post>("select * from posts").ToList();
    Console.WriteLine("transaction1 第二次查询数据库 End 查询条数:{0}", detail2.Count);
    transaction1.Commit();
    Console.WriteLine("transaction1 {0} End", transaction1Level);
}

执行结果如下:

2017-12-22-22-11-02

2017-12-22-22-11-13

结论:当事务1隔离级别为Serializable时对后面的事务的增删改改操作进行强制排序。避免数据出错造成不必要的麻烦。

注:在.NET Core中IsolationLevel枚举值中还提供了另外三种隔离级别:ChaosSnapshotUnspecified由于这种事务隔离级别MySql不支持设置时会报异常:

2017-12-20-21-31-13

总结

本节通过Dapper对MySql中事务的四种隔离级别下进行测试,并且指出事务之间的相互关系和问题以供大家参考。

1、事务1隔离级别为ReadUncommitted时,可以读取其它任何事务隔离级别下未提交的数据

2、事务1隔离级别为ReadCommitted时,不可以读取其它事务未提交的数据,但是允许其它事务对数据表进行查询、添加、修改和删除;并且可以将其它事务增删改重新获取出来。

3、事务1隔离级别为RepeatableRead时,不可以读取其它事务未提交的数据,但是允许其它事务对数据表进行查询、添加、修改和删除;但是其它事务的增删改不影响事务1的查询结果

4、事务1隔离级别为Serializable时,对其它事务对数据库的修改(增删改)强制串行处理。

脏读 重复读 幻读
Read uncommitted
Read committed 不会
Repeatable read 不会 不会
Serializable 不会 不会 不会

作者:xdpie 出处:http://www.cnblogs.com/vipyoumay/p/8134434.html

以上内容有任何错误或不准确的地方请大家指正,不喜勿喷! 本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。如果觉得还有帮助的话,可以点一下右下角的【推荐】,希望能够持续的为大家带来好的技术文章!想跟我一起进步么?那就【关注】我吧。

Hadoop和大数据:60款顶级开源工具

说到处理大数据的工具,普通的开源解决方案(尤其是Apache Hadoop)堪称中流砥柱。弗雷斯特调研公司的分析师Mike Gualtieri最近预测,在接下来几年,“100%的大公司”会采用Hadoop。Market Research的一份报告预测,到2011年,Hadoop市场会以58%的年复合增长率(CAGR)高速增长;到2020年,市场产值会超过10亿美元。IBM更是非常看好开源大数据工具,派出了3500名研究人员开发Apache Spark,这个工具是Hadoop生态系统的一部分。

这回我们推出了最新的顶级开源大数据工具排行榜。这个领域最近方兴未艾,许多新项目纷纷启动。许多最知名的项目由Apache基金会管理,与Hadoop密切相关。

请注意:本文不是要搞什么排名;相反,项目按类别加以介绍。与往常一样,要是你知道另外的开源大数据及/或Hadoop工具应该榜上有名,欢迎留言交流。

一、Hadoop相关工具

1. Hadoop

Apache的Hadoop项目已几乎与大数据划上了等号。它不断壮大起来,已成为一个完整的生态系统,众多开源工具面向高度扩展的分布式计算。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://hadoop.apache.org

2. Ambari

作为Hadoop生态系统的一部分,这个Apache项目提供了基于Web的直观界面,可用于配置、管理和监控Hadoop集群。有些开发人员想把Ambari的功能整合到自己的应用程序当中,Ambari也为他们提供了充分利用REST(代表性状态传输协议)的API。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://ambari.apache.org

3. Avro

这个Apache项目提供了数据序列化系统,拥有丰富的数据结构和紧凑格式。模式用JSON来定义,它很容易与动态语言整合起来。

支持的操作系统:与操作系统无关。

相关链接: http://avro.apache.org

4. Cascading

Cascading是一款基于Hadoop的应用程序开发平台。提供商业支持和培训服务。

支持的操作系统:与操作系统无关。

相关链接: http://www.cascading.org/projects/cascading/

5. Chukwa

Chukwa基于Hadoop,可以收集来自大型分布式系统的数据,用于监控。它还含有用于分析和显示数据的工具。

支持的操作系统:Linux和OS X。

相关链接: http://chukwa.apache.org

6. Flume

Flume可以从其他应用程序收集日志数据,然后将这些数据送入到Hadoop。官方网站声称:“它功能强大、具有容错性,还拥有可以调整优化的可靠性机制和许多故障切换及恢复机制。”

支持的操作系统:Linux和OS X。

相关链接: https://cwiki.apache.org/confluence/display/FLUME/Home

7. HBase

HBase是为有数十亿行和数百万列的超大表设计的,这是一种分布式数据库,可以对大数据进行随机性的实时读取/写入访问。它有点类似谷歌的Bigtable,不过基于Hadoop和Hadoop分布式文件系统(HDFS)而建。

支持的操作系统:与操作系统无关。

相关链接: http://hbase.apache.org

8. Hadoop分布式文件系统(HDFS

HDFS是面向Hadoop的文件系统,不过它也可以用作一种独立的分布式文件系统。它基于Java,具有容错性、高度扩展性和高度配置性。

支持的操作系统:Windows、Linux和OS X。

相关链接: https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsUserGuide.html

9. Hive

Apache Hive是面向Hadoop生态系统的数据仓库。它让用户可以使用HiveQL查询和管理大数据,这是一种类似SQL的语言。

支持的操作系统:与操作系统无关。

相关链接: http://hive.apache.org

10. Hivemall

Hivemall结合了面向Hive的多种机器学习算法。它包括诸多高度扩展性算法,可用于数据分类、递归、推荐、k最近邻、异常检测和特征哈希。

支持的操作系统:与操作系统无关。

相关链接: https://github.com/myui/hivemall

11. Mahout

据官方网站声称,Mahout项目的目的是“为迅速构建可扩展、高性能的机器学习应用程序打造一个环境。”它包括用于在Hadoop MapReduce上进行数据挖掘的众多算法,还包括一些面向Scala和Spark环境的新颖算法。

支持的操作系统:与操作系统无关。

相关链接: http://mahout.apache.org

12. MapReduce

作为Hadoop一个不可或缺的部分,MapReduce这种编程模型为处理大型分布式数据集提供了一种方法。它最初是由谷歌开发的,但现在也被本文介绍的另外几个大数据工具所使用,包括CouchDB、MongoDB和Riak。

支持的操作系统:与操作系统无关。

相关链接: http://hadoop.apache.org/docs/current/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html

13. Oozie

这种工作流程调度工具是为了管理Hadoop任务而专门设计的。它能够按照时间或按照数据可用情况触发任务,并与MapReduce、Pig、Hive、Sqoop及其他许多相关工具整合起来。

支持的操作系统:Linux和OS X。

相关链接: http://oozie.apache.org

14. Pig

Apache Pig是一种面向分布式大数据分析的平台。它依赖一种名为Pig Latin的编程语言,拥有简化的并行编程、优化和可扩展性等优点。

支持的操作系统:与操作系统无关。

相关链接: http://pig.apache.org

15. Sqoop

企业经常需要在关系数据库与Hadoop之间传输数据,而Sqoop就是能完成这项任务的一款工具。它可以将数据导入到Hive或HBase,并从Hadoop导出到关系数据库管理系统(RDBMS)。

支持的操作系统:与操作系统无关。

相关链接: http://sqoop.apache.org

16. Spark

作为MapReduce之外的一种选择,Spark是一种数据处理引擎。它声称,用在内存中时,其速度比MapReduce最多快100倍;用在磁盘上时,其速度比MapReduce最多快10倍。它可以与Hadoop和Apache Mesos一起使用,也可以独立使用。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://spark.apache.org

17. Tez

Tez建立在Apache Hadoop YARN的基础上,这是“一种应用程序框架,允许为任务构建一种复杂的有向无环图,以便处理数据。”它让Hive和Pig可以简化复杂的任务,而这些任务原本需要多个步骤才能完成。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://tez.apache.org

18. Zookeeper

这种大数据管理工具自称是“一项集中式服务,可用于维护配置信息、命名、提供分布式同步以及提供群组服务。”它让Hadoop集群里面的节点可以彼此协调。

支持的操作系统:Linux、Windows(只适合开发环境)和OS X(只适合开发环境)。

相关链接: http://zookeeper.apache.org

二、大数据分析平台和工具

19. Disco

Disco最初由诺基亚开发,这是一种分布式计算框架,与Hadoop一样,它也基于MapReduce。它包括一种分布式文件系统以及支持数十亿个键和值的数据库。

支持的操作系统:Linux和OS X。

相关链接: http://discoproject.org

20. HPCC

作为Hadoop之外的一种选择,HPCC这种大数据平台承诺速度非常快,扩展性超强。除了免费社区版外,HPCC Systems还提供收费的企业版、收费模块、培训、咨询及其他服务。

支持的操作系统:Linux。

相关链接: http://hpccsystems.com

21. Lumify

Lumify归Altamira科技公司(以国家安全技术而闻名)所有,这是一种开源大数据整合、分析和可视化平台。你只要在Try.Lumify.io试一下演示版,就能看看它的实际效果。

支持的操作系统:Linux。

相关链接: http://www.jboss.org/infinispan.html

22. Pandas

Pandas项目包括基于Python编程语言的数据结构和数据分析工具。它让企业组织可以将Python用作R之外的一种选择,用于大数据分析项目。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://pandas.pydata.org

23. Storm

Storm现在是一个Apache项目,它提供了实时处理大数据的功能(不像Hadoop只提供批任务处理)。其用户包括推特、美国天气频道、WebMD、阿里巴巴、Yelp、雅虎日本、Spotify、Group、Flipboard及其他许多公司。

支持的操作系统:Linux。

相关链接: https://storm.apache.org

三、数据库/数据仓库

24. Blazegraph

Blazegraph之前名为“Bigdata”,这是一种高度扩展、高性能的数据库。它既有使用开源许可证的版本,也有使用商业许可证的版本。

支持的操作系统:与操作系统无关。

相关链接: http://www.systap.com/bigdata

25. Cassandra

这种NoSQL数据库最初由Facebook开发,现已被1500多家企业组织使用,包括苹果、欧洲原子核研究组织(CERN)、康卡斯特、电子港湾、GitHub、GoDaddy、Hulu、Instagram、Intuit、Netfilx、Reddit及其他机构。它能支持超大规模集群;比如说,苹果部署的Cassandra系统就包括75000多个节点,拥有的数据量超过10 PB。

支持的操作系统:与操作系统无关。

相关链接: http://cassandra.apache.org

26. CouchDB

CouchDB号称是“一款完全拥抱互联网的数据库”,它将数据存储在JSON文档中,这种文档可以通过Web浏览器来查询,并且用JavaScript来处理。它易于使用,在分布式上网络上具有高可用性和高扩展性。

支持的操作系统:Windows、Linux、OS X和安卓。

相关链接: http://couchdb.apache.org

27. FlockDB

由推特开发的FlockDB是一种非常快、扩展性非常好的图形数据库,擅长存储社交网络数据。虽然它仍可用于下载,但是这个项目的开源版已有一段时间没有更新了。

支持的操作系统:与操作系统无关。

相关链接: https://github.com/twitter/flockdb

28. Hibari

这个基于Erlang的项目自称是“一种分布式有序键值存储系统,保证拥有很强的一致性”。它最初是由Gemini Mobile Technologies开发的,现在已被欧洲和亚洲的几家电信运营商所使用。

支持的操作系统:与操作系统无关。

相关链接: http://hibari.github.io/hibari-doc/

29. Hypertable

Hypertable是一种与Hadoop兼容的大数据数据库,承诺性能超高,其用户包括电子港湾、百度、高朋、Yelp及另外许多互联网公司。提供商业支持服务。

支持的操作系统:Linux和OS X。

相关链接: http://hypertable.org

30. Impala

Cloudera声称,基于SQL的Impala数据库是“面向Apache Hadoop的领先的开源分析数据库”。它可以作为一款独立产品来下载,又是Cloudera的商业大数据产品的一部分。

支持的操作系统:Linux和OS X。

相关链接: http://www.cloudera.com/content/cloudera/en/products-and-services/cdh/impala.html

31. InfoBright社区版

InfoBright为数据分析而设计,这是一种面向列的数据库,具有很高的压缩比。InfoBright.com提供基于同一代码的收费产品,提供支持服务。

支持的操作系统:Windows和Linux。

相关链接: http://www.infobright.org

32. MongoDB

mongoDB的下载量已超过1000万人次,这是一种极其受欢迎的NoSQL数据库。MongoDB.com上提供了企业版、支持、培训及相关产品和服务。

支持的操作系统:Windows、Linux、OS X和Solaris。

相关链接: http://www.mongodb.org

33. Neo4j

Neo4j自称是“速度最快、扩展性最佳的原生图形数据库”,它承诺具有大规模扩展性、快速的密码查询性能和经过改进的开发效率。用户包括电子港湾、必能宝(Pitney Bowes)、沃尔玛、德国汉莎航空公司和CrunchBase。

支持的操作系统:Windows和Linux。

相关链接: http://neo4j.org

34. OrientDB

这款多模型数据库结合了图形数据库的一些功能和文档数据库的一些功能。提供收费支持、培训和咨询等服务。

支持的操作系统:与操作系统无关。

相关链接: http://www.orientdb.org/index.htm

35. Pivotal Greenplum Database

Pivotal声称,Greenplum是“同类中最佳的企业级分析数据库”,能够非常快速地对庞大的海量数据进行功能强大的分析。它是Pivotal大数据库套件的一部分。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://pivotal.io/big-data/pivotal-greenplum-database

36. Riak

Riak“功能完备”,有两个版本:KV是分布式NoSQL数据库,S2提供了面向云环境的对象存储。它既有开源版,也有商业版,还有支持Spark、Redis和Solr的附件。

支持的操作系统:Linux和OS X。

相关链接: http://basho.com/riak-0-10-is-full-of-great-stuff/

37. Redis

Redis现在由Pivotal赞助,这是一种键值缓存和存储系统。提供收费支持。要注意:虽然该项目并不正式支持Windows,不过微软在GitHub上有一个Windows派生版。

支持的操作系统:Linux。

相关链接: http://redis.io

四、商业智能

38. Talend Open Studio

Talend的下载量已超过200万人次,其开源软件提供了数据整合功能。该公司还开发收费的大数据、云、数据整合、应用程序整合和主数据管理等工具。其用户包括美国国际集团(AIG)、康卡斯特、电子港湾、通用电气、三星、Ticketmaster和韦里逊等企业组织。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://www.talend.com/index.php

39. Jaspersoft

Jaspersoft提供了灵活、可嵌入的商业智能工具,用户包括众多企业组织:高朋、冠群科技、美国农业部、爱立信、时代华纳有线电视、奥林匹克钢铁、内斯拉斯加大学和通用动力公司。除了开源社区版外,它还提供收费的报表版、亚马逊网络服务(AWS)版、专业版和企业版。

支持的操作系统:与操作系统无关。

相关链接: http://www.jaspersoft.com

40. Pentaho

Pentaho归日立数据系统公司所有,它提供了一系列数据整合和业务分析工具。官方网站上提供了三个社区版;访问Pentaho.com,即可了解收费支持版方面的信息。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://community.pentaho.com

41. SpagoBI

Spago被市场分析师们称为“开源领袖”,它提供商业智能、中间件和质量保证软件,另外还提供Java EE应用程序开发框架。该软件百分之分免费、开源,不过也提供收费的支持、咨询、培训及其他服务。

支持的操作系统:与操作系统无关。

相关链接: http://www.spagoworld.org/xwiki/bin/view/SpagoWorld/

42. KNIME

KNIME的全称是“康斯坦茨信息挖掘工具”(Konstanz Information Miner),这是一种开源分析和报表平台。提供了几个商业和开源扩展件,以增强其功能。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://www.knime.org

43. BIRT

BIRT的全称是“商业智能和报表工具”。它提供的一种平台可用于制作可以嵌入到应用程序和网站中的可视化元素及报表。它是Eclipse社区的一部分,得到了Actuate、IBM和Innovent Solutions的支持。

支持的操作系统:与操作系统无关。

相关链接: http://www.eclipse.org/birt/

五、数据挖掘

44.DataMelt

作为jHepWork的后续者,DataMelt可以处理数学运算、数据挖掘、统计分析和数据可视化等任务。它支持Java及相关的编程语言,包括Jython、Groovy、JRuby和Beanshell。

支持的操作系统:与操作系统无关。

相关链接: http://jwork.org/dmelt/

45. KEEL

KEEL的全称是“基于进化学习的知识提取”,这是一种基于Java的机器学习工具,为一系列大数据任务提供了算法。它还有助于评估算法在处理递归、分类、集群、模式挖掘及类似任务时的效果。

支持的操作系统:与操作系统无关。

相关链接: http://keel.es

46. Orange

Orange认为数据挖掘应该是“硕果累累、妙趣横生”,无论你是有多年的丰富经验,还是刚开始接触这个领域。它提供了可视化编程和Python脚本工具,可用于数据可视化和分析。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://orange.biolab.si

47. RapidMiner

RapidMiner声称拥有250000多个用户,包括贝宝、德勤、电子港湾、思科和大众。它提供一系列广泛的开源版和收费版,不过要注意:免费的开源版只支持CSV格式或Excel格式的数据。

支持的操作系统:与操作系统无关。

相关链接: https://rapidminer.com

48. Rattle

Rattle的全称是“易学易用的R分析工具”。它为R编程语言提供了一种图形化界面,简化了这些过程:构建数据的统计或可视化摘要、构建模型以及执行数据转换。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://rattle.togaware.com

49. SPMF

SPMF现在包括93种算法,可用于顺序模式挖掘、关联规则挖掘、项集挖掘、顺序规则挖掘和集群。它可以独立使用,也可以整合到其他基于Java的程序中。

支持的操作系统:与操作系统无关。

相关链接: http://www.philippe-fournier-viger.com/spmf/

50. Weka

怀卡托知识分析环境(Weka)是一组基于Java的机器学习算法,面向数据挖掘。它可以执行数据预处理、分类、递归、集群、关联规则和可视化。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://www.cs.waikato.ac.nz/~ml/weka/

六、查询引擎

51. Drill

这个Apache项目让用户可以使用基于SQL的查询,查询Hadoop、NoSQL数据库和云存储服务。它可用于数据挖掘和即席查询,它支持一系列广泛的数据库,包括HBase、MongoDB、MapR-DB、HDFS、MapR-FS、亚马逊S3、Azure Blob Storage、谷歌云存储和Swift。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://drill.apache.org

七、编程语言

52. R

R类似S语言和环境,旨在处理统计计算和图形。它包括一套整合的大数据工具,可用于数据处理、计算和可视化。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://www.r-project.org

53. ECL

企业控制语言(ECL)是开发人员用来在HPCC平台上构建大数据应用程序的语言。HPCC Systems官方网站上有集成开发环境(IDE)、教程以及处理该语言的众多相关工具。

支持的操作系统:Linux。

相关链接: http://hpccsystems.com/download/docs/ecl-language-reference

八、大数据搜索

54. Lucene

基于Java的Lucene可以非常迅速地执行全文搜索。据官方网站声称,它在现代硬件上每小时能够检索超过150GB的数据,它含有强大而高效的搜索算法。开发工作得到了Apache软件基金会的赞助。

支持的操作系统:与操作系统无关。

相关链接: http://lucene.apache.org/core/

55. Solr

Solr基于Apache Lucene,是一种高度可靠、高度扩展的企业搜索平台。知名用户包括eHarmony、西尔斯、StubHub、Zappos、百思买、AT&T、Instagram、Netflix、彭博社和Travelocity。

支持的操作系统:与操作系统无关。

相关链接: http://lucene.apache.org/solr/

九、内存中技术

56. Ignite

这个Apache项目自称是“一种高性能、整合式、分布式的内存中平台,可用于对大规模数据集执行实时计算和处理,速度比传统的基于磁盘的技术或闪存技术高出好几个数量级。”该平台包括数据网格、计算网格、服务网格、流媒体、Hadoop加速、高级集群、文件系统、消息传递、事件和数据结构等功能。

支持的操作系统:与操作系统无关。

相关链接: https://ignite.incubator.apache.org

57. Terracotta

Terracotta声称其BigMemory技术是“世界上数一数二的内存中数据管理平台”,声称拥有210万开发人员,250家企业组织部署了其软件。该公司还提供商业版软件,另外提供支持、咨询和培训等服务。

支持的操作系统:与操作系统无关。

相关链接: http://www.terracotta.org

58. Pivotal GemFire/Geode

今年早些时候,Pivotal宣布它将开放其大数据套件关键组件的源代码,其中包括GemFire内存中NoSQL数据库。它已向Apache软件基金会递交了一项提案,以便在“Geode”的名下管理GemFire数据库的核心引擎。还提供该软件的商业版。

支持的操作系统:Windows和Linux。

相关链接: http://pivotal.io/big-data/pivotal-gemfire

59. GridGain

由Apache Ignite驱动的GridGrain提供内存中数据结构,用于迅速处理大数据,还提供基于同一技术的Hadoop加速器。它既有收费的企业版,也有免费的社区版,后者包括免费的基本支持。

支持的操作系统:Windows、Linux和OS X。

相关链接: http://www.gridgain.com

60. Infinispan

作为一个红帽JBoss项目,基于Java的Infinispan是一种分布式内存中数据网格。它可以用作缓存、用作高性能NoSQL数据库,或者为诸多框架添加集群功能。

支持的操作系统:与操作系统无关。

相关链接: http://www.jboss.org/infinispan.html

转载自:   http://os.51cto.com/art/201508/487936_all.htm 译者: 布加迪

更多大数据与分析相关行业资讯、解决方案、案例、教程等请点击查看>>>

详情请咨询在线客服

客服热线:023-66090381

基于Redis的限流系统的设计

本文讲述基于Redis的限流系统的设计,主要会谈及限流系统中限流策略这个功能的设计;在实现方面,算法使用的是令牌桶算法来,访问Redis使用lua脚本。

1、概念

In computer networks, rate limiting is used to control the rate of traffic sent or received by a network interface controller and is used to prevent DoS attacks

用我的理解翻译一下:限流是对系统的出入流量进行控制,防止大流量出入,导致资源不足,系统不稳定。

限流系统是对资源访问的控制组件,控制主要的两个功能:限流策略熔断策略,对于熔断策略,不同的系统有不同的熔断策略诉求,有的系统希望直接拒绝、有的系统希望排队等待、有的系统希望服务降级、有的系统会定制自己的熔断策略,很难一一列举,所以本文只针对限流策略这个功能做详细的设计。

针对限流策略这个功能,限流系统中有两个基础概念:资源和策略。

  • 资源 :或者叫稀缺资源,被流量控制的对象;比如写接口、外部商户接口、大流量下的读接口
  • 策略 :限流策略由限流算法和可调节的参数两部分组成

熔断策略:超出速率阈值的请求处理策略,是我自己理解的一个叫法,不是业界主流的说法。

2、限流算法

  • 限制瞬时并发数
  • 限制时间窗最大请求数
  • 令牌桶

2.1、限制瞬时并发数

定义:瞬时并发数,系统同时处理的请求/事务数量

优点:这个算法能够实现控制并发数的效果

缺点:使用场景比较单一,一般用来对入流量进行控制

java伪代码实现

AtomicInteger atomic = new AtomicInteger(1)
try {    
    if(atomic.incrementAndGet() > 限流数) {   
        //熔断逻辑
    } else {
        //处理逻辑
    } 
} finally {
    atomic.decrementAndGet();
}

2.2、限制时间窗最大请求数

定义:时间窗最大请求数,指定的时间范围内允许的最大请求数

优点:这个算法能够满足绝大多数的流控需求,通过时间窗最大请求数可以直接换算出最大的QPS(QPS = 请求数/时间窗)

缺点:这种方式可能会出现流量不平滑的情况,时间窗内一小段流量占比特别大

lua代码实现

--- 资源唯一标识
local key = KEYS[1]
--- 时间窗最大并发数
local max_window_concurrency = tonumber(ARGV[1])  
--- 时间窗
local window = tonumber(ARGV[2])   
--- 时间窗内当前并发数
local curr_window_concurrency = tonumber(redis.call('get', key) or 0)  
if current + 1 > limit then
    return false
else
    redis.call("INCRBY", key,1)    
    if window > -1 then
        redis.call("expire", key,window)    
    end
    return true
end

2.3、令牌桶

算法描述

  • 假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中
  • 假设桶中最多可以存放b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃
  • 当流量以速率v进入,从桶中以速率v取令牌,拿到令牌的流量通过,拿不到令牌流量不通过,执行熔断逻辑

属性

  • 长期来看,符合流量的速率是受到令牌添加速率的影响,被稳定为:r
  • 因为令牌桶有一定的存储量,可以抵挡一定的流量突发情况
    • M是以字节/秒为单位的最大可能传输速率:M>r
    • T max = b/(M-r)    承受最大传输速率的时间
    • B max = T max * M   承受最大传输速率的时间内传输的流量

优点:流量比较平滑,并且可以抵挡一定的流量突发情况

因为我们限流系统的实现就是基于令牌桶这个算法,具体的代码实现参考下文。

3、工程实现

3.1、技术选型

  • mysql:存储限流策略的参数等元数据
  • redis+lua:令牌桶算法实现

说明:因为我们把redis 定位为:缓存、计算媒介,所以元数据都是存在db中

3.2、架构图

3.3、 数据结构

字段 描述
name 令牌桶的唯一标示
apps 能够使用令牌桶的应用列表
max_permits 令牌桶的最大令牌数
rate 向令牌桶中添加令牌的速率
created_by 创建人
updated_by 更新人

限流系统的实现是基于redis的,本可以和应用无关,但是为了做限流元数据配置的统一管理,按应用维度管理和使用,在数据结构中加入了apps这个字段,出现问题,排查起来也比较方便。

3.4、代码实现

3.4.1、代码实现遇到的问题

参考令牌桶的算法描述,一般思路是在RateLimiter-client放一个重复执行的线程,线程根据配置往令牌桶里添加令牌,这样的实现由如下缺点:

  • 需要为每个令牌桶配置添加一个重复执行的线程
  • 重复的间隔精度不够精确:线程需要每1/r秒向桶里添加一个令牌,当r>1000 时间线程执行的时间间隔根本没办法设置(从后面性能测试的变现来看RateLimiter-client 是可以承担 QPS > 5000 的请求速率)

3.4.2、解决方案

基于上面的缺点,参考了google的guava中RateLimiter中的实现,我们使用了触发式添加令牌的方式。

算法描述

  • 基于上述的令牌桶算法
  • 将添加令牌改成触发式的方式,取令牌的是做添加令牌的动作
  • 在去令牌的时候,通过计算上一次添加令牌和当前的时间差,计算出这段间应该添加的令牌数,然后往桶里添加
    • curr_mill_second = 当前毫秒数
    • last_mill_second = 上一次添加令牌的毫秒数
    • r = 添加令牌的速率
    • reserve_permits = (curr_mill_second-last_mill_second)/1000 * r
  • 添加完令牌之后再执行取令牌逻辑

3.4.3、 lua代码实现

--- 获取令牌
--- 返回码
--- 0 没有令牌桶配置
--- -1 表示取令牌失败,也就是桶里没有令牌
--- 1 表示取令牌成功
--- @param key 令牌(资源)的唯一标识
--- @param permits  请求令牌数量
--- @param curr_mill_second 当前毫秒数
--- @param context 使用令牌的应用标识
local function acquire(key, permits, curr_mill_second, context)
    local rate_limit_info = redis.pcall("HMGET", key, "last_mill_second", "curr_permits", "max_permits", "rate", "apps")    
    local last_mill_second = rate_limit_info[1]    
    local curr_permits = tonumber(rate_limit_info[2])    
    local max_permits = tonumber(rate_limit_info[3])    
    local rate = rate_limit_info[4]    
    local apps = rate_limit_info[5]    
    --- 标识没有配置令牌桶
    if type(apps) == 'boolean' or apps == nil or not contains(apps, context) then
        return 0
    end
    local local_curr_permits = max_permits;    
    --- 令牌桶刚刚创建,上一次获取令牌的毫秒数为空
    --- 根据和上一次向桶里添加令牌的时间和当前时间差,触发式往桶里添加令牌
    --- 并且更新上一次向桶里添加令牌的时间
    --- 如果向桶里添加的令牌数不足一个,则不更新上一次向桶里添加令牌的时间
    if (type(last_mill_second) ~= 'boolean' and last_mill_second ~= false and last_mill_second ~= nil) then
        local reverse_permits = math.floor(((curr_mill_second - last_mill_second) / 1000) * rate)        
        local expect_curr_permits = reverse_permits + curr_permits;
        local_curr_permits = math.min(expect_curr_permits, max_permits);       
         --- 大于0表示不是第一次获取令牌,也没有向桶里添加令牌
        if (reverse_permits > 0) then
            redis.pcall("HSET", key, "last_mill_second", curr_mill_second)       
      end
    else
        redis.pcall("HSET", key, "last_mill_second", curr_mill_second)   
    end
    local result = -1
    if (local_curr_permits - permits >= 0) then
        result = 1
        redis.pcall("HSET", key, "curr_permits", local_curr_permits - permits)    
    else
        redis.pcall("HSET", key, "curr_permits", local_curr_permits)    
    end
    return result
end

关于限流系统的所有实现细节,我都已经放到github上,gitbub地址:https://github.com/wukq/rate-limiter,有兴趣的同学可以前往查看,由于笔者经验与知识有限,代码中如有错误或偏颇,欢迎探讨和指正。

3.4.4、管理界面

前面的设计中,限流的配置是和应用关联的,为了更够更好的管理配置,需要一个统一的管理页面去对配置进行管控:

  • 按应用对限流配置进行管理
  • 不同的人分配不同的权限;相关人员有查看配置的权限,负责人有修改和删除配置的权限

3.5、性能测试

配置:aws-elasticcache-redis 2核4g

因为Ratelimiter-client的功能比较简单,基本上是redis的性能打个折扣。

  • 单线程取令牌:Ratelimiter-client的 QPS = 250/s
  • 10个线程取令牌:Ratelimiter-client的 QPS = 2000/s
  • 100个线程取令牌:Ratelimiter-client的 QPS = 5000/s

4、总结

限流系统从设计到实现都比较简单,但是确实很实用,用四个字来形容就是:短小强悍,其中比较重要的是结合公司的权限体系和系统结构,设计出符合自己公司规范的限流系统。

不足

  • redis 我们用的是单点redis,只做了主从,没有使用redis高可用集群(可能使用redis高可用集群,会带来新的问题)
  • 限流系统目前只做了应用层面的实现,没有做接口网关上的实现
  • 熔断策略需要自己定制,如果实现的好一点,可以给一些常用的熔断策略模板

参考书籍:

1.《Redis 设计与实现》
2.《Lua编程指南》

参考文章:

1. redis官网

2. lua编码规范

3. 聊聊高并发系统之限流特技

4. guava Ratelimiter 实现

5. Token_bucket wiki 词条

Web网页爬虫对抗指南 Part.1

从本质上来讲,阻止Web 爬虫就意味着你需要让脚本和机器难以从你的网站上获取它们所需的数据,但不能让那些真正的用户和搜索引擎爬取数据变得困难。

然而不幸的是,要做到这一点很难,你需要在防止Web 爬虫和降级真实用户和搜索引擎的可访问性之间进行权衡。

为了阻止Web 爬虫(也称为Webscraping,Screenscraping,Web数据挖掘,Web收集或Web数据提取),你需要了解这些爬虫的工作原理,以及它们为什么能够运行良好的原因,本文的内容将会告诉你阻止爬虫爬取数据的答案。

一般来说,这些爬虫程序是为了从你的网站提取特定的信息,如文章,搜索结果,产品详细信息,或你的案例中爬取艺术家和专辑等信息。通常,人们使用爬虫爬取特定的数据的目的是将这些数据在自己的网站上进行重用(并从你的内容中赚自己的钱),或为你的网站(如移动应用程序)构建一种“假冒的”替代方案,甚至用于私人研究或分析目的。

基本上,网络中存在着各种类型的爬虫,每一种爬虫的工作方式都不尽相同:

· 蜘蛛(Spider),例如谷歌的机器人或网站复制工具,比如HTTrack,这中爬虫会访问你的网站,并递归地跟随到其他页面的链接,以获取数据。这些爬虫有时用于定向的爬取来获取特定的数据,通常与HTML解析器结合,然后从每个页面中提取所需要的数据。

· Shell脚本:有时候,常用的Unix工具也用于爬取数据,例如:使用shell脚本执行Wget或Curl下载Web页面,通常再结合Grep(使用正则表达式)提取所需的数据。这些是最简单的爬虫,也是最脆弱的一种爬虫(不要尝试使用正则表达式解析HTML!)。

· HTML爬虫和解析器,例如基于JsoupScrapy等等的爬虫技术。这类爬虫有点类似于基于shell脚本的正则表达式的爬虫,都是通过基于在HTML中进行正则匹配提取你的页面中的数据,并忽略其他的内容。

所以,如果你的网站具有搜索功能,那么这样的爬虫可能会提交一个搜索的HTTP请求,然后从结果页的HTML中获取所有结果的链接及其标题,有时候会发起请求数百次和数百种不同的搜索行为,以便专门获取搜索结果链接和网页标题。这些爬虫是最常见的一种。

·  屏幕爬虫,这类爬虫是基于例如 SeleniumPhantomJS编写的。实际上这种爬虫是在真正的浏览器中打开了你的网站,并运行JavaScript,AJAX等等,然后再从网页中获取所需的文本,一般的做法如下:

o   在页面加载和JavaScript运行之后,从浏览器中获取HTML,然后使用HTML解析器来提取所需的数据或文本。这些是最常见的爬取数据的手法,这些方法在HTML解析器和其他的爬虫方法不能用的情况下也有效。

o   对渲染页面进行屏幕截图,然后使用OCR技术从屏幕截图中提取所需的文本。这中方法比较罕见,只有在爬取数据没有办法的情况下才会使用这种专用的爬虫。

基于浏览器的屏幕爬虫更难处理,因为它们运行脚本,渲染HTML,并且可以像一个真正的人在浏览你的网站一样。

· 在线的Web爬虫服务,如ScrapingHubKimono。事实上,有些人的工作就是弄清楚如何抓取你的网站的数据,并提出内容给别人使用。这类爬虫技术,有时候会使用庞大的代理网络和不断变化的IP地址来突破限制和阻止,所以它们是问题比较严重的一类爬虫。

专业的爬虫服务是最难以阻止的,这不需要感到奇怪,但是如果你弄清楚Web 爬虫是如何爬取你的网站的,那么这些爬虫(以及为这类爬虫服务付款方式的人)可能不会因为你的网站的数据而遭到困扰了。

· 将你的网站使用框架嵌入其他网站的网页中,并将你的网站嵌入到移动应用中。

虽然在技术上来说这种手法不算是爬虫,但这的确也算是一个问题,因为移动应用程序(Android和iOS)可以嵌入你的网站,甚至注入自定义的CSS和JavaScript,从而完全改变你的网站的外观,并且只显示所需的信息,如文章内容本身或搜索结果列表,以及隐藏一些东西,如页眉,页脚或广告。

· 人工复制和粘贴:网站访客会复制和粘贴你的网站的内容,以便在其他地方使用。不幸的是,要阻止这种方法你基本上无能为力。

这些不同类型的爬虫之间存在很多重叠的地方,即使使用不同的技术和方法来获取你的内容,许多爬虫也将表现出相似的行为。

以上这些提示信息大部分是我自己的想法,包括我在编写爬虫时遇到的各种困难,以及来自互联网上的一些信息和想法。

如何阻止爬虫

一些检测和阻止爬虫的常见方法:

监控你的日志和流量模式; 如果发现异常活动,则限制访问:

定期检查你的日志,如果发现了一些自动访问(爬虫)的异常活动(例如来自同一个IP地址的许多类似的请求操作),那么你可以阻止或限制这些IP地址的访问。

具体来说,限制措施如下:

· 访问速率限制:

只允许用户(和爬虫)在一定时间内执行有限数量的操作 – 例如,只允许任何特定的IP地址或用户每秒进行几次搜索。这样就会减慢爬虫的使用,使爬取功能逐渐无效。如果操作的完成速度比实际用户要快得多或更快,你也可以显示一个验证码来阻止爬虫。

· 检测异常活动:

如果你发现了异常活动,例如来自某个特定的IP地址的许多类似的请求,或者是某个用户浏览了过多的页面或执行的搜索次数不同寻常,那么也可以阻止这类IP或用户的访问,或者是在后续的请求中显示验证码。

· 不要只是通过IP地址监控和限制访问速率 —— 也可以使用其他指标:

如果你阻止或限制访问速率,请不要仅在每个IP地址的基础上进行; 你可以使用其他指标和方法来识别特定的用户或爬虫。下面包括了一些可以帮助你识别特定用户/爬虫的指标:

o   用户填写表单的速度以及他们点击的按钮的位置;

o   你可以使用JavaScript收集客户端的大量信息,例如屏幕尺寸/分辨率,时区,已安装的字体等; 你可以使用这些信息来识别用户。

o   Http协议头及其顺序,尤其是User-Agent。

例如,如果你发现从单个IP地址上发出了许多请求,则所有使用相同的User agent,屏幕尺寸(用JavaScript确定)的用户(此时应该是爬虫)始终以相同的方式和相同的时间间隔定期点击按钮,那么它可能是一个屏幕爬虫; 你可以暂时阻止类似的请求(例如,阻止来自特定的IP地址中相同的user agent和屏幕大小的所有请求),这样你就不会对该IP地址上的真实用户造成影响,例如:在共享互联网连接的情况下。

你还可以进一步的阻止爬虫,因为你可以识别类似的请求,如果这些请求来自不同的IP地址,那么意味着这可能是一个分布式Web 爬虫(使用僵尸网络或代理网络的爬虫)。如果你发现了还有很多相同的其他请求,但是它们来自不同的IP地址,那么你也可以阻止IP地址的访问。再次提醒一下,不要在无意中阻止了真实的用户。

这可以对运行JavaScript的屏幕截图类的爬虫很有效,因为你可以从中获取大量的信息。

Security Stack Exchange上面与此有关的相关问题:

· 如何识别出口IP地址相同的用户?

· 为什么人们在IP地址经常变化时使用IP地址黑名单?

· 请使用验证码替代临时阻止访问的方案:

实现访问速率限制的简单方法是在一段时间内临时阻止访问,但是使用验证码可能会更好一些,详细内容请参阅“验证码”部分。

要求注册和登录

如果你的网站可以做到需要创建帐户才能查看你的网站的内容的话,那么这对爬虫来说可能是一个很好的威慑,但对真实用户来说也可能是一个很好的威慑。

· 如果你要求客户端创建帐户和登录,那么你可以准确的跟踪用户和爬虫的操作。这样的话,你就可以轻松的检测某些帐户是何时被用于爬虫的,然后禁止掉这些用户。访问速率限制或检测滥用(例如在短时间内的大量搜索请求)也会变得更加容易,因为你可以识别特定的爬虫而不仅仅是识别出IP地址。

为了避免自动化创建许多帐户的脚本,你应该做以下这些事情:

· 注册时需要一个电子邮件地址,并且用户必须打开通过你所发送的链接来验证该电子邮件地址才能激活该帐户。每个电子邮件地址只允许一个帐户。

· 需要在注册/帐户创建期间显示验证码,以防止自动化脚本创建帐户。

要求创建帐户才能查看内容的做法会导致用户和搜索引擎不能正常访问; 如果你需要创建帐户才能查看文章,那么用户将会去其他网站,而不是去注册一个账户然后再查看文章。

阻止来自云托管主机和爬虫服务的IP地址的访问

有时,爬虫会从网络托管服务(如Amazon Web Services或Google App Engine)或VPSes运行。限制(或显示验证码)来源于此类云主机服务使用的IP地址请求访问你的网站。你还可以阻止从爬虫服务使用的IP地址的访问。

同样,你还可以限制代理或VPN提供商使用的IP地址的访问,因为爬虫可能会使用此类代理服务器来避免单个IP发出许多请求。

请注意,通过阻止代理服务器和VPN的访问,你将对真实用户产生负面的影响。

如果你要采取阻止访问的做法,请将你的错误消息进行调整

如果你采取了阻止/限制访问的手段,那么你应该确保不要告诉爬虫导致阻塞的实际原因,从而给爬虫制作者如何修复爬虫的线索。所以用文本显示错误页面可能是一个坏主意:

· 你的IP地址请求过于频繁,请稍后再试。

· 错误,User Agent不存在,非法访问!

相反,显示一个友好的错误消息,不要告诉爬虫是什么原因造成不能正常访问的。

· 抱歉,网站出了一些问题。如果问题仍然存在,你可以联系helpdesk@example.com。

这也是真正对用户友好的一种显示内容。你还应该考虑在后续的请求中不要直接显示验证码,以防真实用户看到这个错误消息,导致你阻止了合法用户的访问行为。

如果你的网站被爬虫访问,请使用Captchas。

Captchas(“完全自动化测试以分辨电脑和人类”)对于阻止爬虫非常有效。不幸的是,这种方式对正常用户的请求有干扰。

因此,当你怀疑你的网站可能被爬了,那么这种方式是非常有用的,因为这种方法只是阻止了Web 爬虫,而不会阻止真实的正常用户的访问。

使用Captchas时需要注意下面几个事情:

·  不要自己实现验证码,应该使用像Google的reCaptcha这样的东西:这比自己实现一个验证码要容易得多,它比一些模糊和扭曲的文本解决方案对用户来说更友好,而且比起自己实现验证码,它能更好的解决问题

·  不要在HTML标记中包含验证码的解决方案:实际上我已经看到一个网站在页面中嵌入了验证码的解决方案(虽然隐藏的很好)。但是不要这样做。还是推荐使用像reCaptcha这样的服务,如果你正确使用它,就不会出现问题。

· Captchas是可以批量验证的:在网上有一些低报酬的人工打码服务可以批量解决验证码的验证服务。所以,我在这里还是推荐使用reCaptcha,因为它具有很好的自我保护作用。这种服务不太可能被用于人工打码,除非你的数据真的很有价值。

将你的文本内容转成图片

你可以在服务器端将文本转换成图像然后在客户端显示,这种方法可以阻碍简单的爬虫提取文本。

然而,这对于屏幕阅读器,搜索引擎,性能以及其他的一些事情都是不利的。在某些方面(比如无障碍操作,例如美国残疾人法案)也可能是非法的,并且也很容易被一些OCR技术绕过,所以最好还是不要这样做。

你可以做类似于CSS精灵的东西,但是也同样会遇到上面的问题。

不要公开你的完整数据集:

如果可行的话,请不要为脚本或爬虫机器人提供所有数据集的方法。例如:你有一个新闻网站,有很多单独的文章。你可以通过现场搜索来搜索这些文章,并且如果你没有列出任何网站上的所有文章及其URL,那么这些文章将只能通过使用搜索来访问特征。这意味着一个想要从你的网站上获得所有文章的脚本将不得不搜索可能出现在你的文章中的所有可能的短语,才能找到文章的全部内容,这对于爬虫来说是很耗时的,而且效率低的可怕,所以爬虫一般都会放弃爬取数据。

如果是以下这些情况,那么这种方法将变得无效:

· 爬虫机器人或脚本并不想或需要完整的数据集。

· 你的文章的URL是一个看起来像 example.com/article.php?articleId=12345这种(和类似的东西)的格式,这将允许爬虫可以简单地迭代所有articleId的文章,并请求所有的文章内容。

· 还有其他方式可以最终找到所有的文章,比如通过编写一个脚本来跟踪其他文章的内容中的链接。

· 搜索“&”或“”的东西可以显示几乎所有的东西,这是一些需要注意的事情。(你只能返回前10或20个结果来避免这种情况)。

· 你需要搜索引擎来查找你的内容。

本文翻译自:https://github.com/JonasCz/How-To-Prevent-Scraping/blob/master/README.md ,如若转载,请注明原文地址: http://www.4hou.com/technology/8482.html?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io

开源项目挣钱实用手册

我列出了我从各种渠道所知道的开源项目带来收入的人们的故事,每种出资类型都有一系列的真实案例,粗略的按照出资量从小到大排列。(我已尽可能的将链接指向具体故事而非主页面)

本文中的出资类型并非互斥的,例如一个项目既可以由基金会也可以通过众筹来筹集资金,而一个人既可以靠咨询挣钱也可以获得捐赠。本文的主要目的是提供一个详尽的挣钱方式列表,而你只需要从中选出适合你的。

本中文版是原版的翻译版本。

原项目名称Lemonade Stand是指销售柠檬汁的小摊,而在美国,这种小摊通常是由小朋友运作的。


目录

  1. 捐赠按钮
  2. 悬赏
  3. 众筹(一次性)
  4. 众筹(持续性)
  5. 卖书及周边
  6. 广告
  7. 受雇于公司并继续你的项目
  8. 在职时启动项目
  9. 补贴
  10. 咨询服务
  11. SaaS
  12. 双重协议
  13. 开放核心
  14. 基金会
  15. 风险投资

附录: 贡献 // 协议

“个人努力” 用来标记其资金是由个人而非项目主导筹集获得的

捐赠按钮

在你的网站页面里放上捐赠按钮。Stripe和PayPal都可以很方便的提供这项服务。

优点

  • 限制条件少
  • 工作量小:放好后就可以不管了

缺点

  • 除非你努力筹款,通常都不会有太多钱
  • 需要一个法人实体来接受捐赠(SFCOpenCollective可在这方面提供帮助),因此较难管理个人的国际性捐赠
  • 在多人项目中很难明确如何分配这笔捐赠

案例学习

悬赏

项目或公司可能时不时的张贴开源项目的悬赏工作(例如“修漏洞赚$100”),下面列出了一些网站,他们收集和发布了这类悬赏工作。

优点

  • 参与到开源社区
  • 确定的工作有确定的回报
  • 在修复安全漏洞方面特别流行

缺点

  • 会在项目中产生不合理的激励机制(低质量的PR也会影响项目的专注性)
  • 通常没多少钱 (~<$500)
  • 无法提供持久的收入

案例学习

众筹(一次性)

如果你想实现一个特别的想法(区别于长期项目),一次性的众筹活动可以帮助你筹集到你需要的资金,许多个人和公司都可能会为你的想法捐款。

优点

  • 限制条件少
  • 对个人来说也可以很容易合法的进行,例如通过Kickstarter

缺点

  • 一大堆的市场工作需要做
  • 通常都需要为捐赠者提供回报甚至是特权
  • 通常钱也不是那么多(一次大约$50K)
  • 公司并不常愿意向众筹捐款

案例学习

众筹(持续性)

如果你想要为持续性的项目筹集资金,可以设立一个持续性的众筹,捐赠者承诺按月或按年提供资金直到捐赠者退出。对于那些经常使用你的项目的个人或公司可能会愿意为你的项目提供资金。

优点

缺点

  • 很难获得承诺的持续性捐赠(这通常需要你或项目已经有一定的名声)
  • 很难解释持续性的捐赠能获得什么样明确的回报或特权
  • 通常钱也不是那么多(一个月$1-4K)

案例学习

卖书及周边

如果你是某个领域的专家,你可以写书卖书,可以找个出版社(像O’Reilly)或自己出版(译者注:在中国不行)。除了卖书之外,有些项目也卖短袖外套等。

优点

  • 筹集到的资金并不和项目本身管理关联,所以项目本身可以保持创作的自由
  • 销售的商品本身也可以当做为项目做的宣传
  • 在初次销售后可以成为持续性的资金来源

缺点

  • 通常钱也不是那么多
  • 会影响用在项目上的精力
  • 卖周边需要准备预付资金

案例学习

广告

如果你的项目已经有了一定的受众,你可以帮助广告商向你的受众推销。通常你的项目都会有明确的受众,这是你的优势也是广告商所喜欢的。(比如你有一个Python项目,那么基本上可以假定你的受众一定是技术上熟悉Python的)

优势

  • 这是成熟明确而且大众也能接受的商业模式

缺点

  • 需要足够多的受众才能请来广告商
  • 需要通过透明化来让受众相信你(比如让其信任你不会追踪他们)
  • 需要许多精力来寻找和管理广告商

案例学习

受雇于公司并继续你的项目

公司有时候会雇佣一些个人来做开源项目,你可以寻找一个正在使用你的开源项目的公司。当然具体在公司里可能公司工作和开源项目工作时间会是五五分。除此之外,也可以找一个愿意尝试使用你的新项目的公司。如果你有展示项目经验,这将会非常有用。

优点

  • 可以利用公司的资源
  • 可以很好的和公司的需求保持一致
  • 稳定的收入

缺点

  • 获得这样的机会需要极好的运气,现目前没有明确可重复的方式获得这样的机会
  • 项目通常需要非常出名并且被使用
  • 对于没有为公司的利润工作,这使得个人很容易被公司优先舍弃
  • 公司可能会过分影响项目的发展
  • 可能会因平衡不好两边而影响项目

案例学习

在职时启动项目

许多开源项目最初都是员工的编外项目(Side Project),即便其最终可能会成长为一家公司,但以编外项目的形式在公司里进行孵化应该是不错的选择。

如果你想走这条路,请确定你理解了公司在开源项目上的政策。有些公司鼓励员工在工作时间从事开源项目开发,而有些则将你的任何工作视作公司项目。不要假定任何前提,最好在开始前问问你公司里的相关人员。

优点

  • 可以不用担心收入的情况下尝试新想法
  • 可以和公司的需求很好的保持一致
  • 适合尝试新想法

缺点

  • 需要用业余时间开发,或者获准在工作时间开发
  • 有被公司过分影响的风险
  • 持续下去可能会出现极度复杂的管理情况

案例学习

补贴

补贴是不需要偿还的极其有效的大笔捐赠,提供补贴的组织通常能够通过给予补贴而从其他方面获得利益,例如接近你,展示其影响力,获得你的工作汇报或税率优惠。

补贴可能来自很多地方,包括公司、软件基金会、慈善基金会以及政府,其技术及法律方面会因其来源的不同有很大的差异。比如一家公司可以通过开咨询费发票给你补贴,而慈善基金会则只能给非盈利组织或个人,通常你得找到一个非盈利组织来帮助你。如果你对补贴不熟悉,了解它的最好方式就是和曾经获得过的人去了解。下面列出了一些成功的案例

优点

  • 限制条件少
  • 有保证的资金可以确保你能在一段时间里专注在你的项目上
  • 让你的项目有时间喘口气和做些试验

缺点

  • 软件方面没太多提供补贴的组织
  • 补贴是有限的,始终需要在用完补贴前找到一个持续方法

案例学习

咨询服务

咨询是一种为开源项目提供资金的灵活方式。你可以更加自由的规划你的时间,比如一周30小时做咨询业务,10小时做开源项目。咨询师通常收费相对较贵,因为工作不稳定且没有公司福利。如果你打算做这类公司,你应该想要创建一家有限责任公司。

优点

  • 这是成熟明确而且大众也能接受的商业模式

缺点

  • 咨询工作需要人力,而且不宜规模化(除了极少数)
  • 商业本身的需求会分散开源项目上的注意力
  • 可能会和软件的简易要求有差异
  • 项目需要足够流行,才会让人愿意为相关服务付钱

案例学习

SaaS

SaaS 即 软件即服务(Software as a Service)。在这个模型下,代码本身是开源的,但是你可以提供增值服务使得用户更容易使用。一个典型的增值服务就是托管付费。

优点

  • 可以围绕这个开源项目建立一个社区,然后通过托管服务赚钱
  • 让开源项目可以专注在用户层面,当需求增加后再帮助企业采用这个项目
  • 可以根据用户数进行规模化改造

缺点

  • 对增值服务使用者来说,通常意味着托管服务必须比招聘一个人来维护项目更便宜
  • 增值服务有可能会使得免费用户不太高兴

案例学习

双重协议

有时候项目可以在完全相同的代码里提供两种不同的授权协议:一个是商业友好的,而另外一个则不(例如GPL)。后者对于个人来说可以免费使用,而公司需要通过购买商业协议来获得合法的商业授权。

优点

  • 这是成熟明确而且大众也能接受的商业模式
  • 如果成功的话,也可以做到规模化

缺点

  • 会和让软件自由获得的理想有冲突
  • 项目需要足够大以满足客户的需求

案例学习

开放核心

开放核心模型中,项目的一些方面是免费的,但一些功能则是私有的,且只对付费用户开放,通常要求这个项目有企业的需求。

优点

  • 这是成熟明确而且大众也能接受的商业模式
  • 如果成功的话,也可以做到规模化

缺点

  • 需要设计付费项目,且该项目应该是独有的
  • 会和让软件自由获得的理想有冲突
  • 独有功能有可能会使得免费用户不太高兴

案例学习

基金会

基金会是可以接收和支出的合法法人实体。鉴于其目的并非获得利润,因此可以更好保持中立地管理项目。在美国,基金会可以是501c3(非盈利)或501c6(贸易联盟),许多软件基金会都是贸易联盟,因为非盈利基金会要求展示出慈善的目的,这在软件开发中比较困难。

优点

  • 中立,基金会可以帮助保护代码和管理社区
  • 可以在许多捐赠者中散布影响力
  • 使得项目合法,公司会更愿意向基金会付款/捐赠而非个人

缺点

  • 只适合大项目
  • 由于IRS的限制,项目能做什么会有所限制
  • 需要大量社区的努力和各种技能,而且之后仍旧需要努力获得筹款

案例学习

风险投资

风险投资是高增长业务的一种筹资形式,不像银行贷款或者任意一种债务财务形式,风险投资者通过提供资金来占有你业务的一定股份。这种交易不像贷款,如果你的业务挂了你并不需要偿还出资方。当然,如果你成功了,你也需要成倍的返还给你的投资者。

风险投资是高风险高回报的:风投者相比银行对风险更加宽容,当然他们也期待你成功后的巨额回报。如果你打算获得风险投资,你应该建立股份有限公司。如果你对风险投资过程不熟悉,开始的最好方式就是询问成功获得风险投资的人。

优点

  • 制度上的支持对成长中的业务有益
  • 大量的风投资金蓄势待发

缺点

  • 风险投资在投资之初便做好了获得成倍回报后退出的打算,历史证明,由于开源项目的结构特点,风险投资的成功很难。
  • 风险投资者可能会因为优先级改变其动机

(译者注:在中国有一类风险投资者故意诱导涉世不深的创业者签订不平等协议,尤其是在创业者热情最浓而又最困难的时候,使其在业务失败时也能全身而退,相应的,创业者会输得很惨,值得小心对待)

案例学习

贡献

我写这个手册主要是为了将我头脑中的知识都整理出来,不过我并没有打算做主要的贡献或改变。优点和缺点大多是从我的观点出发的主观想法。

如果有什么错误(尤其是案例学习),非常欢迎大家的修改。同时,如果发现有什么分类漏掉了,我也非常欢迎大家的修改。

关于中文版的贡献,如果有信息上的错误或遗失,请到原文中提出,如果有中文翻译上的错误,请在这里直接提出。如果你觉得有信息上的错误或遗失,但又各种原因无法用英文表述,担心中文表述不会被采纳,这种情况请还是在原文中提出,我或者其他同时会英文和中文的小伙伴会帮助解决问题的。

本翻译项目使用翻译辅助工具辅助同步原文修订部分。

协议

原文协议为Creative Commons CC0 1.0 License,即你可以自由的使用,商业或非商业,不过如果你用了,很开心能从你那里听到点什么,在这里找到我@nayafia,当然这并不是要求必须做的。

中文版协议为Creative Commons Attribution 4.0 International License,和原文协议差不多了,基本上可以说怎么用都可以,唯一不要修改原作者名字或以原作者名字声明演绎作品就可以了。

Python 语法速览与实战清单

本文是对于 现代 Python 开发:语法基础与工程实践的总结,更多 Python 相关资料参考 Python 学习与实践资料索引;本文参考了 Python Crash Course – Cheat Sheetspysheeet等。本文仅包含笔者在日常工作中经常使用的,并且认为较为关键的知识点与语法,如果想要进一步学习 Python 相关内容或者对于机器学习与数据挖掘方向感兴趣,可以参考程序猿的数据科学与机器学习实战手册

基础语法

Python 是一门高阶、动态类型的多范式编程语言;定义 Python 文件的时候我们往往会先声明文件编码方式:

# 指定脚本调用方式
#!/usr/bin/env python
# 配置 utf-8 编码
# -*- coding: utf-8 -*-

# 配置其他编码
# -*- coding: <encoding-name> -*-

# Vim 中还可以使用如下方式
# vim:fileencoding=<encoding-name>

人生苦短,请用 Python,大量功能强大的语法糖的同时让很多时候 Python 代码看上去有点像伪代码。譬如我们用 Python 实现的简易的快排相较于 Java 会显得很短小精悍:

def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) / 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)
    
print quicksort([3,6,8,10,1,2,1])
# Prints "[1, 1, 2, 3, 6, 8, 10]"

控制台交互

可以根据 __name__ 关键字来判断是否是直接使用 python 命令执行某个脚本,还是外部引用;Google 开源的 fire 也是不错的快速将某个类封装为命令行工具的框架:

import fire

class Calculator(object):
  """A simple calculator class."""

  def double(self, number):
    return 2 * number

if __name__ == '__main__':
  fire.Fire(Calculator)

# python calculator.py double 10  # 20
# python calculator.py double --number=15  # 30

Python 2 中 print 是表达式,而 Python 3 中 print 是函数;如果希望在 Python 2 中将 print 以函数方式使用,则需要自定义引入:

from __future__ import print_function

我们也可以使用 pprint 来美化控制台输出内容:

import pprint

stuff = ['spam', 'eggs', 'lumberjack', 'knights', 'ni']
pprint.pprint(stuff)

# 自定义参数
pp = pprint.PrettyPrinter(depth=6)
tup = ('spam', ('eggs', ('lumberjack', ('knights', ('ni', ('dead',('parrot', ('fresh fruit',))))))))
pp.pprint(tup)

模块

Python 中的模块(Module)即是 Python 源码文件,其可以导出类、函数与全局变量;当我们从某个模块导入变量时,函数名往往就是命名空间(Namespace)。而 Python 中的包(Package)则是模块的文件夹,往往由 __init__.py 指明某个文件夹为包:

# 文件目录
someDir/
    main.py
    siblingModule.py

# siblingModule.py

def siblingModuleFun():
    print('Hello from siblingModuleFun')
    
def siblingModuleFunTwo():
    print('Hello from siblingModuleFunTwo')

import siblingModule
import siblingModule as sibMod

sibMod.siblingModuleFun()

from siblingModule import siblingModuleFun
siblingModuleFun()

try:
    # Import 'someModuleA' that is only available in Windows
    import someModuleA
except ImportError:
    try:
        # Import 'someModuleB' that is only available in Linux
        import someModuleB
    except ImportError:

Package 可以为某个目录下所有的文件设置统一入口:

someDir/
    main.py
    subModules/
        __init__.py
        subA.py
        subSubModules/
            __init__.py
            subSubA.py

# subA.py

def subAFun():
    print('Hello from subAFun')
    
def subAFunTwo():
    print('Hello from subAFunTwo')

# subSubA.py

def subSubAFun():
    print('Hello from subSubAFun')
    
def subSubAFunTwo():
    print('Hello from subSubAFunTwo')

# __init__.py from subDir

# Adds 'subAFun()' and 'subAFunTwo()' to the 'subDir' namespace 
from .subA import *

# The following two import statement do the same thing, they add 'subSubAFun()' and 'subSubAFunTwo()' to the 'subDir' namespace. The first one assumes '__init__.py' is empty in 'subSubDir', and the second one, assumes '__init__.py' in 'subSubDir' contains 'from .subSubA import *'.

# Assumes '__init__.py' is empty in 'subSubDir'
# Adds 'subSubAFun()' and 'subSubAFunTwo()' to the 'subDir' namespace
from .subSubDir.subSubA import *

# Assumes '__init__.py' in 'subSubDir' has 'from .subSubA import *'
# Adds 'subSubAFun()' and 'subSubAFunTwo()' to the 'subDir' namespace
from .subSubDir import *
# __init__.py from subSubDir

# Adds 'subSubAFun()' and 'subSubAFunTwo()' to the 'subSubDir' namespace
from .subSubA import *

# main.py

import subDir

subDir.subAFun() # Hello from subAFun
subDir.subAFunTwo() # Hello from subAFunTwo
subDir.subSubAFun() # Hello from subSubAFun
subDir.subSubAFunTwo() # Hello from subSubAFunTwo

表达式与控制流

条件选择

Python 中使用 if、elif、else 来进行基础的条件选择操作:

if x < 0:
     x = 0
     print('Negative changed to zero')
 elif x == 0:
     print('Zero')
 else:
     print('More')

Python 同样支持 ternary conditional operator:

a if condition else b

也可以使用 Tuple 来实现类似的效果:

# test 需要返回 True 或者 False
(falseValue, trueValue)[test]

# 更安全的做法是进行强制判断
(falseValue, trueValue)[test == True]

# 或者使用 bool 类型转换函数
(falseValue, trueValue)[bool(<expression>)]

循环遍历

for-in 可以用来遍历数组与字典:

words = ['cat', 'window', 'defenestrate']

for w in words:
    print(w, len(w))

# 使用数组访问操作符,能够迅速地生成数组的副本
for w in words[:]:
    if len(w) > 6:
        words.insert(0, w)

# words -> ['defenestrate', 'cat', 'window', 'defenestrate']

如果我们希望使用数字序列进行遍历,可以使用 Python 内置的 range 函数:

a = ['Mary', 'had', 'a', 'little', 'lamb']

for i in range(len(a)):
    print(i, a[i])

基本数据类型

可以使用内建函数进行强制类型转换(Casting):

int(str)
float(str)
str(int)
str(float)

Number: 数值类型

x = 3
print type(x) # Prints "<type 'int'>"
print x       # Prints "3"
print x + 1   # Addition; prints "4"
print x - 1   # Subtraction; prints "2"
print x * 2   # Multiplication; prints "6"
print x ** 2  # Exponentiation; prints "9"
x += 1
print x  # Prints "4"
x *= 2
print x  # Prints "8"
y = 2.5
print type(y) # Prints "<type 'float'>"
print y, y + 1, y * 2, y ** 2 # Prints "2.5 3.5 5.0 6.25"

布尔类型

Python 提供了常见的逻辑操作符,不过需要注意的是 Python 中并没有使用 &&、|| 等,而是直接使用了英文单词。

t = True
f = False
print type(t) # Prints "<type 'bool'>"
print t and f # Logical AND; prints "False"
print t or f  # Logical OR; prints "True"
print not t   # Logical NOT; prints "False"
print t != f  # Logical XOR; prints "True" 

String: 字符串

Python 2 中支持 Ascii 码的 str() 类型,独立的 unicode() 类型,没有 byte 类型;而 Python 3 中默认的字符串为 utf-8 类型,并且包含了 byte 与 bytearray 两个字节类型:

type("Guido") # string type is str in python2
# <type 'str'>

# 使用 __future__ 中提供的模块来降级使用 Unicode
from __future__ import unicode_literals
type("Guido") # string type become unicode
# <type 'unicode'>

Python 字符串支持分片、模板字符串等常见操作:

var1 = 'Hello World!'
var2 = "Python Programming"

print "var1[0]: ", var1[0]
print "var2[1:5]: ", var2[1:5]
# var1[0]:  H
# var2[1:5]:  ytho

print "My name is %s and weight is %d kg!" % ('Zara', 21)
# My name is Zara and weight is 21 kg!
str[0:4]
len(str)

string.replace("-", " ")
",".join(list)
"hi {0}".format('j')
str.find(",")
str.index(",")   # same, but raises IndexError
str.count(",")
str.split(",")

str.lower()
str.upper()
str.title()

str.lstrip()
str.rstrip()
str.strip()

str.islower()
# 移除所有的特殊字符
re.sub('[^A-Za-z0-9]+', '', mystring) 

如果需要判断是否包含某个子字符串,或者搜索某个字符串的下标:

# in 操作符可以判断字符串
if "blah" not in somestring: 
    continue

# find 可以搜索下标
s = "This be a string"
if s.find("is") == -1:
    print "No 'is' here!"
else:
    print "Found 'is' in the string."

Regex: 正则表达式

import re

# 判断是否匹配
re.match(r'^[aeiou]', str)

# 以第二个参数指定的字符替换原字符串中内容
re.sub(r'^[aeiou]', '?', str)
re.sub(r'(xyz)', r'\1', str)

# 编译生成独立的正则表达式对象
expr = re.compile(r'^...$')
expr.match(...)
expr.sub(...)

下面列举了常见的表达式使用场景:

# 检测是否为 HTML 标签
re.search('<[^/>][^>]*>', '<a href="#label">')

# 常见的用户名密码
re.match('^[a-zA-Z0-9-_]{3,16}$', 'Foo') is not None
re.match('^\w|[-_]{3,16}$', 'Foo') is not None

# Email
re.match('^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$', 'hello.world@example.com')

# Url
exp = re.compile(r'''^(https?:\/\/)? # match http or https
                ([\da-z\.-]+)            # match domain
                \.([a-z\.]{2,6})         # match domain
                ([\/\w \.-]*)\/?$        # match api or file
                ''', re.X)
exp.match('www.google.com')

# IP 地址
exp = re.compile(r'''^(?:(?:25[0-5]
                     |2[0-4][0-9]
                     |[1]?[0-9][0-9]?)\.){3}
                     (?:25[0-5]
                     |2[0-4][0-9]
                     |[1]?[0-9][0-9]?)$''', re.X)
exp.match('192.168.1.1')

集合类型

List: 列表

Operation: 创建增删

list 是基础的序列类型:

l = []
l = list()

# 使用字符串的 split 方法,可以将字符串转化为列表
str.split(".")

# 如果需要将数组拼装为字符串,则可以使用 join 
list1 = ['1', '2', '3']
str1 = ''.join(list1)

# 如果是数值数组,则需要先进行转换
list1 = [1, 2, 3]
str1 = ''.join(str(e) for e in list1)

可以使用 append 与 extend 向数组中插入元素或者进行数组连接

x = [1, 2, 3]

x.append([4, 5]) # [1, 2, 3, [4, 5]]

x.extend([4, 5]) # [1, 2, 3, 4, 5],注意 extend 返回值为 None

可以使用 pop、slices、del、remove 等移除列表中元素:

myList = [10,20,30,40,50]

# 弹出第二个元素
myList.pop(1) # 20
# myList: myList.pop(1)

# 如果不加任何参数,则默认弹出最后一个元素
myList.pop()

# 使用 slices 来删除某个元素
a = [  1, 2, 3, 4, 5, 6 ]
index = 3 # Only Positive index
a = a[:index] + a[index+1 :]

# 根据下标删除元素
myList = [10,20,30,40,50]
rmovIndxNo = 3
del myList[rmovIndxNo] # myList: [10, 20, 30, 50]

# 使用 remove 方法,直接根据元素删除
letters = ["a", "b", "c", "d", "e"]
numbers.remove(numbers[1])
print(*letters) # used a * to make it unpack you don't have to

Iteration: 索引遍历

你可以使用基本的 for 循环来遍历数组中的元素,就像下面介个样纸:

animals = ['cat', 'dog', 'monkey']
for animal in animals:
    print animal
# Prints "cat", "dog", "monkey", each on its own line.

如果你在循环的同时也希望能够获取到当前元素下标,可以使用 enumerate 函数:

animals = ['cat', 'dog', 'monkey']
for idx, animal in enumerate(animals):
    print '#%d: %s' % (idx + 1, animal)
# Prints "#1: cat", "#2: dog", "#3: monkey", each on its own line

Python 也支持切片(Slices):

nums = range(5)    # range is a built-in function that creates a list of integers
print nums         # Prints "[0, 1, 2, 3, 4]"
print nums[2:4]    # Get a slice from index 2 to 4 (exclusive); prints "[2, 3]"
print nums[2:]     # Get a slice from index 2 to the end; prints "[2, 3, 4]"
print nums[:2]     # Get a slice from the start to index 2 (exclusive); prints "[0, 1]"
print nums[:]      # Get a slice of the whole list; prints ["0, 1, 2, 3, 4]"
print nums[:-1]    # Slice indices can be negative; prints ["0, 1, 2, 3]"
nums[2:4] = [8, 9] # Assign a new sublist to a slice
print nums         # Prints "[0, 1, 8, 9, 4]"

Comprehensions: 变换

Python 中同样可以使用 map、reduce、filter,map 用于变换数组:

# 使用 map 对数组中的每个元素计算平方
items = [1, 2, 3, 4, 5]
squared = list(map(lambda x: x**2, items))

# map 支持函数以数组方式连接使用
def multiply(x):
    return (x*x)
def add(x):
    return (x+x)

funcs = [multiply, add]
for i in range(5):
    value = list(map(lambda x: x(i), funcs))
    print(value)

reduce 用于进行归纳计算:

# reduce 将数组中的值进行归纳

from functools import reduce
product = reduce((lambda x, y: x * y), [1, 2, 3, 4])

# Output: 24

filter 则可以对数组进行过滤:

number_list = range(-5, 5)
less_than_zero = list(filter(lambda x: x < 0, number_list))
print(less_than_zero)

# Output: [-5, -4, -3, -2, -1]

字典类型

创建增删

d = {'cat': 'cute', 'dog': 'furry'}  # 创建新的字典
print d['cat']       # 字典不支持点(Dot)运算符取值

如果需要合并两个或者多个字典类型:

# python 3.5
z = {**x, **y}

# python 2.7
def merge_dicts(*dict_args):
    """
    Given any number of dicts, shallow copy and merge into a new dict,
    precedence goes to key value pairs in latter dicts.
    """
    result = {}
    for dictionary in dict_args:
        result.update(dictionary)
    return result

索引遍历

可以根据键来直接进行元素访问:

# Python 中对于访问不存在的键会抛出 KeyError 异常,需要先行判断或者使用 get
print 'cat' in d     # Check if a dictionary has a given key; prints "True"

# 如果直接使用 [] 来取值,需要先确定键的存在,否则会抛出异常
print d['monkey']  # KeyError: 'monkey' not a key of d

# 使用 get 函数则可以设置默认值
print d.get('monkey', 'N/A')  # Get an element with a default; prints "N/A"
print d.get('fish', 'N/A')    # Get an element with a default; prints "wet"


d.keys() # 使用 keys 方法可以获取所有的键

可以使用 for-in 来遍历数组:

# 遍历键
for key in d:

# 比前一种方式慢
for k in dict.keys(): ...

# 直接遍历值
for value in dict.itervalues(): ...

# Python 2.x 中遍历键值
for key, value in d.iteritems():

# Python 3.x 中遍历键值
for key, value in d.items():

其他序列类型

集合

# Same as {"a", "b","c"}
normal_set = set(["a", "b","c"])
 
# Adding an element to normal set is fine
normal_set.add("d")
 
print("Normal Set")
print(normal_set)
 
# A frozen set
frozen_set = frozenset(["e", "f", "g"])
 
print("Frozen Set")
print(frozen_set)
 
# Uncommenting below line would cause error as
# we are trying to add element to a frozen set
# frozen_set.add("h")

函数

函数定义

Python 中的函数使用 def 关键字进行定义,譬如:

def sign(x):
    if x > 0:
        return 'positive'
    elif x < 0:
        return 'negative'
    else:
        return 'zero'


for x in [-1, 0, 1]:
    print sign(x)
# Prints "negative", "zero", "positive"

Python 支持运行时创建动态函数,也即是所谓的 lambda 函数:

def f(x): return x**2

# 等价于
g = lambda x: x**2

参数

Option Arguments: 不定参数

def example(a, b=None, *args, **kwargs):
  print a, b
  print args
  print kwargs

example(1, "var", 2, 3, word="hello")
# 1 var
# (2, 3)
# {'word': 'hello'}

a_tuple = (1, 2, 3, 4, 5)
a_dict = {"1":1, "2":2, "3":3}
example(1, "var", *a_tuple, **a_dict)
# 1 var
# (1, 2, 3, 4, 5)
# {'1': 1, '2': 2, '3': 3}

生成器

def simple_generator_function():
    yield 1
    yield 2
    yield 3

for value in simple_generator_function():
    print(value)

# 输出结果
# 1
# 2
# 3
our_generator = simple_generator_function()
next(our_generator)
# 1
next(our_generator)
# 2
next(our_generator)
#3

# 生成器典型的使用场景譬如无限数组的迭代
def get_primes(number):
    while True:
        if is_prime(number):
            yield number
        number += 1

装饰器

装饰器是非常有用的设计模式:

# 简单装饰器

from functools import wraps
def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print('wrap function')
        return func(*args, **kwargs)
    return wrapper

@decorator
def example(*a, **kw):
    pass

example.__name__  # attr of function preserve
# 'example'
# Decorator 

# 带输入值的装饰器

from functools import wraps
def decorator_with_argument(val):
  def decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
      print "Val is {0}".format(val)
      return func(*args, **kwargs)
    return wrapper
  return decorator

@decorator_with_argument(10)
def example():
  print "This is example function."

example()
# Val is 10
# This is example function.

# 等价于

def example():
  print "This is example function."

example = decorator_with_argument(10)(example)
example()
# Val is 10
# This is example function.

类与对象

类定义

Python 中对于类的定义也很直接:

class Greeter(object):
    
    # Constructor
    def __init__(self, name):
        self.name = name  # Create an instance variable
        
    # Instance method
    def greet(self, loud=False):
        if loud:
            print 'HELLO, %s!' % self.name.upper()
        else:
            print 'Hello, %s' % self.name
        
g = Greeter('Fred')  # Construct an instance of the Greeter class
g.greet()            # Call an instance method; prints "Hello, Fred"
g.greet(loud=True)   # Call an instance method; prints "HELLO, FRED!"
# isinstance 方法用于判断某个对象是否源自某个类
ex = 10
isinstance(ex,int)

Managed Attributes: 受控属性

# property、setter、deleter 可以用于复写点方法

class Example(object):
    def __init__(self, value):
       self._val = value
    @property
    def val(self):
        return self._val
    @val.setter
    def val(self, value):
        if not isintance(value, int):
            raise TypeError("Expected int")
        self._val = value
    @val.deleter
    def val(self):
        del self._val
    @property
    def square3(self):
        return 2**3

ex = Example(123)
ex.val = "str"
# Traceback (most recent call last):
#   File "", line 1, in
#   File "test.py", line 12, in val
#     raise TypeError("Expected int")
# TypeError: Expected int

类方法与静态方法

class example(object):
  @classmethod
  def clsmethod(cls):
    print "I am classmethod"
  @staticmethod
  def stmethod():
    print "I am staticmethod"
  def instmethod(self):
    print "I am instancemethod"

ex = example()
ex.clsmethod()
# I am classmethod
ex.stmethod()
# I am staticmethod
ex.instmethod()
# I am instancemethod
example.clsmethod()
# I am classmethod
example.stmethod()
# I am staticmethod
example.instmethod()
# Traceback (most recent call last):
#   File "", line 1, in
# TypeError: unbound method instmethod() ...

对象

实例化

属性操作

Python 中对象的属性不同于字典键,可以使用点运算符取值,直接使用 in 判断会存在问题:

class A(object):
    @property
    def prop(self):
        return 3

a = A()
print "'prop' in a.__dict__ =", 'prop' in a.__dict__
print "hasattr(a, 'prop') =", hasattr(a, 'prop')
print "a.prop =", a.prop

# 'prop' in a.__dict__ = False
# hasattr(a, 'prop') = True
# a.prop = 3

建议使用 hasattr、getattr、setattr 这种方式对于对象属性进行操作:

class Example(object):
  def __init__(self):
    self.name = "ex"
  def printex(self):
    print "This is an example"


# Check object has attributes
# hasattr(obj, 'attr')
ex = Example()
hasattr(ex,"name")
# True
hasattr(ex,"printex")
# True
hasattr(ex,"print")
# False

# Get object attribute
# getattr(obj, 'attr')
getattr(ex,'name')
# 'ex'

# Set object attribute
# setattr(obj, 'attr', value)
setattr(ex,'name','example')
ex.name
# 'example'

异常与测试

异常处理

Context Manager – with

with 常用于打开或者关闭某些资源:

host = 'localhost'
port = 5566
with Socket(host, port) as s:
    while True:
        conn, addr = s.accept()
        msg = conn.recv(1024)
        print msg
        conn.send(msg)
        conn.close()

单元测试

from __future__ import print_function

import unittest

def fib(n):
    return 1 if n<=2 else fib(n-1)+fib(n-2)

def setUpModule():
        print("setup module")
def tearDownModule():
        print("teardown module")

class TestFib(unittest.TestCase):

    def setUp(self):
        print("setUp")
        self.n = 10
    def tearDown(self):
        print("tearDown")
        del self.n
    @classmethod
    def setUpClass(cls):
        print("setUpClass")
    @classmethod
    def tearDownClass(cls):
        print("tearDownClass")
    def test_fib_assert_equal(self):
        self.assertEqual(fib(self.n), 55)
    def test_fib_assert_true(self):
        self.assertTrue(fib(self.n) == 55)

if __name__ == "__main__":
    unittest.main()

存储

文件读写

路径处理

Python 内置的 __file__ 关键字会指向当前文件的相对路径,可以根据它来构造绝对路径,或者索引其他文件:

# 获取当前文件的相对目录
dir = os.path.dirname(__file__) # src\app

## once you're at the directory level you want, with the desired directory as the final path node:
dirname1 = os.path.basename(dir) 
dirname2 = os.path.split(dir)[1] ## if you look at the documentation, this is exactly what os.path.basename does.

# 获取当前代码文件的绝对路径,abspath 会自动根据相对路径与当前工作空间进行路径补全
os.path.abspath(os.path.dirname(__file__)) # D:\WorkSpace\OWS\tool\ui-tool-svn\python\src\app

# 获取当前文件的真实路径
os.path.dirname(os.path.realpath(__file__)) # D:\WorkSpace\OWS\tool\ui-tool-svn\python\src\app

# 获取当前执行路径
os.getcwd()

可以使用 listdir、walk、glob 模块来进行文件枚举与检索:

# 仅列举所有的文件
from os import listdir
from os.path import isfile, join
onlyfiles = [f for f in listdir(mypath) if isfile(join(mypath, f))]

# 使用 walk 递归搜索
from os import walk

f = []
for (dirpath, dirnames, filenames) in walk(mypath):
    f.extend(filenames)
    break

# 使用 glob 进行复杂模式匹配
import glob
print(glob.glob("/home/adam/*.txt"))
# ['/home/adam/file1.txt', '/home/adam/file2.txt', .... ]

简单文件读写

# 可以根据文件是否存在选择写入模式
mode = 'a' if os.path.exists(writepath) else 'w'

# 使用 with 方法能够自动处理异常
with open("file.dat",mode) as f:
    f.write(...)
    ...
    # 操作完毕之后记得关闭文件
    f.close()

# 读取文件内容
message = f.read()

复杂格式文件

JSON

import json

# Writing JSON data
with open('data.json', 'w') as f:
     json.dump(data, f)

# Reading data back
with open('data.json', 'r') as f:
     data = json.load(f)

XML

我们可以使用 lxml 来解析与处理 XML 文件,本部分即对其常用操作进行介绍。lxml 支持从字符串或者文件中创建 Element 对象:

from lxml import etree

# 可以从字符串开始构造
xml = '<a xmlns="test"><b xmlns="test"/></a>'
root = etree.fromstring(xml)
etree.tostring(root)
# b'<a xmlns="test"><b xmlns="test"/></a>'

# 也可以从某个文件开始构造
tree = etree.parse("doc/test.xml")

# 或者指定某个 baseURL
root = etree.fromstring(xml, base_url="http://where.it/is/from.xml")

其提供了迭代器以对所有元素进行遍历:

# 遍历所有的节点
for tag in tree.iter():
    if not len(tag):
        print tag.keys() # 获取所有自定义属性
        print (tag.tag, tag.text) # text 即文本子元素值

# 获取 XPath
for e in root.iter():
    print tree.getpath(e)

lxml 支持以 XPath 查找元素,不过需要注意的是,XPath 查找的结果是数组,并且在包含命名空间的情况下,需要指定命名空间:

root.xpath('//page/text/text()',ns={prefix:url})

# 可以使用 getparent 递归查找父元素
el.getparent()

lxml 提供了 insert、append 等方法进行元素操作:

# append 方法默认追加到尾部
st = etree.Element("state", name="New Mexico")
co = etree.Element("county", name="Socorro")
st.append(co)

# insert 方法可以指定位置
node.insert(0, newKid)

Excel

可以使用 [xlrd]() 来读取 Excel 文件,使用 xlsxwriter 来写入与操作 Excel 文件。

# 读取某个 Cell 的原始值
sh.cell(rx, col).value
# 创建新的文件
workbook = xlsxwriter.Workbook(outputFile)
worksheet = workbook.add_worksheet()

# 设置从第 0 行开始写入
row = 0

# 遍历二维数组,并且将其写入到 Excel 中
for rowData in array:
    for col, data in enumerate(rowData):
        worksheet.write(row, col, data)
    row = row + 1

workbook.close()

文件系统

对于高级的文件操作,我们可以使用 Python 内置的 shutil

# 递归删除 appName 下面的所有的文件夹
shutil.rmtree(appName)

网络交互

Requests

Requests 是优雅而易用的 Python 网络请求库:

import requests

r = requests.get('https://api.github.com/events')
r = requests.get('https://api.github.com/user', auth=('user', 'pass'))

r.status_code
# 200
r.headers['content-type']
# 'application/json; charset=utf8'
r.encoding
# 'utf-8'
r.text
# u'{"type":"User"...'
r.json()
# {u'private_gists': 419, u'total_private_repos': 77, ...}

r = requests.put('http://httpbin.org/put', data = {'key':'value'})
r = requests.delete('http://httpbin.org/delete')
r = requests.head('http://httpbin.org/get')
r = requests.options('http://httpbin.org/get')

数据存储

MySQL

import pymysql.cursors

# Connect to the database
connection = pymysql.connect(host='localhost',
                             user='user',
                             password='passwd',
                             db='db',
                             charset='utf8mb4',
                             cursorclass=pymysql.cursors.DictCursor)

try:
    with connection.cursor() as cursor:
        # Create a new record
        sql = "INSERT INTO `users` (`email`, `password`) VALUES (%s, %s)"
        cursor.execute(sql, ('webmaster@python.org', 'very-secret'))

    # connection is not autocommit by default. So you must commit to save
    # your changes.
    connection.commit()

    with connection.cursor() as cursor:
        # Read a single record
        sql = "SELECT `id`, `password` FROM `users` WHERE `email`=%s"
        cursor.execute(sql, ('webmaster@python.org',))
        result = cursor.fetchone()
        print(result)
finally:
    connection.close() 
「真诚赞赏,手留余香」

初学机器学习的你,是否掌握了这样的Linux技巧?

随着软件系统的不断发展,今天,不同的操作系统对应着不同的适用人群:Windows 面向办公室和商用,Mac 面向创意人群,而 Linux 面向软件开发者。对于操作系统提供商而言,这种市场分割大幅度简化了产品技术需求、用户体验和产品方向上的投入。然而,这也加剧了兼容性问题,让不同业务进入了狭窄、互不相容的领域:商务人士无法对创意提供洞察力,而开发者也无法深入到商务决策中去。

在现实中,知识和技能是流动的,跨越多个学科和领域。与其说「你只能擅长一件事」的理念是迈向精通的路线图,还不如说一种过早优化的方法。一旦从社会中采样大量的任务,你就只能知道你擅长什么,也许你还发现自己擅长它们中的很多。

对于现代的业务分析师,弥补业务与软件之间的鸿沟尤其重要。业务分析必须是「双重平台」,能够利用仅在 Linux(或 OS X)上可用的命令行工具,但是仍然受益于 Windows 的 Microsoft Office。可以理解的是,Linux 会使具有商学学位的人感到恐惧。幸运的是,正如大多数事情一样,你只需 20% 的任务即可完成 80% 的工作。下面是我的 20%。

业务分析是基于数据的,而机器学习正是强大的数据分析工具。我们利用机器学习模型分析数据最好的环境却恰恰是 Linux 系统,这不仅是因为它支持广泛的 Python 机器学习库,同时在于环境配置与管理的简单明了。因此,本文将为机器学习读者梳理 Linux 系统的基本特性与命令。

为什么机器学习分析师需要了解 Linux

由于其开源的底层,Linux 从不断从数以万计的开发者贡献中受益。他们构建的程序和工具不仅使其工作更简单,也简化了跟随他们的编程人员的工作。结果,开源开发带来了一种网络效应:在平台上构建工具的开发者越多,能够利用这些工具立刻编写其程序的其他开发者就越多。

结果就是 Linux 中编写的 Linux 程序和实用工具(统称为软件)的扩展套件——其中很多从未用于 Windows。一个示例是被称作 git 的流行的版本控制系统(VCS)。开发者本可以编写这一在 Windows 工作的软件,但是却没有。他们让其在 Linux 命令行上工作,因为生态系统已经提供全部所需的工具。

具体来说,Windows 上的开发有两个主要问题:

1. 基本任务,比如文件解析、工作调度和文本搜索比运行命令行工具更为重要。

2. 编程语言(比如 Python、C++)及其相关代码库会引发错误,因为它们期望特定的 Linux 参数或文件系统定位。

这意味着若想在 Windows 上进行开发,我们需要花费更多的时间来重写 Linux 中已有的基本工具,并排除操作系统兼容性错误。这并不令人意外——Windows 生态系统当初并没有考虑软件开发设计的需求。

借助这个 Linux 开发案例,让我们从最基本的开始。

Linux 的基本单元:「shell」

「shell」(也被称为终端、控制台或命令行)是一个基于文本的用户界面,通过它把命令发送给机器。在 Linux 中,shell 的默认语言是 bash。与主要在 Windows 内部进行点击操作的 Windows 用户不同,Linux 开发者坚持使用键盘把命令输入到 shell。对于那些没有编程背景的人来说,这种转变一开始也许会不自然,但是在 Linux 中开发的好处很容易超过最初的学习投资。

学习几个重要的概念

和成熟的编程语言相比,bash 只需要学习几个主要的概念。这一步完成之后,之后 bash 的学习就只剩下记忆了。更清楚地说就是:要学好 bash,只需要记住 20—30 个命令(command)以及其中最常用的参数(argument)就可以了。

对于非开发者而言,Linux 很令人费解,因为开发者似乎能随意且不费力地使用深奥的终端命令。其实是因为他们只记住了少量的命令—对于更复杂的问题,他们(和所有普通人一样)也需要谷歌一下。

以下就是 bash 中的主要概念。

命令语法

bash 中的命令是区分大小写的,且遵循 {命令}{参数} 的语法结构。

例如,在『grep-inr』中,grep 是命令(搜索文本的一个字符串),-inr 是标记(flag)或参数(随 grep 默认运行而变化)。理解这个命令的唯一方法是使用谷歌搜索,或输入『man grep』命令。我推荐同时学习命令和其中最常用的参数,否则单独学习每一个标记的作用是很费力的。

目录相对地址

当前目录:.

上一级目录的上一级目录:..

用户的主目录:~

文件的系统根目录:/

例如,为了从当前目录换到上一级目录,需要输入:「cd..」。类似地,为了复制位于「/path/to/file.txt」文件到上一级目录中,需要输入「cp /path/to/file.txt.」(请注意命令末尾的点)。这些例子中使用的都是相对路径,可以使用绝对路径替换。

标准输入(STDIN)/标准输出(STDOUT)

任何输入和提交(通过键入 ENTER)到窗口的命令都被称为标准输入(standard input,STDIN)。

任何程序打印(print)到终端的东西(例如,一份文件中的文本)都被称为标准输出(standard output,STDOUT)。

管道(PIPING)

1 |

一种管道,其左方是一个命令的 STNOUT,将作为管道右方的另一个命令的 STDIN。

例如:echo ‘test text’ | wc -l

2 >

大于号,作用是取一个命令 STDOUT 位于左方,并将其写入/覆写(overwrite)入右方的一个新文件。

例如:ls > tmp.txt

3 >>

两个大于号,作用是取一个命令 STDOUT 位于左方,并将其追加到右方的一个新的或现有文件中。

例如:date >> tmp.txt

通配符(WILDCARDS)

这类似于 SQL 中的% 符号,例如,使用「WHERE first_name LIKE 『John%』」搜索所有以 John 起始的名字。

在 bash 中,相应的命令是「John*」。如果想列出一个文件夹中所有以「.json」结尾的文件,可以输入:「ls *.json」。

TAB 键自动完成

如果我们输入一个命令并按下 TAB 键,那么 Bash 将自动完成该命令。但是,我们也应该使用一些如 zsh 或 fish 工具来自动完成,因为我们很难记住各种命令及它们的参数。更准确地说,这些工具会基于我们的命令行历史自动完成命令语句。

退出

有时候我们会卡在一些程序中并不知道如何退出它们。这在 Linux 新手中是很常见的问题,这也会大大损害新手的积极性。一般来说,退出命令会和字母「q」有一些关系,所以记住以下的退出命令或快捷键就十分有用了。

  • Bash

CTRL+c

q

exit

  • Python

quit()

CTRL+d

  • Nano: CTRL+x
  • Vim: <Esc> :q!

常用 Bash 命令

以下是在 Linux 中最常用到的指令,在使用新系统进行开发时,记住这些指令对于快速上手非常重要。

  • cd {directory}:转换当前目录
  • ls -lha:列出目录文件(详细信息)
  • vim or nano:命令行编辑器
  • touch {file}:创建一个新的空文件
  • cp -R {original_name} {new_name}:复制一个文件或目录(包含内部所有文件)
  • mv {original_name} {new_name}:移动或重命名文件
  • rm {file}:删除文件
  • rm -rf {file/folder}:永久删除文件或文件夹(小心使用)
  • pwb:打印当前工作目录
  • cat or less or tail or head -n10 {file}:文件的标准输出内容
  • mkdir {directory}:创建一个空的目录
  • grep -inr {string}:在当前目录或子目录的文件中搜索一个字符串
  • column -s, -t <delimited_file>:在 columnar 格式中展示逗号分隔文件
  • ssh {username}@{hostname}:连接到远程机器中
  • tree -LhaC 3:向下展示三级目录结构(带有文件大小信息和隐藏目录信息)
  • htop (or top):任务管理器
  • pip install –user {pip_package}:Python 安装包管理器,安装包到~/.local/bin 目录下
  • pushd . ; popd ; dirs; cd -:在堆栈上 push/pop/view 一个目录,并变回最后一个目录
  • sed -i “s/{find}/{replace}/g” {file}:替代文件中的一个字符串
  • find . -type f -name ‘*.txt’ -exec sed -i “s/{find}/{replace}/g” {} \;:替换当前目录和子目录下后缀名为.txt 文件的一个字符串
  • tmux new -s session, tmux attach -t session:创建另一个终端会话界面而不创建新的窗口 [高级命令]
  • wget {link}:下载一个网页或网页资源
  • curl -X POST -d “{key: value}” http://www.google.com:发送一个 HTTP 请求到网站服务器
  • find <directory>:递归地列出所有目录和其子目录的内容

高级 & 不常用的指令

保留一个有用命令列表以备不需也是非常必要的,即使这些情况不常发生(如某个进程阻塞了几个网络端口)。以下我们将列出几个不常用命令:

  • lsof -i :8080:列出打开文件的描述符(-i 是网络接口的标记)
  • netstat | head -n20:列出当前打开的 Internet/UNIX 接口(socket)以及相关信息
  • dstat -a:输出当前硬盘、网络、CPU 活动等信息
  • nslookup <IP address>:找到远程 IP 地址的主机名
  • strace -f -e <syscall> <cmd>:跟踪程序的系统调用(-e 标记用于过滤某些系统调用)
  • ps aux | head -n20:输出目前活动的进程
  • file <file>:检查文件类型(例如可执行文件、二进制文件、ASCII 文本文件)
  • uname -a:内核信息
  • lsb_release -a:系统信息
  • hostname:检视你的机器的主机名(即其他电脑可以搜索到的名称)
  • pstree:可视化分支进程
  • time <cmd>:执行一个命令并报告用时
  • CTRL + z ; bg; jobs; fg:从当前 tty 中传递一个进程到后台再返回前台
  • cat file.txt | xargs -n1 | sort | uniq -c:统计文件中的独特字(unique words)数量
  • wc -l <file>:计算文件的行数
  • du -ha:在磁盘上显示目录及其内容的大小
  • zcat <file.gz>:显示压缩文本文件的内容
  • scp <user@remote_host> <local_path>:将文件从远端复制到本地服务器,或反过来
  • man {command}:为一个命令显示 manual(说明文档),但是通常这样不如谷歌搜索好用

原文链接:http://alexpetralia.com/posts/2017/6/26/learning-linux-bash-to-get-things-done