0%

HackPython致力于有趣有价值的编程教学

简介

在上一篇,讨论了阻塞/非阻塞、同步/异步、并发/并行等概念,本节主要来讨论一下生成器、yield以及yield from概念并进行简单的使用。

关键概念

Python中利用了asyncio这个标准库作为异步编程框架,而aysncio以及其他多数协程库内部都大量使用了生成器,所以先从生成器聊起。为什么会是生成器🤔?回想一下生成器的特性,其利用了yield关键字做到了随时暂停以及随时执行的能力,而协程从技术实现角度而言,它的作用其实就是一个可以随时暂停会执行的函数。

生成器

生成器与迭代器关系紧密,😗其实生成器就是迭代器另一种更优雅的实现方式,其利用了yield关键字实现了迭代器的功能,生成器可以迭代式的利用内存空间,让数据在需要使用时才被载入,这减少内存的消耗,其利用yield关键字使用了这个功能,当生成器函数执行过程中遇到yield就会被展厅执行,等下次迭代时再次从暂停处继续执行。

为了让生成器可以实现简单的协程,🤩在Python 2.5 的时候对生成器的能力进行了增强,此时利用yield可以暂停生成器函数的执行返回数据,也可以通过send()方法向生成器发送数据,并且还可以利用throw()向生成器内抛出异常以实现可随时终止生成器的目的。

yield的作用直观如下图:

从图中可看出,在一开始调用simple_coro2()方法时,获得的my_coro2变量并不是具体的值,而是一个生成器对象,此时调用其next()方法进行迭代,next()方法会让生成器函数执行到yield处,到yield后就会会将紧随在其后的变量返回,接着可以利用send()方法将值传递到生成器中,并让暂停的函数继续从暂停处执行😏,next()与send()的不同之处在于next()并不能向生成器内部传递值而send()可以,可以直接使用send(None)来实现next()方法的效果。从图中也可以看出,next()与send()会获得下一个yield返回的值。

顺带一提,for迭代也调用了迭代器中的__next__方法,next()内部也是该方法🤫。

yield from

为了让生成器分成多个子生成器后可以很容易使用next()、send()、throw()等方法,Python3.3中引入了yield from语言🤩,它允许将一个生成器的部分操作委派给另一个生成器。

虽然yield from设计的目的是为了让生成器本身可以委派给子生成器,但yield from可以向任意可迭代对象进行委派操作🤭。

yield from iterable 本质其实就是 for item in iterable: yield item,只是写法更优雅了,简单使用如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
In [1]: def gen1():
...: for i in 'abc':
...: yield i
...: for i in range(5):
...: yield i
...:

In [2]: list(gen1())
Out[2]: ['a', 'b', 'c', 0, 1, 2, 3, 4]

In [4]: def gen2():
...: yield from 'abc'
...: yield from range(5)
...:

In [5]: list(gen2())
Out[5]: ['a', 'b', 'c', 0, 1, 2, 3, 4]

上述代码中其实涉及几个概念,其中gen2()方法因为包含了yield from表达式,所以被称为😀委派生成器,而yield from后接着的表达式通常称为😀子生成器,上述代码中的’abc’,range(5)都是子生成器,而滴啊用委派生成器的代码称为😀调用方。

此外,yield from还可以直接将调用方发送的信息直接传递给子生成器,具体可以看下面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
from collections import namedtuple
Result = namedtuple('Result', 'count average')
# the subgenerator
def averager():
total = 0.0
count = 0
average = None
while True:
term = yield
print('term:', term)
if term is None:
break
total += term
count += 1
average = total / count
return Result(count, average)

# the delegating generator
def grouper(results, key):
while True:
#只有当生成器averager()结束,才会返回结果给results赋值
results[key] = yield from averager()
print('resluts[key]:', results[key])

def report(results):
for key, result in sorted(results.items()):
group, unit = key.split(';')
print('{:2} {:5} averaging {:.2f}{}'.format(result.count, group, result.average, unit))

def main(data):
results = {}
for key, values in data.items():
group = grouper(results, key)
print(type(group))
next(group)
for value in values:
r = group.send(value)
print('r:',r)
print('value:',value)
group.send(None)
report(results)

data = {
'girls;kg':[40.9, 38.5, 44.3, 42.2, 45.2, 41.7, 44.5, 38.0, 40.6, 44.5],
'girls;m':[1.6, 1.51, 1.4, 1.3, 1.41, 1.39, 1.33, 1.46, 1.45, 1.43],
'boys;kg':[39.0, 40.8, 43.2, 40.8, 43.1, 38.6, 41.4, 40.6, 36.3],
'boys;m':[1.38, 1.5, 1.32, 1.25, 1.37, 1.48, 1.25, 1.49, 1.46],
}
if __name__ == '__main__':
main(data)

