以前听学长提过Git钩子,但是自己一直没有仔细了解过,记得我还写过一个github更新的Python包,现在想想其实用自带的钩子就能很好的完成

什么是钩子?

我们知道Git是迭代式开发工具,我们的开发流程都是git addgit commitgit push,钩子呢就是你完成每一步Git给你的“回调”,举个例子假如你想让服务器每次上传完新的代码后更新网站,如果你没有钩子,你只能自己ssh登录上服务器,自己更新软件,一次两次还好,多了的话你会骂娘的,所以钩子是给我偷懒的脚手架,我们可以很轻松的写一些脚步帮我们完成一些重复的步骤

介绍玩钩子的作用,我们来介绍一下钩子的分类

我们知道Git核心是commitpush两个命令,一个对应客户端,一个对应服务端,所以钩子主要分客户端和服务端,由于Git步骤分的很细,所以每个大分类下面还有很多小分类,比如pre-commitpost-commit这些。

钩子的全部放在.git/hooks下面,在新建一个项目仓库的时候,Git已经在这个文件夹下给我们生成了很多个.sample后缀的钩子,这些钩子只要把.sample去掉就可以运行了,我们可以在这些sample上面修改完成我们自己的钩子

客户端钩子

客户端钩子很好理解,你commit之后想做其他事,比如说编译一下程序啥的,这里我就不多讲,主要由下面几个钩子组成

  • pre-commit 提交之前
  • post-commit 提交之后
  • pre-rebase 变基之前
  • post-rewrite 替换提交记录之后
  • pre-push 推之前

详细的可以看官网链接钩子

客户端钩子我觉得一般没有太多作用,因为我在提交之前就会运行脚步进行开发调试什么的,我把介绍重点放在服务端钩子

服务端钩子

服务端钩子就是你push之后的事情服务器要运行的脚步,有用推的步骤只有一个,所以钩子只有四个

  • pre-receive 接受之前
  • update 更新之前
  • post-update 更新之后
  • post-receive 接受之后

服务器接收到客户端请求时,pre-receive先进行调用,如果返回值为非0就会拒绝推送,所以我们写钩子的时候一定要记住最后要返回0才能正常接收更新,update主要处理多分支推送,有的时候你一次更新,推三四个分支到服务器,pre-receive只会调用一次,update会对每个的分支调用一次,后面两个都很容易理解

一般我们就是要在服务端更新代码之后运行脚步,所以我们要修改的就是post-update或者post-receive

bash脚步大家都会写,但是大家可能会很陌生什么是Git服务端,接下来我们就来介绍一下Git服务端是什么

Git 服务端

大家一般使用Git都是使用的客户端,但是Git这个工具的确很强,它不但可以当做客户端,也可以当做服务端,为了让大家更好的理解Git服务端,我们先来拿本地文件做”服务器“

首先我们先新建一个文件夹为server,在新建一个文件夹为local,假设文件夹都在/root文件夹下

我们执行下面的命令生成服务器

cd /root/server
git init --bare

只需要在init后面添加一个--bare选项告诉Git,Git就会帮我们生成一个空的“服务端”,我们可以查看一下文件,我们发现Git 给我们生成下面几个文件夹,其中就有我们的hooks

branches  config  description  HEAD  hooks  info  objects  refs

但是服务端和客户端生成的位置不一样,客户端是给我们生成一个.git文件夹,里面放了这些文件夹,然而服务端直接将这些文件夹放在主目录了

行我们已经生成了服务端的,接下来我们生成客户端的钩子

cd /root/local
git init

很简单,同我们往常操作一样,我们这时候添加一个README.md 然后commit一下准备开始往服务端推代码了

在 linux 下直接执行下面命令就行

echo “local update” >> README.md
git add README.md
git commit -m “Add ReadME”

接下来我们就要向”服务器“提交代码了,我们先添加本地文件作为远程服务器

git remote add origin file:////root/server

然后直接推代码

git push origin master

这样我们就向我们文件提交了代码,这时候我们回到我们”服务器“

cd /root/server
ls
branches  config  description  HEAD  hooks  info  objects  refs

我们惊奇的发现服务器并没有我们新建的README.md文件,原来Git服务端并不像SVN一样只保留一份代码大家共同修改,Git服务端只是记录文件变化和分支变化

这里插一句我为什么会去了解Git钩子,由于一开始实现服务器自动更新我的FastProxyScan项目代码,但是我又不想使用Github钩子(push后发送http请求),太麻烦了,后来我一想干脆直接推到我的服务器上,但是推到服务器上的代码只是记录了分支和提交信息,不包含源文件,所以我只好在在服务器上部署这个项目,并添加一个服务器钩子,当服务器更新完成后,再用钩子把服务器上的项目代码更新

如何写服务器钩子

通过上面对本地文件新建仓库,我们知道Git“服务端”新建很简单,我们一般接触比较多的是Github服务端,但是Git非常强大,他可以支持多种协议来连接“服务端”,比如说我们上面用到的本地文件(file协议),假如你用ssh连接远程服务器,你也可以使用类似git remote add origin ssh://username@ip/file/path添加ssh远程仓库

git 支持的协议有ssh、http、https、file、git等协议,你只要确保你能连接上远程服务器就行,接下来我们谈谈如何写服务器钩子

在使用git init --bare新建了一个Git服务端之后,在服务端文件下面有一个hooks文件夹,我们要做的就是把脚本放到hooks文件夹里面(当然你要确保它有执行权限),如果你更擅长写PythonRuby那些脚步也可以,不过要确保前缀后后缀正确。

这里要提到很重要的一点,由于在执行钩子的时候,环境变量GIT_DIR被设置为服务端当前目录,如果你像我一样想更新在另外一个文件夹下面的项目代码,你必须使用uset GIT_DIR清除变量名,否则只会更新服务端,而不会更新你的项目代码

这里我提供一个模板

文件名为 post-update或者post-receive

#!/bin/sh
cd /project/path/ || exit
unset GIT_DIR
git pull origin master

exec git-update-server-info

你只需修改项目文件路径和仓库名即可

总结

通过这个Git钩子了解了Git服务端,也让自己更加了解Git这个软件,以前一直懵懵懂懂,只会向Github提交文件,一直以为Git只是一个版本记录工具而且,现在看来神器之名不是浪得虚名,通过一个小小的钩子,摇身一变成部署神器。

为了给我的站点增加人气,我把这个项目的介绍放到我的博客,如果你觉得这个项目还不错的话,请不要吝啬你的star

github传送门
Demo传送门

引子

一开始自己只想做一个代理池,于是搜了搜Github发现类似的项目,大多数都是爬取网上的一些代理商的免费代理,这部分代理大多都是没有用的,可用性非常低,于是我自己就干脆做一个“代理商”,自己扫描主机把可用的代理扫描出来。

但是现在网络主机实在太多了,至少几百万台,所以这个项目的核心就是快速扫描,在最短的时间内检测更多的代理,目前项目的速度最好只能完成1000代理每小时的速度(日扫描两万代理),希望能继续优化代码,加快速度,如果你对这个项目感兴趣可以Fork下来,欢迎各位的Pull Request

项目依赖

项目基于Python3.5+开发

软件依赖

  • nmap

项目结构

项目主要由三个部分组成

  • 主机扫描
  • 端口检测
  • 代理检查

项目结构为

  • proxy_pool
    • scanner
    • display
    • database

现在依次介绍在搜索速度上的优化

  • 主机搜索

全球的IP都是有ISP统一分配的,ISP主要由下面几大洲分配,我们中国处于亚太区,所以我们的IP由亚太互联网络信息中心(APNIC)分配IP,目前中国分配的IPV4总数为3亿左右,这个数量还是比较大

我们可以从 http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest下载最新分配的IP地址

但是代理服务器只存在特定的服务器上,所以现在版本还没有发布V1.0主要是因为搜索的效果不是很好(搜索的主机代理转换率太低),目前还在想其他方法,等有更好的解决方法就会发布V1.0

  • 端口检测

使用nmap的“TCP SYN scan”最大化加快端口检测速度(需要root权限)

  • 代理检测

