滴滴行程分享 Api
分享一个很久之前写的一个关于滴滴行程的脚本,脚本的主要功能是根据滴滴行程分享的 url 获取订单情况,通过定时轮询还可以获取乘客到目的地的距离和时间
1 | #!/usr/bin/env python |
分享一个很久之前写的一个关于滴滴行程的脚本,脚本的主要功能是根据滴滴行程分享的 url 获取订单情况,通过定时轮询还可以获取乘客到目的地的距离和时间
1 | #!/usr/bin/env python |
要不要换个新的城市开始新的生活?
这是今年一直盘旋在我脑海里的想法。想法到落地中间并不是简单的 1 + 1 的问题,俩者之间隔着巨大的鸿沟,因为这意味着可能一切都需要重新开始。
从零开始并不是想象的那么简单,需要离开从大学开始就一直生活的城市,一个对我来说生活了八年多的地方,离开熟悉的天空、熟悉的街道、熟悉的人和熟悉的工作环境。记得上次有这种想法的时候,是刚毕业一年多的时候,也就是四年前,很遗憾,那个时候一个人的勇气并不能够支撑着我背起行囊潇洒地离开这里。
是的,没错,这次离开,是两个人一起。两个人能够在这种事情上达成一致的想法,是何其幸运的事情。在这里这些年最大的收获,就是收获了爱情,在开始新的阶段的时候,有人与我同行。
勇气是打开新的阶段的钥匙的话,那么找工作就是到达终点前的“拦路虎”了。
由于自己还处于在职状态,所以所有的面试基本上都只能约在下班之后了,自然面试前都准备工作也同样只能放在下班之后了。人生最让人觉得(有趣)无奈的事情就是,你越不想事情多的时候,基本上你的事情就会越来越多。恰巧开始准备复习的时候,负责的项目需要演示汇报,而这个时候郑州又赶上了暴雨加疫情,所谓的“涝疫结核”,一个多月的足不出户的生活,还有爷爷的离开。
因为疫情的原因,有一两年没有回老家了,没想到再次回去就是收到爷爷病重的消息,紧接着就是在暴雨和疫情期间被告知爷爷离开的消息,我记得那天的没有下雨,天比较阴沉,我坐在沙发上接到老爸打来的电话,说的什么我已经记不清了,我只记得挂断电话之后,眼泪不由自主地流了下来。
那个时候,我才切身体会道什么叫做紧绷着的弦,所有的事情都在不停地拉扯着神经。整个人变得有点焦虑,焦虑到夜里睡不着,焦虑到一个多月瘦了6-7斤,焦虑到茶饭不思。很难想象如果这个时候没有我的另一半一直陪着我,我该如何度过这一阶段。
潮起日落,日子总是一天一天在指尖划过。度过最难过的一段时光,后面的面试除了时间上比较紧凑,面试后面还是比较顺利的。工作了五年之后,再次收获了一份满意的 offer,很快就要去新的城市开始新的生活了。你说我现在的心情是什么样的,有向往,也有紧张,向往全新的生活,对新的开始难免有点彷徨和紧张。
最近处于离职的阶段,也算是闲了下来,可以偶尔跑跑步、看看书,前几天晚上还把藏在柜子里深处的尤克里里翻了出来,久违的轻松惬意,希望 2021 年接下来的日子都顺顺利利的。
原文连接: When to use assert
assert
又称为断言,在 Python 代码中经常被使用,但是显然也存在滥用的情况。那么在什么时候使用 assert
呢?又或者 assert
的最佳实践是怎么样的呢?
Python 的 assert
通常用来检查一个条件,如果它是真的,则不做任何事情,如果它是假的,则引发一个 AssertionError,并给出一个可选的错误信息:
1 | py> x = 23 |
许多人喜欢通过 assert
来简单地快速地触发异常,比如下面的代码:
1 | if not isinstance(x, int): |
通过检验参数,并抛出 AssertionError
,实际上,这种做法是错误的,并且还很危险。正确的做法是应该抛出一个 TypeError
。
之所以危险是英文,assert
有一个特性:使用 -O
或 -OO
优化指令去运行 Python 的话,它会被被编译掉,而永远不会被执行。当你能够正确使用 assert
时,这是会是一个 feature,但当使用不当的话,则会导致代码在使用 -O
标志运行时出问题。
所以应该在什么情况下使用 assert 呢
在下面这四种情况下都可以考虑使用 assert
:
对断言的看法各不相同,它可以看作是一种对代码正确性的信心的表现形式吧。如果你确定你的代码毫无问题的话,显然断言(assert
)是没有任何意义的,完全可以把这些断言移除;如果你确定断言可能会被触发,那你也完全可以不用断言(assert
),毕竟它在某些情况下肯定会编译掉,导致你的检查被跳过。
介于这两种情况之间的情况才是有趣的,当你确定代码是正确的,但又不完全确定的时候。
你确定代码是正确的,但又不是绝对确定。有可能漏掉一些场景,在这种情况下,通过断言(assert)进行运行时检查有助于我们尽早的发现错误。
断言的另一个用途是检查程序的不变量。所谓的不变量就是一些你可以依赖的真实条件,除非一个错误导致它变成假的。如果有一个bug,最好能尽早发现所以我们要对它进行测试,但我们不想因为这样的测试而降低代码的速度。但我们不想因为这些测试而使代码变慢。因此我们可以通过可在开发中打开而在生产中关闭断言(assert)。
不变量的一个例子是,如果你的函数预期一个数据库连接是 open,并承诺它在返回时仍然是open,这就是函数的一个不变式。open 就是该函数的一个不变量。
1 | def some_function(arg): |
断言也是很好的检查性评论,而不是写一个评论:
1 | # when we reach here, we know that n > 2 |
断言也是防御性编程的一种形式。你不是在保护现在的代码中的错误,而是在保护以后引入错误的变化。理想情况下,单元测试会发现这些错误,但是实际上,即使存在测试,它们也是往往是不完整的。有可能在几周内都没有人注意到构建机器人,或者有的时候在提交代码前忘记运行测试。有一个内部检查是防止错误潜入的另一道防线,特别是阻止那些会导致代码故障和返回错误结果的静默错误。
假设你有个代码中有 if…elif 代码块,这个时候你是知道这些分支对应了哪些变量,并且了解逻辑是什么:
1 | # target is expected to be one of x, y, or z, and nothing else. |
假设这个代码现在是完全正确的。但它会一直正确吗?需求改变了,代码也会改变。现在要求改变为当 target == w,执行 run_w_code。如果我们改变了设置目标的代码,但忽略了改变这个代码块,它将错误地调用 run_z_code():
1 | target = w |
最好是把这段代码写成防御性的,哪怕之后有变化,它也是要么是正确的,要么立即失败。
写代码块一开始加注释是很好的习惯,但是通常时间久了,我们就没有阅读和更新注释的习惯。有可能注释很快就会被淘汰。但通过一个断言,我们既可以记录这个块的假设,也可以在断言检验没通过的时候直接抛出错误。
1 | assert target in (x, y, z) |
断言既是防御性编程,也是经过检查的文档,下面这个代码会更好一点:
1 | if target == x: |
契约设计是另一种对断言的不错的应用。在按契约设计中,我们认为函数与它们的调用者签订了 “契约”。
例如,”如果你传递给我一个非空的字符串,我保证返回该字符串的第一个的第一个字符转换成大写字母”。
如果这个契约被函数或调用它的代码所破坏,代码就是有问题的。我们说,函数有预设条件(即
参数的约束)和后置条件(返回结果的约束) 因此,这个函数可能被代码为:
1 | def first_upper(astring): |
契约设计的目的是,在一个正确的程序中,前条件和后条件总是成立的。当我们发布无错误的程序并将其投入生产时,我们可以安全地删除这些断言。
AssertionError
。如果他们看到了,这就是一个需要修复的错误。assert
比一个测试和 raise 短就使用它。assert
用于任何你期望恢复的错误。 换句话说,你没有理由在生产代码中捕捉一个AssertionError
异常。Python3.7 引入了一个新的模块那就是 dataclasses
,早在 3.6 版本的时候我就通过安装 dataclasses
三方库体验了一波,那么为什么要用 dataclasses
呢?
一个简单的场景,当你想定义一个对象的属性的时候,比如一本书,通常你会这样
1 | class Book: |
如果在不定义 __repr__
的情况下,初始化这个对象,并输出的话:
1 | >>> book = Book('桃子',10.0,author='桃子') |
显然输出不够友好,对于属性比较少的对象,定义一个 __repr__
并不太麻烦,但是一旦需要定义的对象很多或者属性很多的情况下,这样子做就显得略微麻烦了一点。这个时候 dataclasses
就派上用场了。
我们用 dataclasses
把 Book
改造一下
1 | from dataclasses import dataclass |
然后简单验证一下:
1 | >>> from dataclasses import dataclass,asdict,astuple |
dataclasses
甚至还具备 asdict
函数可以将对象转成 dict
,也存在 astuple
可以将对象转成tupple
,是不是很方便,但是还不够,有时候我们对不同对参数进行一定对校验,很遗憾 dataclasses
并不能做到,这个时候就需要看 attrs
和 pydantic
了。
除此之外,attrs
和 pydantic
还有其他的 dataclasses
不具备的特性,见下表:
attrs 和 pydantic 都需要通过 pip 安装
1 | pip install attrs |
1 | import attr |
运行一下,验证一下结果:
1 | Traceback (most recent call last): |
1 | from pydantic import BaseModel,validator, ValidationError |
同样的验证一下结果:
1 | Traceback (most recent call last): |
除了 validators ,attrs 和 pydantic 还同时具有 converters(转换器) 功能。
转换器也是一个十分实用的功能,所谓的转换器就是在将参数在传递到 __init__
之前,将它按照需求转换成所需格式的数据。
最常见的就是将 str 转成 datetime 格式,attrs 和 pydantic 在转换器的实现上有所区别:
1 | import attr |
运行程序
1 | Book(name='attrs', price=1.0, author='hynek Hynek Schlawack', publish_date=datetime.datetime(2021, 9, 13, 0, 0)) |
不难看出,str 类型的字符串被成功转化了。
1 | from pydantic import BaseModel,validator |
验证结果:
1 | name='attrs' price=1.0 author='hynek Hynek Schlawack' publish_date=datetime.datetime(2021, 9, 13, 16, 3) |
从上面的代码不难看出,attrs 本身内置 converter 参数,可以通过传递内置函数或者自定义函数实现转化器的功能,而 pydantic 则需要借助 validator 去实现,在这一点上 pydantic 还是稍微逊色 attrs。
除了上面提到的两个功能,attrs 和 pydantic 都具备 immutable
的方法用来修饰属性,从而实现属性不可修改。
attrs 还具备 slots
和 programmatic creation
有兴趣的可以进一步阅读官方文档。
从上面的例子,不难看出 pydantic 有下面几个问题:
slots
和 programmatic creation
在参考文章中还提到了 pydantic 对 unions 的策略有问题,不容易定制,并且对定制的(非)结构化的支持很弱。
所以如果有复杂的需求的话,建议使用 attrs
,只是想简单的呈现对象的属性的话,可以考虑用 dataclasses
。
dataclasses vs attrs vs Pydantic
默认情况下 es 是不支持 pdf、doc 等文档的搜索的,但是可以通过安装 Ingest attachment plugin
插件来使得 es 提取通用格式的文件,从而可以实现搜索的功能。
安装很简单,通过 elasticsearch-plugin
可以直接进行安装
1 | bin/elasticsearch-plugin install ingest-attachment |
Ingest attachment plugin 允许 Elasticsearch 通过使用 Apache 文本提取库 Tika 提取通用格式(例如:PPT,XLS 和 PDF)的文件附件。Apache Tika 工具包可从一千多种不同的文件类型中检测并提取元数据和文本。所有这些文件类型都可以通过一个界面进行解析,从而使 Tika 对搜索引擎索引,内容分析,翻译等有用。需要注意的是,源字段必须是 Base64 编码的二进制,如果不想增加在 Base64 之间来回转换的开销,则可以使用 CBOR 格式而不是 JSON,并将字段指定为字节数组而不是字符串表示形式,这样处理器将跳过 Base64 解码。
通过 kibana 的开发工具进行请求
1 | PUT _ingest/pipeline/pdfattachment |
返回结果:
1 | { |
表示创建成功,接下来就是验证上传 pdf 以及搜索功能了。
对于 Ingest attachment plugin 来说,它的数据必须是 Base64 的。这里为了快速创建,我们通过一个 bash 脚本去处理用来测试的 pdf
1 | !/bin/bash |
上传成功会返回如下的结果:
1 | { |
如果出现提示下述提示
1 | { |
说明上传的文件大小超过了 Kibana 默认的上限(默认 1M),修改 kibana 的配置 kibana.yml
1 | server.maxPayloadBytes: "209715200" |
通过下面的命令可以查看 pdf-test1 的索引情况:
1 | GET pdf-test1/_search |
结果如下图
其中 _source
里有个 content
字段,就是 pdf 的内容,结果如下:
1 | { |
其中 file
就是 base64 格式的内容,content
则包含了 pdf 的内容,如果不想要 file
则可以通过 remove processor
去除这个字段
1 | PUT _ingest/pipeline/pdfattachment |
通过在 kibana 的开发工具执行下述命令,验证一下搜索:
1 | GET pdf-test1/_search |
1 | docker network create elastic |
1 | docker pull docker.elastic.co/kibana/kibana:7.14.0 |
启动完成之后访问 http://localhost:5601
即可,默认是英文,配置中文可以参考
在 kibana.yml
中加入
1 | i18n.locale: "zh-CN" |
然后重启 kibana 即可
配置完成后重启服务,界面如下图:
es 中本身自带英文分词,但是实际业务需要存在中文分词的场景,所以手动安装中文分词插件。
从 github 上根据不同的 es 版本下载对应的 ik 插件压缩包
https://github.com/medcl/elasticsearch-analysis-ik/releases
1 | cd your-es-plugins/plugins/ && mkdir ik |
然后重新启动 es,这里我是使用 docker 启动 es 的,所以先通过 docker cp
的命令将插件复制到容器中的
1 | docker cp es.zip container-id:/usr/share/elasticsearch/plugins/es |
重启 es 的服务,如果成功加载插件,控制台会有如下的输出:
Recently I was working on rss3 SDK. In order to facilitate development, I just make a python version of the reference JavaScript SDK, which means the usage should be pretty similar between both.
What’s more, to make a clear code,I use type hinting in the new project. However, there is a code snippet in JavaScript version:
1 | # index.ts |
Yes, the two files refer to each other. Actually, it happens on many other files. When I turned this into Python version:
1 | # index.py |
It didn’t seem any errors, but when I started testing the problems appeared.
1 | E ImportError: cannot import name 'RSS3' from partially initialized module 'rss3.src.index' (most likely due to a circular import) |
how to solve this problem ? Don’t worry, PEP 484 has given a solution.
When a type hint contains names that have not been defined yet, that definition may be expressed as a string literal, to be resolved later.
So we can modify our code like the following:
1 | class File: |
That’s okay already.
Sometimes there’s code that must be seen by a type checker (or other static analysis tools) but should not be executed. For such situations the typing module defines a constant, TYPE_CHECKING, that is considered True during type checking (or other static analysis) but False at runtime.
Modify out code again:
1 | if TYPE_CHECKING: |
if we are using Python 3.7+, we can at least skip having to provide an explicit string annotation by taking advantage of PEP 563:
1 | from __future__ import annotations |
The from __future__ import annotations
import will make all type hints be strings and skip evaluating them.
前段时间用 Flutter 做了一个开源的项目 RSSAid,因为需要打包 apk,在此之前一直是在本地签名打包的。后来和别人交流了一下,想起来可以用 Github Action 构建持续化集成,自动打包。然后就研究了一下,最后完成了根据 tag 版本自动生成 apk 的 workflows。
自动化构建脚本如下:
1 | # main.yml |
脚本中有很多环境变量,都需要实现定义好,在项目的 secrets 中添加上。
这个环境变量需要在 Personal access tokens 申请,需要注意的是,申请完成之后,不要着急关闭这个页面,因为一旦关闭就不能再次查看生成的 token 了,这个 token 需要申请 repo 和 workflow 的权限
生成 token 成功后,找到项目的 **Settings => Secrets **选项,新建名为 RELEASE_TOKEN 的 secrets 然后 value 值为刚才生成的 token。这个完成之后,就需要对设置生成 apk 需要的签名进行变量设置了。
正常 app 签名步骤可以参考 app签名,最终我们会创建一个 key.properties 的文件,文件内容如下:
1 | storePassword=<password from previous step> |
然后在 android/app/build.grade 中配置
1 | signingConfigs { |
但是将 key.properties 上传到仓库显然是不安全的。所以需要对代码进行修改,将对应的变量添加到 secrets 中,从 secrets 中获取变量。
1 | storeFile file(System.getenv("KEYSTORE") ?:"keystore.jks") |
其中 **KEYSTORE_PASSWORD、KEY_ALIAS、KEY_PASSWORD **直接就可以添加,那么 **KEYSTORE **这个变量怎么处理呢?KEYSTORE 对应着 jks 文件位置。jks 文件显然也不可能上传到仓库,所以我们换种方法,在构建的时候生成 jks 文件。
正常情况下打开生成的 jks 文件多半是乱码,所以我们可以通过 base64 对文件进行编码,然后在构建的时候,再解码重新生成文件。
首先获取 base64 格式的 keystore
1 | openssl base64 -A -in <jks.文件位置> |
然后将输出的结果复制下来
将编码后的 keystore 内容,添加到 secrets ,变量名命名为 ENCODED_KEYSTORE,然后在构建过程中就可以将 keystore 文件还原了。
1 | echo $ENCODED_KEYSTORE | base64 -di > android/app/keystore.jks |
主备同步,也叫主从复制,是 mysql 提供的一种高可用的解决方案,保证主备数据一致性的解决方案。
在生产环境中,会有很多不可控因素,比如数据库服务器宕机等,因此在生产环境中,都会采用主备同步。在应用的规模不大的情况下,一般会采用一主一备。除此之外,采用主备同步还可以:
那么主备同步的原理是什么?
主备同步模式之所以能够实现,显然是有一种手段可以,将主的 mysql 服务(以下简称 master)执行的 DDL 和 DML 语句传递给 备份的 mysql 服务(以下简称为 slave),这个就是 MySQL 的 binlog,mysql 的 binlog 是 MySQL 最重要的日志,它记录了所有的DDL和DML(除了数据查询语句)语句,以事件形式记录,还包含语句所执行的消耗的时间,MySQL的二进制日志是事务安全型的。
所以 binlog 一般会有两个用途:
当 master 将 binlog 传递到 slave 的时候,会被传到 slave 的 relay log,relay log 也叫中继日志,是连接 master 和 slave 的核心,relay-log 的结构和 binlog 非常相似,只不过他多了一个 master.info 和 relay-log.info 的文件。
master.info 记录了上一次读取到 master 同步过来的 binlog 的位置,以及连接 master 和启动复制必须的所有信息。
relay-log.info 记录了文件复制的进度,下一个事件从什么位置开始,由 sql 线程负责更新。
说完了原理,接下来说说常见的俩种主备模式以及实践吧。
常见的主备模式有俩种分别是 M-S 结构 和 双 M 结构,本篇文章介绍前者 – M-S 结构
M-S结构,两个节点,一个当主库、一个当备库,不允许两个节点互换角色。
在状态1中,客户端的读写都直接访问节点A,而节点B是A的备库,只是将A的更新都同步过来,到本地执行。这样可以保持节点B和A的数据是相同的。
当需要切换的时候,就切成状态2。这时候客户端读写访问的都是节点B,而节点A是B的备库。
首先修改 master 中的 my.cnf
1 | vi /etc/my.cnf |
在 my.cnf 中 [mysqld] 中添加
1 | [mysqld] |
保存之后,然后重启 MySQL
1 | service restart mysql |
接着以 root 帐号登录 master 的 mysql,创建一个用于同步的帐号,以下建立同步帐号名为 replication,密码为 password, slave 的 ip 是 192.168.1.101
1 | mysql> CREATE USER replication@192.168.1.101; |
到此关于 master 的配置结束了,如果这个时候,要同步的数据库有数据,可以将数据 sql 文件,以便于前期的数据初始化
1 | mysqldump –skip-lock-tables –all-databases –user=root –password –master-data > master.sql |
接下来对 slave 的进行修改,首先修改 my.cnf,将下述内容加到 [mysqld] 区域:
1 | [mysqld] |
然后重启 mysql,重启之后,以 root 用户登录 MySQL, 新建要同步的数据库
1 | service restart mysql |
将 master 的导出的 sql 文件,加载到 slave 数据中,进行数据初始化
1 | mysql -u root -p database-name < master.sql |
在开启 slave 服务之前,还需要进行一些设置
1 | mysql> CHANGE MASTER TO |
每个参数的含义如下:
master_host 为主库IP地址
master_user 为主库用户名
master_password 为主库密码
master_log_file 为主库日志文件
master_log_pos 为主库日志所占位置
其中 MASTER_LOG_FILE 及 MASTER_LOG_POS 是在 Master 上在 MySQL 执行 “SHOW MASTER STATUS;” 的结果。
1 | mysql> START SLAVE; |
如果没有错误信息的话,就说明配置成功了。
scrapy 是 python 中一个优秀的爬虫框架,基于这个框架,用户可以快速构建自己的爬虫程序。框架涉及很多模块,其中有两个核心概念 items 和 item pipelines。
Items: 爬取的主要目标就是从非结构性的数据源提取结构性数据,例如网页。 Scrapy提供
Item
类来满足这样的需求。Item
对象是种简单的容器,保存了爬取到得数据。 其提供了 类似于词典(dictionary-like) 的API以及用于声明可用字段的简单语法。
Item Pipeline:当Item在Spider中被收集之后,它将会被传递到Item Pipeline,一些组件会按照一定的顺序执行对Item的处理。
每个item pipeline组件(有时称之为“Item Pipeline”)是实现了简单方法的Python类。他们接收到Item并通过它执行一些行为,同时也决定此Item是否继续通过pipeline,或是被丢弃而不再进行处理。
``
通常情况下 scrapy 会有一个默认的 Pipeline:
1 | class KnowledgeGraphPipeline: |
正常对于传过来的数据,我们通过 item 对应的类型,可以进行判断然后执行逻辑
1 | if isinstance(item, XXItem): |
一旦当我们的 spider 多了,将所有的处理逻辑放到一个 pipeline 显然是不合理的,所以会希望每个 spider 都有一个专门的 pipeline,这个时候我们可以通过复写 spider 的默认配置就可以实现这样的目的
1 | class XXSpider(scrapy.Spider): |