在上述代码中,grouper函数是委托生成器😗,averager函数是子生成器😗,而main()函数就是调度者😗。

在main()函数中,首先通过grouper()获得对应的生成器对象,然后调用next()方法进行初步的迭代,此时会执行到averager()的yield处,因为yield后没有跟对应的变量,则yield返回的值为None,该值会有grouper()委托生成器直接传递给main()调度者,观察变量r的打印则可,接着for迭代中使用委托生成器的send()方法,该方法发送的数据会有委托生成器直接传递给子生成器,即averager()函数中term的值,上述代码调度的关系如下图:

从图中看出,😐调度者使用send()方法传递的数据会被委派生成器直接传递给子生成器,而子生成器yield的方法数据也被直接传递会调度者,如果子生成器产生StopIteration异常则表示子生成器已经迭代完了,此时委派生成器会接收到该异常,从而继续执行yield from整个表达式后的其他表达式,这里grouper()函数中yield from执行完后,就没有逻辑了。

可以看出,委派生成器具有组织多个子生成器的能力,并将调度者的信息转手传递给子生成器😯。

结尾

在本节中,主要介绍Python中生成器、yield以及yield from的概念与使用,在下一篇中,会接着讨论Python的asyncio框架以及async/await原生协程,最后欢迎学习HackPython的教学课程并感谢您的阅读与支持。

参考文章

Python 异步编程详解
Python 也能高并发

HackPython致力于有趣有价值的编程教学

简介

因为GIL(全局锁)的存在,Python在运行性能方便一直是个短板,但在IO密集型网络编程里,利用aysncio等异步处理的方式可以提升百倍运行效率,但在计算密集型程序中,Python并不是最佳的选择。

异步编程会涉及比较多的概念,为了减轻阅读压力,将其分为上、中、下三篇文章。

关键概念

要理解异步编程,首先需要理解阻塞、非阻塞、同步以及异步的概念与关系。

当程序在等待某个操作完成期间,自己无法做其他事情的状态称为「阻塞」,专业点讲,当程序因未得到运行所需要的计算资源而被挂起的等待状态称为「阻塞」,「阻塞」其实无处不在,常见的有:磁盘IO阻塞、网络IO阻塞。

阻塞与非阻塞

与「阻塞」对应的就是「非阻塞」状态,所谓「非阻塞」指的是程序在等待某个操作是,自己还可以做其他事情,需要注意的是,「非阻塞」并不能在任何程序级别都可以存在,只有当程序可以囊括独立子程序单元时,它才可能存在非阻塞状态,其本质原因在于CPU单个核在那个瞬间只能处理一件事情。

与「阻塞」和「非阻塞」关联很大的概念分别是「同步」与「异步」。「同步」指不同的程序单元为了完成某个任务通过某种通信方式协调一致,此时可以称这些程序是同步执行的,这里协调一致并不是说要同时做,而是指大家有个顺序,「同步其实也就意味着有序」。

同步与异步

对应的「异步」指不同程序单元直接不需要协调也可单独完成任务,通常,没有先后顺序关联的业务逻辑可言利用异步的方式来实现,比如爬虫下载不同的网页、保存等操作都可以独立完成,下载程序单元直接无需通信协调,这也造成了「异步」是无序的。

一个「非阻塞」进程中通常由多个独立的子进程构成,如执行到下载逻辑时,交由下载子进程去执行自身的逻辑,而自己继续之前其他逻辑,这个「交由下载子进程」的动作就可以称为「异步」。

关于「同步」或「异步」描述中提出通信协调通常指异步编程或「并发」编程的同步原语,如信号量、锁、同步队列等。

从具体的技术层面而言,如果调用一个耗时函数,函数会挂起直达执行完毕,返回结果,那么这个函数所在程序就是「阻塞」的,其操作就是「同步」的,如果调用一个耗时函数立刻返回,等需要的数据到达后再通知函数的调用者,则该函数所在程序就是「非阻塞」的,其操作就是「异步」的。

并发与并行

「同步」和「异步」的概念可能会与「并发」和「并行」混淆,但两者其实描述的是不同级别的事情,对于「并发」和「并行」,Erlang 之父 Joe Armstrong 给出的图很好的解释了两者的区别

「并发」是两个队列交替使用一台咖啡机,「并行」是两个队列同时使用两台咖啡机。