使用nmap先验端口与Python异步最大化代理检测速率

安装教程

项目采用Django做后台管理,所以只需要一点Django基础知识就能在这个项目上做二次开发,如果你只想获取最新的可用代理,可以通过http://115.159.146.115 调用API接口获取最新可用代理(我的站点带宽有限,所以只开放最新100个代理,并且只是20分钟更新一次)

环境安装

  • nmap 安装
  • python3.5+ 安装

运行:

git clone https://github.com/mrzhangboss/FastProxyScan.git
cd FastProxyScan
python3.5 install -r requirement.txt
  • 主机检测
cd pool/proxy_pool
sudo python3.5 manage.py scan --vps
  • 端口检测

    sudo python3.5 manage.py scan –proxy -m 100

m是并行参数,值越大速度越快

c是检测ip头网址,可以使用我提供的 http://115.159.146.115/ip 返回请求头,可以参考我的上一篇博文 代理的前世今生

在我搭建的DEMO站点上,我使用supervisor让这三个程序循环运行,你可以使用crontab定时调度也可以像我一样。

数据库当前采用的Sqlite3,但是数据库模型全部使用ORM开发,你可以很方便的修改settings.py来放入其他数据库,如果你懂一点Django的话

总结

如果你像了解更多开发这个项目背后的知识的话,可以看看我上一篇博文 代理的前世今生

引言

自己对代理认识不深,也只是会使用而已,由于最近想做一个代理池,于是查了很多资料,发现代理这个东西还是非常有趣的

代理是什么?

从编程上来看,requests只需要在请求里面加上proxies参数例如requests.get('http://www.baidu.com', proxies={'http': '127.0.0.1:3128'}),我们就能连上代理进行访问,由于requests包装了太多细节,我们无法知道用了代理和没有用代理的区别

接下来我们来看一下Python3的内库是如何使用代理的

  • 首先要申请一个ProxyHandler

    from urllib import request
    proxy = request.ProxyHandler({‘http’: ‘127.0.0.1:3128’})

然后我们通过这个proxy创建一个opener,然后用opener打开页面,最后输出结果。

opener = request.build_opener(proxy)
resp = opener.open('http://www.baidu.com')
print(resp.read())

然后我们看一下如果没有使用代理的请求是什么

resp = request.urlopen('http://www.baidu.com')
print(resp.read())

你可以很清楚的看到,如果我们不需要代理,之前打开url就行,也就是说我们的请求,其实全部发给代理,交给代理了

这里就稍稍谈点感想了,以前经常在书上看到人歌颂互联网的伟大,但是自己一直不明白这个伟大在哪,以前一直从表象感受互联网,有了互联网,不用打开电视就可以看影片,不用去图书馆就可以看书,感觉互联网神奇的地方就是给自己带来方便,但是没有去互联网本身的架构的伟大,通过一根根网线交换器,无数主机“连”在了一起,构成了宏大的互联网,或许你通过浏览器打开的网站主机离你几百公里,但是你不要做飞机轮船,你直接在家里就能通过层层代理传递你的请求将千里之外的“敌将首级” 探入囊中。

互联网神奇的地方就是看起来各个部分非常分散,但是他们却能通过一根一根线紧密的联系起来,只有有“距离”的时候你才能感受到他的美丽

以前刚学习网站的时候,在本地调试的时候,你在本地跑一个web,直接打开浏览器访问,这时候我就有一种错觉,web就是两端,客户端和服务器端,在本地调试可以这样理解,但是一旦拿到互联网,这就是不完整的,完整的应该是客户端-代理端-服务器端,当然我们的代理端有时候可能是网关、路由器、交换机等等。

了解这些有什么用呢,因为我自己以前一直对代理没有什么很深理解,用爬虫的时候使用代理就不会被封IP,感觉代理是很BUG的东西,但其实代理很普通,而且无处不在,我们要想真正理解代理,就必须把它拖下圣坛。

代理其实很普通

前面我们知道代理其实很普通,但是要深入了解代理必须要先了解IP,IP是什么呢,IP就是互联网的身份证,要想在互联网上“混”,必须要有“身份”

那为什么我们要用代理呢,比如说假如你是未满18岁的小朋友,你想要买上网,你必须要借一张大人的身份证去上网,这个时候代理的作用就是帮用它的身份证帮你干事。

我觉得中国文化博大精深,其实一听代理这个词,我们就能知道代理是干什么用的。其实把代理吹得神乎其神没什么用,那些作用都是它的他自己瞎几把搞的,从我们客户端来看,代理就是服务端,了解这点非常重要,因为他能让我们把所以的事情都简化,而且从客户端来看,代理就是这样的

客户端、代理、 服务器三者之间的关系

前面我们已经谈了客户端和代理之间的关系,对于客户端来说,代理就是服务器端,我们啥都不管,把请求发给代理,相信它就是我们请求的服务器

对于代理来说,其实它自己最清楚,自己就是个代理,它必须要把请求转发给服务器,然后在把服务器的响应发给客户端,代理就是一个中介人,有的时候我们也可以把它看做一个双向中继,把请求传递一下,再传回来,所以在这三者之间,只有代理是个明白人,它必须清楚这次任务所以细节,所以有时候虽然说代理是安全的,但其实它也不安全,只要把代理攻克了就能了解到底是哪个家伙干的坏事,所以网上干坏事的人,一般都用很多个代理,层层代理,就算你攻克了一个,也找不到坏人

对于服务器来说,代理就是客户端,它只负责响应就行,对于代理和客户端来说都是一样的策略。但是时候很奇怪,服务器为什么知道你是个代理,原来全是代理自己的锅,我们细谈一下代理的分类

代理的分类

代理也分很多种,有的时候代理也不老实,把客户信息暴露了,这个时候我们就说它是小透明(透明代理),有的时候它不告诉你客户信息,但是告诉服务器我是个代理,我们就说它是匿名代理,但是有时候它连它自己是啥都不告诉你,它伪装成它是客户端,这个时候我们称它为高匿代理,所以这些代理根据暴露信息的不同可以分为这三种

  • 透明代理
  • 匿名代理
  • 高匿代理

当然我们最喜欢高匿代理,你可以把它当做你的分身,除了身份证不一样,两个人长得一模一样。

所以我们判断一个代理的类别,必须要检测它向服务器发的报文,所以在我项目FastProxyScan,我搭建了一个服务器,返回客户端向服务器请求头,主要是HTTP_X_FORWARDED_FORHTTP_VIA头来分别暴露客户端信息和代理端信息,所以我们只要请求头检测有没有这两个字段就可以完成检测,原理非常简单

在这里我介绍用Nginx高效返回检测信息

  location ~ ^/ip {
      default_type application/json;
      return 200 '{"REMOTE_ADDR":"$remote_addr","HTTP_VIA":"$http_via", "HTTP_X_FORWARDED_FOR": "$http_x_forwarded_for"}';
}

在Nginx配置里面加上这个端口,我们只有请求/ip,就能直接从Nginx返回请求头信息,速度贼快

总结

当然网上还有很多对代理的分类,缓存代理,正向代理,反向代理,但是这些都是代理自己的额外功能,我们前面介绍的代理都是傻呼呼,客户端要什么,它就做什么,这些如缓存代理高级的代理就是很聪明,它的目的就是最快返回客户端需求,比如说虽然说这个傻客户端傻乎乎一个请求请求了几十遍还没记住,代理自己拿个小本子记好,你下次来,正好对上号,直接抄给你,不用再跑几千里去拿了。但是其实本质上它还是逃不出上面的分类,只不过它有的自己的不同罢了。

0x00 引子

排序是很多算法的基础,简简单单的排序前人就归纳出很多种算法,但是这些算法多多少少都有着相同的原理

排序算法有很多,这里我们就简单的谈谈下面7种排序的特点

  • 冒泡排序
  • 选择排序
  • 插入排序
  • 希尔排序
  • 堆排序
  • 归并排序
  • 快速排序

0x01 Summary

