0%

Python

在本教程中,你将了解如何使用 pathlib 模块操作目录和文件的名称。 学习如何读取和写入文件,拼接路径和操作底层文件系统的新方法,以及如何列出文件并迭代它们的一些示例。 大多人处理文件用的最多的还是 os 模快吧,比如下面这样的操作

1
>>> path.rsplit('\\', maxsplit=1)[0]

或者写出下面这样长长的代码

1
>>> os.path.isfile(os.path.join(os.path.expanduser('~'), 'realpython.txt'))

使用 pathlib 模块,可以使代码使用优雅,可读和 Pythonic 代码重写上面的两个示例,如:

1
2
>>> path.parent
>>> (pathlib.Path.home() / 'realpython.txt').is_file()

Python 文件路径处理问题

由于许多不同的原因,使用文件和与文件系统交互很重要。 最简单的情况可能只涉及读取或写入文件,但有时候会有更复杂的任务。 也许你需要列出给定类型的目录中的所有文件,查找给定文件的父目录,或者创建一个尚不存在的唯一文件名。 一般情况,Python 使用常规文本字符串表示文件路径。 一般在使用 os,glob 和 shutil 等库的时候会使用到路径拼接的操作,使用os模块拼接起来显得略显复杂,以下示例仅需要三个 import 语句来将所有文本文件移动到归档目录:

1
2
3
4
5
6
7
import glob
import os
import shutil

for file_name in glob.glob('*.txt'):
new_path = os.path.join('archive', file_name)
shutil.move(file_name, new_path)

使用常规的字符串去拼接路径是可以的,但是由于不同的操作系统使用的分隔符不同,这样就容易出现问题,所以一般我们使用最多的还是使用 os.path.join()。 Python 3.4 中引入了 pathlib 模块(PEP 428)再一次的优化了路径的拼接。使用 pathlib 库的 Path 方法,可以将一个普通的字符串转换为 pathlib.Path 对象类型的路径。 早期,其他软件包仍然使用字符串作为文件路径,但从 Python 3.6 开始,pathlib 模块在整个标准库中得到支持,部分原因是由于增加了文件系统路径协议。 如果你坚持使用传统的 Python,那么 Python 2 也有一个可用的向后移植。 ok,说了那么多下面让我们看看 pathlib 如何在实践中发挥作用。

创建路径

这里我们首先要知道两个用法,先看代码:

1
from pathlib import Path

你真正需要知道的是 pathlib.Path 类。 创建路径有几种不同的方式。 首先,有类方法,如 .cwd(当前工作目录)和 .home(用户的主目录):

1
2
3
4
5
6
7
from pathlib import Path

now_path = Path.cwd()
home_path = Path.home()

print("当前工作目录",now_path,type(now_path))
print("home目录",home_path,type(home_path))

输出内容

1
2
当前工作目录 /Users/chennan/pythonproject/demo <class 'pathlib.PosixPath'>
home目录 /Users/chennan <class 'pathlib.PosixPath'>

可以发现路径格式为 pathlib.PosixPath 这是在 unix 系统下的显示。在不同的系统上显示的格式也是不一样,在 windows 系统会显示为 WindowsPath。但是不管什么显示类型,都不影响后面的操作。 前面我们提到过可以通过把字符串类型的路径,转换为 Pathlib.Path 类型的路径,经过测试发现在 Python3.4 以后很多模块以及支持该格式的路径。不用转为成字符串使用了。比起 os.path.join 拼接路径的方式, pathlib 使用起来更加的方便,使用示例如下:

1
2
3
import pathlib
DIR_PATH = pathlib.Path("/Users/chennan/CDM")
print(DIR_PATH,type(DIR_PATH))

输出内容:

1
/Users/chennan/CDM <class 'pathlib.PosixPath'>

通过 “/“ 我们就可以对路径进行拼接了,怎么样是不是很方便呢。

读文件和写文件

在我们使用 open 来操作文件读写操作的时候,不仅可以使用字符串格式的路径,对于 pathlib 生成的路径完全可以直接使用:

1
2
3
4
path = pathlib.Path.cwd() / 'test.md'
with open(path, mode='r') as fid:
headers = [line.strip() for line in fid if line.startswith('#')]
print('\n'.join(headers))

或者在 pathlib 的基础使用 open,我们推荐使用下面的方式

1
2
3
4
5
import pathlib
DIR_PATH = pathlib.Path("/Users/chennan/CDM") / "2000" / "hehe.txt"
with DIR_PATH.open("r") as fs:
data = fs.read()
print(data)

这样写的好处就是 open 里面我们不需要再去传入路径了,直接指定文件读写模式即可。实际上这里的 open 方法,底层也是调用了 os.open 的方法。使用哪种方式看个人的喜好。 pathlib 还提供几种文件的读写方式: 可以不用再使用 with open 的形式即可以进行读写。

1
2
3
4
.read_text(): 找到对应的路径然后打开文件,读成str格式。等同open操作文件的"r"格式。
.read_bytes(): 读取字节流的方式。等同open操作文件的"rb"格式。
.write_text(): 文件的写的操作,等同open操作文件的"w"格式。
.write_bytes(): 文件的写的操作,等同open操作文件的"wb"格式。

使用 resolve 可以通过传入文件名,来返回文件的完整路径,使用方式如下

1
2
3
import pathlib
py_path =pathlib.Path("superdemo.py")
print(py_path.resolve())

输出

1
/Users/chennan/pythonproject/demo/superdemo.py

需要注意的是 “superdemo.py” 文件要和我当前的程序文件在同一级目录。

选择路径的不同组成部分

pathlib 还提供了很多路径操作的属性,这些属性可以选择路径的不用部位,如 .name: 可以获取文件的名字,包含拓展名。 .parent: 返回上级文件夹的名字 .stem: 获取文件名不包含拓展名 .suffix: 获取文件的拓展名 .anchor: 类似盘符的一个东西,

1
2
3
4
5
6
7
8
import pathlib

now_path = pathlib.Path.cwd() / "demo.txt"
print("name",now_path.name)
print("stem",now_path.stem)
print("suffix",now_path.suffix)
print("parent",now_path.parent)
print("anchor",now_path.anchor)

输出内容如下

1
2
3
4
5
name demo.txt
stem demo
suffix .txt
parent /Users/chennan/pythonproject/demo
anchor /

移动和删除文件

当然 pathlib 还可以支持文件其他操作,像移动,更新,甚至删除文件,但是使用这些方法的时候要小心因为,使用过程不用有任何的错误提示即使文件不存在也不会出现等待的情况。 使用 replace 方法可以移动文件,如果文件存在则会覆盖。为避免文件可能被覆盖,最简单的方法是在替换之前测试目标是否存在。

1
2
3
4
5
6
import pathlib

destination = pathlib.Path.cwd() / "target"
source = pathlib.Path.cwd() / "demo.txt"
if not destination.exists():
source.replace(destination)

但是上面的方法存在问题就是,在多个进程多 destination 进行的操作的时候就会现问题,可以使用下面的方法避免这个问题。也就是说上面的方法适合单个文件的操作。

1
2
3
4
5
6
7
import pathlib

destination = pathlib.Path.cwd() / "target"
source = pathlib.Path.cwd() / "demo.txt"
with destination.open(mode='xb') as fid:
#xb表示文件不存在才操作
fid.write(source.read_bytes())

当 destination文件存在的时候上面的代码就会出现 FileExistsError 异常。 从技术上讲,这会复制一个文件。 要执行移动,只需在复制完成后删除源即可。 使用 with_name 和 with.shuffix 可以修改文件名字或者后缀。

1
2
3
import pathlib
source = pathlib.Path.cwd() / "demo.py"
source.replace(source.with_suffix(".txt")) #修改后缀并移动文件,即重命名

可以使用 .rmdir() 和 .unlink() 来删除文件。

1
2
3
4
5
import pathlib

destination = pathlib.Path.cwd() / "target"
source = pathlib.Path.cwd() / "demo.txt"
source.unlink()

几个 pathlib 的使用例子

统计文件个数

我们可以使用.iterdir方法获取当前文件下的所以文件.

1
2
3
4
5
import pathlib
from collections import Counter
now_path = pathlib.Path.cwd()
gen = (i.suffix for i in now_path.iterdir())
print(Counter(gen))

输出内容

1
Counter({'.py': 16, '': 11, '.txt': 1, '.png': 1, '.csv': 1})

通过配合使用 collections 模块的 Counter 方法,我们获取了当文件夹下文件类型情况。 前面我们说过 glob 模块点这里了解【https://www.cnblogs.com/c-x-a/p/9261832.html】,同样的 pathlib 也有 glob 方法和 rglob 方法,不同的是 glob 模块里的 glob 方法结果是列表形式的,iglob 是生成器类型,在这里 pathlib 的 glob 模块返回的是生成器类型,然后 pathlib 还有一个支持递归操作的 rglob 方法。 下面的这个操作我通过使用 glob 方法,设定规则进行文件的匹配。

1
2
3
4
import pathlib
from collections import Counter
gen =(p.suffix for p in pathlib.Path.cwd().glob('*.py'))
print(Counter(gen))

展示目录树

下一个示例定义了一个函数 tree(),该函数的作用是打印一个表示文件层次结构的可视树,该树以一个给定目录为根。因为想列出其子目录,所以我们要使用 .rglob() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
import pathlib
from collections import Counter
def tree(directory):
print(f'+ {directory}')
for path in sorted(directory.rglob('*')):
depth = len(path.relative_to(directory).parts)
spacer = ' ' * depth
print(f'{spacer}+ {path.name}')

now_path = pathlib.Path.cwd()

if __name__ == '__main__':
tree(now_path)

其中 relative_to 的方法的作用是返回 path 相对于 directory 的路径。 parts 方法可以返回路径的各部分。例如

1
2
3
4
import pathlib
now_path = pathlib.Path.cwd()
if __name__ == '__main__':
print(now_path.parts)

返回

1
('/', 'Users', 'chennan', 'pythonproject', 'demo')

获取文件最后一次修改时间

iterdir(),.glob()和.rglob()方法非常适合于生成器表达式和列表理解。 使用stat()方法可以获取文件的一些基本信息,使用.stat().st_mtime可以获取文件最后一次修改的信息

1
2
3
4
5
import pathlib
now_path = pathlib.Path.cwd()
from datetime import datetime
time, file_path = max((f.stat().st_mtime, f) for f in now_path.iterdir())
print(datetime.fromtimestamp(time), file_path)

甚至可以使用类似的表达式获取上次修改的文件内容

1
2
3
4
5
import pathlib
from datetime import datetime
now_path =pathlib.Path.cwd()
result = max((f.stat().st_mtime, f) for f in now_path.iterdir())[1]
print(result.read_text())

.stat().st_mtime 会返回文件的时间戳,可以使用 datetime 或者 time 模块对时间格式进行进一步转换。

其他内容

关于 pathlib.Path 格式路径转换为字符串类型

因为通过 pathlib 模块操作生成的路径,不能直接应用字符串的一些操作,所以需要转换成字符串,虽然可以使用 str() 函数进行转换,但是安全性不高,建议使用 os.fspath() 方法,因为如果路径格式非法的,可以抛出一个异常。str()就不能做到这一点。

拼接符号”/“背后的秘密

/ 运算符由 truediv 方法定义。 实际上,如果你看一下 pathlib 的源代码,你会看到类似的东西。

1
2
3
4
class PurePath(object):

def __truediv__(self, key):
return self._make_child((key,))

后记

从 Python 3.4 开始,pathlib 已在标准库中提供。 使用 pathlib,文件路径可以由适当的 Path 对象表示,而不是像以前一样用纯字符串表示。 这些对象使代码处理文件路径:

  • 更容易阅读,特别是可以使用“/”将路径连接在一起
  • 更强大,直接在对象上提供最必要的方法和属性
  • 在操作系统中更加一致,因为Path对象隐藏了不同系统的特性

在本教程中,你已经了解了如何创建 Path 对象、读取和写入文件、操作路径和底层文件系统,以及如何遍历多个文件路径等一系列实例。 最后,建议下去自己多加练习,我对文章中的代码都进行了验证,不会出现运行错误的情况。 ————————————————————————————————————————————— 原文: https://realpython.com/python-pathlib/ 译者: 陈祥安 [gallery ids=”6600”] 更多精彩内容,请关注微信公众号: python学习开发。

JavaScript

想写这篇文章很久了,也想做这件事很久了,我个人感觉自己是有强迫症的,所以一直有什么事让我看着不太舒服就想把它纠正过来。 文字,也不例外。 现在大家看各种新闻啊、文章啊,几乎每篇文章都会有点数字和英文的吧,比如就拿 Python 来说,看下面两句话:

  • 卧槽 Python 真牛逼啊排名第 1 了。
  • 卧槽 Python 真牛逼啊排名第 1 了。

Python 是不是第一先不说,就看看上面两句话的排版,哪个看起来更舒服?说实话我是真觉得第一句话太别扭了。因为我们大部分的文本编辑器和浏览器是没有对中文和外文的混排做排版优化的,所以如果写的时候如果二者之间不加个空格,二者就会紧紧贴在一起,然后就变成了上面第一句的样子。 当然如果你觉得第一句的排版更好看,好吧,那么本文后面的内容其实可以不必看了。OK,如果你觉得第二个好看,那不妨接着看下去哈。

出发点

首先有一点需要明确的是,中英文排版的美学是在于 Readability,易读性。而为了易读性,中英文之间是需要留有”间距”的,注意这里是间距,不是说的”空格”。”空格”会造成间距,但是间距不一定非得需要”空格”。 好,所以,其实我们只需要留有适当的间距,就会显得美观易读,这个间距大约是一个半角空格的距离。 好明确了这一点,我们只要能留有间距,不一定非得加空格。 现在很多专业的排版软件,比如 Adobe InDesign、Microsoft Word 对中英文混排支持非常好,他们会有这么一个功能:可以设置中文西文之间留适当的间距。 所以,如果如果我们使用了这些软件,本身就可以做到 Readability,这就够了。 但是,为什么还会说空格的问题呢?这是因为现在绝大多数软件,不管是文本编辑器还是网页,都没有这个机制。 几乎所有的文本编辑器和浏览器中,只要我们中文和英文连续输入,它们之间是不会出现间距的,就像文章开头所示的样例中的第一句话,显得很别扭。但比如 Adobe InDesign、Microsoft Word、IE 浏览器会有这方面的支持。 所以,怎么解决?手动加空格。 因此,总结下:

  • 间距要有,但不一定是空格。
  • 部分软件能自动呈现间距,那就不必加空格。
  • 绝大多数软件不能自动呈现间距,那就需要手动加空格。

所以,作为强迫症的我,一定是会为了这个间距而去敲下一个空格的。 「有研究显示,打字的时候不喜欢在中文和英文之间加空格的人,感情路都走得很辛苦,有七成的比例会在 34 岁的时候跟自己不爱的人结婚,而其余三成的人最后只能把遗产留给自己的猫。毕竟爱情跟书写都需要适时地留白。与大家共勉之。」 盘古之白 所以,求求你加个空格吧(逃。

规范

好,下面就说到规范的问题了,到底什么时候应该加空格什么时候不加,这也是有讲究的。下面的内容摘自 GitHub 上的一个中英文混排规范,网址为:https://github.com/mzlogin/chinese-copywriting-guidelines,下面转述一下。

1. 中英文之间需要增加空格

中英文之间是需要添加空格的,不论是普通英文还是引用的英文,下面给个示例: 正确:

  • 在 LeanCloud 上,数据存储是围绕 AVObject 进行的。

错误:

  • 在 LeanCloud 上,数据存储是围绕AVObject进行的。
  • 在 LeanCloud 上,数据存储是围绕AVObject 进行的。

完整的正确用法:

  • 在 LeanCloud 上,数据存储是围绕 AVObject 进行的。每个 AVObject 都包含了与 JSON 兼容的 key-value 对应的数据。数据是 schema-free 的,你不需要在每个 AVObject 上提前指定存在哪些键,只要直接设定对应的 key-value 即可。

但有例外,比如「豆瓣 FM」等产品名词,按照官方所定义的格式书写。 再比如,我的公众号为「进击的 Coder」,那么这里面就不要加空格,按照其本身的形式书写即可。

中文与数字之间需要增加空格

中文和数字之间也是需要的,下面给个示例: 正确:

  • 今天出去买菜花了 5000 元。

错误:

  • 今天出去买菜花了 5000 元。
  • 今天出去买菜花了 5000 元。

数字与单位之间无需增加空格

但是数字和单位之间不需要再加额外的空格了,下面给个 正确:

  • 我家的光纤入户宽带有 10Gbps,SSD 一共有 10TB。

错误:

  • 我家的光纤入户宽带有 10 Gbps,SSD 一共有 20 TB。

另外,度/百分比与数字之间不需要增加空格: 正确:

  • 今天是 233° 的高温。
  • 新 MacBook Pro 有 15% 的 CPU 性能提升。

错误:

  • 今天是 233 ° 的高温。
  • 新 MacBook Pro 有 15 % 的 CPU 性能提升。

全角标点与其他字符之间不加空格

标点是分全角和半角的,全角标点一般是在中文状态下输出来的,比如 ,半角标点一般是在英文状态下输出来的,比如 ,.!,两个看起来不一样吧?所以,如果是中文标点,即全角标点,那不需要加空格。 正确:

  • 刚刚买了一部 iPhone,好开心!

错误:

  • 刚刚买了一部 iPhone ,好开心!

嗯,基本就是以上的几个规范,只要明白了这些规范,中英文混排就 OK 了!

网页

有人说,我就是不想打空格,在网页中,我能像 Microsoft Word 一样不打空格而直接显示间距吗? 也就是说,我能不能设置一个 CSS 样式,就能使得中英文之间自动留有间距呢? 其实,只有 IE 有这样的支持。这个 CSS 样式叫做 \-ms-text-autospace ,可以在这里了解下:https://msdn.microsoft.com/library/ms531164(v=vs.85).aspx.aspx>)。 但是很遗憾的是,几乎所有其他的浏览器都不支持这个,Chrome、Firefox 统统都不支持这个特性。放弃吧。 image-20190507220822252 这里提供一些手动的解决方案,比如使用 JavaScript 添加标记,然后 CSS 控制标记的间距,解决方案可以参考:http://mastermay.github.io/text-autospace.js/

编辑器

那么有编辑器支持这个吗?有,Microsoft Word,用它我们不用加空格,会自动给我们加好间距。 有人说,我平时不想用 Word,我就想用 Markdown,有编辑器吗?有,叫做 MarkEditor,它的 2.0 Pro 版本可以在打字的时候自动给我们添加空格。注意,这里是自动添加空格,不是自动留间距,是用空格的方式实现了间距。但是这个只能在你一个个打字的时候自动添加空格,如果把一个不带空格的话粘贴进去是不行的。另外 MarkEditor 解锁这个功能需要付费,所以我个人感觉其实不太划算的。 所以,平时还是自己手动加空格吧,经济实惠方便。 其他的编辑器如有好用的欢迎大家推荐哈。

类库

好吧,看到现在,你是不是现在都想把自己的中英文笔记加上空格了?难道要手调吗?不需要。 有现成的工具了,名字叫做 pangu,它支持各种语言,另外还有浏览器插件可以用,列表如下:

浏览器插件

开发工具包

比如 Python 的话,就可以使用 pangu.py 这个包,GitHub 地址为:https://github.com/vinta/pangu.py,安装方式如下

1
pip3 install -U pangu

这么用就好了:

1
2
import pangu
print(pangu.spacing_text('當你凝視著bug,bug也凝視著你'))

运行结果如下:

1
當你凝視著 bug,bug 也凝視著你

嗯,它自动给我们添加好了空格,非常不错。 不过这有点费劲,有简单一点的工具吗? 有,我为此专门做了一个网页,功能很简单。 在左侧输入源文本,右侧就会显示添加空格之后的文本,页面如下: image-20190507222427295 这个是我用 Vue.js 开发的,实际上就是用了 pangu.js 这个库实现的,原理非常简单,主要目的就是为了方便空格排版。 另外这个网站我也部署了一下,叫做:http://space.cuiqingcai.com/,大家以后也可以直接访问使用,以后我有想调整的文本,直接就用它了。 如果大家想获取源码,可以在公众号「进击的 Coder」回复”空格”。 希望对大家有所帮助。 最后,为了世界的美好与和平,加个空格吧!

Python

最近碰到了一个问题,项目中很多文件都是接手过来的中文命名的一些素材,结果在部署的时候文件名全都乱码了,导致项目无法正常运行。 后来请教了一位大佬怎么解决文件名乱码的问题,他说这个需要正面解决吗?不需要,把文件名全部改掉,文件名永远不要用中文,永远不要。 我想他这么说的话,一定也是凭经验得出来的。 这里也友情提示大家,项目里面文件永远不要用中文,永远不要! 好,那不用中文用啥?平时来看,一般我们都会用英文来命名,一般也不会出现中文,比如 resource, controller, result, view, spider 等等,所以绝大多数情况下,是不会出现什么问题的。但是也有个别的情况,比如一些素材、资源文件可能的中文命名的,那么这时候该咋办呢? 首先像,因为是中文资源文件,我们要改成非中文命名的,无非两种,一种是英文,一种是拼音。 如果改英文,当然可以翻译、我们想翻译的话,逐个人工翻译成本太高,机器翻译的话,翻译完可能有些文不对题了,而且我们自己也不知道一些奇怪的资源英语应该叫什么,所以到时候真的找起来都找不到了。 所以第二种解决方案,那就是拼音了。中文转拼音,很自然,而且一个字就对应一串拼音,而且也非常容易从拼音看懂是什么意思,所以这确实是一个不错的方案。 那么问题就来了,怎样把一批中文文件转拼音命名呢?下面就让我们来了解 Python 的一个库 PyPinyin 吧!

概述

Python 中提供了汉字转拼音的库,名字叫做 PyPinyin,可以用于汉字注音、排序、检索等等场合,是基于 hotto/pinyin 这个库开发的,一些站点链接如下:

它有这么几个特性:

  • 根据词组智能匹配最正确的拼音。
  • 支持多音字。
  • 简单的繁体支持, 注音支持。
  • 支持多种不同拼音/注音风格。

是不是等不及了呢?那就让我们来了解一下它的用法吧!

安装

首先就是这个库的安装了,通过 pip 安装即可:

1
pip3 install pypinyin

安装完成之后导入一下这个库,如果不报错,那就说明安装成功了。

1
\>>> import pypinyin

好,接下来我们看下它的具体功能。

基本拼音

首先我们进行一下基本的拼音转换,方法非常简单,直接调用 pinyin 方法即可:

1
2
from pypinyin import pinyin
print(pinyin('中心'))

运行结果:

1
[['zhōng'], ['xīn']]

可以看到结果会是一个二维的列表,每个元素都另外成了一个列表,其中包含了每个字的读音。 那么如果这个词是多音字咋办呢?比如 “朝阳”,它有两个读音,我们拿来试下:

1
2
from pypinyin import pinyin
print(pinyin('朝阳'))

运行结果:

1
[['zhāo'], ['yáng']]

好吧,它只给出来了一个读音,但是如果我们想要另外一种读音咋办呢? 其实很简单,只需添加 heteronym 参数并设置为 True 就好了,我们试下:

1
2
from pypinyin import pinyin
print(pinyin('朝阳', heteronym=True))

运行结果:

1
[['zhāo', 'cháo'], ['yáng']]

OK 了,这下子就显示出来了两个读音了,而且我们也明白了结果为什么是一个二维列表,因为里面的一维的结果可能是多个,比如多音字的情况就是这样。 但这个多少解析起来有点麻烦,很多情况下我们是不需要管多音字的,我们只是用它来转换一下名字而已,而处理上面的二维数组又比较麻烦。 所以有没有一个方法直接给我们一个一维列表呢?有! 我们可以使用 lazy_pinyin 这个方法来生成,尝试一下:

1
2
from pypinyin import lazy_pinyin
print(lazy_pinyin('聪明的小兔子'))

运行结果:

1
['cong', 'ming', 'de', 'xiao', 'tu', 'zi']

这时候观察到得到的是一个列表,并且不再包含音调了。 这里我们就有一个疑问了,为啥 pinyin 方法返回的结果默认是带音调的,而 lazy_pinyin 是不带的,这里面就涉及到一个风格转换的问题了。

风格转换

我们可以对结果进行一些风格转换,比如不带声调风格、标准声调风格、声调在拼音之后、声调在韵母之后、注音风格等等,比如我们想要声调放在拼音后面,可以这么来实现:

1
2
3
4
from pypinyin import lazy_pinyin, Style

style = Style.TONE3
print(lazy_pinyin('聪明的小兔子', style=style))

运行结果:

1
['cong1', 'ming2', 'de', 'xiao3', 'tu4', 'zi']

可以看到运行结果每个拼音后面就多了一个声调,这就是其中的一个风格,叫做 TONE3,其实还有很多风格,下面是我从源码里面找出来的定义:

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
#: 普通风格,不带声调。如: 中国 -> ``zhong guo``
NORMAL = 0
#: 标准声调风格,拼音声调在韵母第一个字母上(默认风格)。如: 中国 -> ``zhōng guó``
TONE = 1
#: 声调风格2,即拼音声调在各个韵母之后,用数字 [1-4] 进行表示。如: 中国 -> ``zho1ng guo2``
TONE2 = 2
#: 声调风格3,即拼音声调在各个拼音之后,用数字 [1-4] 进行表示。如: 中国 -> ``zhong1 guo2``
TONE3 = 8
#: 声母风格,只返回各个拼音的声母部分(注:有的拼音没有声母,详见 `#27`_)。如: 中国 -> ``zh g``
INITIALS = 3
#: 首字母风格,只返回拼音的首字母部分。如: 中国 -> ``z g``
FIRST_LETTER = 4
#: 韵母风格,只返回各个拼音的韵母部分,不带声调。如: 中国 -> ``ong uo``
FINALS = 5
#: 标准韵母风格,带声调,声调在韵母第一个字母上。如:中国 -> ``ōng uó``
FINALS_TONE = 6
#: 韵母风格2,带声调,声调在各个韵母之后,用数字 [1-4] 进行表示。如: 中国 -> ``o1ng uo2``
FINALS_TONE2 = 7
#: 韵母风格3,带声调,声调在各个拼音之后,用数字 [1-4] 进行表示。如: 中国 -> ``ong1 uo2``
FINALS_TONE3 = 9
#: 注音风格,带声调,阴平(第一声)不标。如: 中国 -> ``ㄓㄨㄥ ㄍㄨㄛˊ``
BOPOMOFO = 10
#: 注音风格,仅首字母。如: 中国 -> ``ㄓ ㄍ``
BOPOMOFO_FIRST = 11
#: 汉语拼音与俄语字母对照风格,声调在各个拼音之后,用数字 [1-4] 进行表示。如: 中国 -> ``чжун1 го2``
CYRILLIC = 12
#: 汉语拼音与俄语字母对照风格,仅首字母。如: 中国 -> ``ч г``
CYRILLIC_FIRST = 13

有了这些,我们就可以轻松地实现风格转换了。 好,再回到原来的问题,为什么 pinyin 的方法默认带声调,而 lazy_pinyin 方法不带声调,答案就是:它们二者使用的默认风格不同,我们看下它的函数定义就知道了: pinyin 方法的定义如下:

1
def pinyin(hans, style=Style.TONE, heteronym=False, errors='default', strict=True)

lazy_pinyin 方法的定义如下:

1
def lazy_pinyin(hans, style=Style.NORMAL, errors='default', strict=True)

这下懂了吧,因为 pinyin 方法默认使用了 TONE 的风格,而 lazy_pinyin 方法默认使用了 NORMAL 的风格,所以就导致二者返回风格不同了。 好了,有了这两个函数的定义,我们再来研究下其他的参数,比如定义里面的 errors 和 strict 参数又怎么用呢?

错误处理

在这里我们先做一个测试,比如我们传入无法转拼音的字,比如:

1
2
from pypinyin import lazy_pinyin
print(lazy_pinyin('你好☆☆,我是xxx'))

其中包含了星号两个,还有标点一个,另外还包含了一个 xxx 英文字符,结果会是什么呢?

1
['ni', 'hao', '☆☆,', 'wo', 'shi', 'xxx']

可以看到结果中星号和英文字符都作为一个整体并原模原样返回了。 那么这种特殊字符可以单独进行处理吗?当然可以,这里就用到刚才提到的 errors 参数了。 errors 参数是有几种模式的:

  • default:默认行为,不处理,原木原样返回
  • ignore:忽略字符,直接抛掉
  • replace:直接替换为去掉 u 的 unicode 编码
  • callable 对象:当传入一个可调用的对象的时候,则可以自定义处理方式。

下面是 errors 这个参数的源码实现逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def _handle_nopinyin_char(chars, errors='default'):
"""处理没有拼音的字符"""
if callable_check(errors):
return errors(chars)

if errors == 'default':
return chars
elif errors == 'ignore':
return None
elif errors == 'replace':
if len(chars) > 1:
return ''.join(text_type('%x' % ord(x)) for x in chars)
else:
return text_type('%x' % ord(chars))

当处理没有拼音的字符的时候,errors 的不同参数会有不同的处理结果,更详细的逻辑可以翻看源码。 好了,下面我们来尝试一下,比如我们想将不能转拼音的字符去掉,则可以这么设置:

1
2
from pypinyin import lazy_pinyin
print(lazy_pinyin('你好☆☆,我是xxx', errors='ignore'))

运行结果:

1
['ni', 'hao', 'wo', 'shi']

如果我们想要自定义处理,比如把 转化为 ,则可以这么设置:

1
print(lazy_pinyin('你好☆☆,我是xxx', errors=lambda item: ''.join(['※' if c == '☆' else c for c in item])))

运行结果:

1
['ni', 'hao', '※※,', 'wo', 'shi', 'xxx']

如上便是一些相关异常处理的操作,我们可以随心所欲地处理自己想处理的字符了。

严格模式

最后再看下 strict 模式,这个参数用于控制处理声母和韵母时是否严格遵循 《汉语拼音方案》 标准。 下面的一些说明来源于官方文档: 当 strict 参数为 True 时根据 《汉语拼音方案》 的如下规则处理声母、在韵母相关风格下还原正确的韵母:

  • 21 个声母: b p m f d t n l g k h j q x zh ch sh r z c sy, w 不是声母
  • i行的韵母,前面没有声母的时候,写成yi(衣),ya(呀),ye(耶),yao(腰),you(忧),yan(烟), yin(因),yang(央),ying(英),yong(雍)。(y 不是声母
  • u行的韵母,前面没有声母的时候,写成wu(乌),wa(蛙),wo(窝),wai(歪),wei(威),wan(弯), wen(温),wang(汪),weng(翁)。(w 不是声母
  • ü行的韵母,前面没有声母的时候,写成yu(迂),yue(约),yuan(冤),yun(晕);ü上两点省略。 (韵母相关风格下还原正确的韵母 ü
  • ü行的韵跟声母j,q,x拼的时候,写成ju(居),qu(区),xu(虚),ü上两点也省略; 但是跟声母n,l拼的时候,仍然写成nü(女),lü(吕)。(韵母相关风格下还原正确的韵母 ü
  • iou,uei,uen前面加声母的时候,写成iu,ui,un。例如niu(牛),gui(归),lun(论)。 (韵母相关风格下还原正确的韵母 iou,uei,uen

当 strict 为 False 时就是不遵守上面的规则来处理声母和韵母, 比如:y, w 会被当做声母,yu(迂) 的韵母就是一般认为的 u 等。 具体差异可以查看源码中 tests/test_standard.py 中的对比结果测试用例。

自定义拼音

如果对库返回的结果不满意,我们还可以自定义自己的拼音库,这里用到的方法就有 load_single_dict 和 load_phrases_dict 方法了。 比如刚才我们看到 “朝阳” 两个字的发音默认返回的是 zhao yang,我们想默认返回 chao yang,那可以这么做:

1
2
3
4
5
6
7
8
from pypinyin import lazy_pinyin, load_phrases_dict

print(lazy_pinyin('朝阳'))
personalized_dict = {
'朝阳': [['cháo'], ['yáng']]
}
load_phrases_dict(personalized_dict)
print(lazy_pinyin('朝阳'))

这里我们自定义了一个词典,然后使用 load_phrases_dict 方法设置了一下就可以了。 运行结果:

1
2
['zhao', 'yang']
['chao', 'yang']

这样就可以完成自定义的设置了。 在一些项目里面我们可以自定义很多拼音库,然后加载就可以了。 另外我们还可以注册样式实现自定义,比如将某个拼音前面加上 Emoji 表情,样例:

1
2
3
4
5
6
7
8
9
10
from pypinyin.style import register
from pypinyin import lazy_pinyin

@register('kiss')
def kiss(pinyin, **kwargs):
if pinyin == 'me':
return f'?{pinyin}'
return pinyin

print(lazy_pinyin('么么哒', style='kiss'))

运行结果:

1
['?me', '?me', 'dá']

这里我们调用 register 方法注册了一个样式 style,然后转换的时候指定即可,通过观察运行结果我们可以发现,这样我们就可以将 me 字的拼音前面加上 ? 这个 Emoji 表情了。 以上就是 PyPinyin 这个库的基本用法,更多的用法建议大家看看源码或者看 API 文档:https://pypinyin.readthedocs.io/zh_CN/master/api.html

个人随笔

正式入职微软,提交了第一个 PR 之后,我坐在椅子上思考人生。终于我也变成了一名正式的企业员工,变成了一名正式的踏入社会的职业人士,从此我的学生生涯也算是画上了一个句号,不,更确切的说应该是画上了一个引号。 和同事租了房子,生活条件算是还不错,有了属于自己的房间,有了专属自己的衣柜、书橱、办公桌,想要的硬件、软件、日常用品想配就配,算是应有尽有了。首先日常生活上最大的感受就是自己的生活条件变得更好、更自由,不再像学校一样有各种限制了,虽然日常花销变多了,但总体上来说我更喜欢现在的生活环境。

回顾

先回想一下自己的学生生活吧,初高中就不说了,就天天上课为了高考,后来在大学基本上就是三点一线,宿舍——食堂——教室/实验室,然后读研,研究生模式也差不多,宿舍——食堂——实验室/公司。

编程入门

我大学的时候选的就是计算机科学与技术这个专业,当时学校开的第一门入门课就是 Java,当时可以说是对编程一窍不通,什么 print?打印机吗?控制台是什么?控制谁?什么面向对象?我对象在哪里?都是些什么玩意。就这样,随着老师的课堂洗脑和一些并不怎么感兴趣的编程作业,我慢慢理解了原来 print 是这么个玩意,对象原来不是那个对象,就慢慢对编程建立了一个概念。插句话,其实我感觉编程的一些概念和思维还是很重要的,有人说编程学不会,可能就是脑中没有形成一个比较清晰的概念,想清楚它能做到什么,怎样从用双手解决一件事的思维转换成用编程解决一件事的思维。 扯远了,但那时候仅仅 Java 是一门课而已,虽然最后考试考得还不错,但是还是不是很懂它能为我带来些什么东西。后来大学就开了一个叫做课程设计的课,意思就是说让自己动手编程实现一个可以操作的项目。我们当时学校要求的就是实现一个黑白棋在线对战系统,当时可把我为难坏了。后来了解到了这里面还挺复杂的,又得编写界面又得搞一些算法,还得搞一个在线 WebSocket 通信,当时可以说是毫无思路,然后就去网上搜一些 Java 的教程,当时是搜到了马士兵的 Java 课就顺着看了起来,可以说马士兵是我的 Java 最重要的启蒙老师了,慢慢地把一些原理和基础学会了之后就有了基本的编程思路了,开始上手编,做界面,做服务器等等。另外还有一些小插曲,有的界面还得抠图,当时为了打造一个完美的棋盘效果还学了 PS 来花了一个木质棋盘和黑白棋子,估计得花了好几个月的时间终于做出来了一个像样点的系统,虽然现在源码已经找不到了,但真的说这个课程设计真的让我理解了编程的一些思维和理念以及它能为我带来些什么,我能够用它做到什么,脑中的一些概念变得更加清晰了,收获非常大。 后来学校开了数据结构和算法的一些课,慢慢地我又对一些基础的算法和 C++ 的一些编程语言有了一定的了解。再后来就是一些基础专业课了,比如操作系统,计算机组成原理,计算机网络等等,总体来说其实我没太感觉出具体有多大作用,但你要假设我没学过,我可能有很多东西都不知其理。人就是这样,有些东西在拥有的时候觉不出有什么好的,但一旦没有才会有明显的感觉。

加实验室

好吧又扯远了,然后就可以说迈入了我人生中一个比较重要的点了,那就是加入学校的一个实验室。之前许多东西我和室友自己瞎倒腾,比如当时进行版本控制的话,就是自己手动压缩一下,命名项目的时候添加一个版本号并用下划线分割,后来进了实验室才知道还有 Git 这么牛逼的东西,于是乎就跟着学习了 Git,了解了 GitHub,觉得整个代码世界都光明了。当时我加入的是后台组,一开始是从 PHP 开始学起的,从原生的 PHP,到普通的 CodeIgniter 框架,到高级的 Laravel 框架,当时写的时候主要用是 MVC 模式,所以前端的东西也难免需要用到的,所以那会儿又学习了前端的一些知识,慢慢地就变成了 Web 前后端通吃,自己也可以逐渐完成一些大型项目的开发工作。当时还自己开通了博客来记录自己学习的一些经验,然后跟着实验室一起接外包做外包,做了不下十个门户及商业网站的开发。

学习爬虫

再往后可能就是临近大学毕业的时候了,那会儿实验室的一位学长写了一些爬虫的入门文章,当时也跟着学了起来,边学边记录,学的整理的一些知识点都放在了自己的博客上。后来又探索了一些新的爬取方案,也一并整理到博客上了,形成了一个入门到进阶的一套简易版教程,后来随着写的越来越多,来看的人也越来越多了,后来访问量也逐渐上来了,现在的话日均访问量可以达到 15000+。 再往后可能就差不多大学毕业了,当时由于是保研到北航的,所以就提前来到了北京开始了研究生的预备工作,也节省了不少时间。那会儿就有充足的时间来做自己的事情,比如学习一些网络课程,继续做一些关于爬虫的研究工作。当时随着我的博客访问量越来越大,图灵便联系我看能不能写一本关于爬虫的书,当时想的一个是可以把自己学习的知识好好整理一下,还可以作为自己的一部个人作品出版出来,的确是一件非常不错的事情,所以当时就答应下来了。不得不说写书的过程是非常艰辛的,舍去了好多平时的休息时间,同时还发现了自己的很多不足的地方去查漏补缺,最终也不得不延期了好几个月才交上稿。后来又审校了非常久的时间,到去年五月份才出版出来,定名字叫《Python3网络爬虫开发实战》。不过后来的销量还算不错,现在已经重印了 10 次,50000 本了。现在还在继续撰写第二版中,把一些过期的案例和知识点更新,再把一些新的技术加进去。

研究生生活

OK,当然研究生阶段也不是都写书了。研究生阶段其实一开始是比较迷茫的,其实当时并不知道自己毕业之后要做什么方面的工作以及想去哪里。最初读的时候是选了网络安全的方向,做一些 Web 渗透方面的研究,后来觉得研究得差不多了,就又转了自然语言处理的方向,从吴恩达的机器学习开始学起,然后了解了深度学习的一些模型,又了解了自然语言处理的一些知识。与此同时,我也在微软这边当实习生,从最初的爬虫、 Platform 再到 Science NLP 研究,慢慢地也认识了一些大佬,和他们一起交流的确让我学习到了不少。 由于我在微软这边实习时间不短,所以当时也参加了实习生转正的面试。微软整个的面试流程还是很规范和严格的,包括多面技术面,另外每一面的要求也都不低的。首先最基础的要求就是算法,给你几道题目,来白纸上把这道题的代码写出来,面试官会非常注重边界处理和细节把握,如果要写不出来,基本上也离凉凉不远了。接下来还有一些基本的公式推导,比如如果要面试机器学习算法工程师的话,可能会让手写推导 SVM、LR 等算法。另外还有一些系统设计题,来看看你的思维和架构是不是能达到要求。最后我记得还有一些智力题,看看反应得快不快。再往后的面试也是谈谈自己对行业的一些理解和看法,谈得还是比较深入的。总之考察得非常综合,当时准备面试的期间真的是无比焦虑,感觉人心惶惶的,当时在疯狂地刷题,复习各种算法推导,准备了也差不多有一两个月。最后得知 Offer 的那一刻,一块石头终于落地了。最后我也如愿入职了微软小冰,今年三月刚刚入职,也希望能为小冰带来更多的贡献,也希望大家可以多多关注微软小冰。 其实我已经在微软这边实习了一年多的时间了,平时很多时间也都呆在公司里,自己也算提前一步迈入职场了,经过我个人实习的体验和感受,同时也结合自己平时的了解,总结出来了一些经验。当然这仅仅是我个人的一些看法,仁者见仁智者见智,在这里仅仅做一些经验总结和分享,总结一些从学生迈入职场之后,我会注重的一些地方。如果对你有些启发,那是再好不过了。

工作相关

首先就是工作相关的一些东西了,由学生到工作,我个人觉得还是有一些需要调整的地方的,下面稍微这里说一下。

转换思维

学生到职场的转换,第一个重要的就是转变自己看待事情的思维。迈入职场,就别再有一种”我是学生“、”我刚刚毕业“ 这样的想法,在职场里别人才不管我们是不是刚毕业的学生,他们看的基本上都是我们能不能完成工作或者配合他们完成工作。 另外也别总有一种”努力必有回报“这样的思维。学生时代,可能一道题解不出来,一个项目做不出来,努努力很大程度上还是可以能解的。但是到了职场,这个很不一样,很多事情并不是一定存在因果这样的线性关系的。比如说某个项目你辛辛苦苦做了很久,可能就因为领导不想要这个功能而直接砍掉了。比如说你辛辛苦苦写了稿子,可能因为和某个评阅人的想法不一致而被直接拒掉了。想开点,有时候就是这么操蛋。 还有一个就是别把一件事想的太简单,在工作中,其实很多事上牵扯的东西是很多的。学生的一件事可能就是一件事,一道题可能就是一套题。工作中一件事可能不仅仅是一件事,它所关系到的东西要复杂的多。我们可能会考虑到对公司、对领导、对同事、对绩效、对家人的很多事情,多考虑考虑。 总之,思维的转换是第一步,别再像之前当学生的时候一样了。

靠真本事

首先得考虑清楚啊,公司把我们招来是为了让我们来发挥价值的。 而我们的价值在哪里发挥?当然是体现在工作成果中。那成果哪里来的?那当然是把自己的能力转化过来的。 不同职位有不同的要求,首先确保的是要利用自己的能力把本职工作做好。当然我是做技术的,我所专注的就是技术这个领域了,技术能力是必不可少的,当然做这份工作需要的其实也不仅仅是技术能力,还有一些非表面意义上的能力,如学习探索能力、沟通合作能力等等。 而这些吧,归根结底还是要靠自己的真本事的。别想着投机取巧,别想着耍点小聪明,虽然一时方便了,但是长远来看,吃亏的还是自己。 当然这例子很多,比如明明不是自己做的,却要为了某些目的非要伪装自己,被看穿之后,不少人其实是看破不说破的,慢慢地自己就会知道后果了。比如为了某些目标,背地里各种跟领导各种小恩小惠等,其实领导基本不会 Care 你这种小聪明,甚至还觉得这个人有点靠不住,自己的受信任度也会大打折扣,另外自己不会感到心虚吗? 最好的办法是什么,其实就是把该做完的工作保质保量完成,靠自己的真本事,实打实做好要求的每一件事就好了,领导看重的就是这个。

利益为先

职场上面啊,你说人情,当然也是会有的,但是更多的人关注的其实是工作本身以及自己的利益。这和学校的差距还是很大的。比如你要是个学生的话,有些人可能觉得你的身份而稍微谅解一下,而踏入社会之后,基本就不会了。 比如你要跟某个人交流和合作,就别总是寒暄以此来套近乎,不是说这个一点用都没有,多少可能还是有点用的,如果你有价值,靠这个可能能快速拉近他与你的距离。但很多情况下,这个的效果可能真的作用不大。 到了职场,大家都很忙的,比如找人谈事情,稍微介绍几句,开口直接谈正事、谈利益就好了。如果不能为对方提供有价值的信息,或者提供的价值并不是对方想要的,对方会觉得比较浪费时间。这种时候就别掺杂什么别的什么交情之类的东西了,虽说我认为成年人只看利益不够准确,但基本上是没错的。 比如跟人谈个合作的时候,讲清楚两点,第一是我们能提供什么价值和服务,第二是他能得到什么利益。当然第二点有的情况下是不需要说的,因为我们本身提供的价值就是给对方的利益,对方觉得值,自然会跟我们合作的。其中也不乏一些沟通的小技巧,还需要在这个过程中慢慢摸索。

保持交流

领导给了一个要求,如果有疑问,要多问多交流,别偏离了方向,别是自作主张。 之前的时候,我接到了一个任务,了解了基本的需求时候就开始开工干。在做的过程中其实有不少不确定的地方,但是当时我也不知道是不好意思还是什么原因,就没有跟领导交流,按照自己的想法做了出来。最后演示的时候,领导说你怎么做成这样了?我本意不是这样的,你有疑问怎么不跟我讨论呢? 我就意识到了,这其实也可以多少印证”选择比努力更重要“,方向性或方案性的东西是很重要的。 所以,遇到什么不确定的点,要多多沟通交流。当然这并不是说突然想到了某个点就去找对方讨论,把自己的疑问以及可能解决方案和结果梳理清楚再好好交流一下,效果一定好上太多。

工作日志

做好工作日志,时间长了,会发现这个非常有用,它的有用不仅仅是体现在它仅仅是做了总结,它也会潜移默化地影响我们自己。 比如每天可以找个时间简单记录一下自己今天做了什么事情,收获了什么,还需要做什么。这个记录的过程就是一个思考的过程,它会让我们反思自己的一些不足和需要做的更好的地方,会变成一种激励的。 另外工作日志,等某天你打开的时候,会发现成就感满满,同时写什么总计和汇报也就不用愁了。

个人相关

刚才所说的挺多都是和工作相关的,另外还有一些和个人相关的我觉得也应该好好注意下。

迈出第一步

我曾经尝试过很多坚持每天做点什么的事情,发现有的挺难坚持下来的,一而再再而三地累积,就变得越来越多,后来整个坚持的事情就失败了,这就是拖延症的一种现象。 拖延症应该很多人都会有,当然我也是。我平时分析拖延症的一个很大的原因就是迈不出第一步,进而一件事就搁置了。就比如说要去健身房,我觉得最难的就是出家门;比如说要每天刷一道算法题,最难的可能就是打开LeetCode界面。不知道你们什么意见,我至少是这么认为的。 因此,迈出第一步非常重要,迈出了第一步,可以说就成功了一半。

言多必失

的确是,言多必失,这话没毛病。 可能我们仅仅是刚入职的新人,很多情况下不该说的就别说。你永远也不知道你说了某些话之后,你在别人耳中会变成什么样的版本。另外如果我们突然说错了什么话,被人揪住了把柄,那可是很难的。所以,一些场景中,我们一些没必要说的就不要去说,不知道该说不该说的也不要说。 另外我个人比较反感的是明一套暗一套。我们不可能让所有人都认同我们自己,不认同我们的,他们表现出来,我们道不同不相为谋。认同我们的支持我们的,我们可以与他们成为朋友。但表面上显得非常友好,然而在背地里面却说坏话,这是非常令人反感的。

终身学习

虽然表面上看学生生涯结束了,但实际上迈入职场恰恰是一个新的开始,其实人与人之间的差距就是因为工作后的这些年逐渐拉开的,所以不论什么时候都不能放弃学习。 当然迈入职场以后,学习的一些侧重点可能就不太一样了。工作后学习的第一肯定是能用在岗位之上的专业知识,我们所掌握的知识一定至少要能够让我们顺利地专业地完成自己的任务。其次可以扩宽一下知识面,比如可以关注下经济、理财、交际等知识。 总之还是一句话,学习到的本事是别人所偷不走的,做一个终身学习者。

做好记录

很多事情是确实很有必要记录下来的。首先不瞒说,随着事情的增多,工作的忙碌,我发现比原来更加”忘事“了。后来我看了一篇报道,说其实并不是脑子记忆力下降了多少,而是集中的注意力变得更少了。 一件事如果注意力分配得少了,自然很多事情就不容易被我们记住。想想确实是这样的,现在我每天都把自己的计划安排得满满当当,现在连拿出时间好好读一本书的时间都不多了,因此就别提那些日常小事了。那咋办?随手记录下来,比如记到手机的备忘录或者自己的 TodoList 软件里面,然后再进一步安排如何执行就好。 当然记录并不仅仅局限于这个记录下来平时的闪念,平时的一些工作总结、学习笔记也可以时刻记录下来。我有记录学习笔记的习惯,当然肯定也有的时候有一些内容没有及时记录下来。过了一段时间,我发现唯一记得的就是自己曾经整理过笔记甚至发表博客的那些内容,没写过的或者没发过的基本都忘干净了。 记录,成为更好的自己。

作息调整

良好的作息还是非常有必要的。现在肯定非常多的朋友会倾向于熬夜,十二点之后才睡。我之前实习的时候,比这个更狠,经常一两点钟睡觉,睡到将近中午,就直接吃中午饭得了,然后一上午就没有了,当时就有一种半天已经被我浪费的感觉,会有一种负罪感。 后来我开始逐步调整我的作息,尽量早睡早起,从之前的九点多慢慢地调整到八点,甚至是七点多,醒来之后整理一些东西,开始全新的一天,整体的体验确实是比之前睡到中午好太多了。另外我个人也参加了一个早起打卡活动,现在已经坚持每天打卡将近一个月了,基本上也养成了早起的习惯。 另外工作之后,下班的时间也可以好好利用起来,比如晚上稍微拉伸一下,睡前记得喝水等等,由于我平时也每天对着电脑,所以也需要定期地起来活动一下。我把它们设置到滴答清单里面,每天都有软件的定时提醒,到了时间就做,一天天坚持下来。总体来说,感觉也比之前好多了。 所以从现在起,拿起笔好好规划下自己的目标和平时的作息吧。 另外可能大家听说过,某个老板每天只睡五个多小时,晚上工作到很晚才走,早上一大早就在了。我当时也比较好奇这到底是怎么做到的?难道有什么特殊的睡眠技巧?经过我的一阵搜罗,结果是,没有。BOSS 们并没有我们这么忙,可能早上开完会,我们去干活了,而人家去补觉了。所以,保证充足的睡眠还是很有必要的。

合理分配时间

从学生到工作,其实就相当于我们的事情又多了一部分,工作日大部分为工作时间,非工作日则大部分是自己的时间。另外工作之后,相比学生时代来说,事情可能也会变得更多更繁琐。那么如何合理分配自己的时间呢? 我这边采用了目前比较流行的四象限时间管理法,他就是把处理的事按“重要”和“紧急”两个维度划分,并对应到四种待处理状态中,分别叫做”重要不紧急“,”重要紧急“,”不重要紧急“,”不重要不紧急“四个类别。 通常来说,我们需要马上执行“急事”,确保它们不会延期。但长远来看,我们最好将重心放在“要事”上。可能有一些紧急的小事,它确实是不重要的,但我们能比较快的做完,然后把它勾掉,所以现在很多人可能更加优先处理的是紧急的事情而不是重要事情,最后紧急的事情做完了,剩下重要的事情没做完,所以要么加班熬夜,要么就延期。回想起来,其实这是很得不偿失的,忙活了一天,重要的事情还是没完成。 所以,我们需要尽量减少”不重要紧急“事件的忙碌,尽早处理”重要不紧急“的事情,合理分配时间,去做对我们而言重要的事情,才是四象限理论的核心。 我自己使用了滴答清单这款软件,之前使用的是 Todoist,这两款软件都非常不错,但是由于滴答清单多了番茄时钟的功能,我就改用了滴答清单,我利用它建立了四个智能清单,分别叫做”重要不紧急“,”重要紧急“,”不重要紧急“,”不重要不紧急“,然后建立筛选条件,软件就会根据任务的优先级和到期日期自动更新。然后就可以每天查看、处理并调整任务——做完的事情及时打勾,有变更的事情及时移动,可以遵循这样的一些准则:

  • 优先执行重要且紧急的事件
  • 尽量提前规划重要不紧急事件,在它们变得紧急前就完成
  • 如果可以的话,试着将不重要但紧急的事情交由他人处理,或者学会对别人的请求说 Sorry
  • 需要控制去做不重要不紧急事件的时间,不要过度放松

有了这几条原则,我们就可以很好的分配我们的时间了,如果大家觉得有用的话也可以试一下。 以上就是我的成长历程以及我所想到的一些需要调整和坚持的一些做法,如果能为大家带来一些帮助,那就再好不过了。

技术杂谈

入职微软之后,这边大多数是使用 Windows 进行开发的,比如我的台式机是 Windows 的,还有一部分服务器是 Windows 的,当然 Linux 是也非常多。 很多情况下我是使用自己的 Mac 笔记本来远程连接我的 Windows 机器来开发的。比如如果我在工位上,我会用我的 Mac 连接两块显示屏,然后一种一块用来远程桌面连接我的 Windows 开发机,这样另外一块屏幕和 Mac 自带的屏幕就用来看文档或者使用 Teams 通讯等等。如果我回家了,我家里也是有两块屏,开上 VPN,照样用一块屏使用远程桌面,另外一块屏幕和 Mac 自带屏幕就可以做其他事情了。 这样就解决了一个问题:我的 Windows 基本上都是仅用作开发的,一块屏幕就开着一个 Visual Studio,其他的操作都会在 Mac 进行,比如查文档,发消息等等。这样我下班之后照样使用远程连接的方式来操作,和在公司就是一样的。这样就避免了一些软件的来回登录,比如如果我上班只用公司机器,下班了之后换了 Mac 还得切 Teams、切微信、切浏览器等等,还是很麻烦的,而且上班期间 Mac 就闲置了也不好。所以我就采取了这样的开发方案。

需求分析

有了这个情景,就引入了一个问题。开了一个远程桌面之后,我几乎一个屏幕都是被 Visual Studio 占据的,而远程桌面貌似只能开一个屏幕?如果我要再开一个终端窗口的话,那可能屏幕就不太够用了,或者它就得覆盖我的全屏 Visual Stuido。 另外我平时 Mac 终端软件都是使用 SSH 的,基本都是用来连 Linux 的,Windows 一般都是开远程桌面。但命令行这个情形的确让我头疼,让我感到不够爽,因为毕竟远程桌面之后,Windows 里面的操作都得挤在一个桌面里面操作了。当然可能能设置多个桌面,如果可以的话,麻烦大家告知一下谢谢。 所以解决的痛点在于:我要把一些操作尽量从 Windows 里面分离出来,例如终端软件,我能否在远程桌面外面操作,能否使用 SSH 来控制我的 Windows 机器。 好,有需求才有动力,说干就干。

配置

查了一下,Windows 上其实也是有 SSH 服务器的,只不过默认是没有装的,这里只需要安装一个 OpenSSH 服务器就好了。 Win10 的话,就在设置里面可以安装,从开始菜单打开“设置”,然后选择应用和功能,这里就有一个“管理可选功能”的选项。 image-20190319093941643 点击之后便可以看到一个可选功能,选择 OpenSSH 服务器即可,一般情况下是没有安装的。如果没有安装的话它会提示一个安装按钮,这里我已经安装好了,就提示了一个卸载按钮。 image-20190319094113033 OK,有了它,直接点击安装即可完成 OpenSSH 服务器的安装。 当然如果你是想批量部署 Windows 服务器的话,当然是推荐使用 PowerShell 来自动化部署了。 首先需要用管理员身份启动 PowerShell,使用如下命令看一下,要确保 OpenSSH 可用于安装:

1
Get-WindowsCapability -Online | ? Name -like 'OpenSSH*'

输出应该是类似的结果:

1
2
3
4
Name  : OpenSSH.Client~~~~0.0.1.0
State : NotPresent
Name : OpenSSH.Server~~~~0.0.1.0
State : NotPresent

然后使用 PowerShell 安装服务器即可:

1
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0

输出结果类似:

1
2
3
Path          :
Online : True
RestartNeeded : False

这样也可以完成 OpenSSH 的安装。 安装完成之后,就需要进行一些初始化配置了,还是以管理员身份,使用 PowerShell 执行即可。 首先需要开启 SSHD 服务:

1
Start-Service sshd

然后设置服务的自动启动:

1
Set-Service -Name sshd -StartupType 'Automatic'

最后确认一下防火墙是否是放开的:

1
Get-NetFirewallRule -Name *ssh*

如果是放开的,那么结果会提示 OpenSSH-Server-In-TCP这个状态是 enabled。 好了,完成如上操作之后我们就可以使用 SSH 来连接我们的 Windows 服务器了。

连接

连接非常简单了,用户名密码就是 Windows 的用户名和密码,使用 IP 地址链接即可。 比如我的 Windows 开发机的局域网 IP 为:10.172.134.88,那么就可以使用如下命令完成链接:

1
ssh user@10.172.134.88

然后输入密码,就连接成功了,和 Linux 的是一样的。 另外我自己现在 Mac 常用的 SSH 客户端工具有 Termius,可以多终端同步使用,非常方便,这里我只需要添加我的 Windows 机器就好了,如图所示: image-20190319101812208 OK,以后就可以非常轻松地用 SSH 连接我的 Windows 服务器了,爽歪歪,上面的需求也成功解决。 以上便是使用 SSH 来连接 Windows 服务器的方法,如果大家有需求可以试试。

Python

都说程序猿是一类不解风情的生物,“赚的多,花的少,死的早”已经成为了程序猿的标志,“眼镜、格子衫、垢面蓬头、拖鞋裤衩”已然也成了程序猿的代表形象,“代码、游戏、老湿”也已经快要成了程序猿的生命。 但!有的时候,比如情人节,我们就可以发挥我们的特长了,我们程序猿也可以有自己的浪漫! 不过这个第一步是,你得有一个女朋友(哦哦,是不是可以不用往下看了? 那么有了第一步之后,下面我们应该怎么办呢? 下面介绍一个比较实用的可以送给女朋友的礼物(这其实也是我今天送给女朋友的礼物嘿嘿。 首先想想,作为程序猿,我们的专长是什么?废话,当然是代码。 有了代码,还需要送口红吗?还需要送包包吗?还需要送鲜花吗?废话,都有了代码了,这些当然就….还是要送的。万一写的代码你女朋友看不懂那岂不是死翘翘了。 好那送完了口红或包包或鲜花之后,确保已经平安无事了,我们就可以再发挥我们的光和热了(听起来咋这么奇怪呢? 进入正题,那我们可以利用代码做点什么呢?想想可以做文章的地方有什么,你们的纪念日,你们曾经做过的事情,你们在一起的时间,这些都是属于你们的独一无二的,我们可以想方设法把它们和代码联系起来。 那怎么发给女朋友看呢?做个 App、小程序、网页什么的都是可以的吧,其中网页可能是做起来最快最方便的了,然后配上一个专属域名,简直美滋滋。 好,那一想,基本方向就确定了,直接开干,接下来就描述一下我准备这个礼物的历程吧。 对于我来说,我就计划做一个网页,同时用代码的形式把和女朋友在一起的时间呈现出来,通过网页的动效来呈现我们在一起的时间,另外还计划把我们之间的故事用代码关联表示出来。 本来我打算是从零开始手撸一个的,但是一些组件比如动画特效,还有一些倒计时的组件是相对比较难做的,于是我就在 GitHub 上逛了一下,看了几个示例,找到了一个和我理想作品差不多的项目,然后在它的基础上做了一些改动,就成了最终的效果。 主要功能如下:

  • 第一是通过代码来表述出来和女朋友之间的故事。由于我和女朋友是因为 Python 认识的,而且我们两个平时都会写一些 Python,所以我决定用 Python 来写出我们之间的故事,加上 Python 的注释来辅助描述每一行代码的意义。
  • 第二是通过代码来呈现我和女朋友在一起的时间。这里就用上了一些动画特效和秒数计时方案,实时地呈现出来我和女朋友在一起已经有多久了。

最终完成之后的效果是这样子的: 预览图 然后由于我自己有一个域名,叫做 cuiqingcai.com,然后我就把它设置了二级域名解析,二级域名名称就叫做 love,域名最终就是 love.cuiqingcai.com。 最终的效果大家可以扫码或者复制链接查看一下最终的效果:http://love.cuiqingcai.com/,二维码如下: 二维码 感觉还可以吧?如果你也想送这样的礼物的话,可以根据我现有的代码来进行修改,我已经将源码放到 GitHub 了,地址为:https://github.com/Germey/ValentinesDay,大家可以修改源码,把它变成属于你和你女朋友的专属页面,然后送给女朋友。 下面说一些关键的技术和需要修改的点。

代码动画

打开页面之后,我们可以看到页面的代码是一个字一个字敲出来的,这实际上是利用了一个定时器来实现的。 首先我们可以预定义好所有的文本,然后动画播放的时候,首先把所有的文本隐藏,然后每隔几十毫秒读取一个字符,然后将其呈现出来。由于文本本身就是换行的,所以在呈现的时候就会一行一行地像打字机一样呈现出来。 另外为了模拟打字的效果,在呈现的时候可以在最后的字符后面添加一个下划线符号,模拟打字的效果。 其关键的实现代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
(function (a) {
a.fn.typewriter = function () {
this.each(function () {
var d = a(this), c = d.html(), b = 0;
d.html("");
var e = setInterval(function () {
var f = c.substr(b, 1);
if (f == "<") {
b = c.indexOf(">", b) + 1
} else {
b++
}
d.html(c.substring(0, b) + (b & 1 ? "_" : ""));
if (b >= c.length) {
clearInterval(e)
}
}, 75)
});
return this
}
})(jQuery);

这里可以看到,首先获取了页面代码区域的内容,然后通过 DOM 操作将代码先清空,然后利用 setInterval 方法设置一个定时器,定时间隔 75 毫秒,也就是说 75 毫秒循环调用一次。每调用一次,就会从原来的字符上多取一个字符,然后尾部拼接一个下划线就好了。

代码内容

接下来就是代码内容了,这里面要想好怎样把一些关键时间来表示出来。比如和女朋友怎样认识的,后来什么时间在一起的,一起做过什么事情,将来有什么计划和打算,都可以来描述出来,另外编程语言可以选择你喜欢的语言,然后配以一定的注释来描述代码所代表的含义。 我和女朋友是在 PyCon 认识的,也算是因为 Python 结缘,然后平时我们都会写一些 Python,所以我就选用 Python 作为编程语言了。 然后我又加上了我们认识的时间、在一起的时间、一起做过的事情,然后再配以一段代码来表达自己的想法,其中的一些灵感也是我看了一些案例想出来的,在表述过程中我使用了面向对象的思维声明了两个对象,一个代表我,一个代表我女朋友,然后一起做过的事情就可以通过对象调用方法的形式来表述出来了,另外一些动作和标志可以通过自定义方法或者代码的参数来表示出来,其中每一行代码的动作我都配以一条 Python 的注释来完成,注释当然是用英文,一些话我还用了翻译软件一句句查的。 然后最后我用了一段 Python 代码来表达了自己的感情,内容如下:

1
2
3
4
5
6
# You are the greatest love of my life.
while True:
if u.with(i):
you = everything
else:
everything = u

这个代码的含义叫做“无论天涯海角,你都是我的一切。“,一个 while True 循环代表了永久。 这些代码其实都是在 HTML 代码中预定义好的,其中注释需要用 span 标签配以 comments 的 class 来修饰,缩进需要用 span 标签配以 placeholder 的 class 来修饰,例如:

1
2
3
4
5
6
<span class="comments"># You are the greatest love of my life.</span><br/>
while <span class="keyword">True</span>:<br/>
<span class="placeholder"></span><span class="keyword">if</span> u.with(i):<br/>
<span class="placeholder"></span><span class="placeholder"></span>you = everything<br/>
<span class="placeholder"></span><span class="keyword">else</span>:<br/>
<span class="placeholder"></span><span class="placeholder"></span>everything = u<br/>

这里不同的格式用 span 的不同 class 来标识,空格缩进一个 placeholder 是两个空格,comments 代表注释格式,关键词使用 keyword 来标识。如果你需要自定义自己的内容,通过控制这些内容穿插写入就好了。

纪念日计时

关于纪念日,这个实现起来其实挺简单的,就是首先定义好你们的纪念日,然后获取当前系统时间,然后计算秒数差值,然后将其转化为天数、小时数即可,关键核心代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function timeElapse(c) {
var e = Date();
var f = (Date.parse(e) - Date.parse(c)) / 1000;
var g = Math.floor(f / (3600 * 24));
f = f % (3600 * 24);
var b = Math.floor(f / 3600);
if (b < 10) {
b = "0" + b
}
f = f % 3600;
var d = Math.floor(f / 60);
if (d < 10) {
d = "0" + d
}
f = f % 60;
if (f < 10) {
f = "0" + f
}
}

另外它也是通过一个定时器来实现的时间刷新,每隔 500 毫秒调用一次:

1
2
3
setInterval(function () {
timeElapse(together);
}, 500);

动画心形

动画心形,其实这个实现起来是很巧妙的。这里在画的时候实际上是利用了贝塞尔曲线来绘制一个心形,同时在在画的过程中还加了花开的效果,花开的效果使用了随机数和随机颜色生成。 其中动画画心形的核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Petal.prototype = {
draw: function () {
var a = this.bloom.garden.ctx;
var e, d, c, b;
e = new Vector(0, this.r).rotate(Garden.degrad(this.startAngle));
d = e.clone().rotate(Garden.degrad(this.angle));
c = e.clone().mult(this.stretchA);
b = d.clone().mult(this.stretchB);
a.strokeStyle = this.bloom.c;
a.beginPath();
a.moveTo(e.x, e.y);
a.bezierCurveTo(c.x, c.y, b.x, b.y, d.x, d.y);
a.stroke()
}, render: function () {
if (this.r <= this.bloom.r) {
this.r += this.growFactor;
this.draw()
} else {
this.isfinished = true
}
}
};

这里最关键的一个部分就是 bezierCurveTo,这里传入的是绘制贝塞尔曲线的参数坐标,那这些坐标怎么生成的呢,这里是利用了数学上的一个桃心线方程,如图所示: 贝塞尔曲线 其中心形线的解析方程为: 这个公式代表了绘制坐标点的 x、y 的解析方程,用代码表示出来就是:

1
2
3
4
5
6
function getHeartPoint(c) {
var b = c / Math.PI;
var a = 19.5 * (16 * Math.pow(Math.sin(b), 3));
var d = -20 * (13 * Math.cos(b) - 5 * Math.cos(2 * b) - 2 * Math.cos(3 * b) - Math.cos(4 * b));
return new Array(offsetX + a, offsetY + d)
}

这里是生产了心形线方程的 x、y 坐标,然后再以此绘制出带有动画效果的心形。 最终呈现的效果就是现在你看到的样子。 不过这些在改代码的时候实际上不用关心,只需要修改你们在一起的时间就好了,就是代码中的这一行:

1
2
together.setFullYear(2018, 10, 5);
together.setHours(15);

这里修改你们在一起的时间和小时就可以了,然后页面就会自动更新你们在一起多久了,并动态呈现出来了。

域名解析

对于域名解析,这个建议大家可以申请一个域名,比如我的域名就是 cuiqingcai.com,我可以设置一个二级域名解析,叫做 love.cuiqingcai.com。 如果没有域名的话可以现买一个,比如阿里云、腾讯云购买,然后设置解析即可。 如果没有域名,也可以使用一些虚拟云服务器,他们会帮你设置二级域名,当然也可以使用 GitHub Pages,甚至你使用 IP 地址来访问也是没问题的。

项目代码

项目的代码我都放在了:https://github.com/Germey/ValentinesDay,大家可以自行修改成想要的样子送给女朋友,只能帮你到这里啦。 嘿嘿,这就是我今天送给女朋友的礼物,女朋友收到了开心得不得了,开心。

我的礼物

其实我今天也收到了女朋友送的特殊的礼物,可以说她确实花了不少心思啊,她送了我什么呢?令我没想到的是,她居然刚申请了一个微店,然后她微店上架了好多商品,我看到时候惊呆了,店铺如图所示: image-20190214182451019 里面上架了什么商品?洗水果服务?做饭刷碗服务?捏肩膀服务?还有自动哄老婆机?我惊了。 她把商品发给我,我好奇问她这是干嘛的。 她说:要获得我的洗水果服务,捏肩膀服务,只需要在我的小店里购买使用就好了(作掐腰状)!还有自动哄老婆机,你要惹我生气了,只需要购买一个自动哄老婆机,我就会不生气了!嘿嘿合不合算? 我说:多少钱?999!这么贵的吗! 她说:当然不是啦,亲亲我们店里有活动的,使用优惠券满 999 减 998 呢,您是我的 VIP 唯一专属客户,我会给您发优惠券的呀,使用优惠券只需要一块钱就可以购买了。购买之后,您每次使用一张,我就可以给您洗水果、捏肩膀了!这个情人节的话呢,我要送亲亲 10 张!可省着点话,不能累到店长我啊! 哦哦,卧槽真牛逼啊!于是乎我就快快乐乐领取到了十张优惠券购买了女朋友的这些服务,等着时不时用一张,享受一下帝王级的待遇,美滋滋!哈哈~ 最后,祝大家情人节快乐!幸福!

技术杂谈

作为一名程序员,能够利用好工具提高开发和工作效率是非常重要的。我个人使用的都是苹果系列产品,电脑为 MacBook Pro 15 寸,手机 iPhone 7P,另外还有一个 iPad Pro 和一副 Apple Watch。我一直觉得 Mac 是非常适合做程序开发的,它既有比较不错的页面,也有类 Unix 的操作系统,使得日常使用和程序开发都极其便利,另外由于苹果本身自有的 iCloud 机制,使用 Mac、iPhone、iPad 跨平台开发和工作也变得十分便利。

近期我又对自己的一些工具进行了整理,弃用了一些工具,新启用了一些工具。目的也只有一个,就是提高自己的工作和开发效率,让生活变得更美好。如果你也在用 Mac 开发,或者你也有使用 iPad、iPhone,下面我所总结的个人的一些工具或许能给你带来帮助。

快速导航

这是 Mac 上的一个工具,要说到提高效率,首推 Alfred,可以说是 Mac 必备软件,利用它我们可以快速地进行各种操作,大幅提高工作效率,如快速打开某个软件、快速打开某个链接、快速搜索某个文档,快速定位某个文件,快速查看本机 IP,快速定义某个色值,几乎我们能想到的都能对接实现。

其实 Mac 本身已经自带了软件搜索还有 Spotlight,但是其功能还是远远比不上 Alfred,有了它,所有的快捷操作几乎都能实现。

这些快速功能是怎么实现的呢?实际上是 Alfred 对接了很多 Workflow,我们可以使用 Workflow 方便地进行功能扩展,一些比较优秀的 Workflow 已经有人专门做过整理了,可以参见:https://github.com/zenorocha/alfred-workflows,大家可以安装自己所需要的 Workflow,大大提高效率。

复制粘贴

Mac 上默认只有一个粘贴板,当我们新复制了一段文字之后,如果我们想再找寻之前复制的历史记录就找不到了,这其实是很反人类的。

好在 Paste 这款软件帮我们解决了这个问题,它可以保存我们粘贴板的历史记录,等需要粘贴某个内容的时候只需要呼出 Paste 历史粘贴板,然后选择某个特定的内容粘贴就好了,另外它还支持文本格式调整粘贴板分类和搜索,还可以支持快速便捷粘贴。有了它,再也不用担心粘贴板丢失了!

另外使用 Mac 和 iPhone、iPad 之间也可以相互之间复制粘贴,可以在一台 Apple 设备上拷贝文本、图像、照片和视频,然后在另一台 Apple 设备上粘贴该内容。例如,可以拷贝在 Mac 上浏览网页时发现的食谱,然后将其粘贴到附近 iPhone 上“备忘录”中的购物清单。这是在 macOS Sierra 版本之后出来的功能,若要使用需要确保 Mac 的版本是 Sierra 及以后。若要使用,几个设备必须满足“连续互通”系统要求。它们还必须在“系统偏好设置”(在 Mac 上)和“设置”(在 iOS 设备上)中打开 Wi-Fi、蓝牙和 Handoff,另外必须在所有设备上使用同一 Apple ID登录 iCloud。

具体的操作流程可以参见苹果的官网说明:https://support.apple.com/kb/PH25168,有了这个功能,日常的一些操作便可以直接同步了,甚至不再需要 AirDrop,更不需要微信和邮件。

时间管理

现在这个时候,时间比什么都重要,每个人的时间都是公平的,如果我们能够合理规划好自己的时间和工作,这就跨出了成功的一步。

我曾经尝试用手写的方式来记录自己的一些任务,但总感觉它有一些并不方便的地方。比如某时某刻突然想起来,想要添加一件事情或者完成了一件事情,或者想要修改截止时间,或者想要划分优先级,其实都不怎么方便。最好的方式还是通过一些专用的时间管理软件来分配分配和管理自己的时间。

我曾经使用过非常多款时间管理工具,最终我选择的是 Todoist,这个是我感觉体验非常不错的一款。这个软件里面基本的任务添加与勾划功能当然必不可少,它也支持优先级管理,分类管理,时间设置,另外还有几个我觉得非常加分的几个点,比如:

  • 添加时间时可以直接通过一句话来添加,比如”每隔两天晚上九点运动”,它会自动识别并设置为循环任务,并能在相应的时刻提醒你。

  • 支持全部平台,不论是网页还是 Windows、Mac、Android、iPhone、iPad、Apple Watch、Chrome、Firefox,你能想到的平台,应有尽有。

  • 它还支持事件同步,可以在 Mac 或者 iPhone 的日历中添加 Todoist 的同步,这样你所有的事情都会被定时同步到日历软件中,这样日历中就既包含了节日、生日等信息,又可以把每天我们需要做的事同步进来,日程信息一目了然。这样你就可以把日历变成一个提醒器,设置什么时候提醒就好了,现在我就在用 Mac 或者 iPhone 上的日历来提醒我什么时间该做什么事情了。

  • 它还支持多人写作,就类似于 Worktile、Teambition、Trello,我曾经使用这款软件完成了多个项目的任务分配和多人协作开发,还是非常方便的。

  • 另外它还支持过期智能重新安排任务,比如有一些任务没有完成,它还可以根据优先级来重新进行时间规划和安排,同时也有任务评分和目标评价机制,来反映我们任务完成情况。

另外关于时间管理还有一个非常重要的四象限法则,大家也可以了解一下。有了这个法则,大家可以合理安排优先级,合理分配每个任务的时间。有关于我的时间管理经验我后面还会详细写一篇相关的文章,介绍一下我平时会怎样进行时间规划和学习的。

另外我还尝试过番茄土豆这个软件,这个软件的缺点在于整体的功能还比较简陋,而且不能和我已有的 Todo List 进行同步。好处就是可以自己设置番茄,保持专注工作。但目前我尚未发现满意的产品,如有还希望大家可以留言推荐一下,谢谢。

笔记记录

在学习的时候来进行记录是非常非常重要的,强烈建议一边学习一边把所做所想记录下来,最后做一下整理成文。一方面方便查阅,另一方面加深印象和理解。

Markdown 想必大家都已经很耳熟了,现在我写文章或者笔记几乎全都用 Markdown 来写,现在很多云笔记也慢慢逐步支持 Markdown 的语法了,我的博客后台也自己配置了 Markdown 的支持。不过也有某些平台尚未支持 Markdown,比如知乎,忍不住吐槽一句,知乎的编辑器实在用得是心累,当然可以使用插件来解决,也当然也有所好,我就不再说什么了。不过我还是强烈推荐 Markdown 来进行写作和记录的,用过之后你可能就不再想用 Word 了。

言归正传,既然谈到笔记和写作。我的笔记本是 Mac,之前几乎所有的笔记,包括写书,都几乎是在 Mac 上完成的,但是确实有的时候是不方便的。比如 Mac 不在身边或者想用 iPhone 或者 iPad 来写点东西的时候,一个需要解决的问题就是云同步问题。有了云同步,我们如果在电脑上写了一部分内容,接着切换了另一台台式机,或者切换了手机的时候,照样能够接着在原来的基础上写,非常方便。

这时候可能就有小伙伴推荐有道云笔记、印象笔记等软件,它们支持 Markdown,但这并不是它们的主打支持方向,对 Makrdown 的支持当然没有一些专业的 Makrdown 编辑工具专业。对我个人而言,我不想因为它们自带了云同步而抛弃了纯粹的 Makrdown 写作环境,我只想要一个纯粹的 Makrdown 写作环境,而不想引入比如有道云里面的普通笔记、签到等冗余的功能,也不想看到里面的广告推荐等内容。所以对于云同步,我使用了另外的解决方案。对于写作软件,我也摸索出了自己的一套方案。

对于电脑端的 Markdown 写作软件,推荐两款,一款是 Typora,另一款是 MarkEditor。

对于 Makrdown 编辑器来说,我觉得有几个比较重要的点:

  • 不能纯写 Makrdown,要实时地能够看到自己写完 Makrdown 之后最终呈现的效果是怎样的。

  • 插入图片要方便,很多编辑器需要先将图片挪到某个文件夹下或者上传图片才可以插入图片,这是很不友好的,如果能够直接通过复制粘贴的形式插入图片,甚至能够自动将图片上传到云端,那就再好不过了。

  • 能够打开一整个 Makrdown 文件夹,左侧显示文件列表,右侧进行写作编辑,不能仅仅支持一个 Markdown 文件的编辑。

  • 如果需要用到公司,那么编辑器需要对 Markdown 公式支持比较好。

以上介绍的这两款软件都可以做到。

  • Typora 是免费的,更加轻量级,而且支持即写即得,界面支持和公式支持都比较好,图片的话可以结合 iPic 软件直接上传到图床,同时也可以直接将复制的图片直接粘贴到编辑器中,非常友好,目前我正在使用。

  • MarkEditor 是收费的,功能更为丰富,支持左右分栏模式、阅读模式等,它也支持直接复制和粘贴图片,另外还有强大的导出功能,还可以直接将文件发布为一个网站等等,也十分推荐。

不过目前由于 Typora 更轻量级,并且能和 iPic 而且功能配合使用,粘贴后的图片可以点击直接上传到云端,非常方便,我目前已经由 MarkEditor 切换到 Typora 了。

写作界面如下:

如图这是打开了一个文件夹,这个文件夹里面有好多 Makrdown 文件,都是我在研究和学习过程中所写的笔记。

然后需要解决的就是云同步问题,云同步其实使用网盘就足够了。由于我使用 Mac,所以我选用了 iCloud,开了 200G 的空间,足够了。这样我所记录的内容能够秒级同步到 iCloud Drive 中,这样我再使用 iPhone、iPad 就可以直接看到最新的内容了。当然还有一些推荐的,比如 OneDrive、谷歌云等多种云盘同步工具,哪个方便用哪个。Mac 和 iPhone 的好处就是已经内置了 iCloud Drive,所以不用再去在各个终端上配置了。

接下来就是在其他的电脑以及 iPhone、iPad 上进行写作的解决方案了。由于我的文件都已经存放在了 iCloud Drive,所以就需要一款 Makrdown 编辑软件可以直接读写 iCloud Drive 里面的内容,同时界面还要友好,功能完善一点。在这里我最终选择了 Markdown Pro,它的功能简洁但是又比较完善,打开之后直接选取 iCloud Drive 里面的 Makrdown 文件即可开始编辑,并且它是左右分栏的,即左侧编辑,右侧预览,非常方便简洁,另外它对公式的支持也很好,下图是我在 iPad 上对本文进行编辑的效果预览图。

对于图片的插入,在 iPhone 和 iPad 上我借助了另外一个工具,叫做 SM.MS,这个软件可以直接选取图片,然后上传到云端,点击复制即可得到链接和 Makrdown 图片链接,所以插图也不是什么问题了。

如图所示,上传照片之后,便会出现各种各样的图片链接形式,有纯链接、HTML、Markdown 等等,直接点击复制按钮即可复制,然后粘贴到文档中。

另外如果你用了 Windows 的话,只要下载一个 iCloud 云盘软件即可同步。如果使用的是其他的云盘软件,也只需要配置一下就好了。

有了这套,我们就可以实现随时随地写笔记,Mac、iPhone、iPad 无缝切换。

思维导图

很多时候我们在构思方案或者流程的时候需要对思维做梳理,或者在列方案呈现的时候也需要分门别类地进行呈现。这时候大多数情况下就需要用到一个工具,思维导图。

思维导图工具我个人使用的是 MindNode,在 Mac 上用它可以通过各种快捷键快速的增删思维导图节点,另外界面也非常绚丽多彩。

对于思维导图软件来说,我也希望能全平台同步,其实 MindNode 也有对应的移动端软件,同样是 MindNode,二者可以通过 iCloud Drive 进行同步,同样可以做到无缝衔接。

另外还有很多朋友也在用 XMind,功能同样很强大,大家也可以试试。

远程控制

我们经常会和各种服务器打交道,例如我们经常使用 SSH 来远程连接某台 Linux 服务器,原生 Terminal 是支持 SSH 的,但你会发现原生带的这个太难用了。可能很多小伙伴使用 iTerm,不得不说这确实是个神器,大大方便了远程管理流程。但我在这里还要推荐一个我经常使用的 SSH Shell,没错,它的名字就是 SSH Shell,它的页面操作简洁,同时管理和记录远程主机十分方便,另外还支持秘钥管理、自动重连、自定义主题等等功能,个人用起来十分顺手,强烈推荐!

当然除了电脑,当我们出去在外的时候,紧急情况也可能需要使用 SSH 来连接和管理我们的服务器,所以我也在 iPhone 和 iPad 上装了远程管理软件,叫做 Termius,同样功能十分强大,快捷操作十分便捷,有免费的试用期限,我觉得非常好用就订购了,推荐给大家。

代码记录

作为一名程序员,我们会经常写或者使用一些关键代码。

比如有一天我写了一些方法,这些方法可以完成非常重要的功能,后面的项目也会经常遇到,那么怎么办呢?很多情况下我们想把它保存起来,放到某个收藏夹里面备用,等到用的时候重新把它复制出来。或者有一些繁琐的命令,我实在是记不住,或许我们也想把它记录下来。

很多情况下,我们可能简单地使用文本文件,但并不方便同步和查找。或者云笔记保存下来,但这些并不是专门用来保存代码的。更高级一点,我们会联想到使用 GitHub Gists,但每次记录的这个流程也比较麻烦。

这里推荐一个专门用来记录代码片段的软件,叫做 SnippetsLab,适用于 Mac 系统,可以专门用来管理代码片段,还支持多种代码格式。比如我就将代码按照编程语言划分,划分为 Python、JavaScript 等等,分文件夹存储,有不错的代码就随手贴过来,另外它也支持搜索,管理代码非常方便。如果某一天想查某个代码了,直接打开它一搜就有了,可以大大提高开发效率。

以上就暂且总结这么多,其实还有不少好用的用具,后面再一一为大家总结分享。

另外再问下大家,你们买 iPad 了吗?是不是觉得比较鸡肋,或者平时都用不上,那这样就没有发挥 iPad 的最大效用,如果利用好了,它可以进一步方便我们的生活,后面我也会专门写一下 iPad 方面的一些用途。

由于水平和见识有限,如果大家有更好的软件或者方案推荐,欢迎大家留言!也希望我的一些方案对大家有所帮助,谢谢!

Python

爬虫是做什么的?是帮助我们来快速获取有效信息的。然而做过爬虫的人都知道,解析是个麻烦事。 比如一篇新闻吧,链接是这个:https://news.ifeng.com/c/7kQcQG2peWU,页面预览图如下: 预览图 我们需要从页面中提取出标题、发布人、发布时间、发布内容、图片等内容。一般情况下我们需要怎么办?写规则。 那么规则都有什么呢?怼正则,怼 CSS 选择器,怼 XPath。我们需要对标题、发布时间、来源等内容做规则匹配,更有甚者再需要正则表达式来辅助一下。我们可能就需要用 re、BeautifulSoup、pyquery 等库来实现内容的提取和解析。 但如果我们有成千上万个不同样式的页面怎么办呢?它们来自成千上万个站点,难道我们还需要对他们一一写规则来匹配吗?这得要多大的工作量啊。另外这些万一弄不好还会解析有问题。比如正则表达式在某些情况下匹配不了了,CSS、XPath 选择器选错位了也会出现问题。 想必大家可能见过现在的浏览器有阅读模式,比如我们把这个页面用 Safari 浏览器打开,然后开启阅读模式,看看什么效果: Safari预览 页面一下子变得非常清爽,只保留了标题和需要读的内容。原先页面多余的导航栏、侧栏、评论等等的统统都被去除了。它怎么做到的?难道是有人在里面写好规则了?那当然不可能的事。其实,这里面就用到了智能化解析了。 那么本篇文章,我们就来了解一下页面的智能化解析的相关知识。

智能化解析

所谓爬虫的智能化解析,顾名思义就是不再需要我们针对某一些页面来专门写提取规则了,我们可以利用一些算法来计算出来页面特定元素的位置和提取路径。比如一个页面中的一篇文章,我们可以通过算法计算出来,它的标题应该是什么,正文应该是哪部分区域,发布时间是什么等等。 其实智能化解析是非常难的一项任务,比如说你给人看一个网页的一篇文章,人可以迅速找到这篇文章的标题是什么,发布时间是什么,正文是哪一块,或者哪一块是广告位,哪一块是导航栏。但给机器来识别的话,它面临的是什么?仅仅是一系列的 HTML 代码而已。那究竟机器是怎么做到智能化提取的呢?其实这里面融合了多方面的信息。

  • 比如标题。一般它的字号是比较大的,而且长度不长,位置一般都在页面上方,而且大部分情况下它应该和 title 标签里的内容是一致的。
  • 比如正文。它的内容一般是最多的,而且会包含多个段落 p 或者图片 img 标签,另外它的宽度一般可能会占用到页面的三分之二区域,并且密度(字数除以标签数量)会比较大。
  • 比如时间。不同语言的页面可能不同,但时间的格式是有限的,如 2019-02-20 或者 2019/02/20 等等,也有的可能是美式的记法,顺序不同,这些也有特定的模式可以识别。
  • 比如广告。它的标签一般可能会带有 ads 这样的字样,另外大多数可能会处于文章底部、页面侧栏,并可能包含一些特定的外链内容。

另外还有一些特点就不再一一赘述了,这其中包含了区块位置、区块大小、区块标签、区块内容、区块疏密度等等多种特征,另外很多情况下还需要借助于视觉的特征,所以说这里面其实结合了算法计算、视觉处理、自然语言处理等各个方面的内容。如果能把这些特征综合运用起来,再经过大量的数据训练,是可以得到一个非常不错的效果的。

业界进展

未来的话,页面也会越来越多,页面的渲染方式也会发生很大的变化,爬虫也会越来越难做,智能化爬虫也将会变得越来越重要。 目前工业界,其实已经有落地的算法应用了。经过我的一番调研,目前发现有这么几种算法或者服务对页面的智能化解析做的比较好:

那么这几种算法或者服务到底哪些好呢,Driffbot 官方曾做过一个对比评测,使用 Google 新闻的一些文章,使用不同的算法依次摘出其中的标题和文本,然后与真实标注的内容进行比较,比较的指标就是文字的准确率和召回率,以及根据二者计算出的 F1 分数。 其结果对比如下:

Service/Software

Precision

Recall

F1-Score

Diffbot

0.968

0.978

0.971

Boilerpipe

0.893

0.924

0.893

Readability

0.819

0.911

0.854

AlchemyAPI

0.876

0.892

0.850

Embedly

0.786

0.880

0.822

Goose

0.498

0.815

0.608

经过对比我们可以发现,Diffbot 的准确率和召回率都独占鳌头,其中的 F1 值达到了 0.97,可以说准确率非常高了。另外接下来比较厉害的就是 Boilerpipe 和 Readability,Goose 的表现则非常差,F1 跟其他的算法差了一大截。下面是几个算法的 F1 分数对比情况: F1分数对比 有人可能好奇为什么 Diffbot 这么厉害?我也查询了一番。Diffbot 自 2010 年以来就致力于提取 Web 页面数据,并提供许多 API 来自动解析各种页面。其中他们的算法依赖于自然语言技术、机器学习、计算机视觉、标记检查等多种算法,并且所有的页面都会考虑到当前页面的样式以及可视化布局,另外还会分析其中包含的图像内容、CSS 甚至 Ajax 请求。另外在计算一个区块的置信度时还考虑到了和其他区块的关联关系,基于周围的标记来计算每个区块的置信度。 总之,Diffbot 也是一直致力于这一方面的服务,整个 Diffbot 就是页面解析起家的,现在也一直专注于页面解析服务,准确率高也就不足为怪了。 但它们的算法开源了吗?很遗憾,并没有,而且我也没有找到相关的论文介绍它们自己的具体算法。 所以,如果想实现这么好的效果,那就使用它们家的服务就好了。 接下来的内容,我们就来说说如何使用 Diffbot 来进行页面的智能解析。另外还有 Readability 算法也非常值得研究,我会写专门的文章来介绍 Readability 及其与 Python 的对接使用。

Diffbot 页面解析

首先我们需要注册一个账号,它有 15 天的免费试用,注册之后会获得一个 Developer Token,这就是使用 Diffbot 接口服务的凭证。 接下来切换到它的测试页面中,链接为:https://www.diffbot.com/dev/home/,我们来测试一下它的解析效果到底是怎样的。 这里我们选择的测试页面就是上文所述的页面,链接为:https://news.ifeng.com/c/7kQcQG2peWU,API 类型选择 Article API,然后点击 Test Drive 按钮,接下来它就会出现当前页面的解析结果: 结果 这时候我们可以看到,它帮我们提取出来了标题、发布时间、发布机构、发布机构链接、正文内容等等各种结果。而且目前来看都十分正确,时间也自动识别之后做了转码,是一个标准的时间格式。 接下来我们继续下滑,查看还有什么其他的字段,这里我们还可以看到有 html 字段,它和 text 不同的是,它包含了文章内容的真实 HTML 代码,因此图片也会包含在里面,如图所示: 结果 另外最后面还有 images 字段,他以列表形式返回了文章套图及每一张图的链接,另外还有文章的站点名称、页面所用语言等等结果,如图所示: 结果 当然我们也可以选择 JSON 格式的返回结果,其内容会更加丰富,例如图片还返回了其宽度、高度、图片描述等等内容,另外还有各种其他的结果如面包屑导航等等结果,如图所示: 结果 经过手工核对,发现其返回的结果都是完全正确的,准确率相当之高! 所以说,如果你对准确率要求没有那么非常非常严苛的情况下,使用 Diffbot 的服务可以帮助我们快速地提取页面中所需的结果,省去了我们绝大多数的手工劳动,可以说是非常赞了。 但是,我们也不能总在网页上这么试吧。其实 Diffbot 也提供了官方的 API 文档,让我们来一探究竟。

Diffbot API

Driffbot 提供了多种 API,如 Analyze API、Article API、Disscussion API 等。 下面我们以 Article API 为例来说明一下它的用法,其官方文档地址为:https://www.diffbot.com/dev/docs/article/,API 调用地址为:

1
https://api.diffbot.com/v3/article

我们可以用 GET 方式来进行请求,其中的 Token 和 URL 都可以以参数形式传递给这个 API,其必备的参数有:

  • token:即 Developer Token
  • url:即要解析的 URL 链接

另外它还有几个可选参数:

  • fields:用来指定返回哪些字段,默认已经有了一些固定字段,这个参数可以指定还可以额外返回哪些可选字段
  • paging:如果是多页文章的话,如果将这个参数设置为 false 则可以禁止多页内容拼接
  • maxTags:可以设置返回的 Tag 最大数量,默认是 10 个
  • tagConfidence:设置置信度的阈值,超过这个值的 Tag 才会被返回,默认是 0.5
  • discussion:如果将这个参数设置为 false,那么就不会解析评论内容
  • timeout:在解析的时候等待的最长时间,默认是 30 秒
  • callback:为 JSONP 类型的请求而设计的回调

这里大家可能关注的就是 fields 字段了,在这里我专门做了一下梳理,首先是一些固定字段:

  • type:文本的类型,这里就是 article 了
  • title:文章的标题
  • text:文章的纯文本内容,如果是分段内容,那么其中会以换行符来分隔
  • html:提取结果的 HTML 内容
  • date:文章的发布时间,其格式为 RFC 1123
  • estimatedDate:如果日期时间不太明确,会返回一个预估的时间,如果文章超过两天或者没有发布日期,那么这个字段就不会返回
  • author:作者
  • authorUrl:作者的链接
  • discussion:评论内容,和 Disscussion API 返回结果一样
  • humanLanguage:语言类型,如英文还是中文等
  • numPages:如果文章是多页的,这个参数会控制最大的翻页拼接数目
  • nextPages:如果文章是多页的,这个参数可以指定文章后续链接
  • siteName:站点名称
  • publisherRegion:文章发布地区
  • publisherCountry:文章发布国家
  • pageUrl:文章链接
  • resolvedPageUrl:如果文章是从 pageUrl 重定向过来的,则返回此内容
  • tags:文章的标签或者文章包含的实体,根据自然语言处理技术和 DBpedia 计算生成,是一个列表,里面又包含了子字段:
    • label:标签名
    • count:标签出现的次数
    • score:标签置信度
    • rdfTypes:如果实体可以由多个资源表示,那么则返回相关的 URL
    • type:类型
    • uri:Diffbot Knowledge Graph 中的实体链接
  • images:文章中包含的图片
  • videos:文章中包含的视频
  • breadcrumb:面包屑导航信息
  • diffbotUri:Diffbot 内部的 URL 链接

以上的预定字段就是如果可以返回那就会返回的字段,是不能定制化配置的,另外我们还可以通过 fields 参数来指定扩展如下可选字段:

  • quotes:引用信息
  • sentiment:文章的情感值,-1 到 1 之间
  • links:所有超链接的顶级链接
  • querystring:请求的参数列表

好,以上便是这个 API 的用法,大家可以申请之后使用这个 API 来做智能化解析了。 下面我们用一个实例来看一下这个 API 的用法,代码如下:

1
2
3
4
5
6
7
8
9
10
import requests, json

url = 'https://api.diffbot.com/v3/article'
params = {
'token': '77b41f6fbb24496d5113d528306528fa',
'url': 'https://news.ifeng.com/c/7kQcQG2peWU',
'fields': 'meta'
}
response = requests.get(url, params=params)
print(json.dumps(response.json(), indent=2, ensure_ascii=False))

这里首先定义了 API 的链接,然后指定了 params 参数,即 GET 请求参数。 参数中包含了必选的 token、url 字段,也设置了可选的 fields 字段,其中 fields 为可选的扩展字段 meta 标签。 我们来看下运行结果,结果如下:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
{
"request": {
"pageUrl": "https://news.ifeng.com/c/7kQcQG2peWU",
"api": "article",
"fields": "sentiment, meta",
"version": 3
},
"objects": [
{
"date": "Wed, 20 Feb 2019 02:26:00 GMT",
"images": [
{
"naturalHeight": 460,
"width": 640,
"diffbotUri": "image|3|-1139316034",
"url": "http://e0.ifengimg.com/02/2019/0219/1731DC8A29EB2219C7F2773CF9CF319B3503D0A1_size382_w690_h460.png",
"naturalWidth": 690,
"primary": true,
"height": 426
},
// ...
],
"author": "中国新闻网",
"estimatedDate": "Wed, 20 Feb 2019 06:47:52 GMT",
"diffbotUri": "article|3|1591137208",
"siteName": "ifeng.com",
"type": "article",
"title": "故宫,你低调点!故宫:不,实力已不允许我继续低调",
"breadcrumb": [
{
"link": "https://news.ifeng.com/",
"name": "资讯"
},
{
"link": "https://news.ifeng.com/shanklist/3-35197-/",
"name": "大陆"
}
],
"humanLanguage": "zh",
"meta": {
"og": {
"og:time ": "2019-02-20 02:26:00",
"og:image": "https://e0.ifengimg.com/02/2019/0219/1731DC8A29EB2219C7F2773CF9CF319B3503D0A1_size382_w690_h460.png",
"og:category ": "凤凰资讯",
"og: webtype": "news",
"og:title": "故宫,你低调点!故宫:不,实力已不允许我继续低调",
"og:url": "https://news.ifeng.com/c/7kQcQG2peWU",
"og:description": "  “我的名字叫紫禁城,快要600岁了,这上元的夜啊,总是让我沉醉,这么久了却从未停止。”   “重"
},
"referrer": "always",
"description": "  “我的名字叫紫禁城,快要600岁了,这上元的夜啊,总是让我沉醉,这么久了却从未停止。”   “重",
"keywords": "故宫 紫禁城 故宫博物院 灯光 元宵节 博物馆 一票难求 元之 中新社 午门 杜洋 藏品 文化 皇帝 清明上河图 元宵 千里江山图卷 中英北京条约 中法北京条约 天津条约",
"title": "故宫,你低调点!故宫:不,实力已不允许我继续低调_凤凰资讯"
},
"authorUrl": "https://feng.ifeng.com/author/308904",
"pageUrl": "https://news.ifeng.com/c/7kQcQG2peWU",
"html": "<p>“我的名字叫紫禁城,快要600岁了,这上元的夜啊,总是让我沉醉,这么久了却从未停止。...</blockquote> </blockquote>",
"text": "“我的名字叫紫禁城,快要600岁了,这上元的夜啊,总是让我沉醉,这么久了却从未停止。”\\n“...",
"authors": [
{
"name": "中国新闻网",
"link": "https://feng.ifeng.com/author/308904"
}
]
}
]
}

可见其返回了如上的内容,是一个完整的 JSON 格式,其中包含了标题、正文、发布时间等等各种内容。 可见,不需要我们配置任何提取规则,我们就可以完成页面的分析和抓取,得来全不费功夫。

Diffbot SDK

另外 Diffbot 还提供了几乎所有语言的 SDK 支持,我们也可以使用 SDK 来实现如上功能,链接为:https://www.diffbot.com/dev/docs/libraries/,如果大家使用 Python 的话,可以直接使用 Python 的 SDK 即可,Python 的 SDK 链接为:https://github.com/diffbot/diffbot-python-client。 这个库并没有发布到 PyPi,需要自己下载并导入使用,另外这个库是使用 Python 2 写的,其实本质上就是调用了 requests 库,如果大家感兴趣的话可以看一下。 下面是一个调用示例:

1
2
3
4
5
6
7
from client import DiffbotClient,DiffbotCrawl

diffbot = DiffbotClient()
token = 'your_token'
url = 'http://shichuan.github.io/javascript-patterns/'
api = 'article'
response = diffbot.request(url, token, api)

通过这行代码我们就可以通过调用 Article API 来分析我们想要的 URL 链接了,返回结果是类似的。 具体的用法大家直接看下它的源码注释就一目了然了,还是很清楚的。 好,以上便是对智能化提取页面原理的基本介绍以及对 Diffbot 的用法的讲解,后面我会继续介绍其他的智能化解析方法以及一些相关实战,希望大家可以多多关注。

Python

大家好,我是四毛,欢迎关注我的公众号。

有什么想要交流的可以在后台第一时间私我。

今天的文章内容主要是关于字体反爬。

目前已知的几个字体反爬的网站是猫眼,汽车之家,天眼查,起点中文网等等。 以前也看过这方面的文章,今天跟个老哥在交流的时候,终于实操了一把,弄懂了字体反爬是个啥玩意。下面听我慢慢道来。

本文用到的第三方库

fontTools

1、目标网站

url = “https://su.58.com/qztech/

2、反爬虫机制

网页上看见的 后台源代码里面的 从上面可以看出,生这个字变成了乱码,请大家特别注意箭头所指的数字。

3、解决

1、确定反爬方法

在看了别人的解析文章之后,确定采取的是字体反爬机制,即网站定义了字体文件,然后进行相应的查找替换,在前端看起来,是没有任何差异的。其实从审查元素的也是可以看到的: 和大众点评的反爬差不多,都是通过 css 搞得。

2、寻找字体文件

以上面方框里的”customfont“为关键词搜了一下,发现就在源代码里面: 而且还有 base64,直接进行解密,但是解密出来的其实是乱码,这个时候其实要做的很简单,把解密后的内容保存为.ttf格式即可。

ttf 文件: *.ttf 是字体文件格式。TTF(TrueTypeFont)是 Apple 公司和 Microsoft 公司共同推出的字体文件格式,随着 windows 的流行,已经变成最常用的一种字体文件表示方式。 @font-face 是 CSS3 中的一个模块,主要是实现将自定义的 Web 字体嵌入到指定网页中去。

因为我们要对字体进行研究,所以必须将它打开,这里我是用的是FontCreator,打开以后是这个样子(其实很多字,在这里为了看的清楚,所以只截了下面的图): 很明显,每个字可以看到字形和字形编码。 观察现在箭头指的地方和前面箭头指的地方的数字是不是一样啊,没错,就是通过这种方法进行映射的。 所以我们现在的思路似乎就是在源代码里找到箭头指的数字,然后再来字体里找到后替换就行了。 恭喜你,如果你也是这么想的,那你就掉坑里了。 因为每次访问,字体字形是不变的,但字符的编码确是变化的。因此,我们需要根据每次访问,动态解析字体文件。 字体 1: 字体 2: 所以想通过写死的方式也是行不通的。 这个时候我们就要对字体文件进行更深一步的研究了。 3、研究字体文件 刚刚的.ttf 文件我们是看不到内部的东西的,所以这个时候我们要对字体文件进行转换格式,将其转换为 xml 格式,然后来查看: 具体操作如下:

1
2
3
from fontTools.ttLib import TTFont
font_1 = TTFont('58_font_1.ttf')
font_base.saveXML('font_1.xml')

xml 的格式如下: 文件很长,我只截取了一部分。 仔细的观察一下,你会发现~这俩下面的 x,y,on 值都是一毛一样的。所以我们的思路就是以一个已知的字体文件为基本,然后将获取到的新的字体文件的每个文字对应的 x,y,on 值进行比较,如果相同,那么说明新的文字对就 可以在基础字体那里找到对应的文字,有点绕,下面举个小例子。 假设: “我” 在基本字体中的名为 uni1,对应的 x=1,y=1,n=1 新的字体文件中,一个名为 uni2 对应的 x,y, n 分别于上面的相等,那么这个时候就可以确定 uni2 对应的文字为”我”。 查资料的时候,发现在特殊情况下,有时候两个字体中的文字对应的 x,y 不相等,但是差距都是在某一个阈值之内,处理方法差不多,只不过上面是相等,这种情况下就是要比较一下。 其实,如果你用画图工具按照上面的 x 与 y 值把点给连起来,你会发现,就是汉字的字形~ 所以,到此总结一下:

一、将某次请求获取到的字体文件保存到本地[基本字体]; 二、用软件打开后,人工的找出每一个数字对应的编码[ 一定要保证顺序的正确,要不然会出事]; 三、我们以后访问网页时,需要保存新字体文件; 四、用 Fonttools 库对基本字体与新字体进行处理,找 到新的字体与基本字体之间的映射; 五、替换;

4、上代码

微信里上代码真的太丑了, 还是算了吧,微信后台关键词“字体加密” 即可获取 github 地址。 看一下成果

总结

其实这个流程最大的问题就是我们人工录入的基本字体的字典数据有可能是会发生变化的,这就导致我们后面还要手动去改。 现在,如果你已经看懂了本文,那么还不快去其他几个网站试试? 如果有任何问题,欢迎交流。

Python

大家好,我是四毛,欢迎大家关注我的公众号。

今天在工作中,碰到了第一次碰见的反爬虫机制,感觉很有意思,在这里记录一下,希望对大家有帮助。

今天用到的库

requests [请求] lzstring [解压数据] pyexecjs [执行 JS]

简单粗暴,直接上网站部分源代码,因为这个网站应该不太希望别人来爬,所以就不上网站了。为什么这么说,因为刚开始请求的时候,老是给我返回 GO TO HELL ,哈哈。 这个网站点击鼠标右键审查元素,查看网页源代码是无法用的,但是这个好像只能防住小白啊,简单的按 F12 审查元素,CTRL+u 直接查看源代码(谷歌浏览器)。 这次的目的主要是为了获取下面的链接(重度打码)

xxxxxxxxxxx/xxxxx-003-a5f7xxxxxx?cid=xxxxx&xxx=siOE_q4XxBtwdoXqD0xxxx

其中,红色加粗的就是我们要找的变量了。

一、观察与抓包

首先,我观察到了网页源代码中的一部分 js 代码:

type=”text/javascript”>window“\x65\x76\x61\x6c”{e=function(c){return(c35?String.fromCharCode(c+29):c.toString(36))};if(!’’.replace(/^/,String)){while(c—)d[e(c)]=k[c]||e(c);k=[function(e){return d[e]}];e=function(){return’\\w+’};c=1;};while(c—)if(k[c])p=p.replace(new RegExp(‘\\b’+e(c)+’\\b’,’g’),k[c]);return p;}(‘29.2a({“2c”:1k,”26”:”21”,”22”:”1k.24”,”2o”:2n,”2s”:”2r”,”2l”:[“0-2f-2e.3.2”,”0-2j-2i.3.2”,”0-1r-1L.3.2”,”0-1r-1K.3.2”,”0-1e-1X.3.2”,”0-1e-1Q.3.2”,”0-1x-1W.3.2”,”0-1x-1M.3.2”,”0-1B-33.3.2”,”0-1B-36.3.2”,”0-1F-30.3.2”,”0-1F-2U.3.2”,”0-1g-2Y.3.2”,”0-1g-3e.3.2”,”0-1q-3i.3.2”,”0-1q-38.3.2”,”0-1o-37.3.2”,”0-1o-3a.3.2”,”0-1s-2B.3.2”,”0-1s-2E.3.2”,”0-1i-2v.3.2”,”0-1i-2y.3.2”,”0-1G-2P.3.2”,”0-1G-2S.3.2”,”0-1E-2R.3.2”,”0-1E-2H.3.2”,”0-1I-2L.3.2”,”0-1I-2J.3.2”,”0-t-2K.3.2”,”0-t-2G.3.2”,”0-p-2I.3.2”,”0-p-2M.3.2”,”0-o-2Q.3.2”,”0-o-2N.3.2”,”0-u-2O.3.2”,”0-u-2w.3.2”,”0-D-2x.3.2”,”0-D-2t.3.2”,”0-z-2u.3.2”,”0-z-2z.3.2”,”0-y-2D.3.2”,”0-y-2F.3.2”,”0-7-2A.3.2”,”0-7-2C.3.2”,”0-5-2T.3.2”,”0-5-3b.3.2”,”0-4-3c.3.2”,”0-4-39.3.2”,”0-g-3d.3.2”,”0-g-3h.3.2”,”0-i-3j.3.2”,”0-i-3f.3.2”,”0-b-3g.3.2”,”0-b-2X.3.2”,”0-d-2Z.3.2”,”0-d-2V.3.2”,”0-11-2W.3.2”,”0-11-34.3.2”,”0-13-35.3.2”,”0-13-31.3.2”,”0-10-32.3.2”,”0-10-1J.3.2”,”0-15-1O.3.2”,”0-15-1P.3.2”,”0-1d-1R.3.2”,……………………………………,’M4UxFsAsB9odxAIwA7WQOwObQAwCYBWXPAFlwHYBmY6ncousuvYgDlwDZ38BOT8zn3wcKARmICKOXK1qsmrFjlHdRAnKxEb1rBqK2ih5dSSbkteJXnEbpGm+SF47HLZSEElBOwRsEmlOqU3AS0JHYkWiRKJAwkYTYkQiQmIQwcShy0HDaUSpQuDJQ2HAG0BEFFWgTVQhw+3JRMOEw8Shq4PK……………………………………..GHXTzLjxjzN5CziB+3ODSAhicwuCyeBJ3YJD+eKhEi83fHm76PmK3Y/htPJFzw8LzOCqKTMcXGHr7Jm/nlPtuSUfPBAA===’‘\x73\x70\x6c\x69\x63’,0,{})) </script>

为了节省篇幅,我把一些替换成…….了。如果你对这些数据的解压有兴趣,请后台联系我。 我第一眼看见时,做了 2 件事: 1、把[\x65\x76\x61\x6c],[ \x73\x70\x6c\x69\x63],[\x7c]分别解码,解码出的结果为 eval 和 splic,| ; 注:其实这里看到 eval 时就应该想到可以试试直接用 pyexecjs 执行后面的那段 js 匿名函数,但是当时确实啥都没有想起来,很惭愧,后来复盘时才发现这一点。虽然执行了也会报错。 2、把那一长串的字母试着用 base64 解码了,但是解不出来; 然后,就只能再去其他 js 文件里找了,也找到了 js 代码,打了断点,但是看着还是很烦,于是这个时候我就没有在 js 上面死抠。 接着,我调转了方向,在 GITHUB,Google 运用了我祖传的高级搜索技巧,Finally,终于可以盘它了。

二、解密

下面开始解密: 1、数据解压 包含 A===的字符串到底时啥呢,其实这个是 js 的一种数据的压缩方式,知道了这种方式你就可以立即破解这种反爬虫机制了,反正我以前是不知道的,第一次见到,学习了。 在这里,lzstring 闪亮登场,运用这个库,执行下面的代码:

lzstring.LZString().decompressFromBase64(string)

这样就可以把上面的那串字符给解码了,解出来的数据像下面这样:

webp|png|025|024|073

没错,数据解压就是这么的简单; 2、看懂 JS 我们在上面解码出来了一个 splic 和一个[ | ],再看看我们上面解压出来的数据,是不是很有感觉;但是,查了好一会资料,也没有发现 js 里面有这个方法,可能是在别的地方定义了;在我找到的资料里面,那个作者是直接用了 split,最后的结果是对的,走逻辑上也说得过去,不知道为什么这么设置成 splic,还希望有大神可以指教; 3、执行 JS 所以,到这里就很明显了,把解码以后以及解压以后的数据在替换会原 js 数据中,我们前面解码出来的 eval 就有用了,我找到的资料里面作者使用 node 做的,我没有安装 node 环境,所以我就直接用了 pyexecjs.eval 直接执行了,结果也正确; 4、梳理流程: 匹配长字符串==>lzstring 解压+解码后的字段==>拼凑成新的 js 代码==>pyexecjs 执行==>出结果 5、部分代码 执行的主要步骤和结果

execjs.eval(res)

没错,就是这么简单,res 就是替换后的 js,然后就可以直接出来我们上面网址中需要的两个字段了。 截个图吧

三、结语

到这里,今天的文章就结束了,总结一下就是知道了一种 JS 数据的压缩方式,并且学习了解压的方式,JS 的执行,同时写代码的时候**多观察,多想,多试**。 如果觉得还不错,欢迎动动手指关注我。

Python

大家好,我是四毛,好久没有写东西了,欢迎大家关注我的公众号。

今天的文章是关于如何使用 requests 来爬取大众点评的数据。 看完本文,你可以:

1、了解大众点评的 CSS 反爬虫机制 2、破解反爬虫机制 3、使用 requests 即可正确获取到评论数,平均价格,服务,味道,环境数据,评论文本数据;

如果你想跟我继续交流的话,欢迎加我的个人微信,二维码在最后。 如果你想获取更多的代码,请关注我的公众号,并发送 “大众点评”即可。 同时,代码我并没有做太多的优化,因为没有大量的代理,爬不了太多的内容。 这里只是跟大家分享一下处理的流程。欢迎来公众号留言探讨。

正文开始。

1.前言

在工作生活中,发现越来越多的人对大众点评的数据感兴趣,而大众点评的反爬又是比较严格的。采取的策略差不多是宁可错杀一万,也不放过一个。有的时候正常浏览都会跳出验证码。 另外,在 PC 端的展示数据是通过 CSS 来控制的,从网页上看不出来太大的区别,但是用普通的脚本取获取时,会发现数据是获取不到的,具体的源代码是下面这样的: 然,在搜资料的时候,你会发现,很多教程都是用的 selenium 之类的方法,效率太低,没有啥技术含量。 所以,这篇文章的面向的对象就是 PC 端的大众点评;目标是解决这种反爬虫措施,使用 requests 获取到干净正确的数据; 跟着我,绝不会让你失望。

2.正文开始

相信搞过大众点评网站的同学都应该知道上面的这种是一个 css 反爬的方法,具体的解析操作,即将开始。

找到藏着秘密的 css

当我们的鼠标在上面框框内的 span 上面点击时,会发现右边部分会相应的发生变化: 这张图片很重要,很重要,很重要,我们要的值,几乎都从这里匹配出来。 这里我们看到了“vxt20”这个变量对应的两个像素值,前面的是控制用哪个数字,后面的是控制用哪一段的数字集合,先记下,后面要用,同时这里的值应该是 6; 这里其实就是整个破解流程最关键的一步了。在这里我们看到了一个链接。 瞎猫当死耗子吧,点进去看看。 https://s3plus.meituan.net/v1/mss_0a06a471f9514fc79c981b5466f56b91/svgtextcss/f556c0559161832a4c6192e097db3dc2.svg 你会发现,返回的是一些数字,我一开始是不知道这是啥玩意的,看过一些大神的解析才知道,其实这里就是我们看到的数字的来源,也就是整个破解的源头,知道了这些数字的含义,也就可以直接破解了整个反爬的流程。 现在直接看源代码: 可以看到这里面的几个关键数字:font-size:字体大小;还有几个 y 的值,我到后面才知道原来这个 y 是个阈值,起的是个控制的作用。 所以,这一反爬的原理就是:

获取属性值与偏移量和阈值映射,然后从 svg 文件中找到真数据。

现在我们就要用到上面的像素值了。

1.把所有的值取绝对值; 2.用后面的值来选择用哪个段的数字,这里的值是 103,所以使用第三个段的数字集合; 3.因为每个字体是 12 个像素,所以用 163/12=13.58,约等于 14,那么我们数一下第 14 个数字是啥,没错,是 6,和预期一样。你可以多试验几次。

以上,就是整个破解的逻辑过程。 画个流程图,装个逼:

3.Show Code

下面开始晒代码,俗话说得好,天下代码一大抄。 这里对主要的步骤代码进行解释, 如果你想获取更多的代码,请关注我的公众号,并发送 “大众点评”即可。1.获取 css_url 及 span 对应的 TAG 值;

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
def get_tag(_list, offset=1):
# 从第一个开始查
_new_list = [data[0:offset] for data in _list]

if len(set(_new_list)) == 1:
# 如果set后只有一个值,说明全部重复,这个时候就把offset加1
offset += 1
return get_tag(_list, offset)
else:
_return_data = [data[0:offset - 1] for data in _list][0]

return _return_data

def get_css_and_tag(content):
"""
:param url: 待爬链接
:return: css链接,该span对应的tag
"""
find_css_url = re.search(r'href="([^"]+svgtextcss[^"]+)"', content, re.M)
if not find_css_url:
raise Exception("cannot find css_url ,check")
css_url = find_css_url.group(1)

css_url = "https:" + css_url
# 这个网页上不同的字段是由不同的css段来进行控制的,所以要找到这个评论数据对应的tag,在这里返回的值为vx;而在获取评论数据时,tag就是fu-;
# 具体可以观察上面截图的3个span对应的属性值,相等的最长部分为vx
class_tag = re.findall("<b class=\"(.*?)\"></b>", content)
_tag = get_tag(class_tag)

return css_url, _tag

2.获取属性与像素值的对应关系

1
2
3
4
5
6
7
8
9
10
11
12
13
def get_css_and_px_dict(css_url):
con = requests.get(css_url, headers=headers).content.decode("utf-8")
find_datas = re.findall(r'(\.[a-zA-Z0-9-]+)\{background:(\-\d+\.\d+)px (\-\d+\.\d+)px', con)
css_name_and_px = {}
for data in find_datas:
# 属性对应的值
span_class_attr_name= data[0][1:]
# 偏移量
offset = data[1]
# 阈值
position = data[2]
css_name_and_px[span_class_attr_name] = [offset, position]
return css_name_and_px

3.获取 svg 文件的 url

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def get_svg_threshold_and_int_dict(css_url, _tag):
con = requests.get(css_url, headers=headers).content.decode("utf-8")
index_and_word_dict = {}
# 根据tag值匹配到相应的svg的网址

find_svg_url = re.search(r'span[class\^="%s"].*?background\-image: url\((.*?)\);' % _tag, con)
if not find_svg_url:
raise Exception("cannot find svg file, check")
svg_url = find_svg_url.group(1)
svg_url = "https:" + svg_url
svg_content = requests.get(svg_url, headers=headers).content
root = H.document_fromstring(svg_content)
datas = root.xpath("//text")
# 把阈值和对应的数字集合放入一个字典中
last = 0
for index, data in enumerate(datas):
y = int(data.xpath('@y')[0])
int_data = data.xpath('text()')[0]
index_and_word_dict[int_data] = range(last, y+1)
last = y
return index_and_word_dict

4. 获取最终的值

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
def get_data(url ):
"""
:param page_url: 待获取url
:return:
"""
con = requests.get(url, headers=headers).content.decode("utf-8")
# 获取css url,及tag
css_url, _tag = get_css(con)
# 获取css对应名与像素的映射
css_and_px_dict = get_css_and_px_dict(css_url)
# 获取svg的阈值与数字集合的映射
svg_threshold_and_int_dict = get_svg_threshold_and_int_dict(css_url, _tag)

doc = etree.HTML(con)
shops = doc.xpath('//div[@id="shop-all-list"]/ul/li')
for shop in shops:
# 店名
name = shop.xpath('.//div[@class="tit"]/a')[0].attrib["title"]
print name
comment_num = 0

comment_and_price_datas = shop.xpath('.//div[@class="comment"]')
for comment_and_price_data in comment_and_price_datas:
_comment_data = comment_and_price_data.xpath('a[@class="review-num"]/b/node()')
# 遍历每一个node,这里node的类型不同,分别有etree._ElementStringResult(字符),etree._Element(元素),etree._ElementUnicodeResult(字符)
for _node in _comment_data:
# 如果是字符,则直接取出
if isinstance(_node, etree._ElementStringResult):
comment_num = comment_num * 10 + int(_node)
else:
# 如果是span类型,则要去找数据
# span class的attr
span_class_attr_name = _node.attrib["class"]
# 偏移量,以及所处的段
offset, position = css_and_px_dict[span_class_attr_name]
index = abs(int(float(offset) ))
position = abs(int(float(position)))
# 判断
for key, value in svg_threshold_and_int_dict.iteritems():
if position in value:
threshold = int(math.ceil(index/12))
number = int(key[threshold])
comment_num = comment_num * 10 + number
print comment_num

4.结果展示

评论条数数据

其实,其他的我都写好了,就不贴了

评论具体数据

5.结语

以上就是大众点评 Css 反爬破解的全部步骤和部分代码。 如果你想获取更多的代码,请关注我的公众号,并发送 “大众点评”即可。

个人日记

最近一段时间没有更新原创文章了,主要是因为最近整个在忙硕士毕业的各种事情,毕业答辩完了以后休假了一小段时间,整个十二月就这么过去了。 转眼已经 2019 年了,其实去年我并没有写年终总结,现在到头来还是蛮后悔的,说实话总结其实还是蛮有必要的,现在就趁着这个时间来对自己的 2018 做一下总结,并立一下 2019 的一些 Flag,再等到 2020 年翻出来打脸吧。 世界上最了解自己的人当然是自己,一句话总结我的 2018 可以说是: 成就不算少,进步不算多 的一年。为什么成就不算少呢?因为今年可能发生的事相比我之前几年,自己取得了一些从未有的成就,或者完成了一些比较重要的事情。下面就先总结一下自己取得的一些成就吧: 1. 2018 年中出版了自己人生中第一本书籍作品《Python3网络爬虫开发实战》,半年多时间现在已经累计印刷九次,共计 45000 册,上市初长期处在京东科技新书榜第一位的位置,豆瓣评分 9.2。 2. 2018 年 1 月 20 日正式开始运营自己的个人公众号“进击的Coder”,发布了两百多篇文章,其中大约一半为原创,粉丝现在是 25000 多人。 3. 发布了自己开发的一款分布式网络爬虫管理框架 Gerapy,可以完成爬虫项目的配置、部署、管理、监控等功能,目前 GitHub 上 Star 数目将近 1000。 4. 在 GitHub 上创建和贡献了近 100 个项目,目前个人 Followers 达到 3000 人,收获 Star 数目在 2000 左右。 5. 开始从事机器学习、深度学习方向的研究和开发,并发布了自己开发的一套深度学习脚手架 ModelZoo,并不断对接各种模型,持续完善中。 6. 比较顺利地完成秋招,并最终拿到了微软的 Offer,达成理想的结果。 7. 完成了多次大型公开演讲并结识了许多优秀的业界大佬。 8. 顺利地完成了毕业论文和毕业答辩,成功取得北航硕士学位。 9. 凭借自己赚得人生第二桶金,实现收入翻番。 10. Last but not least,她。 当然还有一些其他的就不再一一列举了。这些可能有的跟一些大佬比起来可能真的是小巫见大巫,不过对我来说,这些成就我总体还是比较满意的。 在这些成就里,有些是真的改变我人生轨迹的事情,有些可能只是我人生路顺理成章踏过去的事情,有的可能只是昙花一现几年后毫无价值的东西。但拿掉其中一个,不管是看似有用或者没用的,也可能会产生意想不到的影响,他们确实是我真实经历过,并出现在我生命旅途中的一部分。 本来把成就一个个地展开来说,但后来想想没这个必要。昨日取得的成就就让它过去吧,不要念过去,把更多的精力着眼在未来吧。重要的在于,我在这一年里收获了多少,这些收获的东西能够为我未来的成长带来多少价值。以及另外重要的是,对过去的反思,我还有哪些做的不好的地方,我需要在未来做怎样的调整。 所以为什么说进步不算多呢?虽然自己达成了某些目标,但我真的没怎么感觉到自己相比去年年初的自己有特别大的成长,或许一切都是潜移默化的,或许真的把我现在放回到 2018年初,和现在的自己相比,我才会感觉到差别,但现在自己仿佛真的感觉自己本身和之前没有太多不同。但总的来说,我还有非常多需要反思和改进的地方。 下面再好好反思下自己吧,我总觉得自己的这一年,自己的有些方面做得不够好,总体体现在这么几个方面吧: 1. 拖延症。还是觉得自己的拖延症太严重,执行力还不够好。有些事总是会拖个好几天才完成,没太有恒心。或者有的时候这件事到了要做的时间,却又不想做。之前也会有,但是感觉今年犯的很严重。其实我挺佩服那些持之以恒的人的,自制力和执行力真的很重要。 2. 记录少。挺多自己觉得会的东西,由于懒或者忙,没有记下来,其实还是因为懒。到头来发现真正学会的其实仅仅就是自己记录的部分,因为当时没做记录的,又忘得一干二净了。所以我总体感觉,今年输出的文章并不多。 3. 白忙活。这一年来,其实回想起来,感觉做的无用功是很多了。许多事情感觉做完了之后,真正的价值并不大,所以这个真的需要我来好好权衡。 4. 虎头蛇尾。由于自己的很多事情安排的满满当当,所以有的项目可能就容易被搁置了,导致有一些项目做得虎头蛇尾,草草收场,想想还是很可惜的,但有时候就是缺乏那重新捡起来的一步动力,这个必须得改。 5. 时间规划。我使用 Todoist 已经好多年了,感谢它帮我完成了大部分的时间规划,但有些时候发现还是有些不够好。我多少还会有一些小事优先,大事拖延的倾向,但总体来说已经把握得还算可以了,还得继续加油。 6. 缺乏锻炼。平时事情安排的挺多,有时候也懒得去健身房锻炼,唯一锻炼的时间就是睡前的几分钟了,但是效果不大。可能偶尔一时兴起会去几次健身房,但是还是难以坚持下来。虽然还没感觉到身体有啥不适,但也得加强锻炼了。 所以总体感觉下来,感觉到自己做的不够好的地方还有很多,或许真的要改正对我来说真的挺难,但我也要尽力去做的更好。 这一年中,领悟或贯彻的一些理念也不少,其实太多太多,在这里也不能一一列举了,这里就把现在想到的一些说一下吧: 1. 做一件事前,想想这件事的意义和成本有多大,别把时间浪费在无意义的事情上面。 2. 记录和总结真的很重要。别以为自己当时记住了,等回过头来,你有时候会发现自己的收获就是你所记录的那一部分,没有记录的你想也想不起来了。 3. 如果自己没本事,认识很多牛人,没什么太大意义,重要的还是自己要牛逼,多投资自己。自己牛逼了,别人就都会看到你了。 4. 成年人、社会人的世界,有太多大家都已经默认的规矩,自己要去懂,没几个人会教你。 5. 钱,不能保证能买到所有东西,但是它的确可以提供便利或者免去不必要的麻烦。它是可以增加幸福感的。 6. 如果想要赚钱,最好不要选和时间或精力成正比的事情,多考虑复利。 7. 如果一件东西,花了钱,买到的又不是达到自己预期的,这是双亏。如果买到的是自己所满意或喜欢的的,这就是值的。 8. 选择比努力很重要,眼界比本事更重要。 9. 所有的一切,如果有能力,就不要将就,可能一时由于自己的能力还无法改变,但一定不要丢了改变下去的动力。 10. 如果一件事是自己不喜欢的但是非要做,那就用最短的时间去完成。 11. 健康第一。 还有很多,就暂且写这些吧。这是我现阶段所领悟到的一些东西,也是我成长路上的一种认知转变过程吧。也可能再过几年我就不会这么想了。不过现阶段我会秉承这些理念继续前行。 最后就立几个 2019 年的 Flag 吧(等明年翻出来打脸: 1. 踏入社会和职场的第一年,好好工作,把握好节奏。 2. 完成《Python3网络爬虫开发实战(第二版)》以及现在初步规划的另外的两部作品(名称尚未完全确定暂不透露。 3. 微信公众号读者粉丝在 2019 年底达到 10 万。 4. 自己的 Gerapy、ModelZoo 两个项目持续运营维护,力求完善。 5. 坚持记录,持续输出(内容规划了挺多),达到自己内心中设定的基本目标。 6. 2019 年年收入比 2018 年继续翻番。 7. 结识更多的人,扩展更广的人脉。 8. Last but not least,和她好好地在一起,过我们的向往的生活,做喜欢的事情。 好啦,感谢大家一直以来的支持!新的一年,继续努力!愿我们都成为更好的自己!

Linux

Hello 各位小伙伴 雷门吼!

在教程之前首先申明!此教程适合土豪不缺钱的玩家

潜水了许久了,今天来更新点东西~ 今天说点啥呢? 那就是代理!! 代理在爬虫界的重要作用相信各位应该清楚吧!毕竟绝大部分反爬可以靠代理解决;不能被代理解决的也得要代理配合解决。 市面上各种代理也是琳琅满目的说··· 相信大家最喜欢用的之一应该就是 某布云。 根据官网的显示他他家的代理是这个样子的:

  • 无须切换 IP,每一个请求一个随机 IP。

哇!感觉很爽的样子今天我们就来实现一个类似的代理! 其实 So Easy! 我们需要借助 公有云 来实现。 下面我以 AWS 举例(其它公有云操作类似,唯一的区别的就是:各个服务的名字不同而已)

  1. 首先我们需要需要使用 EC2 来建立一个代理(Google Could 叫 GCE)
    1. 安装 Squid(当然你可以使用其它的代理)
      1. 无认证安装参考这儿(点我)设置代理服务器那一段
      2. MySQL 认证安装(点我)
      3. Note: 请注意检查!!!务必设置 Ipv4 转发
  2. 安装完成之后我们制作启动模板(毕竟一个 EC2 一个 IP 你总不能安装很多很多台吧!会死人的)
    1. 注意设置你的安全组!正常情况下 入站规则只应该有你需要的端口(squid 使用的端口一定要放心!嫌麻烦的小伙伴儿 可以进出都放行全部流量!)出站则是全部流量!
    2. 启动 EC2 的时候选择安全组一定要看清是否是设置过放行的规则! 不要选错了!
    3. 好了现在就可以批量启动了!
    4. 实例数量就是需要同时有多少个 IP 就启动多少个了。
  3. 设置前端负载均衡(提供一个固定地址,这个地址负责随机将请求转发到后端代理服务器上)
    1. 必须使用 TCP 四层负载!原因为啥大家自己百度一哈
    2. 等待负载均衡器启动完成!
    3. 启动完成后获取负载地址
  4. 下面来测试一下效果!

以上完毕!你可以不停的重启 Ec2 实例!你就有百万 IP 池啦!!(前提是你有钱啊) 下面是重启 Ec2 的示例:

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
import boto3
from boto3 import setup_default_session


setup_default_session(aws_access_key_id='XXXXXX',
aws_secret_access_key='XXXXXX',
region_name='区域')

ec2 = boto3.client('ec2')


def get_public_ip_address():
"""
获取IP和实例ID
:return: {实例ID: ip}
"""
response = ec2.describe_instances()
reservations = response.get('Reservations')
instances = [i.get('Instances')[0] for i in reservations]
instance_id_public_ip_address = {i.get('InstanceId'): i.get('PublicIpAddress') for i in instances}
return instance_id_public_ip_address


def reboot_ec2(ip):
"""重启实例
:param ip:
:return:
"""
instance_id_public_ip_address = get_public_ip_address()
instance_id = instance_id_public_ip_address.get(ip)
try:
ec2.reboot_instances(InstanceIds=[instance_id], DryRun=True)
except ClientError as e:
if 'DryRunOperation' not in str(e):
print("You don't have permission to reboot instances.")
raise

try:
response = ec2.reboot_instances(InstanceIds=[instance_id], DryRun=False)
print('Success', response)
except ClientError as e:
print('Error', e)

Python

前言

随着大数据时代的到来,爬虫已经成了获取数据的必不可少的方式,做过爬虫的想必都深有体会,爬取的时候莫名其妙 IP 就被网站封掉了,毕竟各大网站也不想自己的数据被轻易地爬走。 对于爬虫来说,为了解决封禁 IP 的问题,一个有效的方式就是使用代理,使用代理之后可以让爬虫伪装自己的真实 IP,如果使用大量的随机的代理进行爬取,那么网站就不知道是我们的爬虫一直在爬取了,这样就有效地解决了反爬的问题。 那么问题来了,使用什么代理好呢?这里指的代理一般是 HTTP 代理,主要用于数据爬取。现在打开搜索引擎一搜 HTTP 代理,免费的、付费的太多太多品牌,我们该如何选择呢?看完这一篇文章,想必你心中就有了答案。 对于免费代理,其实想都不用想了,可用率能超过 10% 就已经是谢天谢地了。真正靠谱的代理还是需要花钱买的,那这么多家到底哪家可用率高?哪家响应速度快?哪家比较稳定?哪家性价比比较高?为此,我对市面上比较流行的多家付费代理针对可用率、爬取速度、爬取稳定性、价格、安全性、请求限制等做了详细的评测,让我们来一起看一下到底哪家更强!

测评范围

免费代理

在这里我主要测试的是付费代理,免费代理可用率太低,几乎不会超过 10%,但为了作为对比,我选取了西刺免费代理进行了测试。

付费代理

付费代理我选取了站大爷、芝麻 HTTP 代理、太阳 HTTP 代理、讯代理、快代理、蘑菇代理、阿布云代理、全网代理、云代理、大象代理、多贝云进行了对比评测,购买了他们的各个不同级别的套餐使用同样的网络环境进行了测评,详情如下:

代理商家 套餐类型 官方网站
芝麻 HTTP 代理 默认版 http://h.zhimaruanjian.com
阿布云代理 专业版 https://www.abuyun.com
动态版
经典版
大象代理 个人版 http://www.daxiangdaili.com
专业版
企业版
全网代理 普通版 http://www.goubanjia.com
动态版
快代理 开放代理 https://www.kuaidaili.com
私密代理
独享代理
蘑菇代理 默认版 http://www.mogumiao.com
太阳 HTTP 代理 默认版 http://http.taiyangruanjian.com
讯代理 优质代理 http://www.xdaili.cn
混播代理
独享代理
云代理 VIP 套餐 http://www.ip3366.net
站大爷代理 普通代理 http://ip.zdaye.com
短效优质代理

注:其中蘑菇代理、太阳 HTTP 代理、芝麻 HTTP 代理的默认版表示此网站只有这一种代理,不同套餐仅是时长区别,代理质量没有差别。 嗯,我把上面的套餐全部买了一遍,以供下面的评测使用。

测评目标

本次测评主要分析代理的可用率、响应速度、稳定性、价格、安全性、使用频率等因素,下面我们来一一进行说明。

可用率

可用率就是提取的这些代理中可以正常使用的比率。假如我们无法使用这个代理请求某个网站或者访问超时,那么就代表这个代理不可用,在这里我的测试样本大小为 500,即提取 500 个代理,看看里面可用的比率多少。

响应速度

响应速度可以用耗费时间来衡量,即计算使用这个代理请求网站一直到得到响应所耗费的时间。时间越短,证明代理的响应速度越快,这里同样是 500 个样本,计算时只对正常可用的代理做统计,计算耗费时间的平均值。

稳定性

由于爬虫时我们需要使用大量代理,如果一个代理响应速度特别快,很快就能得到响应,而下一次请求使用的代理响应速度特别慢,等了三十秒才得到响应,那势必会影响爬取效率,所以我们需要看下商家提供的这些代理稳定性怎样,总不能这一个特别快,下一个又慢的不行。所以这里我们需要统计一下耗费时间的方差,方差越大,证明稳定性越差。

价格

价格,这个当然是需要考虑的内容,如果一个代理不论是响应速度还是稳定性都特别不错,但是价格非常非常高,这也是不可接受的。

安全性

这的确也是需要考虑的因素,比如一旦不小心把代理提取的 API 泄露出去了,别人就肆意使用我们的 API 提取代理使用,而一直耗费的是我们的套餐。另外一旦别人通过某些手段获取了我们的代理列表,而这些代理是没有安全验证的,这也会导致别人偷偷使用我们的代理。在生产环境上,这方面尤其需要注意。

使用频率

有些代理套餐在 API 调用提取代理时有频率限制,有的代理套餐则会限制请求频率,这些因素都会或多或少影响爬虫的效率,这部分因素我们也需要考虑进来。

测评标准

要做标准的测评,那就必须在标准的测评环境下进行,且尽可能排除一些杂项的干扰,如网络波动、传输延迟等一系列的影响。

主机选取

由于我的个人笔记本是使用 WiFi 上网的,所以可能会有网络波动,而且实际带宽其实并不太好把控,因此它并不适合来做标准评测使用。评测需要在一个网络稳定的条件下进行,而且多个代理的评测环境必须相同,在此我选择了一台腾讯云主机作为测试,主机配置如下:

参数名 参数值
操作系统 Ubuntu 16.04.1 LTS (GNU/Linux 4.4.0-53-generic x86_64)
带宽 5 Mbps
核心数 2
内存 4GB
Python 版本 3.5.2

这样我们就可以保证一个标准统一的测试环境了。

现取现测

另外在评测时还需要遵循一个原则,那就是现取现测,即取一个测一个。现在很多付费代理网站都提供了 API 接口,我们可以一次性提取多个代理,但是这样会导致一个问题,每个代理在提取出来的时候,商家是会尽量保证它的可用性的,但过一段时间,这个代理可能就不好用了,所以假如我们一次性提取出来了 100 个代理,但是这 100 个代理并没有同时参与测试,后面的代理就会经历一个的等待期,过一段时间再测这些代理的话,肯定会影响后半部分代理的有效性,所以这里我们将提取的数量统一设置成 1,即请求一次接口获取一个代理,然后立即进行测试,这样可以保证测试的公平性,排除了不同代理有效期的干扰。

时间计算

由于我们有一项是测试代理的响应速度,所以我们需要计算程序请求之前和得到响应之后的时间差,这里我们使用的测试 Python 库是 requests,所以我们就计算发起请求和得到响应之间的时间差即可,时间计算方法如下所示:

1
2
3
4
start_time = time.time()
requests.get(test_url, timeout=timeout, proxies=proxies)
end_time = time.time()
used_time = end_time - start_time

这里 used_time 就是使用代理请求的耗时,这样测试的就仅仅是发起请求到得到响应的时间。

测试链接

测试时我们也需要使用一个稳定的且没有反爬虫的链接,这样可以排除服务器的干扰,这里我们使用百度来作为测试目标。

超时限制

在测试时免不了的会遇到代理请求超时的问题,所以这里我们也需要统一一个超时时间,这里设置为 60 秒,如果使用代理请求百度,60 秒还没有得到响应,那就视为该代理无效。

测试数量

要做测评,那么样本不能太小,如只有十几次测试是不能轻易下结论的,这里我选取了一个适中的测评数量 500,即每个套餐获取 500 个代理进行测试。

测评过程

嗯,测评过程这边主要说一下测评的代码逻辑,首先测的时候是取一个测一个的,所以这里定义了一个 test_proxy() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
test_url = 'https://www.baidu.com/'
timeout = 60

def test_proxy(proxy):
try:
proxies = {
'https': 'http://' + proxy
}
start_time = time.time()
requests.get(test_url, timeout=timeout, proxies=proxies)
end_time = time.time()
used_time = end_time - start_time
print('Proxy Valid', 'Used Time:', used_time)
return True, used_time
except (ProxyError, ConnectTimeout, SSLError, ReadTimeout, ConnectionError):
print('Proxy Invalid:', proxy)
return False, None

这里需要传入一个参数 proxy,代表一个代理,即 IP 加端口组成的代理,然后这里使用了 requests 的 proxies 参数传递给 get() 方法。对于代理无效的检测,这里判断了 | ProxyError, ConnectTimeout, SSLError, ReadTimeout, ConnectionError 这几种异常,如果发生了这些异常统统视为代理无效,返回错误。如果在 timeout 60 秒内得到了响应,那么就计算其耗费时间并返回。 在主程序里,就是获取 API 然后统计结果了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
max = 500

def main():
print('Testing')
used_time_list = []
valid_count = 0
total_count = 0
while True:
flag, result = get_page(api_url)
if flag:
proxy = result.strip()
if is_proxy(proxy):
total_count += 1
print('Testing proxy', proxy)
test_flag, test_result = test_proxy(proxy=proxy)
if test_flag:
valid_count += 1
used_time_list.append(test_result)
stats_result(used_time_list, valid_count, total_count)
time.sleep(wait)
if total_count == max:
break

这里加了一些判断,如 is_proxy() 方法判断了获取的是不是符合有效的代理规则,即判断它是不是 IP 加端口的形式,这样可以排除 API 返回一些错误信息的干扰。另外这里设置了 total_count 和 valid_count 变量,只有符合代理规则的代理参与了测试,这样才算一次有效测试,total_count 加一,如果测试可用,那么 valid_count 加一并记录耗费时间。最后调用了 stats_results 方法进行了统计:

1
2
3
4
5
6
7
8
9
10
11
import numpy as np

def stats_result(used_time_list, valid_count, total_count):
if not used_time_list or not total_count:
return
used_time_array = np.asarray(used_time_list, np.float32)
print('Total Count:', total_count,
'Valid Count:', valid_count,
'Valid Percent: %.2f%%' % (valid_count * 100.0 / total_count),
'Used Time Mean:', used_time_array.mean(),
'Used Time Var', used_time_array.var())

这里使用了 Numpy 来统计了耗费时间的均值和方差,分别反映代理的响应速度和稳定性。 嗯,就这样,利用这个方法我对各个不同的代理套餐逐一进行了测试。

测评结果

经过测评,初步得到如下统计结果:

代理商家 套餐类型 测试次数 有效次数 可用率 响应时间均值 响应时间方差
芝麻 HTTP 代理 默认版 500 495 99.00% 0.916853 1.331989
阿布云代理 专业版 500 452 90.40% 0.68770707 1.1477163
动态版 500 494 98.80% 1.83994 6.0491614
经典版 500 499 99.80% 0.49301904 0.25724468
大象代理 个人版 500 238 47.60% 5.340489 78.56444
专业版 500 284 56.80% 6.87078 105.7984
企业版 500 259 51.80% 6.3081837 121.08402
全网代理 普通版 500 220 44.00% 5.584057 47.442596
动态版 500 485 97.00% 2.776973 17.568045
快代理 开放代理 500 178 35.60% 16.636587 221.69661
私密代理 500 495 99.00% 1.2044522 3.72582
独享代理 500 497 99.40% 0.5435687 2.27832
蘑菇代理 默认版 500 497 99.40% 1.0985725 9.532586
太阳 HTTP 代理 默认版 500 400 80.00% 1.2522483 12.662229
讯代理 优质代理 500 495 99.00% 1.0512681 6.4247565
混播代理 500 494 98.80% 1.0664985 6.451699
独享代理 500 500 100% 0.7056521 0.35416448
云代理 VIP 套餐 500 489 97.80% 3.4216988 38.120296
站大爷代理 普通代理 500 92 18.40% 5.067193 66.12128
短效优质代理 500 488 97.60% 1.5625348 8.121197
西刺代理 免费 500 31 6.2% 9.712833 95.09569

注:

  • 表中的响应时间方差越大,代表稳定性越低。
  • 阿布云代理经典版方差较小是因为它是长时间锁定了同一个 IP,因此极其稳定,但每秒最大请求默认 5 次。
  • 多贝云代理套餐一二方差较小是因为它是长时间锁定了同一个 IP,因此极其稳定,但每秒最大请求默认 20 次。

测评分析

下面我们将从各个方面分析一下各个套餐的优劣。

可用率

通过可用率统计,我们可以发现可用率较高的代理套餐有:

级别 套餐 描述
第一梯队 讯代理独享代理、阿布云代理经典版、快代理私密代理、蘑菇代理、芝麻 HTTP 代理、快代理独享代理、讯代理优质代理 可用率 99% 以上
第二梯队 阿布云代理动态版、讯代理混播代理、云代理、站大爷短效优质代理、全网代理动态版、阿布云代理专业版 可用率 99% 以下,90% 以上
第三梯队 太阳 HTTP 代理、大象代理专业版、大象代理企业版 可用率 90% 以下,50% 以上
第四梯队 大象代理个人版、全网代理普通版、快代理开放代理 可用率 50% 以下,20% 以上
第五梯队 站大爷普通代理、西刺代理 可用率 20% 以下

响应速度

通过平均响应速度判别,我们可以发现响应速度较快的代理套餐有:

级别 套餐 描述
第一梯队 阿布云代理经典版、阿布云代理专业版、快代理私密代理、讯代理独享代理、芝麻 HTTP 代理 响应时间 1s 以内
第二梯队 讯代理优质代理、快代理独享代理、讯代理混播代理、蘑菇代理、太阳代理、站大爷短效优质代理、阿布云代理动态版 响应时间 1s 以上,2s 以内
第三梯队 全网代理动态版、云代理 响应时间 2s 以上,5s 以内
第四梯队 站大爷普通代理、大象代理个人版、全网代理普通版、大象代理企业版、大象代理专业版、西刺代理 响应时间 5s 以上,10s 以内
第五梯队 快代理开放代理 响应时间 10s 以上

稳定性

通过平均响应速度方差分析,我们可以发现稳定性较高的代理套餐有:

级别 套餐 描述
第一梯队 阿布云代理经典版、讯代理独享代理、快代理私密代理、阿布云代理专业版、芝麻 HTTP 代理 方差 3 以内
第二梯队 快代理独享代理、阿布云代理动态版、讯代理优质代理、讯代理混播代理、站大爷短效优质代理、蘑菇代理 方差 10 以内,3 以上
第三梯队 太阳HTTP代理、全网代理动态版、云代理、全网代理普通版、站大爷普通代理、大象代理个人版、西刺代理 方差 100 以内,10 以上
第四梯队 大象代理专业版、大象代理企业版、快代理开放代理 方差 100 以上

价格

我们可以看一下各个套餐的价格:

代理商家 套餐类型 价格描述 价格 URL 备注
芝麻 HTTP 代理 默认版 ¥98/周 ¥360/月 http://h.zhimaruanjian.com/newrecharge/ 另有包量套餐、长效 IP 套餐可选购,定期有优惠活动,可领免费 IP,可免费试用
阿布云代理 专业版 ¥1/时 ¥16/天 ¥108/周 ¥429/月 https://www.abuyun.com/ 每秒请求只有5个,多加每秒请求1个需要 1¥0.5/月,¥90 /年
动态版 ¥1/时 ¥16/天 ¥108/周 ¥429/月
经典版 ¥1/时 ¥16/天 ¥108/周 ¥429/月
大象代理 个人版 ¥9/天 ¥98/月 http://www.daxiangdaili.com/ 好评可送时长
专业版 ¥19/天 ¥198/月
企业版 ¥49/天 ¥498/月
全网代理 普通版 ¥9/天 ¥35/周 ¥93/月 ¥500/年 http://www.goubanjia.com/buy/high.shtml
动态版 ¥10/天 ¥160/月 ¥1250/年 http://www.goubanjia.com/buy/dynamic.shtml
快代理 开放代理 ¥20/天 ¥60/周 ¥200/月 ¥2000/年 https://www.kuaidaili.com/pricing 有普通、VIP、SVIP、专业版可选
独享代理 ¥8/天 ¥32/周 ¥80/月
私密代理 ¥48/天 ¥240/周 ¥720/月
蘑菇代理 默认版 ¥6/天 ¥169/月 ¥1699/年 http://www.mogumiao.com/buy 另有包量套餐可选购,可免费试用
太阳 HTTP 代理 默认版 ¥60/周 ¥198/月 ¥498/季 ¥1590/年 http://http.taiyangruanjian.com/newrecharge/ 另有保量套餐可选购,可领免费 IP,可免费试用
讯代理 优质代理 ¥9/天 ¥210/月 ¥2100/年 http://www.xdaili.cn/buyproxy 可免费试用
混播代理 ¥29/天 ¥729/月 ¥6999/年
独享代理 ¥9/天 ¥210/月 ¥2100/年
云代理 VIP 套餐 ¥10/天 ¥120/月 ¥599/年 http://www.ip3366.net/pricing/ 另有普通套餐可选
站大爷代理 普通代理 ¥8/天 ¥80/月 ¥720/年 http://ip.zdaye.com/buy.html 另有私密代理可选
短效优质代理 ¥17/天 ¥475/月 ¥4569/年 http://ip.zdaye.com/ShortProxy.html

安全性

对于安全性,此处主要考虑提取 API 是否有访问验证,使用代理时是否有访问验证,即可以通过设置白名单来控制哪些可以使用。 其中只有芝麻 HTTP 代理、太阳 HTTP 代理默认使用了白名单限制,即只有将使用 IP 添加到白名单才可以使用,可以有效控制使用权限。 另外阿布云代理提供了隧道代理验证,只有成功配置了用户名和密码才可以正常使用。 所以在此归纳如下:

级别 套餐 描述
第一梯队 快代理、芝麻 HTTP 代理、太阳 HTTP 代理、阿布云代理、多贝云代理 默认使用了白名单控制或隧道代理验证
第二梯队 其他 可直接使用

调取频率

不同的接口具有不同的 API 调用频率限制,归纳如下:

代理商家 套餐类型 调取频率限制
芝麻 HTTP 代理 默认版 1秒
阿布云代理 专业版 无需获取
动态版 无需获取
经典版 无需获取
大象代理 个人版 1秒
专业版 1秒
企业版 无限制
全网代理 普通版 无限制
动态版 100毫秒
快代理 开放代理 200毫秒
独享代理 100毫秒
私密代理 100毫秒
蘑菇代理 默认版 5秒
太阳 HTTP 代理 默认版 1秒
讯代理 优质代理 5秒
混播代理 10秒
独享代理 15秒
云代理 VIP 套餐 无限制
站大爷代理 普通代理 3秒
短效优质代理 10秒
西刺代理 免费 无限制

在此可以简单总结如下:

级别

级别 套餐 描述
第一梯队 云代理、全网代理普通版、大象代理企业版、西刺代理、阿布云(调取无限制,请求默认最大 1 秒 5 请求) 无限制
第二梯队 全网代理动态版、快代理(所有套餐) 小于 1s
第三梯队 大象代理个人版、大象代理专业版、芝麻 HTTP 代理、太阳 HTTP 代理、站大爷普通代理、蘑菇代理、讯代理优质代理 1s – 5s
第四梯队 讯代理混播代理、讯代理独享代理、站大爷短效优质代理 大于 5s

特色功能

除了常规的测试之外,我这边还选取了某些套餐的与众不同之处进行说明,这些特点有的算是缺点,有的算是优点,现列举如下:

代理 描述
阿布云代理 多贝云代理 快代理 使用隧道技术实现,代理不能直接拿到,必须配置访问认证,默认 1 秒只能支持 5/20 个请求,如需更多需要付费。
讯代理 独享代理拨号时间略长,可用主机少,容易出现拨号失败现象,单个代理有效时长可控。
芝麻 HTTP 代理、快代理 必须要设置白名单才可以使用,后台可控,使用 API 提取代理不扣费,使用时才扣费。

测评综合

分项了解了各个代理套餐的可用率、响应速度、稳定性、性价比、安全性等内容之后,最后做一下总结:

代理商家 套餐类型 可用率 可用率评价 响应时间均值 响应速度评价 响应时间方差 稳定性 包月价格 价格评价 安全性 访问频率限制 调取频率限制 推荐指数
芝麻 HTTP 代理 默认版 99% 极高 0.916853 极快 1.331989 极好 360 较高 1 秒 ★★★★★
阿布云代理 专业版 90.4% 0.68770707 极快 1.1477163 极好 429 无需获取 ★★★☆
动态版 98.8% 1.83994 6.0491614 429 无需获取 ★★★★
经典版 99.8% 极高 0.49301904 极快 0.25724468 极好 429 无需获取 ★★★★
大象代理 个人版 47.6% 5.340489 78.56444 一般 98 1 秒 ★★
专业版 56.8% 一般 6.87078 105.7984 198 较低 1 秒 ★☆
企业版 51.8% 一般 6.3081837 121.08402 498 无限制
全网代理 普通版 44% 5.584057 47.442596 一般 93 无限制 ★★
动态版 97% 2.776973 一般 17.568045 一般 160 较低 100毫秒 ★★★
快代理 开放代理 35.6% 一般 16.636587 极慢 221.69661 200 200毫秒
独享代理 99.00% 极高 1.2044522 3.72582 64 100毫秒 ★★★★★
私密代理 99.40% 极高 0.5435687 极快 2.27832 720 100毫秒 ★★★★☆
蘑菇代理 默认版 99.4% 极高 1.0985725 9.532586 169 较低 5秒 ★★★★☆
太阳 HTTP 代理 默认版 80% 一般 1.2522483 12.662229 一般 198 较低 1秒 ★★★★
讯代理 优质代理 99% 极高 1.0512681 6.4247565 210 5秒 ★★★★☆
混播代理 98.8% 1.0664985 6.451699 729 10秒 ★★★☆
独享代理 100% 极高 0.7056521 极快 0.35416448 极好 210 15秒 ★★★★☆
云代理 VIP 套餐 97.8% 3.4216988 一般 38.120296 一般 120 较低 无限制 ★★★☆
站大爷代理 普通代理 18.4% 极低 5.067193 66.12128 一般 80 3秒 ★☆
短效优质代理 97.6% 1.5625348 8.121197 475 10秒 ★★★☆
西刺代理 免费 6.2% 极低 9.712833 95.09569 一般 0 免费 无限制

所以在综合来看比较推荐的有:芝麻代理、快代理、讯代理、阿布云、多贝云代理,详细的对比结果可以参照表格。 以上便是各家代理的详细对比测评情况,希望此文能够在大家选购代理的时候有所帮助。

Python

大家好,我是四毛,下面的是我的公众号,欢迎关注。

今天的内容主要讲的是破解一个网站的 rsa 加密,当然肯定不是破解这个算法,而是找到加密的参数,正确模拟这个算法即可。

1. 什么是 rsa 算法

下面的资料摘抄自阮一峰老师的文章, 点这里了解更多 1976 年,两位美国计算机学家 Whitfield Diffie 和 Martin Hellman,提出了一种崭新构思,可以在不直接传递密钥的情况下,完成解密。这被称为“Diffie-Hellman 密钥交换算法”。这个算法启发了其他科学家。人们认识到,加密和解密可以使用不同的规则,只要这两种规则之间存在某种对应关系即可,这样就避免了直接传递密钥。 这种新的加密模式被称为”非对称加密算法”。

(1)乙方生成两把密钥(公钥和私钥)。公钥是公开的,任何人都可以获得,私钥则是保密的。 (2)甲方获取乙方的公钥,然后用它对信息加密。 (3)乙方得到加密后的信息,用私钥解密。

如果公钥加密的信息只有私钥解得开,那么只要私钥不泄漏,通信就是安全的。

2. 研究目标

从我要研究的网站来说,就是根据参数得到正确的公钥,加密以后返回给服务器,让服务器使用私钥可以解密出正确的数据即可。 同时,本文不会将具体的网站说出来,只是给大家提供一个解决问题的思路。

3. 开始

3.1 抓包找参数

首先,打开某个网站的登录页面,输入用户名,密码,验证码之类的参数, 抓包看到了下面这个页面: 我实际输入的值全是 1, 然后都被加密了, 没办法,只能去找加密的方法了。 经过一番搜索过后,才发现,原来加密的算法就在源代码里面,这里截个图: 从这里就可以看到具体的算法名以及相关的参数了,你会说,这是什么算法我都不知道啊?搜啊,用关键词搜一下就能知道了。 同时,是不是觉得这个网站好傻逼,这不太简单了吗? 肯定不是!!! 这么简单,说明此处也是必有玄机!!! 至于什么玄机,到后面说,都是泪。

3.2 分析加密流程

首先, 我们知道了公钥以后,解析这个公钥,就可以得到相关的参数,给大家找了示例代码

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
# /usr/bin/python
# encoding: utf-8

import base64

def str2key(s):
# 对字符串解码
b_str = base64.b64decode(s)

if len(b_str) < 162:
return False

hex_str = ''

# 按位转换成16进制
for x in b_str:
h = hex(ord(x))[2:]
h = h.rjust(2, '0')
hex_str += h

# 找到模数和指数的开头结束位置
m_start = 29 * 2
e_start = 159 * 2
m_len = 128 * 2
e_len = 3 * 2

modulus = hex_str[m_start:m_start + m_len]
exponent = hex_str[e_start:e_start + e_len]

return modulus,exponent

if __name__ == "__main__":

pubkey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDC7kw8r6tq43pwApYvkJ5laljaN9BZb21TAIfT/vexbobzH7Q8SUdP5uDPXEBKzOjx2L28y7Xs1d9v3tdPfKI2LR7PAzWBmDMn8riHrDDNpUpJnlAGUqJG9ooPn8j7YNpcxCa1iybOlc2kEhmJn5uwoanQq+CA6agNkqly2H4j6wIDAQAB"
key = str2key(pubkey)
print key

相应的输出

1
2
('c2ee4c3cafab6ae37a7002962f909e656a58da37d0596f6d530087d3fef7b16e86f31fb43c49474fe6e0cf5c404acce8f1d8bdbccbb5ecd5df6fded74f7ca2362d1ecf033581983327f2b887ac30cda54a499e500652a246f68a0f9fc8fb60da5cc426b58b26ce95cda41219899f9bb0a1a9d0abe080e9a80d92a972d87e23eb',
'010001')

从代码中可以看出,解析了公钥之后得到了两个值,一个就是 010001,和我们在网站源代码里面找到的值是一样的。所以,源代码里面的参数我们应该就是可以直接使用的,是不是有种找到组织的赶脚。 接下来,利用下面的代码,来对数据进行加密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import rsa
import binascii
def en_test():
param_1 = "010001"
# 某次我找到的
param_2 = "955120AB9334B7CD52FCDB422DBF564AFD46DEBDC706F33502BBFAD9DD216A22E4D5012CB70F28473B46FB7190D08C31B4B8E76B5112ACE1C5552408961530B1C932DEEA8FC38A9A624AD22073F56F02BF453DD2C1FEA0164106D6B099CC9E5EC88C356FC164FCA47C766DD565D3D11048D27F2DD4221A0B26AB59BD7D09841F"
message = 'nihao'
modulus = int(param_2, 16)
exponent = int(param_1, 16)
rsa_pubkey = rsa.PublicKey(modulus, exponent)
crypto = rsa.encrypt(message, rsa_pubkey)
data = binascii.b2a_hex(crypto)

print data

if __name__ == '__main__':
en_test()

但是当我这样做完,进行模拟登录,还以为自己很牛逼的时候,服务器却给我返回了这样的结果, 目瞪狗呆啊:

1
{"Status":false,"ResultValue":"","StatusCode":"REFRESH","StatusMessage":"请尝试重新登录","RecordCount":0,"Data":null}

可以看到信息提示要刷新,但是当时是百思不得其解,为毛线要刷新? 困惑了一会之后,我再次从头走了一遍流程,这下我才发现,原来源代码里面的那个长长的数据是会改变的,直到这个时候,我才意识到为什么要我刷新。。。。。。 服务器啊,你就不能直接说参数错误吗?刷新你大爷啊。 果然,我还是太年轻啊。

果然,天上掉下的绝不是馅饼,绝逼是个陷阱。

知道这个坑以后就好办了,用个正则匹配一下就行了,而结果也是对的:

1
{"Status":true,"ResultValue":"","StatusCode":"OK","StatusMessage":"成功","RecordCount":0,"Data":{"LoginUrl":"/System/Welcome"}}

4 总结

到这里这篇文章就结束了,这个案例相对于来说很简单,而且为了保护网站的隐私,所以没办法展开说。 有些网站的加密方式是很变态的,比如网易云音乐,知晓常见的加密方法,就可以处理大部分的情况了。 其实,网易云音乐并不是一定要加密, 有想知道非加密的方法的,可以关注我,私聊我。有点敏感,就不写文章了。 反正,我爬了 1000W+的网易云音乐都是不加密的~~ 如果你有类似的问题待解决或者想了解的更清楚的细节的,欢迎关注我的公众号以后,后台私我一下。

Linux

最近和几个朋友开发项目,期间使用了一台服务器跑模型,这台服务器是多人公用的,很多人都在上面有自己的账号,互不干涉内政,一切看起来十分井然有序。近期,这个服务器上刚挂载了一块新硬盘,是一位朋友使用 root 账号挂载的,然后将磁盘映射到某个文件夹下。然而挂载好了之后发现使用普通账号没有权限在文件夹下操作,无法创建文件,于是他干脆就直接把文件夹权限改成 777 了。我心想,这还了得,改成 777 了,其他人在里面乱改咋办?会出人命的!所以,我就这件事详细梳理了一下 Linux 下的用户、用户组、文件权限等基本知识,看完这些,以后不要动不动就把文件夹改成 777 权限了。

基本操作

首选我们梳理一下 Linux 下的用户、用户组、文件权限等基本知识,然后后面通过一个案例来实际演示一下权限设置的一些操作。 首先 Linux 系统中,是有用户和用户组的概念的,用户就是身份的象征,我们必须以某一个用户身份来操作一个系统,实际上这就对应着我们登录系统时的账号。而用户组就是一些用户的集合,我们可以通过用户组来划分和统一管理某些用户。 比如我要在微信发一条朋友圈,我只想给我的亲人们看,难道我发的时候还要一个个去勾选所有的人?这未免太麻烦了。为了解决这问题,微信里面就有了标签的概念,我们可以提前给好友以标签的方式分类,发的时候直接勾选某个标签就好了,简单高效。实际上这就是用户组的概念,我们可以将某些人进行分组和归类,到时候只需要指定类别或组别就可以了,而不用一个个人去对号入座,从而节省了大量时间。 在 Linux 中,一个用户是可以属于多个组的,一个组也是可以包含多个用户的,下面我以一台 Ubuntu Linux 为例来演示一下相关的命令和操作。

用户和用户组

首先查看所有用户,命令如下:

1
cut -d':' -f 1 /etc/passwd

结果:

1
2
3
4
5
6
7
root
daemon
bin
sys
...
ubuntu
mysql

这里一行就是一个用户名,由于太多,部分就省略了,实际上这个命令就是从密码文件中把用户名单独列出来了。 然后查看所有用户组,命令也是类似的:

1
cut -d':' -f 1 /etc/group

结果:

1
2
3
4
5
6
7
root
daemon
bin
sys
...
ubuntu
mysql

结果基本是类似的,因为每个用户在被创建的时候都会自动创建一个同名的组作为其默认的用户组。 这里我是使用 ubuntu 这个账号来登录的,下面我来看下 ubuntu 这个账号是属于哪些组。 查看一个用户所属组的命令格式如下:

1
gorups <username>

这里就是 groups 命令加上用户名就能查看该用户名所属的组了,如果不加用户名的话就默认是当前用户。 例如查看 ubuntu 这个用户所属于的组,命令如下:

1
groups ubuntu

结果:

1
ubuntu : ubuntu adm cdrom sudo dip plugdev lxd lpadmin sambashare

还不少,这个用户被分配到了很多组下,比如同名的组 ubuntu,还有 sudo 组,另外还有一些其他的组。 其中 sudo 组比较特殊,如果被分到了这个组里面就代表该账号拥有 root 权限,可以使用 sudo 命令。 了解了怎样查看用户所属的组,我们也应该反过来了解如何查看一个用户组里面包含哪些用户啊。 查看某个用户组下所有用户命令如下:

1
members <group>

不过这个命令不是自带的,需要额外安装 members 包,命令如下:

1
sudo apt-get install members

例如查看 sudo 用户组下的所有用户,即拥有 root 权限的用户:

1
members sudo

结果:

1
ubuntu hadoop

可以看到拥有 root 权限的用户有两个,ubuntu 和 hadoop,当然不同的机器结果肯定是不一样的。 接下来介绍一个比较有用的命令,就是 id 命令,它可以用来查看用户的所属组别,格式如下:

1
id <username>

例如查看 ubuntu 用户的信息,就是这样:

1
id ubuntu

结果:

1
uid=500(ubuntu) gid=500(ubuntu) groups=500(ubuntu),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd),115(lpadmin),116(sambashare)

这里有一个 gid,作为主工作组,后面还有个 groups,它列出了用户所在的所有组。主工作组只有一个,而后者的数量则不限。可以看到用户组的结果和使用 groups 命令看到的结果是一致的。 接下来我们再来了解一下如何创建一个用户和怎样为用户分配组别。 添加一个用户命令格式如下:

1
sudo adduser <username>

比如我要添加一个用户 cqc,命令就可以这么写:

1
sudo adduser cqc

这里使用的命令前面都带有 sudo,因为毕竟是系统级别的操作。 添加一个组的命令格式如下:

1
sudo groupadd <group>

格式是类似的,后面跟一个组的名称就可以了,例如我要为我的实验室创建一个用户组,那么就可以使用如下命令:

1
sudo groupadd lab

创建完了用户和组,那得把它们关联起来吧,关联的意思就是把某个用户加入到某个组里面,命令格式如下:

1
sudo adduser <username> <group>

或者使用 usermod 命令:

1
sudo usermod -G <group> <username>

如果要添加多个组的话,可以通过 -a 选项指定多个名称:

1
sudo usermod -aG <group1,group2,group3..> <username>

例如我要将 cqc 用户添加到 sudo 用户组中,命令就是:

1
sudo adduser cqc sudo

或:

1
sudo usermod -G sudo cqc

这样就为用户和用户组做好关联了。

文件权限管理

了解了这些之后,我们再来了解一下文件权限的相关知识,下面我们先随便找一个目录,查看一下文件的列表。 列出某个目录下文件详细信息的命令如下:

1
ll

或者使用:

1
ls -l

比如我这里列出了 /etc/nginx 目录下的文件列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
total 80
drwxr-xr-x   7 root root  4096 Jun 21 22:16 ./
drwxr-xr-x 103 root root  4096 Sep  4 18:04 ../
drwxr-xr-x   2 root root  4096 Jul 12  2017 conf.d/
-rw-r--r--   1 root root  1077 Feb 12  2017 fastcgi.conf
-rw-r--r--   1 root root  1007 Feb 12  2017 fastcgi_params
-rw-r--r--   1 root root  2837 Feb 12  2017 koi-utf
-rw-r--r--   1 root root  2223 Feb 12  2017 koi-win
-rw-r--r--   1 root root  3957 Feb 12  2017 mime.types
-rw-r--r--   1 root root  1505 Jun 21 20:24 nginx.conf
-rw-r--r--   1 root root 12288 Jun 21 20:44 .nginx.conf.swp
-rw-r--r--   1 root root   180 Feb 12  2017 proxy_params
-rw-r--r--   1 root root   636 Feb 12  2017 scgi_params
drwxr-xr-x   2 root root  4096 Jun 21 22:42 sites-available/
drwxr-xr-x   2 root root  4096 Jun 21 19:08 sites-enabled/
drwxr-xr-x   2 root root  4096 Jun 21 19:08 snippets/
-rw-r--r--   1 root root   664 Feb 12  2017 uwsgi_params
drwxr-xr-x   2 root root  4096 Jun 22 02:44 vhosts/
-rw-r--r--   1 root root  3071 Feb 12  2017 win-utf

我们注意到了每一行都是一个文件或文件夹的信息,一共包括七列:

  • 第一列是文件的权限信息
  • 第二列表示该文件夹连接的文件数
  • 第三列表示文件所属用户
  • 第四列表示文件所属用户组
  • 第五列表示文件大小(字节)
  • 第六列表示最后修改日期
  • 第七列表示文件名

其中第一列的文件权限信息是非常重要的,它由十个字符组成:

  • 第一个字符代表文件的类型,有三种,- 代表这是一个文件,d 代表这是一个文件夹,l 代表这是一个链接。
  • 第 2-4 个字符代表文件所有者对该文件的权限,r 就是读,w 就是写,x 就是执行,如果是文件夹的话,执行就意味着查看文件夹下的内容,例如 rw- 就代表文件所有者可以对该文件进行读取和写入。
  • 第 5-7 个字符代表文件所属组对该文件的权限,含义是一样的,如 r-x 就代表该文件所属组内的所有用户对该文件有读取和执行的权限。
  • 第 8-10 个字符代表是其他用户对该文件的权限,含义也是一样的,如 r— 就代表非所有者,非用户组的用户只拥有对该文件的读取权限。

我们可以使用 chmod 命令来改变文件或目录的权限,有这么几种用法。 一种是数字权限命名,rwx 对应一个二进制数字,如 101 就代表拥有读取和执行的权限,而转为十进制的话,r 就代表 4,w 就代表 2,x 就代表 1,然后三个数字加起来就和二进制数字对应起来了。如 7=4+2+1,这就对应着 rwx;5=4+1,这就对应着 r-x。所以,相应地 777 就代表了 rwxrwxrwx,即所有者、所属用户组、其他用户对该文件都拥有读取、写入、执行的权限,这是相当危险的! 赋予权限的命令如下:

1
sudo chmod <permission> <file>

例如我要为一个 file.txt 赋予 777 权限,就写成:

1
sudo chmod 777 file.txt

另外我们也可以使用代号来赋予权限,代号有 u、g、o、a 四中,分别代表所有者权限,用户组权限,其他用户权限和所有用户权限,这些代号后面通过 + 和 - 符号来控制权限的添加和移除,再后面跟上权限类型就好,例如:

1
sudo chmod u-x file.txt

就是给所有者移除 x 权限,也就是执行权限。

1
sudo chmod g+w file.txt

就是为用户组添加 w 权限,即写入权限。 另外如果是文件夹的话还可以对文件夹进行递归赋权限操作,如:

1
sudo chmod -R 777 share

就是将 share 文件夹和其内所有内容都赋予 777 权限。 好,有了权限的标识,那我们还得把用户和用户组与文件关联起来啊,这里使用的命令就是 chown 和 chgrp 命令。 命令格式如下:

1
2
sudo chown <username> <file>
sudo chgrp <group> <file>

例如我要将 file.txt 的所有者换成 cqc,那就可以使用如下命令:

1
sudo chown cqc file.txt

如果我要将 file.txt 所属用户组换成 lab,那就可以使用如下命令:

1
sudo chgrp lab file.txt

另外同样可以使用 -R 来进行递归操作,如将 share 文件夹及其内所有内容的所有者都换成 cqc,命令如下:

1
sudo chown -R cqc share/

好,了解了 chown、chgrp、chmod 之后,我们就可以灵活地对文件权限进行控制了。

实战演示

可能上面说起来有点抽象,下面我们以一个实例来演示一下权限控制的流程,通过这个流程,相信理解以上的命令都不在话下了。 首先情况是这样的,我要在某台主机上共享一些文件给我实验室的人,但这台主机上还有其他非实验室的人在使用,我只想让实验室的人查看和修改这些文件,其他人不行。 另外我自己的账号要有最高权限来管理这些文件的共享权限,即要有 root 权限。 现在我已经登录了一个 ubuntu 的账号,是系统初始化的,拥有 root 权限。 下面我就模拟创建三个账号和一个用户组,来得到如下效果:

  • 账号 cqc 是我自己使用的账号,拥有最高权限,可以自由调整文件权限信息,可以自由为某个用户分配用户组。
  • 账号 lbd 是我实验室的人员,没有 root 权限,但能查看和修改我共享的文件。
  • 账号 slb 不是我实验室的人员,没有 root 权限,也不能修改我共享的文件。

创建自己的账户

首先我先为自己创建一个账户,添加一个 cqc 的用户:

1
sudo adduser cqc

运行之后会提示输入密码和其他信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Adding user `cqc' ...
Adding new group `cqc' (1002) ...
Adding new user `cqc' (1002) with group `cqc' ...
Creating home directory `/home/cqc' ...
Copying files from `/etc/skel' ...
Enter new UNIX password: 
Retype new UNIX password: 
passwd: password updated successfully
Changing the user information for cqc
Enter the new value, or press ENTER for the default
        Full Name []: 
        Room Number []: 
        Work Phone []: 
        Home Phone []: 
        Other []: 
Is the information correct? [Y/n]

这时候发现一个同名的组就被创建了,查看下 cqc 所在的组:

1
groups cqc

结果如下:

1
cqc : cqc

再用 id 命令查看下信息:

1
id cqc

结果如下:

1
uid=1002(cqc) gid=1002(cqc) groups=1002(cqc)

可以看到当前 cqc 只属于 cqc 用户组。 接下来我们创建一个用户组,叫做 lab,来标明我的实验室,命令如下:

1
sudo groupadd lab

然后查看下用户组里面的成员:

1
members lab

没有任何结果,说明我们创建了一个空的组,没有任何成员。 然后我们将刚才创建的 cqc 加入到该组中,因为我自己也属于该实验室,肯定也要加进来,命令如下:

1
sudo adduser cqc lab

结果:

1
2
3
Adding user `cqc' to group `lab' ...
Adding user cqc to group lab
Done.

然后查看下组内成员:

1
members lab

结果:

1
cqc

这样 lab 组内就有了 cqc 这个用户了。 别忘了 cqc 还需要拥有 root 权限,所以我们还需要将 cqc 添加到 sudo 组内,命令如下:

1
sudo adduser cqc sudo

结果:

1
2
3
Adding user `cqc' to group `sudo' ...
Adding user cqc to group sudo
Done.

这样就成功加入到 sudo 组了,cqc 也就是我的账户就可以使用 sudo 命令了。 查看下用户状态:

1
id cqc

结果如下:

1
uid=1002(cqc) gid=1002(cqc) groups=1002(cqc),27(sudo),1003(lab)

这样 cqc 就属于三个用户组了,既是实验室成员,又拥有 root 权限。 上面的分配用户组的命令我们也可以使用 usermod 来实现:

1
sudo usermod -aG sudo,lab cqc

这样就添加到多个组了。

添加实验室用户

接下来,再添加实验室的另外一个人员 lbd,然后将其添加到 lab 组中,流程是类似的,命令如下:

1
2
sudo adduser lbd
sudo adduser lbd lab

运行完毕之后,id 命令查看其信息:

1
id lbd

结果如下:

1
uid=1004(lbd) gid=1005(lbd) groups=1005(lbd),1003(lab)

这样就成功创建 lbd,并将其添加到实验室 lab 组了。

添加非实验室用户

最后另外添加一个用户 slb,非实验室成员,只创建账户就好了,命令如下:

1
sudo adduser slb

但是我们不把他加入 lab 组中。 查看他的状态:

1
id slb

结果如下:

1
uid=1003(slb) gid=1004(slb) groups=1004(slb)

所以三人的状态是这样的:

1
2
3
4
5
6
id cqc
uid=1002(cqc) gid=1002(cqc) groups=1002(cqc),27(sudo),1003(lab)
id lbd
uid=1004(lbd) gid=1005(lbd) groups=1005(lbd),1003(lab)
id slb
uid=1003(slb) gid=1004(slb) groups=1004(slb)

文件权限分配

接下来我们创建一个文件夹来共享实验室数据,放在 /srv 目录下。然后调用 mkdir 命令创建名称为 share 的文件夹,命令如下:

1
2
cd /srv
sudo mkdir share

注意这里我还是使用 ubuntu 账户来创建的。 先看下当前目录权限:

1
ls -l

结果如下:

1
2
3
4
total 12
drwxr-xr-x  3 root root 4096 Sep  4 18:17 ./
drwxr-xr-x 24 root root 4096 Sep  4 18:17 ../
drwxr-xr-x  2 root root 4096 Sep  4 18:17 share/

可以看到 share 文件的所有者是 root,用户组也是 root,权限是 755,即只有 root 拥有修改权限,其他的只有读取和执行权限。 然后进入 share 文件夹创建一个 names.txt:

1
2
cd share
sudo vi names.txt

编辑内容如下:

1
2
cqc
lbd

保存完毕之后,这时查看一下文件权限,如下:

1
-rw-r----- 1 root root    8 Sep  4 20:00 names.txt

权限是 640,这表明只有所有者 root 拥有写入的权限,所在组只有读的权限。 这时开启另外一个终端,登录 cqc 账号,实际上是不能查看和修改任何该文件的内容的,下面的修改和读取命令都会提示权限不够:

1
2
vi names.txt
cat names.txt

为什么呢?因为该文件是刚才由 ubuntu 账号使用 sudo 命令创建的,因此文件的所有者是 root,并不是 cqc,因此即使文件的权限是 640,那也就不能使用文件所有者的权限,而且 cqc 也不属于 root 组,所以也不能使用文件组的权限了,因此什么都看不了,什么都改不了。 但 cqc 属于 sudo 组啊,可以利用 sudo 命令临时获取 root 权限,临时以 root 的身份来操作该文件,这样就可以来查看和修改文件了,因此下面的命令是有效的:

1
2
sudo vi names.txt
sudo cat names.txt

但这样还是需要使用 sudo 才能修改,很不方便。 这时如果我们把文件的所有者改成 cqc,情况那就不一样了。 使用 ubuntu 账号,对 names.txt 更改其所有者为 cqc,改的命令如下:

1
sudo chown cqc names.txt

这时查看下文件信息:

1
-rw-r----- 1 cqc  root    8 Sep  4 20:29 names.txt

可以看到所有者信息已经变成了 cqc,这样 cqc 账号再直接查看和修改,那就可以了,不再需要 sudo 命令:

1
2
vi names.txt
cat names.txt

这样就不会有权限提示,当然加上 sudo 更是没问题。 好,接下来 lbd 呢?我们登录试试修改。 首先当前的文件状态是这样的:

1
-rw-r----- 1 cqc  root    8 Sep  4 20:31 names.txt

lbd 不是所有者了,因此前面的 rw- 权限是没什么用的,但他属于 lab 组,而该文件对于用户组的权限是 r—,也就是读取权限。 我们使用 lbd 账号来尝试看下文件的内容:

1
2
cat names.txt 
cat: names.txt: Permission denied

很遗憾,又没有权限。因为什么?因为这个文件的用户组并不是 lab 啊,而 lbd 这个用户又不属于 root 组,所以没有任何权限。 那咋办?将文件的用户组改成 lab 就好了,使用 ubuntu 账号或 cqc 账号来操作:

1
sudo chgrp lab names.txt

这样就成功将文件所属用户组改成 lab 了,接下来再使用 lbd 账号查看下文件内容:

1
cat names.txt

就成功读取了。 然而 lbd 现在是没有写入权限的,因为对于用户组来说,该文件的权限是 r—,如果要获取写入权限,我们可以使用如下命令:

1
sudo chmod g+w names.txt

或:

1
sudo chmod 660 names.txt

这样就相当于赋予了 rw- 权限,下面我们再使用 lbd 账号尝试修改这个文件:

1
vi names.txt

就没问题了。 那么对于非实验室同学 slb 呢?它没有任何权限,我们登录 slb 账号尝试修改和读取该文件:

1
2
cat names.txt
vi names.txt

均无权限。 所以说,这样我们就成功为实验室的人员赋予了权限,而非实验室的人则没有任何权限。 如果我要为 slb 赋予读取权限咋办呢?很简单,添加一下就好了:

1
sudo chmod o+r names.txt

这就是为其他用户添加了读取权限。这时 slb 就可以读取文件,但不能修改文件,也是比较安全的。 好,如果我的文件非常多呢?比如十几二十个,都放在 share 文件夹内,那不能一个个进行权限设置吧? 这时候我们只需要针对文件夹进行操作即可,下面的命令就可以为 share 文件夹赋予 775 权限,即所有者 cqc 和所在组 lab 可对其进行查看和修改,其他的人只能看不能改:

1
2
3
sudo chmod -R 775 share/
sudo chown -R cqc share/
sudo chgrp -R lab share/

注意文件夹一般都会赋予 x 权限,不然连进入文件夹的权限都没有。这也就是文件夹一般会赋予 775、755,而文件会赋予 664、600、644、640 的原因了。 赋予 775 权限之后,share 的权限就变成了:

1
drwxrwxr-x  2 cqc  lab  4096 Sep  4 20:31 share/

这样其他用户就只能看,不能改,这样普通文件就没什么问题了。 如文件夹内包含了可执行文件,还可以单独为其他用户针对可执行文件去除 x 权限,如去除 Python 文件的可执行权限:

1
sudo chmod o-x *.py

好了,到现在为止,我们就得心应手地完成了权限控制了! 相信如果你耐心看完的话,什么用户管理、权限管理,都不在话下!

Linux

本文介绍一下如何给 Azure 的云服务器增加一块磁盘。

页面操作

首先切换到磁盘页面,然后点击添加数据磁盘按钮: 然后选定存储容器,这里使用的是存储账户 Blob,然后点击确定按钮: 主机缓存切换为“读/写”,然后点击保存: 这样就添加好了。

挂载磁盘

接下来回到 Linux 服务器下,我们需要将磁盘进行挂载。 首先 SSH 连接到服务器,然后使用 dmesg 命令来查找磁盘:

1
dmesg | grep SCSI

输出类似如下:

1
2
3
4
5
6
[    0.728389] SCSI subsystem initialized
[ 2.139341] Block layer SCSI generic (bsg) driver version 0.4 loaded (major 244)
[ 2.978928] sd 1:0:1:0: [sdb] Attached SCSI disk
[ 3.341183] sd 0:0:0:0: [sda] Attached SCSI disk
[ 18.397942] Loading iSCSI transport class v2.0-870.
[ 6641.364794] sd 3:0:0:0: [sdc] Attached SCSI disk

这里 sdc 就是我们新添加的一块硬盘。 然后我们使用 fdisk 对其进行分区,将其设置为分区 1 中的主磁盘,并接受其他的默认值,命令如下:

1
sudo fdisk /dev/sdc

使用 n 命令添加新分区,然后 p 选择主分区,其他的默认:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Welcome to fdisk (util-linux 2.27.1).
Changes will remain in memory only, until you decide to write them.
Be careful before using the write command.


Device does not contain a recognized partition table.
Created a new DOS disklabel with disk identifier 0xc305fe54.

Command (m for help): n
Partition type
p primary (0 primary, 0 extended, 4 free)
e extended (container for logical partitions)
Select (default p): p
Partition number (1-4, default 1):
First sector (2048-2145386495, default 2048):
Last sector, +sectors or +size{K,M,G,T,P} (2048-2145386495, default 2145386495):

Created a new partition 1 of type 'Linux' and of size 1023 GiB.

然后使用 p 打印分区表并使用 w 将表写入磁盘,然后退出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Command (m for help): p
Disk /dev/sdc: 1023 GiB, 1098437885952 bytes, 2145386496 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0xc305fe54

Device Boot Start End Sectors Size Id Type
/dev/sdc1 2048 2145386495 2145384448 1023G 83 Linux

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Syncing disks.

接下来使用 mkfs 命令将文件系统写入分区,指定文件系统的类型和设备名称:

1
sudo mkfs -t ext4 /dev/sdc1

输出类似如下:

1
2
3
4
5
6
7
8
9
10
11
12
mke2fs 1.42.13 (17-May-2015)
Creating filesystem with 268173056 4k blocks and 67043328 inodes
Filesystem UUID: d744c5d7-f4d1-4f81-9f56-59dfab956782
Superblock backups stored on blocks:
32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208,
4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968,
102400000, 214990848

Allocating group tables: done
Writing inode tables: done
Creating journal (32768 blocks): done
Writing superblocks and filesystem accounting information: done

然后使用 mkdir 创建一个目录来装载该文件系统,然后挂载:

1
2
sudo mkdir /datadrive
sudo mount /dev/sdc1 /datadrive

这样就挂载成功了。

添加引导信息

若要确保在重新引导后自动重新装载驱动器,必须将其添加到 /etc/fstab 文件。 此外,强烈建议在 /etc/fstab 中使用 UUID(全局唯一标识符)来引用驱动器而不是只使用设备名称(例如 /dev/sdc1)。 如果 OS 在启动过程中检测到磁盘错误,使用 UUID 可以避免将错误的磁盘装载到给定位置。 然后,为剩余的数据磁盘分配这些设备 ID。 若要查找新驱动器的 UUID,请使用 blkid 实用工具:

1
sudo -i blkid

输入类似如下:

1
2
3
/dev/sdb1: UUID="d5b61f40-4129-4b39-b861-c2d3b09cee69" TYPE="ext4" PARTUUID="4927b944-01"
/dev/sda1: LABEL="cloudimg-rootfs" UUID="b2e62f4f-d338-470e-9ae7-4fc0e014858c" TYPE="ext4" PARTUUID="577c3e7c-01"
/dev/sdc1: UUID="d744c5d7-f4d1-4f81-9f56-59dfab956782" TYPE="ext4" PARTUUID="c305fe54-01"

然后编辑 /etc/fstab,添加下面一行:

1
UUID=d744c5d7-f4d1-4f81-9f56-59dfab956782       /datadrive      ext4    defaults,nofail 1      2

然后保存退出即可。 这样就成功添加了一块外部磁盘。

Other

为何要搭建 Elasticsearch 集群

凡事都要讲究个为什么。在搭建集群之前,我们首先先问一句,为什么我们需要搭建集群?它有什么优势呢?

高可用性

Elasticsearch 作为一个搜索引擎,我们对它的基本要求就是存储海量数据并且可以在非常短的时间内查询到我们想要的信息。所以第一步我们需要保证的就是 Elasticsearch 的高可用性,什么是高可用性呢?它通常是指,通过设计减少系统不能提供服务的时间。假设系统一直能够提供服务,我们说系统的可用性是 100%。如果系统在某个时刻宕掉了,比如某个网站在某个时间挂掉了,那么就可以它临时是不可用的。所以,为了保证 Elasticsearch 的高可用性,我们就应该尽量减少 Elasticsearch 的不可用时间。 那么怎样提高 Elasticsearch 的高可用性呢?这时集群的作用就体现出来了。假如 Elasticsearch 只放在一台服务器上,即单机运行,假如这台主机突然断网了或者被攻击了,那么整个 Elasticsearch 的服务就不可用了。但如果改成 Elasticsearch 集群的话,有一台主机宕机了,还有其他的主机可以支撑,这样就仍然可以保证服务是可用的。 那可能有的小伙伴就会说了,那假如一台主机宕机了,那么不就无法访问这台主机的数据了吗?那假如我要访问的数据正好存在这台主机上,那不就获取不到了吗?难道其他的主机里面也存了一份一模一样的数据?那这岂不是很浪费吗? 为了解答这个问题,这里就引出了 Elasticsearch 的信息存储机制了。首先解答上面的问题,一台主机宕机了,这台主机里面存的数据依然是可以被访问到的,因为在其他的主机上也有备份,但备份的时候也不是整台主机备份,是分片备份的,那这里就又引出了一个概念——分片。 分片,英文叫做 Shard,顾名思义,分片就是对数据切分成了多个部分。我们知道 Elasticsearch 中一个索引(Index)相当于是一个数据库,如存某网站的用户信息,我们就建一个名为 user 的索引。但索引存储的时候并不是整个存一起的,它是被分片存储的,Elasticsearch 默认会把一个索引分成五个分片,当然这个数字是可以自定义的。分片是数据的容器,数据保存在分片内,分片又被分配到集群内的各个节点里。当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里,所以相当于一份数据被分成了多份并保存在不同的主机上。 那这还是没解决问题啊,如果一台主机挂掉了,那么这个分片里面的数据不就无法访问了?别的主机都是存储的其他的分片。其实是可以访问的,因为其他主机存储了这个分片的备份,叫做副本,这里就引出了另外一个概念——副本。 副本,英文叫做 Replica,同样顾名思义,副本就是对原分片的复制,和原分片的内容是一样的,Elasticsearch 默认会生成一份副本,所以相当于是五个原分片和五个分片副本,相当于一份数据存了两份,并分了十个分片,当然副本的数量也是可以自定义的。这时我们只需要将某个分片的副本存在另外一台主机上,这样当某台主机宕机了,我们依然还可以从另外一台主机的副本中找到对应的数据。所以从外部来看,数据结果是没有任何区别的。 一般来说,Elasticsearch 会尽量把一个索引的不同分片存储在不同的主机上,分片的副本也尽可能存在不同的主机上,这样可以提高容错率,从而提高高可用性。 但这时假如你只有一台主机,那不就没办法了吗?分片和副本其实是没意义的,一台主机挂掉了,就全挂掉了。

健康状态

针对一个索引,Elasticsearch 中其实有专门的衡量索引健康状况的标志,分为三个等级:

  • green,绿色。这代表所有的主分片和副本分片都已分配。你的集群是 100% 可用的。
  • yellow,黄色。所有的主分片已经分片了,但至少还有一个副本是缺失的。不会有数据丢失,所以搜索结果依然是完整的。不过,你的高可用性在某种程度上被弱化。如果更多的分片消失,你就会丢数据了。所以可把 yellow 想象成一个需要及时调查的警告。
  • red,红色。至少一个主分片以及它的全部副本都在缺失中。这意味着你在缺少数据:搜索只能返回部分数据,而分配到这个分片上的写入请求会返回一个异常。

如果你只有一台主机的话,其实索引的健康状况也是 yellow,因为一台主机,集群没有其他的主机可以防止副本,所以说,这就是一个不健康的状态,因此集群也是十分有必要的。

存储空间

另外,既然是群集,那么存储空间肯定也是联合起来的,假如一台主机的存储空间是固定的,那么集群它相对于单个主机也有更多的存储空间,可存储的数据量也更大。 所以综上所述,我们需要一个集群!

详细了解 Elasticsearch 集群

接下来我们再来了解下集群的结构是怎样的。 首先我们应该清楚多台主机构成了一个集群,每台主机称作一个节点(Node)。 如图就是一个三节点的集群:

在图中,每个 Node 都有三个分片,其中 P 开头的代表 Primary 分片,即主分片,R 开头的代表 Replica 分片,即副本分片。所以图中主分片 1、2,副本分片 0 储存在 1 号节点,副本分片 0、1、2 储存在 2 号节点,主分片 0 和副本分片 1、2 储存在 3 号节点,一共是 3 个主分片和 6 个副本分片。同时我们还注意到 1 号节点还有个 MASTER 的标识,这代表它是一个主节点,它相比其他的节点更加特殊,它有权限控制整个集群,比如资源的分配、节点的修改等等。 这里就引出了一个概念就是节点的类型,我们可以将节点分为这么四个类型:

  • 主节点:即 Master 节点。主节点的主要职责是和集群操作相关的内容,如创建或删除索引,跟踪哪些节点是群集的一部分,并决定哪些分片分配给相关的节点。稳定的主节点对集群的健康是非常重要的。默认情况下任何一个集群中的节点都有可能被选为主节点。索引数据和搜索查询等操作会占用大量的 cpu,内存,io 资源,为了确保一个集群的稳定,分离主节点和数据节点是一个比较好的选择。虽然主节点也可以协调节点,路由搜索和从客户端新增数据到数据节点,但最好不要使用这些专用的主节点。一个重要的原则是,尽可能做尽量少的工作。
  • 数据节点:即 Data 节点。数据节点主要是存储索引数据的节点,主要对文档进行增删改查操作,聚合操作等。数据节点对 CPU、内存、IO 要求较高,在优化的时候需要监控数据节点的状态,当资源不够的时候,需要在集群中添加新的节点。
  • 负载均衡节点:也称作 Client 节点,也称作客户端节点。当一个节点既不配置为主节点,也不配置为数据节点时,该节点只能处理路由请求,处理搜索,分发索引操作等,从本质上来说该客户节点表现为智能负载平衡器。独立的客户端节点在一个比较大的集群中是非常有用的,他协调主节点和数据节点,客户端节点加入集群可以得到集群的状态,根据集群的状态可以直接路由请求。
  • 预处理节点:也称作 Ingest 节点,在索引数据之前可以先对数据做预处理操作,所有节点其实默认都是支持 Ingest 操作的,也可以专门将某个节点配置为 Ingest 节点。

以上就是节点几种类型,一个节点其实可以对应不同的类型,如一个节点可以同时成为主节点和数据节点和预处理节点,但如果一个节点既不是主节点也不是数据节点,那么它就是负载均衡节点。具体的类型可以通过具体的配置文件来设置。

怎样搭建 Elasticsearch 集群

好,接下来我们就来动手搭建一个集群吧。 这里我一共拥有七台 Linux 主机,系统是 Ubuntu 16.04,都连接在一个内网中,IP 地址为:

1
2
3
4
5
6
7
10.0.0.4
10.0.0.5
10.0.0.6
10.0.0.7
10.0.0.8
10.0.0.9
10.0.0.10

每台主机的存储空间是 1TB,内存是 13GB。 下面我们来一步步介绍如何用这几台主机搭建一个 Elasticsearch 集群,这里使用的 Elasticsearch 版本是 6.3.2,另外我们还需要安装 Kibana 用来可视化监控和管理 Elasticsearch 的相关配置和数据,使得集群的管理更加方便。 环境配置如下所示:

名称

内容

主机台数

7

主机内存

13GB

主机系统

Ubuntu 16.04

存储空间

1TB

Elasticsearch 版本

6.3.2

Java 版本

1.8

Kibana 版本

6.3.2

安装 Java

Elasticsearch 是基于 Lucene 的,而 Lucene 又是基于 Java 的。所以第一步我们就需要在每台主机上安装 Java。 首先更新 Apt 源:

1
sudo apt-get update

然后安装 Java:

1
sudo apt-get install default-jre

安装好了之后可以检查下 Java 的版本:

1
java -version

这里的版本是 1.8,类似输出如下:

1
2
3
openjdk version "1.8.0_171"
OpenJDK Runtime Environment (build 1.8.0_171-8u171-b11-0ubuntu0.16.04.1-b11)
OpenJDK 64-Bit Server VM (build 25.171-b11, mixed mode)

如果看到上面的内容就说明安装成功了。 注意一定要每台主机都要安装。

安装 Elasticsearch

接下来我们来安装 Elasticsearch,同样是每台主机都需要安装。 首先需要添加 Apt-key:

1
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -

然后添加 Elasticsearch 的 Repository 定义:

1
echo "deb https://artifacts.elastic.co/packages/6.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-6.x.list

接下来安装 Elasticsearch 即可:

1
2
sudo apt-get update 
sudo apt-get install elasticsearch

运行完毕之后我们就完成了 Elasticsearch 的安装,注意还是要每台主机都要安装。

配置 Elasticsearch

这时我们只是每台主机都安装好了 Elasticsearch,接下来我们还需要将它们联系在一起构成一个集群。 安装完之后,Elasticsearch 的配置文件是 /etc/elasticsearch/elasticsearch.yml,接下来让我们编辑一下配置文件:

  • 集群的名称

通过 cluster.name 可以配置集群的名称,集群是一个整体,因此名称都要一致,所有主机都配置成相同的名称,配置示例:

1
cluster.name: germey-es-clusters
  • 节点的名称

通过 node.name 可以配置每个节点的名称,每个节点都是集群的一部分,每个节点名称都不要相同,可以按照顺序编号,配置示例:

1
node.name: es-node-1

其他的主机可以配置为 es-node-2、es-node-3 等。

  • 是否有资格成为主节点

通过 node.master 可以配置该节点是否有资格成为主节点,如果配置为 true,则主机有资格成为主节点,配置为 false 则主机就不会成为主节点,可以去当数据节点或负载均衡节点。注意这里是有资格成为主节点,不是一定会成为主节点,主节点需要集群经过选举产生。这里我配置所有主机都可以成为主节点,因此都配置为 true,配置示例: node.master: true

  • 是否是数据节点

通过 node.data 可以配置该节点是否为数据节点,如果配置为 true,则主机就会作为数据节点,注意主节点也可以作为数据节点,当 node.master 和 node.data 均为 false,则该主机会作为负载均衡节点。这里我配置所有主机都是数据节点,因此都配置为 true,配置示例: node.data: true

  • 数据和日志路径

通过 path.data 和 path.logs 可以配置 Elasticsearch 的数据存储路径和日志存储路径,可以指定任意位置,这里我指定存储到 1T 硬盘对应的路径下,另外注意一下写入权限问题,配置示例: path.data: /datadrive/elasticsearch/data path.logs: /datadrive/elasticsearch/logs

  • ​ 设置访问的地址和端口

我们需要设定 Elasticsearch 运行绑定的 Host,默认是无法公开访问的,如果设置为主机的公网 IP 或 0.0.0.0 就是可以公开访问的,这里我们可以都设置为公开访问或者部分主机公开访问,如果是公开访问就配置为:

1
network.host: 0.0.0.0

如果不想被公开访问就不用配置。 另外还可以配置访问的端口,默认是 9200: http.port: 9200

  • 集群地址设置

通过 discovery.zen.ping.unicast.hosts 可以配置集群的主机地址,配置之后集群的主机之间可以自动发现,这里我配置的是内网地址,配置示例:

1
discovery.zen.ping.unicast.hosts["10.0.0.4""10.0.0.5""10.0.0.6""10.0.0.7""10.0.0.8""10.0.0.9""10.0.0.10"]

这里请改成你的主机对应的 IP 地址。

  • 节点数目相关配置

为了防止集群发生“脑裂”,即一个集群分裂成多个,通常需要配置集群最少主节点数目,通常为 (可成为主节点的主机数目 / 2) + 1,例如我这边可以成为主节点的主机数目为 7,那么结果就是 4,配置示例:

1
discovery.zen.minimum_master_nodes: 4

另外还可以配置当最少几个节点回复之后,集群就正常工作,这里我设置为 4,可以酌情修改,配置示例:

1
gateway.recover_after_nodes: 4

其他的暂时先不需要配置,保存即可。注意每台主机都需要配置。

启动 Elasticsearch

配置完成之后就可以在每台主机上分别启动 Elasticsearch 服务了,命令如下:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable elasticsearch.service
sudo systemctl start elasticsearch.service

所有主机都启动之后,我们在任意主机上就可以查看到集群状态了,命令行如下:

1
curl -XGET 'http://localhost:9200/_cluster/state?pretty'

类似的输出如下:

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
{
    "cluster_name""germey-es-clusters",
    "compressed_size_in_bytes"20799,
    "version"658,
    "state_uuid""a64wCwPnSueKRtVuKx8xRw",
    "master_node""73BQvOC2TpSXcr-IXBcDdg",
    "blocks": {},
    "nodes": {
        "I2M80AP-T7yVP_AZPA0bpA": {
            "name""es-node-1",
            "ephemeral_id""KpCG4jNvTUGKNHNwKKoMrA",
            "transport_address""10.0.0.4:9300",
            "attributes": {
                "ml.machine_memory""7308464128",
                "ml.max_open_jobs""20",
                "xpack.installed""true",
                "ml.enabled""true"
            }
        },
        "73BQvOC2TpSXcr-IXBcDdg": {
            "name""es-node-7",
            "ephemeral_id""Fs9v2XTASnGbqrM8g7IhAQ",
            "transport_address""10.0.0.10:9300",
            "attributes": {
                "ml.machine_memory""14695202816",
                "ml.max_open_jobs""20",
                "xpack.installed""true",
                "ml.enabled""true"
            }
        },
....

可以看到这里输出了集群的相关信息,同时 nodes 字段里面包含了每个节点的详细信息,这样一个基本的集群就构建完成了。

安装 Kibana

接下来我们需要安装一个 Kibana 来帮助可视化管理 Elasticsearch,依然还是通过 Apt 安装,只需要任意一台主机安装即可,因为集群是一体的,所以 Kibana 在任意一台主机只要能连接到 Elasticsearch 即可,安装命令如下:

1
sudo apt-get install kibana

安装之后修改 /etc/kibana/kibana.yml,设置公开访问和绑定的端口:

1
2
server.port: 5601
server.host: "0.0.0.0"

然后启动服务:

1
2
3
sudo systemctl daemon-reload
sudo systemctl enable kibana.service
sudo systemctl start kibana.service

这样我们可以在浏览器输入该台主机的 IP 加端口,查看 Kibana 管理页面了,类似如下:

这样 Kibana 可视化管理就配置成功了。

配置认证

现在集群已经初步搭建完成了,但是现在集群很危险,如果我们配置了可公网访问,那么它是可以被任何人操作的,比如储存数据,增删节点等,这是非常危险的,所以我们必须要设置访问权限。 在 Elasticsearch 中,配置认证是通过 X-Pack 插件实现的,幸运的是,我们不需要额外安装了,在 Elasticsearch 6.3.2 版本中,该插件是默认集成到 Elasticsearch 中的,所以我们只需要更改一部分设置就可以了。 首先我们需要升级 License,只有修改了高级版 License 才能使用 X-Pack 的权限认证功能。 在 Kibana 中访问 Management -> Elasticsearch -> License Management,点击右侧的升级 License 按钮,可以免费试用 30 天的高级 License,升级完成之后页面会显示如下:

另外还可以使用 API 来更新 License,详情可以参考官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/6.2/update-license.html。 然后每台主机需要修改 /etc/elasticsearch/elasticsearch.yml 文件,开启 Auth 认证功能:

1
xpack.security.enabledtrue

随后设置 elastic、kibana、logstash_system 三个用户的密码,任意一台主机修改之后,一台修改,多台生效,命令如下:

1
/usr/share/elasticsearch/bin/elasticsearch-setup-passwords interactive

运行之后会依次提示设置这三个用户的密码并确认,一共需要输入六次密码,完成之后就成功设置好了密码了。 修改完成之后重启 Elasticsearch 和 Kibana 服务:

1
2
sudo systemctl restart elasticsearch.service
sudo systemctl restart kibana.service

这时再访问 Kibana 就会跳转到登录页面了:

可以使用 elastic 用户登录,它的角色是超级管理员,登录之后就可以重新进入 Kibana 的管理页面。 我们还可以自行修改和添加账户,在 Management -> Security -> User/Roles 里面:

例如这里添加一个超级管理员的账户:

这样以后我们就可以使用新添加的用户来登录和访问了。 另外修改权限认证之后,Elasticsearch 也不能直接访问了,我们也必须输入用户密码才可以访问和调用其 API,保证了安全性。

开启内存锁定

系统默认会进行内存交换,这样会导致 Elasticsearch 的性能变差,我们查看下内存锁定状态,在任意一台主机上的访问 http://ip:port/\_nodes?filter_path=\*\*.mlockall: 可以看到如下结果:

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
{
    "nodes": {
        "73BQvOC2TpSXcr-IXBcDdg": {
            "process": {
                "mlockall"false
            }
        },
        "9tRr4nFDT_2rErLLQB2dIQ": {
            "process": {
                "mlockall"false
            }
        },
        "hskSDv_JQlCUnjp_INI8Kg": {
            "process": {
                "mlockall"false
            }
        },
        "LgaRuqXBTZaBdDGAktFWJA": {
            "process": {
                "mlockall"false
            }
        },
        "ZcsZgowERzuvpqVbYOgOEA": {
            "process": {
                "mlockall"false
            }
        },
        "I2M80AP-T7yVP_AZPA0bpA": {
            "process": {
                "mlockall"false
            }
        },
        "_mSmfhUtQiqhzTKZ7u75Dw": {
            "process": {
                "mlockall"true
            }
        }
    }
}

这代表内存交换没有开启,会影响 Elasticsearch 的性能,所以我们需要开启内存物理地址锁定,每台主机需要修改 /etc/elasticsearch/elasticsearch.yml 文件,修改如下配置:

1
bootstrap.memory_lock: true

但这样修改之后重新启动是会报错的,Elasticsearch 无法正常启动,查看日志,报错如下:

1
2
[1] bootstrap checks failed
[1]: memory locking requested for elasticsearch process but memory is not locked

这里需要修改两个地方,第一个是 /etc/security/limits.conf,添加如下内容:

1
2
3
4
5
6
soft nofile 65536
hard nofile 65536
soft nproc 32000
hard nproc 32000
hard memlock unlimited
soft memlock unlimited

另外还需要修改 /etc/systemd/system.conf,修改如下内容:

1
2
3
DefaultLimitNOFILE=65536
DefaultLimitNPROC=32000
DefaultLimitMEMLOCK=infinity

详细的解释可以参考:https://segmentfault.com/a/1190000014891856。 修改之后重启 Elasticsearch 服务:

1
sudo systemctl restart elasticsearch.service

重新访问刚才的地址,即可发现每台主机的物理地址锁定都被打开了:

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
{
    "nodes": {
        "73BQvOC2TpSXcr-IXBcDdg": {
            "process": {
                "mlockall"true
            }
        },
        "9tRr4nFDT_2rErLLQB2dIQ": {
            "process": {
                "mlockall"true
            }
        },
        "hskSDv_JQlCUnjp_INI8Kg": {
            "process": {
                "mlockall"true
            }
        },
        "LgaRuqXBTZaBdDGAktFWJA": {
            "process": {
                "mlockall"true
            }
        },
        "ZcsZgowERzuvpqVbYOgOEA": {
            "process": {
                "mlockall"true
            }
        },
        "I2M80AP-T7yVP_AZPA0bpA": {
            "process": {
                "mlockall"true
            }
        },
        "_mSmfhUtQiqhzTKZ7u75Dw": {
            "process": {
                "mlockall"true
            }
        }
    }
}

这样我们就又解决了性能的问题。

安装分词插件

另外还推荐安装中文分词插件,这样可以对中文进行全文索引,安装命令如下:

1
sudo /usr/share/elasticsearch/bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.3.2/elasticsearch-analysis-ik-6.3.2.zip

安装完之后需要重启 Elasticsearch 服务:

1
sudo systemctl restart elasticsearch.service

主机监控

到此为止,我们的 Elasticsearch 集群就搭建完成了。 最后我们看下 Kibana 的部分功能,看下整个 Elasticsearch 有没有在正常工作。 访问 Kibana,打开 Management -> Elasticsearch ->Index Management,即可看到当前有的一些索引和状态:

打开 Monitoring,可以查看 Elasticsearch 和 Kibana 的状态:

进一步点击 Nodes,可以查看各个节点的状态:

打开任意节点,可以查看当前资源状况变化:

另外还有一些其他的功能如可视化、图表、搜索等等,这里就不再一一列举了,更多功能可以详细了解 Kibana。 以上都是自己在安装过程中趟过的坑,如有疏漏,还望指正。 还有更多的 Elasticsearch 相关的内容可以参考官方文档:https://www.elastic.co/guide/index.html

参考资料

Python

大家好,我是四毛,下面是我的个人公众号,欢迎关注。有问题的可以私信我,看到就会回复。

更新 2018 年 08 月 03 日 14:39:32

其实可以利用 scrapy 的扩展展示更多的数据,立个 flag,后面更新上来

好,开始今天的文章。 今天主要是来说一下怎么可视化来监控你的爬虫的状态。 相信大家在跑爬虫的过程中,也会好奇自己养的爬虫一分钟可以爬多少页面多大的数据量,当然查询的方式多种多样。今天我来讲一种可视化的方法。

关于爬虫数据在 mongodb 里的版本我写了一个可以热更新配置的版本,即添加了新的爬虫配置以后,不用重启程序,即可获取刚刚添加的爬虫的状态数据,大家可以通过关注我的公众号以后, 回复“可视化”即可获取脚本地址

1.成品图

这个是监控服务器网速的最后成果,显示的是下载与上传的网速,单位为 M。爬虫的原理都是一样的,只不过将数据存到 InfluxDB 的方式不一样而已, 如下图。

可以实现对爬虫数量,增量,大小,大小增量的实时监控。

2. 环境

  • InfluxDb,是目前比较流行的时间序列数据库;
  • Grafana,一个可视化面板(Dashboard),有着非常漂亮的图表和布局展示,功能齐全的度量仪表盘和图形编辑器,支持 Graphite、zabbix、InfluxDB、Prometheus 和 OpenTSDB 作为数据源
  • Ubuntu
  • influxdb(pip install influxdb)

  • Python 2.7

3. 原理

获取要展示的数据,包含当前的时间数据,存到 InfluxDb 里面,然后再到 Grafana 里面进行相应的配置即可展示;

4. 安装

4.1 Grafana 安装

官方安装指导 安装好以后,打开本地的 3000 端口,即可进入管理界面,用户名与密码都是admin

4.2 InfulxDb 安装

这个安装就网上自己找吧,有很多的配置我都没有配置,就不在这里误人子弟了。

5. InfluxDb 简单操作

碰到了数据库,肯定要把增删改查学会了啊, 和 sql 几乎一样,只有一丝丝的区别,具体操作,大家可以参考官方的文档。

  • influx 进入命令行
  • CREATE DATABASE test 创建数据库
  • show databases 查看数据库
  • use test 使用数据库
  • show series 看表
  • select * from table_test 选择数据
  • DROP MEASUREMENT table_test 删表

6. 存数据

InfluxDb 数据库的数据有一定的格式,因为我都是利用 python 库进行相关操作,所以下面将在 python 中的格式展示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
json_body = [
{
"measurement": "crawler",
"time": current_time,
"tags": {
"spider_name": collection_name
},
"fields": {
"count": current_count,
"increase_count": increase_amount,
"size": co_size,
"increase_size": increase_co_size

}
}
]

其中:

  • measurement, 表名
  • time,时间
  • tags,标签
  • fields,字段

可以看到,就是个列表里面,嵌套了一个字典。其中,对于时间字段,有特殊要求,可以参考这里, 下面是 python 实现方法:

1
2
from datetime import datetime
current_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')

所以,到这里,如何将爬虫的相关属性存进去呢?以 MongoDB 为例

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
mongodb_client = pymongo.MongoClient(uri)
for db_name, collection_name in dbs_and_cos.iteritems():
# 数据库操作
db = mongodb_client[db_name]
co = db[collection_name]
# 集合大小
co_size = round(float(db.command("collstats", collection_name).get('size')) / 1024 / 1024, 2)
# 集合内数据条数
current_count = co.count()

# 初始化,当程序刚执行时,初始量就设置为第一次执行时获取的数据
init_count = _count_dict.get(collection_name, current_count)
# 初始化,当程序刚执行时,初始量就设置为第一次执行时获取的数据大小
init_size = _size_dict.get(collection_name, co_size)

# 条数增长量
increase_amount = current_count - init_count
# 集合大小增长量
increase_co_size = co_size - init_size

current_time = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')

# 赋值
_size_dict[collection_name] = co_size
_count_dict[collection_name] = current_count

json_body = [
{
"measurement": "crawler",
"time": current_time,
"tags": {
"spider_name": collection_name
},
"fields": {
"count": current_count,
"increase_count": increase_amount,
"size": co_size,
"increase_size": increase_co_size

}
}
]
print json_body
client.write_points(json_body)

完整代码,关注上面的公众号,发送“”可视化“”即可获取。 那么现在我们已经往数据里存了数据了,那么接下来要做的就是把存的数据展示出来。

7.展示数据

7.1 配置数据源

以 admin 登录到 Grafana 的后台后,我们首先需要配置一下数据源。点击左边栏的最下面的按钮,然后点击 DATA SOURCES,这样就可以进入下面的页面: 点击 ADD DATA SOURCE,进行配置即可,如下图: 其中,name 自行设定;Type 选择 InfluxDB;url 为默认的 http://localhost:8086, 其他的因为我前面没有进行配置,所以默认的即可。然后在 InfluxDB Details 里的填入 Database 名,最后点击测试,如果没有报错的话,则可以进入下一步的展示数据了;

7.2 展示数据

点击左边栏的+号,然后点击 GRAPH 接着点击下图中的 edit 进入编辑页面: 从上图中可以发现:

  • 中间板块是最后的数据展示
  • 下面是数据的设置项
  • 右上角是展示时间的设置板块,在这里可以选择要展示多久的数据

7.2.1 配置数据

  1. 在 Data Source 中选择刚刚在配置数据源的时候配置的 NAME 字段,而不是 database 名。
  2. 接着在下面选择要展示的数据。看着就很熟悉是不是,完全是 sql 语句的可视化。同时,当我们的数据放到相关的字段上的时候,双击,就会把可以选择的项展示出来了,我们要做的就是直接选择即可;
  3. 设置右上角的时间,则可以让数据实时进行更新与展示

因为下面的配置实质就是 sql 查询语句,所以大家按照自己的需求,进行选择配置即可,当配置完以后,就可以在中间的面板里面看到数据了。

8. 总结

到这里,本篇文章就结束了。其中,对于 Grafana 的操作我没有介绍的很详细,因为本篇主要讲的是怎么利用这几个工具完成我们的任务。 同时,里面的功能确实很多,还有可以安装的插件。我自己目前还是仅仅对于用到的部分比较了解,所以大家可以查询官方的或者别的教程资料来对 Grafana 进行更深入的了解,制作出更加好看的可视化作品来。 最后,关注公众号,回复“可视化” 即可获取本文代码哦

Python

什么是 Elasticsearch

想查数据就免不了搜索,搜索就离不开搜索引擎,百度、谷歌都是一个非常庞大复杂的搜索引擎,他们几乎索引了互联网上开放的所有网页和数据。然而对于我们自己的业务数据来说,肯定就没必要用这么复杂的技术了,如果我们想实现自己的搜索引擎,方便存储和检索,Elasticsearch 就是不二选择,它是一个全文搜索引擎,可以快速地储存、搜索和分析海量数据。

为什么要用 Elasticsearch

Elasticsearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。 那 Lucene 又是什么?Lucene 可能是目前存在的,不论开源还是私有的,拥有最先进,高性能和全功能搜索引擎功能的库,但也仅仅只是一个库。要用上 Lucene,我们需要编写 Java 并引用 Lucene 包才可以,而且我们需要对信息检索有一定程度的理解才能明白 Lucene 是怎么工作的,反正用起来没那么简单。 那么为了解决这个问题,Elasticsearch 就诞生了。Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目标是使全文检索变得简单,相当于 Lucene 的一层封装,它提供了一套简单一致的 RESTful API 来帮助我们实现存储和检索。 所以 Elasticsearch 仅仅就是一个简易版的 Lucene 封装吗?那就大错特错了,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确的形容:

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

总之,是一个相当牛逼的搜索引擎,维基百科、Stack Overflow、GitHub 都纷纷采用它来做搜索。

Elasticsearch 的安装

我们可以到 Elasticsearch 的官方网站下载 Elasticsearch:https://www.elastic.co/downloads/elasticsearch,同时官网也附有安装说明。 首先把安装包下载下来并解压,然后运行 bin/elasticsearch(Mac 或 Linux)或者 bin\elasticsearch.bat (Windows) 即可启动 Elasticsearch 了。 我使用的是 Mac,Mac 下个人推荐使用 Homebrew 安装:

1
brew install elasticsearch

Elasticsearch 默认会在 9200 端口上运行,我们打开浏览器访问 http://localhost:9200/ 就可以看到类似内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
  "name" : "atntrTf",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "e64hkjGtTp6_G2h1Xxdv5g",
  "version" : {
    "number""6.2.4",
    "build_hash""ccec39f",
    "build_date""2018-04-12T20:37:28.497551Z",
    "build_snapshot"false,
    "lucene_version""7.2.1",
    "minimum_wire_compatibility_version""5.6.0",
    "minimum_index_compatibility_version""5.0.0"
  },
  "tagline" : "You Know, for Search"
}

如果看到这个内容,就说明 Elasticsearch 安装并启动成功了,这里显示我的 Elasticsearch 版本是 6.2.4 版本,版本很重要,以后安装一些插件都要做到版本对应才可以。 接下来我们来了解一下 Elasticsearch 的基本概念以及和 Python 的对接。

Elasticsearch 相关概念

在 Elasticsearch 中有几个基本的概念,如节点、索引、文档等等,下面来分别说明一下,理解了这些概念对熟悉 Elasticsearch 是非常有帮助的。

Node 和 Cluster

Elasticsearch 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elasticsearch 实例。 单个 Elasticsearch 实例称为一个节点(Node)。一组节点构成一个集群(Cluster)。

Index

Elasticsearch 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。 所以,Elasticsearch 数据管理的顶层单位就叫做 Index(索引),其实就相当于 MySQL、MongoDB 等里面的数据库的概念。另外值得注意的是,每个 Index (即数据库)的名字必须是小写。

Document

Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。 Document 使用 JSON 格式表示,下面是一个例子。 同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。

Type

Document 可以分组,比如 weather 这个 Index 里面,可以按城市分组(北京和上海),也可以按气候分组(晴天和雨天)。这种分组就叫做 Type,它是虚拟的逻辑分组,用来过滤 Document,类似 MySQL 中的数据表,MongoDB 中的 Collection。 不同的 Type 应该有相似的结构(Schema),举例来说,id 字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的一个区别。性质完全不同的数据(比如 products 和 logs)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。 根据规划,Elastic 6.x 版只允许每个 Index 包含一个 Type,7.x 版将会彻底移除 Type。

Fields

即字段,每个 Document 都类似一个 JSON 结构,它包含了许多字段,每个字段都有其对应的值,多个字段组成了一个 Document,其实就可以类比 MySQL 数据表中的字段。 在 Elasticsearch 中,文档归属于一种类型(Type),而这些类型存在于索引(Index)中,我们可以画一些简单的对比图来类比传统关系型数据库:

1
2
Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices   -> Types  -> Documents -> Fields

以上就是 Elasticsearch 里面的一些基本概念,通过和关系性数据库的对比更加有助于理解。

Python 对接 Elasticsearch

Elasticsearch 实际上提供了一系列 Restful API 来进行存取和查询操作,我们可以使用 curl 等命令来进行操作,但毕竟命令行模式没那么方便,所以这里我们就直接介绍利用 Python 来对接 Elasticsearch 的相关方法。 Python 中对接 Elasticsearch 使用的就是一个同名的库,安装方式非常简单:

1
pip3 install elasticsearch

官方文档是:https://elasticsearch-py.readthedocs.io/,所有的用法都可以在里面查到,文章后面的内容也是基于官方文档来的。

创建 Index

我们先来看下怎样创建一个索引(Index),这里我们创建一个名为 news 的索引:

1
2
3
4
5
from elasticsearch import Elasticsearch

es = Elasticsearch()
result = es.indices.create(index='news'ignore=400)
print(result)

如果创建成功,会返回如下结果:

1
{'acknowledged': True'shards_acknowledged': True'index': 'news'}

返回结果是 JSON 格式,其中的 acknowledged 字段表示创建操作执行成功。 但这时如果我们再把代码执行一次的话,就会返回如下结果:

1
{'error': {'root_cause': [{'type''resource_already_exists_exception''reason''index [news/QM6yz2W8QE-bflKhc5oThw] already exists', 'index_uuid''QM6yz2W8QE-bflKhc5oThw', 'index''news'}], 'type''resource_already_exists_exception''reason''index [news/QM6yz2W8QE-bflKhc5oThw] already exists', 'index_uuid''QM6yz2W8QE-bflKhc5oThw', 'index''news'}, 'status'400}

它提示创建失败,status 状态码是 400,错误原因是 Index 已经存在了。 注意这里我们的代码里面使用了 ignore 参数为 400,这说明如果返回结果是 400 的话,就忽略这个错误不会报错,程序不会执行抛出异常。 假如我们不加 ignore 这个参数的话:

1
2
3
es = Elasticsearch()
result = es.indices.create(index='news')
print(result)

再次执行就会报错了:

1
2
raise HTTP_EXCEPTIONS.get(status_code, TransportError)(status_code, error_message, additional_info)
elasticsearch.exceptions.RequestError: TransportError(400, 'resource_already_exists_exception', 'index [news/QM6yz2W8QE-bflKhc5oThwalready exists')

这样程序的执行就会出现问题,所以说,我们需要善用 ignore 参数,把一些意外情况排除,这样可以保证程序的正常执行而不会中断。

删除 Index

删除 Index 也是类似的,代码如下:

1
2
3
4
5
from elasticsearch import Elasticsearch

es = Elasticsearch()
result = es.indices.delete(index='news', 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': 'news''index_uuid''_na_''index''news'}], 'type''index_not_found_exception''reason''no such index', 'resource.type': 'index_or_alias''resource.id': 'news''index_uuid''_na_''index''news'}, 'status'404}

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

插入数据

Elasticsearch 就像 MongoDB 一样,在插入数据的时候可以直接插入结构化字典数据,插入数据可以调用 create() 方法,例如这里我们插入一条新闻数据:

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

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

data = {'title''美国留给伊拉克的是个烂摊子吗''url''http://view.news.qq.com/zt2011/usa_iraq/index.htm'}
result = es.create(index='news'doc_type='politics'id=1, body=data)
print(result)

这里我们首先声明了一条新闻数据,包括标题和链接,然后通过调用 create() 方法插入了这条数据,在调用 create() 方法时,我们传入了四个参数,index 参数代表了索引名称,doc_type 代表了文档类型,body 则代表了文档具体内容,id 则是数据的唯一标识 ID。 运行结果如下:

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

结果中 result 字段为 created,代表该数据插入成功。 另外其实我们也可以使用 index() 方法来插入数据,但与 create() 不同的是,create() 方法需要我们指定 id 字段来唯一标识该条数据,而 index() 方法则不需要,如果不指定 id,会自动生成一个 id,调用 index() 方法的写法如下:

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

create() 方法内部其实也是调用了 index() 方法,是对 index() 方法的封装。

更新数据

更新数据也非常简单,我们同样需要指定数据的 id 和内容,调用 update() 方法即可,代码如下:

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

es = Elasticsearch()
data = {
    'title''美国留给伊拉克的是个烂摊子吗',
    'url''http://view.news.qq.com/zt2011/usa_iraq/index.htm',
    'date''2011-12-16'
}
result = es.update(index='news'doc_type='politics'body=data, id=1)
print(result)

这里我们为数据增加了一个日期字段,然后调用了 update() 方法,结果如下:

1
{'_index': 'news', '_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,可以参见上例的运行结果,这次更新之后版本号就变成了 2,以后每更新一次,版本号都会加 1。 另外更新操作其实利用 index() 方法同样可以做到,写法如下:

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

可以看到,index() 方法可以代替我们完成两个操作,如果数据不存在,那就执行插入操作,如果已经存在,那就执行更新操作,非常方便。

删除数据

如果想删除一条数据可以调用 delete() 方法,指定需要删除的数据 id 即可,写法如下:

1
2
3
4
5
from elasticsearch import Elasticsearch

es = Elasticsearch()
result = es.delete(index='news'doc_type='politics'id=1)
print(result)

运行结果如下:

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

可以看到运行结果中 result 字段为 deleted,代表删除成功,_version 变成了 3,又增加了 1。

查询数据

上面的几个操作都是非常简单的操作,普通的数据库如 MongoDB 都是可以完成的,看起来并没有什么了不起的,Elasticsearch 更特殊的地方在于其异常强大的检索功能。 对于中文来说,我们需要安装一个分词插件,这里使用的是 elasticsearch-analysis-ik,GitHub 链接为:https://github.com/medcl/elasticsearch-analysis-ik,这里我们使用 Elasticsearch 的另一个命令行工具 elasticsearch-plugin 来安装,这里安装的版本是 6.2.4,请确保和 Elasticsearch 的版本对应起来,命令如下:

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

这里的版本号请替换成你的 Elasticsearch 的版本号。 安装之后重新启动 Elasticsearch 就可以了,它会自动加载安装好的插件。 首先我们新建一个索引并指定需要分词的字段,代码如下:

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='news', ignore=[400, 404])
es.indices.create(index='news'ignore=400)
result = es.indices.put_mapping(index='news'doc_type='politics'body=mapping)
print(result)

这里我们先将之前的索引删除了,然后新建了一个索引,然后更新了它的 mapping 信息,mapping 信息中指定了分词的字段,指定了字段的类型 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
datas = [
    {
        'title': '美国留给伊拉克的是个烂摊子吗',
        'url': 'http://view.news.qq.com/zt2011/usa_iraq/index.htm',
        'date': '2011-12-16'
    },
    {
        'title': '公安部:各地校车将享最高路权',
        'url': 'http://www.chinanews.com/gn/2011/12-16/3536077.shtml',
        'date': '2011-12-16'
    },
    {
        'title': '中韩渔警冲突调查:韩警平均每天扣1艘中国渔船',
        'url': 'https://news.qq.com/a/20111216/001044.htm',
        'date': '2011-12-17'
    },
    {
        'title': '中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首',
        'url': 'http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml',
        'date': '2011-12-18'
    }
]

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

这里我们指定了四条数据,都带有 title、url、date 字段,然后通过 index() 方法将其插入 Elasticsearch 中,索引名称为 news,类型为 politics。 接下来我们根据关键词查询一下相关内容:

1
2
result = es.search(index='news'doc_type='politics')
print(result)

可以看到查询出了所有插入的四条数据:

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
51
52
53
54
55
56
57
58
59
60
{
  "took"0,
  "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""c05G9mQBD9BuE5fdHOUT",
        "_score"1.0,
        "_source": {
          "title""美国留给伊拉克的是个烂摊子吗",
          "url""http://view.news.qq.com/zt2011/usa_iraq/index.htm",
          "date""2011-12-16"
        }
      },
      {
        "_index""news",
        "_type""politics",
        "_id""dk5G9mQBD9BuE5fdHOUm",
        "_score"1.0,
        "_source": {
          "title""中国驻洛杉矶领事馆遭亚裔男子枪击,嫌犯已自首",
          "url""http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml",
          "date""2011-12-18"
        }
      },
      {
        "_index""news",
        "_type""politics",
        "_id""dU5G9mQBD9BuE5fdHOUj",
        "_score"1.0,
        "_source": {
          "title""中韩渔警冲突调查:韩警平均每天扣1艘中国渔船",
          "url""https://news.qq.com/a/20111216/001044.htm",
          "date""2011-12-17"
        }
      },
      {
        "_index""news",
        "_type""politics",
        "_id""dE5G9mQBD9BuE5fdHOUf",
        "_score"1.0,
        "_source": {
          "title""公安部:各地校车将享最高路权",
          "url""http://www.chinanews.com/gn/2011/12-16/3536077.shtml",
          "date""2011-12-16"
        }
      }
    ]
  }
}

可以看到返回结果会出现在 hits 字段里面,然后其中有 total 字段标明了查询的结果条目数,还有 max_score 代表了最大匹配分数。 另外我们还可以进行全文检索,这才是体现 Elasticsearch 搜索引擎特性的地方:

1
2
3
4
5
6
7
8
9
10
11
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
{
  "took"1,
  "timed_out"false,
  "_shards": {
    "total"5,
    "successful"5,
    "skipped"0,
    "failed"0
  },
  "hits": {
    "total"2,
    "max_score"2.546152,
    "hits": [
      {
        "_index""news",
        "_type""politics",
        "_id""dk5G9mQBD9BuE5fdHOUm",
        "_score"2.546152,
        "_source": {
          "title""中国驻洛杉矶领事馆遭亚裔男子枪击,嫌犯已自首",
          "url""http://news.ifeng.com/world/detail_2011_12/16/11372558_0.shtml",
          "date""2011-12-18"
        }
      },
      {
        "_index""news",
        "_type""politics",
        "_id""dU5G9mQBD9BuE5fdHOUj",
        "_score"0.2876821,
        "_source": {
          "title""中韩渔警冲突调查:韩警平均每天扣1艘中国渔船",
          "url""https://news.qq.com/a/20111216/001044.htm",
          "date""2011-12-17"
        }
      }
    ]
  }
}

这里我们看到匹配的结果有两条,第一条的分数为 2.54,第二条的分数为 0.28,这是因为第一条匹配的数据中含有“中国”和“领事馆”两个词,第二条匹配的数据中不包含“领事馆”,但是包含了“中国”这个词,所以也被检索出来了,但是分数比较低。 因此可以看出,检索时会对对应的字段全文检索,结果还会按照检索关键词的相关性进行排序,这就是一个基本的搜索引擎雏形。 另外 Elasticsearch 还支持非常多的查询方式,详情可以参考官方文档:https://www.elastic.co/guide/en/elasticsearch/reference/6.3/query-dsl.html 以上便是对 Elasticsearch 的基本介绍以及 Python 操作 Elasticsearch 的基本用法,但这仅仅是 Elasticsearch 的基本功能,它还有更多强大的功能等待着我们的探索,后面会继续更新,敬请期待。 本节代码:https://github.com/Germey/ElasticSearch

资料推荐

另外推荐几个不错的学习站点:

参考资料

Python

大家好, 我不是崔老师,我是四毛,下面是我的个人公众号,欢迎大家关注。

好久没有写东西了,一直都记录在了自己的笔记上,这一篇是关于 glom 的一个介绍与初步使用,后期会将里面的各种 API 再给大家介绍下,同时,最近在搞爬虫的实时数据监控,也挺有意思,后面会和大家分享,敬请期待。

猛然发现,英语水平巅峰就在高考那一天。 因为是边看,边练习,然后翻译,所以个人理解可能有偏差,有错误的地方,请大家指正。 首先,这个库是用来处理一些嵌套的数据的,作者也在 PyCon 2018 上做了个分享,老美的 PyCon 还是有点质量的,不像国内的,搞的什么玩意。 视频地址:https://www.youtube.com/watch?v=bTAFl8P2DkE&t=18m07s

更新: 2018 年 7 月 28 日 10:32:08

经过咨询库的作者,在最后留的那个问题的准确解法如下:

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

target = {
'data': {
'name': 'just_test',
'likes': [{'ball': 'basketball'},
{'ball': 'football'},
{'water': 'swim'}]
}
}

spec = {
'name' : ('data.name'),
'likes' : ('data', 'likes', [glom.Coalesce('ball', 'water')])
}

print glom.glom(target, spec)
####
{'name': 'just_test', 'likes': ['basketball', 'football', 'swim']}

非常棒,准确来说就是得灵活运用 Coalesce 方法啊,不能太死板。非常 Pythonic。 另附网址,作者有个很搞笑 little four hair ,哈哈哈哈 Issue 地址

1. 官方文档地址

文档地址

2. 安装方法

1
pip install glom

3. 正式开始

glom,官方的说法是用 PYTHONIC 的方式来处理内嵌的数据。对于现实世界中的数据处理更加给力,现实世界中的数据,我的理解就是 AJAX 越来越流行了,处理这类数据会越来越频繁。有如下特点:

  • 对于嵌套数据结构的基于路径式的访问
  • 可读,有意义的错误消息
  • 声明性数据转换,使用轻量级,Pythonic 规范
  • 内置数据探索和调试功能

3.1 原始处理嵌套数据

下面的脚本包导入

1
from glom import glom

下面的 data 就是个简单的嵌套数据,一般都可以用下面几种方法进行处理

1
2
3
4
data = {'a': {'b': {'c': 'd'}}}
data['a']['b']['c']
data.get('a').get('b').get('c')
data.get('a', {}).get('b',{}).get('c')

但是当我们的数据改变成下面的这样时:

1
2
3
4
5
data2 = {'a': {'b': None}}
data2['a']['b']['c']
Traceback (most recent call last):
...
TypeError: 'NoneType' object has no attribute '__getitem__'

会报错,而且由于是嵌套数据,从错误信息里我们只知道有个 None 值,但是到底谁是呢,是 a,是 b 呢,反正肯定不是我们的朋友小哪吒。

3.2 glom 出场

那么 glom 怎么处理上面的数据呢? 如其所言,路径式:

1
2
data = {'a': {'b': {'c': 'd'}}}
print glom(data, 'a.b.c') # d

看起来还是很优雅, 很 Pythonic。

1
2
data2 = {'a': {'b': None}}
glom(data2, 'a.b.c')

错误信息如下:

1
glom.core.PathAccessError: could not access 'c', part 2 of Path('a', 'b', 'c'), got error: AttributeError("'NoneType' object has no attribute 'c'",)

很明显,这个错误就很直观。 难道仅仅只有这个?当然不是

3.2.1 Going Beyond Access

上面的是原标题,我的理解是不仅仅获取数据,还有别的呢。 首先,介绍两个基本的术语

1
2
target 目标数据,可以是字典,列表,或其他任意的对象
spec 我们想要的输出格式 【specifications】, 定义你自己所需要的格式

现在让我们跟随宇航员的脚步,探索太阳系吧。

  • 获取某个行星的名字:
1
2
3
4
5
target = {'galaxy': {'system': {'planet': 'jupiter'}}}
# 这个格式就是需要个字段值,所以输出的就是个字段值
spec = 'galaxy.system.planet'
glom(target, spec)
# 'jupyter'
  • 现在,宇航员们想把行星的名字放进一个列表中,数据是这样:
1
target = {'system': {'planets': [{'name': 'earth'}, {'name': 'jupiter'}]}}
  • 通常,处理这样的话,都要写个循环,或者搞个列表解析式,那么 glom 怎么处理呢?
1
2
3
glom(target, ('system.planets', ['name']))
print glom(target, spec)
# ['earth', 'jupiter']

是不是很简单。那么现在新需求又来了,宇航员想得到下面这个数据里面的行星的卫星的数:

1
2
target = {'system': {'planets': [{'name': 'earth', 'moons': 1},
{'name': 'jupiter', 'moons': 69}]}}
  • glom 解决方法:
1
2
3
4
5
# 自定义的格式
spec = {'names': ('system.planets', ['name']),
'moons': ('system.planets', ['moons'])}
print glom(target, spec)
# {'moons': [1, 69], 'names': ['earth', 'jupiter']}

3.2.2 Changing Requirements

Coalesce 是 glom 定义的一种结构,允许我们对于 spec 中的子 spec 进行进一步的处理,你只要在子 spec 中将可能存在的值定义好就行了,听起来有点绕,现在来梳理一下。

  • 首先,子 spec 是什么?
1
2
3
spec = {'names': ('system.planets', ['name']),
'moons': ('system.planets', ['moons'])}
# 以这个为例,这里面的system.planets就是个子spec
  • 然后,使用其解析数据:
1
2
3
4
5
6
7
8
9
target = {'system': {
'planets': [{'name': 'earth', 'moons': 1}, {'name': 'jupiter', 'moons': 69}],
}
}
spec = {'names': (Coalesce('system.planets', 'system.dwarf_planets'), ['name']),
'moons': (Coalesce('system.planets', 'system.dwarf_planets'), ['moons'])}

print glom(target, spec)
# {'moons': [1, 69], 'names': ['earth', 'jupiter']}
  • 接着当我们的数据变成了这个以后
1
2
3
4
5
6
target = {'system': {'dwarf_planets': [{'name': 'pluto', 'moons': 5},
{'name': 'ceres', 'moons': 0}]}}
spec = {'names': (Coalesce('system.planets', 'system.dwarf_planets'), ['name']),
'moons': (Coalesce('system.planets', 'system.dwarf_planets'), ['moons'])}
print glom(target, spec)
# {'moons': [5, 0], 'names': ['pluto', 'ceres']}

可以看到,依然可以使用相同的 spec 来解析不同的目标数据。 有意思的是,你可以在 target 里面同时写入 plantes 和 dwarf_plants 数据试试看,会返回什么数据。 【这里应该是个惰性的匹配,只要匹配到一个,后面的就不再去匹配了】

3.2.3 True Python Native

真正的原生 python 在 glom 里面,你可以传值给 python 里面的任意的函数 举例:

  • 求和
1
2
3
4
5
6
target = {'system': {'planets': [{'name': 'earth', 'moons': 1},
{'name': 'jupiter', 'moons': 69}]}}

print glom(target, {'moon_count': ('system.planets', ['moons'], sum)})

# {'moon_count': 70}

原教程这里还有个案例,但是我还没有理解好,就不写出来了,大家可以点击链接自己看一下。

4. 结论

下一节,为大家带来其中一些重要的函数。 最后,在用的过程中,一直有个疑问,数据如下:

1
2
3
4
5
6
7
8
target = {
'data': {
'name': 'just_test',
'likes': [{'ball': 'basketball'},
{'ball': 'football'},
{'water': 'swim'}]
}
}

现在,我想返回的数据格式为:

1
{'name': 'just_for_test', 'likes': ['basketball', 'football', 'water']}

一开始我以为可以这么用:

1
2
3
4
spec = {
'name': ('data.name'),
'likes': ('data.likes', ['ball', 'water'] ),
}

但是不行,这样会报错。后来用了另外的方法:

1
2
3
4
5
6
7
spec = {
'name': ('data.name'),
'likes': ('data.likes', [lambda x: x.values()[0] if 'ball' or 'water' in x.keys() else ''] ),
}

print glom(target, spec)
# {'name': 'just_test', 'likes': ['basketball', 'football', 'swim']}

这样感觉很不爽啊,还望会的同学不吝赐教啊。

Python

开门见山

话不多说了!第三波送书活动来了!这次送 20 本签名版《Python3 网络爬虫开发实战》。 本书目前上市三个月已经重印 6 次,上市三个月以来长期位居京东计算机类新书榜第一位(现已不算新书),目前在豆瓣的评分是 9.2 分。

书籍介绍

本书《Python3 网络爬虫开发实战》全面介绍了利用 Python3 开发网络爬虫的知识,书中首先详细介绍了各种类型的环境配置过程和爬虫基础知识,还讨论了 urllib、requests 等请求库和 Beautiful Soup、XPath、pyquery 等解析库以及文本和各类数据库的存储方法,另外本书通过多个真实新鲜案例介绍了分析 Ajax 进行数据爬取,Selenium 和 Splash 进行动态网站爬取的过程,接着又分享了一些切实可行的爬虫技巧,比如使用代理爬取和维护动态代理池的方法、ADSL 拨号代理的使用、各类验证码(图形、极验、点触、宫格等)的破解方法、模拟登录网站爬取的方法及 Cookies 池的维护等等。 此外,本书的内容还远远不止这些,作者还结合移动互联网的特点探讨了使用 Charles、mitmdump、Appium 等多种工具实现 App 抓包分析、加密参数接口爬取、微信朋友圈爬取的方法。此外本书还详细介绍了 pyspider 框架、Scrapy 框架的使用和分布式爬虫的知识,另外对于优化及部署工作,本书还包括 Bloom Filter 效率优化、Docker 和 Scrapyd 爬虫部署、分布式爬虫管理框架 Gerapy 的分享。 全书共 604 页,足足两斤重呢~ 定价为 99 元!

作者介绍

看书就先看看谁写的嘛,我们来了解一下~ 崔庆才,静觅博客博主(https://cuiqingcai.com),博客 Python 爬虫博文阅读量已过千万,北京航空航天大学硕士,天善智能、网易云课堂讲师,微软小冰大数据工程师,有多个大型分布式爬虫项目经验,乐于技术分享,文章通俗易懂 ^^ 附皂片一张 ~(@^^@)~

图文介绍

呕心沥血设计的宣传图也得放一下~

专家评论

书是好是坏,得让专家看评一评呀,那么下面就是几位专家的精彩评论,快来看看吧~ 在互联网软件开发工程师的分类中,爬虫工程师是非常重要的。爬虫工作往往是一个公司核心业务开展的基础,数据抓取下来,才有后续的加工处理和最终展现。此时数据的抓取规模、稳定性、实时性、准确性就显得非常重要。早期的互联网充分开放互联,数据获取的难度很小。随着各大公司对数据资产日益看重,反爬水平也在不断提高,各种新技术不断给爬虫软件提出新的课题。本书作者对爬虫的各个领域都有深刻研究,书中探讨了 Ajax 数据的抓取、动态渲染页面的抓取、验证码识别、模拟登录等高级话题,同时也结合移动互联网的特点探讨了 App 的抓取等。更重要的是,本书提供了大量源码,可以帮助读者更好地理解相关内容。强烈推荐给各位技术爱好者阅读!

——梁斌,八友科技总经理

数据既是当今大数据分析的前提,也是各种人工智能应用场景的基础。得数据者得天下,会爬虫者走遍天下也不怕!一册在手,让小白到老司机都能有所收获!

——李舟军,北京航空航天大学教授,博士生导师

本书从爬虫入门到分布式抓取,详细介绍了爬虫技术的各个要点,并针对不同的场景提出了对应的解决方案。另外,书中通过大量的实例来帮助读者更好地学习爬虫技术,通俗易懂,干货满满。强烈推荐给大家!

——宋睿华,微软小冰首席科学家

有人说中国互联网的带宽全给各种爬虫占据了,这说明网络爬虫的重要性以及中国互联网数据封闭垄断的现状。爬是一种能力,爬是为了不爬。

——施水才,北京拓尔思信息技术股份有限公司总裁

全书目录

书的目录也有~ 看这里!

  • 1-开发环境配置
  • 1.1-Python3 的安装
  • 1.2-请求库的安装
  • 1.3-解析库的安装
  • 1.4-数据库的安装
  • 1.5-存储库的安装
  • 1.6-Web 库的安装
  • 1.7-App 爬取相关库的安装
  • 1.8-爬虫框架的安装
  • 1.9-部署相关库的安装
  • 2-爬虫基础
  • 2.1-HTTP 基本原理
  • 2.2-网页基础
  • 2.3-爬虫的基本原理
  • 2.4-会话和 Cookies
  • 2.5-代理的基本原理
  • 3-基本库的使用
  • 3.1-使用 urllib
  • 3.1.1-发送请求
  • 3.1.2-处理异常
  • 3.1.3-解析链接
  • 3.1.4-分析 Robots 协议
  • 3.2-使用 requests
  • 3.2.1-基本用法
  • 3.2.2-高级用法
  • 3.3-正则表达式
  • 3.4-抓取猫眼电影排行
  • 4-解析库的使用
  • 4.1-使用 XPath
  • 4.2-使用 Beautiful Soup
  • 4.3-使用 pyquery
  • 5-数据存储
  • 5.1-文件存储
  • 5.1.1-TXT 文本存储
  • 5.1.2-JSON 文件存储
  • 5.1.3-CSV 文件存储
  • 5.2-关系型数据库存储
  • 5.2.1-MySQL 存储
  • 5.3-非关系型数据库存储
  • 5.3.1-MongoDB 存储
  • 5.3.2-Redis 存储
  • 6-Ajax 数据爬取
  • 6.1-什么是 Ajax
  • 6.2-Ajax 分析方法
  • 6.3-Ajax 结果提取
  • 6.4-分析 Ajax 爬取今日头条街拍美图
  • 7-动态渲染页面爬取
  • 7.1-Selenium 的使用
  • 7.2-Splash 的使用
  • 7.3-Splash 负载均衡配置
  • 7.4-使用 Selenium 爬取淘宝商品
  • 8-验证码的识别
  • 8.1-图形验证码的识别
  • 8.2-极验滑动验证码的识别
  • 8.3-点触验证码的识别
  • 8.4-微博宫格验证码的识别
  • 9-代理的使用
  • 9.1-代理的设置
  • 9.2-代理池的维护
  • 9.3-付费代理的使用
  • 9.4-ADSL 拨号代理
  • 9.5-使用代理爬取微信公众号文章
  • 10-模拟登录
  • 10.1-模拟登录并爬取 GitHub
  • 10.2-Cookies 池的搭建
  • 11-App 的爬取
  • 11.1-Charles 的使用
  • 11.2-mitmproxy 的使用
  • 11.3-mitmdump 爬取“得到”App 电子书信息
  • 11.4-Appium 的基本使用
  • 11.5-Appium 爬取微信朋友圈
  • 11.6-Appium+mitmdump 爬取京东商品
  • 12-pyspider 框架的使用
  • 12.1-pyspider 框架介绍
  • 12.2-pyspider 的基本使用
  • 12.3-pyspider 用法详解
  • 13-Scrapy 框架的使用
  • 13.1-Scrapy 框架介绍
  • 13.2-Scrapy 入门
  • 13.3-Selector 的用法
  • 13.4-Spider 的用法
  • 13.5-Downloader Middleware 的用法
  • 13.6-Spider Middleware 的用法
  • 13.7-Item Pipeline 的用法
  • 13.8-Scrapy 对接 Selenium
  • 13.9-Scrapy 对接 Splash
  • 13.10-Scrapy 通用爬虫
  • 13.11-Scrapyrt 的使用
  • 13.12-Scrapy 对接 Docker
  • 13.13-Scrapy 爬取新浪微博
  • 14-分布式爬虫
  • 14.1-分布式爬虫原理
  • 14.2-Scrapy-Redis 源码解析
  • 14.3-Scrapy 分布式实现
  • 14.4-Bloom Filter 的对接
  • 15-分布式爬虫的部署
  • 15.1-Scrapyd 分布式部署
  • 15.2-Scrapyd-Client 的使用
  • 15.3-Scrapyd 对接 Docker
  • 15.4-Scrapyd 批量部署
  • 15.5-Gerapy 分布式管理

购买链接

想必很多小伙伴已经等了很久了,之前预售那么久也一直迟迟没有货,发售就有不少网店又售空了,不过现在起不用担心了!

书籍现已在京东、天猫、当当等网店上架并全面供应啦,复制链接到浏览器打开或扫描二维码打开即可购买了!

京东商城

https://item.jd.com/12333540.html

天猫商城

https://detail.tmall.com/item.htm?id=566699703917

当当网

http://product.dangdang.com/25249602.html

欢迎大家购买,谢谢支持!O(∩_∩)O

免费预览

不放心?想先看看有些啥,没问题!看这里: 免费章节试读: https://cuiqingcai.com/5052.html 将一直免费开放前 7 章节,欢迎大家试读! 好了,接下来就是我们的福利环节啦~

福利一:签名书!!!

恭喜你看到这里了!那么接下来的福利时间就到了!后面还有两个福利不容错过~ 赠书活动第三波来袭,送 20 本作者亲笔签名书籍!!! 活动流程(重要,请一定认真阅读): 公众号进击的 Coder 回复 “赠书” 获取序列码参与活动,2018.7.24 22:00 截止,逾期参与无效,请记住您的序列码,这是您的唯一标识。 您可以转发活动页面邀请好友帮忙积攒人气值,最终取人气值前 20 位赠书,截止日期 2018.7.24 22:00,该时刻人气值前 20 位的朋友每人会获得签名书一本。 最终赠书名单会在微信公众号进击的 Coder 公布,届时请关注公众号消息!

福利二:独家优惠!!!

等等,你以为这就是全部福利吗?当然不是!除了抽奖送书,我们还拿到了拨号 VPS 知名品牌云立方的独家优惠,在公众号(进击的 Coder )中回复:“优惠券”,即可免费领取云立方 50 元主机优惠券,数量有限,先到先得!优惠券可在云立方官网(www.yunlifang.cn)购买动态IP拨号VPS时抵扣现金,有了它,爬虫代理易如反掌! 你问我动态拨号 VPS 能做什么?应该怎么用在爬虫里?来这里了解一下: 轻松获得海量稳定代理!ADSL 拨号代理的搭建

福利三:视频课程!!!

当然除了书籍,也有配套的视频课程,目前半价促销中,作者同样是崔庆才,二者结合学习效果更佳!限时优惠折扣中!扫描下图中二维码即可了解详情! 最后也是最重要的就是参与活动的地址了!!!快来扫码回复领取属于你的福利吧!!!

特别致谢

最后特别感谢云立方、天善智能对本活动的大力支持!

Python

1. 前言

在执行一些 IO 密集型任务的时候,程序常常会因为等待 IO 而阻塞。比如在网络爬虫中,如果我们使用 requests 库来进行请求的话,如果网站响应速度过慢,程序一直在等待网站响应,最后导致其爬取效率是非常非常低的。 为了解决这类问题,本文就来探讨一下 Python 中异步协程来加速的方法,此种方法对于 IO 密集型任务非常有效。如将其应用到网络爬虫中,爬取效率甚至可以成百倍地提升。 注:本文协程使用 async/await 来实现,需要 Python 3.5 及以上版本。

2. 基本了解

在了解异步协程之前,我们首先得了解一些基础概念,如阻塞和非阻塞、同步和异步、多进程和协程。

2.1 阻塞

阻塞状态指程序未得到所需计算资源时被挂起的状态。程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。 常见的阻塞形式有:网络 I/O 阻塞、磁盘 I/O 阻塞、用户输入阻塞等。阻塞是无处不在的,包括 CPU 切换上下文时,所有的进程都无法真正干事情,它们也会被阻塞。如果是多核 CPU 则正在执行上下文切换操作的核不可被利用。

2.2 非阻塞

程序在等待某操作过程中,自身不被阻塞,可以继续运行干别的事情,则称该程序在该操作上是非阻塞的。 非阻塞并不是在任何程序级别、任何情况下都可以存在的。 仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。 非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。

2.3 同步

不同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致,称这些程序单元是同步执行的。 例如购物系统中更新商品库存,需要用“行锁”作为通信信号,让不同的更新请求强制排队顺序执行,那更新库存的操作是同步的。 简言之,同步意味着有序。

2.4 异步

为完成某个任务,不同程序单元之间过程中无需通信协调,也能完成任务的方式,不相关的程序单元之间可以是异步的。 例如,爬虫下载网页。调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是无关的,也无需相互通知协调。这些异步操作的完成时刻并不确定。 简言之,异步意味着无序。

2.5 多进程

多进程就是利用 CPU 的多核优势,在同一时间并行地执行多个任务,可以大大提高执行效率。

2.6 协程

协程,英文叫做 Coroutine,又称微线程,纤程,协程是一种用户态的轻量级线程。 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此协程能保留上一次调用时的状态,即所有局部状态的一个特定组合,每次过程重入时,就相当于进入上一次调用的状态。 协程本质上是个单进程,协程相对于多进程来说,无需线程上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单。 我们可以使用协程来实现异步操作,比如在网络爬虫场景下,我们发出一个请求之后,需要等待一定的时间才能得到响应,但其实在这个等待过程中,程序可以干许多其他的事情,等到响应得到之后才切换回来继续处理,这样可以充分利用 CPU 和其他资源,这就是异步协程的优势。

3. 异步协程用法

接下来让我们来了解下协程的实现,从 Python 3.4 开始,Python 中加入了协程的概念,但这个版本的协程还是以生成器对象为基础的,在 Python 3.5 则增加了 async/await,使得协程的实现更加方便。 Python 中使用协程最常用的库莫过于 asyncio,所以本文会以 asyncio 为基础来介绍协程的使用。 首先我们需要了解下面几个概念:

  • event_loop:事件循环,相当于一个无限循环,我们可以把一些函数注册到这个事件循环上,当满足条件发生的时候,就会调用对应的处理方法。
  • coroutine:中文翻译叫协程,在 Python 中常指代为协程对象类型,我们可以将协程对象注册到时间循环中,它会被事件循环调用。我们可以使用 async 关键字来定义一个方法,这个方法在调用时不会立即被执行,而是返回一个协程对象。
  • task:任务,它是对协程对象的进一步封装,包含了任务的各个状态。
  • future:代表将来执行或没有执行的任务的结果,实际上和 task 没有本质区别。

另外我们还需要了解 async/await 关键字,它是从 Python 3.5 才出现的,专门用于定义协程。其中,async 定义一个协程,await 用来挂起阻塞方法的执行。

3.1 定义协程

首先我们来定义一个协程,体验一下它和普通进程在实现上的不同之处,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
import asyncio

async def execute(x):
    print('Number:', x)

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

loop = asyncio.get_event_loop()
loop.run_until_complete(coroutine)
print('After calling loop')

运行结果:

1
2
3
4
Coroutine: <coroutine object execute at 0x1034cf830>
After calling execute
Number: 1
After calling loop

首先我们引入了 asyncio 这个包,这样我们才可以使用 async 和 await,然后我们使用 async 定义了一个 execute() 方法,方法接收一个数字参数,方法执行之后会打印这个数字。 随后我们直接调用了这个方法,然而这个方法并没有执行,而是返回了一个 coroutine 协程对象。随后我们使用 get_event_loop() 方法创建了一个事件循环 loop,并调用了 loop 对象的 run_until_complete() 方法将协程注册到事件循环 loop 中,然后启动。最后我们才看到了 execute() 方法打印了输出结果。 可见,async 定义的方法就会变成一个无法直接执行的 coroutine 对象,必须将其注册到事件循环中才可以执行。 上文我们还提到了 task,它是对 coroutine 对象的进一步封装,它里面相比 coroutine 对象多了运行状态,比如 running、finished 等,我们可以用这些状态来获取协程对象的执行情况。 在上面的例子中,当我们将 coroutine 对象传递给 run_until_complete() 方法的时候,实际上它进行了一个操作就是将 coroutine 封装成了 task 对象,我们也可以显式地进行声明,如下所示:

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

async def execute(x):
    print('Number:', x)
    return x

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

loop = asyncio.get_event_loop()
task = loop.create_task(coroutine)
print('Task:'task)
loop.run_until_complete(task)
print('Task:'task)
print('After calling loop')

运行结果:

1
2
3
4
5
6
Coroutine: <coroutine object execute at 0x10e0f7830>
After calling execute
Task: <Task pending coro=<execute() running at demo.py:4>>
Number: 1
Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1>
After calling loop

这里我们定义了 loop 对象之后,接着调用了它的 create_task() 方法将 coroutine 对象转化为了 task 对象,随后我们打印输出一下,发现它是 pending 状态。接着我们将 task 对象添加到事件循环中得到执行,随后我们再打印输出一下 task 对象,发现它的状态就变成了 finished,同时还可以看到其 result 变成了 1,也就是我们定义的 execute() 方法的返回结果。 另外定义 task 对象还有一种方式,就是直接通过 asyncio 的 ensure_future() 方法,返回结果也是 task 对象,这样的话我们就可以不借助于 loop 来定义,即使我们还没有声明 loop 也可以提前定义好 task 对象,写法如下:

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

async def execute(x):
    print('Number:', x)
    return x

coroutine = execute(1)
print('Coroutine:', coroutine)
print('After calling execute')

task = asyncio.ensure_future(coroutine)
print('Task:'task)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:'task)
print('After calling loop')

运行结果:

1
2
3
4
5
6
Coroutine: <coroutine object execute at 0x10aa33830>
After calling execute
Task: <Task pending coro=<execute() running at demo.py:4>>
Number: 1
Task: <Task finished coro=<execute() done, defined at demo.py:4> result=1>
After calling loop

发现其效果都是一样的。

3.2 绑定回调

另外我们也可以为某个 task 绑定一个回调方法,来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import asyncio
import requests

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

def callback(task):
    print('Status:'task.result())

coroutine = request()
task = asyncio.ensure_future(coroutine)
task.add_done_callback(callback)
print('Task:'task)

loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:'task)

在这里我们定义了一个 request() 方法,请求了百度,返回状态码,但是这个方法里面我们没有任何 print() 语句。随后我们定义了一个 callback() 方法,这个方法接收一个参数,是 task 对象,然后调用 print() 方法打印了 task 对象的结果。这样我们就定义好了一个 coroutine 对象和一个回调方法,我们现在希望的效果是,当 coroutine 对象执行完毕之后,就去执行声明的 callback() 方法。 那么它们二者怎样关联起来呢?很简单,只需要调用 add_done_callback() 方法即可,我们将 callback() 方法传递给了封装好的 task 对象,这样当 task 执行完毕之后就可以调用 callback() 方法了,同时 task 对象还会作为参数传递给 callback() 方法,调用 task 对象的 result() 方法就可以获取返回结果了。 运行结果:

1
2
3
Task: <Task pending coro=<request() running at demo.py:5> cb=[callback() at demo.py:11]>
Status: <Response [200]>
Task: <Task finished coro=<request() done, defined at demo.py:5> result=<Response [200]>>

实际上不用回调方法,直接在 task 运行完毕之后也可以直接调用 result() 方法获取结果,如下所示:

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

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

coroutine = request()
task = asyncio.ensure_future(coroutine)
print('Task:'task)

loop = asyncio.get_event_loop()
loop.run_until_complete(task)
print('Task:'task)
print('Task Result:'task.result())

运行结果是一样的:

1
2
3
Task: <Task pending coro=<request() running at demo.py:4>>
Task: <Task finished coro=<request() done, defined at demo.py:4> result=<Response [200]>>
Task Result: <Response [200]>

3.3 多任务协程

上面的例子我们只执行了一次请求,如果我们想执行多次请求应该怎么办呢?我们可以定义一个 task 列表,然后使用 asyncio 的 wait() 方法即可执行,看下面的例子:

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

async def request():
    url = 'https://www.baidu.com'
    status = requests.get(url)
    return status

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
print('Tasks:', tasks)

loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

for task in tasks:
    print('Task Result:', task.result())

这里我们使用一个 for 循环创建了五个 task,组成了一个列表,然后把这个列表首先传递给了 asyncio 的 wait() 方法,然后再将其注册到时间循环中,就可以发起五个任务了。最后我们再将任务的运行结果输出出来,运行结果如下:

1
2
3
4
5
6
Tasks: [<Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>, <Task pending coro=<request() running at demo.py:5>>]
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>
Task Result: <Response [200]>

可以看到五个任务被顺次执行了,并得到了运行结果。

3.4 协程实现

前面说了这么一通,又是 async,又是 coroutine,又是 task,又是 callback,但似乎并没有看出协程的优势啊?反而写法上更加奇怪和麻烦了,别急,上面的案例只是为后面的使用作铺垫,接下来我们正式来看下协程在解决 IO 密集型任务上有怎样的优势吧! 上面的代码中,我们用一个网络请求作为示例,这就是一个耗时等待的操作,因为我们请求网页之后需要等待页面响应并返回结果。耗时等待的操作一般都是 IO 操作,比如文件读取、网络请求等等。协程对于处理这种操作是有很大优势的,当遇到需要等待的情况的时候,程序可以暂时挂起,转而去执行其他的操作,从而避免一直等待一个程序而耗费过多的时间,充分利用资源。 为了表现出协程的优势,我们需要先创建一个合适的实验环境,最好的方法就是模拟一个需要等待一定时间才可以获取返回结果的网页,上面的代码中使用了百度,但百度的响应太快了,而且响应速度也会受本机网速影响,所以最好的方式是自己在本地模拟一个慢速服务器,这里我们选用 Flask。 如果没有安装 Flask 的话可以执行如下命令安装:

1
pip3 install flask

然后编写服务器代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
from flask import Flask
import time

app = Flask(__name__)

@app.route('/')
def index():
    time.sleep(3)
    return 'Hello!'

if __name__ == '__main__':
    app.run(threaded=True)

这里我们定义了一个 Flask 服务,主入口是 index() 方法,方法里面先调用了 sleep() 方法休眠 3 秒,然后接着再返回结果,也就是说,每次请求这个接口至少要耗时 3 秒,这样我们就模拟了一个慢速的服务接口。 注意这里服务启动的时候,run() 方法加了一个参数 threaded,这表明 Flask 启动了多线程模式,不然默认是只有一个线程的。如果不开启多线程模式,同一时刻遇到多个请求的时候,只能顺次处理,这样即使我们使用协程异步请求了这个服务,也只能一个一个排队等待,瓶颈就会出现在服务端。所以,多线程模式是有必要打开的。 启动之后,Flask 应该默认会在 127.0.0.1:5000 上运行,运行之后控制台输出结果如下:

1
 * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

接下来我们再重新使用上面的方法请求一遍:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import asyncio
import requests
import time

start = time.time()

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = requests.get(url)
    print('Get response from', url, 'Result:', response.text)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:'end - start)

在这里我们还是创建了五个 task,然后将 task 列表传给 wait() 方法并注册到时间循环中执行。 运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 15.049368143081665

可以发现和正常的请求并没有什么两样,依然还是顺次执行的,耗时 15 秒,平均一个请求耗时 3 秒,说好的异步处理呢? 其实,要实现异步处理,我们得先要有挂起的操作,当一个任务需要等待 IO 结果的时候,可以挂起当前任务,转而去执行其他任务,这样我们才能充分利用好资源,上面方法都是一本正经的串行走下来,连个挂起都没有,怎么可能实现异步?想太多了。 要实现异步,接下来我们再了解一下 await 的用法,使用 await 可以将耗时等待的操作挂起,让出控制权。当协程执行的时候遇到 await,时间循环就会将本协程挂起,转而去执行别的协程,直到其他的协程挂起或执行完毕。 所以,我们可能会将代码中的 request() 方法改成如下的样子:

1
2
3
4
5
async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = await requests.get(url)
    print('Get response from', url, 'Result:', response.text)

仅仅是在 requests 前面加了一个 await,然而执行以下代码,会得到如下报错:

1
2
3
4
5
6
7
8
9
10
11
12
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Cost time: 15.048935890197754
Task exception was never retrieved
future: <Task finished coro=<request() done, defined at demo.py:7> exception=TypeError("object Response can't be used in 'await' expression",)>
Traceback (most recent call last):
  File "demo.py", line 10in request
    status = await requests.get(url)
TypeError: object Response can't be used in 'await' expression

这次它遇到 await 方法确实挂起了,也等待了,但是最后却报了这么个错,这个错误的意思是 requests 返回的 Response 对象不能和 await 一起使用,为什么呢?因为根据官方文档说明,await 后面的对象必须是如下格式之一:

  • A native coroutine object returned from a native coroutine function,一个原生 coroutine 对象。
  • A generator-based coroutine object returned from a function decorated with types.coroutine(),一个由 types.coroutine() 修饰的生成器,这个生成器可以返回 coroutine 对象。
  • An object with an await method returning an iterator,一个包含 await 方法的对象返回的一个迭代器。

可以参见:https://www.python.org/dev/peps/pep-0492/#await-expression。 reqeusts 返回的 Response 不符合上面任一条件,因此就会报上面的错误了。 那么有的小伙伴就发现了,既然 await 后面可以跟一个 coroutine 对象,那么我用 async 把请求的方法改成 coroutine 对象不就可以了吗?所以就改写成如下的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import asyncio
import requests
import time

start = time.time()

async def get(url):
    return requests.get(url)

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    response = await get(url)
    print('Get response from', url, 'Result:', response.text)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:'end - start)

这里我们将请求页面的方法独立出来,并用 async 修饰,这样就得到了一个 coroutine 对象,我们运行一下看看:

1
2
3
4
5
6
7
8
9
10
11
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 15.134317874908447

还是不行,它还不是异步执行,也就是说我们仅仅将涉及 IO 操作的代码封装到 async 修饰的方法里面是不可行的!我们必须要使用支持异步操作的请求方式才可以实现真正的异步,所以这里就需要 aiohttp 派上用场了。

3.5 使用 aiohttp

aiohttp 是一个支持异步请求的库,利用它和 asyncio 配合我们可以非常方便地实现异步请求操作。 安装方式如下:

1
pip3 install aiohttp

官方文档链接为:https://aiohttp.readthedocs.io/,它分为两部分,一部分是 Client,一部分是 Server,详细的内容可以参考官方文档。 下面我们将 aiohttp 用上来,将代码改成如下样子:

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
import asyncio
import aiohttp
import time

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result

async def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = await get(url)
    print('Get response from', url, 'Result:', result)

tasks = [asyncio.ensure_future(request()) for _ in range(5)]
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

end = time.time()
print('Cost time:'end - start)

在这里我们将请求库由 requests 改成了 aiohttp,通过 aiohttp 的 ClientSession 类的 get() 方法进行请求,结果如下:

1
2
3
4
5
6
7
8
9
10
11
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Waiting for http://127.0.0.1:5000
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Get response from http://127.0.0.1:5000 Result: Hello!
Cost time: 3.0199508666992188

成功了!我们发现这次请求的耗时由 15 秒变成了 3 秒,耗时直接变成了原来的 1/5。 代码里面我们使用了 await,后面跟了 get() 方法,在执行这五个协程的时候,如果遇到了 await,那么就会将当前协程挂起,转而去执行其他的协程,直到其他的协程也挂起或执行完毕,再进行下一个协程的执行。 开始运行时,时间循环会运行第一个 task,针对第一个 task 来说,当执行到第一个 await 跟着的 get() 方法时,它被挂起,但这个 get() 方法第一步的执行是非阻塞的,挂起之后立马被唤醒,所以立即又进入执行,创建了 ClientSession 对象,接着遇到了第二个 await,调用了 session.get() 请求方法,然后就被挂起了,由于请求需要耗时很久,所以一直没有被唤醒,好第一个 task 被挂起了,那接下来该怎么办呢?事件循环会寻找当前未被挂起的协程继续执行,于是就转而执行第二个 task 了,也是一样的流程操作,直到执行了第五个 task 的 session.get() 方法之后,全部的 task 都被挂起了。所有 task 都已经处于挂起状态,那咋办?只好等待了。3 秒之后,几个请求几乎同时都有了响应,然后几个 task 也被唤醒接着执行,输出请求结果,最后耗时,3 秒! 怎么样?这就是异步操作的便捷之处,当遇到阻塞式操作时,任务被挂起,程序接着去执行其他的任务,而不是傻傻地等着,这样可以充分利用 CPU 时间,而不必把时间浪费在等待 IO 上。 有人就会说了,既然这样的话,在上面的例子中,在发出网络请求后,既然接下来的 3 秒都是在等待的,在 3 秒之内,CPU 可以处理的 task 数量远不止这些,那么岂不是我们放 10 个、20 个、50 个、100 个、1000 个 task 一起执行,最后得到所有结果的耗时不都是 3 秒左右吗?因为这几个任务被挂起后都是一起等待的。 理论来说确实是这样的,不过有个前提,那就是服务器在同一时刻接受无限次请求都能保证正常返回结果,也就是服务器无限抗压,另外还要忽略 IO 传输时延,确实可以做到无限 task 一起执行且在预想时间内得到结果。 我们这里将 task 数量设置成 100,再试一下:

1
tasks = [asyncio.ensure_future(request()) for _ in range(100)]

耗时结果如下:

1
Cost time3.106252670288086

最后运行时间也是在 3 秒左右,当然多出来的时间就是 IO 时延了。 可见,使用了异步协程之后,我们几乎可以在相同的时间内实现成百上千倍次的网络请求,把这个运用在爬虫中,速度提升可谓是非常可观了。

3.6 与单进程、多进程对比

可能有的小伙伴非常想知道上面的例子中,如果 100 次请求,不是用异步协程的话,使用单进程和多进程会耗费多少时间,我们来测试一下: 首先来测试一下单进程的时间:

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

start = time.time()

def request():
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = requests.get(url).text
    print('Get response from', url, 'Result:', result)

for _ in range(100):
    request()

end = time.time()
print('Cost time:'end - start)

最后耗时:

1
Cost time305.16639709472656

接下来我们使用多进程来测试下,使用 multiprocessing 库:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import requests
import time
import multiprocessing

start = time.time()

def request(_):
    url = 'http://127.0.0.1:5000'
    print('Waiting for', url)
    result = requests.get(url).text
    print('Get response from', url, 'Result:', result)

cpu_count = multiprocessing.cpu_count()
print('Cpu count:', cpu_count)
pool = multiprocessing.Pool(cpu_count)
pool.map(request, range(100))

end = time.time()
print('Cost time:', end - start)

这里我使用了multiprocessing 里面的 Pool 类,即进程池。我的电脑的 CPU 个数是 8 个,这里的进程池的大小就是 8。 运行时间:

1
Cost time48.17306900024414

可见 multiprocessing 相比单线程来说,还是可以大大提高效率的。

3.7 与多进程的结合

既然异步协程和多进程对网络请求都有提升,那么为什么不把二者结合起来呢?在最新的 PyCon 2018 上,来自 Facebook 的 John Reese 介绍了 asyncio 和 multiprocessing 各自的特点,并开发了一个新的库,叫做 aiomultiprocess,感兴趣的可以了解下:https://www.youtube.com/watch?v=0kXaLh8Fz3k。 这个库的安装方式是:

1
pip3 install aiomultiprocess

需要 Python 3.6 及更高版本才可使用。 使用这个库,我们可以将上面的例子改写如下:

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
import asyncio
import aiohttp
import time
from aiomultiprocess import Pool

start = time.time()

async def get(url):
    session = aiohttp.ClientSession()
    response = await session.get(url)
    result = await response.text()
    session.close()
    return result

async def request():
    url = 'http://127.0.0.1:5000'
    urls = [url for _ in range(100)]
    async with Pool() as pool:
        result = await pool.map(get, urls)
        return result

coroutine = request()
task = asyncio.ensure_future(coroutine)
loop = asyncio.get_event_loop()
loop.run_until_complete(task)

end = time.time()
print('Cost time:'end - start)

这样就会同时使用多进程和异步协程进行请求,当然最后的结果其实和异步是差不多的:

1
Cost time3.1156570434570312

因为我的测试接口的原因,最快的响应也是 3 秒,所以这部分多余的时间基本都是 IO 传输时延。但在真实情况下,我们在做爬取的时候遇到的情况千变万化,一方面我们使用异步协程来防止阻塞,另一方面我们使用 multiprocessing 来利用多核成倍加速,节省时间其实还是非常可观的。 以上便是 Python 中协程的基本用法,希望对大家有帮助。

4. 参考来源

技术杂谈

最近有一个朋友刚入手了 Mac,准备专门搞开发用,让我给他推荐几款软件,然后我就把我的 Launchpad 截图发给了他,他看到这密密麻麻的软件完全不知所措。 于是乎,我就大略整理了一些我比较推荐的几款软件,同时分享给大家,希望对大家有所帮助! 下面的一些软件都是我个人比较喜欢的,其实还有很多其他的恕不能一一列举了,如果大家有其他推荐的欢迎留言给我,谢谢!

日常工具

一些日常工具在这里我就不一一列举了,大部分使用 Mac 的小伙伴都会安装,比如 QQ、微信、Chrome 浏览器、网易云音乐、迅雷等等,这些在 Windows 上也几乎都是必备软件,这里就不再展开说明了。

效率工具

效率工具顾名思义,可以方便和简化 Mac 的操作,提高生产工作效率的工具,下面推荐几款我比较常用的。

Alfred

首推 Alfred,可以说是 Mac 必备软件,利用它我们可以快速地进行各种操作,大幅提高工作效率,如快速打开某个软件、快速打开某个链接、快速搜索某个文档,快速定位某个文件,快速查看本机 IP,快速定义某个色值,几乎你能想到的都能对接实现。 这些快速功能是怎么实现的呢?实际上是 Alfred 对接了很多 Workflow,我们可以使用 Workflow 方便地进行功能扩展,一些比较优秀的 Workflow 已经有人专门做过整理了,可以参见:https://github.com/zenorocha/alfred-workflows。 推荐指数:★★★★★

Todoist

大家肯定也在使用各种 Todo List 的软件,这种软件其实也是五花八门,经过我本人试用,我觉得 Todoist 这款软件是最方便的。 它支持各种类型的任务定制,还可以设置分组、优先级、Deadline、执行人员、提醒、协作、效率统计等功能。另外它的各个平台支持真是异常地全啊,网页、PC、移动端就不用说了,都必须有的,另外它还有浏览器插件版、电邮版、可穿戴设备(如 Apple Watch、Google Wear)版,另外他还可以和 Mac 的日历事件进行同步,日历添加的事件也会自动添加到 Todoist 里面,非常方便,是目前我体验过的最好用的一款。 这款软件个人推荐购买专业版解锁全部功能,一个月 3 刀,但个人觉得确实非常值。 推荐指数:★★★★☆

Paste

Mac 上默认只有一个粘贴板,当我们新复制了一段文字之后,如果我们想再找寻之前复制的历史记录就找不到了,这其实是很反人类的。 好在 Paste 这款软件帮我们解决了这个问题,它可以保存我们粘贴板的历史记录,等需要粘贴某个内容的时候只需要呼出 Paste 历史粘贴板,然后选择某个特定的内容粘贴就好了,另外它还支持文本格式调整粘贴板分类和搜索,还可以支持快速便捷粘贴。有了它,妈妈再也不用担心我的粘贴板丢失了! 推荐指数:★★★★★

Synergy

工作时我会使用公司的台式机,是 Windows 系统,另外自己的个人笔记本 Mac 也会放在旁边,两台 PC 有时候会交替使用,但是我总不能配两套键盘和鼠标吧,这样就显得累赘了,而且也没那么多地方放啊。 有了 Synergy,我们可以将两台 PC 关联,实现键盘鼠标共享。我们可以使用一套键盘和鼠标来操作两台 PC,注意这是两个完全独立的 PC,各自有各自的屏幕和系统,使用 Synergy 我们可以做到一套键鼠同时控制两台电脑,鼠标可以直接从一台电脑的屏幕滑动到另一台电脑屏幕上,同时键盘、粘贴板也都是共享的。 设想这么个情景,我在我的台式机 Windows 上打开了一个页面,需要让我输入一个很长的序列号,而这个序列号又恰巧存在 Mac 上,这时如果有了 Synergy 将二者关联,我们只需要把鼠标从 Windows 的屏幕上直接滑动到 Mac 的屏幕上,选中序列号,然后键盘按下复制的快捷键,然后再把鼠标移回 Windows,粘贴即可,一气呵成。而不必再想办法发消息传输了,大大提高效率。 推荐指数:★★★★

Feedly、Reeder

博客现在已经越来越多了,越来越多的人开始在博客上发表文章,而当我们遇到优质的博客时,我们还想随时知道博客的发表动态,一旦有新文章发表我们想立马得到相关动态,这样可以实现吗? 肯定是可行的,现在绝大多数博客都有 RSS 订阅功能,有了它我们可以订阅自己喜欢的博客,这里我使用的 RSS 订阅工具就是 Feedly,利用它我可以很轻松地添加自己喜欢的博客或论坛到自己的 Feed 流里面,一旦有文章更新,我就会收到相应提示。 但是 Feedly 有个小问题,就是在国内速度太慢了,所以我又使用 Reeder 将 Feedly 里面的 Feed 流做了转接,它可以添加 Feedly 源,并带有灵活的分类、标记等管理功能,还支持各种预览方式,还支持存储到 Pocket,还有各种分享方式,功能十分齐全。 总之,推荐 Feedly 来添加自己喜欢的博客,用 Reeder 来阅读订阅的内容,双剑合璧,另外 Reeder 对移动版的支持也很不错,可以体验一下。 推荐指数:★★★★

Mindnode

有时候在思考问题的时候我们想要把一些思路记录下来,另外在做一些概要设计的时候需要把概要图大体描述出来,这时候画一个思维导图再合适不过了,比如你现在读的这篇文章就很适合用一个思维导图画一下。 画思维导图我个人比较喜欢的一款软件是 Mindnode,觉得比较简洁好用,当然也有不少人使用 XMind,也很不错。可能是先入为主,也可能是界面设计风格,我个人更加偏向于使用 Mindnode。 推荐指数:★★★★

1Password

随着年龄的增长,我们可能变得越来越忘事了。另外还有些反人类的网站密码必须要至少大写、小写、数字、特殊符号,有的还要求不少于多少位,有的还要求我么能定时更换密码,还不能与之前用过的相同!这会使得我们之前预想设计的很多密码都没法用了。另外网站又这么多,谁又能把网站的密码都记下来啊? 这时候我们就需要一款专门管理密码的软件,我个人推荐一款叫做 1Password,有了它我们可以将各个平台的密码保存起来,同时它还可以根据我们的要求帮我们随机生成一些密码并保存,这对注册一些新网站非常有用,同时使用随机的密码还降低了撞库的风险,不然一个平台的密码被盗了,其他平台用的同样的密码的话,就很不安全了。 1Password 还支持各种平台,如网页、PC、移动版都通通完美支持,实现密码云同步,妈妈再也不用担心我忘记密码了! 推荐指数:★★★★

系统工具

下面介绍的两款系统工具软件几乎是装机必备的。

Tuxera NTFS For Mac

用了 Mac,我们在使用移动硬盘的时候可能会遇到一个无法传输数据(如拷贝文件)的问题,这是因为部分移动硬盘是 NTFS 格式的,而 Mac 的磁盘不是这个格式,因此就会导致二者之间无法拷贝文件。有一个解决方法就是使用 Tuxera NTFS For Mac,有了它,我们就可以比较顺利地拷贝文件了。 另外还有其他品牌的 NTFS For Mac 软件,也可以尝试使用一下。 推荐指数:★★★★☆

VMware、Parallels Desktop

用了 Mac 之后,难免会有些情况下也还会不得不使用 Windows,毕竟很多软件可能只有 Windows 版本,但用 Mac 我就不推荐装双系统了,直接装虚拟机就好了,Mac 上虚拟机软件有两款比较好用,一个就是著名的 VMware,另一个就是 Parallels Desktop,这两款我都使用过,觉得都非常不错,现在用的是 VMware。 推荐指数:★★★★☆

CleanMyMac

很多时候用着用着磁盘就不够用了,如果你的 Mac 硬盘是 512GB 的倒还好,256GB 的你就得多注意一下了,另外 1T 定制版土豪请绕道,这款软件不适合你。 CleanMyMac 可以非常方便地帮助我们扫描缓存、大文件、废纸篓、残留项等内容,清理这些内容之后我们可以节省很多硬盘空间,另外它还支持软件卸载和残留清扫功能,可以帮我们非常干净地移除 Mac 中的软件,目前应该是出到第三版了,非常推荐。 推荐指数:★★★★☆

编辑器

既然做程序开发嘛,不配置好自己的开发环境怎么行,下面推荐一下我平常使用的开发软件。

JetBrains

我目前使用的 IDE 是 JetBrains 全家桶,目前我编写 Python 比较多,所以主要使用 PyCharm,另外写前端的时候也会使用 WebStorm,写 Java 就用 IntelliJ IDEA,C、C++ 用 CLion,PHP 的话就用 PhpStorm,Ruby 的话就用 RubyMine,其他的语言用的就少了,就没有装了。 当然有的小伙伴会说 JetBrains 系列的 IDE 需要购买啊?我只想说,国人的力量是无穷的,在网上其实可以搜到各种破解方法,如 License Server 验证,你能搜到各种五花八门的 License Server。另外 JetBrains 还有专门的 Educational Programs,可以来这里申请:https://www.jetbrains.com/education/programs/?fromMenu,学生、老师或教育工作者可以使用学校的 edu 邮箱申请免费的 License,如果你还是学生的话,那么申请是十分方便的,因为我还是个学生,我目前就在使用学生套餐,当然如果你已经工作的话也可以向正在上学的弟弟妹妹们借一下嘛。 总之我个人比较喜欢 JetBrains 全家桶,不论是页面风格还是开发习惯我都比较喜欢,推荐使用。 推荐指数:★★★★☆

Sublime

有时候我们可能下载了或接收了一些单个的文本文件,我们只想看看文本文件内容是什么,或者对其再做一些简单的修改操作,这时候就没必要单独用 JetBrains 的 IDE 打开了,显得有点重了。或者有时候需要修改某个配置文件,这时候也需要一个比较好用的编辑器。我使用的就是 Sublime,对于一些日常的文本编辑是足够了,另外 Sublime 还可以扩展好多插件,配置好了功能上基本不输 JetBrains IDE,非常推荐。 推荐指数:★★★★

MarkEditor

现在越来越多的写作平台开始支持 MarkDown,不得不说这确实是一门提高文字生产效率的语言,写 MarkDown 我强烈推荐 MarkEditor,我之前尝试过各种 MarkDown 写作软件,觉得都不如这款好用,如 Typora、MWeb、GitBook 等等。 MarkEditor 支持写作及预览模式,更重要的是支持文件管理,很多软件如 Typora 只能打开单个的 Makrdown 文件,不能打开整个文件夹,这就很鸡肋了。另外 MarkEditor 支持直接插入图片,如我们截了一张图或者刚从网上复制了一张图,在 MarkEditor 里面直接粘贴就可以了,它会自动把这张图保存到当前目录下,同时生成 Makrdown 格式的的图片链接,不能更方便了!另外还支持主题自定义、样式自定义,还可以快速插入某些 Makrdown 元素,还支持 Latex 公式,还可以快速导出电子书,快速生成文稿网页,快速局域网共享,功能应有尽有,强烈推荐! 这个软件我购买了 Pro 版,解锁了全部功能,订购地址:https://www.markeditor.com/,个人觉得物超所值! 推荐指数:★★★★★

SnippetLab

在写代码的时候,我们经常会有一些常用代码或者精华代码,或者一些常用的配置,想要单独保存下来复用,这时我们可能会把它保存到某个文本文件里面,更高级点可以使用云笔记,如有道云笔记或者印象笔记,用过 GitHub Gists 的小伙伴可能会选择 GitHub Gists,但我觉得这些都不是最佳的。 首先文本文件、云笔记里面其实并不是专门为了保存代码使用的,另外 GitHub Gists 保存操作并没有那么便捷,而且打开速度也很慢,影响体验。在这里推荐一款专门用来保存代码的软件叫做 SnippetLab,涉设计初衷就是为了保存短代码片的,它支持几乎所有编程语言,另外支持分类、分级、加标签、加描述等,另外它还可以和 Alfred 对接实现快速搜索查找,另外还支持备份、导出、云同步等各种功能,非常适合做代码片的管理。 推荐指数:★★★★

Beyond Compare

有时候我们需要比较两个文件的不同之处,以便于快速得知两个版本的修改内容,我使用的软件是 Beyond Compare,个人觉得比较简洁好用,同时删除和添加的内容有对应的红绿颜色标识,推荐给大家使用。 推荐指数:★★★☆

管理工具

有时候我们需要管理很多文件,或者还需要远程管理很多终端设备,在这里推荐几款比较好用的工具。

Filezlla

有时候我们需要管理一些远程的服务器,比如 Linux 服务器。那么如何和这些服务器之间传递数据和文件呢?这里推荐一个轻便简洁的软件 Filezlla,它支持 FTP、SFTP 等协议类型,使用它我们可以方便地进行文件传输和远程文件管理。 推荐指数:★★★

ForkLift

Mac 上的 Finder 你是不是已经受够了?在一些方面做得相当不友好,例如在当前打开的目录下新建一个空白文件,在当前的目录下打开命令行工具等等,有了 ForkLift 这些都是小意思了。另外 ForkLift 还集成了 Filezlla 的功能,利用它我们还可以像普通文件管理器一样管理远程的主机内容,它还支持 FTP、SFTP、SMB、WebDAV、NFS 等等各种协议。同时界面也非常美观,有了它,几乎可以抛弃 Finder 和 Filezlla 了,强烈推荐! 推荐指数:★★★★☆

SSH Shell

我们经常会和各种服务器打交道,例如我们经常使用 SSH 来远程连接某台 Linux 服务器,原生 Terminal 是支持 SSH 的,但你会发现原生带的这个太难用了。可能很多小伙伴使用 iTerm,不得不说这确实是个神器,大大方便了远程管理流程。但我在这里还要推荐一个我经常使用的 SSH Shell,没错,它的名字就是 SSH Shell,它的页面操作简洁,同时管理和记录远程主机十分方便,另外还支持秘钥管理、自动重连、自定义主题等等功能,个人用起来十分顺手,强烈推荐! 推荐指数:★★★★☆

HomeBrew、CakeBrew

对于开发者来说,这个软件几乎是 Mac 上必备的一个软件,它的官方简介就是 “The missing package manager for macOS”,算是 Mac 上的一个软件包平台,它里面包含着非常多的 Mac 开发软件包,比如 Python、PHP、Redis、MySQL、RabbitMQ、HBase 等等,几乎你能想到的开发软件都集成在里面了,堪称神器! 它的安装也非常简单,参见这里:https://brew.sh/,另外 HomeBrew 也有对应的图形界面,叫做 CakeBrew,如果不喜欢命令行操作的话可以使用 CakeBrew 来代替。 推荐指数:★★★★★

影音图像

IINA

这个必须要赞一下,非常强大简洁好用的视频播放器,是 GitHub 上的一个开源软件,链接是:https://lhc70000.github.io/iina/,播放控制、视频设置、音频设置、字幕设置、文件操作,几乎你能想到的应有尽有,而且无广告,简洁清爽,支持的视频格式也十分广泛,推荐使用! 推荐指数:★★★★

ScreenFlow

之前我曾录制过一些 Python 的视频课程,本来尝试过 QuickTime 录制,可是实在是太难用了,另外视频剪辑、音频剪辑等又是个麻烦事。后来我就使用了 ScreenFlow,它集录制、剪辑、配音、字幕、特效等功能于一体,另外录制质量,渲染质量也是一流,大大提高了我的效率,堪称神器! 推荐指数:★★★★☆

iPic

有时候我们在写 MarkDown 的时候,可能突然需要一张插入一张图片,比如我们想插入一张屏幕截图,我们就需要把这张图片先存下来,然后加上图片的路径,如果转发给别人还需要连着图片一并发给对方,这其实是不怎么方便的,倘若这张图片是一张来自网络的图片,我们直接用 HTTP 访问的话,那岂不是方便太多了? 要将图片传到网上分几步?三步。第一步,把上传页面打开,第二步,把图片传到网上并把传后链接拷贝下来,第三步,把上传页面关闭。简直是太麻烦了对不对?另外找个合适的图床也是个麻烦事啊,七牛?又拍?你不得又得申请和注册。那么有了 iPic,一切就不是难事了,它可以监听 Mac 的粘贴板,一旦我们复制了一张图或者新截了一张图,它就能显示到待上传队列里面,我们点一下它就会把图片上传到网络上,然后生成上传后的链接,默认使用的是新浪的图床,网速也非常快。有了它,传图什么的都不是事了!另外付费版还支持各种自定义图床,如七牛云、又拍云、阿里云、腾讯云等等。 推荐指数:★★★★☆

PixelMator

在 Windows 上我们常用 PS 来修改和处理图片,Mac 上我是没有使用 PS,使用了 PixelMator,个人觉得使用这款软件能完全胜任 PS 的工作,一般的图片设计、排版、抠图、特效、蒙版等操作都支持,我个人比较喜欢使用这款软件做设计。 推荐指数:★★★★

Polarr Photo Editor

这个软件又名“泼辣修图”,类似 Mac 上的美图秀秀,它自带了各种后期滤镜,还带有 Lightroom 的很多调光调色的工具,能够帮我们快速对照片进行后期处理,效果也还不错,当然比不上 Photoshop 和 Lightroom 那么专业,但对于快速进行后处理的小伙伴来说不失为一个好的选择。 推荐指数:★★★★

Boom2

我有边工作边听歌的习惯,所以音乐几乎离不开我的生活,入了个好耳机,那当然就得配上好音乐。大家肯定也听说过音效均衡器,我们可以调整不同的音效参数来达到不同的声音效果,如电子音、人声、环绕、重低音等等,在 Mac 上我觉得最好用的就是 Boom2 了,它内置了各种音效均衡器,还有一些高保真效果的渲染,效果非常给力。我一般听歌的时候就会把 Boom2 开起来,享受不一样的音效感觉,美哉。 推荐指数:★★★★

趣味扩展

另外还有几个比较有意思的工具推荐下。

Tickeys

使用过机械键盘吗?按键感觉和声音很爽吧,但是用了 Mac,你如果不使用外接键盘的话,想必手感就差上不少,但这款软件或许可以拯救一下,它可以模拟机械键盘的按键声,每次按键都有有机械键盘清脆的声音,我平时戴耳机撸代码的时候就会开着这个软件,感觉体验还是不错的,建议尝试一下。 推荐指数:★★★☆

Duet

Duet 这款软件可以将 iPad 或 iPhone 变成电脑的扩展屏幕,如果你有一个大屏的比如 12.9 寸的 iPad 的话,非常建议你尝试一下这款软件,这样如果正你在用 Mac 不用 iPad 的话,完全可以用 Duet 把 iPad 和电脑屏幕连接起来来扩展显示,充分利用资源。 推荐指数:★★★☆ 好了,暂时推荐这么多,其实还有很多很多,尤其是专门针对于开发者的一些工具,这些就太偏极客化了,后面再为大家整理一些好用的开发者工具,敬请期待。 还不尽兴的小伙伴可以关注 GitHub 上的一个仓库叫 awesome-mac,里面列出来了 Mac 上推荐的非常多的软件,总结得非常非常详细,链接是:https://github.com/jaywcjlove/awesome-mac,大家可以去看下。

Tips

可能有的小伙伴好奇我的 Launchpad 为啥能放那么多图标,是怎么做到的?其实很简单,几行代码就搞定了。 调整每列显示图标数量,这里以 7 为例:

1
defaults write com.apple.dock springboard-rows -int 7

调整每行显示图标的数量,这里以 8 为例:

1
defaults write com.apple.dock springboard-columns -int 8

上面两行代码最后的数字可以自行修改。 修改完了之后还需要重置一下 Launchpad,代码如下:

1
defaults write com.apple.dock ResetLaunchPad -bool TRUE;killall Dock

好了,这样我们就可以自由定制我们的 Launchpad 图标数量啦! 另外,还有的小伙伴会说,很多软件都需要花钱购买啊,咋办?告诉你个网址:http://xclient.info/,几乎你想找的破解版都有,别说别的了,雷锋也别叫了,省下的钱打赏给我一点就行哈哈。 以上就是我的一些 Mac 常用软件分享及 Tips,希望对大家有帮助! 另外大家如有还有推荐的软件,欢迎留言给我,非常感谢!

Python

嗨~ 给大家重磅推荐一本书!上市两月就已经重印 4 次的 Python 爬虫书!它就是由静觅博客博主崔庆才所作的《Python3 网络爬虫开发实战》!!!同时文末还有抽奖赠书活动,不容错过!!!

书籍介绍

本书《Python3 网络爬虫开发实战》全面介绍了利用 Python3 开发网络爬虫的知识,书中首先详细介绍了各种类型的环境配置过程和爬虫基础知识,还讨论了 urllib、requests 等请求库和 Beautiful Soup、XPath、pyquery 等解析库以及文本和各类数据库的存储方法,另外本书通过多个真实新鲜案例介绍了分析 Ajax 进行数据爬取,Selenium 和 Splash 进行动态网站爬取的过程,接着又分享了一些切实可行的爬虫技巧,比如使用代理爬取和维护动态代理池的方法、ADSL 拨号代理的使用、各类验证码(图形、极验、点触、宫格等)的破解方法、模拟登录网站爬取的方法及 Cookies 池的维护等等。 此外,本书的内容还远远不止这些,作者还结合移动互联网的特点探讨了使用 Charles、mitmdump、Appium 等多种工具实现 App 抓包分析、加密参数接口爬取、微信朋友圈爬取的方法。此外本书还详细介绍了 pyspider 框架、Scrapy 框架的使用和分布式爬虫的知识,另外对于优化及部署工作,本书还包括 Bloom Filter 效率优化、Docker 和 Scrapyd 爬虫部署、分布式爬虫管理框架 Gerapy 的分享。 全书共 604 页,足足两斤重呢~ 定价为 99 元!

作者介绍

看书就先看看谁写的嘛,我们来了解一下~ 崔庆才,静觅博客博主(https://cuiqingcai.com),博客 Python 爬虫博文阅读量已过千万,北京航空航天大学硕士,天善智能、网易云课堂讲师,微软小冰大数据工程师,有多个大型分布式爬虫项目经验,乐于技术分享,文章通俗易懂 ^^ 附皂片一张 ~(@^^@)~

图文介绍

呕心沥血设计的宣传图也得放一下~

专家评论

书是好是坏,得让专家看评一评呀,那么下面就是几位专家的精彩评论,快来看看吧~ 在互联网软件开发工程师的分类中,爬虫工程师是非常重要的。爬虫工作往往是一个公司核心业务开展的基础,数据抓取下来,才有后续的加工处理和最终展现。此时数据的抓取规模、稳定性、实时性、准确性就显得非常重要。早期的互联网充分开放互联,数据获取的难度很小。随着各大公司对数据资产日益看重,反爬水平也在不断提高,各种新技术不断给爬虫软件提出新的课题。本书作者对爬虫的各个领域都有深刻研究,书中探讨了 Ajax 数据的抓取、动态渲染页面的抓取、验证码识别、模拟登录等高级话题,同时也结合移动互联网的特点探讨了 App 的抓取等。更重要的是,本书提供了大量源码,可以帮助读者更好地理解相关内容。强烈推荐给各位技术爱好者阅读!

——梁斌,八友科技总经理

数据既是当今大数据分析的前提,也是各种人工智能应用场景的基础。得数据者得天下,会爬虫者走遍天下也不怕!一册在手,让小白到老司机都能有所收获!

——李舟军,北京航空航天大学教授,博士生导师

本书从爬虫入门到分布式抓取,详细介绍了爬虫技术的各个要点,并针对不同的场景提出了对应的解决方案。另外,书中通过大量的实例来帮助读者更好地学习爬虫技术,通俗易懂,干货满满。强烈推荐给大家!

——宋睿华,微软小冰首席科学家

有人说中国互联网的带宽全给各种爬虫占据了,这说明网络爬虫的重要性以及中国互联网数据封闭垄断的现状。爬是一种能力,爬是为了不爬。

——施水才,北京拓尔思信息技术股份有限公司总裁

全书目录

书的目录也有~ 看这里!

  • 1-开发环境配置
  • 1.1-Python3 的安装
  • 1.2-请求库的安装
  • 1.3-解析库的安装
  • 1.4-数据库的安装
  • 1.5-存储库的安装
  • 1.6-Web 库的安装
  • 1.7-App 爬取相关库的安装
  • 1.8-爬虫框架的安装
  • 1.9-部署相关库的安装
  • 2-爬虫基础
  • 2.1-HTTP 基本原理
  • 2.2-网页基础
  • 2.3-爬虫的基本原理
  • 2.4-会话和 Cookies
  • 2.5-代理的基本原理
  • 3-基本库的使用
  • 3.1-使用 urllib
  • 3.1.1-发送请求
  • 3.1.2-处理异常
  • 3.1.3-解析链接
  • 3.1.4-分析 Robots 协议
  • 3.2-使用 requests
  • 3.2.1-基本用法
  • 3.2.2-高级用法
  • 3.3-正则表达式
  • 3.4-抓取猫眼电影排行
  • 4-解析库的使用
  • 4.1-使用 XPath
  • 4.2-使用 Beautiful Soup
  • 4.3-使用 pyquery
  • 5-数据存储
  • 5.1-文件存储
  • 5.1.1-TXT 文本存储
  • 5.1.2-JSON 文件存储
  • 5.1.3-CSV 文件存储
  • 5.2-关系型数据库存储
  • 5.2.1-MySQL 存储
  • 5.3-非关系型数据库存储
  • 5.3.1-MongoDB 存储
  • 5.3.2-Redis 存储
  • 6-Ajax 数据爬取
  • 6.1-什么是 Ajax
  • 6.2-Ajax 分析方法
  • 6.3-Ajax 结果提取
  • 6.4-分析 Ajax 爬取今日头条街拍美图
  • 7-动态渲染页面爬取
  • 7.1-Selenium 的使用
  • 7.2-Splash 的使用
  • 7.3-Splash 负载均衡配置
  • 7.4-使用 Selenium 爬取淘宝商品
  • 8-验证码的识别
  • 8.1-图形验证码的识别
  • 8.2-极验滑动验证码的识别
  • 8.3-点触验证码的识别
  • 8.4-微博宫格验证码的识别
  • 9-代理的使用
  • 9.1-代理的设置
  • 9.2-代理池的维护
  • 9.3-付费代理的使用
  • 9.4-ADSL 拨号代理
  • 9.5-使用代理爬取微信公众号文章
  • 10-模拟登录
  • 10.1-模拟登录并爬取 GitHub
  • 10.2-Cookies 池的搭建
  • 11-App 的爬取
  • 11.1-Charles 的使用
  • 11.2-mitmproxy 的使用
  • 11.3-mitmdump 爬取“得到”App 电子书信息
  • 11.4-Appium 的基本使用
  • 11.5-Appium 爬取微信朋友圈
  • 11.6-Appium+mitmdump 爬取京东商品
  • 12-pyspider 框架的使用
  • 12.1-pyspider 框架介绍
  • 12.2-pyspider 的基本使用
  • 12.3-pyspider 用法详解
  • 13-Scrapy 框架的使用
  • 13.1-Scrapy 框架介绍
  • 13.2-Scrapy 入门
  • 13.3-Selector 的用法
  • 13.4-Spider 的用法
  • 13.5-Downloader Middleware 的用法
  • 13.6-Spider Middleware 的用法
  • 13.7-Item Pipeline 的用法
  • 13.8-Scrapy 对接 Selenium
  • 13.9-Scrapy 对接 Splash
  • 13.10-Scrapy 通用爬虫
  • 13.11-Scrapyrt 的使用
  • 13.12-Scrapy 对接 Docker
  • 13.13-Scrapy 爬取新浪微博
  • 14-分布式爬虫
  • 14.1-分布式爬虫原理
  • 14.2-Scrapy-Redis 源码解析
  • 14.3-Scrapy 分布式实现
  • 14.4-Bloom Filter 的对接
  • 15-分布式爬虫的部署
  • 15.1-Scrapyd 分布式部署
  • 15.2-Scrapyd-Client 的使用
  • 15.3-Scrapyd 对接 Docker
  • 15.4-Scrapyd 批量部署
  • 15.5-Gerapy 分布式管理

购买链接

想必很多小伙伴已经等了很久了,之前预售那么久也一直迟迟没有货,发售就有不少网店又售空了,不过现在起不用担心了!

书籍现已在京东、天猫、当当等网店上架并全面供应啦,复制链接到浏览器打开或扫描二维码打开即可购买了!

京东商城

https://item.jd.com/12333540.html

天猫商城

https://detail.tmall.com/item.htm?id=566699703917

当当网

http://product.dangdang.com/25249602.html

欢迎大家购买,谢谢支持!O(∩_∩)O

免费预览

不放心?想先看看有些啥,没问题!看这里: 免费章节试读(复制粘贴至浏览器打开): https://cuiqingcai.com/5052.html 将一直免费开放前 7 章节,欢迎大家试读! 好了,接下来就是我们的福利环节啦~

福利一:抽奖送书!!!

恭喜你看到这里了!那么接下来的福利时间就到了!后面还有两个福利不容错过哦~ 抽奖送书活动第二波来袭(后面还有很多波哦),公众号抽奖送 30 本作者亲笔签名书籍!!! 活动流程(重要,请一定认真阅读): 公众号进击的 Coder 回复 “抽奖” 获取抽奖码,2018.6.24 22:00 截止,逾期参与无效,请记住您的抽奖码,活动结束后会从参与活动的小伙伴中根据幸运值按照权重比例抽取 30 位并在微信公众号公布,届时请关注公众号抽奖结果的公布!获奖的小伙伴会获得作者亲笔签名的《Python3 网络爬虫开发实战》一本。

福利二:独家优惠!!!

等等,你以为这就是全部福利吗?当然不是!除了抽奖送书,我们还拿到了拨号 VPS 知名品牌云立方的独家优惠,在公众号(进击的 Coder )中回复:“优惠券”,即可免费领取云立方 50 元主机优惠券,数量有限,先到先得!优惠券可在云立方官网(www.yunlifang.cn)购买动态IP拨号VPS时抵扣现金,有了它,爬虫代理易如反掌! 你问我动态拨号 VPS 能做什么?应该怎么用在爬虫里?来这里了解一下: 轻松获得海量稳定代理!ADSL 拨号代理的搭建

福利三:视频课程!!!

当然除了书籍,也有配套的视频课程,作者同样是崔庆才,二者结合学习效果更佳!限时优惠折扣中!扫描下图中二维码即可了解详情! 最后也是最重要的就是参与活动的地址了!!!快来扫码回复领取属于你的福利吧!!!

特别致谢

最后特别感谢云立方、天善智能对本活动的大力支持!

Python

在做自然语言处理的过程中,我们经常会遇到需要找出相似语句的场景,或者找出句子的近似表达,这时候我们就需要把类似的句子归到一起,这里面就涉及到句子相似度计算的问题,那么本节就来了解一下怎么样来用 Python 实现句子相似度的计算。

基本方法

句子相似度计算我们一共归类了以下几种方法:

  • 编辑距离计算
  • 杰卡德系数计算
  • TF 计算
  • TFIDF 计算
  • Word2Vec 计算

下面我们来一一了解一下这几种算法的原理和 Python 实现。

编辑距离计算

编辑距离,英文叫做 Edit Distance,又称 Levenshtein 距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数,如果它们的距离越大,说明它们越是不同。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。 例如我们有两个字符串:string 和 setting,如果我们想要把 string 转化为 setting,需要这么两步:

  • 第一步,在 s 和 t 之间加入字符 e。
  • 第二步,把 r 替换成 t。

所以它们的编辑距离差就是 2,这就对应着二者要进行转化所要改变(添加、替换、删除)的最小步数。 那么用 Python 怎样来实现呢,我们可以直接使用 distance 库:

1
2
3
4
5
6
7
8
import distance

def edit_distance(s1, s2):
return distance.levenshtein(s1, s2)

s1 = 'string'
s2 = 'setting'
print(edit_distance(s1, s2))

这里我们直接使用 distance 库的 levenshtein() 方法,传入两个字符串,即可获取两个字符串的编辑距离了。 运行结果如下:

1
2

这里的 distance 库我们可以直接使用 pip3 来安装:

1
pip3 install distance

这样如果我们想要获取相似的文本的话可以直接设定一个编辑距离的阈值来实现,如设置编辑距离为 2,下面是一个样例:

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

def edit_distance(s1, s2):
return distance.levenshtein(s1, s2)

strings = [
'你在干什么',
'你在干啥子',
'你在做什么',
'你好啊',
'我喜欢吃香蕉'
]

target = '你在干啥'
results = list(filter(lambda x: edit_distance(x, target) <= 2, strings))
print(results)

这里我们定义了一些字符串,然后定义了一个目标字符串,然后用编辑距离 2 的阈值进行设定,最后得到的结果就是编辑距离在 2 及以内的结果,运行结果如下:

1
['你在干什么', '你在干啥子']

通过这种方式我们可以大致筛选出类似的句子,但是发现一些句子例如“你在做什么” 就没有被识别出来,但他们的意义确实是相差不大的,因此,编辑距离并不是一个好的方式,但是简单易用。

杰卡德系数计算

杰卡德系数,英文叫做 Jaccard index, 又称为 Jaccard 相似系数,用于比较有限样本集之间的相似性与差异性。Jaccard 系数值越大,样本相似度越高。 实际上它的计算方式非常简单,就是两个样本的交集除以并集得到的数值,当两个样本完全一致时,结果为 1,当两个样本完全不同时,结果为 0。 算法非常简单,就是交集除以并集,下面我们用 Python 代码来实现一下:

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
from sklearn.feature_extraction.text import CountVectorizer
import numpy as np


def jaccard_similarity(s1, s2):
def add_space(s):
return ' '.join(list(s))

# 将字中间加入空格
s1, s2 = add_space(s1), add_space(s2)
# 转化为TF矩阵
cv = CountVectorizer(tokenizer=lambda s: s.split())
corpus = [s1, s2]
vectors = cv.fit_transform(corpus).toarray()
# 求交集
numerator = np.sum(np.min(vectors, axis=0))
# 求并集
denominator = np.sum(np.max(vectors, axis=0))
# 计算杰卡德系数
return 1.0 * numerator / denominator


s1 = '你在干嘛呢'
s2 = '你在干什么呢'
print(jaccard_similarity(s1, s2))

这里我们使用了 Sklearn 库中的 CountVectorizer 来计算句子的 TF 矩阵,然后利用 Numpy 来计算二者的交集和并集,随后计算杰卡德系数。 这里值得学习的有 CountVectorizer 的用法,通过它的 fit_transform() 方法我们可以将字符串转化为词频矩阵,例如这里有两句话“你在干嘛呢”和“你在干什么呢”,首先 CountVectorizer 会计算出不重复的有哪些字,会得到一个字的列表,结果为:

1
['么', '什', '你', '呢', '嘛', '在', '干']

这个其实可以通过如下代码来获取,就是获取词表内容:

1
cv.get_feature_names()

接下来通过转化之后,vectors 变量就变成了:

1
2
[[0 0 1 1 1 1 1]
[1 1 1 1 0 1 1]]

它对应的是两个句子对应词表的词频统计,这里是两个句子,所以结果是一个长度为 2 的二维数组,比如第一句话“你在干嘛呢”中不包含“么”字,那么第一个“么”字对应的结果就是0,即数量为 0,依次类推。 后面我们使用了 np.min() 方法并传入了 axis 为 0,实际上就是获取了每一列的最小值,这样实际上就是取了交集,np.max() 方法是获取了每一列的最大值,实际上就是取了并集。 二者分别取和即是交集大小和并集大小,然后作商即可,结果如下:

1
0.5714285714285714

这个数值越大,代表两个字符串越接近,否则反之,因此我们也可以使用这个方法,并通过设置一个相似度阈值来进行筛选。

TF 计算

第三种方案就是直接计算 TF 矩阵中两个向量的相似度了,实际上就是求解两个向量夹角的余弦值,就是点乘积除以二者的模长,公式如下:

1
cosθ=a·b/|a|*|b|

上面我们已经获得了 TF 矩阵,下面我们只需要求解两个向量夹角的余弦值就好了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
from scipy.linalg import norm

def tf_similarity(s1, s2):
def add_space(s):
return ' '.join(list(s))

# 将字中间加入空格
s1, s2 = add_space(s1), add_space(s2)
# 转化为TF矩阵
cv = CountVectorizer(tokenizer=lambda s: s.split())
corpus = [s1, s2]
vectors = cv.fit_transform(corpus).toarray()
# 计算TF系数
return np.dot(vectors[0], vectors[1]) / (norm(vectors[0]) * norm(vectors[1]))


s1 = '你在干嘛呢'
s2 = '你在干什么呢'
print(tf_similarity(s1, s2))

在在这里我们使用了 np.dot() 方法获取了向量的点乘积,然后通过 norm() 方法获取了向量的模长,经过计算得到二者的 TF 系数,结果如下:

1
0.7302967433402214

TFIDF 计算

另外除了计算 TF 系数我们还可以计算 TFIDF 系数,TFIDF 实际上就是在词频 TF 的基础上再加入 IDF 的信息,IDF 称为逆文档频率,不了解的可以看下阮一峰老师的讲解:http://www.ruanyifeng.com/blog/2013/03/tf-idf.html,里面对 TFIDF 的讲解也是十分透彻的。 下面我们还是借助于 Sklearn 中的模块 TfidfVectorizer 来实现,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np
from scipy.linalg import norm


def tfidf_similarity(s1, s2):
def add_space(s):
return ' '.join(list(s))

# 将字中间加入空格
s1, s2 = add_space(s1), add_space(s2)
# 转化为TF矩阵
cv = TfidfVectorizer(tokenizer=lambda s: s.split())
corpus = [s1, s2]
vectors = cv.fit_transform(corpus).toarray()
# 计算TF系数
return np.dot(vectors[0], vectors[1]) / (norm(vectors[0]) * norm(vectors[1]))


s1 = '你在干嘛呢'
s2 = '你在干什么呢'
print(tfidf_similarity(s1, s2))

这里的 vectors 变量实际上就对应着 TFIDF 值,内容如下:

1
2
[[0.         0.         0.4090901  0.4090901  0.57496187 0.4090901 0.4090901 ]
[0.49844628 0.49844628 0.35464863 0.35464863 0. 0.35464863 0.35464863]]

运行结果如下:

1
0.5803329846765686

所以通过 TFIDF 系数我们也可以进行相似度的计算。

Word2Vec 计算

Word2Vec,顾名思义,其实就是将每一个词转换为向量的过程。如果不了解的话可以参考:https://blog.csdn.net/itplus/article/details/37969519。 这里我们可以直接下载训练好的 Word2Vec 模型,模型的链接地址为:https://pan.baidu.com/s/1TZ8GII0CEX32ydjsfMc0zw,是使用新闻、百度百科、小说数据来训练的 64 维的 Word2Vec 模型,数据量很大,整体效果还不错,我们可以直接下载下来使用,这里我们使用的是 news_12g_baidubaike_20g_novel_90g_embedding_64.bin 数据,然后实现 Sentence2Vec,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import gensim
import jieba
import numpy as np
from scipy.linalg import norm

model_file = './word2vec/news_12g_baidubaike_20g_novel_90g_embedding_64.bin'
model = gensim.models.KeyedVectors.load_word2vec_format(model_file, binary=True)

def vector_similarity(s1, s2):
def sentence_vector(s):
words = jieba.lcut(s)
v = np.zeros(64)
for word in words:
v += model[word]
v /= len(words)
return v

v1, v2 = sentence_vector(s1), sentence_vector(s2)
return np.dot(v1, v2) / (norm(v1) * norm(v2))

在获取 Sentence Vector 的时候,我们首先对句子进行分词,然后对分好的每一个词获取其对应的 Vector,然后将所有 Vector 相加并求平均,这样就可得到 Sentence Vector 了,然后再计算其夹角余弦值即可。 调用示例如下:

1
2
3
s1 = '你在干嘛'
s2 = '你正做什么'
vector_similarity(s1, s2)

结果如下:

1
0.6701133967824016

这时如果我们再回到最初的例子看下效果:

1
2
3
4
5
6
7
8
9
10
11
12
strings = [
'你在干什么',
'你在干啥子',
'你在做什么',
'你好啊',
'我喜欢吃香蕉'
]

target = '你在干啥'

for string in strings:
print(string, vector_similarity(string, target))

依然是前面的例子,我们看下它们的匹配度结果是多少,运行结果如下:

1
2
3
4
5
你在干什么 0.8785495016487204
你在干啥子 0.9789649689827049
你在做什么 0.8781992402695274
你好啊 0.5174225914249863
我喜欢吃香蕉 0.582990841450621

可以看到相近的语句相似度都能到 0.8 以上,而不同的句子相似度都不足 0.6,这个区分度就非常大了,可以说有了 Word2Vec 我们可以结合一些语义信息来进行一些判断,效果明显也好很多。 所以总体来说,Word2Vec 计算的方式是非常好的。 另外学术界还有一些可能更好的研究成果,这个可以参考知乎上的一些回答:https://www.zhihu.com/question/29978268/answer/54399062。 以上便是进行句子相似度计算的基本方法和 Python 实现,本节代码地址:https://github.com/AIDeepLearning/SentenceDistance

Python

Scrapy-Redis 详解

通常我们在一个站站点进行采集的时候,如果是小站的话 我们使用scrapy本身就可以满足。 但是如果在面对一些比较大型的站点的时候,单个scrapy就显得力不从心了。 要是我们能够多个Scrapy一起采集该多好啊 人多力量大。 很遗憾Scrapy官方并不支持多个同时采集一个站点,虽然官方给出一个方法: 将一个站点的分割成几部分 交给不同的scrapy去采集 似乎是个解决办法,但是很麻烦诶!毕竟分割很麻烦的哇 下面就改轮到我们的额主角Scrapy-Redis登场了!

什么??你这么就登场了?还没说为什么呢?

好吧 为了简单起见 就用官方图来简单说明一下: 这张图大家相信大家都很熟悉了。重点看一下SCHEDULER 1. 先来看看官方对于SCHEDULER的定义: SCHEDULER接受来自Engine的Requests,并将它们放入队列(可以按顺序优先级),以便在之后将其提供给Engine 点我看文档 2. 现在我们来看看SCHEDULER都提供了些什么功能: 根据官方文档说明 在我们没有没有指定 SCHEDULER 参数时,默认使用:’scrapy.core.scheduler.Scheduler’ 作为SCHEDULER(调度器) scrapy.core.scheduler.py:

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
class Scheduler(object):

def __init__(self, dupefilter, jobdir=None, dqclass=None, mqclass=None,
logunser=False, stats=None, pqclass=None):
self.df = dupefilter
self.dqdir = self._dqdir(jobdir)
self.pqclass = pqclass
self.dqclass = dqclass
self.mqclass = mqclass
self.logunser = logunser
self.stats = stats
# 注意在scrpy中优先注意这个方法,此方法是一个钩子 用于访问当前爬虫的配置
@classmethod
def from_crawler(cls, crawler):
settings = crawler.settings
# 获取去重用的类 默认:scrapy.dupefilters.RFPDupeFilter
dupefilter_cls = load_object(settings['DUPEFILTER_CLASS'])
# 对去重类进行配置from_settings 在 scrapy.dupefilters.RFPDupeFilter 43行
# 这种调用方式对于IDE跳转不是很好 所以需要自己去找
# @classmethod
# def from_settings(cls, settings):
# debug = settings.getbool('DUPEFILTER_DEBUG')
# return cls(job_dir(settings), debug)
# 上面就是from_settings方法 其实就是设置工作目录 和是否开启debug
dupefilter = dupefilter_cls.from_settings(settings)
# 获取优先级队列 类对象 默认:queuelib.pqueue.PriorityQueue
pqclass = load_object(settings['SCHEDULER_PRIORITY_QUEUE'])
# 获取磁盘队列 类对象(SCHEDULER使用磁盘存储 重启不会丢失)
dqclass = load_object(settings['SCHEDULER_DISK_QUEUE'])
# 获取内存队列 类对象(SCHEDULER使用内存存储 重启会丢失)
mqclass = load_object(settings['SCHEDULER_MEMORY_QUEUE'])
# 是否开启debug
logunser = settings.getbool('LOG_UNSERIALIZABLE_REQUESTS', settings.getbool('SCHEDULER_DEBUG'))
# 将这些参数传递给 __init__方法
return cls(dupefilter, jobdir=job_dir(settings), logunser=logunser,
stats=crawler.stats, pqclass=pqclass, dqclass=dqclass, mqclass=mqclass)


def has_pending_requests(self):
"""检查是否有没处理的请求"""
return len(self) > 0

def open(self, spider):
"""Engine创建完毕之后会调用这个方法"""
self.spider = spider
# 创建一个有优先级的内存队列 实例化对象
# self.pqclass 默认是:queuelib.pqueue.PriorityQueue
# self._newmq 会返回一个内存队列的 实例化对象 在110 111 行
self.mqs = self.pqclass(self._newmq)
# 如果self.dqdir 有设置 就创建一个磁盘队列 否则self.dqs 为空
self.dqs = self._dq() if self.dqdir else None
# 获得一个去重实例对象 open 方法是从BaseDupeFilter继承的
# 现在我们可以用self.df来去重啦
return self.df.open()

def close(self, reason):
"""当然Engine关闭时"""
# 如果有磁盘队列 则对其进行dump后保存到active.json文件中
if self.dqs:
prios = self.dqs.close()
with open(join(self.dqdir, 'active.json'), 'w') as f:
json.dump(prios, f)
# 然后关闭去重
return self.df.close(reason)

def enqueue_request(self, request):
"""添加一个Requests进调度队列"""
# self.df.request_seen是检查这个Request是否已经请求过了 如果有会返回True
if not request.dont_filter and self.df.request_seen(request):
# 如果Request的dont_filter属性没有设置(默认为False)和 已经存在则去重
# 不push进队列
self.df.log(request, self.spider)
return False
# 先尝试将Request push进磁盘队列
dqok = self._dqpush(request)
if dqok:
# 如果成功 则在记录一次状态
self.stats.inc_value('scheduler/enqueued/disk', spider=self.spider)
else:
# 不能添加进磁盘队列则会添加进内存队列
self._mqpush(request)
self.stats.inc_value('scheduler/enqueued/memory', spider=self.spider)
self.stats.inc_value('scheduler/enqueued', spider=self.spider)
return True

def next_request(self):
"""从队列中获取一个Request"""
# 优先从内存队列中获取
request = self.mqs.pop()
if request:
self.stats.inc_value('scheduler/dequeued/memory', spider=self.spider)
else:
# 不能获取的时候从磁盘队列队里获取
request = self._dqpop()
if request:
self.stats.inc_value('scheduler/dequeued/disk', spider=self.spider)
if request:
self.stats.inc_value('scheduler/dequeued', spider=self.spider)
# 将获取的到Request返回给Engine
return request

def __len__(self):
return len(self.dqs) + len(self.mqs) if self.dqs else len(self.mqs)

def _dqpush(self, request):
if self.dqs is None:
return
try:
reqd = request_to_dict(request, self.spider)
self.dqs.push(reqd, -request.priority)
except ValueError as e: # non serializable request
if self.logunser:
msg = ("Unable to serialize request: %(request)s - reason:"
" %(reason)s - no more unserializable requests will be"
" logged (stats being collected)")
logger.warning(msg, {'request': request, 'reason': e},
exc_info=True, extra={'spider': self.spider})
self.logunser = False
self.stats.inc_value('scheduler/unserializable',
spider=self.spider)
return
else:
return True

def _mqpush(self, request):
self.mqs.push(request, -request.priority)

def _dqpop(self):
if self.dqs:
d = self.dqs.pop()
if d:
return request_from_dict(d, self.spider)

def _newmq(self, priority):
return self.mqclass()

def _newdq(self, priority):
return self.dqclass(join(self.dqdir, 'p%s' % priority))

def _dq(self):
activef = join(self.dqdir, 'active.json')
if exists(activef):
with open(activef) as f:
prios = json.load(f)
else:
prios = ()
q = self.pqclass(self._newdq, startprios=prios)
if q:
logger.info("Resuming crawl (%(queuesize)d requests scheduled)",
{'queuesize': len(q)}, extra={'spider': self.spider})
return q

def _dqdir(self, jobdir):
if jobdir:
dqdir = join(jobdir, 'requests.queue')
if not exists(dqdir):
os.makedirs(dqdir)
return dqdir

只挑了一些重点的写了一些注释剩下大家自己领会(才不是我懒哦 ) 从上面的代码 我们可以很清楚的知道 SCHEDULER的主要是完成了 push Request pop Request 和 去重的操作。 而且queue 操作是在内存队列中完成的。 大家看queuelib.queue就会发现基于内存的(deque) 那么去重呢?

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
class RFPDupeFilter(BaseDupeFilter):
"""Request Fingerprint duplicates filter"""

def __init__(self, path=None, debug=False):
self.file = None
self.fingerprints = set()
self.logdupes = True
self.debug = debug
self.logger = logging.getLogger(__name__)
if path:
# 此处可以看到去重其实打开了一个名叫 requests.seen的文件
# 如果是使用的磁盘的话
self.file = open(os.path.join(path, 'requests.seen'), 'a+')
self.file.seek(0)
self.fingerprints.update(x.rstrip() for x in self.file)

@classmethod
def from_settings(cls, settings):
debug = settings.getbool('DUPEFILTER_DEBUG')
return cls(job_dir(settings), debug)

def request_seen(self, request):
fp = self.request_fingerprint(request)
if fp in self.fingerprints:
# 判断我们的请求是否在这个在集合中
return True
# 没有在集合就添加进去
self.fingerprints.add(fp)
# 如果用的磁盘队列就写进去记录一下
if self.file:
self.file.write(fp + os.linesep)
  按照正常流程就是大家都会进行重复的采集;我们都知道进程之间内存中的数据不可共享的,那么你在开启多个Scrapy的时候,它们相互之间并不知道对方采集了些什么那些没有没采集。那就大家伙儿自己玩自己的了。完全没没有效率的提升啊! ![](https://thsheep-wordpress.oss-cn-beijing.aliyuncs.com/7015cb643d0c05854dab5b8457f076af.jpg) 怎么解决呢? 这就是我们Scrapy-Redis解决的问题了,不能协作不就是因为Request 和 去重这两个 不能共享吗? 那我把这两个独立出来好了。 将Scrapy中的SCHEDULER组件独立放到大家都能访问的地方不就OK啦!加上scrapy-redis后流程图就应该变成这样了? ![](https://thsheep-wordpress.oss-cn-beijing.aliyuncs.com/0a94645a8f10707fe80610b5ebeb945e.jpg) So············· 这样是不是看起来就清楚多了??? 下面我们来看看Scrapy-Redis是怎么处理的? scrapy_redis.scheduler.py:
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
class Scheduler(object):
"""Redis-based scheduler

Settings
--------
SCHEDULER_PERSIST : bool (default: False)
Whether to persist or clear redis queue.
SCHEDULER_FLUSH_ON_START : bool (default: False)
Whether to flush redis queue on start.
SCHEDULER_IDLE_BEFORE_CLOSE : int (default: 0)
How many seconds to wait before closing if no message is received.
SCHEDULER_QUEUE_KEY : str
Scheduler redis key.
SCHEDULER_QUEUE_CLASS : str
Scheduler queue class.
SCHEDULER_DUPEFILTER_KEY : str
Scheduler dupefilter redis key.
SCHEDULER_DUPEFILTER_CLASS : str
Scheduler dupefilter class.
SCHEDULER_SERIALIZER : str
Scheduler serializer.

"""

def __init__(self, server,
persist=False,
flush_on_start=False,
queue_key=defaults.SCHEDULER_QUEUE_KEY,
queue_cls=defaults.SCHEDULER_QUEUE_CLASS,
dupefilter_key=defaults.SCHEDULER_DUPEFILTER_KEY,
dupefilter_cls=defaults.SCHEDULER_DUPEFILTER_CLASS,
idle_before_close=0,
serializer=None):
"""Initialize scheduler.

Parameters
----------
server : Redis
这是Redis实例
persist : bool
是否在关闭时清空Requests.默认值是False。
flush_on_start : bool
是否在启动时清空Requests。 默认值是False。
queue_key : str
Request队列的Key名字
queue_cls : str
队列的可导入路径(就是使用什么队列)
dupefilter_key : str
去重队列的Key
dupefilter_cls : str
去重类的可导入路径。
idle_before_close : int
等待多久关闭

"""
if idle_before_close < 0:
raise TypeError("idle_before_close cannot be negative")

self.server = server
self.persist = persist
self.flush_on_start = flush_on_start
self.queue_key = queue_key
self.queue_cls = queue_cls
self.dupefilter_cls = dupefilter_cls
self.dupefilter_key = dupefilter_key
self.idle_before_close = idle_before_close
self.serializer = serializer
self.stats = None

def __len__(self):
return len(self.queue)

@classmethod
def from_settings(cls, settings):
kwargs = {
'persist': settings.getbool('SCHEDULER_PERSIST'),
'flush_on_start': settings.getbool('SCHEDULER_FLUSH_ON_START'),
'idle_before_close': settings.getint('SCHEDULER_IDLE_BEFORE_CLOSE'),
}

# If these values are missing, it means we want to use the defaults.
optional = {
# TODO: Use custom prefixes for this settings to note that are
# specific to scrapy-redis.
'queue_key': 'SCHEDULER_QUEUE_KEY',
'queue_cls': 'SCHEDULER_QUEUE_CLASS',
'dupefilter_key': 'SCHEDULER_DUPEFILTER_KEY',
# We use the default setting name to keep compatibility.
'dupefilter_cls': 'DUPEFILTER_CLASS',
'serializer': 'SCHEDULER_SERIALIZER',
}
# 从setting中获取配置组装成dict(具体获取那些配置是optional字典中key)
for name, setting_name in optional.items():
val = settings.get(setting_name)
if val:
kwargs[name] = val

# Support serializer as a path to a module.
if isinstance(kwargs.get('serializer'), six.string_types):
kwargs['serializer'] = importlib.import_module(kwargs['serializer'])
# 或得一个Redis连接
server = connection.from_settings(settings)
# Ensure the connection is working.
server.ping()

return cls(server=server, **kwargs)

@classmethod
def from_crawler(cls, crawler):
instance = cls.from_settings(crawler.settings)
# FIXME: for now, stats are only supported from this constructor
instance.stats = crawler.stats
return instance

def open(self, spider):
self.spider = spider

try:
# 根据self.queue_cls这个可以导入的类 实例化一个队列
self.queue = load_object(self.queue_cls)(
server=self.server,
spider=spider,
key=self.queue_key % {'spider': spider.name},
serializer=self.serializer,
)
except TypeError as e:
raise ValueError("Failed to instantiate queue class '%s': %s",
self.queue_cls, e)

try:
# 根据self.dupefilter_cls这个可以导入的类 实例一个去重集合
# 默认是集合 可以实现自己的去重方式 比如 bool 去重
self.df = load_object(self.dupefilter_cls)(
server=self.server,
key=self.dupefilter_key % {'spider': spider.name},
debug=spider.settings.getbool('DUPEFILTER_DEBUG'),
)
except TypeError as e:
raise ValueError("Failed to instantiate dupefilter class '%s': %s",
self.dupefilter_cls, e)

if self.flush_on_start:
self.flush()
# notice if there are requests already in the queue to resume the crawl
if len(self.queue):
spider.log("Resuming crawl (%d requests scheduled)" % len(self.queue))

def close(self, reason):
if not self.persist:
self.flush()

def flush(self):
self.df.clear()
self.queue.clear()

def enqueue_request(self, request):
"""这个和Scrapy本身的一样"""
if not request.dont_filter and self.df.request_seen(request):
self.df.log(request, self.spider)
return False
if self.stats:
self.stats.inc_value('scheduler/enqueued/redis', spider=self.spider)
# 向队列里面添加一个Request
self.queue.push(request)
return True

def next_request(self):
"""获取一个Request"""
block_pop_timeout = self.idle_before_close
# block_pop_timeout 是一个等待参数 队列没有东西会等待这个时间 超时就会关闭
request = self.queue.pop(block_pop_timeout)
if request and self.stats:
self.stats.inc_value('scheduler/dequeued/redis', spider=self.spider)
return request

def has_pending_requests(self):
return len(self) > 0

来先来看看 以上就是Scrapy-Redis中的SCHEDULER模块。下面我们来看看queue和本身的什么不同: scrapy_redis.queue.py 以最常用的优先级队列 PriorityQueue 举例:

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
51
52
53
54
55
56
57
58
59
60
61
class PriorityQueue(Base):
"""Per-spider priority queue abstraction using redis' sorted set"""
"""其实就是使用Redis的有序集合 来对Request进行排序,这样就可以优先级高的在有序集合的顶层 我们只需要"""
"""从上往下依次获取Request即可"""
def __len__(self):
"""Return the length of the queue"""
return self.server.zcard(self.key)

def push(self, request):
"""Push a request"""
"""添加一个Request进队列"""
# self._encode_request 将Request请求进行序列化
data = self._encode_request(request)
"""
d = {
'url': to_unicode(request.url), # urls should be safe (safe_string_url)
'callback': cb,
'errback': eb,
'method': request.method,
'headers': dict(request.headers),
'body': request.body,
'cookies': request.cookies,
'meta': request.meta,
'_encoding': request._encoding,
'priority': request.priority,
'dont_filter': request.dont_filter,
'flags': request.flags,
'_class': request.__module__ + '.' + request.__class__.__name__
}

data就是上面这个字典的序列化
在Scrapy.utils.reqser.py 中的request_to_dict方法中处理
"""

# 在Redis有序集合中数值越小优先级越高(就是会被放在顶层)所以这个位置是取得 相反数
score = -request.priority
# We don't use zadd method as the order of arguments change depending on
# whether the class is Redis or StrictRedis, and the option of using
# kwargs only accepts strings, not bytes.
# ZADD 是添加进有序集合
self.server.execute_command('ZADD', self.key, score, data)

def pop(self, timeout=0):
"""
Pop a request
timeout not support in this queue class
有序集合不支持超时所以就木有使用timeout了 这个timeout就是挂羊头卖狗肉
"""
"""从有序集合中取出一个Request"""
# use atomic range/remove using multi/exec
"""使用multi的原因是为了将获取Request和删除Request合并成一个操作(原子性的)在获取到一个元素之后 删除它,因为有序集合 不像list 有pop 这种方式啊"""
pipe = self.server.pipeline()
pipe.multi()
# 取出 顶层第一个
# zrange :返回有序集 key 中,指定区间内的成员。0,0 就是第一个了
# zremrangebyrank:移除有序集 key 中,指定排名(rank)区间内的所有成员 0,0也就是第一个了
# 更多请参考Redis官方文档
pipe.zrange(self.key, 0, 0).zremrangebyrank(self.key, 0, 0)
results, count = pipe.execute()
if results:
return self._decode_request(results[0])

以上就是SCHEDULER在处理Request的时候做的操作了。 是时候来看看SCHEDULER是怎么处理去重的了! 只需要注意这个?方法即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def request_seen(self, request):
"""Returns True if request was already seen.

Parameters
----------
request : scrapy.http.Request

Returns
-------
bool

"""
# 通过self.request_fingerprint 会生一个sha1的指纹
fp = self.request_fingerprint(request)
# This returns the number of values added, zero if already exists.
# 添加进一个Redis集合如果self.key这个集合中存在fp这个指纹会返回1 不存在返回0
added = self.server.sadd(self.key, fp)
return added == 0
这样大家就都可以访问同一个Redis 获取同一个spider的Request 在同一个位置去重,就不用担心重复啦 大概就像这样:
  1. spider1:检查一下这个Request是否在Redis去重,如果在就证明其它的spider采集过啦!如果不在就添加进调度队列,等待别 人获取。自己继续干活抓取网页 产生新的Request了 重复之前步骤。
  2. spider2:以相同的逻辑执行

可能有些小伙儿会产生疑问了~~!spider2拿到了别人的Request了 怎么能正确的执行呢?逻辑不会错吗? 这个不用担心啦 因为整Request当中包含了,所有的逻辑,回去看看上面那个序列化的字典。 总结一下:

  1. 1. Scrapy-Reids 就是将Scrapy原本在内存中处理的 调度(就是一个队列Queue)、去重、这两个操作通过Redis来实现
  2. 多个Scrapy在采集同一个站点时会使用相同的redis key(可以理解为队列)添加Request 获取Request 去重Request,这样所有的spider不会进行重复采集。效率自然就嗖嗖的上去了。
  3. 3. Redis是原子性的,好处不言而喻(一个Request要么被处理 要么没被处理,不存在第三可能)

另外Scrapy-Redis本身不支持Redis-Cluster,大量网站去重的话会给单机很大的压力(就算使用boolfilter 内存也不够整啊!) 改造方式很简单:

  1. 使用 rediscluster 这个包替换掉本身的Redis连接
  2. Redis-Cluster 不支持事务,可以使用lua脚本进行代替(lua脚本是原子性的哦)
  3. 注意使用lua脚本 不能写占用时间很长的操作(毕竟一大群人等着操作Redis 你总不能让人家等着吧)

以上!完毕 对于懒人小伙伴儿 看看这个我改好的: 集群版Scrapy-Redis PS: 支持Python3.6+ 哦 ! 其余的版本没测试过

Python

在 PyCon 2018 上,Mario Corchero 介绍了在开发过程中如何更方便轻松地记录日志的流程。

整个演讲的内容包括:

  • 为什么日志记录非常重要
  • 日志记录的流程是怎样的
  • 怎样来进行日志记录
  • 怎样进行日志记录相关配置
  • 日志记录使用常见误区

下面我们来梳理一下整个演讲的过程,其实其核心就是介绍了 logging 模块的使用方法和一些配置。

日志记录的重要性

在开发过程中,如果程序运行出现了问题,我们是可以使用我们自己的 Debug 工具来检测到到底是哪一步出现了问题,如果出现了问题的话,是很容易排查的。但程序开发完成之后,我们会将它部署到生产环境中去,这时候代码相当于是在一个黑盒环境下运行的,我们只能看到其运行的效果,是不能直接看到代码运行过程中每一步的状态的。在这个环境下,运行过程中难免会在某个地方出现问题,甚至这个问题可能是我们开发过程中未曾遇到的问题,碰到这种情况应该怎么办? 如果我们现在只能得知当前问题的现象,而没有其他任何信息的话,如果我们想要解决掉这个问题的话,那么只能根据问题的现象来试图复现一下,然后再一步步去调试,这恐怕是很难的,很大的概率上我们是无法精准地复现这个问题的,而且 Debug 的过程也会耗费巨多的时间,这样一旦生产环境上出现了问题,修复就会变得非常棘手。但这如果我们当时有做日志记录的话,不论是正常运行还是出现报错,都有相关的时间记录,状态记录,错误记录等,那么这样我们就可以方便地追踪到在当时的运行过程中出现了怎样的状况,从而可以快速排查问题。 因此,日志记录是非常有必要的,任何一款软件如果没有标准的日志记录,都不能算作一个合格的软件。作为开发者,我们需要重视并做好日志记录过程。

日志记录的流程框架

那么在 Python 中,怎样才能算作一个比较标准的日志记录过程呢?或许很多人会使用 print 语句输出一些运行信息,然后再在控制台观察,运行的时候再将输出重定向到文件输出流保存到文件中,这样其实是非常不规范的,在 Python 中有一个标准的 logging 模块,我们可以使用它来进行标注的日志记录,利用它我们可以更方便地进行日志记录,同时还可以做更方便的级别区分以及一些额外日志信息的记录,如时间、运行模块信息等。 接下来我们先了解一下日志记录流程的整体框架。 如图所示,整个日志记录的框架可以分为这么几个部分:

  • Logger:即 Logger Main Class,是我们进行日志记录时创建的对象,我们可以调用它的方法传入日志模板和信息,来生成一条条日志记录,称作 Log Record。
  • Log Record:就代指生成的一条条日志记录。
  • Handler:即用来处理日志记录的类,它可以将 Log Record 输出到我们指定的日志位置和存储形式等,如我们可以指定将日志通过 FTP 协议记录到远程的服务器上,Handler 就会帮我们完成这些事情。
  • Formatter:实际上生成的 Log Record 也是一个个对象,那么我们想要把它们保存成一条条我们想要的日志文本的话,就需要有一个格式化的过程,那么这个过程就由 Formatter 来完成,返回的就是日志字符串,然后传回给 Handler 来处理。
  • Filter:另外保存日志的时候我们可能不需要全部保存,我们可能只需要保存我们想要的部分就可以了,所以保存前还需要进行一下过滤,留下我们想要的日志,如只保存某个级别的日志,或只保存包含某个关键字的日志等,那么这个过滤过程就交给 Filter 来完成。
  • Parent Handler:Handler 之间可以存在分层关系,以使得不同 Handler 之间共享相同功能的代码。

以上就是整个 logging 模块的基本架构和对象功能,了解了之后我们详细来了解一下 logging 模块的用法。

日志记录的相关用法

总的来说 logging 模块相比 print 有这么几个优点:

  • 可以在 logging 模块中设置日志等级,在不同的版本(如开发环境、生产环境)上通过设置不同的输出等级来记录对应的日志,非常灵活。
  • print 的输出信息都会输出到标准输出流中,而 logging 模块就更加灵活,可以设置输出到任意位置,如写入文件、写入远程服务器等。
  • logging 模块具有灵活的配置和格式化功能,如配置输出当前模块信息、运行时间等,相比 print 的字符串格式化更加方便易用。

下面我们初步来了解下 logging 模块的基本用法,先用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
import logging

logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

在这里我们首先引入了 logging 模块,然后进行了一下基本的配置,这里通过 basicConfig 配置了 level 信息和 format 信息,这里 level 配置为 INFO 信息,即只输出 INFO 级别的信息,另外这里指定了 format 格式的字符串,包括 asctime、name、levelname、message 四个内容,分别代表运行时间、模块名称、日志级别、日志内容,这样输出内容便是这四者组合而成的内容了,这就是 logging 的全局配置。 接下来声明了一个 Logger 对象,它就是日志输出的主类,调用对象的 info() 方法就可以输出 INFO 级别的日志信息,调用 debug() 方法就可以输出 DEBUG 级别的日志信息,非常方便。在初始化的时候我们传入了模块的名称,这里直接使用 name 来代替了,就是模块的名称,如果直接运行这个脚本的话就是 main,如果是 import 的模块的话就是被引入模块的名称,这个变量在不同的模块中的名字是不同的,所以一般使用 name 来表示就好了,再接下来输出了四条日志信息,其中有两条 INFO、一条 WARNING、一条 DEBUG 信息,我们看下输出结果:

1
2
3
2018-06-03 13:42:43,526 - __main__ - INFO - This is a log info
2018-06-03 13:42:43,526 - __main__ - WARNING - Warning exists
2018-06-03 13:42:43,526 - __main__ - INFO - Finish

可以看到输出结果一共有三条日志信息,每条日志都是对应了指定的格式化内容,另外我们发现 DEBUG 的信息是没有输出的,这是因为我们在全局配置的时候设置了输出为 INFO 级别,所以 DEBUG 级别的信息就被过滤掉了。 这时如果我们将输出的日志级别设置为 DEBUG,就可以看到 DEBUG 级别的日志输出了:

1
logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

输出结果:

1
2
3
4
2018-06-03 13:49:22,770 - __main__ - INFO - This is a log info
2018-06-03 13:49:22,770 - __main__ - DEBUG - Debugging
2018-06-03 13:49:22,770 - __main__ - WARNING - Warning exists
2018-06-03 13:49:22,770 - __main__ - INFO - Finish

由此可见,相比 print 来说,通过刚才的代码,我们既可以输出时间、模块名称,又可以输出不同级别的日志信息作区分并加以过滤,是不是灵活多了? 当然这只是 logging 模块的一小部分功能,接下来我们首先来全面了解一下 basicConfig 的参数都有哪些:

  • filename:即日志输出的文件名,如果指定了这个信息之后,实际上会启用 FileHandler,而不再是 StreamHandler,这样日志信息便会输出到文件中了。
  • filemode:这个是指定日志文件的写入方式,有两种形式,一种是 w,一种是 a,分别代表清除后写入和追加写入。
  • format:指定日志信息的输出格式,即上文示例所示的参数,详细参数可以参考:docs.python.org/3/library/l…,部分参数如下所示:
    • %(levelno)s:打印日志级别的数值。
    • %(levelname)s:打印日志级别的名称。
    • %(pathname)s:打印当前执行程序的路径,其实就是sys.argv[0]。
    • %(filename)s:打印当前执行程序名。
    • %(funcName)s:打印日志的当前函数。
    • %(lineno)d:打印日志的当前行号。
    • %(asctime)s:打印日志的时间。
    • %(thread)d:打印线程ID。
    • %(threadName)s:打印线程名称。
    • %(process)d:打印进程ID。
    • %(processName)s:打印线程名称。
    • %(module)s:打印模块名称。
    • %(message)s:打印日志信息。
  • datefmt:指定时间的输出格式。
  • style:如果 format 参数指定了,这个参数就可以指定格式化时的占位符风格,如 %、{、$ 等。
  • level:指定日志输出的类别,程序会输出大于等于此级别的信息。
  • stream:在没有指定 filename 的时候会默认使用 StreamHandler,这时 stream 可以指定初始化的文件流。
  • handlers:可以指定日志处理时所使用的 Handlers,必须是可迭代的。

下面我们再用一个实例来感受一下:

1
2
3
4
5
6
7
8
9
10
11
12
import logging

logging.basicConfig(level=logging.DEBUG,
filename='output.log',
datefmt='%Y/%m/%d %H:%M:%S',
format='%(asctime)s - %(name)s - %(levelname)s - %(lineno)d - %(module)s - %(message)s')
logger = logging.getLogger(__name__)

logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

这里我们指定了输出文件的名称为 output.log,另外指定了日期的输出格式,其中年月日的格式变成了 %Y/%m/%d,另外输出的 format 格式增加了 lineno、module 这两个信息,运行之后便会生成一个 output.log 的文件,内容如下:

1
2
3
4
2018/06/03 14:43:26 - __main__ - INFO - 9 - demo3 - This is a log info
2018/06/03 14:43:26 - __main__ - DEBUG - 10 - demo3 - Debugging
2018/06/03 14:43:26 - __main__ - WARNING - 11 - demo3 - Warning exists
2018/06/03 14:43:26 - __main__ - INFO - 12 - demo3 - Finish

可以看到日志便会输出到文件中,同时输出了行号、模块名称等信息。 以上我们通过 basicConfig 来进行了一些全局的配置,我们同样可以使用 Formatter、Handler 进行更灵活的处理,下面我们来了解一下。

Level

首先我们来了解一下输出日志的等级信息,logging 模块共提供了如下等级,每个等级其实都对应了一个数值,列表如下:

等级

数值

CRITICAL

50

FATAL

50

ERROR

40

WARNING

30

WARN

30

INFO

20

DEBUG

10

NOTSET

0

这里最高的等级是 CRITICAL 和 FATAL,两个对应的数值都是 50,另外对于 WARNING 还提供了简写形式 WARN,两个对应的数值都是 30。 我们设置了输出 level,系统便只会输出 level 数值大于或等于该 level 的的日志结果,例如我们设置了输出日志 level 为 INFO,那么输出级别大于等于 INFO 的日志,如 WARNING、ERROR 等,DEBUG 和 NOSET 级别的不会输出。

1
2
3
4
5
6
7
8
9
10
11
import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.WARN)

# Log
logger.debug('Debugging')
logger.critical('Critical Something')
logger.error('Error Occurred')
logger.warning('Warning exists')
logger.info('Finished')

这里我们设置了输出级别为 WARN,然后对应输出了五种不同级别的日志信息,运行结果如下:

1
2
3
Critical Something
Error Occurred
Warning exists

可以看到只有 CRITICAL、ERROR、WARNING 信息输出了,DEBUG、INFO 信息没有输出。

Handler

下面我们先来了解一下 Handler 的用法,看下面的实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.INFO)
handler = logging.FileHandler('output.log')
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

这里我们没有再使用 basicConfig 全局配置,而是先声明了一个 Logger 对象,然后指定了其对应的 Handler 为 FileHandler 对象,然后 Handler 对象还单独指定了 Formatter 对象单独配置输出格式,最后给 Logger 对象添加对应的 Handler 即可,最后可以发现日志就会被输出到 output.log 中,内容如下:

1
2
3
2018-06-03 14:53:36,467 - __main__ - INFO - This is a log info
2018-06-03 14:53:36,468 - __main__ - WARNING - Warning exists
2018-06-03 14:53:36,468 - __main__ - INFO - Finish

另外我们还可以使用其他的 Handler 进行日志的输出,logging 模块提供的 Handler 有:

  • StreamHandler:logging.StreamHandler;日志输出到流,可以是 sys.stderr,sys.stdout 或者文件。
  • FileHandler:logging.FileHandler;日志输出到文件。
  • BaseRotatingHandler:logging.handlers.BaseRotatingHandler;基本的日志回滚方式。
  • RotatingHandler:logging.handlers.RotatingHandler;日志回滚方式,支持日志文件最大数量和日志文件回滚。
  • TimeRotatingHandler:logging.handlers.TimeRotatingHandler;日志回滚方式,在一定时间区域内回滚日志文件。
  • SocketHandler:logging.handlers.SocketHandler;远程输出日志到TCP/IP sockets。
  • DatagramHandler:logging.handlers.DatagramHandler;远程输出日志到UDP sockets。
  • SMTPHandler:logging.handlers.SMTPHandler;远程输出日志到邮件地址。
  • SysLogHandler:logging.handlers.SysLogHandler;日志输出到syslog。
  • NTEventLogHandler:logging.handlers.NTEventLogHandler;远程输出日志到Windows NT/2000/XP的事件日志。
  • MemoryHandler:logging.handlers.MemoryHandler;日志输出到内存中的指定buffer。
  • HTTPHandler:logging.handlers.HTTPHandler;通过”GET”或者”POST”远程输出到HTTP服务器。

下面我们使用三个 Handler 来实现日志同时输出到控制台、文件、HTTP 服务器:

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
import logging
from logging.handlers import HTTPHandler
import sys

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)

# StreamHandler
stream_handler = logging.StreamHandler(sys.stdout)
stream_handler.setLevel(level=logging.DEBUG)
logger.addHandler(stream_handler)

# FileHandler
file_handler = logging.FileHandler('output.log')
file_handler.setLevel(level=logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

# HTTPHandler
http_handler = HTTPHandler(host='localhost:8001', url='log', method='POST')
logger.addHandler(http_handler)

# Log
logger.info('This is a log info')
logger.debug('Debugging')
logger.warning('Warning exists')
logger.info('Finish')

运行之前我们需要先启动 HTTP Server,并运行在 8001 端口,其中 log 接口是用来接收日志的接口。 运行之后控制台输出会输出如下内容:

1
2
3
4
This is a log info
Debugging
Warning exists
Finish

output.log 文件会写入如下内容:

1
2
3
2018-06-03 15:13:44,895 - __main__ - INFO - This is a log info
2018-06-03 15:13:44,947 - __main__ - WARNING - Warning exists
2018-06-03 15:13:44,949 - __main__ - INFO - Finish

HTTP Server 会收到控制台输出的信息。 这样一来,我们就通过设置多个 Handler 来控制了日志的多目标输出。 另外值得注意的是,在这里 StreamHandler 对象我们没有设置 Formatter,因此控制台只输出了日志的内容,而没有包含时间、模块等信息,而 FileHandler 我们通过 setFormatter() 方法设置了一个 Formatter 对象,因此输出的内容便是格式化后的日志信息。 另外每个 Handler 还可以设置 level 信息,最终输出结果的 level 信息会取 Logger 对象的 level 和 Handler 对象的 level 的交集。

Formatter

在进行日志格式化输出的时候,我们可以不借助于 basicConfig 来全局配置格式化输出内容,可以借助于 Formatter 来完成,下面我们再来单独看下 Formatter 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.WARN)
formatter = logging.Formatter(fmt='%(asctime)s - %(name)s - %(levelname)s - %(message)s', datefmt='%Y/%m/%d %H:%M:%S')
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logger.addHandler(handler)

# Log
logger.debug('Debugging')
logger.critical('Critical Something')
logger.error('Error Occurred')
logger.warning('Warning exists')
logger.info('Finished')

在这里我们指定了一个 Formatter,并传入了 fmt 和 datefmt 参数,这样就指定了日志结果的输出格式和时间格式,然后 handler 通过 setFormatter() 方法设置此 Formatter 对象即可,输出结果如下:

1
2
3
2018/06/03 15:47:15 - __main__ - CRITICAL - Critical Something
2018/06/03 15:47:15 - __main__ - ERROR - Error Occurred
2018/06/03 15:47:15 - __main__ - WARNING - Warning exists

这样我们可以每个 Handler 单独配置输出的格式,非常灵活。

捕获 Traceback

如果遇到错误,我们更希望报错时出现的详细 Traceback 信息,便于调试,利用 logging 模块我们可以非常方便地实现这个记录,我们用一个实例来感受一下:

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
import logging

logger = logging.getLogger(__name__)
logger.setLevel(level=logging.DEBUG)

# Formatter
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# FileHandler
file_handler = logging.FileHandler('result.log')
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

# StreamHandler
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
logger.addHandler(stream_handler)

# Log
logger.info('Start')
logger.warning('Something maybe fail.')
try:
result = 10 / 0
except Exception:
logger.error('Faild to get result', exc_info=True)
logger.info('Finished')

这里我们在 error() 方法中添加了一个参数,将 exc_info 设置为了 True,这样我们就可以输出执行过程中的信息了,即完整的 Traceback 信息。 运行结果如下:

1
2
3
4
5
6
7
8
9
2018-06-03 16:00:15,382 - __main__ - INFO - Start print log
2018-06-03 16:00:15,382 - __main__ - DEBUG - Do something
2018-06-03 16:00:15,382 - __main__ - WARNING - Something maybe fail.
2018-06-03 16:00:15,382 - __main__ - ERROR - Faild to get result
Traceback (most recent call last):
File "/private/var/books/aicodes/loggingtest/demo8.py", line 23, in <module>
result = 10 / 0
ZeroDivisionError: division by zero
2018-06-03 16:00:15,383 - __main__ - INFO - Finished

可以看到这样我们就非常方便地记录下来了报错的信息,一旦出现了错误,我们也能非常方便地排查。

配置共享

在写项目的时候,我们肯定会将许多配置放置在许多模块下面,这时如果我们每个文件都来配置 logging 配置那就太繁琐了,logging 模块提供了父子模块共享配置的机制,会根据 Logger 的名称来自动加载父模块的配置。 例如我们这里首先定义一个 main.py 文件:

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

logger = logging.getLogger('main')
logger.setLevel(level=logging.DEBUG)

# Handler
handler = logging.FileHandler('result.log')
handler.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

logger.info('Main Info')
logger.debug('Main Debug')
logger.error('Main Error')
core.run()

这里我们配置了日志的输出格式和文件路径,同时定义了 Logger 的名称为 main,然后引入了另外一个模块 core,最后调用了 core 的 run() 方法。 接下来我们定义 core.py,内容如下:

1
2
3
4
5
6
7
8
import logging

logger = logging.getLogger('main.core')

def run():
logger.info('Core Info')
logger.debug('Core Debug')
logger.error('Core Error')

这里我们定义了 Logger 的名称为 main.core,注意这里开头是 main,即刚才我们在 main.py 里面的 Logger 的名称,这样 core.py 里面的 Logger 就会复用 main.py 里面的 Logger 配置,而不用再去配置一次了。 运行之后会生成一个 result.log 文件,内容如下:

1
2
3
4
2018-06-03 16:55:56,259 - main - INFO - Main Info
2018-06-03 16:55:56,259 - main - ERROR - Main Error
2018-06-03 16:55:56,259 - main.core - INFO - Core Info
2018-06-03 16:55:56,259 - main.core - ERROR - Core Error

可以看到父子模块都使用了同样的输出配置。 如此一来,我们只要在入口文件里面定义好 logging 模块的输出配置,子模块只需要在定义 Logger 对象时名称使用父模块的名称开头即可共享配置,非常方便。

文件配置

在开发过程中,将配置在代码里面写死并不是一个好的习惯,更好的做法是将配置写在配置文件里面,我们可以将配置写入到配置文件,然后运行时读取配置文件里面的配置,这样是更方便管理和维护的,下面我们以一个实例来说明一下,首先我们定义一个 yaml 配置文件:

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
version: 1
formatters:
brief:
format: "%(asctime)s - %(message)s"
simple:
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
handlers:
console:
class : logging.StreamHandler
formatter: brief
level : INFO
stream : ext://sys.stdout
file:
class : logging.FileHandler
formatter: simple
level: DEBUG
filename: debug.log
error:
class: logging.handlers.RotatingFileHandler
level: ERROR
formatter: simple
filename: error.log
maxBytes: 10485760
backupCount: 20
encoding: utf8
loggers:
main.core:
level: DEBUG
handlers: [console, file, error]
root:
level: DEBUG
handlers: [console]

这里我们定义了 formatters、handlers、loggers、root 等模块,实际上对应的就是各个 Formatter、Handler、Logger 的配置,参数和它们的构造方法都是相同的。 接下来我们定义一个主入口文件,main.py,内容如下:

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
import logging
import core
import yaml
import logging.config
import os


def setup_logging(default_path='config.yaml', default_level=logging.INFO):
path = default_path
if os.path.exists(path):
with open(path, 'r', encoding='utf-8') as f:
config = yaml.load(f)
logging.config.dictConfig(config)
else:
logging.basicConfig(level=default_level)


def log():
logging.debug('Start')
logging.info('Exec')
logging.info('Finished')


if __name__ == '__main__':
yaml_path = 'config.yaml'
setup_logging(yaml_path)
log()
core.run()

这里我们定义了一个 setup_logging() 方法,里面读取了 yaml 文件的配置,然后通过 dictConfig() 方法将配置项传给了 logging 模块进行全局初始化。 另外这个模块还引入了另外一个模块 core,所以我们定义 core.py 如下:

1
2
3
4
5
6
7
8
import logging

logger = logging.getLogger('main.core')

def run():
logger.info('Core Info')
logger.debug('Core Debug')
logger.error('Core Error')

这个文件的内容和上文是没有什么变化的。 观察配置文件,主入口文件 main.py 实际上对应的是 root 一项配置,它指定了 handlers 是 console,即只输出到控制台。另外在 loggers 一项配置里面,我们定义了 main.core 模块,handlers 是 console、file、error 三项,即输出到控制台、输出到普通文件和回滚文件。 这样运行之后,我们便可以看到所有的运行结果输出到了控制台:

1
2
3
4
5
6
2018-06-03 17:07:12,727 - Exec
2018-06-03 17:07:12,727 - Finished
2018-06-03 17:07:12,727 - Core Info
2018-06-03 17:07:12,727 - Core Info
2018-06-03 17:07:12,728 - Core Error
2018-06-03 17:07:12,728 - Core Error

在 debug.log 文件中则包含了 core.py 的运行结果:

1
2
3
2018-06-03 17:07:12,727 - main.core - INFO - Core Info
2018-06-03 17:07:12,727 - main.core - DEBUG - Core Debug
2018-06-03 17:07:12,728 - main.core - ERROR - Core Error

可以看到,通过配置文件,我们可以非常灵活地定义 Handler、Formatter、Logger 等配置,同时也显得非常直观,也非常容易维护,在实际项目中,推荐使用此种方式进行配置。 以上便是 logging 模块的基本使用方法,有了它,我们可以方便地进行日志管理和维护,会给我们的工作带来极大的方便。

日志记录使用常见误区

在日志输出的时候经常我们会用到字符串拼接的形式,很多情况下我们可能会使用字符串的 format() 来构造一个字符串,但这其实并不是一个好的方法,因为还有更好的方法,下面我们对比两个例子:

1
2
3
4
5
6
7
8
import logging

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

# bad
logging.debug('Hello {0}, {1}!'.format('World', 'Congratulations'))
# good
logging.debug('Hello %s, %s!', 'World', 'Congratulations')

这里有两种打印 Log 的方法,第一种使用了字符串的 format() 的方法进行构造,传给 logging 的只用到了第一个参数,实际上 logging 模块提供了字符串格式化的方法,我们只需要在第一个参数写上要打印输出的模板,占位符用 %s、%d 等表示即可,然后在后续参数添加对应的值就可以了,推荐使用这种方法。 运行结果如下:

1
2
2018-06-03 22:27:51,220 - root - DEBUG - Hello World, Congratulations!
2018-06-03 22:27:51,220 - root - DEBUG - Hello World, Congratulations!

另外在进行异常处理的时候,通常我们会直接将异常进行字符串格式化,但其实可以直接指定一个参数将 traceback 打印出来,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
import logging

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')

try:
result = 5 / 0
except Exception as e:
# bad
logging.error('Error: %s', e)
# good
logging.error('Error', exc_info=True)
# good
logging.exception('Error')

如果我们直接使用字符串格式化的方法将错误输出的话,是不会包含 Traceback 信息的,但如果我们加上 exc_info 参数或者直接使用 exception() 方法打印的话,那就会输出 Traceback 信息了。 运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
2018-06-03 22:24:31,927 - root - ERROR - Error: division by zero
2018-06-03 22:24:31,927 - root - ERROR - Error
Traceback (most recent call last):
File "/private/var/books/aicodes/loggingtest/demo9.py", line 6, in <module>
result = 5 / 0
ZeroDivisionError: division by zero
2018-06-03 22:24:31,928 - root - ERROR - Error
Traceback (most recent call last):
File "/private/var/books/aicodes/loggingtest/demo9.py", line 6, in <module>
result = 5 / 0
ZeroDivisionError: division by zero

以上便是整个对 logging 模块的介绍。嗯,是时候抛弃 print 了,开始体验下 logging 的便利吧!

参考内容

Python

2022 年最新第二版《Python3 网络爬虫开发实战》

现在《Python3 网络爬虫开发实战(第二版)》已经在 2021 年底正式上市了~
之前第一版的爬虫书《Python3 网络爬虫开发实战》在 2018 年出版,上市三年来,一直处于市面上所有爬虫书的销冠位置,豆瓣评分 9.0 分,销量 10w 册。

如今,这本书现在又进一步做了升级,第二版将案例进行了全面升级,自建了案例平台防止代码过期,同时增加了非常多的新技术、新知识的介绍,比如异步爬虫、JavaScript 逆向、安卓逆向、Kubernetes、智能解析。

可以说,目前市面上的爬虫书,其他的书跟这本书相比,内容方面这本书的算是最全的,没有之一。能将最前沿的爬虫技术比如异步、JavaScript 逆向、安卓逆向、智能解析、WebAssembly、Kubernetes 等技术都涵盖的,目前应该就是本新发布的《Python3 网络爬虫开发实战(第二版)》了。

就是这本:

2018 年 5 月《Python3 网络爬虫开发实战》的第一版出版了,从上市到现在三年多销量约 10w 册。后来,由于一些技术更迭,第二版书开始策划中。

2021 年 11 月,这本书历经各种反复修改、审稿等阶段,到今天终于上架了!

第二版更新内容

大家第一个问题可能就会问,第二版比第一版更新了哪些内容?

因为技术总是在不断发展和进步的,爬虫技术也是一样,它在爬虫和反爬虫不断斗争的过程中也在不断演进。比如现在越来越多的网页采取了各种防护措施,比如前端代码的压缩和混淆、API 的参数加密、WebDriver 的检测,要做到高效的数据爬取,我们就需要懂得一些 JavaScript 逆向分析相关技术。App 也是一样,App 的抓包防护、加壳保护、Native 化、风控检测使得越来越多的 App 数据难以爬取,所以我们也不得不了解一些逆向相关技术,如 Xposed、Frida、IDA Pro 等工具的使用。除此之外,近几年深度学习和人工智能发展得也是如火如荼,所以爬虫也可以和人工智能结合起来,比如基于深度学习的验证码识别、网页内容的智能化解析和提取等技术我们也可以进行学习和了解。另外,一些大规模爬虫的管理和运维技术也在不断发展,当前 Kubernetes、Docker、Prometheus 等云原生技术也非常火爆,基于 Kubernetes 等云原生技术的爬虫管理和运维解决方案也已经很受青睐。然而,之前第一版书对以上提到的这些新兴技术几乎没有提及。

除此之外,第一版书在讲解数据爬取的过程中引用了很多案例和服务,比如猫眼电影网站、淘宝网站、代理服务网站,然而几年过去了,有些案例网站和服务早已经改版或者停止维护,这就导致第一版书中的很多案例已经不能正常运行了。这其实是一个很大的问题,因为程序运行不通会大大降低学习的积极性和成就感,而且会浪费不少时间。另外,即使案例对应的爬虫代码及时更新了,那我们也不知道这些案例网站和服务什么时候会再次改版,因为这都是不可控的。所以,为了彻底解决这个问题,我花费了近半年的时间构建了一个爬虫案例平台(https://scrape.center),平台包含了几十个爬虫案例,包括服务端渲染(SSR)网站、单页面应用(SPA)网站、各类反爬网站、验证码网站、模拟登录网站、各类 App 等,覆盖了现在爬虫和反爬虫相关的大多数技术,整个平台都是我来维护的,书中几乎所有案例都是从案例平台来的,从而解决了页面改版的问题。

所以,本书相比第一版来说,更新的内容主要如下:

  • 绝大多数都迁移到了自建的案例平台,以后再也不用担心案例有过期或改版问题。

  • 替换了原本第一章环境安装的章节,将环境配置的部分全部汇总并迁移到案例平台(https://setup.scrape.center)并在书中以外链的形式附上,以确保环境的配置和安装说明能够被及时更新。

  • 增加了一些新的请求库、解析库、存储库等的介绍,如 httpx、parsel、Elasticsearch 等库的介绍。

  • 增加了异步爬虫的介绍,如协程的基本原理、aiohttp 的使用和爬取实战介绍。

  • 增加了一些新兴自动化工具的介绍,如 Pyppeteer、Playwright 的介绍。

  • 增加了深度学习相关内容,如图形验证码、滑动验证码的识别方案。

  • 丰富了模拟登录章节的内容,如增加了 JWT 模拟登录的介绍和实战、大规模账号池的优化。

  • 增加了 JavaScript 逆向的章节,包括网站加密和混淆技术、JavaScript 逆向调试技巧、JavaScript 的各种模拟执行方式、AST 还原混淆代码、WebAssembly 等相关技术的介绍。

  • 丰富了 App 自动化爬取技术的章节,如新兴框架 Airtest 的介绍、手机群控和云手机技术的介绍。

  • 增加了 Android 逆向章节,如反编译、反汇编、Hook、脱壳、so 文件分析和模拟执行等技术的介绍。

  • 增加了网页智能化解析章节,包括列表页、详情页内容提取算法和分类算法。

  • 丰富了 Scrapy 相关章节的介绍,如 Pyppeteer 的对接、RabbitMQ 的对接、Prometheus 的对接等。

  • 增加了基于 Kubernetes、Docker、Prometheus、Grafana 等云原生技术爬虫管理和运维解决方案的介绍。

以上就是第二版的主要更新内容。

章节介绍

为了让大家更直接地了解到全书的内容,这里就直接放目录了:

没错!全书一共 900 多页,量了下有 4.3 厘米厚,定价是 139.8 元。

可以直接看第二版吗?

当然,有朋友也会担心,我需不需要先学习第一版,然后才能学第二版呢?

答案是:可以直接学第二版,第二版书爬虫的内容知识体系是完整的,一些旧的技术已经在第一版中移除,第二版的书籍是对所有爬虫知识体系的全新升级。

没有基础可以学吗?

有朋友也可能会问,没有爬虫或者 Python 基础可以学吗?

答案是:可以,本书就是专为零爬虫基础的朋友准备的,本书从最基础的环境配置、基础知识的讲解开始,循序渐进地对爬虫的各个知识点进行介绍,所以完全不用担心没有爬虫基础学不会的问题。如果没有 Python 基础,那也没关系(当然有会更好),书中也会提及 Python 环境的配置并附上一些 Python 入门学习资料(链接),同时也会通过各个 Python 代码片段来进行讲解,很多案例也很简单易懂,学爬虫的时候 Python 也就会逐渐掌握了。

大咖推荐

这本书同时还获得了 Python 之父的推荐(没错就是 Python 的创始人,Guido van Rossum)。另外我还有幸获得了微软亚洲互联网工程院副院长曾文峰、知名爬虫专家梁斌 penny、中国人民大学高瓴人工智能学院长聘副教授宋睿华的推荐。

下面是推荐语的内容:

宣传彩页

另外编辑还为本书制作了几张宣传彩页,是对整本书的一个宣传介绍,大家可以看下:

有没有电子版?

看到这里,大家可能也会问了,有没有电子版呢?可能有的朋友习惯用电子版书籍来学习,有的朋友可能在海外也不方便购买,所以想要电子版。

但还是很遗憾地说:没有电子版。

因为你知道的,如果出了电子版,那么马上就会有各种盗版袭来,网上也会造成各种恶意传播。

所以,为了保护版权,这本书是没有上电子版的。

购买链接

是的,最后就是大家最关心的部分了,到哪里能够买到呢?

购买链接:https://item.jd.com/13527222.html

为了方便购买,我把这个链接转成了二维码,大家可以直接扫码购买:

谢谢大家支持!

2018 年第一版爬虫书

以下是 2018 年第一版《Python3 网络爬虫开发实战》简介。

嗨~ 给大家重磅推荐一本新书!还未上市前就已经重印 3 次的 Python 爬虫书!那么它就是由静觅博客博主崔庆才所作的《Python3 网络爬虫开发实战》!!!

书籍介绍

本书《Python3 网络爬虫开发实战》全面介绍了利用 Python3 开发网络爬虫的知识,书中首先详细介绍了各种类型的环境配置过程和爬虫基础知识,还讨论了 urllib、requests 等请求库和 Beautiful Soup、XPath、pyquery 等解析库以及文本和各类数据库的存储方法,另外本书通过多个真实新鲜案例介绍了分析 Ajax 进行数据爬取,Selenium 和 Splash 进行动态网站爬取的过程,接着又分享了一些切实可行的爬虫技巧,比如使用代理爬取和维护动态代理池的方法、ADSL 拨号代理的使用、各类验证码(图形、极验、点触、宫格等)的破解方法、模拟登录网站爬取的方法及 Cookies 池的维护等等。 此外,本书的内容还远远不止这些,作者还结合移动互联网的特点探讨了使用 Charles、mitmdump、Appium 等多种工具实现 App 抓包分析、加密参数接口爬取、微信朋友圈爬取的方法。此外本书还详细介绍了 pyspider 框架、Scrapy 框架的使用和分布式爬虫的知识,另外对于优化及部署工作,本书还包括 Bloom Filter 效率优化、Docker 和 Scrapyd 爬虫部署、分布式爬虫管理框架 Gerapy 的分享。 全书共 604 页,足足两斤重呢~ 定价为 99 元!

作者介绍

看书就先看看谁写的嘛,我们来了解一下~ 崔庆才,静觅博客博主(https://cuiqingcai.com),博客 Python 爬虫博文已过百万,北京航空航天大学硕士,微软小冰大数据工程师,有多个大型分布式爬虫项目经验,乐于技术分享,文章通俗易懂 ^^ 附皂片一张 ~(@^^@)~

图文介绍

呕心沥血设计的宣传图也得放一下~

专家评论

书是好是坏,得让专家看评一评呀,那么下面就是几位专家的精彩评论,快来看看吧~ 在互联网软件开发工程师的分类中,爬虫工程师是非常重要的。爬虫工作往往是一个公司核心业务开展的基础,数据抓取下来,才有后续的加工处理和最终展现。此时数据的抓取规模、稳定性、实时性、准确性就显得非常重要。早期的互联网充分开放互联,数据获取的难度很小。随着各大公司对数据资产日益看重,反爬水平也在不断提高,各种新技术不断给爬虫软件提出新的课题。本书作者对爬虫的各个领域都有深刻研究,书中探讨了 Ajax 数据的抓取、动态渲染页面的抓取、验证码识别、模拟登录等高级话题,同时也结合移动互联网的特点探讨了 App 的抓取等。更重要的是,本书提供了大量源码,可以帮助读者更好地理解相关内容。强烈推荐给各位技术爱好者阅读!

——梁斌,八友科技总经理

数据既是当今大数据分析的前提,也是各种人工智能应用场景的基础。得数据者得天下,会爬虫者走遍天下也不怕!一册在手,让小白到老司机都能有所收获!

——李舟军,北京航空航天大学教授,博士生导师

本书从爬虫入门到分布式抓取,详细介绍了爬虫技术的各个要点,并针对不同的场景提出了对应的解决方案。另外,书中通过大量的实例来帮助读者更好地学习爬虫技术,通俗易懂,干货满满。强烈推荐给大家!

——宋睿华,微软小冰首席科学家

有人说中国互联网的带宽全给各种爬虫占据了,这说明网络爬虫的重要性以及中国互联网数据封闭垄断的现状。爬是一种能力,爬是为了不爬。

——施水才,北京拓尔思信息技术股份有限公司总裁

全书目录

书的目录也有~ 看这里!

  • 1-开发环境配置
  • 1.1-Python3 的安装
  • 1.2-请求库的安装
  • 1.3-解析库的安装
  • 1.4-数据库的安装
  • 1.5-存储库的安装
  • 1.6-Web 库的安装
  • 1.7-App 爬取相关库的安装
  • 1.8-爬虫框架的安装
  • 1.9-部署相关库的安装
  • 2-爬虫基础
  • 2.1-HTTP 基本原理
  • 2.2-网页基础
  • 2.3-爬虫的基本原理
  • 2.4-会话和 Cookies
  • 2.5-代理的基本原理
  • 3-基本库的使用
  • 3.1-使用 urllib
  • 3.1.1-发送请求
  • 3.1.2-处理异常
  • 3.1.3-解析链接
  • 3.1.4-分析 Robots 协议
  • 3.2-使用 requests
  • 3.2.1-基本用法
  • 3.2.2-高级用法
  • 3.3-正则表达式
  • 3.4-抓取猫眼电影排行
  • 4-解析库的使用
  • 4.1-使用 XPath
  • 4.2-使用 Beautiful Soup
  • 4.3-使用 pyquery
  • 5-数据存储
  • 5.1-文件存储
  • 5.1.1-TXT 文本存储
  • 5.1.2-JSON 文件存储
  • 5.1.3-CSV 文件存储
  • 5.2-关系型数据库存储
  • 5.2.1-MySQL 存储
  • 5.3-非关系型数据库存储
  • 5.3.1-MongoDB 存储
  • 5.3.2-Redis 存储
  • 6-Ajax 数据爬取
  • 6.1-什么是 Ajax
  • 6.2-Ajax 分析方法
  • 6.3-Ajax 结果提取
  • 6.4-分析 Ajax 爬取今日头条街拍美图
  • 7-动态渲染页面爬取
  • 7.1-Selenium 的使用
  • 7.2-Splash 的使用
  • 7.3-Splash 负载均衡配置
  • 7.4-使用 Selenium 爬取淘宝商品
  • 8-验证码的识别
  • 8.1-图形验证码的识别
  • 8.2-极验滑动验证码的识别
  • 8.3-点触验证码的识别
  • 8.4-微博宫格验证码的识别
  • 9-代理的使用
  • 9.1-代理的设置
  • 9.2-代理池的维护
  • 9.3-付费代理的使用
  • 9.4-ADSL 拨号代理
  • 9.5-使用代理爬取微信公众号文章
  • 10-模拟登录
  • 10.1-模拟登录并爬取 GitHub
  • 10.2-Cookies 池的搭建
  • 11-App 的爬取
  • 11.1-Charles 的使用
  • 11.2-mitmproxy 的使用
  • 11.3-mitmdump 爬取“得到”App 电子书信息
  • 11.4-Appium 的基本使用
  • 11.5-Appium 爬取微信朋友圈
  • 11.6-Appium+mitmdump 爬取京东商品
  • 12-pyspider 框架的使用
  • 12.1-pyspider 框架介绍
  • 12.2-pyspider 的基本使用
  • 12.3-pyspider 用法详解
  • 13-Scrapy 框架的使用
  • 13.1-Scrapy 框架介绍
  • 13.2-Scrapy 入门
  • 13.3-Selector 的用法
  • 13.4-Spider 的用法
  • 13.5-Downloader Middleware 的用法
  • 13.6-Spider Middleware 的用法
  • 13.7-Item Pipeline 的用法
  • 13.8-Scrapy 对接 Selenium
  • 13.9-Scrapy 对接 Splash
  • 13.10-Scrapy 通用爬虫
  • 13.11-Scrapyrt 的使用
  • 13.12-Scrapy 对接 Docker
  • 13.13-Scrapy 爬取新浪微博
  • 14-分布式爬虫
  • 14.1-分布式爬虫原理
  • 14.2-Scrapy-Redis 源码解析
  • 14.3-Scrapy 分布式实现
  • 14.4-Bloom Filter 的对接
  • 15-分布式爬虫的部署
  • 15.1-Scrapyd 分布式部署
  • 15.2-Scrapyd-Client 的使用
  • 15.3-Scrapyd 对接 Docker
  • 15.4-Scrapyd 批量部署
  • 15.5-Gerapy 分布式管理

购买链接

想必很多小伙伴已经等了很久了,之前预售那么久也一直迟迟没有货,发售就有不少网店又售空了,不过现在起不用担心了!

书籍现已在京东、天猫、当当等网店上架并全面供应啦,复制链接到浏览器打开或扫描二维码打开即可购买了!

京东商城

https://item.jd.com/12333540.html

天猫商城

https://detail.tmall.com/item.htm?id=566699703917

当当网

http://product.dangdang.com/25249602.html

欢迎大家购买,谢谢支持!O(∩_∩)O

免费预览

不放心?想先看看有些啥,没问题!看这里: 免费章节试读(复制粘贴至浏览器打开): https://cuiqingcai.com/5052.html 将一直免费开放前 7 章节,欢迎大家试读!

视频教程

当然除了书籍,也有配套的视频课程,作者同样是崔庆才,二者结合学习效果更佳!限时优惠折扣中!扫描下图中二维码即可了解详情! 视频教程链接: https://edu.hellobi.com/course/157 http://study.163.com/course/courseMain.htm?courseId=1003827039 感谢大家支持!

Python

大家好,今天才发现很多学习 Flask 的小伙伴都有这么一个问题,清理缓存好麻烦啊,今天就教大家怎么解决。

大家在使用 Flask 静态文件的时候,每次更新,发现 CSS 或是 Js 或者其他的文件不会更新。 这是因为浏览器的缓存问题。 普遍大家是这几步解决办法。

  • 清理浏览器缓存
  • 设置浏览器不缓存
  • 也有以下这么写的
1
2
3
4
5
6
7
8
9
10
11
@app.context_processor
def override_url_for():
return dict(url_for=dated_url_for)

def dated_url_for(endpoint, **values):
if endpoint == 'static':
filename = values.get('filename', None)
if filename:
file_path = os.path.join(app.root_path, endpoint, filename)
values['q'] = int(os.stat(file_path).st_mtime)
return url_for(endpoint, **values)

如果是我,我不会这么做,效率很低。 这是 Flask 的 config 的源码,里面可以看到,有设置缓存最大时间 SEND_FILE_MAX_AGE_DEFAULT 可以看到,它是一个 temedelta 的值 我们去更改配置。 第 2 行: 我们引入了datetimetimedelta对象 第 6 行: 我们配置缓存最大时间 这样就解决了缓存问题,不用去写多余的代码,不用去清理浏览器的缓存。 一定要学着去看官方文档和框架的源代码!!

有什么问题请联系

[caption id=”attachment_5953” align=”aligncenter” width=”320”] 微信二维码[/caption]

Linux

现在我们有一台内网主机 A,在局域网内是可以访问的,但是如果我们现在不处在局域网内,可以选择 VPN 连接,但这样其实并不太方便,所以本节我们来说明一下利用 SSH 反向隧道来实现访问内网主机的方法。

准备

首先我们需要有一台公网主机作为跳板,这台主机是可以公网访问的,我们将其命名为 B,它的 IP 假设为 10.10.10.10。 所以两台机器网络配置如下:

A 内网机器

  • IP:192.168.1.2
  • SSH端口: 22
  • 用户名:usera
  • 密码:passworda
  • 内网配置端口:22(即配置 SSH 端口的反向隧道)

B 公网机器

  • IP:10.10.10.10
  • SSH端口: 22
  • 用户名:userb
  • 密码:passwordb
  • 公网端口:22001(即用 B 的 22001 端口连到 A 的 SSH 22 端口)

配置SSH秘钥

首先我们需要在 A 主机上生成 SSH 秘钥,和 B 用 SSH 建立认证。 首先在主机 A 上执行如下命令生成 SSH 秘钥:

1
ssh-keygen -t rsa -C "your@email.com"

命令里面的邮箱需要自行更换。 然后利用如下命令将 A 的 SSH 秘钥添加到 B 的 authorized_keys 里面:

1
ssh-copy-id userb@10.10.10.10

执行后会提示输入主机 B 的密码,执行完毕之后,我们登录到 B,就发现 authorized_keys 里面就多了 A 的 SSH 公钥了,成功建立 SSH 认证。

B 主机配置

B 主机需要更改 /etc/ssh/sshd_config 文件,修改如下一行:

1
GatewayPorts yes

这样可以把监听的端口绑定到任意IP 0.0.0.0上,否则只有本机 127.0.0.1 可以访问。 然后重启 sshd 服务:

1
sudo service sshd restart

A 主机配置

主机 A 再安装一个 AutoSSH,以 Ubuntu 为例,命令如下:

1
sudo apt-get install autossh

然后执行如下命令即可完成反向 SSH 配置:

1
autossh -M 55555 -NfR 0.0.0.0:22001:localhost:22 userb@10.10.10.10

这里 -M 后面任意填写一个可用端口即可,-N 代表只建立连接,不打开shell ,-f 代表建立成功后在后台运行,-R 代表指定端口映射。 这里是将 A 主机的 22 端口映射到 B 主机的 22001 端口,这样就完成了配置。 主要我们再访问 B 主机的 22001 端口,就会自动转发到 A 主机的 22 端口了,即可以公网访问了。

连接测试

接下来 SSH 测试连接 A 主机即可:

1
ssh usera@10.10.10.10 -p 22001

输入密码,完成连接。

Python

本节来详细说明一下 Seq2Seq 模型中一个非常有用的 Attention 的机制,并结合 TensorFlow 中的 AttentionWrapper 来剖析一下其代码实现。

Seq2Seq

首先来简单说明一下 Seq2Seq 模型,如果搞过深度学习,想必一定听说过 Seq2Seq 模型,Seq2Seq 其实就是 Sequence to Sequence,也简称 S2S,也可以称之为 Encoder-Decoder 模型,这个模型的核心就是编码器(Encoder)和解码器(Decoder)组成的,架构雏形是在 2014 年由论文 Learning Phrase Representations using RNN Encoder-Decoder for Statistical Machine Translation, Cho et al 提出的,后来 Sequence to Sequence Learning with Neural Networks, Sutskever et al 算是比较正式地提出了 Sequence to Sequence 的架构,后来 Neural Machine Translation by Jointly Learning to Align and Translate, Bahdanau et al 又提出了 Attention 机制,将 Seq2Seq 模型推上神坛,并横扫了非常多的任务,现在也非常广泛地用于机器翻译、对话生成、文本摘要生成等各种任务上,并取得了非常好的效果。 下面的图示意了 Seq2Seq 模型的基本架构: 可以看到图中有一个中间状态 $ c $ 向量,在 $ c $ 向量左侧的我们可以称之为编码器(Encoder),编码器这里示意的是 RNN 序列,另外 RNN 单元还可以使用 LSTM、GRU 等变体, 在编码器下方输入了 $ x_1 $、$ x_2 $、$ x_3 $、$ x_4 $,代表模型的输入内容,例如在翻译模型中可以分别代表“我爱中国”这四个字,这样经过序列处理,它就会得到最后的输出,我们将其表示为 $ c $ 向量,这样编码器的工作就完成了。在图中 $ c $ 向量的右侧部分我们可以称之为解码器(Decoder),它拿到编码器生成的 $ c $ 向量,然后再进行序列解码,得到输出结果 $ y_1 $、$ y_2 $、$ y_3 $,例如刚才输入的“我爱中国”四个字便被解码成了 “I love China”,这样就实现了翻译任务,以上就是最基本的 Seq2Seq 模型原理。 另外还有一种变体,$ c $ 向量在每次解码的时候都会作为解码器的输入,其实原理都是类似的,如图所示: 这种模型架构是通用的,所以它的适用场景也非常广泛。如机器翻译、对话生成、文本摘要、阅读理解、语音识别,也可以用在一些趣味场景中,如诗词生成、对联生成、代码生成、评论生成等等,效果都很不错。

Attention

通过上图我们可以发现,Encoder 把所有的输入序列编码成了一个 $ c $ 向量,然后使用 $ c $ 向量来进行解码,因此,$ c $ 向量中必须包含了原始序列中的所有信息,所以它的压力其实是很大的,而且由于 RNN 容易把前面的信息“忘记”掉,所以基本的 Seq2Seq 模型,对于较短的输入来说,效果还是可以接受的,但是在输入序列比较长的时候,$ c $ 向量存不下那么多信息,就会导致生成效果大大折扣。 Attention 机制解决了这个问题,它可以使得在输入文本长的时候精确率也不会有明显下降,它是怎么做的呢?既然一个 $ c $ 向量存不了,那么就引入多个 $ c $ 向量,称之为 $ c1 $、$ c_2 $、…、$ c_i $,在解码的时候,这里的 $ i $ 对应着 Decoder 的解码位次,每次解码就利用对应的 $ c_i $ 向量来解码,如图所示: 这里的每个 $ c_i $ 向量其实包含了当前所输出与输入序列各个部分重要性的相关的信息。不同的 $ c_i $ 向量里面包含的输入信息各部分的权重是不同的,先放一个示意图: 还是上面的例子,例如输入信息是“我爱中国”,输出的的理想结果应该是“I love China”,在解码的时候,应该首先需要解码出 “I” 这个字符,这时候会用到 $ c_1 $ 向量,而 $ c_1 $ 向量包含的信息中,“我”这个字的重要性更大,因此它便倾向解码输出 “I”,当解码第二个字的时候,会用到 $ c_2 $ 向量,而 $ c_2 $ 向量包含的信息中,“爱” 这个字的重要性更大,因此会解码输出 “love”,在解码第三个字的时候,会用到 $ c_3 $ 向量,而 $ c_3 $向量包含的信息中,”中国” 这两个字的权重都比较大,因此会解码输出 “China”。所以其实,Attention 注意力机制中的 $ c_i $ 向量记录了不同解码时刻应该更关注于哪部分输入数据,也实现了编码解码过程的对齐。经过实验发现,这种机制可以有效解决输入信息过长时导致信息解码效果不理想的问题,另外解码生成效果同时也有提升。 下面我们以 Bahdanau 提出的 Attention 为例来详细剖析一下 Attention 机制。 在没有引入 Attention 之前,Decoder 在某个时刻解码的时候实际上是依赖于三个部分的,首先我们知道 RNN 中,每次输出结果会依赖于隐层和输入,在 Seq2Seq 模型中,还需要依赖于 $ c $ 向量,所以这里我们设在 $ i $ 时刻,解码器解码的内容是 $ y_i $,上一次解码结果是 $ y{i-1} $,隐层输出是 $ st $,所以它们满足这样的关系: $$ y_i = g(y{i-1}, si, c) s_i = f(s{i-1}, y{i-1}, c) y_i = g(y{i-1}, si, c_i) s_i = f(s{i-1}, y{i-1}, c_i) $$ 所以,这里每次解码得出 $ y_i $ 时,都有与之对应的 $ c_i $ 向量。那么这个 $ c_i $ 向量又是怎么来的呢?实际上它是由编码器端每个时刻的隐含状态加权平均得到的,这里假设编码器端的的序列长度为 $ T_x $,序列位次用 $ j $ 来表示,编码器段每个时刻的隐含状态即为 $ h_1 $、$ h_2 $、…、$ h_j $、…、$ h{Tx} $,对于解码器的第 $ i $ 时刻,对应的 $ c_i $ 表示如下: $$ c_i = \sum{j=1}^{Tx} \alpha{ij}hj $$ 编码器输出的结果中,$ h_j $ 中包含了输入序列中的第 $ j $ 个词及前面的一些信息,如果是用了双向 RNN 的话,则包含的是第 $ j $ 个词即前后的一些词的信息,这里 $ \alpha{ij} $ 代表了分配的权重,这代表在生成第 i 个结果的时候,对于输入信息的各个阶段的 $ hj $ 的注意力分配是不同的。 当 $ a{ij} $ 的值越高,表示第 $ i $ 个输出在第 $ j $ 个输入上分配的注意力越多,这样就会导致在生成第 $ i $ 个输出的时候,受第 $ j $ 个输入的影响也就越大。 那么 $ a{ij} $ 又是怎么得来的呢?其实它就又关系到第 $ i-1 $ 个输出隐藏状态 $ s{i-1} $ 以及输入中的各个隐含状态 $ h_j $,公式表示如下: $$ \alpha{ij} = \frac {exp(e{ij})} {\sum{k=1}^{Tx} exp(e{ik})} e{ij} = a(s{i-1}, hj) = {v_a}^Ttanh(W_as{i-1} + Uah_j) $$ 这也就是说,这个权重就是 $ s{i-1} $ 和 $ hj $ 分别计算得到一个数值,然后再过一个 softmax 函数得到的,结果就是 $ \alpha{ij} $。 因此 $ ci $ 就可以表示为: $$ c_i = \sum{j=1}^{Tx} softmax(a(s{i-1}, h_j)) \cdot h_j $$ 以上便是整个 Attention 机制的推导过程。

TensorFlow AttentionWrapper

我们了解了基本原理,但真正离程序实现出来其实还是有很大差距的,接下来我们就结合 TensorFlow 框架来了解一下 Attention 的实现机制。 在 TensorFlow 中,Attention 的相关实现代码是在 tensorflow/contrib/seq2seq/python/ops/attention_wrapper.py 文件中,这里面实现了两种 Attention 机制,分别是 BahdanauAttention 和 LuongAttention,其实现论文分别如下:

整个 attention_wrapper.py 文件中主要包含几个类,我们主要关注其中几个:

  • AttentionMechanism、_BaseAttentionMechanism、LuongAttention、BahdanauAttention 实现了 Attention 机制的逻辑。
    • AttentionMechanism 是 Attention 类的父类,继承了 object 类,内部没有任何实现。
    • _BaseAttentionMechanism 继承自 AttentionMechanism 类,定义了 Attention 机制的一些公共方法实现和属性。
    • LuongAttention、BahdanauAttention 均继承 _BaseAttentionMechanism 类,分别实现了上面两篇论文的 Attention 机制。
  • AttentionWrapperState 用来存储整个计算过程中的 state,和 RNN 中的 state 类似,只不过这里额外还存储了 attention、time 等信息。
  • AttentionWrapper 主要用于对封装 RNNCell,继承自 RNNCell,封装后依然是 RNNCell 的实例,可以构建一个带有 Attention 机制的 Decoder。
  • 另外还有一些公共方法,例如 hardmax、safe_cumpord 等。

下面我们以 BahdanauAttention 为例来说明 Attention 机制及 AttentionWrapper 的实现。

BahdanauAttention

首先我们来介绍 BahdanauAttention 类的具体原理。 首先我们来看下它的初始化方法:

1
2
3
4
5
6
7
8
9
def __init__(self,
num_units,
memory,
memory_sequence_length=None,
normalize=False,
probability_fn=None,
score_mask_value=None,
dtype=None,
name="BahdanauAttention"):

这里一共接受八个参数,下面一一进行说明:

  • numunits:神经元节点数,我们知道在计算 $ e{ij} $ 的时候,需要使用 $ s_{i-1} $ 和 $ h_j $ 来进行计算,而二者的维度可能并不是统一的,需要进行变换和统一,所以这里就有了 $ W_a $ 和 $ U_a $ 这两个系数,所以在代码中就是用 num_units 来声明了一个全连接 Dense 网络,用于统一二者的维度,以便于下一步的计算:
1
2
query_layer=layers_core.Dense(num_units, name="query_layer", use_bias=False, dtype=dtype)
memory_layer=layers_core.Dense(num_units, name="memory_layer", use_bias=False, dtype=dtype)

这里我们可以看到声明了一个 querylayer 和 memory_layer,分别和 $ s{i-1} $ 及 $ h_j $ 做全连接变换,统一维度。

  • memory:The memory to query; usually the output of an RNN encoder. 即解码时用到的上文信息,维度需要是 [batch_size, max_time, context_dim]。这时我们观察一下父类 _BaseAttentionMechanism 的初始化方法,实现如下:
1
2
3
4
5
6
7
8
with ops.name_scope(
name, "BaseAttentionMechanismInit", nest.flatten(memory)):
self._values = _prepare_memory(
memory, memory_sequence_length,
check_inner_dims_defined=check_inner_dims_defined)
self._keys = (
self.memory_layer(self._values) if self.memory_layer
else self._values)

这里通过 _prepare_memory() 方法对 memory 进行处理,然后调用 memory_layer 对 memory 进行全连接维度变换,变换成 [batch_size, max_time, num_units]。

  • memory_sequence_length:Sequence lengths for the batch entries in memory. 即 memory 变量的长度信息,类似于 dynamic_rnn 中的 sequence_length,被 _prepare_memory() 方法调用处理 memory 变量,进行 mask 操作:
1
2
3
4
5
6
7
seq_len_mask = array_ops.sequence_mask(
memory_sequence_length,
maxlen=array_ops.shape(nest.flatten(memory)[0])[1],
dtype=nest.flatten(memory)[0].dtype)
seq_len_batch_size = (
memory_sequence_length.shape[0].value
or array_ops.shape(memory_sequence_length)[0])
  • normalize:Whether to normalize the energy term. 即是否要实现标准化,方法出自论文:Weight Normalization: A Simple Reparameterization to Accelerate Training of Deep Neural Networks, Salimans, et al
  • probability_fn:A callable function which converts the score to probabilities. 计算概率时的函数,必须是一个可调用的函数,默认使用 softmax(),还可以指定 hardmax() 等函数。
  • score_mask_value:The mask value for score before passing into probability_fn. The default is -inf. Only used if memory_sequence_length is not None. 在使用 probability_fn 计算概率之前,对 score 预先进行 mask 使用的值,默认是负无穷。但这个只有在 memory_sequence_length 参数定义的时候有效。
  • dtype:The data type for the query and memory layers of the attention mechanism. 数据类型,默认是 float32。
  • name:Name to use when creating ops,自定义名称。

接下来类里面定义了一个 call() 方法:

1
2
3
4
5
6
def __call__(self, query, previous_alignments):
with variable_scope.variable_scope(None, "bahdanau_attention", [query]):
processed_query = self.query_layer(query) if self.query_layer else query
score = _bahdanau_score(processed_query, self._keys, self._normalize)
alignments = self._probability_fn(score, previous_alignments)
return alignments

这里首先定义了 processed_query,这里也是通过 query_layer 过了一个全连接网络,将最后一维统一成 num_units,然后调用了 bahdanau_score() 方法,这个方法是比较重要的,主要用来计算公式中的 $ e{ij} $,传入的参数是 processed_query 以及上文中提及的 keys 变量,二者一个代表了 $ s{i-1} $,一个代表了 $ h_j $,_bahdanau_score() 方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def _bahdanau_score(processed_query, keys, normalize):
dtype = processed_query.dtype
# Get the number of hidden units from the trailing dimension of keys
num_units = keys.shape[2].value or array_ops.shape(keys)[2]
# Reshape from [batch_size, ...] to [batch_size, 1, ...] for broadcasting.
processed_query = array_ops.expand_dims(processed_query, 1)
v = variable_scope.get_variable(
"attention_v", [num_units], dtype=dtype)
if normalize:
# Scalar used in weight normalization
g = variable_scope.get_variable(
"attention_g", dtype=dtype,
initializer=math.sqrt((1. / num_units)))
# Bias added prior to the nonlinearity
b = variable_scope.get_variable(
"attention_b", [num_units], dtype=dtype,
initializer=init_ops.zeros_initializer())
# normed_v = g * v / ||v||
normed_v = g * v * math_ops.rsqrt(
math_ops.reduce_sum(math_ops.square(v)))
return math_ops.reduce_sum(normed_v * math_ops.tanh(keys + processed_query + b), [2])
else:
return math_ops.reduce_sum(v * math_ops.tanh(keys + processed_query), [2])

这里其实就是实现了 keys 和 processedquery 的加和,如果指定了 normalize 的话还需要进行额外的 normalize,结果就是公式中的 $ e{ij} $,在 TensorFlow 中常用 score 变量表示。 接下来再回到 call() 方法中,这里得到了 score 变量,接下来可以对齐求 softmax() 操作,得到 $ \alpha_{ij} $:

1
alignments = self._probability_fn(score, previous_alignments)

这就代表了在 $ i $ 时刻,Decoder 的时候对 Encoder 得到的每个 $ hj $ 的权重大小比例,在 TensorFlow 中常用 alignments 变量表示。 所以综上所述,BahdanauAttention 就是初始化时传入 num_units 以及 Encoder Outputs,然后调时传入 query 用即可得到权重变量 alignments。

AttentionWrapperState

接下来我们再看下 AttentionWrapperState 这个类,这个类其实比较简单,就是定义了 Attention 过程中可能需要保存的变量,如 cell_state、attention、time、alignments 等内容,同时也便于后期的可视化呈现,代码实现如下:

1
2
3
4
class AttentionWrapperState(
collections.namedtuple("AttentionWrapperState",
("cell_state", "attention", "time", "alignments",
"alignment_history"))):

可见它就是继承了 namedtuple 这个数据结构,其实整个 AttentionWrapperState 就像声明了一个结构体,可以传入需要的字段生成这个对象。

AttentionWrapper

了解了 Attention 机制及 BahdanauAttention 的原理之后,最后我们再来了解一下 AttentionWrapper,可能你用过很多其他的 Wrapper,如 DropoutWrapper、ResidualWrapper 等等,它们其实都是 RNNCell 的实例,其实 AttentionWrapper 也不例外,它对 RNNCell 进行了封装,封装后依然还是 RNNCell 的实例。一个普通的 RNN 模型,你要加入 Attention,只需要在 RNNCell 外面套一层 AttentionWrapper 并指定 AttentionMechanism 的实例就好了。而且如果要更换 AttentionMechanism,只需要改变 AttentionWrapper 的参数就好了,这可谓对 Attention 的实现架构完全解耦,配置非常灵活,TF 大法好! 接下来我们首先来看下它的初始化方法,其参数是这样的:

1
2
3
4
5
6
7
8
9
def __init__(self,
cell,
attention_mechanism,
attention_layer_size=None,
alignment_history=False,
cell_input_fn=None,
output_attention=True,
initial_cell_state=None,
name=None):

下面对参数进行一一说明:

  • cell:An instance of RNNCell. RNNCell 的实例,这里可以是单个的 RNNCell,也可以是多个 RNNCell 组成的 MultiRNNCell。
  • attention_mechanism:即 AttentionMechanism 的实例,如 BahdanauAttention 对象,另外可以是多个 AttentionMechanism 组成的列表。
  • attention_layer_size:是数字或者数字做成的列表,如果是 None(默认),直接使用加权计算后得到的 Attention 作为输出,如果不是 None,那么 Attention 结果还会和 Output 进行拼接并做线性变换再输出。其代码实现如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
if attention_layer_size is not None:
attention_layer_sizes = tuple(attention_layer_size if isinstance(attention_layer_size, (list, tuple)) else (attention_layer_size,))
if len(attention_layer_sizes) != len(attention_mechanisms):
raise ValueError("If provided, attention_layer_size must contain exactly one integer per attention_mechanism, saw: %d vs %d" % (len(attention_layer_sizes), len(attention_mechanisms)))
self._attention_layers = tuple(layers_core.Dense(attention_layer_size, name="attention_layer", use_bias=False, dtype=attention_mechanisms[i].dtype) for i, attention_layer_size in enumerate(attention_layer_sizes))
self._attention_layer_size = sum(attention_layer_sizes)
else:
self._attention_layers = None
self._attention_layer_size = sum(attention_mechanism.values.get_shape()[-1].value for attention_mechanism in attention_mechanisms)

for i, attention_mechanism in enumerate(self._attention_mechanisms):
attention, alignments = _compute_attention(attention_mechanism, cell_output, previous_alignments[i], self._attention_layers[i] if self._attention_layers else None)
alignment_history = previous_alignment_history[i].write(state.time, alignments) if self._alignment_history else ()
  • alignment_history:即是否将之前的 alignments 存储到 state 中,以便于后期进行可视化展示。
  • cell_input_fn:将 Input 进行处理的方式,默认会将上一步的 Attention 进行 拼接操作,以免造成重复关注同样的内容。代码调用如下:
1
cell_inputs = self._cell_input_fn(inputs, state.attention)
  • output_attention:是否将 Attention 返回,如果是 False 则返回 Output,否则返回 Attention,默认是 True。
  • initial_cell_state:计算时的初始状态。
  • name:自定义名称。

AttentionWrapper 的核心方法在它的 call() 方法,即类似于 RNNCell 的 call() 方法,AttentionWrapper 类对其进行了重载,代码实现如下:

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
def call(self, inputs, state):
# Step 1
cell_inputs = self._cell_input_fn(inputs, state.attention)
# Step 2
cell_state = state.cell_state
cell_output, next_cell_state = self._cell(cell_inputs, cell_state)
# Step 3
if self._is_multi:
previous_alignments = state.alignments
previous_alignment_history = state.alignment_history
else:
previous_alignments = [state.alignments]
previous_alignment_history = [state.alignment_history]
all_alignments = []
all_attentions = []
all_histories = []
for i, attention_mechanism in enumerate(self._attention_mechanisms):
attention, alignments = _compute_attention(attention_mechanism, cell_output, previous_alignments[i], self._attention_layers[i] if self._attention_layers else None)
alignment_history = previous_alignment_history[i].write(state.time, alignments) if self._alignment_history else ()
all_alignments.append(alignments)
all_histories.append(alignment_history)
all_attentions.append(attention)
# Step 4
attention = array_ops.concat(all_attentions, 1)
# Step 5
next_state = AttentionWrapperState(
time=state.time + 1,
cell_state=next_cell_state,
attention=attention,
alignments=self._item_or_tuple(all_alignments),
alignment_history=self._item_or_tuple(all_histories))
# Step 6
if self._output_attention:
return attention, next_state
else:
return cell_output, next_state

在这里将一些异常判断代码去除了,以便于结构看得更清晰。 首先在第一步中,调用了 _cell_input_fn() 方法,对 inputs 和 state.attention 变量进行处理,默认是使用 concat() 函数拼接,作为当前时间步的输入。因为可能前一步的 Attention 可能对当前 Attention 有帮助,以免让模型连续两次将注意力放在同一个地方。 在第二步中,其实就是调用了普通的 RNNCell 的 call() 方法,得到输出和下一步的状态。 第三步中,这时得到的输出其实并没有用上 AttentionMechanism 中的 alignments 信息,所以当前的输出信息中我们并没有跟 Encoder 的信息做 Attention,所以这里还需要调用 _compute_attention() 方法进行权重的计算,其方法实现如下:

1
2
3
4
5
6
7
8
9
10
def _compute_attention(attention_mechanism, cell_output, previous_alignments, attention_layer):
alignments = attention_mechanism(cell_output, previous_alignments=previous_alignments)
expanded_alignments = array_ops.expand_dims(alignments, 1)
context = math_ops.matmul(expanded_alignments, attention_mechanism.values)
context = array_ops.squeeze(context, [1])
if attention_layer is not None:
attention = attention_layer(array_ops.concat([cell_output, context], 1))
else:
attention = context
return attention, alignments

这个方法接收四个参数,其中 attentionmechanism 就是 AttentionMechanism 的实例,cell_output 就是当前 Output,previous_alignments 是上步的 alignments 信息,调用 attention_mechanism 计算之后就会得到当前步的 alignments 信息了,即 $ \alpha{ij} $。接下来再利用 alignments 信息进行加权运算,得到 attention 信息,即 $ c_{i} $,最后将二者返回。 在第四步中,就是将 attention 结果每个时间步进行 concat,得到 attention vector。 第五步中,声明 AttentionWrapperState 作为下一步的状态。 第六步,判断是否要输出 Attention,如果是,输出 Attention 及下一步状态,否则输出 Outputs 及下一步状态。 好,以上便是整个 AttentionWrapper 源码解析过程,了解了源码之后,再做模型优化的话就非常得心应手了。

参考来源

Python

前言

我们在运行 Python 项目的时候经常会遇到一些版本问题,例如 A 项目依赖于 Django 1.5,而 B 项目又依赖 Django 2.0,而我们的系统却只有一个 Python 解释器,我们所有的包都被装在了 Python 安装目录的 site-packages 目录下,所以 Django 只能是某个特定的版本,所以这样就会导致运行的时候导致 A 或 B 项目出现兼容问题。为了解决这个问题,我们可能会使用 virtualenv 来为项目创建一套独立的 Python 运行环境,或者我们可能会使用 Docker 容器来实现不同项目的隔离运行,但总的来说,它们使用起来其实并没有那么方便。另外在进行 Python 包管理时,requirements.txt 这样的包依赖标识文件也显得很鸡肋,在某些情况下可能会带来一些麻烦。为了解决这些问题,一个更加使用方便的包管理工具诞生了,叫做 Pipenv,接下来就让我们一起来了解一下它的用法。

简介

Pipenv,它的项目简介为 Python Development Workflow for Humans,是 Python 著名的 requests 库作者 kennethreitz 写的一个包管理工具,它可以为我们的项目自动创建和管理虚拟环境并非常方便地管理 Python 包,现在它也已经是 Python 官方推荐的包管理工具。 Pipenv 我们可以简单理解为 pip 和 virtualenv 的集合体,它可以为我们的项目自动创建和管理一个虚拟环境。virtualenv 在使用时我们需要手动创建一个虚拟环境然后激活,Pipenv 会自动创建。另外我们之前可能使用 requirements.txt 文件来标识项目所需要的依赖,但是这样会带来一些问题,如有的 requirements.txt 中只是将库名列出来了,没有严格指定版本号,这样就可能会导致不同时间安装的库版本是不同的,如 requirements.txt 文件中对 Django 的依赖只写了一个 django,可能在 2016 年的时候运行安装会安装 Django 的 1.x 版本,到了 2017 年就会安装 Django 的 2.x 版本,所以可能导致一些麻烦。为了解决这个问题,Pipenv 直接弃用了 requirements.txt,会同时它会使用一个叫做 Pipfile 和 Pipfile.lock 的文件来管理项目所需的依赖包,而不再是简单地使用 requirements.txt 文件来记录项目所需要的依赖。 总的来说,Pipenv 可以解决如下问题:

  • 我们不需要再手动创建虚拟环境,Pipenv 会自动为我们创建,它会在某个特定的位置创建一个 virtualenv 环境,然后调用 pipenv shell 命令切换到虚拟环境。
  • 使用 requirements.txt 可能会导致一些问题,所以 Pipenv 使用 Pipfile 和 Pipfile.lock 来替代之,而且 Pipfile 如果不存在的话会自动创建,而且在安装、升级、移除依赖包的时候会自动更新 Pipfile 和 Pipfile.lock 文件。
  • 广泛使用 Hash 校验,保证安全性。
  • 可以更清晰地查看 Python 包及其关系,调用 pipenv graph 即可呈现,结果简单明了。
  • 可通过自动加载 .env 读取环境变量,简化开发流程。

安装

本文内容基于 Python 3.6 说明,默认的 Python 解释器命令为 python3,包管理工具命令为 pip3。 Pipenv 是基于 Python 开发的包,所以可以直接用 pip 来安装,命令如下:

1
pip3 install pipenv

另外还有多种安装方式,如 Pipsi、Nix、Homebrew,安装方式可以参考:http://pipenv.readthedocs.io/en/latest/#install-pipenv-today

基本使用

首先我们可以新建一个项目,例如叫做 PipenvTest,然后新建一个 Python 脚本,例如叫 main.py,内容为:

1
2
import django
print(django.get_version())

直接用系统的 Python3 运行此脚本:

1
python3 main.py

结果如下:

1
1.11

我们可以看到系统安装的 Django 版本是 1.11。但是我们想要本项目基于 Django 2.x 开发,当然我们可以选择将系统的 Django 版本升级,但这样又可能会影响其他的项目的运行,所以这并不是一个好的选择。为了不影响系统环境的 Django 版本,所以我们可以用 Pipenv 来创建一个虚拟环境。 在该目录下,输入 pipenv 命令即可查看命令的完整用法:

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
51
52
Usage: pipenv [OPTIONS] COMMAND [ARGS]...

Options:
--update Update Pipenv & pip to latest.
--where Output project home information.
--venv Output virtualenv information.
--py Output Python interpreter information.
--envs Output Environment Variable options.
--rm Remove the virtualenv.
--bare Minimal output.
--completion Output completion (to be eval'd).
--man Display manpage.
--three / --two Use Python 3/2 when creating virtualenv.
--python TEXT Specify which version of Python virtualenv should use.
--site-packages Enable site-packages for the virtualenv.
--jumbotron An easter egg, effectively.
--version Show the version and exit.
-h, --help Show this message and exit.


Usage Examples:
Create a new project using Python 3.6, specifically:
$ pipenv --python 3.6

Install all dependencies for a project (including dev):
$ pipenv install --dev

Create a lockfile containing pre-releases:
$ pipenv lock --pre

Show a graph of your installed dependencies:
$ pipenv graph

Check your installed dependencies for security vulnerabilities:
$ pipenv check

Install a local setup.py into your virtual environment/Pipfile:
$ pipenv install -e .

Commands:
check Checks for security vulnerabilities and against PEP 508 markers
provided in Pipfile.
graph Displays currently–installed dependency graph information.
install Installs provided packages and adds them to Pipfile, or (if none
is given), installs all packages.
lock Generates Pipfile.lock.
open View a given module in your editor.
run Spawns a command installed into the virtualenv.
shell Spawns a shell within the virtualenv.
uninstall Un-installs a provided package and removes it from Pipfile.
update Uninstalls all packages, and re-installs package(s) in [packages]
to latest compatible versions.

接下来我们首先验证一下当前的项目是没有创建虚拟环境的,调用如下命令:

1
pipenv --venv

结果如下:

1
No virtualenv has been created for this project yet!

这说明当前的项目尚未创建虚拟环境,接下来我们利用 Pipenv 来创建一个虚拟环境:

1
pipenv --three

1
pipenv --python 3.6

都可以创建一个 Python3 的虚拟环境,—three 代表创建一个 Python3 版本的虚拟环境,—python 则可以指定特定的 Python 版本,当然 —two 则创建一个 Python2 版本的虚拟环境,但前提你的系统必须装有该版本的 Python 才可以。 执行完毕之后,样例输出如下:

1
2
3
4
5
6
7
8
9
10
Warning: the environment variable LANG is not set!
We recommend setting this in ~/.profile (or equivalent) for proper expected behavior.
Creating a virtualenv for this project
Using /usr/local/bin/python3 to create virtualenv…
⠋Running virtualenv with interpreter /usr/local/bin/python3
Using base prefix '/usr/local/Cellar/python3/3.6.1/Frameworks/Python.framework/Versions/3.6'
New python executable in /Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E/bin/python3.6
Also creating executable in /Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E/bin/python
Installing setuptools, pip, wheel...done.
Virtualenv location: /Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E

这里显示 Pipenv 利用 /usr/local/bin/python3 作为 virtualenv 的解释器,然后在 /Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E/bin 目录下创建了一个新的 Python3 解释器,同时还创建了两个可执行文件别名 python3.6 和 python,另外我们还可以发现目录下多了一个 Pipfile 文件,这时虚拟环境就创建完成了。 我们切换到 PipenvTest-VSTVh89E/bin 目录查看一下文件结构,可以看到这里面包含了 pip、pip3、pip3.6、python、python3、python3.6 等可执行文件,实际上目录结构和使用 virtualenv 时是完全一样的,只不过文件夹的位置不同而已。 接下来我们可以切换到该虚拟环境下执行命令,执行如下命令即可:

1
pipenv shell

执行完毕之后样例输出如下:

1
2
3
4
Spawning environment shell (/bin/zsh). Use 'exit' to leave.
source /Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E/bin/activate
CQC-MAC% source /Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E/bin/activate
(PipenvTest-VSTVh89E) CQC-MAC%

实际上这也和 virtualenv 激活的流程一样,也是调用了类似 source venv/bin/activate 方法将这个路径加到全局环境变量最前面,这样就会优先调用该路径下的 python、python3、python3.6 可执行文件了。 这时候我们会发现命令行的样子就变了,前面多了一个 (PipenvTest-VSTVh89E) 的标识,代表当前我们已经切换到了虚拟环境下。 这时我们用 which 或 where 命令查看一下 Python 可执行文件的路径,命令如下:

1
2
3
4
5
6
(PipenvTest-VSTVh89E) CQC-MAC% which python3
/Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E/bin/python3
(PipenvTest-VSTVh89E) CQC-MAC% which python3.6
/Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E/bin/python3.6
(PipenvTest-VSTVh89E) CQC-MAC% which python
/Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E/bin/python

可以发现当前的 Python 可执行路径都被切换到了 PipenvTest-VSTVh89E/bin 目录下,调用的是虚拟环境中的 Python 解释器,这时我们重新执行刚才的脚本,命令如下:

1
(PipenvTest-VSTVh89E) CQC-MAC% python3 main.py

这时我们可以发现报了如下错误:

1
2
3
4
Traceback (most recent call last):
File "main.py", line 1, in <module>
import django
ModuleNotFoundError: No module named 'django'

这其实是因为新的虚拟环境没有安装任何的 Python 第三方包,实际上如果直接使用 virtualenv 时也是这样的结果。这是因为新的虚拟环境是一个全新的 Python 环境,它默认只包含了 Python 内置的包以及 pip、wheel、setuptools 包,其他的第三方包都没有安装。 这时我们可以使用 Pipenv 来安装 django 包,命令如下:

1
pipenv install django

运行后输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
Installing django…
Collecting django
Downloading Django-2.0.2-py3-none-any.whl (7.1MB)
Collecting pytz (from django)
Downloading pytz-2018.3-py2.py3-none-any.whl (509kB)
Installing collected packages: pytz, django
Successfully installed django-2.0.2 pytz-2018.3

Adding django to Pipfile's [packages]…
Locking [dev-packages] dependencies…
Locking [packages] dependencies…
Updated Pipfile.lock (e101fb)!

如果有这样的输出结果就代表成功安装了 Django,可以看到此时安装的 Django 版本为 2.0,代表我们的虚拟环境成功安装了 Django 2.0 版本。 同时我们还注意到它输出了一句话叫做 Updated Pipfile.lock,这时我们可以发现项目路径下又生成了一个 Pipfile.lock 文件,内容如下:

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
51
52
53
{
"_meta": {
"hash": {
"sha256": "7b9623243d9c22b1f333ee710aff70d0cbcdf1dd7e0aac69230dc76855d27270"
},
"host-environment-markers": {
"implementation_name": "cpython",
"implementation_version": "3.6.1",
"os_name": "posix",
"platform_machine": "x86_64",
"platform_python_implementation": "CPython",
"platform_release": "17.4.0",
"platform_system": "Darwin",
"platform_version": "Darwin Kernel Version 17.4.0: Sun Dec 17 09:19:54 PST 2017; root:xnu-4570.41.2~1/RELEASE_X86_64",
"python_full_version": "3.6.1",
"python_version": "3.6",
"sys_platform": "darwin"
},
"pipfile-spec": 6,
"requires": {},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"django": {
"hashes": [
"sha256:af18618ce3291be5092893d8522fe3919661bf3a1fb60e3858ae74865a4f07c2",
"sha256:9614851d4a7ff8cbd32b73c6076441f377c45a5bbff7e771798fb02c43c31f47"
],
"version": "==2.0.2"
},
"pytz": {
"hashes": [
"sha256:ed6509d9af298b7995d69a440e2822288f2eca1681b8cce37673dbb10091e5fe",
"sha256:f93ddcdd6342f94cea379c73cddb5724e0d6d0a1c91c9bdef364dc0368ba4fda",
"sha256:61242a9abc626379574a166dc0e96a66cd7c3b27fc10868003fa210be4bff1c9",
"sha256:ba18e6a243b3625513d85239b3e49055a2f0318466e0b8a92b8fb8ca7ccdf55f",
"sha256:07edfc3d4d2705a20a6e99d97f0c4b61c800b8232dc1c04d87e8554f130148dd",
"sha256:3a47ff71597f821cd84a162e71593004286e5be07a340fd462f0d33a760782b5",
"sha256:5bd55c744e6feaa4d599a6cbd8228b4f8f9ba96de2c38d56f08e534b3c9edf0d",
"sha256:887ab5e5b32e4d0c86efddd3d055c1f363cbaa583beb8da5e22d2fa2f64d51ef",
"sha256:410bcd1d6409026fbaa65d9ed33bf6dd8b1e94a499e32168acfc7b332e4095c0"
],
"version": "==2018.3"
}
},
"develop": {}
}

可以看到里面标识了 Python 环境基本信息,以及依赖包的版本及 hashes 值。 另外我们还可以注意到 Pipfile 文件内容也有更新,[packages] 部分多了一句 django = ““,标识了本项目依赖于 Django,这个其实类似于 requirements.txt 文件。 那么到这里有小伙伴可能就会问了, Pipfile 和 Pipfile.lock 有什么用呢? Pipfile 其实一个 TOML 格式的文件,标识了该项目依赖包的基本信息,还区分了生产环境和开发环境的包标识,作用上类似 requirements.txt 文件,但是功能更为强大。Pipfile.lock 详细标识了该项目的安装的包的精确版本信息、最新可用版本信息和当前库文件的 hash 值,顾明思义,它起了版本锁的作用,可以注意到当前 Pipfile.lock 文件中的 Django 版本标识为 ==2.0.2,意思是当前我们开发时使用的就是 2.0.2 版本,它可以起到版本锁定的功能。 举个例子,刚才我们安装了 Django 2.0.2 的版本,即目前(2018.2.27)的最新版本。但可能 Django 以后还会有更新,比如某一天 Django 更新到了 2.1 版本,这时如果我们想要重新部署本项目到另一台机器上,假如此时不存在 Pipfile.lock 文件,只存在 Pipfile文件,由于 Pipfile 文件中标识的 Django 依赖为 django = ““,即没有版本限制,它会默认安装最新版本的 Django,即 2.1,但由于 Pipfile.lock 文件的存在,它会根据 Pipfile.lock 来安装,还是会安装 Django 2.0.2,这样就会避免一些库版本更新导致不兼容的问题。 请记住:任何情况下都不要手动修改 Pipfile.lock 文件! 好,接下来我们再回归正题,现在已经安装好了 Django 了,那么我们重新运行此脚本便可以成功输出 Django 版本信息了:

1
(PipenvTest-VSTVh89E) CQC-MAC% python3 main.py

结果如下:

1
2.0.2

这样我们就成功安装了 Django 2.x 了,和系统的 Django 1.11 没有任何冲突。 在此模式的命令行下,我们就可以使用虚拟环境下的 Python 解释器,而且所安装的依赖包对外部系统没有任何影响,而且使用 Pipfile 和 Pipfile.lock 来管理项目的依赖更加方便和健壮。 如果想要退出虚拟环境,只需要输入 exit 命令即可:

1
2
3
(PipenvTest-VSTVh89E) CQC-MAC% exit
➜ PipenvTest python3 main.py
1.11

输入退出命令之后,我们重新再运行此脚本,就会重新使用系统的 Python 解释器,Django 版本又重新回到了 1.11。 由此可以看来,有了 Pipenv,我们可以使用 Pipfile 和 Pipfile.lock 来方便地管理和维护项目的依赖包,而且可以实现虚拟环境运行,避免了包冲突问题,可谓一举两得。

常用命令

上文我们介绍了 Pipenv 的基本操作,下面我们再介绍一下它的一些常用命令。

虚拟环境路径

我们可以使用 —venv 参数来获得虚拟环境路径:

1
pipenv --venv

样例输出如下:

1
/Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E

可见这个路径是一个标准的路径,Pipenv 会把虚拟环境统一放到 virtualenvs 文件夹下,而不是本项目路径下。

Python 解释器路径

要获取虚拟环境 Python 解释器路径,可以使用 —py 参数:

1
pipenv --py

样例输出如下:

1
/Users/CQC/.local/share/virtualenvs/PipenvTest-VSTVh89E/bin/python

加载系统 Python 包

默认情况下,新创建的虚拟环境是不包含任何第三方包的,但我们也可以开启加载系统 Python 包功能,使用 —site-packages 即可:

1
pipenv --site-packages

这样创建的虚拟环境便可以使用系统已安装的 Python 包了。

开启虚拟环境

要开启虚拟环境只需要执行如下命令:

1
pipenv shell

这样就可以进入虚拟环境,此时运行的 python、python3 命令都是虚拟环境下的。

安装 Python 包

安装 Python 包我们不再需要 pip 来安装,直接使用 Pipenv 也可安装,如安装 requests,命令如下:

1
pipenv install requests

安装完成之后会同时更新项目目录下的 Pipfile 和 Pipfile.lock 文件。 有时候一些 Python 包是仅仅开发环境需要的,如 pytest,这时候我们通过添加 —dev 参数即可,命令如下:

1
pipenv install pytest --dev

这时候,pytest 的依赖便会记录在 Pipfile 的 [dev-packages] 区域:

1
2
[dev-packages]
pytest = "*"

获取包依赖

我们可以使用命令来清晰地呈现出当前安装的 Python 包版本及之间的依赖关系,命令如下:

1
pipenv graph

样例结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Django==2.0.2
- pytz [required: Any, installed: 2018.3]
pytest==3.4.1
- attrs [required: >=17.2.0, installed: 17.4.0]
- pluggy [required: <0.7,>=0.5, installed: 0.6.0]
- py [required: >=1.5.0, installed: 1.5.2]
- setuptools [required: Any, installed: 38.5.1]
- six [required: >=1.10.0, installed: 1.11.0]
requests==2.18.4
- certifi [required: >=2017.4.17, installed: 2018.1.18]
- chardet [required: >=3.0.2,<3.1.0, installed: 3.0.4]
- idna [required: <2.7,>=2.5, installed: 2.6]
- urllib3 [required: <1.23,>=1.21.1, installed: 1.22]

可以看到结果非常清晰,Django 当前安装了 2.0.2版本,依赖于 pytz 任何版本,已经安装了 2018.3 版本;pytest 已经安装了 3.4.1 版本,依赖 attrs>=17.2.0 版本,已经安装了 17.4.0 版本,另外还依赖 pluggy、py、setuptools、six 这些库。总之包的依赖关系一目了然。

卸载 Python 包

卸载 Python 包也非常简单,如卸载 requests 包,命令如下:

1
pipenv uninstall requests

卸载完成之后,Pipfile 和 Pipfile.lock 文件同样会更新。 如果要卸载全部 Python 包,可以添加 —all 参数:

1
pipenv uninstall --all

产生 Pipfile.lock

有时候可能 Pipfile.lock 文件不存在或被删除了,这时候我们可以使用如下命令生成:

1
pipenv lock

以上便是一些常用的 Pipenv 命令,如果要查看更多用法可以参考其官方文档:https://docs.pipenv.org/#pipenv-usage

结语

本文介绍了 Pipenv 的基本用法,作为 pip 和 virtualenv 的结合体,我们可以利用它更方便地创建和管理 Python 虚拟环境,还可以用更加科学的方式管理 Python 包,一举两得。 嗯,是时候抛弃 virtualenv 和 pip 了!

Python

原理

中文分词,即 Chinese Word Segmentation,即将一个汉字序列进行切分,得到一个个单独的词。表面上看,分词其实就是那么回事,但分词效果好不好对信息检索、实验结果还是有很大影响的,同时分词的背后其实是涉及各种各样的算法的。 中文分词与英文分词有很大的不同,对英文而言,一个单词就是一个词,而汉语是以字为基本的书写单位,词语之间没有明显的区分标记,需要人为切分。根据其特点,可以把分词算法分为四大类:

  • 基于规则的分词方法
  • 基于统计的分词方法
  • 基于语义的分词方法
  • 基于理解的分词方法

下面我们对这几种方法分别进行总结。

基于规则的分词方法

这种方法又叫作机械分词方法、基于字典的分词方法,它是按照一定的策略将待分析的汉字串与一个“充分大的”机器词典中的词条进行匹配。若在词典中找到某个字符串,则匹配成功。该方法有三个要素,即分词词典、文本扫描顺序和匹配原则。文本的扫描顺序有正向扫描、逆向扫描和双向扫描。匹配原则主要有最大匹配、最小匹配、逐词匹配和最佳匹配。

  • 最大匹配法(MM)。基本思想是:假设自动分词词典中的最长词条所含汉字的个数为 i,则取被处理材料当前字符串序列中的前 i 个字符作为匹配字段,查找分词词典,若词典中有这样一个 i 字词,则匹配成功,匹配字段作为一个词被切分出来;若词典中找不到这样的一个 i 字词,则匹配失败,匹配字段去掉最后一个汉字,剩下的字符作为新的匹配字段,再进行匹配,如此进行下去,直到匹配成功为止。统计结果表明,该方法的错误率 为 1/169。
  • 逆向最大匹配法(RMM)。该方法的分词过程与 MM 法相同,不同的是从句子(或文章)末尾开始处理,每次匹配不成功时去掉的是前面的一个汉字。统计结果表明,该方法的错误率为 1/245。
  • 逐词遍历法。把词典中的词按照由长到短递减的顺序逐字搜索整个待处理的材料,一直到把全部的词切分出来为止。不论分词词典多大,被处理的材料多么小,都得把这个分词词典匹配一遍。
  • 设立切分标志法。切分标志有自然和非自然之分。自然切分标志是指文章中出现的非文字符号,如标点符号等;非自然标志是利用词缀和不构成词的词(包 括单音词、复音节词以及象声词等)。设立切分标志法首先收集众多的切分标志,分词时先找出切分标志,把句子切分为一些较短的字段,再用 MM、RMM 或其它的方法进行细加工。这种方法并非真正意义上的分词方法,只是自动分词的一种前处理方式而已,它要额外消耗时间扫描切分标志,增加存储空间存放那些非 自然切分标志。
  • 最佳匹配法(OM)。此法分为正向的最佳匹配法和逆向的最佳匹配法,其出发点是:在词典中按词频的大小顺序排列词条,以求缩短对分词词典的检索时 间,达到最佳效果,从而降低分词的时间复杂度,加快分词速度。实质上,这种方法也不是一种纯粹意义上的分词方法,它只是一种对分词词典的组织方式。OM 法的分词词典每条词的前面必须有指明长度的数据项,所以其空间复杂度有所增加,对提高分词精度没有影响,分词处理的时间复杂度有所降低。

此种方法优点是简单,易于实现。但缺点有很多:匹配速度慢;存在交集型和组合型歧义切分问题;词本身没有一个标准的定义,没有统一标准的词集;不同词典产生的歧义也不同;缺乏自学习的智能性。

基于统计的分词方法

该方法的主要思想:词是稳定的组合,因此在上下文中,相邻的字同时出现的次数越多,就越有可能构成一个词。因此字与字相邻出现的概率或频率能较好地反映成词的可信度。可以对训练文本中相邻出现的各个字的组合的频度进行统计,计算它们之间的互现信息。互现信息体现了汉字之间结合关系的紧密程度。当紧密程 度高于某一个阈值时,便可以认为此字组可能构成了一个词。该方法又称为无字典分词。 该方法所应用的主要的统计模型有:N 元文法模型(N-gram)、隐马尔可夫模型(Hiden Markov Model,HMM)、最大熵模型(ME)、条件随机场模型(Conditional Random Fields,CRF)等。 在实际应用中此类分词算法一般是将其与基于词典的分词方法结合起来,既发挥匹配分词切分速度快、效率高的特点,又利用了无词典分词结合上下文识别生词、自动消除歧义的优点。

基于语义的分词方法

语义分词法引入了语义分析,对自然语言自身的语言信息进行更多的处理,如扩充转移网络法、知识分词语义分析法、邻接约束法、综合匹配法、后缀分词法、特征词库法、矩阵约束法、语法分析法等。

  • 扩充转移网络法。该方法以有限状态机概念为基础。有限状态机只能识别正则语言,对有限状态机作的第一次扩充使其具有递归能力,形成递归转移网络 (RTN)。在RTN 中,弧线上的标志不仅可以是终极符(语言中的单词)或非终极符(词类),还可以调用另外的子网络名字分非终极符(如字或字串的成词条件)。这样,计算机在 运行某个子网络时,就可以调用另外的子网络,还可以递归调用。词法扩充转移网络的使用, 使分词处理和语言理解的句法处理阶段交互成为可能,并且有效地解决了汉语分词的歧义。
  • 矩阵约束法。其基本思想是:先建立一个语法约束矩阵和一个语义约束矩阵, 其中元素分别表明具有某词性的词和具有另一词性的词相邻是否符合语法规则, 属于某语义类的词和属于另一词义类的词相邻是否符合逻辑,机器在切分时以之约束分词结果。

基于理解的分词方法

基于理解的分词方法是通过让计算机模拟人对句子的理解,达到识别词的效果。其基本思想就是在分词的同时进行句法、语义分析,利用句法信息和语义信息来处理歧义现象。它通常包括三个部分:分词子系统、句法语义子系统、总控部分。在总控部分的协调下,分词子系统可以获得有关词、句子等的句法和语义信息来对分词歧义进行判断,即它模拟了人对句子的理解过程。这种分词方法需要使用大量的语言知识和信息。目前基于理解的分词方法主要有专家系统分词法和神经网络分词法等。

  • 专家系统分词法。从专家系统角度把分词的知识(包括常识性分词知识与消除歧义切分的启发性知识即歧义切分规则)从实现分词过程的推理机中独立出来,使知识库的维护与推理机的实现互不干扰,从而使知识库易于维护和管理。它还具有发现交集歧义字段和多义组合歧义字段的能力和一定的自学习功能。
  • 神经网络分词法。该方法是模拟人脑并行,分布处理和建立数值计算模型工作的。它将分词知识所分散隐式的方法存入神经网络内部,通过自学习和训练修改内部权值,以达到正确的分词结果,最后给出神经网络自动分词结果,如使用 LSTM、GRU 等神经网络模型等。
  • 神经网络专家系统集成式分词法。该方法首先启动神经网络进行分词,当神经网络对新出现的词不能给出准确切分时,激活专家系统进行分析判断,依据知识库进行推理,得出初步分析,并启动学习机制对神经网络进行训练。该方法可以较充分发挥神经网络与专家系统二者优势,进一步提高分词效率。

以上便是对分词算法的基本介绍,接下来我们再介绍几个比较实用的分词 Python 库及它们的使用方法。

分词工具

在这里介绍几个比较有代表性的支持分词的 Python 库,主要有:

1. jieba

专用于分词的 Python 库,GitHub:https://github.com/fxsjy/jieba,分词效果较好。 支持三种分词模式:

  • 精确模式,试图将句子最精确地切开,适合文本分析。
  • 全模式,将句子中所有的可能成词的词语都扫描出来,速度非常快,但是不能解决歧义。
  • 搜索引擎模式:在精确模式的基础上,对长词再次切分,提高召回率,适用于搜索引擎分词。

另外 jieba 支持繁体分词,支持自定义词典。 其使用的算法是基于统计的分词方法,主要有如下几种:

  • 基于前缀词典实现高效的词图扫描,生成句子中汉字所有可能成词情况所构成的有向无环图 (DAG)
  • 采用了动态规划查找最大概率路径, 找出基于词频的最大切分组合
  • 对于未登录词,采用了基于汉字成词能力的 HMM 模型,使用了 Viterbi 算法

精确模式分词

首先我们来看下精确模式分词,使用 lcut() 方法,类似 cut() 方法,其参数和 cut() 是一致的,只不过返回结果是列表而不是生成器,默认使用精确模式,代码如下:

1
2
3
4
import jieba
string = '这个把手该换了,我不喜欢日本和服,别把手放在我的肩膀上,工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作'
result = jieba.lcut(string)
print(len(result), '/'.join(result))

结果:

1
38 这个/把手/该换/了//我//喜欢/日本/和服//别/把手/放在//的/肩膀/上//工信处/女干事/每月/经过/下属/科室/都//亲口/交代/24//交换机//技术性/器件/的/安装/工作

可见分词效果还是不错的。

全模式分词

使用全模式分词需要添加 cut_all 参数,将其设置为 True,代码如下:

1
2
result = jieba.lcut(string, cut_all=True)
print(len(result), '/'.join(result))

结果如下:

1
51 这个/把手/该换/了////不/喜欢/日本/和服///别/把手/放在//的/肩膀/上///工信处/处女/女干事/干事/每月/月经/经过/下属/科室/都//亲口/口交/交代/24/口交/交换/交换机/换机/等/技术/技术性/性器/器件//安装/安装工/装工/工作

搜索引擎模式分词

使用搜索引擎模式分词需要调用 cut_for_search() 方法,代码如下:

1
2
result = jieba.lcut_for_search(string)
print(len(result), '/'.join(result))

结果如下:

1
42 这个/把手/该换/了//我//喜欢/日本/和服//别/把手/放在//的/肩膀/上//工信处/干事/女干事/每月/经过/下属/科室//要/亲口/交代/24/口/交换/换机/交换机/等/技术/技术性/器件/的/安装/工作

另外可以加入自定义词典,如我们想把 日本和服 作为一个整体,可以把它添加到词典中,代码如下:

1
2
3
jieba.add_word('日本和服')
result = jieba.lcut(string)
print(len(result), '/'.join(result))

结果如下:

1
37 这个/把手/该换/了//我//喜欢/日本和服/,//把手/放在/我//肩膀//,/工信处/女干事/每月/经过/下属/科室//要/亲口/交代/24/口/交换机/等/技术性/器件//安装/工作

可以看到切分结果中,日本和服 四个字就作为一个整体出现在结果中了,分词数量比精确模式少了一个。

词性标注

另外 jieba 还支持词性标注,可以输出分词后每个词的词性,实例如下:

1
2
words = pseg.lcut(string)
print(list(map(lambda x: list(x), words)))

运行结果:

1
[['这个', 'r'], ['把手', 'v'], ['该', 'r'], ['换', 'v'], ['了', 'ul'], [',', 'x'], ['我', 'r'], ['不', 'd'], ['喜欢', 'v'], ['日本和服', 'x'], [',', 'x'], ['别', 'r'], ['把手', 'v'], ['放在', 'v'], ['我', 'r'], ['的', 'uj'], ['肩膀', 'n'], ['上', 'f'], [',', 'x'], ['工信处', 'n'], ['女干事', 'n'], ['每月', 'r'], ['经过', 'p'], ['下属', 'v'], ['科室', 'n'], ['都', 'd'], ['要', 'v'], ['亲口', 'n'], ['交代', 'n'], ['24', 'm'], ['口', 'n'], ['交换机', 'n'], ['等', 'u'], ['技术性', 'n'], ['器件', 'n'], ['的', 'uj'], ['安装', 'v'], ['工作', 'vn']]

关于词性的说明可以参考:https://gist.github.com/luw2007/6016931

2. SnowNLP

SnowNLP: Simplified Chinese Text Processing,可以方便的处理中文文本内容,是受到了 TextBlob 的启发而写的,由于现在大部分的自然语言处理库基本都是针对英文的,于是写了一个方便处理中文的类库,并且和 TextBlob 不同的是,这里没有用 NLTK,所有的算法都是自己实现的,并且自带了一些训练好的字典。GitHub地址:https://github.com/isnowfy/snownlp

分词

这里的分词是基于 Character-Based Generative Model 来实现的,论文地址:http://aclweb.org/anthology//Y/Y09/Y09-2047.pdf,我们还是以上面的例子说明,相关使用说明如下:

1
2
3
4
5
6
from snownlp import SnowNLP

string = '这个把手该换了,我不喜欢日本和服,别把手放在我的肩膀上,工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作'
s = SnowNLP(string)
result = s.words
print(len(result), '/'.join(result))

运行结果:

1
40 这个/把手//换//,//不/喜欢/日本//服//别把手/放在/我//肩膀//,//信处女/干事/每月/经过/下属/科室/都//亲口/交代/24//交换机//技术性/器件/的/安装/工作

经过观察,可以发现分词效果其实不怎么理想,和服 被分开了,工信处 也被分开了,女干事 也被分开了。 另外 SnowNLP 还支持很多功能,例如词性标注(HMM)、情感分析、拼音转换(Trie树)、关键词和摘要生成(TextRank)。 我们简单看一个实例:

1
2
3
print('Tags:', list(s.tags))
print('Sentiments:', s.sentiments)
print('Pinyin:', s.pinyin)

运行结果:

1
2
3
Tags: [('这个', 'r'), ('把手', 'Ng'), ('该', 'r'), ('换', 'v'), ('了', 'y'), (',', 'w'), ('我', 'r'), ('不', 'd'), ('喜欢', 'v'), ('日本', 'ns'), ('和', 'c'), ('服', 'v'), (',', 'w'), ('别把手', 'ad'), ('放在', 'v'), ('我', 'r'), ('的', 'u'), ('肩膀', 'n'), ('上', 'f'), (',', 'w'), ('工', 'j'), ('信处女', 'j'), ('干事', 'n'), ('每月', 'r'), ('经过', 'p'), ('下属', 'v'), ('科室', 'n'), ('都', 'd'), ('要', 'v'), ('亲口', 'd'), ('交代', 'v'), ('24', 'm'), ('口', 'q'), ('交换机', 'n'), ('等', 'u'), ('技术性', 'n'), ('器件', 'n'), ('的', 'u'), ('安装', 'vn'), ('工作', 'vn')]
Sentiments: 0.015678817603646866
Pinyin: ['zhe', 'ge', 'ba', 'shou', 'gai', 'huan', 'liao', ',', 'wo', 'bu', 'xi', 'huan', 'ri', 'ben', 'he', 'fu', ',', 'bie', 'ba', 'shou', 'fang', 'zai', 'wo', 'de', 'jian', 'bang', 'shang', ',', 'gong', 'xin', 'chu', 'nv', 'gan', 'shi', 'mei', 'yue', 'jing', 'guo', 'xia', 'shu', 'ke', 'shi', 'dou', 'yao', 'qin', 'kou', 'jiao', 'dai', '24', 'kou', 'jiao', 'huan', 'ji', 'deng', 'ji', 'shu', 'xing', 'qi', 'jian', 'de', 'an', 'zhuang', 'gong', 'zuo']

3. THULAC

THULAC(THU Lexical Analyzer for Chinese)由清华大学自然语言处理与社会人文计算实验室研制推出的一套中文词法分析工具包,GitHub 链接:https://github.com/thunlp/THULAC-Python,具有中文分词和词性标注功能。THULAC具有如下几个特点:

  • 能力强。利用集成的目前世界上规模最大的人工分词和词性标注中文语料库(约含5800万字)训练而成,模型标注能力强大。
  • 准确率高。该工具包在标准数据集Chinese Treebank(CTB5)上分词的F1值可达97.3%,词性标注的F1值可达到92.9%,与该数据集上最好方法效果相当。
  • 速度较快。同时进行分词和词性标注速度为300KB/s,每秒可处理约15万字。只进行分词速度可达到1.3MB/s。

我们用一个实例看一下分词效果:

1
2
3
4
5
6
import thulac

string = '这个把手该换了,我不喜欢日本和服,别把手放在我的肩膀上,工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作'
t = thulac.thulac()
result = t.cut(string)
print(result)

运行结果:

1
[['这个', 'r'], ['把手', 'n'], ['该', 'v'], ['换', 'v'], ['了', 'u'], [',', 'w'], ['我', 'r'], ['不', 'd'], ['喜欢', 'v'], ['日本', 'ns'], ['和服', 'n'], [',', 'w'], ['别把手', 'n'], ['放', 'v'], ['在', 'p'], ['我', 'r'], ['的', 'u'], ['肩膀', 'n'], ['上', 'f'], [',', 'w'], ['工信处', 'n'], ['女', 'a'], ['干事', 'n'], ['每月', 'r'], ['经过', 'p'], ['下属', 'v'], ['科室', 'n'], ['都', 'd'], ['要', 'v'], ['亲口', 'd'], ['交代', 'v'], ['24', 'm'], ['口', 'q'], ['交换机', 'n'], ['等', 'u'], ['技术性', 'n'], ['器件', 'n'], ['的', 'u'], ['安装', 'v'], ['工作', 'v']]

4. NLPIR

NLPIR 分词系统,前身为2000年发布的 ICTCLAS 词法分析系统,GitHub 链接:https://github.com/NLPIR-team/NLPIR,是由北京理工大学张华平博士研发的中文分词系统,经过十余年的不断完善,拥有丰富的功能和强大的性能。NLPIR是一整套对原始文本集进行处理和加工的软件,提供了中间件处理效果的可视化展示,也可以作为小规模数据的处理加工工具。主要功能包括:中文分词,词性标注,命名实体识别,用户词典、新词发现与关键词提取等功能。另外对于分词功能,它有 Python 实现的版本,GitHub 链接:https://github.com/tsroten/pynlpir。 使用方法如下:

1
2
3
4
5
6
import pynlpir

pynlpir.open()
string = '这个把手该换了,我不喜欢日本和服,别把手放在我的肩膀上,工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作'
result = pynlpir.segment(string)
print(result)

运行结果如下:

1
[('这个', 'pronoun'), ('把', 'preposition'), ('手', 'noun'), ('该', 'pronoun'), ('换', 'verb'), ('了', 'modal particle'), (',', 'punctuation mark'), ('我', 'pronoun'), ('不', 'adverb'), ('喜欢', 'verb'), ('日本', 'noun'), ('和', 'conjunction'), ('服', 'verb'), (',', 'punctuation mark'), ('别', 'adverb'), ('把', 'preposition'), ('手', 'noun'), ('放', 'verb'), ('在', 'preposition'), ('我', 'pronoun'), ('的', 'particle'), ('肩膀', 'noun'), ('上', 'noun of locality'), (',', 'punctuation mark'), ('工', 'noun'), ('信', 'noun'), ('处女', 'noun'), ('干事', 'noun'), ('每月', 'pronoun'), ('经过', 'preposition'), ('下属', 'verb'), ('科室', 'noun'), ('都', 'adverb'), ('要', 'verb'), ('亲口', 'adverb'), ('交代', 'verb'), ('24', 'numeral'), ('口', 'classifier'), ('交换机', 'noun'), ('等', 'particle'), ('技术性', 'noun'), ('器件', 'noun'), ('的', 'particle'), ('安装', 'verb'), ('工作', 'verb')]

这里 把手 和 和服 也被分开了。

5. NLTK

NLTK,Natural Language Toolkit,是一个自然语言处理的包工具,各种多种 NLP 处理相关功能,GitHub 链接:https://github.com/nltk/nltk。 但是 NLTK 对于中文分词是不支持的,示例如下:

1
2
3
4
5
from nltk import word_tokenize

string = '这个把手该换了,我不喜欢日本和服,别把手放在我的肩膀上,工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作'
result = word_tokenize(string)
print(result)

结果:

1
['这个把手该换了,我不喜欢日本和服,别把手放在我的肩膀上,工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作']

如果要用中文分词的话,可以使用 FoolNLTK,它使用 Bi-LSTM 训练而成,包含分词、词性标注、实体识别等功能,同时支持自定义词典,可以训练自己的模型,可以进行批量处理。 使用方法如下:

1
2
3
4
5
import fool

string = '这个把手该换了,我不喜欢日本和服,别把手放在我的肩膀上,工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作'
result = fool.cut(string)
print(result)

运行结果:

1
[['这个', '把手', '该', '换', '了', ',', '我', '不', '喜欢', '日本', '和服', ',', '别', '把', '手', '放', '在', '我', '的', '肩膀', '上', ',', '工信处', '女', '干事', '每月', '经过', '下属', '科室', '都', '要', '亲', '口', '交代', '24', '口', '交换机', '等', '技术性', '器件', '的', '安装', '工作']]

可以看到这个分词效果还是不错的。 另外还可以进行词性标注,实体识别:

1
2
3
4
result = fool.pos_cut(string)
print(result)
_, ners = fool.analysis(string)
print(ners)

运行结果:

1
2
[[('这个', 'r'), ('把手', 'n'), ('该', 'r'), ('换', 'v'), ('了', 'y'), (',', 'wd'), ('我', 'r'), ('不', 'd'), ('喜欢', 'vi'), ('日本', 'ns'), ('和服', 'n'), (',', 'wd'), ('别', 'd'), ('把', 'pba'), ('手', 'n'), ('放', 'v'), ('在', 'p'), ('我', 'r'), ('的', 'ude'), ('肩膀', 'n'), ('上', 'f'), (',', 'wd'), ('工信处', 'ns'), ('女', 'b'), ('干事', 'n'), ('每月', 'r'), ('经过', 'p'), ('下属', 'v'), ('科室', 'n'), ('都', 'd'), ('要', 'v'), ('亲', 'a'), ('口', 'n'), ('交代', 'v'), ('24', 'm'), ('口', 'q'), ('交换机', 'n'), ('等', 'udeng'), ('技术性', 'n'), ('器件', 'n'), ('的', 'ude'), ('安装', 'n'), ('工作', 'n')]]
[[(12, 15, 'location', '日本')]]

6. LTP

语言技术平台(Language Technology Platform,LTP)是哈工大社会计算与信息检索研究中心历时十年开发的一整套中文语言处理系统。LTP制定了基于XML的语言处理结果表示,并在此基础上提供了一整套自底向上的丰富而且高效的中文语言处理模块(包括词法、句法、语义等6项中文处理核心技术),以及基于动态链接库(Dynamic Link Library, DLL)的应用程序接口、可视化工具,并且能够以网络服务(Web Service)的形式进行使用。 LTP 有 Python 版本,GitHub地址:https://github.com/HIT-SCIR/pyltp,另外运行的时候需要下载模型,模型还比较大,下载地址:http://ltp.ai/download.html。 示例代码如下:

1
2
3
4
5
6
7
8
from pyltp import Segmentor

string = '这个把手该换了,我不喜欢日本和服,别把手放在我的肩膀上,工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作'
segmentor = Segmentor()
segmentor.load('./cws.model')
result = list(segmentor.segment(string))
segmentor.release()
print(result)

运行结果:

1
41 这个/把手//换//,//不/喜欢/日本/和服/,//把//放在//的/肩膀/上//工信/处女/干事/每月/经过/下属/科室//要/亲口/交代/24/口/交换机/等/技术性/器件//安装/工作

可以发现 工信处、女干事 没有正确分开。 以上便是一些分词库的基本使用,个人比较推荐的有 jieba、THULAC、FoolNLTK。

参考来源

Python

本节详细说明一下深度学习环境配置,Ubuntu 16.04 + Nvidia GTX 1080 + Python 3.6 + CUDA 9.0 + cuDNN 7.1 + TensorFlow 1.6。

Python 3.6

首先安装 Python 3.6,这里使用 Anaconda 3 来安装,下载地址:https://www.anaconda.com/download/#linux,点击 Download 按钮下载即可,这里下载的是 Anaconda 3-5.1 版本,如果下载速度过慢可以选择使用清华镜像:https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/。 下载下来之后目录下会出现一个 Anaconda3-5.1.0-Linux-x86_64.sh 文件,然后直接执行即可安装:

1
bash Anaconda3-5.1.0-Linux-x86_64.sh

执行完毕之后按照默认设置走下来即可完成安装。 这里默认它会安装到用户目录下,如果想全局安装,可以在这一步输入你要安装的地址:

1
2
3
4
5
6
7
8
9
Anaconda3 will now be installed into this location:
/home/cqc/anaconda3

- Press ENTER to confirm the location
- Press CTRL-C to abort the installation
- Or specify a different location below

[/home/cqc/anaconda3] >>> /usr/local/anaconda3
PREFIX=/usr/local/anaconda3

这里我指定了将其安装到 /usr/local/anaconda3 目录下,全局安装,所有用户共享,当然如果只想本用户使用的话使用默认配置即可。 安装完成之后添加 python3 和 pip3 的软链接:

1
2
sudo ln -s /usr/local/anaconda3/bin/python3 /usr/local/sbin/python3
sudo ln -s /usr/local/anaconda3/bin/pip /usr/local/sbin/pip3

这里是将软连接其添加到 /usr/local/sbin 目录下了,它默认会存在于环境变量中,因此可以直接调用。 当然也可以选择把 /usr/local/anaconda3/bin 目录添加到环境变量中,可以修改 ~/.bashrc 文件,添加如下内容:

1
export PATH=/usr/local/anaconda3/bin${PATH:+:${PATH}}

然后执行:

1
source ~/.bashrc

即可生效,下次登录时也会默认执行 ~/.bashrc 文件,也会生效。 接下来我们验证下 python3、pip3 命令是否都来自 Anaconda,命令如下:

1
2
pip3 -V
pip 9.0.1 from /usr/local/anaconda3/lib/python3.6/site-packages (python 3.6)
1
2
3
4
5
6
7
which python3
/usr/local/anaconda3/bin/python3
python3
Python 3.6.4 |Anaconda, Inc.| (default, Jan 16 2018, 18:10:19)
[GCC 7.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

如果输入 pip3 和 python3 命令能出现如上类似结果,路径都在 /usr/local/anaconda3,就证明 Python 3 安装成功了。

安装驱动

首先查看一下自己的电脑需要怎样的驱动,我们可以先到 http://www.nvidia.com/Download/index.aspx 查询下我们需要的是怎样的驱动,这里我的显卡是 GTX 1080,所以以此为例说明,勾选好对应的配置: 点击 Search,可以看到查询结果如下所示:

1
2
3
4
5
Version:    390.25
Release Date: 2018.1.29
Operating System: Linux 64-bit
Language: English (US)
File Size: 77.48 MB

这里说明我们需要的版本是 390.25。 接下来如果我们之前安装了驱动的话,可以重新安装一下,如果当前已经安装好了就不必了。 如果要重装,需要首先卸载掉之前的显卡驱动:

1
sudo apt-get remove –purge nvidia*

运行之后 NVIDIA 的一些驱动就被卸载了。 这时候 nvidia-smi 等命令已经不能用了,这就证明显卡驱动已经被卸载了。 然后接下来添加一个 PPA 源,命令如下:

1
sudo add-apt-repository ppa:graphics-drivers/ppa

然后更新一下:

1
sudo apt-get update

随后重新安装显卡驱动:

1
sudo apt-get install nvidia-390

注意这里的 390 就是刚才我们查询出来的版本,以实际查询出来的版本为准。

CUDA 9.0

如果存在之前的旧版本,可以选择先卸载,以免和新的 CUDA 版本产生冲突,在 /usr/local/cuda/bin 目录下有一个 uninstallcuda*.pl 文件,可以直接运行卸载,命令如下:

1
sudo ./uninstall_cuda_*.pl

这样即可将 CUDA 全部卸载。 接下来我们再下载 CUDA 9.0,注意 TensorFlow 1.5 和 1.6 版本依然只是兼容 CUDA 9.0,没有兼容 CUDA 9.1,所以不要下载 9.1,CUDA 9.0 的下载地址是:https://developer.nvidia.com/cuda-90-download-archive,然后依次勾选好系统的版本,如图所示: 这里我们选择 Linux-x86_64-Ubuntu-16.04-runfile 的配置,然后点击 Base Installer 部分的 Download 按钮,下载 CUDA 9.0 安装包。 对应的下载命令是:

1
wget https://developer.nvidia.com/compute/cuda/9.0/Prod/local_installers/cuda_9.0.176_384.81_linux-run

执行此命令,等待下载完成即可。 接下来执行安装,运行如下命令:

1
sudo bash cuda_9.0.176_384.81_linux-run

安装过程需要输入一些确认选项,过程如下:

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
Description

The NVIDIA CUDA Toolkit provides command-line and graphical
tools for building, debugging and optimizing the performance
Do you accept the previously read EULA?
accept/decline/quit: accept

Install NVIDIA Accelerated Graphics Driver for Linux-x86_64 384.81?
(y)es/(n)o/(q)uit: n

Install the CUDA 9.0 Toolkit?
(y)es/(n)o/(q)uit: y

Enter Toolkit Location
[ default is /usr/local/cuda-9.0 ]:

Do you want to install a symbolic link at /usr/local/cuda?
(y)es/(n)o/(q)uit: y

Install the CUDA 9.0 Samples?
(y)es/(n)o/(q)uit: y

Enter CUDA Samples Location
[ default is /home/cqc ]:

Installing the CUDA Toolkit in /usr/local/cuda-9.0 ...

最后如果出现这样的提示,就证明 CUDA 安装好了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Driver:   Not Selected
Toolkit: Installed in /usr/local/cuda-9.0
Samples: Installed in /home/cqc, but missing recommended libraries

Please make sure that
- PATH includes /usr/local/cuda-9.0/bin
- LD_LIBRARY_PATH includes /usr/local/cuda-9.0/lib64, or, add /usr/local/cuda-9.0/lib64 to /etc/ld.so.conf and run ldconfig as root

To uninstall the CUDA Toolkit, run the uninstall script in /usr/local/cuda-9.0/bin

Please see CUDA_Installation_Guide_Linux.pdf in /usr/local/cuda-9.0/doc/pdf for detailed information on setting up CUDA.

***WARNING: Incomplete installation! This installation did not install the CUDA Driver. A driver of version at least 384.00 is required for CUDA 9.0 functionality to work.
To install the driver using this installer, run the following command, replacing <CudaInstaller> with the name of this run file:
sudo <CudaInstaller>.run -silent -driver

然后我们需要配置一下环境变量,更改 ~/.bashrc 文件,添加如下几行:

1
2
3
export PATH=/usr/local/cuda/bin${PATH:+:${PATH}}
export LD_LIBRARY_PATH=/usr/local/cuda/lib64${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}
export CUDA_HOME=/usr/local/cuda

修改完毕之后执行一下使其生效:

1
source ~/.bashrc

这时我们输出 CUDA_HOME、LD_LIBRARY_PATH 就可以看到对应的输出了:

1
2
3
4
echo $CUDA_HOME
/usr/local/cuda
echo $LD_LIBRARY_PATH
/usr/local/cuda/lib64

这样就代表环境变量生效了,CUDA 安装完成。

cuDNN 7.1

cuDNN 的全称是 The NVIDIA CUDA® Deep Neural Network library,是专门用来对深度学习加速的库,它支持 Caffe2, MATLAB, Microsoft Cognitive Toolkit, TensorFlow, Theano 及 PyTorch 等深度学习的加速优化,目前最新版本是 cuDNN 7.1,接下来我们来看下它的安装方式。 下载链接:https://developer.nvidia.com/rdp/cudnn-download,需要注册之后才能打开,这里我们选择 cuDNN v7.1.1 (Feb 28, 2018), for CUDA 9.0,然后选择 cuDNN v7.1.1 Library for Linux,如图所示: 下载下来之后解压安装即可:

1
2
3
4
5
tar -zxvf cudnn-9.0-linux-x64-v7.1.tgz
sudo cp cuda/include/cudnn.h /usr/local/cuda/include/
sudo cp cuda/lib64/libcudnn* /usr/local/cuda/lib64/ -d
sudo chmod a+r /usr/local/cuda/include/cudnn.h
sudo chmod a+r /usr/local/cuda/lib64/libcudnn*

执行完如上命令之后,cuDNN 就安装好了,这时我们可以发现在 /usr/local/cuda/include 目录下就多了 cudnn.h 头文件。

TensorFlow 1.6

到现在为止 Python 3.6、CUDA 9.0 和 cuDNN 7.1 就已经安装好了,而且环境变量也配置好了,接下来我们直接安装 TensorFlow 1.6 即可,TensorFlow 1.6 版本针对 CUDA 9 和 cuDNN 7 做了优化,可以预构建二进制文件。 这里需要安装的是 TensorFlow 的 GPU 版本,命令如下:

1
pip3 install tensorflow-gpu==1.6.0

安装完成之后验证一下:

1
import tensorflow

如果没有报错,那就证明全部环境配置都成功了。 以上便是 Ubuntu 16.04 + Nvidia GTX 1080 + Python 3.6 + CUDA 9.0 + cuDNN 7.1 + TensorFlow 1.6 完整环境配置过程。

Linux

系统

查看系统版本:

1
cat /proc/version

实例结果:

1
Linux version 4.4.0-112-generic (buildd@lgw01-amd64-010) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.5) ) #135-Ubuntu SMP Fri Jan 19 11:48:36 UTC 2018

CPU

查看CPU信息:

1
cat /proc/cpuinfo

示例结果:

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
51
52
53
processor       : 0
vendor_id : GenuineIntel
cpu family : 6
model : 63
model name : Intel(R) Xeon(R) CPU E5-2673 v3 @ 2.40GHz
stepping : 2
microcode : 0x3a
cpu MHz : 1200.000
cache size : 30720 KB
physical id : 0
siblings : 24
core id : 0
cpu cores : 12
apicid : 0
initial apicid : 0
fpu : yes
fpu_exception : yes
cpuid level : 15
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb invpcid_single kaiser tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid cqm xsaveopt cqm_llc cqm_occup_llc dtherm ida arat pln pts
bugs :
bogomips : 4800.24
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:

processor : 1
vendor_id : GenuineIntel
cpu family : 6
model : 63
model name : Intel(R) Xeon(R) CPU E5-2673 v3 @ 2.40GHz
stepping : 2
microcode : 0x3a
cpu MHz : 1207.687
cache size : 30720 KB
physical id : 0
siblings : 24
core id : 1
cpu cores : 12
apicid : 2
initial apicid : 2
fpu : yes
fpu_exception : yes
cpuid level : 15
wp : yes
flags : fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm epb invpcid_single kaiser tpr_shadow vnmi flexpriority ept vpid fsgsbase tsc_adjust bmi1 avx2 smep bmi2 erms invpcid cqm xsaveopt cqm_llc cqm_occup_llc dtherm ida arat pln pts
bugs :
bogomips : 4800.24
clflush size : 64
cache_alignment : 64
address sizes : 46 bits physical, 48 bits virtual
power management:

内存

查看内存及用量(M为单位):

1
free -m

示例结果:

1
2
3
              total        used        free      shared  buff/cache   available
Mem: 128806 7115 115910 77 5780 120877
Swap: 0 0 0

查看内存及用量(G为单位):

1
free -g

示例结果:

1
2
3
              total        used        free      shared  buff/cache   available
Mem: 125 6 113 0 5 118
Swap: 0 0 0

硬盘用量

查看各分区使用情况:

1
df -h

示例结果:

1
2
3
4
5
6
Filesystem      Size  Used Avail Use% Mounted on
udev 63G 0 63G 0% /dev
tmpfs 13G 66M 13G 1% /run
/dev/sda1 224G 101G 113G 48% /
tmpfs 63G 352K 63G 1% /dev/shm
tmpfs 5.0M 4.0K 5.0M 1% /run/lock

查看当前目录文件及文件夹大小:

1
du -sh *

示例结果:

1
2
3
4
5
6.9G    anaconda3
213M cuda_samples
7.9M Desktop
8.0K Documents
7.1G Downloads

GPU

查看 GPU 使用情况:

1
nvidia-smi

示例结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Sun Mar 11 15:34:14 2018       
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 384.111 Driver Version: 384.111 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
|===============================+======================+======================|
| 0 GeForce GTX 1080 Off | 00000000:02:00.0 On | N/A |
| 0% 36C P8 15W / 320W | 224MiB / 8105MiB | 0% Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes: GPU Memory |
| GPU PID Type Process name Usage |
|=============================================================================|
| 0 1322 G /usr/lib/xorg/Xorg 178MiB |
| 0 1880 G compiz 43MiB |
+-----------------------------------------------------------------------------+

进程

查看所有进程:

1
ps -ef

示例结果:

1
2
3
4
5
6
UID        PID  PPID  C STIME TTY          TIME CMD
root 1 0 0 Feb09 ? 00:00:15 /sbin/init splash
root 2 0 0 Feb09 ? 00:00:00 [kthreadd]
root 3 2 0 Feb09 ? 00:00:05 [ksoftirqd/0]
root 5 2 0 Feb09 ? 00:00:00 [kworker/0:0H]
root 6 2 0 Feb09 ? 00:00:01 [kworker/u96:0]

筛选进程:

1
ps -ef | grep python

示例结果:

1
2
chen      6466 44495  0 Mar09 ?        00:01:56 /usr/bin/python2 -m ipykernel_launcher -f /run/user/1000/jupyter/kernel-218926a1-f3b9-4410-bffb-1de1341732be.json
chen 11630 44495 0 Mar09 ? 00:00:13 /home/chen/anaconda3/bin/python -m ipykernel_launcher -f /run/user/1000/jupyter/kernel-c2942b7d-d73c-420a-b6be-78f0169e68c9.json

根据名称强制杀死进程:

1
ps -ef | grep python | cut -c 9-15 | xargs kill -9

用户

添加用户:

1
sudo adduser username

添加用户并设置目录:

1
sudo adduser username --home /home/username

将用户设置超级用户组(如添加到 sudo 用户组):

1
sudo usermod -aG sudo username

查看所有用户组:

1
groups

查看某个用户组下所有用户:

1
2
sudo apt-get install members
members sudo

Python

了解了正则表达式,想必一般情况下的匹配都不会出现什么问题,但是如果一些特殊情况,可能需要用到一些更高级的正则表达式匹配操作,本节我们来说明一下正则表达式的一个较常用又比较重要的知识点——零宽断言。

实例引入

首先我们来看一个例子,这里有一段问答对话:

问:我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件? 答:在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。 问:为什么我看到的卡号输入框显示为*符号? 答:您的浏览器禁止下载执行ActiveX控件 , 对于这种情况 , 您必须打开浏览器的ActiveX的相关权限。 操作方法:在浏览器菜单中选择“工具”|“Internet选项”,在弹出的对话框中选择”安全” |”Internet”|”自定义级别”,在弹出的对话框中选择”重置为 安全级-中” , 点”重置”按钮,确定。 问:看了以上几个问题,还是不能登录,怎么办? 答:您的浏览器由于其他原因不能安装招商银行登录控件, 请下载并安装招商银行登录控件下载版。 问:无法出现个人网上银行大众版登录界面。 答:这种情况是由于您的机器无法和我行服务器建立安全连接,通常是因为代理服务器设置错误引起。如果您是拨号上网,请不要使用代理服务器;如果您过去安装过我行SSL安全代理,请调用“添加-删除程序”删除SSL安全代理;如果您是经过代理访问Internet,请联系您所在网的网络管理员设置代理服务器。IE5.0浏览器设置代理服务器的步骤: Internet选项—>连接—>局域网设置—>使用代理服务器—>高级。 问:我在输入账号和卡号时,总出错,该怎样输? 答:存折账号为10位,按存折本上的账号输入, 密码为6位。如果一卡通是12位卡号的,只需输入地区码后面的8位卡号,不需要输入前面4位的地区码,密码为6位。如果一卡通是16位卡号的,请将16位卡号全部输入,密码为6位。 问:我的存折没有设密码,怎样在个人网上银行大众版中查询余额? 答:存折必须设有密码方可在 个人网上银行大众版 中查询,因此请您到存折开户行给您的存折设置密码。 注:网上个人银行是招商银行为个人客户提供的网上银行。 本页面内容仅供参考,部分业务以当地网点的公告与具体规定为准。

我们需要将这段对话中的问题和答案对提取出来,即提取出如下内容:

Q:我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件? A:在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。 Q:为什么我看到的卡号输入框显示为*符号? A:您的浏览器禁止下载执行ActiveX控件 , 对于这种情况 , 您必须打开浏览器的ActiveX的相关权限。 操作方法:在浏览器菜单中选择“工具”|“Internet选项”,在弹出的对话框中选择”安全” |”Internet”|”自定义级别”,在弹出的对话框中选择”重置为 安全级-中” , 点”重置”按钮,确定。 …

如果要用 Python 实现的话,那么我们很可能自然而然想到 split() 或 findall() 方法,如果用 split() 方法,我们可能会这么写:

1
2
3
4
import re
results = re.split('问:| 答:', text)
for index, result in enumerate(results[1:]):
print(('Q' if index%2 == 0 else 'A') + ': ' + result)

这里 split() 方法的第一个参数传入了 问:| 答: 这个正则表达式,意思是将这段话用 问: 或者 答: 分开,这个功能是正则表达式对字符串进行分割的方法,相比直接字符串的 split() 方法功能更为强大。这里其实得到的结果是一个列表,长度是一个奇数,如果我们把 results 打印出来,结果是这样的:

1
['', '我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件?', '在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。 ', '为什么我看到的卡号输入框显示为*符号?', '您的浏览器禁止下载执行ActiveX控件 , 对于这种情况 , 您必须打开浏览器的ActiveX的相关权限。 操作方法:在浏览器菜单中选择“工具”|“Internet选项”,在弹出的对话框中选择"安全" |"Internet"|"自定义级别",在弹出的对话框中选择"重置为 安全级-中" , 点"重置"按钮,确定。 ', '看了以上几个问题,还是不能登录,怎么办?', '您的浏览器由于其他原因不能安装招商银行登录控件, 请下载并安装招商银行登录控件下载版。 ', '无法出现个人网上银行大众版登录界面。', '这种情况是由于您的机器无法和我行服务器建立安全连接,通常是因为代理服务器设置错误引起。如果您是拨号上网,请不要使用代理服务器;如果您过去安装过我行SSL安全代理,请调用“添加-删除程序”删除SSL安全代理;如果您是经过代理访问Internet,请联系您所在网的网络管理员设置代理服务器。IE5.0浏览器设置代理服务器的步骤: Internet选项-->连接-->局域网设置-->使用代理服务器-->高级。 ', '我在输入账号和卡号时,总出错,该怎样输?', '存折账号为10位,按存折本上的账号输入, 密码为6位。如果一卡通是12位卡号的,只需输入地区码后面的8位卡号,不需要输入前面4位的地区码,密码为6位。如果一卡通是16位卡号的,请将16位卡号全部输入,密码为6位。 ', '我的存折没有设密码,怎样在个人网上银行大众版中查询余额?', '存折必须设有密码方可在 个人网上银行大众版 中查询,因此请您到存折开户行给您的存折设置密码。 注:网上个人银行是招商银行为个人客户提供的网上银行。 本页面内容仅供参考,部分业务以当地网点的公告与具体规定为准。 ']

这是因为我们分割使用的字符本身就处于整个文本的字符,所以一上来就找到了分割的标志 问:,所以它左侧的结果就是空字符串了,所以最终得到的结果第一个内容就是空字符串,后续的内容便是正常的一问一答的短句。所以这里我们还需要对结果进行切片操作,去除第一个元素,然后将其遍历打印输出,最终结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
Q: 我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件?
A: 在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。
Q: 为什么我看到的卡号输入框显示为*符号?
A: 您的浏览器禁止下载执行ActiveX控件 , 对于这种情况 , 您必须打开浏览器的ActiveX的相关权限。 操作方法:在浏览器菜单中选择“工具”|“Internet选项”,在弹出的对话框中选择"安全" |"Internet"|"自定义级别",在弹出的对话框中选择"重置为 安全级-中" , 点"重置"按钮,确定。
Q: 看了以上几个问题,还是不能登录,怎么办?
A: 您的浏览器由于其他原因不能安装招商银行登录控件, 请下载并安装招商银行登录控件下载版。
Q: 无法出现个人网上银行大众版登录界面。
A: 这种情况是由于您的机器无法和我行服务器建立安全连接,通常是因为代理服务器设置错误引起。如果您是拨号上网,请不要使用代理服务器;如果您过去安装过我行SSL安全代理,请调用“添加-删除程序”删除SSL安全代理;如果您是经过代理访问Internet,请联系您所在网的网络管理员设置代理服务器。IE5.0浏览器设置代理服务器的步骤: Internet选项-->连接-->局域网设置-->使用代理服务器-->高级。
Q: 我在输入账号和卡号时,总出错,该怎样输?
A: 存折账号为10位,按存折本上的账号输入, 密码为6位。如果一卡通是12位卡号的,只需输入地区码后面的8位卡号,不需要输入前面4位的地区码,密码为6位。如果一卡通是16位卡号的,请将16位卡号全部输入,密码为6位。
Q: 我的存折没有设密码,怎样在个人网上银行大众版中查询余额?
A: 存折必须设有密码方可在 个人网上银行大众版 中查询,因此请您到存折开户行给您的存折设置密码。 注:网上个人银行是招商银行为个人客户提供的网上银行。 本页面内容仅供参考,部分业务以当地网点的公告与具体规定为准。

这样确实没问题,我们可以顺利地提取出来,但是总感觉这个解法并不那么优雅,因为我们这里是将问题和答案的内容都单独切出来了,并没有将问答对一块提取,而且 split() 方法返回的结果的第一个元素还不是我们想要的结果,所以还需要进行一些切片操作来去除,所以整个写法感觉实现起来并不完美。 所以我们又想到了 findall() 方法,这时我们会这么写:

1
2
3
4
import re
results = re.findall('问:(.*?) 答:(.*?)', text, re.S)
for result in results:
print('Q: ' + result[0], 'A: ' + result[1], sep='\n')

表面上看似乎是把问题答案对用正则表示出来了,而且使用了非贪婪匹配,但是很明显,在末尾我们并没有指定匹配的终点,所以整个的结果就会导致回答是完全匹配不到的,运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
Q: 我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件?
A:
Q: 为什么我看到的卡号输入框显示为*符号?
A:
Q: 看了以上几个问题,还是不能登录,怎么办?
A:
Q: 无法出现个人网上银行大众版登录界面。
A:
Q: 我在输入账号和卡号时,总出错,该怎样输?
A:
Q: 我的存折没有设密码,怎样在个人网上银行大众版中查询余额?
A:

好,那么我们加上匹配的终点吧,以下一个的 问: 作为我们正则表达式匹配的终点总可以了吧?所以我们可能会改写成这样子:

1
2
3
4
import re
results = re.findall('问:(.*?) 答:(.*?)问:', text, re.S)
for result in results:
print('Q: ' + result[0], 'A: ' + result[1], sep='\n')

这样写似乎看起来是可以了,但结果却是这样的:

1
2
3
4
5
6
Q: 我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件?
A: 在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。
Q: 看了以上几个问题,还是不能登录,怎么办?
A: 您的浏览器由于其他原因不能安装招商银行登录控件, 请下载并安装招商银行登录控件下载版。
Q: 我在输入账号和卡号时,总出错,该怎样输?
A: 存折账号为10位,按存折本上的账号输入, 密码为6位。如果一卡通是12位卡号的,只需输入地区码后面的8位卡号,不需要输入前面4位的地区码,密码为6位。如果一卡通是16位卡号的,请将16位卡号全部输入,密码为6位。

结果只剩三个问题答案对了,有三个问答对被“吃”掉了,其实这是因为我们的正则表达式最后加了 问:的缘故,findall() 方法它会查找所有符合正则表达式的结果,但其中匹配的时候它内部也是有一个查找索引在扫描的。在查找第一个符合要求的结果时,由于我们是根据正则表达式结尾的 问:来作为结束标志,所以在找到第一个符合要求的结果时,我们的查找索引就已经移动到了第二个问答对开头的 问: 上面,即查找索引就已经进入到了第二个问答对的位置了,而在下一次查找符合要求的结果时,索引会继续往后移动进行扫描,所以它是从第二个问答对的 问: 后面继续扫描的,所以对于第二个问答对,实际上已经被割裂了,所以它只能查找到第三个问答对的时候才可以发现符合正则表达式的内容。因此,我们可以观察到,返回的结果只是第一、三、五三个问答对。 所以,如果我们想要用该方法找到完整的留个问答对,就需要用到零宽断言了。 解法如下:

1
2
3
4
import re
results = re.findall('问:(.*?) 答:(.*?)(?=问:|\Z)', text, re.S)
for result in results:
print('Q: ' + result[0], 'A: ' + result[1], sep='\n')

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
Q: 我用的是Windows XP+Service Pack 2,为什么无法安装输入卡号和密码的控件?
A: 在Windows XP+Service Pack 2、Windows 2003等操作系统中,用户可以自己选择是否安装控件。
Q: 为什么我看到的卡号输入框显示为*符号?
A: 您的浏览器禁止下载执行ActiveX控件 , 对于这种情况 , 您必须打开浏览器的ActiveX的相关权限。 操作方法:在浏览器菜单中选择“工具”|“Internet选项”,在弹出的对话框中选择"安全" |"Internet"|"自定义级别",在弹出的对话框中选择"重置为 安全级-中" , 点"重置"按钮,确定。
Q: 看了以上几个问题,还是不能登录,怎么办?
A: 您的浏览器由于其他原因不能安装招商银行登录控件, 请下载并安装招商银行登录控件下载版。
Q: 无法出现个人网上银行大众版登录界面。
A: 这种情况是由于您的机器无法和我行服务器建立安全连接,通常是因为代理服务器设置错误引起。如果您是拨号上网,请不要使用代理服务器;如果您过去安装过我行SSL安全代理,请调用“添加-删除程序”删除SSL安全代理;如果您是经过代理访问Internet,请联系您所在网的网络管理员设置代理服务器。IE5.0浏览器设置代理服务器的步骤: Internet选项-->连接-->局域网设置-->使用代理服务器-->高级。
Q: 我在输入账号和卡号时,总出错,该怎样输?
A: 存折账号为10位,按存折本上的账号输入, 密码为6位。如果一卡通是12位卡号的,只需输入地区码后面的8位卡号,不需要输入前面4位的地区码,密码为6位。如果一卡通是16位卡号的,请将16位卡号全部输入,密码为6位。
Q: 我的存折没有设密码,怎样在个人网上银行大众版中查询余额?
A: 存折必须设有密码方可在 个人网上银行大众版 中查询,因此请您到存折开户行给您的存折设置密码。 注:网上个人银行是招商银行为个人客户提供的网上银行。 本页面内容仅供参考,部分业务以当地网点的公告与具体规定为准。

这里我们实际上是使用了 (?=)这样的形式来构建了整个表达式,等号后面的内容是 问:或者结束符 \\Z,这样其实就保证了在匹配的时候,查找索引不会继续向后移,但这也同时标志了结束标志,因此它就可以查找到完整的内容了。

零宽断言

零宽断言,顾名思义,是一种零宽度的匹配,它匹配的内容不会保存到匹配结果中,表达式的匹配内容只是代表了一个位置而已,如标明某个字符的右边界是怎样的构造。 在前面我们使用了 ?=来进行了实例讲解,这是其中一个用法,另外还有 ?<=?!?<!,下面我们来依次进行讲解说明。

  • ?=代表零宽度正预测先行断言,它断言自身出现的位置的后面可以匹配后面跟的表达式。
  • ?<=代表零宽度正回顾后发断言,它断言自身出现的位置的前面可以匹配后面跟的表达式。
  • ?!代表零宽度负预测先行断言,它断言自身出现的位置的后面不可以匹配后面跟的表达式。
  • ?<!代表零宽度负回顾后发断言,它断言自身出现的位置的后面不可以匹配后面跟的表达式。

?=

首先我们来看下 ?=的用法,它断言自身出现的位置的后面可以匹配后面跟的表达式。 比如我们这里有这样的一个字符串:

1
str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'

在这里我们想把我的个人邮箱这句话和个人邮箱单独摘出来,假如我们不使用零宽断言的话,我们需要给个人邮箱后面这一句加一个结束标识符或者单独匹配邮箱作为标识符,我们可能会这么写:

1
2
3
4
import re
str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
result = re.search('我的个人邮箱是(.*?),个人博客', str)
print('整句结果:' + result.group(), '第一个匹配结果:' + result.group(1), sep='\n')

在正则表达式的最后我们加了,个人博客作为匹配的结束符,然后邮箱部分用非贪婪匹配的模式进行匹配,我们看下运行结果:

1
2
整句结果:我的个人邮箱是cqc@cuiqingcai.com,个人博客
第一个匹配结果:cqc@cuiqingcai.com

我们可以看到第一个匹配结果成功得到了邮箱信息,但是我们看整句结果缺并不理想,它多匹配了我们加入的结尾标识,并没有得到正常的一句话。 这时候如果我们改用 ?=来匹配,结果就不会带有此标识符了,改写如下:

1
2
3
4
import re
str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
result = re.search('我的个人邮箱是(.*?)(?=,个人博客)', str)
print('整句结果:' + result.group(), '第一个匹配结果:' + result.group(1), sep='\n')

在这里我们将结尾标识符改成了 (?=,个人博客) ,这样就将此部分内容作为零宽度匹配,它代表后面需要跟 ,个人博客,但是它不会出现在匹配结果中。 运行结果如下:

1
2
整句结果:我的个人邮箱是cqc@cuiqingcai.com
第一个匹配结果:cqc@cuiqingcai.com

可以看到整句结果中已经没有无用的后缀字符了。

?<=

接下来我们再看下 ?<=的用法,它代表零宽度正回顾后发断言,其实就是匹配前面的标识,比如这里我们还是以上面的例子为例,匹配出个人博客这句话,代码如下:

1
2
3
4
import re
str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
result = re.search('(?<=,)个人博客是(.*?)(?=,)', str)
print('整句结果:' + result.group(), '第一个匹配结果:' + result.group(1), sep='\n')

这里我们在个人博客 前面加了一个零宽断言的逗号符号作为开头,使用的就是 ?<=,句子结尾是用的 ?=,这样前后的标识都不会匹配到了,运行结果如下:

1
2
整句结果:个人博客是cuiqingcai.com
第一个匹配结果:cuiqingcai.com

可以看到得到的整句结果也是完整的一句话。

?!

?!代表代表零宽度负预测先行断言,它断言自身出现的位置的后面不可以匹配后面跟的表达式。也是用来匹配后面的文本,但这里是取反,它指定了后面出现的内容不匹配该标识,我们在前面的例子基础上修改如下:

1
2
3
4
import re
str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
result = re.search('我的个人邮箱是(.*?)(?!,个人公众号)(?=,个人博客)', str)
print('整句结果:' + result.group(), '第一个匹配结果:' + result.group(1), sep='\n')

本来是 (?=,个人博客)的标识符,不过这里我们使用 ?!来指定了另一个标识符,个人公众号,这就代表这句话后面跟的需要是(?=,个人博客)而不是,个人公众号,运行结果如下:

1
2
整句结果:我的个人邮箱是cqc@cuiqingcai.com
第一个匹配结果:cqc@cuiqingcai.com

?<!

?<!代表零宽度负回顾后发断言,它断言自身出现的位置的后面不可以匹配后面跟的表达式。我们在前面的例子基础上加以修改:

1
2
3
4
import re
str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
result = re.search('(?<=,)(?<!。)个人博客是(.*?)(?=,)', str)
print('整句结果:' + result.group(), '第一个匹配结果:' + result.group(1), sep='\n')

这里我们写了 ?<!标识符,后面跟了一个句号,这代表前面不应该出现句号。 运行结果如下:

1
2
整句结果:个人博客是cuiqingcai.com
第一个匹配结果:cuiqingcai.com

常用用法

其实上面的示例中我们使用了 search() 方法进行了内容匹配,其实这并不常用,因为一般我们更关注的是匹配分组结果的内容,其实更多的用法是用在了 findall() 方法上,它用来匹配多个结果,也就类似于我们一开始的实例一样,这里我们还是以刚才的字符串为例,来输出一下个人邮箱、个人博客、个人公众号三个内容,代码如下:

1
2
3
4
5
import re
str = '我的个人邮箱是cqc@cuiqingcai.com,个人博客是cuiqingcai.com,个人公众号是进击的Coder'
results = re.findall('个人(.*?)是(.*?)(?=,|\Z)', str)
for result in results:
print(result[0] + ': ' + result[1])

这里我们匹配了个人二字,然后后面跟了非贪婪匹配,然后加了一个字,最关键的是结尾标识符,这里必须要使用零宽断言才可以匹配出三个结果,这里匹配的内容是 ,|\\Z,意思是匹配逗号或结束符。 运行结果如下:

1
2
3
邮箱: cqc@cuiqingcai.com
博客: cuiqingcai.com
公众号: 进击的Coder

这样我们就成功输出了邮箱、博客及公众号的内容了,匹配非常顺利方便。

结语

通过本节,我们应该大体可以了解了正则表达式中零宽断言的基本用法和适用场景,相信理解了零宽断言之后,我们再做正则匹配时会更加得心应手。

Python

什么是(监督式)机器学习?简单来说,它的定义如下:

  • 机器学习系统通过学习如何组合输入信息来对从未见过的数据做出有用的预测。

下面我们来了解一下机器学习的基本术语。

标签

在简单线性回归中,标签是我们要预测的事物,即 y 变量。标签可以是小麦未来的价格、图片中显示的动物品种、音频剪辑的含义或任何事物。

特征

在简单线性回归中,特征是输入变量,即 x 变量。简单的机器学习项目可能会使用单个特征,而比较复杂的机器学习项目可能会使用数百万个特征,按如下方式指定:

{x1,x2,…xN}

在垃圾邮件检测器示例中,特征可能包括:

  • 电子邮件文本中的字词
  • 发件人的地址
  • 发送电子邮件的时段
  • 电子邮件中包含“一种奇怪的把戏”这样的短语。

样本

样本是指数据的特定实例:x。(我们采用粗体 x 表示它是一个矢量。)我们将样本分为以下两类:

  • 有标签样本
  • 无标签样本

有标签样本同时包含特征和标签。即:

1
  labeled examples: {features, label}: (x, y)

我们使用有标签样本来训练模型。在我们的垃圾邮件检测器示例中,有标签样本是用户明确标记为“垃圾邮件”或“非垃圾邮件”的各个电子邮件。 例如,下表显示了从包含加利福尼亚州房价信息的数据集中抽取的 5 个有标签样本:

housingMedianAge (特征)

totalRooms (特征)

totalBedrooms (特征)

medianHouseValue (标签)

15

5612

1283

66900

19

7650

1901

80100

17

720

174

85700

14

1501

337

73400

20

1454

326

65500

无标签样本包含特征,但不包含标签。即:

1
  unlabeled examples: {features, ?}: (x, ?)

在使用有标签样本训练了我们的模型之后,我们会使用该模型来预测无标签样本的标签。在垃圾邮件检测器示例中,无标签样本是用户尚未添加标签的新电子邮件。

模型

模型定义了特征与标签之间的关系。例如,垃圾邮件检测模型可能会将某些特征与“垃圾邮件”紧密联系起来。我们来重点介绍一下模型生命周期的两个阶段:

  • 训练表示创建或学习模型。也就是说,您向模型展示有标签样本,让模型逐渐学习特征与标签之间的关系。
  • 推断表示将训练后的模型应用于无标签样本。也就是说,您使用训练后的模型来做出有用的预测 (y’)。例如,在推断期间,您可以针对新的无标签样本预测 medianHouseValue。

回归与分类

回归模型可预测连续值。例如,回归模型做出的预测可回答如下问题:

  • 加利福尼亚州一栋房产的价值是多少?
  • 用户点击此广告的概率是多少?

分类模型可预测离散值。例如,分类模型做出的预测可回答如下问题:

  • 某个指定电子邮件是垃圾邮件还是非垃圾邮件?
  • 这是一张狗、猫还是仓鼠图片?

原文地址

本节原文地址:问题构建 (Framing):机器学习主要术语

Paper

我们知道,Seq2Seq 现在已经成为了机器翻译、对话聊天、文本摘要等工作的重要模型,真正提出 Seq2Seq 的文章是《Sequence to Sequence Learning with Neural Networks》,但本篇《Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation》比前者更早使用了 Seq2Seq 模型来解决机器翻译的问题,本文是该篇论文的概述。

发布信息

Learning Phrase Representations using RNN Encoder–Decoder for Statistical Machine Translation 2014 年 6 月发布于 Arxiv 上,一作是 Kyunghyun Cho,当时来自蒙特利尔大学,现在在纽约大学任教。

摘要

这篇论文中提出了一种新的模型,叫做 RNN Encoder-Decoder, 并将它用来进行机器翻译和比较不同语言的短语/词组之间的语义近似程度。这个模型由两个 RNN 组成,其中 Encoder 用来将输入的序列表示成一个固定长度的向量,Decoder 则使用这个向量重建出目标序列,另外该论文提出了 GRU 的基本结构,为后来的研究奠定了基础。 因此本文的主要贡献是:

  • 提出了一种类似于 LSTM 的 GRU 结构,并且具有比 LSTM 更少的参数,更不容易过拟合。
  • 较早地将 Seq2Seq 应用在了机器翻译领域,并且取得了不错的效果。

模型

本文提出的模型结构如下图所示:

这里首先对输入上文 x 走一遍 RNN,然后得到一个固定长度的向量 c,作为 Encoder,然后接下来再根据 c 和后续隐状态和输入状态来得到后续状态,Encoder 的行为比较简单,重点在 Decoder 上。

Decoder 中 t 时刻的内部状态的 ht 为:

该时刻的输出概率则为: 模型训练时则去最大化给定输入序列 x 时输出序列为 y 的条件概率: 以上便是核心的公式,上面的这个就是该模型的优化目标。 在机器翻译上,作者用 Moses (一个 SMT 系统) 建立了一个 phrase based 的翻译模型作为 baseline system ,然后对比了以下四个模型的 BLEU 值

  1. Baseline configuration
  2. Baseline + RNN
  3. Baseline + CSLM + RNN
  4. Baseline + CSLM + RNN + Word penalty

四种不同的模型的 BLEU 值如下表所示:

phrase pair 打分的结果如下: 其中第一栏是输入的英语 phrase ,第二栏是用传统的模型得到的最近似的三个法语 phrase,第三栏是用 Encoder-Decoder 模型得到的最近似的三个 phrase。 另外作者还说明该模型可以学习到一个比较好的效果的 Word Embedding 结果,附图如下: 左上角的图代表全局的 Embedding 结果,另外三个图是局部结果,可以看到类似的名词都被聚到了一起。

GRU

另外本文的一个主要贡献是提出了 GRU 的门结构,相比 LSTM 更加简洁,而且效果不输 LSTM,关于它的详细公式推导,可以直接参考论文的最后的附录部分,有详细的介绍,在此截图如下:

结语

本篇论文算是为 Seq2Seq 的研究开了先河,而且其提出的 GRU 结构也为后来的研究做好了铺垫,得到了广泛应用,非常值得一读。

参考

Python

TensorFlow 中的 layers 模块提供用于深度学习的更高层次封装的 API,利用它我们可以轻松地构建模型,这一节我们就来看下这个模块的 API 的具体用法。

概览

layers 模块的路径写法为 tf.layers,这个模块定义在 tensorflow/python/layers/layers.py,其官方文档地址为:https://www.tensorflow.org/api_docs/python/tf/layers,TensorFlow 版本为 1.5。 这里面提供了多个类和方法以供使用,下面我们分别予以介绍。

方法

tf.layers 模块提供的方法有:

  • Input(…): 用于实例化一个输入 Tensor,作为神经网络的输入。
  • average_pooling1d(…): 一维平均池化层
  • average_pooling2d(…): 二维平均池化层
  • average_pooling3d(…): 三维平均池化层
  • batch_normalization(…): 批量标准化层
  • conv1d(…): 一维卷积层
  • conv2d(…): 二维卷积层
  • conv2d_transpose(…): 二维反卷积层
  • conv3d(…): 三维卷积层
  • conv3d_transpose(…): 三维反卷积层
  • dense(…): 全连接层
  • dropout(…): Dropout层
  • flatten(…): Flatten层,即把一个 Tensor 展平
  • max_pooling1d(…): 一维最大池化层
  • max_pooling2d(…): 二维最大池化层
  • max_pooling3d(…): 三维最大池化层
  • separable_conv2d(…): 二维深度可分离卷积层

Input

tf.layers.Input() 这个方法是用于输入数据的方法,其实类似于 tf.placeholder,相当于一个占位符的作用,当然也可以通过传入 tensor 参数来进行赋值。

1
2
3
4
5
6
7
8
Input(
shape=None,
batch_size=None,
name=None,
dtype=tf.float32,
sparse=False,
tensor=None
)

参数说明如下:

  • shape:可选,默认 None,是一个数字组成的元组或列表,但是这个 shape 比较特殊,它不包含 batch_size,比如传入的 shape 为 [32],那么它会将 shape 转化为 [?, 32],这里一定需要注意。
  • batch_size:可选,默认 None,代表输入数据的 batch size,可以是数字或者 None。
  • name:可选,默认 None,输入层的名称。
  • dtype:可选,默认 tf.float32,元素的类型。
  • sparse:可选,默认 False,指定是否以稀疏矩阵的形式来创建 placeholder。
  • tensor:可选,默认 None,如果指定,那么创建的内容便不再是一个 placeholder,会用此 Tensor 初始化。

返回值: 返回一个包含历史 Meta Data 的 Tensor。 我们用一个实例来感受一下:

1
2
3
4
x = tf.layers.Input(shape=[32])
print(x)
y = tf.layers.dense(x, 16, activation=tf.nn.softmax)
print(y)

首先我们用 Input() 方法初始化了一个 placeholder,这时我们没有传入 tensor 参数,然后调用了 dense() 方法构建了一个全连接网络,激活函数使用 softmax,然后将二者输出,结果如下:

1
2
Tensor("input_layer_1:0", shape=(?, 32), dtype=float32)
Tensor("dense/Softmax:0", shape=(?, 16), dtype=float32)

这时我们发现,shape 它给我们做了转化,本来是 [32],结果它给转化成了 [?, 32],即第一维代表 batch_size,所以我们需要注意,在调用此方法的时候不需要去关心 batch_size 这一维。 如果我们在初始化的时候传入一个已有 Tensor,例如:

1
2
3
data = tf.constant([1, 2, 3])
x = tf.layers.Input(tensor=data)
print(x)

结果如下:

1
Tensor("Const:0", shape=(3,), dtype=int32)

可以看到它可以自动计算出其 shape 和 dtype。

batch_normalization

此方法是批量标准化的方法,经过处理之后可以加速训练速度,其定义在 tensorflow/python/layers/normalization.py,论文可以参考:http://arxiv.org/abs/1502.03167 “Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift”。

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
batch_normalization(
inputs,
axis=-1,
momentum=0.99,
epsilon=0.001,
center=True,
scale=True,
beta_initializer=tf.zeros_initializer(),
gamma_initializer=tf.ones_initializer(),
moving_mean_initializer=tf.zeros_initializer(),
moving_variance_initializer=tf.ones_initializer(),
beta_regularizer=None,
gamma_regularizer=None,
beta_constraint=None,
gamma_constraint=None,
training=False,
trainable=True,
name=None,
reuse=None,
renorm=False,
renorm_clipping=None,
renorm_momentum=0.99,
fused=None,
virtual_batch_size=None,
adjustment=None
)

参数说明如下:

  • inputs:必需,即输入数据。
  • axis:可选,默认 -1,即进行标注化操作时操作数据的哪个维度。
  • momentum:可选,默认 0.99,即动态均值的动量。
  • epsilon:可选,默认 0.01,大于0的小浮点数,用于防止除0错误。
  • center:可选,默认 True,若设为True,将会将 beta 作为偏置加上去,否则忽略参数 beta
  • scale:可选,默认 True,若设为True,则会乘以gamma,否则不使用gamma。当下一层是线性的时,可以设False,因为scaling的操作将被下一层执行。
  • beta_initializer:可选,默认 zeros_initializer,即 beta 权重的初始方法。
  • gamma_initializer:可选,默认 ones_initializer,即 gamma 的初始化方法。
  • moving_mean_initializer:可选,默认 zeros_initializer,即动态均值的初始化方法。
  • moving_variance_initializer:可选,默认 ones_initializer,即动态方差的初始化方法。
  • beta_regularizer: 可选,默认None,beta 的正则化方法。
  • gamma_regularizer: 可选,默认None,gamma 的正则化方法。
  • beta_constraint: 可选,默认None,加在 beta 上的约束项。
  • gamma_constraint: 可选,默认None,加在 gamma 上的约束项。
  • training:可选,默认 False,返回结果是 training 模式。
  • trainable:可选,默认为 True,布尔类型,如果为 True,则将变量添加 GraphKeys.TRAINABLE_VARIABLES 中。
  • name:可选,默认 None,层名称。
  • reuse:可选,默认 None,根据层名判断是否重复利用。
  • renorm:可选,默认 False,是否要用 Batch Renormalization (https://arxiv.org/abs/1702.03275)
  • renorm_clipping:可选,默认 None,是否要用 rmax、rmin、dmax 来 scalar Tensor。
  • renorm_momentum,可选,默认 0.99,用来更新动态均值和标准差的 Momentum 值。
  • fused,可选,默认 None,是否使用一个更快的、融合的实现方法。
  • virtual_batch_size,可选,默认 None,是一个 int 数字,指定一个虚拟 batch size。
  • adjustment,可选,默认 None,对标准化后的结果进行适当调整的方法。

最后的一些参数说明不够详尽,更详细的用法参考:https://www.tensorflow.org/api_docs/python/tf/layers/batch_normalization。 其用法很简单,在输入数据后面加一层 batch_normalization() 即可:

1
2
3
x = tf.layers.Input(shape=[32])
x = tf.layers.batch_normalization(x)
y = tf.layers.dense(x, 20)

dense

dense,即全连接网络,layers 模块提供了一个 dense() 方法来实现此操作,定义在 tensorflow/python/layers/core.py 中,下面我们来说明一下它的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dense(
inputs,
units,
activation=None,
use_bias=True,
kernel_initializer=None,
bias_initializer=tf.zeros_initializer(),
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None,
trainable=True,
name=None,
reuse=None
)

参数说明如下:

  • inputs:必需,即需要进行操作的输入数据。
  • units:必须,即神经元的数量。
  • activation:可选,默认为 None,如果为 None 则是线性激活。
  • use_bias:可选,默认为 True,是否使用偏置。
  • kernel_initializer:可选,默认为 None,即权重的初始化方法,如果为 None,则使用默认的 Xavier 初始化方法。
  • bias_initializer:可选,默认为零值初始化,即偏置的初始化方法。
  • kernel_regularizer:可选,默认为 None,施加在权重上的正则项。
  • bias_regularizer:可选,默认为 None,施加在偏置上的正则项。
  • activity_regularizer:可选,默认为 None,施加在输出上的正则项。
  • kernel_constraint,可选,默认为 None,施加在权重上的约束项。
  • bias_constraint,可选,默认为 None,施加在偏置上的约束项。
  • trainable:可选,默认为 True,布尔类型,如果为 True,则将变量添加到 GraphKeys.TRAINABLE_VARIABLES 中。
  • name:可选,默认为 None,卷积层的名称。
  • reuse:可选,默认为 None,布尔类型,如果为 True,那么如果 name 相同时,会重复利用。

返回值: 全连接网络处理后的 Tensor。 下面我们用一个实例来感受一下它的用法:

1
2
3
4
5
6
x = tf.layers.Input(shape=[32])
print(x)
y1 = tf.layers.dense(x, 16, activation=tf.nn.relu)
print(y1)
y2 = tf.layers.dense(y1, 5, activation=tf.nn.sigmoid)
print(y2)

首先我们用 Input 定义了 [?, 32] 的输入数据,然后经过第一层全连接网络,此时指定了神经元个数为 16,激活函数为 relu,接着输出结果经过第二层全连接网络,此时指定了神经元个数为 5,激活函数为 sigmoid,最后输出,结果如下:

1
2
3
Tensor("input_layer_1:0", shape=(?, 32), dtype=float32)
Tensor("dense/Relu:0", shape=(?, 16), dtype=float32)
Tensor("dense_2/Sigmoid:0", shape=(?, 5), dtype=float32)

可以看到输出结果的最后一维度就等于神经元的个数,这是非常容易理解的。

convolution

convolution,即卷积,这里提供了多个卷积方法,如 conv1d()、conv2d()、conv3d(),分别代表一维、二维、三维卷积,另外还有 conv2d_transpose()、conv3d_transpose(),分别代表二维和三维反卷积,还有 separable_conv2d() 方法代表二维深度可分离卷积。它们定义在 tensorflow/python/layers/convolutional.py 中,其用法都是类似的,在这里以 conv2d() 方法为例进行说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
conv2d(
inputs,
filters,
kernel_size,
strides=(1, 1),
padding='valid',
data_format='channels_last',
dilation_rate=(1, 1),
activation=None,
use_bias=True,
kernel_initializer=None,
bias_initializer=tf.zeros_initializer(),
kernel_regularizer=None,
bias_regularizer=None,
activity_regularizer=None,
kernel_constraint=None,
bias_constraint=None,
trainable=True,
name=None,
reuse=None
)

参数说明如下:

  • inputs:必需,即需要进行操作的输入数据。
  • filters:必需,是一个数字,代表了输出通道的个数,即 output_channels。
  • kernel_size:必需,卷积核大小,必须是一个数字(高和宽都是此数字)或者长度为 2 的列表(分别代表高、宽)。
  • strides:可选,默认为 (1, 1),卷积步长,必须是一个数字(高和宽都是此数字)或者长度为 2 的列表(分别代表高、宽)。
  • padding:可选,默认为 valid,padding 的模式,有 valid 和 same 两种,大小写不区分。
  • data_format:可选,默认 channels_last,分为 channels_last 和 channels_first 两种模式,代表了输入数据的维度类型,如果是 channels_last,那么输入数据的 shape 为 (batch, height, width, channels),如果是 channels_first,那么输入数据的 shape 为 (batch, channels, height, width)。
  • dilation_rate:可选,默认为 (1, 1),卷积的扩张率,如当扩张率为 2 时,卷积核内部就会有边距,3x3 的卷积核就会变成 5x5。
  • activation:可选,默认为 None,如果为 None 则是线性激活。
  • use_bias:可选,默认为 True,是否使用偏置。
  • kernel_initializer:可选,默认为 None,即权重的初始化方法,如果为 None,则使用默认的 Xavier 初始化方法。
  • bias_initializer:可选,默认为零值初始化,即偏置的初始化方法。
  • kernel_regularizer:可选,默认为 None,施加在权重上的正则项。
  • bias_regularizer:可选,默认为 None,施加在偏置上的正则项。
  • activity_regularizer:可选,默认为 None,施加在输出上的正则项。
  • kernel_constraint,可选,默认为 None,施加在权重上的约束项。
  • bias_constraint,可选,默认为 None,施加在偏置上的约束项。
  • trainable:可选,默认为 True,布尔类型,如果为 True,则将变量添加到 GraphKeys.TRAINABLE_VARIABLES 中。
  • name:可选,默认为 None,卷积层的名称。
  • reuse:可选,默认为 None,布尔类型,如果为 True,那么如果 name 相同时,会重复利用。

返回值: 卷积后的 Tensor。 下面我们用实例感受一下它的用法:

1
2
3
x = tf.layers.Input(shape=[20, 20, 3])
y = tf.layers.conv2d(x, filters=6, kernel_size=2, padding='same')
print(y)

这里我们首先声明了一个 [?, 20, 20, 3] 的输入 x,然后将其传给 conv2d() 方法,filters 设定为 6,即输出通道为 6,kernel_size 为 2,即卷积核大小为 2 x 2,padding 方式设置为 same,那么输出结果的宽高和原来一定是相同的,但是输出通道就变成了 6,结果如下:

1
Tensor("conv2d/BiasAdd:0", shape=(?, 20, 20, 6), dtype=float32)

但如果我们将 padding 方式不传入,使用默认的 valid 模式,代码改写如下:

1
2
3
x = tf.layers.Input(shape=[20, 20, 3])
y = tf.layers.conv2d(x, filters=6, kernel_size=2)
print(y)

结果如下:

1
Tensor("conv2d/BiasAdd:0", shape=(?, 19, 19, 6), dtype=float32)

结果就变成了 [?, 19, 19, 6],这是因为步长默认为 1,卷积核大小为 2 x 2,所以得到的结果的高宽即为 (20 - (2 - 1)) x (20 - (2 - 1)) = 19 x 19。 当然卷积核我们也可以变换大小,传入一个列表形式:

1
2
3
x = tf.layers.Input(shape=[20, 20, 3])
y = tf.layers.conv2d(x, filters=6, kernel_size=[2, 3])
print(y)

这时我们的卷积核大小变成了 2 x 3,即高为 2,宽为 3,结果就变成了 [?, 19, 18, 6],这是因为步长默认为 1,卷积核大小为 2 x 2,所以得到的结果的高宽即为 (20 - (2 - 1)) x (20 - (3 - 1)) = 19 x 18。 如果我们将步长也设置一下,也传入列表形式:

1
2
3
x = tf.layers.Input(shape=[20, 20, 3])
y = tf.layers.conv2d(x, filters=6, kernel_size=[2, 3], strides=[2, 2])
print(y)

这时卷积核大小变成了 2 x 3,步长变成了 2 x 2,所以结果的高宽为 ceil(20 - (2- 1)) / 2 x ceil(20 - (3- 1)) / 2 = 10 x 9,得到的结果即为 [?, 10, 9, 6]。 运行结果如下:

1
Tensor("conv2d_4/BiasAdd:0", shape=(?, 10, 9, 6), dtype=float32)

另外我们还可以传入激活函数,或者禁用 bias 等操作,实例如下:

1
2
3
x = tf.layers.Input(shape=[20, 20, 3])
y = tf.layers.conv2d(x, filters=6, kernel_size=2, activation=tf.nn.relu, use_bias=False)
print(y)

这样我们就将激活函数改成了 relu,同时禁用了 bias,运行结果如下:

1
Tensor("conv2d_5/Relu:0", shape=(?, 19, 19, 6), dtype=float32)

另外还有反卷积操作,反卷积顾名思义即卷积的反向操作,即输入卷积的结果,得到卷积前的结果,其参数用法是完全一样的,例如:

1
2
3
x = tf.layers.Input(shape=[20, 20, 3])
y = tf.layers.conv2d_transpose(x, filters=6, kernel_size=2, strides=2)
print(y)

例如此处输入的图像高宽为 20 x 20,经过卷积核为 2,步长为 2 的反卷积处理,得到的结果高宽就变为了 40 x 40,结果如下:

1
Tensor("conv2d_transpose/BiasAdd:0", shape=(?, 40, 40, 6), dtype=float32)

pooling

pooling,即池化,layers 模块提供了多个池化方法,这几个池化方法都是类似的,包括 max_pooling1d()、max_pooling2d()、max_pooling3d()、average_pooling1d()、average_pooling2d()、average_pooling3d(),分别代表一维二维三维最大和平均池化方法,它们都定义在 tensorflow/python/layers/pooling.py 中,这里以 max_pooling2d() 方法为例进行介绍。

1
2
3
4
5
6
7
8
max_pooling2d(
inputs,
pool_size,
strides,
padding='valid',
data_format='channels_last',
name=None
)

参数说明如下:

  • inputs: 必需,即需要池化的输入对象,必须是 4 维的。
  • pool_size:必需,池化窗口大小,必须是一个数字(高和宽都是此数字)或者长度为 2 的列表(分别代表高、宽)。
  • strides:必需,池化步长,必须是一个数字(高和宽都是此数字)或者长度为 2 的列表(分别代表高、宽)。
  • padding:可选,默认 valid,padding 的方法,valid 或者 same,大小写不区分。
  • data_format:可选,默认 channels_last,分为 channels_last 和 channels_first 两种模式,代表了输入数据的维度类型,如果是 channels_last,那么输入数据的 shape 为 (batch, height, width, channels),如果是 channels_first,那么输入数据的 shape 为 (batch, channels, height, width)。
  • name:可选,默认 None,池化层的名称。

返回值: 经过池化处理后的 Tensor。 下面我们用一个实例来感受一下:

1
2
3
4
5
6
x = tf.layers.Input(shape=[20, 20, 3])
print(x)
y = tf.layers.conv2d(x, filters=6, kernel_size=3, padding='same')
print(y)
p = tf.layers.max_pooling2d(y, pool_size=2, strides=2)
print(p)

在这里我们首先指定了输入 x,shape 为 [20, 20, 3],然后对其进行了卷积计算,然后池化,最后得到池化后的结果。结果如下:

1
2
3
Tensor("input_layer_1:0", shape=(?, 20, 20, 3), dtype=float32)
Tensor("conv2d/BiasAdd:0", shape=(?, 20, 20, 6), dtype=float32)
Tensor("max_pooling2d/MaxPool:0", shape=(?, 10, 10, 6), dtype=float32)

可以看到这里池化窗口用的是 2,步长也是 2,所以原本卷积后 shape 为 [?, 20, 20, 6] 的结果就变成了 [?, 10, 10, 6]。

dropout

dropout 是指在深度学习网络的训练过程中,对于神经网络单元,按照一定的概率将其暂时从网络中丢弃,可以用来防止过拟合,layers 模块中提供了 dropout() 方法来实现这一操作,定义在 tensorflow/python/layers/core.py。下面我们来说明一下它的用法。

1
2
3
4
5
6
7
8
dropout(
inputs,
rate=0.5,
noise_shape=None,
seed=None,
training=False,
name=None
)

参数说明如下:

  • inputs:必须,即输入数据。
  • rate:可选,默认为 0.5,即 dropout rate,如设置为 0.1,则意味着会丢弃 10% 的神经元。
  • noise_shape:可选,默认为 None,int32 类型的一维 Tensor,它代表了 dropout mask 的 shape,dropout mask 会与 inputs 相乘对 inputs 做转换,例如 inputs 的 shape 为 (batch_size, timesteps, features),但我们想要 droput mask 在所有 timesteps 都是相同的,我们可以设置 noise_shape=[batch_size, 1, features]。
  • seed:可选,默认为 None,即产生随机熟的种子值。
  • training:可选,默认为 False,布尔类型,即代表了是否标志位 training 模式。
  • name:可选,默认为 None,dropout 层的名称。

返回: 经过 dropout 层之后的 Tensor。 我们用一个实例来感受一下:

1
2
3
4
5
6
x = tf.layers.Input(shape=[32])
print(x)
y = tf.layers.dense(x, 16, activation=tf.nn.softmax)
print(y)
d = tf.layers.dropout(y, rate=0.2)
print(d)

运行结果:

1
2
3
Tensor("input_layer_1:0", shape=(?, 32), dtype=float32)
Tensor("dense/Softmax:0", shape=(?, 16), dtype=float32)
Tensor("dropout/Identity:0", shape=(?, 16), dtype=float32)

在这里我们使用 dropout() 方法实现了 droput 操作,并制定 dropout rate 为 0.2,最后输出结果的 shape 和原来是一致的。

flatten

flatten() 方法可以对 Tensor 进行展平操作,定义在 tensorflow/python/layers/core.py。

1
2
3
4
flatten(
inputs,
name=None
)

参数说明如下:

  • inputs:必需,即输入数据。
  • name:可选,默认为 None,即该层的名称。

返回结果: 展平后的 Tensor。 下面我们用一个实例来感受一下:

1
2
3
4
x = tf.layers.Input(shape=[5, 6])
print(x)
y = tf.layers.flatten(x)
print(y)

运行结果:

1
2
Tensor("input_layer_1:0", shape=(?, 5, 6), dtype=float32)
Tensor("flatten/Reshape:0", shape=(?, 30), dtype=float32)

这里输入数据的 shape 为 [?, 5, 6],经过 flatten 层之后,就会变成 [?, 30],即将除了第一维的数据维度相乘,对原 Tensor 进行展平。 假如第一维是一个已知的数的话,它依然还是同样的处理,示例如下:

1
2
3
4
x = tf.placeholder(shape=[5, 6, 2], dtype=tf.float32)
print(x)
y = tf.layers.flatten(x)
print(y)

结果如下:

1
2
Tensor("Placeholder:0", shape=(5, 6, 2), dtype=float32)
Tensor("flatten_2/Reshape:0", shape=(5, 12), dtype=float32)

除了如上的方法,其实我们还可以直接使用类来进行操作,实际上看方法的实现就是实例化了其对应的类,下面我们首先说明一下有哪些类可以使用:

  • class AveragePooling1D: 一维平均池化层类
  • class AveragePooling2D: 二维平均池化层类
  • class AveragePooling3D: 三维平均池化层类
  • class BatchNormalization: 批量标准化层类
  • class Conv1D: 一维卷积层类
  • class Conv2D: 二维卷积层类
  • class Conv2DTranspose: 二维反卷积层类
  • class Conv3D: 三维卷积层类
  • class Conv3DTranspose: 三维反卷积层类
  • class Dense: 全连接层类
  • class Dropout: Dropout 层类
  • class Flatten: Flatten 层类
  • class InputSpec: Input 层类
  • class Layer: 基类、父类
  • class MaxPooling1D: 一维最大池化层类
  • class MaxPooling2D: 二维最大池化层类
  • class MaxPooling3D: 三维最大池化层类
  • class SeparableConv2D: 二维深度可分离卷积层类

其实类这些类都和上文介绍的方法是一一对应的,关于它的用法我们可以在方法的源码实现里面找到,下面我们以 Dense 类的用法为例来说明一下这些类的具体调用方法:

1
2
3
4
x = tf.layers.Input(shape=[32])
dense = tf.layers.Dense(16, activation=tf.nn.relu)
y = dense.apply(x)
print(y)

这里我们初始化了一个 Dense 类,它只接受一个必须参数,那就是 units,相比 dense() 方法来说它没有了 inputs,因此这个实例化的类和 inputs 是无关的,这样就相当于创建了一个 16 个神经元的全连接层。 但创建了不调用是没有用的,我们要将这个层构建到网络之中,需要调用它的 apply() 方法,而 apply() 方法就接收 inputs 这个参数,返回计算结果,运行结果如下:

1
Tensor("dense/Relu:0", shape=(?, 16), dtype=float32)

因此我们可以发现,这些类在初始化的时候实际上是比其对应的方法少了 inputs 参数,其他的参数都是完全一致的,另外需要调用 apply() 方法才可以应用该层并将其构建到模型中。 所以其他的类的用法在此就不一一赘述了,初始化的参数可以类比其对应的方法,实例化类之后,调用 apply() 方法,可以达到同样的构建模型的效果。

结语

以上便是 TensorFlow layers 模块的详细用法说明,更加详细的用法可以参考官方文档:https://www.tensorflow.org/api_docs/python/tf/layers。 本节代码地址:https://github.com/AIDeepLearning/TensorFlowLayers