「并发」表示多个程序可以在同一个时间段内被执行,主要用于描述程序的组织结构,我们称这个程序是可以并发的通常指该程序设计了多个可独立执行的子任务,从而可以在同一时间段内利用有限的计算资源让多个任务以近实时的形式执行。

而「并行」表示多个程序可以在同一时刻被执行,「并行」的关键要有物理上的支持,比如有两台咖啡机,它通常用于描述程序的执行状态而不是程序组织结构,通常用来表示有足够的计算资源让多个任何同时执行。

更严谨的描述为:
1.「并发」是说进程B的开始时间是在进程A的开始时间与结束时间之间,我们就说A和B是「并发」的。
2.「并行」是「并发」的真子集,指同一时间两个进程运行在不同的机器上或者同一个机器不同的核心上。

简单总结上面的概念:
非阻塞可以提高程序整体的执行效率,异步是一种组织非阻塞任务的方式(即操作其中的程序单元),而并发是为了让独立的子任务有机会被尽快执行,但不一定可以加快任务整体的进度,而并行则是利用多核资源加快多任务的完成进度。

要实现并发,就需要将整体任务拆分为多个相互独立的子任务,而不同子任务之间才会有所谓的阻塞/非阻塞、同步/异步等说法,所以并发、非阻塞、异步三个概念总是如影随形。

进程、线程与协程

「进程」是操作系统中的最小的资源分配单位,每个「进程」都有各自独立的地址空间、资源句柄,它们相互独立,每个「进程」中都会有个用于描述当前进程的数据结构,操作系统会利用这些描述来管理进程,同时对操作系统而言,进程的创建于销毁时比较消耗资源的。

「进程」会抢占式的争夺CPU资源,同一时刻下,CPU的一个核只能执行
一个进程,而单核CPU可以快速切换不同的进程来使得多进程看像去在同时执行。

「线程」是CPU调度的最小单位,「线程」是进程的一个实体,一个进程可以包含多个「线程」,同进程的多个「线程」是可以共享当前进程的内存地址空间与资源句柄的,「线程」的切换需要操作系统来实现调度,我们写的程序无法控制这个调度过程,适用于IO密集型任务。

如果想人为的控制线程的调度过程,可以使用「协程」,「协程」不等于可以自己调度的线程,它是属于线程的,即一个线程可以有多个「协程」,是一种用户状态下的轻量级线程,协程的调度可以完全由用户做主,因此使用更加灵活,可以将协程理解为可以在特定位置暂停或恢复的函数/子程序。

异步编程

异步编程就是以进程、线程、协程、函数/方法作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式。

无论什么语言,要想实现异步编程就跳不开回调与事件循环。

在执行耗时函数时,函数会提前注册回调函数返回给调度程序,而函数耗时操作的等待与监听任务交个了操作系统,当操作系统监听到耗时操作的状态改变后就调用回调函数通知调度程序,这整个过程称为「回调」。

而「事件循环」是一种等待程序分配事件或消息的编程框架,简单来说就是当「 A 发生时,执行 B」,即监听当有什么发生时,就去执行什么,事件循环本质就是一种循环,它会不停的收集事件并执行对应这些事件的响应逻辑。

因为异步编程离不开「回调」与「事件循环」,但回调与事件循环的使用容易出现回调地狱、堆栈撕裂、错误定位与处理困难等问题,Python基于asyncio标准库来实现事件循环,为了避免其带来的危害,衍生出了基于协程的处理方案,协程本身的特性也满足了回调的要求。

在Python中协程与asyncio一同构建了异步编程。

结尾

在本节中,主要介绍了异步编程中会涉及的各种概念,在下一篇中,会接着探讨Python中的异步编程,最后欢迎学习HackPython的教学课程并感谢您的阅读与支持。

HackPython致力于有趣有价值的编程教学

简介

在上一篇文件中,介绍了 Elasticsearch 以及其中的关键概念,并且安装了 Elasticsearch 与 对应的 elasticsearch-py ,本章就来使用一下其基本的功能👹。

使用

Elasticsearch 本身提供了一系列 Restful API 来进行存取和查询操作,我们可以使用任意语言来使用这些API,而 elasticsearch-py 已经将这些API封装后,直接调用其中方法则可。

为了正常使用,先运行 elasticsearch

1
cd /usr/local/Cellar/elasticsearch/6.8.0/bin && ./elasticsearch

创建索引 (Index)

导入 elasticsearch 库的 Elasticsearch 类,使用create()方法创建一个名为 names 的 Index