从算法的抽象程度上来看,冒泡、选择和插入是比较好理解,我们能用我们生活中的常见事物来理解,后面四种比较抽象,而且相对于前三种平均时间复杂度O(n) = n ^ 2 的来说,后面四种的平均复杂度都比前面的小,尤其是面对大量数据排序来说,后面四种能比前三种跑的更快

0x02 冒泡、选择和插入的特点

这三种算法空间复杂度都为O(n) = 1,也就是说在给定一个列表的前提下,无论列表数有多大,额外的排序所需的空间都为常量。但是这三种算法的平均时间复杂度为O(n) = n ^ 2, 也就是说在给定一个长度为n的数组,必须要经历 k × n × n 次操作才能排序,当我们的n比较小的时候,我们无法察觉这个算法与更高效的算法的差别,当n很大的时候,比如一个亿,这时候的要进行的操作就瞬间爆炸了。

这三种算法很大程度上是牺牲了运行时间换取运行空间,我们可以从桶排序上面得到相反的例子,桶排序的时间复杂度为O(n) = 1,空间复杂度为O(n) = n,也就是说在排序上面他的速度是最快的,但是它所花费的空间也是巨大的,有时候时间空间就是两个双刃剑,你如果想节省空间必须浪费时间,你如果想节省时间必须浪费空间

这三种算法原理很简单,而且有一个相同的地方,就是他们每一节排序就会“删掉”一个数字,接下来就是对剩下的排序。当然我这里的删掉就是代表已经排好,然而接下来的过程中不会再涉及到这个数字

这个非常好理解,随便给我们一副牌让一个小朋友把他排出来,小朋友一般就是先找出最大的牌放到最前面,然后在剩下里面找到最大的,依次排下去,最后手里就剩一张牌了,这个牌组就排好了

这三种算法都是基于这个核心,但是具体的算法细节不同。冒泡排序就是先从头到尾依次把最大的交换到最后面;插入排序的话就是我们从第一个数字开始从后面把小的数字插入到前面去;选择的话同冒泡有点相似,不过它并不会把数字传递过去,它直接将未排序的最大值与未排序的末尾值交换。

这三种排序我们都非常好理解,但是他们有一个缺点,就是未排序前必须遍历全部数组,我们都知道现在大数据时代,对于上亿数据执行一次遍历就已经非常耗时间了,为了排一个数字要几乎就遍历一遍(排到后期遍历的越来越少),所以这三种算法在面对巨量数据的时候,花在遍历上面的时间比排序时间要更多。

0x03 希尔和插入排序

希尔排序是插入排序的更高效改进方法,说到改进我们就要谈谈插入排序的优缺点

  • 优点

我们给定 [ 1, 3, 2, 5, 8 ] 数组,这个数组基本上已经排好序了,如果使用插入排序,我们只要在插入2的时候,将2和3交换就可以,设我们挪动的距离就为1

我们在看这个数组 [ 1, 3, 5, 8, 2 ],我们可以看到这里如果使用插入排序,我们会在插入2的时候,要将2依次与8、5、3交换,这样移动的距离就为3

希尔排序改进的地方就是步长,如果它的步长选择的好,它的排序效果越好。这个步长是什么呢,插入排序的步长一直为1,也就是每次遍历的时候步子迈一步,假如步长为2,也就是迈两步。在[ 1, 3, 5, 8, 2 ]数组中,比如说我们数字2,它在步长为1的时候,它下一步要比较的是8,假如步长为3,那它下一步就直接与3比较了。

所以希尔排序改进就在于他能直接移动多位,在上面的例子里面,步长为3,我们能直接将数组从3的位置移动到2,如果直接使用插入排序,必须移动3次才能达到希尔排序的效果。

希尔排序原理同插入是一样的,不同在于,插入的步长希尔是可变的,这样就为一些“调皮”的数字的移动加快了速度,一步一步的移动他们太累了,直接把步子迈大,一步到位。

引言

编程开发有时候也像雕刻一件艺术品

以前一直有一种错觉,觉得编程开发就是会用库会用框架,这阶段的感悟只是停留在库的使用上面,然而当你持续工作在一件产品上的时候,你就把思维聚焦在产品,这时候你的感悟就会是架构的搭建,库只会变成你的工具

所以慢慢明白编程届的前辈们一直劝我们在大学不要为了钱选择做一些外包项目,外包项目这种东西就像一次性编程产物,你写完之后就再也不会code review了,而对于编程来说,编程就是在写BUG,只不过对于大神来说写的少,对新手来说就是写的多

诚然大神们也是从一个一个BUG中慢慢走过来的,然而对于我们菜鸡来说,很多时候我们并不能发现自己的BUG,所以让自己成长最快的方法就是立马纠正自己的BUG

当然这里我们所说的BUG有的时候并不是我们经常说的系统无法运行的BUG,我们这里说的BUG可以算成缺陷,有时候是在特殊情况下才触发。比如说运行时,这时候我们称它未漏洞;重构时,我们称它为SHIT代码;测试时,我们称它为过耦合。

这里我也不想过多谈技巧如何去发现这些BUG,以为技巧是死的人是活的,我们不需要太多技巧去避免或者去查找这些BUG,还有重要的一点在于BUG是无法避免的,我们要关注的是产品本身。有幸在实习几个月的时间一直专注于一个产品的开发,期间一直经历了大大小小的重构,随着产品的成型,自己也慢慢感悟到一些方法加速查找系统BUG和如何快速开发。

接下来就介绍一些我自己的浅显感悟

迭代开发 + CODE REVIEW

如何从零开始搭建一个产品,除非你是超级大牛,几个小时就能搞定一个完整的代码的开发流程,普通人都是一步一步来迭代开发,但是这个迭代开发也有讲究,有些人喜欢从头写到尾,然后看看能不能跑起来,再疯狂DEBUG,也有些人喜欢先写局部,慢慢测试,最后把所以组件都串联起来。

这两种方式萝卜青菜各有所爱,第一种速度最快,但是不适合团队合作和CODE REVIEW,第二种速度慢,但是灵活可靠,容错率更高,对于新手来说,选择第二种能让自己的错误不会对系统造成系统性崩塌,而且可以慢慢发现自己的BUG,从而从BUG中提高自己

对于迭代式开发我们就不得不提一下git,作为一个版本控制工具,它在迭代开发的作用堪称神器。然而这个神器我却一直没有找到正确的打开方式,只是把它当做上传服务器的工具,最近才开始慢慢掌握一点小小的技巧。

我原来的git的工作流程用命令概况下来就是

  • git add -A
  • git commit
  • git push

    这三条,然而我大部分时间都只是发挥了git push的功能,纯粹把它当成代码的备份,然而git的核心在于git addgit commit这两个命令上,这里要检讨一下我以前的做法,以前一般完成一个组件的功能就直接快速git add -A然后git commit,虽然我是遵循迭代开发,但是我很少去REVIEW自己这次提交的commit

    所以最主要的问题在于如何在快速迭代开发的时候慢下来,好好思考和REVIEW一下自己这次提交的代码,所以在这里不得不介绍git diff这个命令了,对于在每个新修改的文件来说,在你执行git add之前,你最好git diff一下这个新提交的文件,git会把你所做的修改和原始代码做一个对比。

    有的时候我们并不能记得原始代码是什么,我们到底对代码做了什么改变,幸亏我们有这个神器,只需要git diff一下,我们所做的修改和原始版本的差异就会显示出来,REVIEW代码的过程也就是我们发现错误的过程

    我们要想提高自己的就要不断的改变修正,所以正确的git的工作流程应该是这样

  • git diff

  • git add
  • git commit
  • git push

当然每个commit可以由很多个git diff + git add 组成,但是我们必须要保证自己对git add的每一个文件都要REVIEW一遍,而且我们在每次git add之前,要思考这次的改变是否能够改进,是否必要等

产品开发就像爬山,你不可能一步登山,所以我们要做的就是,在每次停下来的时候确定方向,修正自己,甚至回头

拥抱变化 + 快速开发

前面我们谈了在每一个commit的时候我们都得慎重再慎重,小心又小心,但是这种思想有时候如果把它带入开发过程中则会让你寸步难行,具体是什么呢,我来根据我自己经历来介绍

