Python的GIL一直是被大家攻击其语言的一个弊端,每次在讨论语言特性的时候这点总是会被人们提起,但是这个东西好像就一个“污点”,大家都知道,但是大家都不了解为什么。本片博客就是好好的探索一下GIL,让我们不在畏惧它

引言

其实一开始并没有想到研究GIL,但是在研究Python调用C++的过程中发现我们可以通过这种方式解决掉GIL,让我们的代码不被Python拖累

这篇博客相比于上面的博客更注重于代码的讲解,因为我发现很多讲解调用C++的给的例子实在是过于简单,那个C++代码简直不能算做C++代码,在我眼中,只有充分使用面对对象技术才能叫C++代码,否则那些只能叫C代码,所以本文就从一个实际的C++项目出发介绍如何让Python调用C++并且丢弃GIL

GIL简介

首先我们要知道什么是GIL,为什么它会拖累Python,首先我们看一下Python历史,Python是Guido van Rossum 在1989年发布的,那个时候计算机的主频还没有达到1G,程序全部都是运行在单核计算机上面,直到2005年多核处理器才被Intel开发出来

多核处理器意味着什么呢,就好比一个工厂,你原来只有一个工人干活,现在有很多个了,一开始设计出来只是为了能在每个核心上跑不同的应用,但是随着大家对多核计算机的使用,大家发现有的时候计算器其实很空闲,大部分CPU都在休息,假如只在一个核上跑一个应用的话,那么其他CPU就浪费了,所以大家就开始设计怎么并行在多个CPU上跑同样的任务

现在我们来考虑一下怎么能让CPU力往一处使,我们用数据库来做比方,假设我们计算机上安装一个银行数据库,为了让这个“银行”能够服务更多的人,我们把对钱的操作(增删查改)放到每个CPU上运行。假如我们的顾客一个一个排着队来取钱存钱,我们每个CPU查询都是唯一的,存取也是唯一的,那么我们的“银行”就能正常工作

但是现实的环境往往不是这样的,顾客它可能会因为网络原因个人原因同时进行多个操作,假如它同时取1千万的两次操作(它账号只有1千万),每个CPU上的程序查询时候正好都是账号有一千万,然后依次进行数据的更新,最后我们发现用户的账号变成了0,但是用户却取了两千万出来,你的银行损失了一千万,所以并行任务最重要的就是数据共享

怎么解决这个共享问题呢,很简单加“锁”,我们给需要共享的东西上个锁,每次你想用的时候你就把锁锁上,然后对共享的东西进行操作,当有别人想动这个东西的时候,他一看哎呀有人在用,那我等会。这样就不会造成上面的冲突了,但是这个也造成了一个问题由于我上了一把锁,每次我们想操作的时候,必须去看一下这个锁有没有被人锁上,假如没有我就锁上,有就等待,这一来一去就会造成一个效率问题(感觉这个也是国企的通病,权利依次掌握在领导上,要想完成工作得不断的进行开“锁”、关“锁”,有时候还会造成“死锁”),所以并行的4个任务运行速度不一定是一个任务的四倍,所以我们经常看到一些库在运行说明里面双核速度会比单核加速一点几倍,之说以达不到双倍就是因为这些“锁”的存在

“锁”帮我们能让单任务拆分成子任务并行化加速,但是在一定程度上拖累了运行速度,我们回到Python,因为多核是在2005年才出现的,但是在并行化上面,一个比多核更早出现的概率就是:线程进程

在还没有多核处理器的时候,操作系统为了让程序并行化跑,就创造了进程和线程的概率。用通俗的话来讲,进程就是一家大工厂,而线程就是工人,为了提高生产力,我们可以开很多家工厂,当然我们也可以开一家工厂,招很多工人。但是线程这个东西相比于进程要消耗的少的多,因为它“原材料”都是从“工厂”里面拿的,假如说工厂少了几个工人还可以生产,但是上万个工人没有工厂他们也办法工作。

所以对于Python来说首先得支持线程和进程的概率,对于进程来说很简单,就是多开几家工厂(多开几个Python程序)罢了,但是对于线程来说,由于Python是一门脚本语言,它需要一个解释器来执行代码,我们知道这个解释器它可以当做大一个共享变量,假如在不同的线程里面用“锁”来限制一下的话,环境变量就会乱了套

所以Python对于线程的支持就是给他加一个锁,也就是我们俗称的GIL,由于在操作系统在运行单核的时候就支持线程,一个工人加一个锁其实也没有什么,无非就是多了一点开锁关锁的时间,所以Python在2005前一直没有GIL这个概率,到了2005大家发现Python使用多线程竟然只能使用一个核,完全浪费了其他核,因为虽然Python的线程可以分配到不同的核上运行,但是当他们运行的时候发现这个锁没有被释放,所以每个核上的线程都傻乎乎的在等待,结果最后查看效果多线程比单线程速度还慢(要等GIL释放)

Python社区逐渐发现这个问题,他们也做了很多挽救工作,比如在线程睡觉(sleep)、等待连接的时候让线程主动释放GIL,这样就能让其他线程继续执行,但是对于纯粹的运算代码而不是IO密集代码总也避不开这个锁的存在,如果允许GIL释放,由于历史遗留问题很多代码都会乱了套(理论上其实就是需要重新修改锁的设计,可以参考MySQL的代码去掉“锁”花了5年时间),考虑到Python本来就运行的慢,Python开发者觉得假如你觉得代码很慢,你可以放到C/C++里面执行,所以对于这个GIL就没有继续啃下去,而是把中心放在Python调用C/C++中,提供了一些很方便的方式让我们在C/C++中控制GIL的释放以及获取

所以我们接下来通过一个来学习Python调用C++代码,来了解Python如何调用C++,并且通过一些实验来验证线程、进程和GIL

测试GIL的存在

首先我们要做的第一件事就是测试GIL的存在,现在基本上主流电脑都是多核CPU,所以我们这个实验可以很轻松的在多核下进行

首先我们得安装一些环境:Python3gcchtop(在Windows可以用下任务管理器代替)

首先我得提一下我的一个认识误区,在以前我不太清楚线程、进程与多核直接的关系的时候我有一个误区,我以为C能在单线程里面使用多核(我也不清楚为什么我会这么想,可能是因为了解很少),而Python却不能,后面通过我实验我才发现,无论是CPython只要你的代码不使用线程、进程那么你的代码只能同时运行在同一个核上

怎么来测试呢,我们可以在Python的解释器里面输入

while True:
  pass

然后我们打开htop,我们可以发现某一个CPU始终保持在100%(这个CPU可能会变化,因为操作系统控制每个进程切换CPU时间),假如你没有其他任务过多使用CPU的话,你其他的核心一直保持在很低的利用率,当你ctrl-c你的代码后,那个100%的CUP会立马降下来

然后你在编译一个C程序,使用gcc a.c && ./a.out命令编译下面代码然后运行

// a.c
int main(){while(1){};}

你会发现C也只能消耗一个CPU


未完待续

引言

其实一开始没有想到写关于Python的加速,一开始只想好好了解一下C++这门语言。

虽然我上大学之前就自学过C,但是对于这个C的升级版还是没有过多的了解,花了几天时间学习,发现C++这门语言还是不错的,至少在兼容性上,它能兼容C还有以前的版本。