1
2
3
4
5
6
from elasticsearch import Elasticsearch

es = Elasticsearch()

result = es.indices.create(index='names')
print(result)

如果创建成功,则会返回下面结果🎉

1
{'acknowledged': True, 'shards_acknowledged': True, 'index': 'names'}

Elasticsearch 中不可以创建同名的 Index ,如果重复执行上述代码,则会出现如下错误💔

1
elasticsearch.exceptions.RequestError: RequestError(400, 'resource_already_exists_exception', 'index [names/x-AtvCZ-Q5uL-NB0hNeoFQ] already exists')

错误表明,names 这个 Index 已经存在了,不可以重复创建,错误类型为 400,在创建代码中添加 ignore = 400 可以让程序忽略这个报错。

1
2
3
4
5
6
from elasticsearch import Elasticsearch

es = Elasticsearch()

result = es.indices.create(index='names', ignore = 400)
print(result)

对于一些已知的、可处理的报错可以将其忽略来保证程序的正常运行,但编写程序时,我们是无法保证程序无法出错的,如果程序出现不再预知范围内的错误,最佳的方式就是让其崩溃,而不是隐藏错误,让程序以不正常的状态苟延残喘的运行下去🙅‍♂️。

删除 Index

与创建 Index 类似,代码如下:

1
2
3
4
5
6
from elasticsearch import Elasticsearch

es = Elasticsearch()

result = es.indices.delete(index='names', ignore=[400, 404])
print(result)

同样使用了 ignore 参数,来忽略 Index 不存在而删除失败导致程序中断的问题,如果成功删除,会输出如下结果🎉:

1
{'acknowledged': True}

如果 Index 已经被删除,再执行删除则会输出如下结果💔:

1
{'error': {'root_cause': [{'type': 'index_not_found_exception', 'reason': 'no such index', 'resource.type': 'index_or_alias', 'resource.id': 'names', 'index_uuid': '_na_', 'index': 'names'}], 'type': 'index_not_found_exception', 'reason': 'no such index', 'resource.type': 'index_or_alias', 'resource.id': 'names', 'index_uuid': '_na_', 'index': 'names'}, 'status': 404}

结果表明当前 Index 不存在,删除失败,返回的结果同样是 JSON,状态码是 400,但是由于我们添加了 ignore 参数,忽略了 400 状态码,因此程序正常执行输出 JSON 结果,而不是抛出异常。

插入数据

Elasticsearch 可以直接插入结构化字典数据,代码如下:

1
2
3
4
5
6
7
8
from elasticsearch import Elasticsearch

es = Elasticsearch()
es.indices.create(index='people', ignore=400)

data = {'name': ' 二两', 'age': '28'}
result = es.create(index='people', doc_type='politics', id=1, body=data)
print(result)

🙊创建一条数据,包括人名和年龄,然后通过调用 create () 方法插入了这条数据,在调用 create () 方法时,我们传入了四个参数,index 参数代表了索引名称,doc_type 代表了文档类型,body 则代表了文档具体内容,id 则是数据的唯一标识 ID。

运行结果:

1
{'_index': 'people', '_type': 'politics', '_id': '1', '_version': 1, 'result': 'created', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 0, '_primary_term': 1}

结果中 result 字段为 created,代表该数据插入成功🎉。

除 create() 方法外还可以使用 index () 方法来插入数据,create () 方法需要我们指定 id 字段来唯一标识该条数据,而 index () 方法则不需要,如果不指定 id,会自动生成一个 id,调用 index () 方法的写法如下:

1
es.index(index='people', doc_type='politics', body=data)

更新数据

指定数据的 id 和内容,调用 update () 方法即可,但需要注意的是, Elasticsearch 对应的更新 API 对传递数据的格式是有要求的,更新时使用的具体代码如下:

1
2
3
4
5
6
7
8
9
10
es = Elasticsearch()
data = {
'doc' : {
'name': '二两',
'age': '30',
'desc': 'Java工程师'
}
}
result = es.update(index='people', doc_type='politics', body=data, id=1)
print(result)

这里为数据增加了一个日期字段,然后调用了 update () 方法, update() 其他的数据格式为:

1
2
3
4
{
“ doc ”:{},
script ”:{}
}

成功更新后,可以看到如下结果🎉:

1
{'_index': 'people', '_type': 'politics', '_id': '1', '_version': 2, 'result': 'updated', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 1, '_primary_term': 1}

result 字段为 updated,即表示更新成功, _version字段,这代表更新后的版本号数,2 代表这是第二个版本,因为之前已经插入过一次数据,所以第一次插入的数据是版本 1。