我自己是一个有一点叫做代码“洁癖”的人,由于看了不少编程理论的书,我容忍不了自己写出很SHIT的代码,面对新东西,我一般喜欢研究个透再下手,我要确保我的设计是万无一失的,所以这就造成如果我接触一个新的库或者新的功能我会花上很长时间在上面,而且由于我自己思考原来越深,我可能会把原来简单的问题搞得越来越复杂,等到我觉得开始CODING完的时候,我发现自己把一个超简单的问题搞得那么复杂,牺牲了太多时间却适得其反

造成这种原因主要是吸收太多而没有消化,我看过很多编程理论的书,技术大牛用他们的开发经验告诉我们要模块话开发,要注意设计模式,要考虑系统灵活性耦合性,这种大牛经验是很宝贵,但是这些经验就好像最高的武功秘籍,假如你没有相应的基础,贸然去练的话你会走火入魔,对于新东西新功能,我们就要想爬山者先驱一样,我们不是要找一条最锻炼自己的路去走,而是找一条最简单的路,只有爬到山顶我们才需要考虑其他问题

这种快速开发的思想还有很重要的一点就是“拥抱变化”,我们在快速开发的过程中无法避免由于快速开发造成的部分SHIT代码,当你写出这部分的时候,其实对于菜鸟来说,这部分代码才是你最宝贵的代码,因为它暴露了你的缺点,要想提高自己,必须发现自己的缺点,所以快速开发的过程中不但激发自己的潜能,而且让自己对自己的缺点有了更好的了解,了解了自己缺点,才能慢慢改进,所以快速开发第一能够节省时间,第二个就是能缓慢提高自己,当然前提是去修正它,所以快速开发你必须要把自己的代码”洁癖“和速度结合起来,抱着一颗永不满足的心去不断锤炼自己的”代码“

总结

在我看来编程开发就像是打太极,一方面我们得快,以目标为驱动,快速开发;一方面我们得慢,以变化为驱动,迭代开发

看似快慢是两个极端,其实两者相得益彰,快促进慢,慢促进快,两者相互促进

当然这只是我的自己小小感悟,编程开发博大精深,这些只是技巧,关键在于自己,思考并转换才是最核心的

曾经有人问过我一个问题什么是TCP复用,我当时没有回答上来,后面我又遇到一些并发性能问题的时候,我才开始慢慢明白为什么会有这个问题,以及这个问题背后的秘密

其实当时应该他想考我的是爬虫的请求优化,准确来说是HTTP持久连接(HTTP persistent connection),并不是TCP复用,这才导致我当时查阅很多资料,并没有发现TCP复用能优化客户端,因为TCP复用是服务端的事,现在就让我从源头开始慢慢解读这个问题

起因

我们知道我们每次发的HTTP请求在底层都是一个套接字的通信,我们可以从底层开始做一个测试

我们使用个for循环,申请1024个socket

import socket
l = [ ]
for  i in range(1024):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.bind(('www.baidu.com'),80))
    print(i)
    l.append(sock)

这个过程有点慢,但是你会发现在申请到1000左右的时候,会直接报Open too many file 这个错误,但是我们并没有打开文件,为什么会报这个错误

原来在Unix系统下,我们申请的套接字也就是socket在底层是以文件的形式存在的,客户端通过申请一个socket来写入和接受服务端的请求,这是一个非常重要的概念,对我们后面解析库函数有很大的帮助。

由于系统资源有限而且打开很多文件系统响应会变慢,所以Unix系统或者Windows都对单个进程申请套接字有限制,在Unix系统下我们可以通过ulimit -n查看这个值,在笔者的Ubuntu上这个值为1024,基本没有修改过都是这个值,我们可以通过我们可以在命令行执行ulimit -HSn 4096临时增加至4096,

所以我们一般来说单台机器单个进程最多只能并发1024个请求,在不修改配置的情况下这个值是固定的,所以我们提高并发数只有两种方法

  1. 修改系统配置
  2. 使用多进程

在写这篇文章之前,我一直以为HTTP复用能在作用在并发上提高爬虫性能,但是其实并不是,它能提高性能但是却不是在并发上提高,接下来我们仔细介绍HTTP复用是怎么提高爬虫性能的

HTTP复用

说道HTTP复用,我们不得不介绍一下HTTP和TCP协议,我们都知道Internet是由OSI七层协议构成的,但是OSI只是规定了框架,具体协议我们是通过TCP/IP来实现的

我们先来说说这个TCP,我们都说互联网能够发展到现在这么稳定可靠多亏了这个TCP可靠协议,但是这个可靠是要付出代价的,建立一次连接的过程要经过三次握手,断开的过程也得四次分手,而且这个连接的过程完全不涉及我们要请求的内容,我们知道爬虫一般请求一个站点只有通过一两次请求就行,如果每次请求都得握三次手,还得分四次手,这样的代价也太大了

所以HTTP的复用优化的方向就是减少TCP的连接,谈到如何减少TCP连接,我们就得说说HTTP长连接(HTTP persistent connection)

HTTP长连接

在HTTP1.1规定了默认都是长连接,TCP不断开,并且在请求头添加一个Connection的header,如果是值为keep-alive则保留TCP连接,假如为Close请求完成之后就会关闭,在HTTP1.0的下默认为关闭状态

怎么来理解这个长连接呢,我们都听说过HTTP是无状态的这句话,从HTTP协议上来看,服务器客户端就是一个“Request”,“Response”组成,无论多复杂的页面都是由一个个“Request”组成

为了更好的理解上面的话,我们回到那个套接字,我们把HTTP请求比作打电话,对于每个电话,我们只需要先拨号,然后滴滴滴三下后确定我们同对面连上了(服务器“协商”好),然后我们把我们要说的话通过话筒传给对方,等我们说完之后,由于信号差,对面听完还要想怎么回,然后我们安安静静的在听筒那等,等他想好说什么,在慢慢的说给我们听。

在HTTP1.0的时代,我们每次拨完一次好,说完一句话,听完对面的回应后,我们就会挂断电话,如果我们还想说就得再重复这个过程,在HTTP1.1下我们增加了长连接这个概念,就是如果你想这个电话里多聊几句,那么就在最后加上“你等下不要挂了,我还要说”(在header加上“Connection: Keep-alive”),那么对方就不会挂断电话,等它说完之后也想你一样在听筒那而等着,这样我们就省掉了一次拨号的时间

我们现在了解为什么HTTP复用能够节省爬虫的性能了,接下来我们就从编程语言对HTTP复用的实现上了解如何实现HTTP复用

存贮单元—ConnectionPool

在介绍ConnectionPool之前我们先简单介绍一下HTTP复用的具体表现

TCP与URL的关系

我们知道HTTP复用的是TCP的连接,而TCP连接由四个部分组成

  1. 本地ip
  2. 本地port
  3. 服务器ip
  4. 服务器port

简单来说就是两个二元组(local_ip, local_port), (server_ip, server_port)

但是我们发一次的请求是一般是通过URL,也就是类似“http://www.baidu.com”,这样的url来请求的,这个同我们TCP有什么关系呢?

首先介绍一下“http”代表通信协议,这里使用的是HTTP协议,“://”后面的就是请求的域名,域名后面如果有冒号就是我们请求的端口号这里没有,根据HTTP协议这里默认是80端口(HTTPS是443),域名后面的就是请求路径,这里也没有就默认问“/”,也就是我们通过这个“url”就知道我们这次请求的具体位置了,现在我们找到了端口,但是请求的IP在哪呢?

这里就要介绍一下DNS了,我们为了让我们的站点更好记,我们使用域名代替ip地址,通过在DNS服务上注册我们域名,以及绑定我们域名对应的IP地址,我们就能让计算机通过域名来转换成IP地址,这里就不详细介绍了

所以呢我们现在了解了,一个TCP连接只是涉及到URL的域名和端口号,我们请求站点的时候主要是通过不同的路径来获取内容,所以我们可以很清楚的知道,只要我们URL的域名和端口一样,那么我们所以的URL都能共用这个TCP接口

ConnectionPool的实现