然而作为一个用惯了了脚本语言的人来说,C++最麻烦的就是他的第三方库管理,当然对于类Unix系统有自己带的包管理器(如ubuntu上的apt,CentOS上的yum)可以来安装第三方库(就是我们平常为了安装一些软件,比如要先apt install xxx-dev那些库),由于这些都绑定了平台的,所以你经常能看到有些软件自己编译会列出各个平台下依赖的包,然而对于一些比较新的库(比如googletest,就得去Github上掏下来自己编译安装了。

吐槽完了C++的缺点之后,我们不得不说C++的优点了,虽然比较难装(相比于脚本语言)但是那个速度真是贼快,用腾讯开源的协程库,单台机器就能开启千万协程而且内存不超过2个G,想我大Python开个一万都很嘚瑟了。C++在性能上真的的碾压的。就是因为C++性能上要求到极致,所以它才会有那么多的前面安装的缺点,因为C++是面对硬件的,对于不同的硬件,C++想做到最快,那么通用的代码就不可能的,通用就代表损失性能。然而让我全用C++写代码是不可能的,脚本语言用的多爽呀。所以了解完了C++的强大之后,我就越发的想了解怎么结合两者的方式,所以就有了这篇博文。

(Python)+(C++)难在哪里

大家都知道Python有很多实现,我们这里说的PythonCPython也是最常见的实现,它是由C语言编译出来的,我们的目标就是把两种语言给混合起来,C+C++

我们看看其他语言,比如Java其实也可以混合C++代码,它是采用JNI的方式来进行交互的,如果你了解这种方式,你会发现也非常麻烦,得先写Java的类,然后再生成C/C++头文件。然后你再写C/C++代码,其实我很讨厌这种方式,我希望能把C/C++和你的语言这两种分离开来,我们能简单通过某种方式桥接一下让两个项目能够连贯起来。

我们现在来看看Python是如何调用C++的代码。在这之前我先提一下PythonC的关系。

其实PythonC一直非常友好,相比于其他语言,Python在支持上一直尽最大努力,因为Python开发者也知道Python非常慢(相比于C,C++,而且还有GIL的存在无法使用多线程密集CPU计算),所以Python开发者直接在内库上提供支持:ctypes,一个专为调用C代码的库。你只有编写少量代码就能让Python运行你的C代码。理论上你碰到性能问题直接写C就行了,但是我们为什么还要让Python运行C++来加速呢

四个字:比C更好,C++由于在性能上与C不相上下,而且比C要高级的多(面对对象等),编写速度与维护上比C更加好,而且要知道现在最流行的Java编辑器都是C++写的,还有很多高性能数据库以及机器学习库都是C++写的,虽然在Python中写C更加简单,但是我们还是希望能够用面对对象的方式来编写代码,毕竟我们主要使用的高级语言也是面对对象的

也正是因为C++提供了一些C没有的面对对象,以及高级特性,这就让我们融合CC++带来了一些困难。

Python为什么能够调用C++代码

我们从调用顺序来看,我们其实想用C代码(Python本质其实是C代码)调用C++C++C要高级,出生的也更晚,所以C其实是不知道C++这门语言的,所以C能调用C++,其实是C++C的一种兼容,这种兼容是C++提供的

C++作为一门偏底层语言,它最终的目的是生成二进制码,C最终也生成二进制码,这个二进制码能直接在CPU里面运行,大家都知道一个代码复用的概念,在二进制层次上,就有这个链接库概率,反正无论谁是最终调用主体,被调用方只需要提供一个规定好的函数库,那么就能实现跨语言的一种交互。

但是这个交互存在一个问题,C++C有着更加特性,比如说类,C没有这个概念,假如C++在动态库里面想让C能够调用一个方法,C根本不知道怎么用,一个类要使用必须牵扯到类初始化,类析构等等。所以C++提供一个关键字extern "C"

这个关键字就是告诉C++编译器把这个块域里面的东西编译成C可以接受的,当然有个前提条件里面代码声明必须是C式的,也就是只能使用C关键字来声明函数结构体什么的,但是在函数内部你可以调用C++代码,声明一个类什么的,最后返回结果。

用一句话来总结这个关键词的作用就是:告诉编辑器和用户,里面的函数东西,不管中间过程,只需要在“开头”(函数声明),结尾(结果返回)是C模式的,那么这个函数就能在C里面用

最后我们总结一下Python能够调用C++的代码的原因:只要C++能够”写”成C代码,我们就能调用。这时候你可能有疑惑了,如果把C++写成C那么我们还不如直接写C代码,何必如此复杂的研究这么久了。但是你有没有想过为什么Python是用C写的,最后却能拥有C++Java这些语言的一样的类特征这个概念。

这里我们必须要了解一个名词“语法糖”,在我们看来我们能在PythonJavaC++中使用一些面对对象的特性,比如类、继承、接口。其实这些都只是一些语法糖而已,在这些实现的底层,比如说Python它就是用C的函数来帮助我们构建这些语法糖,我们看到的一个对象的系统函数,其实它是Python帮助我们把一连串函数绑定在一个module上面,虽然表面上我们新建了一个对象,调用了一个对象函数,其实在C层我们就是调用了一连串的函数来完成一个对象的分配

我们可以在官方文档中找到这部分介绍,官方文档告诉我们只要将列表的函数赋给一个模块(module)我们就让你的C/C++代码给Python一个模块可以使用,从官方文档我们就可以很清楚看到语法糖

Python的文档非常丰富,理论上我们能够根据文档完成复杂的C++代码与Python交互,但是我们从文档上可以看到,这个过程是非常繁琐的,相比于调用C的简单,为了实现调用C++的类和数据类型,我们得写很多中间代码进行转换,差不多就重新写了一遍C++的实现

当然作为以简单为美的Pythoner早就发现这个问题,也就这个问题开发了ctypescffi等框架帮助,就连在C++大名鼎鼎的boost库中也提供了boost/python来帮助Python更加简单的调用C++,接下来我就根据我对下面这些库来谈一谈我的看法

框架简析

单纯的介绍这些库的功能太枯燥了,我就按照我对这些的库的理解将他们编成历史故事(真实出现的原因可能不是这样的)

话说在Python作者设计Python之后,它发现Python实在是有点慢,为了能加速它就把PythonCAPI告诉社区的人让他们自己编写C代码然后让Python去调用它

但是这个API实在是太繁琐了,要写太多附件的C代码了,有些人就发现这个问题,他们设计了一种脚本程序,你只要把你想调用的C函数包在%{里面就能帮你生成PythonAPI的C代码,这样减少了不少代码量,这个框架叫做Swig

大家在使用Swig的时候发现一个问题,这个Swig要生成的一个很大的C函数,C++开发者发现了这个问题,他们跟Python开发者说你们是不是瞧不起C++,这个函数这么不优雅,竟然想跟我们代码混起来,想用C++我们帮你,你要生成什么函数告诉我,我帮你生成你引用一下我这个库就行,这样大名鼎鼎的boost::python就开发出来了

你开心的用起来boost::python来包装一下代码,这样写完C++代码再引入boost::pythonPython需要的函数定义一下,编译,OK,但是Windows用户不开心了,这个boost::python是在boost项目下的一个子项目,为了在Windows安装,还得下几百兆的软件包,要是碰到网络不好得下一天。这个时候Python大牛出来了,啥,这么麻烦,我来开发一个包,把boost::pythonboost的掏出来,你只需要pip一下就行

经过几个”小时”开发,pybind11开发出来了,还是原来的配方还是原来的味道,管他Windows还是Unix,直接pip一下就能使用boost::python 一样的语法来用了

就这样安安稳稳的过了一段时间,大家很开心用Python包轻轻松松解决生成PythonC API代码的功能。但是随着大家用的越来越多,大家发现怎么我用pybind11调用C++跑的有点慢,Python大牛开始研究,重要他们发现由于pybind11由于秉承Python的简单至上,很多东西它都做了”通用性“,比如它帮你自动把C++Vector的类型转成Pythonlist,这样程序在编译时候不会报错,但是由于这种类型转换太多了,严重的拖累了C++的速度,所以pybind11虽然用的很开心,但是速度却比原生的Python C API要慢

这个时候精通编译原理、PythonC++的大牛出现了,它发现解决这个问题的办法很简单,创造一门中间语言,这么语言可以详细的定义怎么从C++Python的中间过程,在pybind11 中这个完全是一个黑箱子,只有把这个黑箱子拿出来,这样我们就知道你想怎么调用C++,这样就能设计更加优秀的PythonC API的代码。最后Cython出现了,它的出现让那些苛求性能的人闭上了嘴,它自动出来的生成PythonC API代码近乎人工编写,在这样强的性能加持下,它的速度近乎原生

至此在生成代码PythonC API的中间代码的三方库尘埃落定,没有人想到有更好的办法来优化这一个方向。但是苛刻的人无处不在,他们攻击不了它的性能,只能攻击它的生成方式

为了使用Cython必须编译它,要么借用setuptools来简单这个步骤,要么自己手动编译,一些开发者叫嚣着,都说Python是个动态语言,怎么还要编译呀,麻烦死了,这个时候一些开发者就站出来了,他们觉得这是个挑战,他们想解决掉它,于是cffi被开发出来了,你不需要用专门的文件存贮C/C++代码,你可以像调用函数一样把C/C++函数原文作为参数传进去,实现动态加载,但是这种动态性还是付出了代价,速度有了一定影响,虽然还是比Python快,但是远远比不上Cython,有得必有失

PS: 在这里我没有提ctypes因为它是原生的,而且它对C++支持并不很好

总结

在使用这些三方库来帮助我调用C++过程中,我最喜欢的就是pybind11Cython,前者能很简单的将现成的C++项目变成Python项目,后者在性能上独步天下。然而由于Cython还得学习它自己的语法,所以我在这里的建议是,如果可能尽可以用pybind11来开发,如果性能要求严重,可以慢慢把一些函数用Cython代替

本来是想从一个项目出发介绍pybind11Cython但是写着写着就变成很长了,所以就把它放到介绍PythonGIL的博客,顺便介绍一下在 C++如何将GIL”揪“出来

引言

这两个星期的工作主要是对千万文本数据的处理,由于我以前没有接触过类似的数据量,所以我就把我在处理这千万数据的过程中遇到的问题以及解决的方法总结一下

明确目标

完成任务之前我们必须要明确自己的目标,首先谈一下数据,数据是两张表,一张是文章列表,一张是文章内容,每篇文章都牵涉到一些人,我们的目标就是给定一些搜索条件然后把最可能相关文章给找出来

这个任务有点像实现一个搜索引擎,我们通过输入关键词把相关的网页寻找出来,简单点来实现就是直接使用SQLLike查询,但是这里存在两个很大的问题

  • 搜索精度不准,假如我们搜张华可能有关张华硕的人也会出来
  • 搜索耗时太长,在千万级文档中全文搜索速度非常慢

我们希望我们能精确的实现查询,而且我们希望我们的查询能够实现毫秒级的速度。所以我们就尝试使用ElasticSearch来当我们“数据库”,并且放弃系统默认的分词,自己”手动分词“,来实现精准快速查询

所以我们的目标很简单,将数据从MySQL“塞”到ElasticSearch中,然后想办法再”取”出来

第一个拦路虎“MySQL”

我碰到的第一个拦路虎是数据库的响应速度,为了将数据完整的从数据库里面取出来(新数据还在产生),我按照id从小到大的顺序一小块一小块的从MySQL中获取出来

一开始程序运行的挺Happy,速度一直很稳定,但是我发现跑了十万之后速度突然慢下来,一开始我以为解析有问题,我开始打断点,调试,找了半天原来是数据库返回数据太慢了。

我们来分析一下这个SQL为什么这么慢

select * from a order by id limit 10 offset 100000 

我们虽然限制返回了10个但是后面有个条件我们必须要后面10万个,为了拿到这10个,MySQL必须要扫描10万个数据先,虽然我们是在主键上扫描会快一点,但是十万毕竟很大,即使一次主键扫描花0.01ms,乘以十万也是很大的

基本上每次解决数据库速度的时候,我们第一考虑点就是索引,那么我们这里就多聊两句:索引为什么快?

数据库其实就是一堆数据的集合,它提供工具我们快速获取我们想要,用图书馆来打比方,数据库就是图书馆,他们把所以的图书分好类,你想买什么书,按照分类去寻找就行,这样假设你图书馆有一千万本书,你要找《安徒生童话》,你只要按照这个索引(童话书>丹麦>安徒生)就能找到,假如你想找一本《无类》的书你不知道他的分类,你就得把整个图书馆逛一遍才能找到你要的书了,这就是没有索引的下场。

在程序的世界里也一样,你想快速找到一个记录,如果不用索引,那么就得遍历了,运气好一下子就能找到,运气不好一辈子也找不到。在 MySQL中,索引的背后就是B+树,也就是将数据查找最坏结果降到了一个log2N级。如果你想了解“树”为什么这么快,可以看看我前面写的的博客

怎么来说明这个索引的作业呢,假设你有一千万数据,你最坏的情况下要进行24次查找,24:1000000 达到惊人的41万倍差距,而且当数据越大这个差距越大,从这里我们就知道索引的威力了。

我们回到前面,为了使用索引,那么我们只能在id(主键)上做手脚了,我第一个想到的是按照id分块,但是我仔细看了看数据库,id不是全部连续的(可能是因为删除过数据),假如我用id固定的区间来的话,获取到的数据可能部分有部分没有,虽然能够实现但是不够优雅,我还得增加处理空数据的代码。

这时候我想到了,我们第一次获取的id如果能在后面继续使用,而且更新的话那么我们就能使用上索引了,所以我们只有把每一块数据的最后一个id记住,然后去这个id获取下一批数据,这样就能实现又用索引又不用改太多代码。

那么我们的SQL就改成下面的语句

select * from a where id > 1111 order by id limit 10

通过简单的测试原来需要几分钟才能“掏”出来的数据在几毫秒就取出来了,上万倍的差距。至此我们第一个拦路虎就解决了

PS:在后面看ElasticSearch文档的时候发现他们也提供了一个scroll(全库获取)的超级翻页功能,在他们的参数里面也要提供一个scroll_id,感觉原理应该也是和这个差不多。通过使用索引id来加速“翻页”

ElasticSearch存贮和排序

接下来我就介绍,我怎么优化存储和编写定制动态DSL来实现我们想要的功能。

在我介绍之前,我先简单的谈一下我对ElasticSearch的理解

ElasticSearch简介

在我没有真正使用ElasticSearch之前,我就在知乎上听过它的大名,ElasticSearch真正让我震惊的是当我把上千万数据导入到它里面去,它能在毫秒级别给你响应,而我在MySQL调用SQL进行查询得花几十分钟

我们可以把ElasticSearch类比成一个数据库,相比于MySQL它在查询性能上做到了苛刻的,我一开始想好好介绍它是怎么做到的,但是我发现有人已经总结的非常好了,可以看看这份资料,它之所以能做到这么快的原因就是这个:索引+内存+缓存

ElasticSearch使用倒叙索引让查询时间复杂度降到logN级,使用内存让物理查询速度达到极限,加上一些过滤缓存让其在复杂查询还是简单查询都能保持在一个很平稳的速度

ElasticSearch相比与MySQL还有一个特点,就是对大文本搜索的支持,ElasticSearch对文本默认自动进行分词,并且通过一些高级分类算法(TF/IDF,5.0后使用更加先进的BM25算法),对匹配的文本进行打分,依次返回得分高低列表,而MySQL在大文本检索只有一个全文索引支持,从实现上来看就是一个加了索引的Like查询,所以ElasticSearch在设计的特定算法加持下被称为“搜索引擎”

但是ElasticSearch同现在商业的搜索引擎,比如Google、百度、Bing这些又有些不同,ElasticSearch传入的是纯文本,所以它只能使用一些TF/IDF算法来计算给定关键词与文本的相关项,但是现在商业引擎输入的是网页,所以现在商业引擎比如Google就使用Google Page Rank算法来再次计算文档相关性。当然现代商业引擎不单仅仅使用Google Page Rank算法,他还会考虑更加因素进去(比如百度的竞价排行,Google的恶意影响网页排行检测),但是从本质上来说,无论是ElasticSearch和现代商业引擎都在做同一件事,给匹配项打分,这就是他们与MySQL的全文检索的不同(MySQL没有后面打分排行的概率,他只有order by的这个概率)

设计思路

一开始我准备直接使用ElasticSearch的搜索引擎来对文章中的涉及到的人进行检索排序,但是我们来考虑这样一件事,假如文章中存在这么一句话:“刘二能吃两碗饭”(涉及到的人是刘二能)。假如我们使用“刘二”去检索,这篇文章中的”刘二能“也能检索到,而我们的目的就是尽可能返回最可能的结果,对于那些不可能的结果一律不返回

所以我们就不能让ElasticSearch自动帮我们对文本进行分词,但是我们想利用打分这个机制帮我们完成最可能在最前面返回

所以我们把每篇文章里面的人物解析出来的属性(姓名,出生年月,民族等)设定为keyword类型,这样ElasticSearch就不会对这个字段进行分词,查询的时候也必须全匹配才能命中,由于一篇文章可能设计到多个人,所以我把它用一个list存到一个document里面

但是这个又引起了另外一个问题,对与一个document里面的listElasticSearch会把它进行转换

我们用官方文档的例子解释,我们存入了下面这个document

{
  "group" : "fans",
  "user" : [ 
    {
      "first" : "张",
      "last" :  "华"
    },
    {
      "first" : "李",
      "last" :  "四"
    }
  ]
}

ElasticSearch会把它转换成

{
  "group" :        "fans",
  "user.first" : [ "张", "李" ],
  "user.last" :  [ "华", "四" ]
}

这样你查询这个人张四,我们发现上面这个文档也返回了(选了user.first列表的第一个值,user.last的第二个值,这个结果明显是错误的,我们怎么才能避免ElasticSearch的“自作聪明”呢,答案很简单我们把user声明为nested对象,这样ElasticSearch就不会把它拆开了而是把它当做两个文档(有些人可能会说这个会不要影响它的速率,恰恰相反,ElasticSearch会经常使用类似技术来加速,详情可以看上面的博文)

现在我们解决了重重困难终于要到排序的阶段了,然而我们没有使用string类型(支持TF/IDF算法)而使用了keyword类型,导致我们没有办法使用ElasticSearch提供的高级排序算法,所以我们得自己手动进行提分,怎么来提分呢,很简单使用boost

前面我们提到了我们的文档可能解析出来对象多个属性(姓名、年龄、性别、居住地),但是有些文档也可能没有这些信息,我们查询的时候是有一个信息列表(这个人姓名、年龄、性别等等),所以我们使用boost对命中的信息越多的进行提分,所以我们最终就能完成命中越多信息的排在越前面,当然命中信息少的也会被筛选出来只不过位置稍微靠后

总结

通过这次直面千万数据,让我学习到了不少,虽然一开始目的只想简单搜索出来最匹配的数据,但是在实际过程中,通过不断对产生结果提出问题,最终实现了一个比较满意的产品,整个产品在不断的优化过程中逐渐成型并且稳定,我觉得对我帮助最大就是撰写设计文档,并且在产品成型的过程中把结果反馈上去,最后慢慢迭代一个最好的版本

毕业近两个月了,然而这两个月对自己的状态不太满意故写下这篇博客反思

引言

大约是去年十一月在室友的带领下走上了健身这条路,说起来也搞笑,当初室友邀请我去健身,然而我每天享受免费操场跑步,反而对于要花钱的健身嗤之以鼻,虽然我知道那些肌肉硕大的大块头都是从健身房出来的,但是我一直没有想过去练出他们一样的肌肉,我运动本身的目的就不是为了炫耀什么的,主要是想要一个健康的身体,然而南昌十一月妖风阵阵,虽然还没到冬天,晚上妖风能让你吹到感觉到冬天的气息,然而中午却非常热,我尝试过早上跑,但是我个人体质太容易出汗了,跑完就需要洗澡,然而早上没有热水(尝试过冷水,差点让我冻僵)

正好这个时候室友每天去健身,在我的印象里健身房好像有跑步机,于是我一狠心就花了钱办了一张健身卡,刚开始我是奔着跑步机去的,但是我们班长拉住了我开始教我健身动作,我也是在室友和班长的带领下走上了健身这条“不归路”,可怜的跑步机我就没有用过几次,本来一开始是奔着跑步机去的呀。

为何健体

到现在自己已经在三个不同的健身房办了卡了,扯远了,健身的确是一件神奇的事,我一直以为运动会让你瘦下来,没想到健身却让我“胖”起来,当我健身到六个月时,我们班长和室友惊奇的告诉我,我的胳膊整整“胖”了一圈,他们还记得我刚来健身房的时候胳膊细的同木棍一样,然而健身对于我来说却并没有很多不同,我只是找到一个地方可以挥洒我的汗水,找到一个地方可以突破自己的极限

健体而不健心

健身真的是一件很有成就感的东西,它的成就感不是在于别人夸你的肌肉有多大,而是你能清楚的找到自己的极限,我原来是一个脑洞很大的人,每天大脑天马行空,像一个中二少年一样总想着自己是“超级赛亚人”,能像绿巨人一样发怒把汽车给掀开,一拳把树打倒。当你在健身房,你拿起一个5KG的哑铃,然而你倾尽全身力气却没法做一个标准的动作,你这个时候才知道自己不是”超人“。健身让你更加了解你自己,慢慢的你就接受了这样的自己,健身的成就感也就在于你能慢慢的感受自己的成长。

也可能是过于关注自己的身体的进步吧,而自己一直忽略了自己心理的进步。自从踏入社会,开始工作。对于工作我一直是打着自己十二分的精力去做,然而对于工作结束后的那段时间自己一直没能够充分利用。主要的原因就是自己的不够自律。一方面也有自己身体的原因,自己健身都是在下班后,健身完之后自己总是筋疲力尽,本来需要充分利用的时间全部被自己的羸弱的意志给消磨掉了,虽然自己一开始能够控制自己,但是一不小心大脑发出疲惫的信号,慢慢的自己就变成“葛优躺”,大把时间就在不经意的打开手机玩上两把,我看一下微信,再看一下QQ,大块时间被手机打碎成一小块一小块,搞得一晚上下来该做的没做,不该做的做了一大堆。

在这里我必须要深刻的反省,即使是因为身体疲惫的原因,最主要的还是自己不够自律的原因,我其实有很多想法,有很多想做的事情,但是总是在不自律的自己把时间给浪费掉了,毕业之后书自己也很少看了,记得以前在图书馆捧起一本书津津有味的看上一晚上的酸爽。然而现在的自己,即使是看书也要打开电脑,左手拿着手机,右手捧着书,眼镜盯着电脑看一会,书再看一会。书走马观花,看了也是白看。原来经常更新的博客也停下来了。

总结

自己也不下什么军令状了,我自己不是一个圣人,我会松懈,我会娱乐,但是我需要自己记住不要因为自己身体的虚弱而导致自己内心的失防,过去自己一直在因为自己过分迁就“健体”而忽视”健心“,自己要牢记只有内心强大才能强大。只要未来的自己不要因为过去的蹉跎的后悔就够了。

这段时间没有写博客,一个原因是由于刚毕业没了学校的学习动力反而下降,另外一个方面由于花了很长时间研究编译原理,然而自己却对它没有太多理解,所以也就没有整理自己的知识,现在慢慢稳定下来,会继续像以前一样更新博客

引言

为什么要介绍XPath呢,我一直以为我对XPath还是比较了解的,但是随着我对XPath的了解越来越深,我就对它的越来越敬佩,

简单来说,我以前认为XPath对结构性文档只能是一把“枪”,指哪打哪,没想到它是一个“巡航导弹”,自动追踪目标。

接下来我们就慢慢从XPath的基础来谈谈其威力

什么是XPath

首先我们要知道XPath是一种语言,你可以理解它是正则、也可以理解它是SQL,他们的目的都是从数据中找到我们想要的东西。相比于SQL从数据库中获取数据,XPath是从一个XML文件中获取数据。

好的,我们知道XPath要操作的对象,什么是XML,它是一种结构式文档,我们也可以把它看做一种树结构。

<root>
    <son> I' m son </son>
</root>

如上面就是一个简单的XML文档,首先从一个父节点点出发,到最后的一个父节点结束,中间可以有很多子节点,也可以有孙节点,但对于每个节点来说,其父亲只能有一个。

这种文档的出现是由于我们编码程序中树结构出现而出现的一种数据。相比于正则直接操作文本,XPath要面对的是是一堆有规律的文本,虽然我们也能使用正则来操作XML文档,但是正则无法捕获这种XML的关系,而这个也是XPath最有力的地方。

在这种关系中,我们最常使用也是最核心的就是父子关系,这个关系简单的通过一个/就能体现,比如现在我们把上面的XML复杂化给他添加一个儿子

<root>
        <son id="1"> I' m son1 </son>
        <son id="2"> I'm son2</son>
    </root>

为了获取第二个儿子我们简单的使用这个XPath语句就能获取到

/root/son[@id='1']

PS: 当然在XPath中我们可以使用//来代表一个泛指,通过//son[@id='1']我们可以把儿子找出来而不关心他的父亲,甚至更进一步,我不关心它是谁,只要它的id1就行,我们用node()函数来替代一个节点,这样只要//node()[@id='1']就能拿到id为1的节点了

我们来看看这个XPath,我们定义了一种关系rootson的父子关系,XPath的威力就是能用很简单的语句来定义一个节点的关系,在这句中,rootson都是节点,我们使用/来约定节点父子关系,使用[]来定义节点与自己内部节点或者属性的直接的关系(@是获取属性)

要掌握XPath必须要明白,XPath重要的就是“面”和“点”的关系,“面”代表节点,“点”代表属性,对于面来说,它可以包含很多个点,对于点来说,它有可以看做由很多个更小的面组成(微观上)

就以上面的例子,对于rootson这两个节点,其中root是父节点,我们可以用很多个属性来定义它,比如root[count(son, 2)](意思是选择有两个sonroot),其中对于父节点关系的中son节点来说(有点绕),他又可以用属性来约束比如root[count(son[@id], 2)](意思是选择有两个sonroot,并且每个son都有id这个属性)。从这里我们可以看其实节点和属性是可以相互嵌套的。

从上面这个小例子我们可以看到,XPath的威力就是它可以用来非常详细的约束节点与其他节点或属性的关系,这种关系可以是绝对的,也可以是相对的,一切取决你的取舍,绝对代表严格,相对代表宽松。

PS:当然我们这里的属性是一种宽泛的理解,在XPath中节点还包括text值,我们可以把它看做节点的一种text属性。

XPath的其他关系

前面我们介绍了XPath中最重要的一种关系:父子关系。这个也是我们使用XPath使用的最主要的一种关系,现在基本上网络上的教程都是基于这种关系的,我们这篇博客主要不详细介绍这种关系,你可以在w3cshool上了解更多内容。

我们先用问题来引入其他的关系,我们再把上面的简单XML进行修改

<root>
    <son id="1"> I' m son1 </son>
    <target> son1 target</target>
    <son id="2"> I'm son2</son>
    <target> son2 target </target>
</root>

我们引入两个目标,现在我们想拿到son(id为1)的旁边target,假如我们使用父子关系,使用/root/target[1](XPath索引从1开始)也可以获取到,但是这里引入了一个约束,必须是root下第一个target节点,假如这个XML它是随机的,sontarget是一个集合,但是他们的位置不定,这个时候我们不能仅仅依赖父子关系来确定节点位置。

这里我们引入兄弟(sibling)这个概率,sontarget是一队兄弟,我们能通过知道son的位置从而定位到target的位置,那个这个XPath该怎么写呢,首先我们要确定son的位置

/root/son[@id='1']

接下来我们通过定位的son来拿到它后面的兄弟(也有前面的兄弟语法)

/root/son[@id='1']/following-sibling::target[1]

在这里/following-sibling代表它要找到接下来的兄弟,后面::target[1]是进一步限定我是要拿到兄弟里面第一个target,我们可以通过这个网站在线测试一下我们的XPath

当然我们可以通过第二个son来找到它前面的兄弟,对应语法是下面的

/root/son[@id='2']/preceding-sibling::target[1]

在前面我们可以看到这个following-siblingpreceding-sibling他们都是一种寻找兄弟关系的,其实假如我们把-sibling去掉,他们能更加宽泛。

我们把sontarget包起来,这个在现实中可能更常见

<root>
<group>
    <son id="1"> I' m son1 </son>
    <target> son1 target</target>
</group>
<group>
    <son id="2"> I'm son2</son>
    <target> son2 target </target>
</group>
</root>

假如我们还使用上面的语句,我们会发现,我们没法找到语句,这个时候你把兄弟这个约束去掉

/root/group/son[@id='2']/preceding::target[1]

你会惊奇的发现XPath准确的找到我们的目标,这个令人震惊的是它能实现一种“翻山越岭”的查找。

假如你使用正则或者普通的父子关系,你必须先找到它的group然后再使用for循环来遍历所以的group找到son….

总结

我们使用简单一个前后关系就能轻松实现上百行代码,当我以前不了解这个XPath的关系约束前,为了寻找这个约束写过几十行代码才能定位,而现在简简单单一行就搞定,我们不得不佩服前人的智慧,我只想说一句“真香~~~”。

引言

之所以有这个疑问,是上次阅读Java基础书时碰到讲解char类型没有看明白,并且在代码验证过程中错误的理解了代码的意思,导致我对这么个简单问题产生疑惑并且“恶意揣测”Java内部的黑魔法,这里就把我如何走上歪路,并且最终找到“正确”的道路的故事讲出来

问题的产生

我们知道Java是采用Unicode进行内部编码,但是使用UTF-16作为外部编码。

怎么来理解这个东西呢。首先你要知道Unicode是在我们熟悉的GB 18030BIG-5ISO8859-1之后出现的,它的出现就是为了统一全世界的编码,因为前面这些编码都太片面了,只包含自己国家或者少数几个国家的字符。

Unicode的目的就是包括全世界的编码,并且给未来可能出现的编码留下位置,你可以理解为它是一张大“表”,一般我们使用16进制来表达它,并且在前面加上U+。例如U+0041代表字母A,但是这里有个历史问题

一开始我们知道Unicode为了包含全世界的字符从ASCII的一个字节扩展到两个字节,就能包含65536个字符了,但是随着字符包含越来越多,我们逐渐需要更多字符了,最后扩展到U+0000 -> U+10FFFF去了,为了表示这些我们必须使用三个字符,假设我们不考虑内存成本,每个字符都使用四个字符来表示(不使用三个是为了内存对齐),那么问题就解决了,大家都用Unicode来表示,这样我传给你一串字符你就能秒懂了。

但是学过信息论就知道,单字符越长信息熵也就是信息量就少,其实在日常通信中我们并不是每个字符都会用到,为了提高效率,我们可以使用霍夫曼、香农编码技术对信息重新编码,这个就是UTF-8UTF-16等现代编码的理论基础。

这就好比特种部队手势,我们把作战命令(Unicode)需要的指令放到手势(如UTF-8)里面,这样几个手势就能表达复杂的作战计划(假如用嘴巴说的话)。

接下来我们就从JAVA和Python来看,编码与其关系

表面兄弟:JAVA

Unicode对于JAVA来说,只能算是表面兄弟,虽然内部支持Unicode编码,但是其本质还是基于UTF-16编码,为什么要这么说呢。

我们来回顾一下,我们知道Unicode的范围是U+0000-U+10ffff,这意味着我们没法用两个字节来表示,但是在Java里面char类型字节为2字节,而对于字符串类String来说,其组成就是一个char字组,对于小于U+10000Unicode码来说,String对象最小组成单位就是char,但是对于大于U+10000Unicode码来说却是char数组,我们用代码来展示一下两者之间的关系。

char[] chars = Character.toChars(0x1f121);
String s = new String(chars);

而且我们将s输出的话,会发现它是一个字符,但是它的length却为二,而且我们将s每个字符转换成二进制你会发现他们的值依次为0xd83c0xdd21,他们存贮的值全部以UTF-16的格式存贮,具体编码详细我就不细说了,下面资料介绍的很详细(需要翻墙)。在Unicode里面占一个字符的值,却以两个基本类型存贮,当然为了维持这种“表面兄弟”的关系,Java也使用了“码点”来支持一下兄弟,只要使用codePointAt代替charAt,用codePointCount代替length,我们也能处理超过U+10000Unicode编码(对于不超过U+10000的字符那就是“真兄弟”)

当我不知道一个char只能放两个字节的时候,我强行使用char c = (char)0x1f121来“存”一个超过U+10000Unicode码,结果被Java无情的溢出掉,只取到了部分值,但是我却误以为Java有黑魔法能用两个字节存贮了三个字节才能存下的值,乃至我闹了个笑话。

总结一下Java是一个非常严谨的语言,规定死的东西就不会变,表面上看Java能够支持Unicode编码,但是实际上他只是编译器支持,比如你写一个🄡(0x1f122)的值来赋给String如下面:

String ns = "🄡"

表面上看,Java完全支持Unicode码,但在实际的上面他内部还是用UTF-16进行编码,只是在编译的时候帮我们将0x1f122转换成为两个
0xd83c0xdd21存贮在char字符组里面。

其实这个表面兄弟是相对的,从Python3``Unicode支持来比较一下就能发现不同。

亲兄弟:Python

Python3Unicode是非常友好的,它在明面上完全按照Unicode的编码表使用来存贮Unicode码,对应它的Unicode字符串,最小单元都是Unicode码,多说无意,上代码。

  c = chr(0x1f122)
print(len(c))  # = 1
print(type(c)) # str

我们可以看到我们得到的最小的码元是字符串str类型,无论这个Unicode码是否大于U+10000Python都把它视为一个基本单位,这样避免了你对其进行一些误操作,插句话来讲讲怎么得到这个大小呢,我们使用sys.getsizeof方法就能计算出来

sys.getsizeof(chr(0x1f122))  # 80
sys.getsizeof(chr(0x1f122) * 2) # 84

由于Python使用一些字段来标注类型,所以直接使用sys.getsizeof得不得一个Unicode码需要的字节,所以我们计算两个的差,很清楚的就能得到一个Unicode码使用四个字节,你可以依次乘下去,而且你发现一个有趣的现象,对于小于U+007FUnicode码,其大小为一字节,而对于U+0080-U+07FF其大小为两字节。具体可以看参考资料Python内部是使用UTF-8来存贮Unicode码的,但是Python将这一切都隐藏起来,你从表面上看好像一个Unicode就是一个最小单元,对于其底层我们不得而知,我们可以从侧面来验证一下

 timeit.timeit("'中国人'.encode('gbk')")
>> 0.6366317819999949
timeit.timeit("'中国人'.encode('utf-8')")
>> 0.2109854949999317

我们可以看到将Unicode编译成其他编码方式,其中utf-8速度是最快的,因为基本上是复制一下就行了,而其他的差距到了三倍

总结

通过前面我们知道,Python之所以 Unicode如此“亲兄弟”是因为做了一层封装得来的,相比JavaUnicode码(使用UTF-16作为底层编码)暴露给出来,Java在底层上却是非常“坦诚”,你想直接使用Unicode码值也可以,Java编译器会帮你把Unicode码值转换成UTF-16,你也可以从UTF-16码生成String字符串,这样底层在实现查找的时候也是使用统一的编码进行。但是也正是由于这么“底层”,代码看起来总不是那么“亲”,相比于Python的“一视同仁”,我们也可以理解这就是这两种语言的各自特点所在。

总的来说如果你想直接接触代码底层,推荐使用Java,假如你只想研究其本质,推荐使用Python来进行自然语言处理,他的封装能让你不需要了解其内部组成。

引用

https://zh.wikipedia.org/wiki/UTF-16

https://en.wikipedia.org/wiki/UTF-8

Spring的IOC理解

什么是IOC

在这里我们不谈Spring的基础知识,我们知道谈到Spring就会谈到IOC,这个IOC是什么呢,中文名叫控制反转,这个东西是伴随着一些编程思想出现,其实同Java的本身也有关

就好比我熟悉的Python就是一个鸭子语言,你可以随便把一个值丢掉函数里面去,只要他满足一些特性就能正常运行,但是Java是一种强类型语言,你函数给什么参数,必须传什么参数

这里就不讨论两张语言的设计优劣呢,Java这种特性也做了一些妥协,我们肯定得为语言的扩展性做点事,谁也不知道未来会发生什么,Java里面使用多态来实现这种扩展,只要他是函数参数的家族成员,他就能上去运行

这个多态是实现IOC的基础,但是造成他出现的原因是因为设计模式里面的单一职责原则,这个要求我们类功能要单一,我们这里给一个例子来说明这个问题

class Car {
    void run() {
        System.out.println("Car running...");
    }
}

首先我们有一个Car的类,一开始我们只让他有run这个属性,很好,接下来我们想知道是谁驾驶这辆车,于是我们便给这个类加一个字段driver

public class Car {
    String driver;
    public Car(String driver) {
        this.driver = driver;
    }
    void run() {
        System.out.println("Driver :" + driver);
        System.out.println("Car running...");
    }
}

很好我们知道驾驶这辆车的人,接着我们又想知道这个驾驶人的驾龄,如果我们继续给Car加入字段,这样我们就违背了单一职责原则,Car类不但承担了车的功能还承担了人的功能

于是我们就把驾驶人隔离出来

class Driver{
    String name;
    String age;
    public Driver(String name, String age) {
        this.name = name;
        this.age = age;
    }
}


class Car {
    Driver driver;
    public Car(Driver driver) {
        this.driver = driver;
    }
    void run() {
        System.out.println("Driver age:" + driver.age + "name: " + driver.name);
        System.out.println("Car running...");
    }
}

我们重新将类分成两个类来实现了这个问题,但是这个时候又来了一个问题,我们有一个飞行员的也想驾驶这辆车,但是这辆车只能司机来驾驶,但是飞行员和司机开车的动作步骤是一样的,为了复用run这个函数,你开始揪起了你的头发.

你想呀想突然想到,Java的多态,假如我们声明一个IDriver的接口,让飞行员和司机都继承这个类这样我们只要给车一个IDriver对象就能复用run函数

//IDriver.java
public interface IDriver{
    String getName();
    void setName(String name);
    int getAge();
    void setAge(int age);
}

// Driver.java
public class Driver implements IDriver{
    String name;
    int age;


    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public int getAge() {
        return this.age;
    }

    @Override
    public void setAge(int age) {
        this.age = age;
    }
}



// Aviator.java
public class Aviator implements IDriver{
    String name;
    int age;


    @Override
    public String getName() {
        return null;
    }

    @Override
    public void setName(String name) {

    }

    @Override
    public int getAge() {
        return 0;
    }

    @Override
    public void setAge(int age) {

    }
}


//Car.java
public class Car {
    private IDriver driver;

    public void setDriver(IDriver driver) {
        this.driver = driver;
    }

    void run() {
        System.out.println("Driver age: " + driver.getAge() + " name: " + driver.getName());
        System.out.println("Car running...");
    }
}

我们重构代码把Driver抽象为接口,然后让司机和飞行员都继承它,这样不管我们再添加什么其他的人就能适配这辆车.这个就是依赖倒置(DI)的思想

网上大部分教程就停留到这里了,这里我们继续探索下去,看看Spring是如何让这个DI更加简单的

首先我们反思一下,我们使用接口参数让我们的代码符合了设计模式,但是也带来了一些繁琐,我们来用代码”开”这辆车

IDriver driver = new Driver();
driver.setName("allen");
driver.setAge(18);
Car car = new Car();
car.setDriver(driver);
car.run();

PS:当然可以把赋值放到构造器中减少代码,但是由于Bean依赖方法接口来赋值,所以为了后面讲解Bean这里就不采用构造器来减少代码

代码有2行变成了6行,而且我们发现这个代码现在带来两个问题:

  1. 每次运行都得创建一个实现IDriver的对象
  2. 每次我们想换人开车的时候都得修改源代码

而且这些工作都很繁琐,作为一个偷懒的程序员,我可不想给每个用户都重新写一套代码,我们的想法很简单,我们希望这个Car能够开箱即用,其实前面我们已经实现了控制反转了,现在就是要解决控制反转带来的“负面影响”

而且我们发现了一个问题,假如我们把上面函数放到一个代码里面,每次我们“开车”都得创建一个司机,然而我们还是相信“老司机”的手艺,所以我们也希望是否能够”记住“司机,只让一个老司机开车

接下来就是隆重介绍SpringBean的用法了,前面我们知道我们需要某种机制来去除”IOC“的弊端,我们把每个Car当做一个对象,其实我们需要一个配置文件来记录IDriver这些依赖对象,对象的其实在Java里面表现就是一棵树,所以通俗来讲我们需一个”树结构“数据来存贮依赖关系

我们程序在运行的时候解析这个树结构,然后依次给对象注入你想给他实例话的对象(比如你把”IDriver“设置为飞行员),这样的话,我们把依赖关系成功放到了配置文件中

这样带来两个好处:

  1. 想给不同用户使用软件时候,源代码不需要改变,只要给他们不同的配置文件就行
  2. 我们可以保存依赖实现”老司机“的复用

所以现在我们理理思路,我们需要的有两个东西

  1. 配置文件
  2. 一个加载配置文件并保存依赖的对象

SpringBean中这两个分别对应xml文件和实现ResourceLoader接口对象(有多种实现)

为了更好的理解Bean,接下来我们就从代码出发来测试这个Bean

最简单的实现

首先我们新建一个Spring项目,无论你是用IntelliJ还是Eslipse都没关系,只能你能引用到Spring库就行,我们复用前面的代码,看看使用Spring Bean来如何解决掉IOC的”副作用“

我们把前面的类分别放到同一路径不同的文件夹中,接下来我们先创建一个xml文件,什么名字不重要,我们这里先命名为driver.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans.xsd">


    <bean id="car" class="Car">
        <property name="driver">
            <bean class="Driver">
                <property name="name" value="Allen"></property>
                <property name="age" value="18"></property>
            </bean>
        </property>

    </bean>
</beans>

写入这些东西,接下来我们看看是否能够通过这个xml文件来直接得到一个配置好司机Allen的车

随便新建一个类在上面的路径中,我们这里就新建一个Main

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;


public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("driver.xml");
        Car car  = context.getBean("car", Car.class);
        car.run();

    }
}

输出为

Driver age:18 name: Allen
Car running...

我们成功通过一个xml配置文件和一个ApplicationContext对象实现了一个即开即走的车,而且假如我们想换个司机,我们可以修改配置文件把class换成“飞行员”,而且我们可以发现我们得到的司机都是一样的,验证方法很简单,我就不写代码了,假如我们想换司机怎么办,简单在bean里面加上scope=“prototype”就行(默认值为singleton

接下来我们又有一个疑问,假如我们有一辆特别宝马车,我们希望只有某一种加上员能能开(假设只有飞行员),也就是是说,我们其实即不想放弃IOC,但是又不想将这个配置写到Bean里面去,有办法能够解决吗?

当然有,Spring2.5就支持注解来写Bean配置,对于一些固定的类,我们可以把依赖关系用代码写到类中,这样一方面能够保证IOC,一方面又能实现Bean xml文件瘦身

由于Spring默认不会去扫描注解,所以有三种方式,第一种是在xml里面用加上一个

<context:component-scan base-package="...."></context:component-scan>

第二种是使用AnnotationConfigApplicationContext来对象来进行扫描,第三种就是SpringApplication来运行Spring程序自动扫描

这三种方式假如你最后要做一个web程序的话,第三种是非常方便的,这里我们就不谈怎么使用注解来代替xml文件了,本质上是一样的,其实在我没有理解Bean的强大之前,我比较推崇使用注解来写Bean,但是随着对Bean的探索,我发现xml文件才是最佳选择,他将程序依赖与代码分离开来,假如我们还想用程序依赖写在代码里面,那就违背了Bean的设计初衷

如果你想了解怎么使用注解可以阅读这篇博客

总结

至此,我们从问题的出现到问题的解决探索了IOC背后的故事,但是你可能会有一个疑问,为什么Spring里面会有IOC问题。

其实这个也跟Web的发展有关,我们知道从Web的发展,一开始是没有前端的,只有后端,慢慢的后端分离出来前端,Web端页面也被分离出视图层和数据层,随着逐渐分离,也就出现我们前面举到的例子,类越来越多,比如视图层依赖数据层,数据层依赖控制层…..

这种层层依赖的问题延生出来的IOC的提出,也就慢慢的促进了Bean这个库的开发,也正是因为Bean我们才能享受静态强类型语言的低耦合的酸爽。

引言

本文是学习Tensorflow官方文档的过程中的一点感悟,本文假设你对矩阵运算有一定的了解,具体可以看看下面资料

加载数据

首先我们得先把数据下载下来,Tensorflow给我们提供了一个函数来进行下载,这个函数read_data_sets

这个函数read_data_sets函数很简单,查看在目录下面有没有文件没有就去下载,有就解析加载,一方面方便我们获取数据,一方面方便我们直接开箱即食,但是由于这个默认下载地址是需要翻墙,所以我这里提供一个不需要翻墙的地址,你只需要加载下面的函数

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets("input/", one_hot=True, source_url="http://yann.lecun.com/exdb/mnist/")

等几分钟,数据就会下载到当前目录的input文件夹中,这样你下次运行就能直接本地文件夹中加载图片数据了

观察数据

首先我们看看下载了什么数据,打开input文件夹,我们可以看到,Tensorflow给我下载好了四个文件,分为两组,一组训练集一组测试集,每组里面2个文件,一个是手写图片文件,一个标签文件(每张手写的图片代表的数字)

加载图片数据对于新手来说挺麻烦的,为了让我们专注于模型而不是编程,Tensorflow直接帮我们做好了加载数据的事情,我们上面得到的mnist变量里面就存贮了我们这个项目所需要的数据,我们来看看这个mnist有什么

我们最关心的就是mnist里面训练数据,这里推荐使用notebook来操作这个数据集,我们首先mnist的训练数据是什么

mnist数据来源网络

mnist数据就是上面这些图片,我们把图片把每个像素的二值化,然后把他们放到一个数组中,每张图片对应一个数组

mnist训练数据存贮在这两个变量中

mnist.train.labels
mnist.train.images

其中mnist.train.images是一个(55000, 784)的二维数组,其中mnist.train.labels.shape是一个(55000, 10)的二维数组,现在摆在我们面前的其实很简单,通过55000个图片像素值来训练我们模型,以便能让模型能给一张图片像素值来预测图片代表的数字

这些数字在人看来非常容易辨认,但是怎么能让电脑也能辨别他呢,这就要用到卷积神经网络的力量,通过卷积神经网络,电脑的准确率能到99%,这就非常恐怖了,我们人有时候也会看走眼呢。

在谈卷积之前我们先谈谈我们以前的做法,这样通过对比就能知道卷积到底做了什么优化

传统做法

其实从传统的角度来看,其实图像识别也就是通过图片的特征值来判断图片代表的含义,但是图片这个东西又很特殊,相比于其他机器学习问题,他的特征值很多,这里我们使用28X28的图片就有784个特征,如果我们图片尺寸再大,这个特征值会变得非常巨大,而且我们知道机器学习需要大量数据才能大展身手,然而每个图片如此巨大,训练巨大的数据集电脑也吃不消

所以我们必须要将数据进行降维,机器学习里面有很多降维的方法,比如PCA,LDA这些,但是这些方法都有一个问题他们必须把一个图片看做一个整体输入,也就是前面的将28X28转换成一个784的数组,这个数组我们知道,他丧失了一个非常重要的东西维度,我们仔细观察上面的图片

mnist数据来源网络

每个图片其实我们关注的都是数字的二维分布,我们通过闭合的圆的个数来区分8和0,我们通过中间的空白部分来区分0和1,所以我们希望能使用一种新的方法来确定图片特征,一方面能够保存图片的空间信息,一方面能最终数据一维的结果(图片代表的数字),这个就是卷积的引入了,卷积从二维的角度来提取图片的特征,相比于传统的一维提取,它能最大程度保留图片的信息,并且进行深度降维

从项目了解卷积

一开始学习深度学习卷积神经网络,看了很多资料,但是总是感觉并没有很深的理解,至到接触这个项目,从代码的层次上再去理解卷积才给我恍然大悟的感觉

首先先谈一谈Tensorflow这个库的基础知识,由于Python速度有点慢,所以Tensorflow的后端全部由C++写的,你可以这样理解Tensorflow,Python相当于一个客户端,你可以使用一个session(回话)与服务器(C++)进行交互,这样的话,我们在客户端可以享受Python的方便快捷,也可以享受C++运行的高效性,但是这个也带来一个麻烦,原来Python是一个所见即所得的,现在运行一些东西必须使用session来通知服务器来运行,我们很多中间过程就没法知道,只能通过返回的结果来进行推断了。在官方教程并没有讲太多中间过程,只是一笔带过,所以为了更好的理解卷积神经网络,我们将会以一种很难看的方法运行Tensorflow,但是我们能从这个过程中对卷积的理解更加深刻

所以接下来我们基本上每个操作都会让后端运行并且分析返回结果,为了方便叙述,我们假设你在运行session.run之前都会运行这个session.run(tf.global_variables_initializer())来初始化所以的变量

PS:之所以要运行这个,因为我们使用session与C++进行交互,如果我们“不声明”变量,c++会报错的

下面我们就从这个项目一行一行讲起

准备数据

前面我们知道,卷积就是要从二维空间中来提取我们想要的特征,首先我们把数据还原成二维的

x_image = tf.reshape(x, [-1,28,28,1])

x是上面我们输入的数据,来我们来检测一下,首先我们声明一个session

session = tf.Session()

再从数据集中掏出50张图

data = mnist.train.next_batch(50)[0]

接下来我们看看这个x_image变成了什么

session.run(tf.global_variables_initializer())

x_image_data = session.run(x_image, feed_dict={x: data})

我们输入两者的shape

data.shape, x_image_data.shape
(50, 784) (50, 28, 28, 1)

我们很清楚的看到,我们成功将一维的数组图像(784)变成了二维的数组图像(28X28),其实我们生成了三维(28 X 28 X 1),但是由于我们只有有些图片还会有多个色道(RGB),所以我们为了兼容,声明成28 X 28 X 1

好的,现在我们成功将一维图片还原成二维的,接下来就是将他们卷起来的时候了

第一层

如果你学过一些信号处理你会发现,深度学习使用的卷积其实并不是原始意义上的卷积,他没有“旋转180”的操作,但是他的形式其实是类似的。这个“积”的操作主要是通过矩阵运算来实现的,为了更好的理解卷这个操作,我从网上找了前辈们辛苦做的动图

卷积操作-来源网络

PS: 这个图与我们数据有点不同,我们每张只有一个色道,这个有三个色道,这张图有两个卷积核,但是我们这个第一层会使用32个,但是其实原理都一样,如果你实在理解不过来,你可以先值看最上面那一排

我们回到这种图,最左边就是图像输入,中间是卷积核,最后右边是输出,我们可以从图中可以很清楚的看到卷积的与我们平常操作不同,首先输入上我们是二维数据,通过二维的卷积核进行矩阵运算,最后我们输出二维结果,这就是卷积的强大之处,不但保留了原来的二维信息而且能够使用高效的矩阵运算来加速提取特征

现在我们回到代码

首先是要声明卷积核,我们可以使用简单的方法,将卷积核全部声明为全0矩阵,但是这个有可能造成0梯度,所以我们加入一点噪音,我们看看加入噪音的卷积核是什么值

initial = tf.truncated_normal([5, 5, 1, 32], stddev=0.1)
W_conv1 = tf.Variable(initial)
session.run(tf.global_variables_initializer())

W_conv1_value = session.run(W_conv1)

W_conv1_value.mean(), W_conv1_value.std()
(0.001365028, 0.08744488)

我们使用tf.truncated_normal函数声明了32个5X5X1的随机卷积核,看起来随机性还挺不错哦

PS:前面(5,5,1)代表输入长、宽、色道,后面代表输出输出数量当然我说它是32个它不一点为32个矩阵,应该是(色道X输出数量)个卷积核,但是我们这里只有一个色道,所以只有32个,我们可以通过W_conv1_value.shape查看真实的维度(当前的维度为(5, 5, 1, 32))

这个卷积核就对应上面图中间的小矩阵,他的长宽都为5,图中长宽都为3,当然我们可以把这个长宽修改,使用5是我们的经验值,通过这个大小的卷积核能够在模型表现能力更好。

接下来我们就进行最重要的卷积操作了,由上面图可知,要进行卷积必须要有三维的数据与对应的卷积核进行相卷,其实我们在图中还可以看到一个重要的东西,卷积的步长也就是每个框移动的位置(图中的步长为2)

还有一个较隐秘的知识,你有没有注意到图中的数据原来是7X7的数据,通过卷积核转换之后就变成了3X3了,影响卷积后图像尺寸不但有步长还有框子的大小,假如你的框是7,那图中只剩下一个值了,所以我们避免尺寸减少,我们使用周围填充0来使最边缘的位置卷积也成为到框子的中心,一方面避免边缘数据流失,一方面也能突出边缘数据(周边全为0)

Tensorflow为我们封装好了上面所以的方法,我们只要通过传参过去就能改变部长,改变填充方式,好了现在就开始来正式“卷”了

session.run(tf.global_variables_initializer())

v = session.run(tf.nn.conv2d(x_image, W_conv1, strides=[1, 1, 1, 1], padding='SAME'), feed_dict={x: data})

现在我们来看看卷完后vshape

   v.shape
(50, 28, 28, 32)

50代表50个数据,(28、28)代表图片维度,这个32就是卷积核数,50和32这两个应该是固定的,不难理解,我们现在来看看为什么通过卷积核的“卷”,图片还是保持28X28的,这个也是在知乎上涉及到的一个问题,现在我们从实验上来解决一下

首先我们看tf.nn.conv2d函数,他接受四个参数,第一个图片、第二个卷积核、第三个步长,第四个卷积方式

首先问题是觉得,卷完之后应该是变成24 X 24,这个理解是没错的,我们将pading的值改成VALID再次运行

session.run(tf.global_variables_initializer())

v = session.run(tf.nn.conv2d(x_image, W_conv1, strides=[1, 1, 1, 1], padding='VALID'), feed_dict={x: data})

v.shape
(50, 24, 24, 32)

我们得到了24 X 24的图片,这个SAME和VALID有什么区别呢,这个区别就是填充0没有填充0的原因,SAME在图像周边填0这样就能得到28 X 28

我们也发现,这个还有一个参数strides,这个就是前面填的步长,步长的长宽就是中间两位设置的(最边上两位跟输入有关,第一个是输入图片数量,最后一个是图片的色道),我们在这里使用使用1步长,我们来试试2步长试试

v = session.run(tf.nn.conv2d(x_image, W_conv1, strides=[1, 2, 2, 1], padding='SAME'), feed_dict={x: data})

v.shape
(50, 14, 14, 32)

果然输出的图像变成28的1/2了

接下来我们就要把卷积的值丢到神经元函数里面去了,为了符合实际,我们加入一个偏置量b_conv1

def bias_variable(shape):
  initial = tf.constant(0.1, shape=shape)
  return tf.Variable(initial)
b_conv1 = bias_variable([32])

这里我们使用0.1来初始化偏置量,接下来就是丢到神经元函数,这里我们使用numpy 的array的传播性,将b_conv1传递给所有的28X28的维度

h_conv1 = tf.nn.relu(tf.nn.conv2d(x_image, W_conv1, strides=[1, 1, 1, 1], padding='SAME') + b_conv1)

v = session.run(h_conv1,  feed_dict={x: data})

v.shape
(50, 28, 28, 32)

我们可以看到卷积完后从神经元函数生成的数据是(50X28X28X32)的,最后维度由1变成32,所有我们得使用点方法来缩减数据维度,这里我们使用卷积池的方法

卷积池

由上面可以看到,其实很简单就是把最大的挑出来

h_pool1 = tf.nn.max_pool(h_conv1, ksize=[1, 2, 2, 1],
                    strides=[1, 2, 2, 1], padding='SAME')

这里的参数很简单我就不介绍,这样“瘦身”之后,数据的维度由(50, 28, 28, 32)变成(50, 14, 14, 32),减少4倍

到这里我们的第一层卷积就结束了,接下来就是第二层卷积,为什么要多卷一次呢,因为前一层学到的还是太少了,要加强学习,这层和第一层没什么差别,所以我们就跳过这层

直接贴代码(函数就不复制了,文档里面有)

W_conv2 = weight_variable([5, 5, 32, 64])
b_conv2 = bias_variable([64])

h_conv2 = tf.nn.relu(conv2d(h_pool1, W_conv2) + b_conv2)
h_pool2 = max_pool_2x2(h_conv2)

全连接层

当我们完成两层卷积之后,我们的数据变成了(50,7,7,64)的四维数组了,我们知道我们传统的机器学习其实最后都是采用二维数组来当做训练数据(X代表特征,Y代表样本),所以全连接层就是把卷积给“反卷”过来,这样后面你方便对接传统机器学习,而且最后我们需要的数据也是输出的也是二维的(对一堆数据统一进行预测,所以这里称二维),但是这里要注意全连接层不是输出层,所以我们可以随意设置输出的维度,最后输出层对接再进行一次全连接层类似操作就能输出我们想输出的维度,这里我们看看全连接层权值变量

W_fc1 = weight_variable([7 * 7 * 64, 1024])
b_fc1 = bias_variable([1024])

这里我们声明全连接层的权值变量W_fc1和偏置量b_fc1,我们可以看看W_fc1shape是多少

session.run(tf.global_variables_initializer())

session.run(W_fc1).shape
(3136, 1024)

我们可以看到其实就是一个二维数组维度为(3136,1024),第一个维度跟输入有关,第二个维度影响输出维度,前面我们使用tf.nn.conv2d卷积操作来转换图片,在全连接层我们要使用矩阵运算来转换我们的维度

矩阵运算非常有趣,我们在前面其实也提到过一点,就是降维的实现PCA就是使用矩阵运算来进行降维,我们把数据分为X(特征),Y(数量),经过一次矩阵运算我们可以实现数量不变,而特征改变,这个就非常强大了,我们可以随便修改矩阵参数来动态修改我们特征数量

但是矩阵运算也有一定局限性,就是两个运算的矩阵必须是前者长与后者的宽想同,这个跟矩阵运算特性有关,具体可以看看矩阵运算相关资料

所以为了进行矩阵运算我们第一件事就是改变输入的shape,让它由四维变成二维,以便能够与我们权值矩阵W_fc1进行运算

h_pool2_flat = tf.reshape(h_pool2, [-1, 7*7*64])

我们简单的使用tf.reshape就能把第二层卷积后的输出变量转换成(50,7764)的维度,这样我们就能直接与权值矩阵W_fc1进行运算

h_fc1 = tf.nn.relu(tf.matmul(h_pool2_flat, W_fc1) + b_fc1)

我们这里直接将运算后的值放到激活函数里面去完成全连接层的功能

输出层

其实输出层同全连接层很类似,我们就是把前面的变量转换成我们想输出的维度,在进行这个输出层之前,我们得先搞一层Dropout层,这个能有效的避免神经网络的过拟合问题,具体可以看看这篇论文

keep_prob = tf.placeholder("float")
h_fc1_drop = tf.nn.dropout(h_fc1, keep_prob)

因为同全连接层原理类似,输出层我就不就不详细介绍了

W_fc2 = weight_variable([1024, 10])
b_fc2 = bias_variable([10])

y_conv=tf.nn.softmax(tf.matmul(h_fc1_drop, W_fc2) + b_fc2)

我们可以看看最后我们输出是什么

session.run(tf.global_variables_initializer())

session.run(y_conv, feed_dict={x:data, keep_prob:0.5}).shape
(50, 10)

ok,我们最后得到一个二维数组,50个预测结果(输出采用OneHot方法)

反向传播

在前面我们得到了在初始话随机权值下得到输出结果,但是这个结果肯定是错误的,我们必须通过修改每层的权值来修正模型,使模型越来越聪明,所以第一步,我们必须“自我反省”,了解自己与真实结果差距多少

y_ = tf.placeholder("float", [None, 10])
cross_entropy = -tf.reduce_sum(y_*tf.log(y_conv))

我们引入y_作为实际值(我们模型预测值为y),我们这里使用交叉熵来评判预测准确性,但是单单知道“自己错了”没有什么卵用,我们必须要“改正”,这里我们使用AdamOptimizer优化算法来反向传播我们误差,让模型好好“反省改正”

train_step = tf.train.AdamOptimizer(1e-4).minimize(cross_entropy)

到这里基本上差不多了,我们已经形成了一个闭环,预测->评估->改正->预测->……,只有让它不断的训练下去直到我们能接受他的误差我们的模型就训练好了

correct_prediction = tf.equal(tf.argmax(y_conv,1), tf.argmax(y_,1))
accuracy = tf.reduce_mean(tf.cast(correct_prediction, "float"))
session.run(tf.initialize_all_variables())
for i in range(18000):
  batch = mnist.train.next_batch(50)
  if i%100 == 0:
    train_accuracy = accuracy.eval(feed_dict={
        x:batch[0], y_: batch[1], keep_prob: 1.0}, session=session)
    print("step %d, training accuracy %g"%(i, train_accuracy))
    if abs(train_accuracy - 1) < 0.01:
        break
  train_step.run(feed_dict={x: batch[0], y_: batch[1], keep_prob: 0.5}, session=session)

由于我们使用OneHot方法来输出预测变量,所以我们要使用tf.argmax来得到我们想要的真实数字,经过20000轮训练我们正确率可以达到99%,至此卷积神经网络发挥他的威力。

总结

卷积神经网络是深度学习的一个很重要的组成部分,了解卷积必须要知道为什么要用卷积,用了有什么好处。总而言之,卷积并不是一个很新奇的东西,很早在信号处理中就有应用,但是在图像处理上由于他能保留图像维度信息从而在深度学习领域大放异彩,这也可以看做“是金子总会发光吧”

引用

http://www.tensorfly.cn/tfdoc/tutorials/mnist_pros.html
矩阵运算
通俗理解卷积神经网络
Dropout

由于我的笔记本是农卡,没法安装CUDA加速,而且我的显卡只有2G显存,安装OpenCL费力不讨好,而且由于我有一个Google云的300美元的体验,所以可以在Google云上使用TPU来进行加速,所以我就干脆不安装显卡加速,但是Tensorflow提供了指令集优化,由于默认使用pip安装没有提供这个功能,所以只能手动编译安装

假如你是用pip安装的Tensorflow你可以会得到下面警告

the tensorflow library wasn't compiled to use sse4.1 instructions

安装步骤

  1. 首先你得先看看你CPU支持什么指令集

    cat /proc/cpuinfo|grep flags
    

执行这个指令就能看到你所支持的指令集

  1. 然后安装bazel

    sudo add-apt-repository ppa:webupd8team/java
    sudo apt-get update && sudo apt-get install oracle-java8-installer
    echo "deb [arch=amd64] http://storage.googleapis.com/bazel-apt stable jdk1.8" | sudo tee /etc/apt/sources.list.d/bazel.list  
    curl https://bazel.build/bazel-release.pub.gpg | sudo apt-key add -  
    sudo apt-get update && sudo apt-get install bazel  
    sudo apt-get upgrade bazel  
    
  1. 安装完之后下载tensorflow源码

    mkdir github && cd github
    git clone –recurse-submodules https://github.com/tensorflow/tensorflow
    cd tensorflow
    ./configure

接下来一路选择N就行

  1. 生成whl文件

    bazel build -c opt --copt=-msse3 --copt=-msse4.1 --copt=-msse4.2 --copt=-mavx --copt=-mavx2 --copt=-mfma //tensorflow/tools/pip_package:build_pip_package
    

在源码处开始编译,注意copt命令主要是添加指令集支持,这里你要看看上面的指令集(去掉m就是你的指令集,如-msse3指令集为sse3)你的CPU是否支持(一般都支持我的I5 4200U都支持),如果不支持删掉那个就行

这里你安装的时间比较长,要看你的CPU了

  1. 验证

退出安装目录运行python

执行下面两句

import tensorflow as tf;sess = tf.Session(config=tf.ConfigProto(log_device_placement=True))

总结

如果没有报上面的不支持指令集的warning,那么你的CPU指令集优化版就安装好了,当然这个加速效果因CPU而异,对于Xeon SP系列(100核心以上)已经能加速到50倍,同GPU差距也就2倍了(原来可是100倍),但是对于我的笔记本来说,加速效果可能就在30%左右(核心少),所以当前加速性价比最高的还是GPU加速,骚年还是买个好一点的GPU吧,没事还可以吃吃鸡。


引言

这篇博客其实写于2016年,最近在重新学了一下正则表达之后,觉得有必要重新整理一下正则的用法

Python对正则匹配的库是re,re是基于Perl所用的正则表达式,并有
一定的改进.

正则本质就是搜索所需的文本,正则里面有三种搜索方式

  1. 第一种是知道文本内容直接使用普通字符搜索出来,比如要从abcdefg中搜到cd
  2. 第二种就是模糊查询,比如我想从英文中找一个数字,一般借助特殊符号(.+*?)或者转义符号(\w\d等)
  3. 第三种就是结合前两种,比如我记得一个单词的前两个字母想把那个单词搜出来.

这里不介绍正则基本知识,你想知道可以点这里

ps: 由于在python里面也是用反斜杠做转义字符,所以比如\\\b这两个特殊字符必须用\\\\\\b来代替.但是python提供了一个元字符支持re模块,只要字符前面加上r比如r' regex '就能不关闭python的转义.

正则里面我觉得很重要的一个概念就是组概念,当我们的文本比较复杂的时候将其分成多个小组是利于我们正则的后期维护和改进

正则里面使用一个括号来表示组比如(a)(b)就分成了两个组

re函数里面searchfinall都支持组查询,而且findall方法假如里面有组分布会只显示组成员.

re库支持搜索选项,这几个选项对于正则有时候非常有用

DOTALL [简S]-------------允许点字符匹配换行符
IGNORECASE [简I] --------忽悠大小写
LOCALE  [简L]  ----------支持本地化字符
MULTILINE [简M] ---------多行,每行都支持锚点
UNICODE [简U]  ----------支持Unicode,\w也可以是Unicode了
VERBOSE  [简X]  --------------神器,会无视代码中的注释空格和换行

我们也可以在正则的组里面使用这些搜索选项,只要用上面的简称的小写比如(?is)就可以在组里面使用这些规则.


正则里面还有一些比较有趣的函数,同string里面的translate函数,sub函数可以替换找到的变量
bold = re.compile(r’*{2}(.?)\{2}’)
bold.sub(r’\1‘, ‘this foo and ok‘)

\1代表第一组变量也就是foo和ok
输出为'this <b>foo</b> and <b>ok</b>'我们使用成功用加粗了foo和ok,同translate不同这个方法不需要知道要替换的是什么.


正则的断言

我们可以使用一些特殊的符号来执行一些程序判断选择,比如说判断是否特殊字符,如果有
的话就不匹配,这就是断言

断言有两种一种是前向,一种是后向

前向是指判断语句在前面,这种就相当于一个if语句,而后向是匹配后判断,由于已经匹配好了文字所以
匹配的字符必须是固定长度的(不能使用*.?).

前向就是在判断后面匹配的表达式必须与规定相同,比如一个邮箱地址我们要匹配可以用<>包起来的,但是不匹配只要一个的我们就可以在前面加上这个^(?=(<.*>$)|([^<].*[^>]$))通过使用?=来断言后面必须是用<>包起来或者没有<>,我们使用前向断言可以通过正则直接过滤掉不符合的(当然你可以用多个简单正则来做但是效率没有这个高),还有否定前向就是通过?!来声明.
相对应后向断言就是很简单了,直接在匹配后面使用一个?<=(肯定后向)或?<!(否定后向),不过要注意这个是判断前面匹配是否满足的.

断言只是限定我们想选的文本的范围,他并不会被选择.
断言的一个有趣的应用就是选择字符间的空格,我们知道python其实假设每个字符间都一个空格(这就是我们有时候会选出一些空字符出来的原因),这个空格不是我们自己打上去的.

举个例子

两个字符串a1a 1,第一个我们称为A,第二个我们称他为B,假如我们想把数字和字母分出来,对于B来说,很简单因为数字和字母之间有一个空格,我们可以直接使用字符自带的split就行,但是对于A来说,就不那么简单了.

字母a和数字1中间没有字符,我们必须把字母和数字之间的”空格”给选择出来,这时候就可以用到断言了.

r = re.compile(r'(?<=[a-z])(?=\d)') 

这个r就可以字母和数字直接的隐形空格给选择出来了

遗憾的是由于python的正则并不把隐形的空格当做字符,所以我们不能简单的使用正则的re.split方法(选择字符分割)直接将字符串分解开.

我们就得写几步

第一先把空格换成 $$$(或其他)

>>> s = r.sub('$$$', 'a1')
>>> print(s)
'a$$$1'

然后在分割

>>> s.split('$$$')
>>> ['a', '1']

成功分割好了,当然这个只能处理字母在前数字在后的”隐形空格”,只要加一个"|"在把前向改成后向,后向改成前向就可以选择任意字母和数字直接的”隐形空格”了.


正则的变量

我们可以使用?P来声明一个组(用括号,当然其实我们每使用一个括号re自动帮我们将组取一个
名,依次从1-n

有时候我们可以要求上面的匹配组,下面也要相应匹配组,我们就可以通过两种方法来引用这个变量,假如你没有使用<?P<name>来声明组你只能通过\n来引用,n是这个变量的序号,第二种是通过(?P=name)
来引用这个变量,name为你自己定义的组的名字

re还提供了一种机制来让你修正你的正则,简单来说就是能判断一个组存不存在来约束匹配,语法为

(?(id)yes-expression|no-expression)

id为组的编号或者name.

正则的起源

正则这个东西其实很简单,我们