此外,还可以直接使用 index() 方法来实现更新操作,代码如下:

1
2
3
4
5
6
7
8
es = Elasticsearch()
data = {
'name': '二两',
'age': '29',
'desc': 'Python工程师'
}
result = es.index(index='people', doc_type='politics', body=data, id=1)
print(result)

成功后,会返回相同的结果,只是 _version 变为了 3。 index() 方法对格式没有要求。

删除数据

删除数据调用 delete() 方法,指定需要删除的数据 id ,代码如下:

1
2
3
es = Elasticsearch()
result = es.delete(index='people', doc_type='politics', id=1)
print(result)

成功删除后,输出如下结果🎉:

1
{'_index': 'people', '_type': 'politics', '_id': '1', '_version': 4, 'result': 'deleted', '_shards': {'total': 2, 'successful': 1, 'failed': 0}, '_seq_no': 3, '_primary_term': 1}

查询数据

上面几个操作都非常简单,而 Elasticsearch 最强大的功能是查询功能,这里来使用了 Elasticsearch ,因为中文数据与英文数据不同,英文单词之间天然由空格分割,所以可以直接利用 Elasticsearch 来搜索,而中文数据词与词时相互连接在一起的,所以需要先进行分词,即将中文数据中的词汇分割出来,🙈这里可以使用 elasticsearch-analysis-ik 插件,该插件 Elasticsearch 实现了中文分词🙈,可以使用 elasticsearch-plugin 来安装 Elasticsearch 插件,注意,插件的版本要与 Elasticsearch 主版本对应,这里使用的是 6.8.0 版本,所以安装 6.x 版本的 elasticsearch-analysis-ik 则可。

1
./elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.8.0/elasticsearch-analysis-ik-6.8.0.zip

elasticsearch-analysis-ik Github 地址为: https://github.com/medcl/elasticsearch-analysis-ik 🤫

你安装时,请将 6.8.0 替换为自己 Elasticsearch 的版本,然后交给 elasticsearch-plugin 完成下载与安装。

安装完后,需要重启 Elasticsearch ,启动的过程中 Elasticsearch 会自动加载其中的数据。

简单重启方式🌚:

1.找到Elastic进程ID

1
ps aux | grep elastic | grep -v grep

2.kill

1
kill -9 Elastic 进程ID

3.再次启动

启动后,新建一个索引并指定需要分词的字段,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from elasticsearch import Elasticsearch

es = Elasticsearch()
mapping = {
'properties': {
'title': {
'type': 'text',
'analyzer': 'ik_max_word',
'search_analyzer': 'ik_max_word'
}
}
}
es.indices.delete(index='people', ignore=[400, 404])
es.indices.create(index='news', ignore=400)
result = es.indices.put_mapping(index='news', doc_type='politics', body=mapping)
print(result)

上述代码中,先将之前名为 people 的索引删除,然后新建了名为 news 的索引,然后更新了它的 mapping 信息, mapping 信息中指定了分词的字段,🌝其中将 title 字段的类型 type 指定为 text,并将分词器 analyzer 和搜索分词器 search_analyzer 设置为 ik_max_word ,即使用刚刚安装的中文分词插件,如果不指定,则默认使用英文分词器🙅‍♂️。

接着,插入一些数据,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
def test2():
datas = [
{
'title': '设计灵魂离职,走下神坛的苹果设计将去向何方?',
'url': 'https://www.tmtpost.com/4033397.html',
'date': '2019-06-29 11:30'
},
{
'title': '医生中的建筑设计师,凭什么挽救了上万人的生命?',
'url': 'https://www.tmtpost.com/4034052.html',
'date': '2019-06-29 11:10'
},
{
'title': '中国网红二十年:从痞子蔡、芙蓉姐姐到李佳琦,流量与变现的博弈',
'url': 'https://www.tmtpost.com/4034045.html',
'date': '2019-06-29 11:03'
},
{
'title': '网易云音乐、喜马拉雅等音频类应用被下架,或因违反相关规定',
'url': 'https://www.tmtpost.com/nictation/4034040.html',
'date': '2019-06-29 10:07'
}
]

for data in datas:
es.index(index='news', doc_type='politics', body=data)

可以将插入的内容查询打印出来看看:

1
2
result = es.search(index='news', doc_type='politics')
print(json.dumps(result, indent=4, ensure_ascii=False))