简单来说为了实现HTTP复用,我们只需要保存TCP连接就行了,但是通过前面我们知道,我们保留的TCP连接必须和你要请求的url要域名端口一样,有时候一个站点的服务可能由多个域名多个端口组成,所以原本我们只要用一个变量保留上一次请求的TCP连接,为了程序更加健壮,我们需要一个TCP连接池,存贮不同的TCP连接。

每次新的URL来的时候我们就是先从TCP连接池中查看有没有相同的域名和端口,如果有就用它发请求,如果没有就新建一个TCP连接,这就是TCP连接的基本原理,当然还要一点编程的时候要注意,我们从池子里面取出一个用完必须放回,否则池子用完了又得新建,那就完全丢掉了复用这个概念了

HTTP复用在Requests的具体表现

前面介绍了一大堆概念,但是从头到尾如果让我们自己来做一个实在太难了,幸好我们有Requests这个库,它的Session对象在文档介绍了它就维护了一个TCP连接池并且能够复用TCP连接

接下来我们就从代码入手来更好的理解这个进程池的高级用法,我们为了更好看到每一次请求底层的操作,我们这里自己先自己搭建一个本地服务器,我们使用Flask来搭建一个本地服务器
新建一个web.py文件,在运行

from flask import Flask, request
from werkzeug.serving import WSGIRequestHandler

app = Flask(__name__)
WSGIRequestHandler.protocol_version = "HTTP/1.1"


@app.route('/')
def hello_world():
    return '%s %s' % (request.remote_addr, request.environ.get('REMOTE_PORT'))


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8000)

这里我们在8000端口开了一个服务器并且设置为HTTP/1.1协议,我们返回用户请求的ip和端口

接下来我们开一个Python解释器来看看这个进程池的用法

>>> import requests, logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> session = requests.Session()
>>> session.get('https://baidu.com')
DEBUG:requests.packages.urllib3.connectionpool:Starting new HTTP connection (1): www.baidu.com
            DEBUG:requests.packages.urllib3.connectionpool:http://www.baidu.com:80 "GET / HTTP/1.1" 200 None
<Response [200]>

看我们可以从打印的logging日志看到我们在进程池中新建了一个TCP连接,我们在试着再请求一次

>>> session.get('https://www.baidu.com')
    DEBUG:requests.packages.urllib3.connectionpool:https://www.baidu.com:443 "GET / HTTP/1.1" 200 None
<Response [200]>

看我们的HTTP复用实现了,在同一个TCP连接中我们请求了两次

深入requests的ConnectionPool

在上面我们验证了requestsSession对象的确实现连接池,但是似乎requests并没有给我们接口来操作这个值,通过分析代码和资料,我们发现在Session初始化的时候,绑定了一个 HTTPAdapter对象,这个对象就是requests封装了urllib3.connectionpool.ConnectionPool来实现TCP池

我们查看这个HTTPAdapter文档发现它的用法是这个

>>> import requests
>>> s = requests.Session()
>>> a = requests.adapters.HTTPAdapter(max_retries=3)
>>> s.mount('http://', a)

我们可以通过创建将一个TCP池绑定到一个session对象上,我们可以看一下这个创建一个HTTPAdapter的参数

HTTPAdapter(self, pool_connections=10, pool_maxsize=10, max_retries=0, pool_block=False)

我们主要看这两个参数pool_connectionspool_maxsize,通过一番测试(比较长就不演示了,可以参考引用来进行实验),我们发现这个pool_connections主要控制TCP池的种类数,我们知道在进程池中我们可以有很多相同的TCP连接(主要是并发新建的),这些连接有些是连接相同的域名和端口,这个pool_connections就是控制有多少种类的站点(域名和端口)同时能够存在池中,那么这个pool_maxsize代表的就是池中不管种类有多少总共的TCP连接数

假如你只写单线程程序那么你只要考虑pool_connections这个参数,因为单线程你发出一个请求只会占用一个TCP连接,在你每次开始请求时,池中不同站点的连接只有一种,所以你可以把pool_connections当做池的大小,但是假如你写多线程程序,每个时间点需要的TCP连接同你多线程的个数有关,由于requests不会限制当池中无可用连接时新建TCP连接,所以你一个站点的TCP连接可能有多个,这时我们就要用pool_maxsize来限制池子的容纳量,为了避免无限制存贮TCP连接,TCP连接池会把超过总数的连接按照时间顺序踢出去,让池中保持不大于限制总数的TCP连接。

当然这里有个非常重要的知识点,requests的TCP池并不会限制新建TCP连接,它只是限制存贮量和种类,这个知识点非常重要,这对后面我们理解aiohttp异步请求时候为什么要限制并发数有非常大的帮助(它只限制TCP连接总数)

TCP连接池的作用

经过上面的探索,我们知道TCP连接池一方面能够实现HTTP复用达到减少TCP连接时耗的作用,另一方面我们通过复用TCP连接可以节省套接字,避免经常碰到”Too many file“的错误,顺便提一下,由于TCP连接具有冷启动的特点,在刚连接上TCP时,速度会非常慢,只有系统发现负载不多才会恢复正常速度,所以这就是我们有时候用浏览器打开一个新页面要加载很久的原因。

前面一直在介绍HTTP复用的理论基础,最后我们实战演练一下在异步框架aiohttp使用HTTP复用

异步框架下HTTP复用

在这里我们使用Pythonaiohttp异步请求框架(在这里我们要求Python的版本必须大于等于3.5),aiohttp也提供了TCP连接池的功能,要想共享TCP连接池,我们先新建一个Session对象

connector = aiohttp.TCPConnector(limit=50)
session = aiohttp.ClientSession(connector=connector)

我们直接创建了一个最大容量为50的TCP池,并把它绑定到session对象上,接下来先试试跑个200个请求(要先在按照前面的代码搭建本地服务器)

async def fetch(url, session, semaphore):
    async with semaphore:
        async with session.get(url) as response:
            print(await response.read())



loop = asyncio.get_event_loop()

接下来我们就可以直接使用aiohttp框架

semaphore = asyncio.Semaphore(20)
tasks = [fetch(url, session, semaphore) for x in range(nums)]
begin = time.time()
try:
    loop = asyncio.get_event_loop()
    loop.run_until_complete(asyncio.wait(tasks))
except:
    pass
finally:
    end = time.time()
    loop.close()
    session.close()
    print('cost', end - begin, 'speed', nums / (end - begin), 'req/s')

在我的电脑测试测试下421.73 req/s,基本上达到异步的效率(可以调节limit至100左右达到最大)