输出结果如下🎉:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"took": 1,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 4,
"max_score": 1.0,
"hits": [
{
"_index": "news",
"_type": "politics",
"_id": "MclZoWsB7N68fyc5DUS7",
"_score": 1.0,
"_source": {
"title": "网易云音乐、喜马拉雅等音频类应用被下架,或因违反相关规定",
"url": "https://www.tmtpost.com/nictation/4034040.html",
"date": "2019-06-29 10:07"
}
},
...

返回结果会出现在 hits 字段里面,其中有 total 字段标明了查询的结果条目数, max_score 代表了最大匹配分数。

我们还可以进行全文检索,这才是 Elasticsearch 搜索引擎的特性🥊:

1
2
3
4
5
6
7
8
9
10
dsl = {
'query': {
'match': {
'title': '网红 设计师'
}
}
}
es = Elasticsearch()
result = es.search(index='news', doc_type='politics', body=dsl)
print(json.dumps(result, indent=2, ensure_ascii=False))

Elasticsearch 支持的 DSL 语句来进行查询,使用 match 指定全文检索,检索的字段是 title,内容是 “网红 设计师”,搜索结果如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
"took": 18,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 3,
"max_score": 2.2950945,
"hits": [
{
"_index": "news",
"_type": "politics",
"_id": "L8lZoWsB7N68fyc5DUSx",
"_score": 2.2950945,
"_source": {
"title": "医生中的建筑设计师,凭什么挽救了上万人的生命?",
"url": "https://www.tmtpost.com/4034052.html",
"date": "2019-06-29 11:10"
}
},
{
"_index": "news",
"_type": "politics",
"_id": "MMlZoWsB7N68fyc5DUS2",
"_score": 1.8132976,
"_source": {
"title": "中国网红二十年:从痞子蔡、芙蓉姐姐到李佳琦,流量与变现的博弈",
"url": "https://www.tmtpost.com/4034045.html",
"date": "2019-06-29 11:03"
}
},
{
"_index": "news",
"_type": "politics",
"_id": "LslZoWsB7N68fyc5DEQC",
"_score": 0.71580166,
"_source": {
"title": "设计灵魂离职,走下神坛的苹果设计将去向何方?",
"url": "https://www.tmtpost.com/4033397.html",
"date": "2019-06-29 11:30"
}
}
]
}
}

匹配的结果有两条,第一条的分数为 2.29,第二条的分数为 1.81,即查询“网红 设计师”时, Elasticsearch 认为第一个结果权重更高。从该检索结果可以看出,检索时会对对应的字段全文检索,结果还会按照检索关键词的相关性进行排序,这已经是一个搜索引擎的雏形了🙉。

Elasticsearch 还支持非常多的查询方式:
https://www.elastic.co/guide/en/elasticsearch/reference/6.3/query-dsl.html 🤫

进一步学习

🤓1.Elasticsearch 权威指南:https://www.elastic.co/guide/cn/elasticsearch/guide/current/index.html
🤓2.Elastic 中文社区:https://www.elasticsearch.cn/

结尾

Elasticsearch 简单使用就介绍到这里,Elasticsearch 本身具有一定的复杂性,简单几篇文章只能让大家对其有个基本的理解,后续还会以[课外知识]的方式分享更多 Elasticsearch 方面的内容,最后欢迎学习 HackPython 的教学课程并感觉您的阅读与支持。

HackPython致力于有趣有价值的编程教学

简介

当爬虫爬取了很多数据后,如何全面的了解爬取数据的特征?这就需要对数据进行搜索了,传统通过 SQL 方式进行搜索,在数据量较大时,效率并不佳,此时 Elasticsearch 就是首选,它是一个开源的搜索引擎,建立在全文搜索引擎库 Apache Lucene 的基础上,而 Lucene 是当前无论开源还是私有的搜索引擎中最先进、高性能与全功能的搜索引擎之一,但 Lucene 使用比较复杂,而 Elasticesarch 在 Luncene 基础上进行了封装并提供了一套简单的 RESTful API ,而 elasticsearch-py 则是在这套 API 上的封装,让我们可以在 Python 项目中直接使用 Elasticsearh。

Elasticsearch 的准确形容:

一个分布式的实时文档存储系统,每个字段可以被索引与搜索
一个分布式实时分析搜索引擎
可以在上百个服务节点扩展,并支持 PB 级别的结构化或非结构化数据搜索

安装

可以到 Elasticsearch 的官方网站下载 Elasticsearch 下载不同平台的安装包:

https://www.elastic.co/downloads/elasticsearch

具体安装步骤可以参考官网提供的内容或自行搜索,在 Mac 上可以直接使用 brew 安装。

因为 Elasticsearch 是基于 Java 开发的,所有系统中需要预先安装 Java JDK,在 Mac 中同样可使用 brew 安装。

1
2
3
4
# 安装 JDK
brew cask install homebrew/cask-versions/adoptopenjdk8
# 安装 Elasticsearch
brew install elasticsearch

Mac 下通过 brew 安装完后,可以进入 /usr/local/Cellar/elasticsearch/6.8.0/bin 运行 elasticsearch 。

1
cd /usr/local/Cellar/elasticsearch/6.8.0/bin && ./elasticsearch

如果想将 Elasticsearh 作为守护进程在后台运行,需要加上 -d 参数。

如果你是在 Windows 上面运行 Elasticseach,你应该运行 bin\elasticsearch.bat 而不是 bin\elasticsearch 。

正常启动后,访问 localhost:9200 可看到如下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
curl localhost:9200
{
"name" : "sbUPsxx",
"cluster_name" : "elasticsearch_ayuliao",
"cluster_uuid" : "Bv83QVa4QMOJti9Yn1RJsw",
"version" : {
"number" : "6.8.0",
"build_flavor" : "oss",
"build_type" : "tar",
"build_hash" : "65b6179",
"build_date" : "2019-05-15T20:06:13.172855Z",
"build_snapshot" : false,
"lucene_version" : "7.7.0",
"minimum_wire_compatibility_version" : "5.6.0",
"minimum_index_compatibility_version" : "5.0.0"
},
"tagline" : "You Know, for Search"
}

从上述信息中,可知我们安装的是 6.8.0 版本的 elasticsearch,版本很重要,因为只有安装了对应主版本的 elasticsearch-py 才能在 python 中正常使用 elasticsearch。

对于Elasticsearch 6.0及更高版本,请使用库的主要版本 6(6.x.y)。
对于Elasticsearch 5.0及更高版本,请使用库的主要版本 5(5.x.y)。
对于Elasticsearch 2.0及更高版本,请使用库的主要版本 2(2.x.y),依此类推。

在setup.py或 requirements.txt 中设置需求的推荐方法是:

1
2
3
4
5
6
7
8
# Elasticsearch 6.x
elasticsearch>=6.0.0,<7.0.0

# Elasticsearch 5.x
elasticsearch>=5.0.0,<6.0.0

# Elasticsearch 2.x
elasticsearch>=2.0.0,<3.0.0

通过 pip 安装一下

1
pip install elasticsearch==6.4.0

关键概念

Elasticsearch 是面向文档的,意味着它存储整个对象或文档。Elasticsearch 不仅存储文档,而且索引每个文档的内容使之可以被检索。在 Elasticsearch 中,你对文档进行索引、检索、排序和过滤 – 而不是对行列数据。这是一种完全不同的思考数据的方式,也是 Elasticsearch 能支持复杂全文检索的原因。

下面简单介绍其中几个关键的概念:

节点 (Node) 与集群 (Cluster)

Elasticsearch 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elasticsearch 实例。

单个 Elasticsearch 实例称为一个节点(node),一组节点构成一个集群(cluster),而一个 集群 是一组拥有相同 cluster.name 的节点, 他们能一起工作并共享数据,还提供容错与可伸缩性。

索引 (Index)

Elasticsearch 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。

Elasticsearch 数据管理的顶层单位就叫做 Index(索引),每个 Index 的名称必须是全小写形式的。

文档 (Document)

Index 中单条记录称为文档 (Document),许多条 Document 构成了一个 Index。Document 使用 JSON 格式表示,例子如下:

1
2
3
4
{
"name":"张三",
"age": 28
}

同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。

Type

Document 可以分组,比如 weather 这个 Index 里面,可以按城市分组(北京和上海),也可以按气候分组(晴天和雨天)。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document。但 Elasticsearch 在 7.x 之后的版本中逐渐移除 Type,为了提升搜索速度。

字段 (Fields)

每个 Document 都是一个 JSON 结构,它包含了许多字段,每个字段都有其对应的值,多个字段组成了一个 Document。

注意事项

很多人将 Elsticsearch 中的 index 理解为关系数据中的 database,而将 type 理解为数据库中的一个数据表,虽然表面上看两者确实存在相似性,但却是错误的类比。

我们都知道 Elasticsearch 是基于 Lucene 开发的,而 Elasticsearch 中不同 type 下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。举个例子,两个不同 type 下的两个 user_name,在 Elasticsearch 同一个索引下其实被认为是同一个 filed,你必须在两个不同的 type 中定义相同的 filed 映射。否则,不同 type 中的相同字段名称就会在处理中出现冲突的情况,导致 Lucene 处理效率下降。