在这里解释一下为什么要使用semaphore(asyncio锁),由于当前版本(aiohttp==2.2.5)下aiohttp的HTTP连接池无法在没有锁的情况下复用TCP连接(具体可以看一下我提的这个issue,这里由于牵扯到太多异步框架的知识,我就详细不介绍异步库,如果想了解更多的话就看我上一片博文Python异步的理解

总结

在如何提高请求效率和速度上,HTTP复用算是从协议出发上的一种优化,他主要利用方向是在单个站点多次请求上面,假如每个站点都只是一个请求的话,那他就无用武之地,不过现在站点不可能一次请求就完成交互,所以了解这个HTTP复用如何是非常有帮助的。

引用:

Requests’ secret: pool_connections and pool_maxsize

Making 1 million requests with python-aiohttp

起因

异步的出现主要是单线程的io等待,由于任务大部分是io处于等待,假如让一个线程工作,所有任务按照流水线形式执行,假如一个请求需要1秒,五个请求需要五秒,那么如果能让他们同时运行的话,那么速度就能增加五倍

如何让五个任务同时进行有两种方法

  1. 多线程
  2. 异步

调试过过多线程的人都知道,线程就是从头到复制主线程一遍,开多个线程不仅成本高,而且调试成本高,异步就不一样呢,你可以把它当做一个单线程来进行编程,而且比多线程更加高效

Python异步的多种实现

Python实现异步的框架有很多,但是核心思想大概是基于下面两种方式

  • twister
  • gevent

twister思想是将异步操作封装起来,通过回调的方式来操作,我们看scrapy里面中间请求的实现就是twister方式

scrapy.Request(url='xxx', callback=func)

通过传递封装的request,当框架帮我们请求完后,会通过callback进行回调,如果你的请求很简单那还好,只需要回调一次就可以,假如你的请求较复杂,那么你就会进入回调地狱(callback hell)

而且你还要写处理各种回调产生的异常,你可以看看scrapy中间件的实现就知道scrapy的异常处理有多繁琐了。但是中间间的存在的确让我们代码模块话更加容易,这里暂且不谈。

twister这种回调比较反人类,它必须依赖背后的核心进行调度,离开了背后核心的支持,这个根本跑不起来,而且由于它依赖回调来进行后续步骤处理,所以我们的代码必须被切分为不同的部分,假如我们不知道背后的核心如何回调函数或者约束,我们根本不知道这两个函数是有关联的

这种编程方式比较有利于模块话开发,但是对于我们熟悉顺序编程来看,这种回调方式显然是一场噩梦,相比于twister这种回调方式,gevent采用的是绿色协程的方式进行回调。

PEP-380定义了yield from的语句,Python3.3开始使用,为了区别协程和生成器,Python3.5开始使用await代替yield from,这样协程就有了一个专门的方法来声明(awaitasync),后者用来标记异步函数

协程之所以能够在异步中大方光彩,其中很大一部分就是协程天生就是异步的,理解协程我们可以从一个简单的生成器与普通函数来对比

a = (x for x in range(10))
b = [x for x in range(10))

我们来看这样一个生成器a,一般我们来用这个生成器必须加 for循环才能得到里面的值,假如我们尝试使用a.send(None),我们会发现,我们依次从返回值得到了b里面的序列

就是这么一个send与接受的功能让我们实现了一种”绿色“回调,就是协程这个性质让他写异步变得更加顺理成章了,而且相比twister回调,协程的回调更为彻底,它把”自己”包装起来全部回调回去了。

了解异步基础

前面简单的聊了协程的性质,现在谈谈异步存在的基础,异步的存在最关键的在于等待,为了了解这个等待意思和后面解读asycio库,我们先使用selectors (Python3对select的封装)来做个演示

import selectors
sel = selectors.DefaultSelector()

声明一个select对象sel,现在我们要调用这个核心函数

sel.select(10)

这个10是代表timeout的时长,也就是最长等待时间,10秒之后我们发现,这个结果返回了一个空列表,这是显而易见的,我们并没有指明让它等待什么

selectors这个库的功能非常好理解,类比寄信,你如果想等别人回信,假如你没有寄出去你自己的信,你一直在邮箱那等,除了等到你不想等,否则你是收不到你的回信的,所以这个库的核心在于,“寄信”(register)和等信(select),然后自己选择处理信件

import selectors
import socket

sel = selectors.DefaultSelector()

def accept(sock, mask):
    conn, addr = sock.accept() 
    print('accepted', conn, 'from', addr)

sock = socket.socket()
sock.bind(('localhost', 8000))
sock.listen(100)
sock.setblocking(False)
sel.register(sock, selectors.EVENT_READ, accept)

while True:
    events = sel.select()
    for key, mask in events:
        callback = key.data
        callback(key.fileobj, mask)

这个程序最关键的地方在于sel.registersel.selectcallback那里,前者是注册函数,后面是等待,最后就是回调

上面就是twister式最简单的回调,你可以看到,为了得到连接sock的连接,我们必须把处理注册到等待中去,但是这只是得到sock连接,为了成功建立一个TCP连接,我们还得进行三次握手,还得处理每次回调时的错误

而且你可以看到回调函数与核心驱动select.select()耦合度非常高,我们必须完全了解系统如何回调,处理一件事被回调分割成一段一段

接下来我们来看看基于geventasyncio实现

async def wget(host):
    connect = asyncio.open_connection(host, 80)
    reader, writer = await connect
    header = 'GET / HTTP/1.0\r\nHost: %s\r\n\r\n' % host
    writer.write(header.encode('utf-8'))
    await writer.drain()
    while True:
        line = await reader.readline()
        if line == b'\r\n':
            break
    writer.close()

我们成功的用一个函数描绘了建立一次连接并且进行通信的过程,假如你懂一点asyncio,你就会发现它与twister回调的不同,使用await关键字把函数挂起,然后等待回调,根据回调接着进行下面的操作,我们成功的用同步的语句把异步写出来,而且是使用Python的原生实现,所以当asyncio出来的时候Guido(Python之父)是多么自豪,你可以看下面引用 Tulip: Async I/O for Python 3演讲的视频

浅析Python异步实现

前面我们知道了异步的基础就是等待,那么Guido是如何在协程的帮助下将异步实现出来的呢,接下来我们就简单的谈一下这个实现基础

我们先将上面twister改成gevent方式的

sel = selectors.DefaultSelector()


@asyncio.coroutine
def get_connection(sock):
    sel.register(sock, selectors.EVENT_READ)
    yield True


async def create_connection():
    sock = socket.socket()
    sock.bind(('localhost', 8000))
    sock.listen(100)
    sock.setblocking(False)
    await get_connection(sock)
    conn, addr = sock.accept()
    print('accepted', conn, 'from', addr)


event = create_connection()
event.send(None)
events = sel.select(100)
for key, mask in events:
    try:
        event.send(None)
    except StopIteration:
        pass

我们稍稍修改一下上面的twister函数,我们创建一个get_connection函数把sock绑定到我们的sel上面,然后回调一个True,当然这个回调没有处理异常什么的,然后我们将得到的协程向其发送一个None让它启动,这时候你在在另外一个ipython客户端执行

import socket
socket.socket()..connect(('localhost', 8000))

然后你就会发现在主线程里面打印出来客户端的连接信息

通过这个小例子我们知道,实现异步要解决的问题就是一个公用注册器(能够注册所以的io等待),一个容器(能够存贮所以的协程),一个核心能够一直执行等待回调和处理回调(多个协程)

深入asyncio了解Python异步

通过上面我们简单的知道了,如何通过协程与select合作完成异步操作,然而我们上面写的只是最最最基本的实现,接下来我们来深入asyncio源码了解如何让异步变得更加简单

引用

Python异步并发框架

Python 中的异步编程:Asyncio
Tulip: Async I/O for Python 3
【译】深入理解python3.4中Asyncio库与Node.js的异步IO机制

平常在Django项目中大量使用自增这个键,平常都是使用ORM,很少去了解这个东西在数据库中具体使用,最近遇到要备份和复原数据的事情,趁着这次好好探索一下这个自增键的使用

Django里面大部分都是将其作为Int自增主键来使用,第一个不需要维护一个唯一值,第二个使用Int作为主键的话,搜索和外键关联速度比较快。

我们这次从原生SQL出发,探索一下这个自增主键在数据库中的具体使用

新建数据库

我们先新建一个数据库

create table inc(
id serial not null,
name text
);

PG里面简单的使用serial关键字就会生成一个自增键,默认会在数据库新建一个索引表,例如上面就会新建一个inc_id_seq的索引表,这个字段类型为int,如果数据库很大,我们可以使用BIGSERIAL键申请一个bigint类型的字段

我们可以看一下这个索引表里面有什么

         Sequence "public.inc_id_seq"
    Column     |  Type   |        Value        
---------------+---------+---------------------
 sequence_name | name    | inc_id_seq
 last_value    | bigint  | 1
 start_value   | bigint  | 1
 increment_by  | bigint  | 1
 max_value     | bigint  | 9223372036854775807
 min_value     | bigint  | 1
 cache_value   | bigint  | 1
 log_cnt       | bigint  | 30
 is_cycled     | boolean | f
 is_called     | boolean | t

我们可以这个索引表其实就是维护了一个参数,通过字段我们可以知道,这是一个自增为1的键,下一个值为2,目前没有插入一个值

增删查减

我们通过一些基本操作来看看这个自增键的作用

  • 首先是插入
insert into inc (name) values ('1'),('2'), ('3');

我们插入三个值,我们再查看索引表,发现last_value变成了3

这个是没有指定id的值插入,我们试试显式声明插入

insert into inc  values (1, '1'),(2, '2'), (3, '3'), (4, '4');

我们惊奇的发现,在我们显式声明自增键的值的时候,索引表并没有变化,last_value还是3,这说明只有在不声明自增键,让数据库自己新建的时候,索引表才会更新

我们可以把自增键看做一个默认值,当没有给自增键赋值的时候,这个自增键会从这个键的索引表中得到下一次自增的值

所以我们再尝试使用不声明自增键值的方法插入一个新值

insert into inc (name) values ('4')

我们发现索引表中last_value变成了4

主键自增

由于我们在Django里面使用自增,一般都是将其声明为主键,设为唯一值,所以如果我们将声明表的结构变成

    create table inc(
id serial not null PRIMARY KEY,
name text
    );

上面的情况就不可能发生了,因为我们把自增键声明为主键,不过有意思的事就是如果你像上面一样指定了一个自增主键值为4,然后不指定再插入4,你会发现第一次会报主键不允许重复的错误,第二次则会成功插入,而且索引表的last_value变成了5

看来并不是每次成功的时候才会更新last_value值,只要让系统自己去申请自增值就会更新索引表,我尝试了对表的增删查改,发现只有insert并且申请自增值的时候才会更新索引表,而且这个索引表之后增加,不会减少,所以有时候你删掉最大的值,自增键默认又从最后一次开始更新

总结

在对单个数据表备份还原的时候,由于简单的使用了COPY命令进行备份还原,通过上面的探索我们发现如果涉及到自增主键的导入导出,在新表导入旧数据是不会出错的,但是由于我们没有考虑自增键的影响(我们导入自增键是显示赋值),在后面插入数据的时候有可能会报主键重复的错误

为了避免以后插入入数据出现这样的错误,我们有两种措施

  1. 使用COPY命令导入导出时候不获取自增键值
  2. COPY导入新表后自己更新索引表

第一种的话SQL比较繁琐(必须写出表所有字段值),推荐使用第二种

我们可以简单的使用

SELECT MAX(id) FROM your_table;

先获取自增键最大值,然后更新索引值(999为上面你获取的最大值)

SELECT setval('your_table_id_seq', 999, false);

当然我们可以将这条语句合正一句话

SELECT setval('your_table_id_seq', COALESCE((SELECT MAX(id)+1 FROM your_table), 1), false);

这样我们就可以开心的完成单表导入导出了

ps:
在使用COPY命令时必须是superuser才能从文件中读取和导入数据,最简单的方法是用superuser账号加权使用alter user xxx superuser,待倒完数据后再降权alter user xxx nosuperuser

其实这篇文章很早就像写了,但是自己也一直没有明白自己想写什么,直到最近自己慢慢才有一点思路

这篇文章并不想高谈阔论,只是自己的一些碎碎念,把自己对人生的一些看法的小总结。你可以把它当做一篇小说来看,我也想把它当做小说来写。

引子

中秋回家,同自己表妹聊天,她突然问了一句,大学哥哥没有谈一个女朋友,接着说道没有恋爱的大学是不完整的,我楞了一愣,一本正经的对她说道,大学其实就是培养自己一个完整人格的过程,在这个过程中我们学习并且养成自己一个完整独立认知。

回去之后我仔细想想,我好像并没有回答表妹的问题,但是我自己也陷入了深思,大学这几年到底对我干了什么。

错觉

先不说大学对我做了什么,我仔细想了想我自己对自己定位。

你觉得你很努力?

我一直觉得自己在大学还是很努力的,我没有沉迷游戏超过两天,我没有放弃学习新知识,在大学图书馆借了几百本书,可能比全班人加起来都多,大一到大三我经常去图书馆读书,尤其是大三,有时候会在图书馆读一天书

但是上面的上面全部我自己的错觉,我虽然不沉迷游戏但是经常会被游戏分心,我虽然读过很多书但是我一直是读那些别人认为是必读的书,而且我读过的书大部分都没有转换成为我真正的源泉

我就像一个饥渴的行者,在大河面前用手拼命的往口里塞水,我的确看了很多的书,但是这么多书就像流水一样,全部都流走了;这些知识对于我来说只是解渴之物,当我非常饥渴的时候,我会拼命的想得到它,但是当我满足的时候,这些东西就像泥土一样对我一文不值

所以这就可以解释为什么我每次借书的时候都是兴高采烈,但是当借回来时候往往翻了几页,然后就束之高阁,然后循环往复

你觉得你懂很多?

没有出去之前,我在大学社团里面干过不少项目,所以我有时候觉得自己技术很牛逼,我是大神级别的人。我懂很多别人不知道的知识,我用过很多框架,我知道怎么搭集群,我知道什么是机器学习,什么是分布式,什么是代码规范。

然而出去之后才发现这都是错觉,你做过很多项目,你经历过百万规模的并发吗,你了解很多框架,随便挑一个框架出来,你能说出它的优点和不足吗,你看过源代码吗,你知道如何保证上千集群的容错率,你知道什么是大规模机器学习吗

挑出任何一个你会发现自己一直处在皮毛阶段,有时候你会用你还年轻但是学习能力强来掩饰你的不足,但是这只是你的错觉吧,不懂就是不懂。

你觉得你不需要明确方向?

有段时间我一直很困惑自己未来发展方向,我搞过UI,搞过前端、后端,搞过机器学习、数据分析,搞过分布式、爬虫。编程语言更是多,C、C#、Python、Java、C++,node,JS。我对我自己的定位一直很模糊,我不知道我未来到底想干什么,我很羡慕那些从小就明确目标的人。

我一直为此苦恼,我也看过很多人的书籍、博客,我也看到过很多人写的相同的文章,在很长一段时间我都认为它是正确的,它告诉我你不需要明确你的职业规划,它给了很多有名的人例子,奥巴马、马云、马化腾、李开复他们在大学都不知道自己要干什么。在很长一段时间我都觉得大学就是应该多学东西,把东西学杂。

但是我仔细想想,这个也是错觉

我从大二就开始有转行的念头,当时我是web后端开发,我当时觉得有没有方向无所谓,只要你多学就行,就这样陆陆续续混杂看了一年多书,直到七八个月前,我才开始反思。

我开始明确我的目标,把它当做我要干一辈子的方向去搞,我开始扣书,像一个干涸的大地一样汲取天空飘下的雨滴,我这时候发现知识是那么的宝贵,自己是多么的“native”;从一窍不通到入门到小小成就只花了短短几个月的时间,完成了一年多都完成不了的入门。

当然我最终没有选择这个方向,但是这个过程我从来没有后悔过,而且在这个学习过程中,它帮助我更加了解我自己,而且节省了我选择的时间。

总结

好像我的大学一直全部都是由错误组成的,但是这些错误真的对我来说毫无作用吗。其实未必,当我学完第一门编程语言C的时候,所以的“错误”都在默默的发挥的作用,我用静态语言的辩证思想学Python,我用动态语言的思想反过来学习Java,好像全部的“错误”全部融合成为一个圈,

我们好像一直在害怕自己出错,其实慢慢的发现那些没有错误的人生不是完整的。

小时候很羡慕那些一直走在正确的道路上的人,也有时候会幻想成为他们一样的人。没错那样的人生固然完美,但是我更喜欢一直跌跌撞撞的我,或许我经常走在错误的道路上,但是我享受了沿途的风景,不论最终结果如何,人生的意义还是沿途的风景吧!

最近打了两个比赛,一直忙着工作和打比赛,没有时间总结,今天抽空好好总结一番

先说一下比赛结果吧,队名全为OfferGo唯品会购买预测第五名,携程房屋预测复赛第六名,两个比赛打的都不算太好,只能算勉强及格,虽然离大神的距离还有十万八千米,不过总算可以称的上入了门,现在来总结一下我入门的经验吧。

观察数据

我参加过很多群,发现很多新手缺乏观察数据的能力,他们每次进入一个群总是嚷嚷这让大神发baseline

这一点对于新手来说很不利的,比赛考的就是你对数据的掌握能力,你对数据把握的越好,你的比赛成绩就越好,要真正掌握数据就要从观察数据入手


在我看来观察数据主要从四个方面来,我总结为望闻问切

观察数据缺失值,缺失值对数据影响很大,有时候我们能够从缺失值里面了解很多信息,而且对于缺失值,后期我们对不同的缺失值要采取不同的手段,比如补全、统计占比、丢弃等等。

对于缺失值我一般从两个方面来观察

  • 全局观察
    • 一般采用datafram.info(null_counts=True, verbose=True)方法来观察全局数据缺失情况
  • 局部观察
    • 一般采用series.isnull().count()series.loc[series.notnull()]观察单一列表缺失情况

这个阶段我们主要从大的方向远远的一下数据,主要建立对数据的全局观。

对于数据来说,一般分为三种,一种为数值型数据(整数、浮点数、时间等),一种为字符型,最后一种为图像型,三种类型数据处理难度依次增强

对于大多数比赛都是设计前两种数据,第三种只有牵扯到图像处理才能遇到。对于前两种数据,我们在闻的阶段,主要是探查数据分布情况,了解数据分布情况,我们才能对症下药。

了解数据分布情况有两种方法

  1. 图像观察
  2. 数学统计观察

图像观察主要使用PandasMatplotlib绘图接口,或者使用seaborn(一个友好的封装了Matplotlib的包),一般我们可以从直方图、饼图、频率图方向来观察数据

数学统计我们主要采用Pandasdescribe方法,对于数值型数据,主要从平均值(mean)、中位数(50%)、标准差(std)、最大(max)、最小(min)、非空总数(count)来探测数据,对于字符型我们主要从最频繁的值(top)、最频繁的值的个数(freq),非空总数(count)、不相同的值(unique)。

通过上面两种方法,我们能够从数据分布的角度大致勾画出数据的轮廓。

比赛的目的就是找到最优解,而最优解的跟相关特征紧密联系的,你的特征对结果影响越大,你就要审这个特征

举个例子,我们要预测三组数据

  • 1 1.8 2
  • 2 3.5 4
  • 3 5.4 16

第一行为我们要训练的值,我们发现第二行的数据是第一行的1.8倍,而第三行只是2的次方,对于这两个特征来说,第二个特征就是最好的特征,我们只要建立一个映射,准确率能接近100%,而第三个特征对预测结果毫无联系,这个特征不但对结果没有作用,而且有时候会起到反作用

当然在这里我们举这么一个例子在实际中不可能遇到,我们遇到是更多数据,而数据之间的联系并不是这么简单的线性关系,但是线性关系有的时候能让我察觉到特征与预测值的关联,毕竟如果特征值是随机值那么与预测值之间的相关性是非常低的。

Python里面探测线性关系最简单的方法是调用特征值和预测值的相关性系数(corr),我们可以简单的使用df[['feature', 'target']].corr()就可以得到线性相关系数,这个数的绝对值越接近1,相关性越大,一般来说相关性越大和越好对结果都不好,最好的特征相关性处于中间位置。

相关性低我们可以理解,为什么相关性高反而不好呢,因为数据比赛里面给我们的数据大部分都是不平衡的数据,正负样本失衡,一般相关性很高的值一般为分类同预测值相同,比如一个二分类问题,预测值为0和1,给的样本正负比为1000:1,那么如果有一个特征全为0或者其他,那么他与预测值的相关性会达到90%以上,然而这个值是毫无作用的。

所以我们通过简单的相关系数并不能很好的观察特征真正相关性,一般我们要辅助图像法和统计法。

图像法就是通过将特征值分布与预测值相关性图表画在同一个图表里,具体可以参考可视化特征

统计法类似图标,使用统计方法观察,特征值与预测值的相关性,一般使用groupby方法对两个特征进行统计就可以进行简单的观察

只是一个简单的手段,一般我们在大量添加特征的时候,为了节省模型训练时间,在将特征放入管道之前进行一个简单的过滤删除的工作,真正重要的步骤在这个方面

这个步骤放在最后是因为,这个步骤也是我们一趟循环下来的最后一步

数据比赛中前期大家最喜欢用的模型是树模型,比如随机森林、XgboostLightBoost等,这些模型属于弱学习器组合模型,我们最后可以从训练结果得到每个特征在模型占的比重

对于这个比重,是非常重要的,他代表了每个特征对应在模型中占的权重,也可以理解特征与结果的相关性

对于相关性很强的不同的特征,我们可以将他们组合,有时候这种强强组合生成出来的特征会比原来母特征相关性更强,当然组合的方法有千种万种,如何验证他们有效就要从头开始对数据进行望闻问切

总结: 数据比赛就如同问诊,我们不断对特征进行望闻问切,对于高手来说他们能很快的从原始特征中挑选出病根,对症下药,而新手的话,一阵摸瞎,经常会碰到在比赛中期做出一个很好的结果,接下来很长一段时间都没有进步的情况。掌握科学有效的挑选特征方法需要一个“医者心”,必须学会对特征“负责”,要学会望闻问切

并行化算法

由于Python本身对多核利用不好,如何利用多核加快特征生成对于比赛来说意义重大

就拿我来举例子,我每天下班打比赛的时间不超过8个小时,前期算法没有并行化的时候,走一遍管道要四个小时,这意味着我一天只能跑两次,而进行并行化优化以后,我跑一遍四线程全开(笔记本双核四线程)只要十分钟就能跑完,每次生成新特征只有10分钟就能拿到特征相关性数据,来验证特征的好坏。

下面我从三个方面来谈谈怎么实现并行话算法

1 . 使用系统自带函数,拒绝for循环

举个例子,作为新手,实现对两个个特征求平均,一般采用for循环将每一行两个特征值加起来然后除以2,假如有1000万行,每行加法和除法运算花0.001ms,那1000万也要10秒钟,只是进行一个最简单的求平均,你就花掉10秒钟,上百个特征你得运行几天

学过矩阵的都知道,矩阵就是一种高效的并行化结构,它将集合统一进行计算,可能一个大矩阵运算要比单一计算要慢,但是单一计算要1000万次的话,大矩阵运算只需要两次就够了,这个效率比就出来了

Python由于是一门解释性语言,比其他静态语音速度要慢许多,你一方面使用for循环加大运算次数,一方面执行一次时间长,这相重叠加你的算法会跑的比蜗牛还慢

所以我们避免使用我们写的函数,尽量使用库系统函数,因为库系统函数底层是使用CC++实现的,而且他们在底层进行使用矩阵话运算代替单一浮点计算,我们使用库的函数(比如meangroupby等)一方面能底层能使用C加快速度,一方面使用矩阵运算加快速度,两个叠加你的算法跑的比飞机还快。

2 . 使用多进程,充分发挥使用多核性能

由于PythonGIL锁,使得Python无法利用多核进行计算,所以我们只能使用多个进程来充分利用多核

实现多进程有两个要点(具体可以参考我携程比赛代码 Github地址)

  • 特征提取模块化
  • 进程池的搭建和维护

我在携程比赛中的mult_run.ipynb中搭建了一个进程池,通过第三方调度和监控进程内存CPU等信息,达到充分“榨干”每个核的功效

3 . 压缩数据,让矩阵运算更快

由于在对特征进行提取过程中,Python会自动将低位制值转换成高位制值,比如float16在进行一次groupby之后就会转换成float64,由于在矩阵运算时候,高进制值会占更多内存和运行时间,所以为了加快算法运行,我们要将其压缩,一方面节省内存,一方面能够让算法运行的更快

在携程的比赛中,原始数据有一个G,我将其压缩之后只占用300M内存空间,这为我后面在一台12G内存的笔记本实现并行化算法提供了巨大帮助,当然我每次在生成新特征的时候也会进行压缩,具体可以参考我携程的utils.py文件

总结: 这两次比赛,我从菜鸟出发慢慢的从一个程序员变成了数据挖掘机,在模块化和并行化方面,我觉得我的进步不错,但是在数据特征挖掘方面我与大神之间的差距还是巨大的,这也是我止步于前五的主要原因,接下来我要加强对数据方向的锻炼,希望能够在工作和比赛之中得到更好的进步

在最下面贴一下我的携程比赛代码(基于Notebook)

https://www.github.com/mrzhangboss/ctrip_room_predict