结尾

具体如何使用 Elsaticsearch 与 Elasticsearch-py 留到下一篇再详解,最后欢迎学习 HackPython 的教学课程并感觉您的阅读与支持。

HackPython致力于有趣有价值的编程教学

简介

当我们希望将自己编写好的程序传送给他人使用时,如果对方没有安装 Python 环境就无法使用程序了,我们难以让每个使用者都安装 Python 环境,是否可以在不必安装 Python 环境的前提下使用Python程序呢?

可以使用 Pyinstaller 将程序打包,然后再分享给其他人使用,他人使用只需双击运行打包好的程序。

可以通过 pip 来安装,命令如下:

1
pip install pyinstaller

使用

pyinstaller 最重要的两个参数就是 -F 与 -D 参数。

使用-F参数, pyinstaller 会将 python 程序打包成单个可执行文件。

使用-D参数, pyinstaller 会将 python 程序打包成一个文件夹,运行程序时,需要进入该文件夹,点击运行相应的可执行程序。

为了美观,还可以通过-i参数指定打包程序的图标(icon),但这个命令只能在 Windows 平台下生效,此外还可以使用-n参数指定生成打包文件的名称。

如果你使用了 PyQt5 或 tkinter 开发了界面,通常不会希望程序运行时弹出 cmd 命令行,此时就可以使用-w参数。

简单总结一下:

-F:打包 Python 程序为单个可执行文件
-D:打包 Python 程序为一个文件夹
-i:生成图标,只适用于 Windows 平台
-n:指定打包后生成文件的名称
-w:禁止命令行弹出

综上所述,最常见的命令为:

1
pyinstaller -i xxx.ico -n xxx -w -D xxx.py

-i 参数后必须接 .ico 结尾的图标文件
-D 或 -F 后必须接 python 程序的入库程序,常见情况为 main.py

对应依赖比较多的程序,建议使用 -D, -F 更适合单文件的 py 脚本。

简单原理

打包时,pyinstaller此时会生成相应的spec文件,大体流程如下:

1、在脚本目录生成 xxx.spec 文件(取决于 -n 参数,没传,则与 xxx.py 同名为 xxx );
2、创建一个 build 目录;
3、写入一些日志文件和中间流程文件到 build 目录;
4、创建 dist 目录;
5、生成可执行文件或文件夹到 dist 目录;

此时,进入 dist 目录就可以看见自己的打包文件了。

双击 nameauto.exe 文件,效果如下:

效果如下:

这是一个利用 tkinter 构建的程序。

注意事项

Pyinstaller 是跨平台的,但并不是指其生成应用是跨平台的,而是 Pyinstaller 本身是跨平台的,在 Windows 平台下,可以打包出 exe 文件。

避免打包后,包文件过大

为了避免 Pyinstaller 打包后程序或文件夹过大,如:几百 KB 的程序打包后编程 500M 左右的程序,在引用包时,尽量使用 from … import … 语句,这是因为 Pyinstaller 打包的路径其实是将 python 解释器以及项目中使用的库直接复制过来,所以如果你没事就别 import… ,那么 Pyinstaller 会将整个模块复制过去,此时打出来的包就会很大。

考虑路径问题

使用 python 时,要养成使用 os.path.join 的习惯,这不仅可以避免跨平台的路径坑( windows 路径表达与类 Unix 是不同),又可以在打包时不会出现相对路径的问题,很多 python 程序员编写路径喜欢使用 + 号来链接路径,这会增加项目的维护成本

pyinstaller 打包的项目遇到路径都使用 os.path.join 则可

外部数据问题

虽然在上节中,提及了使用外部数据时,可以自定义 spec 文件中的 datas 字段,但我更常用的做法是直接将数据复制过去,不去修改datas。

比如我的项目中依赖 config 文件夹下的配置文件,执行将 config 文件夹整体直接复制到打包好的文件夹中则可

闪屏结束

如果是简单的程序,可能会出现运行可执行程序后出现一闪而过的情况,这种情况下要么是程序运行结束(比如直接打印的 helloWorld),要么程序出现错误退出了。

这种情况要么通过 input() 函数捕捉输入自己主动结束程序,要么就在 cmd 下运行 exe 文件,从而通过 cmd 看到效果

结尾

掌握了 Pyinstaller 后,你就可以将任意程序打包发送给他人了,最后欢迎学习 HackPython 的教学课程并感觉您的阅读与支持。