SQLAlchemy 中处理 relationship

本文主要关注在 SQLAlchemy 中实现基本的关系模型,一对多,多对一,多对多等等。

一对多关系

通常一对多的关系,使用外键,用最常见的用户,地址来举例,每个地址只有一个住户,但是一个住户可以拥有多个地产。所以在 Address 中有一个外键指向 user.id 的主键。而 User 表则是使用一个 relationship 来表示多个地址。

from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    addresses = relationship("Address", backref="user", lazy='dynamic')

class Address(Base):
    __tablename__ = 'address'
    id = Column(Integer, primary_key=True)
    email = Column(String)
    user_id = Column(Integer, ForeignKey('user.id'))

上面这段代码会使得 User 拥有一个 .addresses 的属性,包含一系列的地址,同时,注意这个使用场景下的 backref,这个关键字会使得 Address 实例拥有一个 .user 的指向 User 的对象,可以通过 address.user 来引用。

而事实上,backref 关键字只是为了省略写两个 relationship() 而引入的缩写方式。其实下面的写法和上面是一致的。

from sqlalchemy import Integer, ForeignKey, String, Column
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship

Base = declarative_base()

class User(Base):
    __tablename__ = 'user'
    id = Column(Integer, primary_key=True)
    name = Column(String)

    addresses = relationship("Address", back_populates="user")

class Address(Base):
    __tablename__ = 'address'
    id = Column(Integer, primary_key=True)
    email = Column(String)
    user_id = Column(Integer, ForeignKey('user.id'))

    user = relationship("User", back_populates="addresses")

只是这种写法需要使用两个 relationship,并且使用了 back_populates

以上两种方式建立了两个对象之间的关联,在 SQLAlchemy 中使用时,SQLAlchemy 会自动填充字段。一旦用户增加了 address

u1 = User()
a1 = Address()
u1.addresses.append(a1)

那么此时,a1 中的 a1.user 同样也被填充了 u1 实例。 backref/back_populates 方法使得所有的 SQL 操作对使用者都隐藏了,调用者不需要关注 SQL 的具体实现,只需要关注 Python 对象的逻辑即可。

记住,使用 backref 单向 ref,和使用 back_populates 双向实现,是完全一致的。

relationship 方法使用

relationship,第一个参数是类名,backref 参数是添加一个属性,第一个用户地址的例子中,就是给地址增加一个 user 属性。

relationship 中 lazy 是加载方式,默认是 select,在查询时自动查询所有数据。lazy 属性根据需求决定,如果每次查询 User 都需要获取 Address,那么 select 可以使用。如果两个表互相有外键指向对方,则 relationship 中 lazy 不能为默认值,需要 dynamic 动态加载。

数据库中,一对多关系是最常用的关系类型,它可以把一个记录和一组相关的记录联系在一起,实现这种关系,只要在多一侧加入一个外键,指向一这侧关联的记录。

多对一关系

多对一,其实和一对多本质上是一样的,参考上面,一对多,一个用户可以有多个地址,而多对一其实就是多个地址对应于一个用户。大部分的其他关系都可以从一对多关系中衍生。

一对一关系

在一对一的双向关系中,使用 uselist 来表示,比如“计划生育”下,父母只能有一个小孩,孩子也只能有一对父母,所以使用 uselist=False

class Parent(Base):
    __tablename__ = 'parent'
    id = Column(Integer, primary_key=True)
    child = relationship("Child", uselist=False, back_populates="parent")

class Child(Base):
    __tablename__ = 'child'
    id = Column(Integer, primary_key=True)
    parent_id = Column(Integer, ForeignKey('parent.id'))
    parent = relationship("Parent", back_populates="child")

多对多关系

一对多,多对一关系和一对一关系至少有一侧是单个实体,所以记录之间的联系通过外键实现,让外键指向这个单一实体。但是实现多对多显然不行。以典型的学生选课为例子,学生表和课程表,显然不能在学生表中加入一个指向课程的外键,以为一个学生可以选择多个课程,一个外键不够用,同样,也不能在课程表中加入一个指向学生的外键,因为一个课程会有很多学生选择,两侧都需要一组外键。解决这种问题的方法就是添加第三张表,这个表称为关联表。这样,多对多关系就可以分解为原表和关联表之前的两个一对多关系。

多对多关系会在两个类之间增加一个关联的表,使用 relationship() 方法中的 secondary 参数。

实现多对多主要可以分为三个步骤:

  1. 定义关联表,保存两个表主键
  2. 定义多对多表模型
  3. 给每个模型添加一个访问对方属性

比如:

registrations = db.Table(
    'registrations',
    db.Column('student_id', db.Integer, db.ForeignKey('students.id')),
    db.Column('class_id', db.Integer, db.ForeignKey('classes.id'))
)


class Student(db.Model):
    __tablename__ = 'students'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64))
    classes = db.relationship('Class',
                              secondary=registrations,
                              backref=db.backref('students', lazy='dynamic'),
                              lazy='dynamic')


class Class(db.Model):
    __tablename__ = 'classes'
    id = db.Column(db.Integer, primary_key=True)
    name = db.Column(db.String(64))

或者使用 Association Object

class Association(Base):
    __tablename__ = 'association'
    left_id = Column(Integer, ForeignKey('left.id'), primary_key=True)
    right_id = Column(Integer, ForeignKey('right.id'), primary_key=True)
    extra_data = Column(String(50))
    child = relationship("Child", back_populates="parents")
    parent = relationship("Parent", back_populates="children")

class Parent(Base):
    __tablename__ = 'left'
    id = Column(Integer, primary_key=True)
    children = relationship("Association", back_populates="parent")

class Child(Base):
    __tablename__ = 'right'
    id = Column(Integer, primary_key=True)
    parents = relationship("Association", back_populates="child")

reference


2018-09-11 sqlalchemy , sql , flask , python

邮件服务器

学习邮件服务器

几个概念

MUA

MUA 全称为 Mail User Agent 邮件用户代理。常见的 MUA 实例有: mutt, outlook, foxmail 等, 其主要任务是让用户能够收信,写信,发信。MUA 并非直接将 E-mail 发送到收件人手上,而是通过 MTA 代为传递。

MTA

全称 Mail Transfer Agent,MTA 仅仅负责邮件的传输。Postfix 扮演的角色。

MDA

全称为 Mail Delivery Agent,负责投递本地邮件到适当的邮箱,一封邮件从 MUA 发出后,通过一个或者多个 MTA 最终到达 MDA。MDA 可以过滤邮件内容,依照规则,将邮件分类到适当的邮箱,甚至可以将邮件转回 MTA,以寄到另一个邮箱。一旦邮件到达邮箱,就原地等待用户通过 MUA 将其取走。

SMTP

SMTP 全称 Simple Mail Transfer Protocol ,简单邮件传输协议。主要工作是把邮件信息从发件人邮件服务器中传送到接收人邮件服务器。SMTP 协议出现比较早,所有很多问题都没有考虑全面,比如信息内容需要是 ASCII 码,再比如 SMTP 没有对发送方进行身份验证,所有现在垃圾邮件非常多。

LMTP

全称是 Local Mail Transfer Protocol,本地邮件传输协议,类似 SMTP,主要应用于非广域网的邮件网关。

POP3

全称是 Post Office Protocol Version 3,RFC1939,用于用户管理邮件服务器上面的电子邮件。当外来邮件发送到收件人的邮件服务器上时,收件人可以使用邮件客户端连接邮件服务器,把未阅读的邮件服务器以及部分信息拉取回本地进行处理。

IMAP

IMAP 全称 Internet Message Access Protocol,RFC 2060 相对于 pop3 协议所有邮件的管理都需要下载下来进而管理,IMAP 提供了用户远程访问邮件服务器的途径。

发送一封邮件的基本流程就是:

发件人 -> MUA -> 发送 -> MTA -> MTA … -> MDA – MUA – 收件人收件

MUA 到 MTA,以及 MTA 和 MTA 之间使用 SMTP 协议,而收件时,MUA 到 MDA 之间最常使用的协议是 POP3 或 IMAP。

Postfix

一个开源的 MTA 服务器,负责通过 SMTP 协议管理发送到本机的邮件以及由本机向外发送的邮件。

Postfix 使用的默认端口为 25

Dovecot

一个开源的 IMAP 及 POP3 服务器。doc

IMAP 协议使用的默认端口是 143

SASL

全称是 Simple Authentication and Security Layer,因为 SMTP 协议没有验证用户身份的能力,虽然信封的寄件人地址已经隐含发信者身份,但是由于信封地址实在太容易造假,所以不能当成身份凭据。所以 SMTP 借助额外的机制 SASL 来验证客户端的身份,来决定谁有权使用转发服务。

SSL TLS

SSL 全称是 Secure Socket Layer,加密传输层,TLS 是 Transport Layer Security 在 SSL 基础上提供更好的安全性。25 端口被设计用来转发邮件,并没有考虑认证和加密等问题,1997 年 465 端口被注册用于加密 (SMTPS)提交邮件,1998 年 STARTTLS 标准出现,规定使用 587 端口使用 STARTTLS 方式提交邮件。

邮件服务器发送接受邮件

假设用户 a@gmail.com 发送一封邮件到 b@qq.com,大致会执行流程:

  • gmail.com 服务器会通过 DNS 查询到 qq.com MX 记录,找到服务器 IP 所在
  • 邮件通过 SMTP 协议发送给 qq.com 服务器

Ubuntu 安装 postfix

安装 postfix

sudo apt install postfix

完成之后可以查看版本

sudo postconf mail_version

安装过程中可能出现这几个选项:

  • No configuration 表示不要做任何配置;
  • Internet Site 表示使用本地 SMTP 服务器发送和接收邮件;
  • Internet with smarthost 表示使用本地 SMTP 服务器接收邮件,但发送邮件时不使用本地 SMTP 服务器,而是使用第三方 smart host 来转发邮件;
  • Satellite system 表示邮件的发送和接收都是由第三方 smarthost 来完成。
  • Local only 表示邮件只能在本机用户之间发送和接收。

选择 Internet Site 即可。

postfix 配置文件主要集中在 /etc/postfix 目录下,最重要的两个文件是 master.cfmain.cf ,先编辑 main.cf 查看 hostname 设置正确,myhostname 的值要对应域名 MX 记录的主机名。

myhostname = mail.example.com

修改配置后重新加载配置

sudo /etc/init.d/postfix reload

其他相关的配置,注意真正配置时,不要在同一行加 # 注释,注释放在上下行。

#这一行为配置域名也就是 @ 后面的部分
mydomain = $mydomain
mydestination = $myhostname
# 默认为 all 表示接受来自所有网络的请求,改为 loopback-only http://www.postfix.org/postconf.5.html
inet_interfaces = loopback-only
# 配置哪些地址邮件能够被 Postfix 转发
relay_domains = $mydomain

测试发信

在配置完成之后就能够通过命令行发送邮件,加入当前登陆的用户是 einverne,那么用户的邮箱就是 einverne@domain.com 类似。在安装 Postfix 同时,也会安装一个 sendmail 程序,可以通过这个命令行交互程序测试发送邮件。

sendmail name@gmail.com

回车之后会进入等待,第一行输入 Subject,回车,输入 . 结束邮件,然后登录邮箱查看邮件,如果收到邮件表示 postfix 已经具有发件能力。

详细一些,可以新建文本 mail.txt:

To: my@domain.com
Subject: sendmail test two
From: me@domain.com

And here goes the e-mail body, test test test..

然后使用 sendmail -vt < ~/.mail.txt

或者

echo "Subject: sendmail test" | sendmail -v my@email.com

测试收件

因为本机还没配置 DNS,所以其他邮件服务商无法识别主机,所以使用 telnet 测试。

telnet localhost 25
Trying 127.0.0.1
Connected to localhost
Escape character is '^]'.
220 mail.example.com ESMTP Postfix (Ubuntu)
MAIL FROM: youremail@gmail.com
250 2.1.0 Ok
RCPT TO: root
250 2.1.5 Ok
DATA
354 End data with .
text
.
250 2.0.0 Ok: queued as 9A13A130FDA
QUIT
Connection closed by foreign host.

中间 MAIL FROM, RCPT,DATA,text,点,QUIT 这几行都是需要手动输入的。然后在本地服务器上查看信件内容

sudo tailf /var/mail/root

大致会看到

Delivered-To: root@yourhost
Received: from localhost (localhost [127.0.0.1])
        by yourhost (Postfix) with SMTP id 9A13A130FDA
        for <root>; Wed,  5 Sep 2018 16:56:14 +0800 (CST)
Message-Id: <20180905085622.9A13A130FDA@yourhost>
Date: Wed,  5 Sep 2018 16:56:14 +0800 (CST)
From: youremail@gmail.com

text

Postfix 默认使用 mbox 格式将系统用户的邮件存放到 /var/mail 目录下。

使用 mail 命令行

之前通过 sendmail 命令能够发送邮件,查看 /var/mail 目录也能够查看收件的信息。通过 sudo apt install mailutils 中的 mail 命令能够方便的进行发信和收信操作。

发信

mail user@gmail.com

命令行会进入发信,自动出现 Cc, Subject 等等,填入主题和正文和使用 Ctrl + D 来发送邮件。

查看收件箱直接输入 mail 就行。如果要查看第一封邮件,输入数字 1。

  • 删除第二封邮件, d 2
  • 删除多封邮件, d 2 3 4 或者 d 2-10
  • 阅读下一封邮件 n
  • 回复第一封邮件, reply 1
  • 退出 q 或者 x ,如果按 q 退出 mail 程序,那么已经阅读的邮件会从 /var/mail 移动到 /home/mbox 中,邮件客户端可能不能阅读这些邮件,如果不想移动可以使用 x 退出。

配置相关

master.cf 配置文件来决定如何启动 Postfix daemon,而 main.cf 配置文件则是配置 Postfix 的主要文件。Postfix 配置参数非常多,为了方便查找,通常在 main.cf 同目录下会附加一个 main.cf.proto 样例文件,里面有非常详细的注释。

Postfix 提供了一个命令行中修改 main.cf 配置文件的工具 —- postconf,但是如果熟悉 main.cf 也可以直接使用 vim 等编辑工具直接修改文件。

设置 MX 记录

如果邮件服务器准备对外使用,需要接收来自其他邮件服务商的邮件,需要将域名 DNS 修改

mail          A       123.45.6.7

主域名设置 MX 记录

mail          MX         mail.domain.com

@ 符号表示主机名,相当于 domain.com,MX 记录制定了 domain.com 这个域名的邮件服务器主机,如果收件人邮箱 @ 域名地址,发件人 MTA 将邮件投递到 mail.domain.com 这个主机,A 记录将 mail.domain.com 解析为 IP

本地测试

dig example.com mx

然后在本地发送邮件测试 sendmail root@example.com

mbox 格式

mbox 格式的邮件,以 From 接一个空格开始,空格之后是邮件地址,然后是收件日期

From name@example.com Wed Feb 25 16:04:34 2018

之后就是邮件正文内容,最后结束总是以一个空行结束。

安装 Dovecot

安装

sudo apt-get install dovecot-core dovecot-imapd
sudo dovecot --version

Dovecot 的配置在 /etc/dovecot/

reference


2018-09-05 mail-server , mail , linux , smtp

Appium 介绍

在接触爬虫的时候遇到了这个工具,看官网介绍应该可以归纳总结为:

  • 开源的移动端自动化测试框架
  • 跨平台,Linux,Mac,Windows 通吃
  • 支持 Android,iOS,混合应用,Web 应用

设计初衷

设计哲学

  • 不需要为了自动化而重新编译修改 App
  • 不限定在某个语言或者框架, C/S 架构,Client 端实现语言不限制
  • 不重造轮子,扩展 webdriver 协议
  • 开源

Jason Huggins 在 2004 年发起了 Selenium 项目,Jason 和他所在的团队采用 Javascript 编写一种测试工具来验证浏览器页面的行为。关于 Selenium 的命名比较有意思,当时 QTP mercury 是主流的商业自化工具,是化学元素汞,而 Selenium 是开源自动化工具,是化学元素硒,硒可以对抗汞。

在 2006 年的时候,Google 的工程师 Simon Stewart 发起了 WebDriver 的项目。WebDriver 是通过原生浏览器支持或者浏览器扩展来直接控制浏览器。

技术架构

安装

Appium Server,npm 命令的安装可以查看这里

npm install -g appium

client 可以参考这里,支持 Ruby,Python,Java,JS,OC,PHP,C# 等等

也可以选择 Appium Desktop

环境安装检查,appium-doctor

$ appium-doctor
info AppiumDoctor Appium Doctor v.1.4.3
info AppiumDoctor ### Diagnostic starting ###
info AppiumDoctor  ✔ The Node.js binary was found at: /usr/bin/node
info AppiumDoctor  ✔ Node version is 8.11.4
info AppiumDoctor  ✔ ANDROID_HOME is set to: /home/einverne/Android/Sdk
info AppiumDoctor  ✔ JAVA_HOME is set to: /usr/local/jdk1.8.0_131
info AppiumDoctor  ✔ adb exists at: /home/einverne/Android/Sdk/platform-tools/adb
info AppiumDoctor  ✔ android exists at: /home/einverne/Android/Sdk/tools/android
info AppiumDoctor  ✔ emulator exists at: /home/einverne/Android/Sdk/tools/emulator
info AppiumDoctor  ✔ Bin directory of $JAVA_HOME is set
info AppiumDoctor ### Diagnostic completed, no fix needed. ###
info AppiumDoctor
info AppiumDoctor Everything looks good, bye!
info AppiumDoctor

总结来看,完成 Appium 安装需要经过如下的步骤:

  • JDK,配置 JAVA_HOME
  • Android SDK,配置 ANDROID_HOME
  • appium 自身,npm 或者 desktop 随意
  • appium client
  • Android 模拟器或者真机

几个概念

Appium Desired Capabilities

大致可以认为 k-v 的配置,具体可以参考官网

SDK 辅助工具

Android SDK 提供了一些辅助工具,大都在 SDK tools 目录下,不同系统可能命名方式有些差异,但基本都能够识别。

uiautomatorviewer

工具在 Sdk/tools/bin/uiautomatorviewer 下, 这是 android sdk 自带的工具可以用来查看控件的属性,id,class 等等,也可以用来查看 package name。

如果目标设备的 API Level 低于 18 则 uiautomatorviewer 不能获得对应的 sesource id,只有等于大于 18 的时候才能使用。

ui automator viewer

Android Device Monitor

工具在 sdk/tools/monitor

android device monitor

实例

查看应用包名

查看包名的方式有很多,有很多 root 权限的应用都可以直接查看包名

查看应用 Activity 名

上面提到的 uiautomatorviewer 是以一种方式,其他

  • 源码
  • 反编译
  • Xposed Inspeckage
  • adb shell dumpsys activity activities | grep 'Hist #' 其他常用的 adb 命令

查找控件

通过上面提到的 uiautomatorviewer 来查看界面中的控件 ID

resource-id 的方式比较简单,在界面中找到 resource-id 即可

find_element_by_id('com.google.android.calculator:id/digit_1')                # 通过 resource-id 来查找

同理, uiautomatorviewer 中的 text 就是要找的 name,但是测试通过 name 找失败的可能性很大。

find_element_by_name()              # 通过名字查找

同样通过 class 查找

find_element_by_class_name('android.widget.TextView')   # 通过类名查找

同样可以使用 Accessibility 来获取控件,要找的是控件的 content-desc 属性:

e_minus = self.driver.find_element_by_accessibility_id('einvernenus')
e_minus.click()

通过 uiautomator

e5 = self.driver.find_element_by_android_uiautomator("new UiSelector().text(\"5\")")
e5.click()

模拟按键点击

在找到控件之后可以使用

login_btn.click()

模拟输入

user_input.send_keys('1234')

模拟返回键

driver.press_keycode(4)

keycode 的定义可以在 Android KeyEvent 中找到。

常用键 code

KEYCODE_CALL 拨号键 5
KEYCODE_ENDCALL 挂机键 6
KEYCODE_HOME 按键 Home 3
KEYCODE_MENU 菜单键 82
KEYCODE_BACK 返回键 4
KEYCODE_SEARCH 搜索键 84
KEYCODE_CAMERA 拍照键 27
KEYCODE_FOCUS 拍照对焦键 80
KEYCODE_POWER 电源键 26
KEYCODE_NOTIFICATION 通知键 83
KEYCODE_MUTE 话筒静音键 91
KEYCODE_VOLUME_MUTE 扬声器静音键 164
KEYCODE_VOLUME_UP 音量增加键 24
KEYCODE_VOLUME_DOWN 音量减小键 25

KEYCODE_ENTER 回车键 66
KEYCODE_ESCAPE ESC 键 111
KEYCODE_DPAD_CENTER 导航键 确定键 23
KEYCODE_DPAD_UP 导航键 向上 19
KEYCODE_DPAD_DOWN 导航键 向下 20
KEYCODE_DPAD_LEFT 导航键 向左 21
KEYCODE_DPAD_RIGHT 导航键 向右 22
KEYCODE_MOVE_HOME 光标移动到开始键 122
KEYCODE_MOVE_END 光标移动到末尾键 123
KEYCODE_PAGE_UP 向上翻页键 92
KEYCODE_PAGE_DOWN 向下翻页键 93
KEYCODE_DEL 退格键 67
KEYCODE_FORWARD_DEL 删除键 112
KEYCODE_INSERT 插入键 124
KEYCODE_TAB Tab 键 61
KEYCODE_NUM_LOCK 小键盘锁 143
KEYCODE_CAPS_LOCK 大写锁定键 115
KEYCODE_BREAK Break/Pause 键 121
KEYCODE_SCROLL_LOCK 滚动锁定键 116
KEYCODE_ZOOM_IN 放大键 168
KEYCODE_ZOOM_OUT 缩小键 169

KEYCODE_ALT_LEFT Alt+Left
KEYCODE_ALT_RIGHT Alt+Right
KEYCODE_CTRL_LEFT Control+Left
KEYCODE_CTRL_RIGHT Control+Right
KEYCODE_SHIFT_LEFT Shift+Left
KEYCODE_SHIFT_RIGHT Shift+Right

KEYCODE_0 按键’0’ 7
KEYCODE_1 按键’1’ 8
KEYCODE_2 按键’2’ 9
KEYCODE_3 按键’3’ 10
KEYCODE_4 按键’4’ 11
KEYCODE_5 按键’5’ 12
KEYCODE_6 按键’6’ 13
KEYCODE_7 按键’7’ 14
KEYCODE_8 按键’8’ 15
KEYCODE_9 按键’9’ 16
KEYCODE_A 按键’A’ 29
KEYCODE_B 按键’B’ 30
KEYCODE_C 按键’C’ 31
KEYCODE_D 按键’D’ 32
KEYCODE_E 按键’E’ 33
KEYCODE_F 按键’F’ 34
KEYCODE_G 按键’G’ 35
KEYCODE_H 按键’H’ 36
KEYCODE_I 按键’I’ 37
KEYCODE_J 按键’J’ 38
KEYCODE_K 按键’K’ 39
KEYCODE_L 按键’L’ 40
KEYCODE_M 按键’M’ 41
KEYCODE_N 按键’N’ 42
KEYCODE_O 按键’O’ 43
KEYCODE_P 按键’P’ 44
KEYCODE_Q 按键’Q’ 45
KEYCODE_R 按键’R’ 46
KEYCODE_S 按键’S’ 47
KEYCODE_T 按键’T’ 48
KEYCODE_U 按键’U’ 49
KEYCODE_V 按键’V’ 50
KEYCODE_W 按键’W’ 51
KEYCODE_X 按键’X’ 52
KEYCODE_Y 按键’Y’ 53
KEYCODE_Z 按键’Z’ 54

其他

锁屏,解锁

lock()
unlock()

隐藏键盘

hide_keyboard()

获取文件

pull_file()

推送文件

push_file()

打开任意的 activity

def start_activity(self, app_package, app_activity, **opts):
    """Opens an arbitrary activity during a test. If the activity belongs to
    another application, that application is started and the activity is opened.

    This is an Android-only method.

    :Args:
    - app_package - The package containing the activity to start.
    - app_activity - The activity to start.
    - app_wait_package - Begin automation after this package starts (optional).
    - app_wait_activity - Begin automation after this activity starts (optional).
    - intent_action - Intent to start (optional).
    - intent_category - Intent category to start (optional).
    - intent_flags - Flags to send to the intent (optional).
    - optional_intent_arguments - Optional arguments to the intent (optional).
    - dont_stop_app_on_reset - Should the app be stopped on reset (optional)?
    """

打开通知栏 Android only

open_notifications()

模拟摇晃设备

shake()

操作应用

包括关闭应用,重启应用,让应用到后台,安装、卸载应用,终止应用。

import time
import unittest

from appium import webdriver


class AppAndroidTests(unittest.TestCase):

    def setUp(self):
        # 测试初始化
        desired_caps = {
            'platformName': 'Android',
            'platformVersion': '6.0.1',
            'deviceName': 'OnePlus3',
            'udid': '2dd11c6e',
            'automationName': 'Appium',
            'app': '/home/einverne/android/ApiDemos-debug.apk',
            'appPackage': 'io.appium.android.apis',
            'appActivity': '.ApiDemos'
        }
        self.driver = webdriver.Remote('http://localhost:4723/wd/hub',
                                       desired_caps)

        self.app_id = 'io.appium.android.apis'

    def tearDown(self):
        # 用例结束时调用
        time.sleep(5)
        self.driver.quit()

    def test_apk_install(self):
        if not self.driver.is_app_installed(self.app_id):
            self.driver.install_app("/home/einverne/android/ApiDemos-debug.apk")
        else:
            print("demo apis installed")
            self.driver.remove_app(self.app_id)

    def test_apk_close(self):
        self.driver.close_app()
        time.sleep(2)
        self.driver.launch_app()
        time.sleep(2)
        self.driver.background_app(2)
        time.sleep(4)
        self.driver.launch_app()
        time.sleep(2)
        self.driver.terminate_app(self.app_id)
        time.sleep(2)
        self.driver.reset()


if __name__ == '__main__':
    suite = unittest.TestLoader().loadTestsFromTestCase(AppAndroidTests)
    unittest.TextTestRunner(verbosity=2).run(suite)

点击事件

各种点击事件,包括短按,长按,滑动等等,主要注意的是,界面的坐标是向下的象限,也就是左上角是 (0,0),右下角是类似 (1080,1920) 这样的坐标。

import time
import unittest

from appium import webdriver
from appium.webdriver.common.touch_action import TouchAction


class PressAndroidTests(unittest.TestCase):
    # 主要演示点击,长按,移动等操作
    def setUp(self):
        # 测试初始化
        desired_caps = {
            'platformName': 'Android',
            'platformVersion': '6.0.1',
            'deviceName': 'OnePlus3',
            'udid': '2dd11c6e',
            'automationName': 'Appium',
            'appPackage': 'com.google.android.calculator',
            'appActivity': 'com.android.calculator2.Calculator'
        }
        self.driver = webdriver.Remote('http://localhost:4723/wd/hub',
                                       desired_caps)
        time.sleep(2)

    def tearDown(self):
        # 用例结束时调用
        time.sleep(5)
        self.driver.quit()

    def test_press(self):
        for i in range(5):
            touch_action = TouchAction(self.driver)

            # release() 取消屏幕指针
            # perform() 执行操作发送命令
            touch_action.press(x=300, y=1700).release()
            touch_action.perform()

        e_del = self.driver.find_element_by_accessibility_id('delete')
        # tap
        time.sleep(2)
        tap_action = TouchAction(self.driver)
        tap_action.tap(e_del).release().perform()

        time.sleep(2)
        # long press
        del_action = TouchAction(self.driver)
        del_action.long_press(e_del, 2)
        del_action.release().perform()

        time.sleep(2)
        arrow = self.driver.find_element_by_id(
            'com.google.android.calculator:id/arrow')
        # move
        move_action = TouchAction(self.driver)
        move_action.press(arrow)
        move_action.move_to(x=500, y=1310)
        move_action.release().perform()

        time.sleep(2)


if __name__ == '__main__':
    suite = unittest.TestLoader().loadTestsFromTestCase(PressAndroidTests)
    unittest.TextTestRunner(verbosity=2).run(suite)

reference


2018-09-03 appium , android , ios , web

Certificate pinning 介绍

在抓包 Instagram 的时候发现所有的请求都被加密,即使使用 MITM 也无法查看请求内容。Google 之后发现 Instagram,Facebook 等等应用都使用了 Certificate pinning(或者也被称为 ssl-pinning) 的技术来加强通信安全。

HTTP 在 TCP 和 IP 协议之上,HTTPS 则是在 TCP 和 HTTP 之间增加了一道 SSL or TLS 协议。

市面上的各种抓包软件的实现原理就是中间人攻击。TLS 建立时客户端生成的随机数 1 服务端生成的随机数 2 都是明文的,只有随机数据 3 使用非对称加密技术加密中间人攻击的关键就是截获服务器返回的证书并伪造证书发送给客户端骗取信任,获取随机数 3,进而达成盗取信息的目的。

Instagram 在开发时就将服务端证书打包到客户端中,在 HTTPS 建立时与服务端返回的证书对比一致性,从而识别中间人攻击后直接在客户端终止连接。

解决方案

方案一

  • iOS 通过越狱安装 ssl-kill-switch2 来绕过证书验证
  • Android 通过 Xposed Module 禁用证书验证 JustTrustMe ,经过我的测试这种方式对 Instagram 已经失效

方案二

反编译高手可以拆解 apk 或者 iap 包,将客户端打包的证书替换掉,再签名,之后使用修改的包抓包

reference


2018-08-30 ssl , http , certificate , android , security

Centos 6/7 下安装 PHP7

下面记录下 Centos 6/ 7 下通过 yum 安装 php7 环境。

2015 年 12 月初 PHP7 正式版发布,迎来自 2004 年以来最大的版本更新。PHP7 最显著的变化就是性能的极大提升,已接近 Facebook 开发的 PHP 执行引擎 HHVM。在 WordPress 基准性能测试中,速度比 5.6 版本要快 2~3 倍,大大减少了内存占用。PHP7 在语言上也有一些变化,比如添加返回类型声明、增加了一些新的保留关键字等。在安全方面,去除了 PHP 安全模式,添加魔术引号等。不仅如此,新版还支持 64 位,而且包含最新版 Zend 引擎。

查看 centos 版本

cat /etc/centos-release

删除之前的 php 版本

yum remove php* php-common

rpm 安装 Php7 相应的 yum 源

CentOS/RHEL 7.x:

rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm

CentOS/RHEL 6.x:

rpm -Uvh https://mirror.webtatic.com/yum/el6/latest.rpm

yum 安装 php7

yum install php70w php70w-opcache

安装其他插件(选装)

php70w
php70w-bcmath
php70w-cli
php70w-common
php70w-dba
php70w-devel
php70w-embedded
php70w-enchant
php70w-fpm
php70w-gd
php70w-imap
php70w-interbase
php70w-intl
php70w-ldap
php70w-mbstring
php70w-mcrypt
php70w-mysql
php70w-mysqlnd
php70w-odbc
php70w-opcache
php70w-pdo
php70w-pdo_dblib
php70w-pear
php70w-pecl-apcu
php70w-pecl-imagick
php70w-pecl-xdebug
php70w-pgsql
php70w-phpdbg
php70w-process
php70w-pspell
php70w-recode
php70w-snmp
php70w-soap
php70w-tidy
php70w-xml
php70w-xmlrpc

重启 Apache

service httpd restart

如果是 Apache + PHP 的话必须使用 PHPIniDir 指定 php5 的配置文件 php.ini 的路径

PHPINIDir /etc/php.ini

reference


2018-08-29 centos , php7 , php , apache , yum

MDX/MDD 文件格式解析

MDict 将字典定义(关键字,解释)放在 MDX 文件中,字典相关的其他资源样式文件,比如图片,发音,样式放在 MDD 文件中,虽然存放的内容是不一样的,但是两种文件的结构是一致的。

源文件格式

MDict 的 html 格式

每个项目两行

第一行是关键字
第二行开始是正文,这里的正文应该包括关键字。可以使用 html 的标记(不要包含<html>) <body></body></html>, 这个程序会自动加上,另注意在转换时要指明源数据为 html).
如果需要显示音标的话,可以利用 html 指定字体就可以显示了。

举例

Whole
<font size=5>whole</font>
<br>
<font face="Kingsoft Phonetic Plain, Tahoma">(hol,hJl; houl)</font>
</>

注意”</>”和下一个记录间不要有任何其他空行

在 html 中连接到其它关键字的方法

<a href="entry://key#section">key</a>

其中 key 是关键字,section 是对应关键字页面中的 section 名称

在 html 中嵌入图片的方法

<img src="file://abc.gif"> 或者 <img src="/abc.gif"> 其中 src 指向的文件名大小写不敏感

并将所有图片文件放在一个单独的目录中(不要与词典源文件放在同一目录中). 数据目录中可以带有子目录。在使用 MdxBuilder 制作词库时,将 Data 路径指向上面存放数据的目录,该目录中的所有文件都会被压缩到后缀名为.mdd 文件中,使用时该.mdd 文件应当与.mdx 文件在同一目录下。

在 html 中嵌入声音的方法

使用链接 <a href="sound://keyword.spx">keyword</a> 这样的形式,点击该链接的时候可以进行发音 发音仅支持.wav 和.spx 格式的音频文件

内部重定向(内容链接)

当两个关键字所指向的内容是一样的时候,可以采取重定向的方式来达到”链接”的效果。内部重定向的格式为:6

@@@LINK= 关键字

例如 color 和 colour 都指向相同的内容,制作词典时可以正常编写 color 词条,但对于 colour 词条,可以写成:

colour
@@@LINK=color
</>

当显示 colour 条目的内容时,程序会自动找到 color 的内容进行显示,链接是可以多重嵌套的。

writemdict

一个 Python 库用来将 mdx 转换为 txt 文本

一个 Python 库可以用来制作 mdx 词典


2018-08-27 python , goldendict , mdx , mdd , dict

使用 Meld 对比文件及文件夹区别

在之前总结 Nemo file browser 的时候接触到了 meld,作为一个 nemo-compare 插件可以用来轻松的比较两个文件夹,两个文件,甚至多个文件。在了解之后发现 meld 其实更加强大,结合 git 使用可以非常轻松地解决 git 的合并冲突问题。

虽然大部分情况我都是用 smartGit 来解决的,smartGit 的三路合并和 meld 能够达到的效果非常相似。

安装

sudo apt install meld

使用

打开 meld 从主界面就能看到应用的分工主要分为三块

  • 文件内容的比较
  • 文件夹内容的比较
  • 版本控制的比较

前两个比较工具都可以选择两个或者三个输入源,后面的版本控制只需要一个版本控制的路径。meld 会自动对选中的内容进行比较。


2018-08-26 meld , linux , git , merge , conflict

Linux 下使用 ClamAV 扫描病毒

ClamAV 是开源的杀毒软件

安装

sudo apt-get install clamav

使用

更新“病毒库”

sudo freshclam

然后可以使用 scan 来扫描

clamscan OPTIONS file/folders

如果可以直接从 root 开始扫描:sudo clamscan

举例

扫描全盘,并显示文件名

clamscan -r /

扫描文件,只显示被感染的文件,并且声音提醒

clamscan -r --bell -i /

扫描全盘文件,只显示感染的文件,后台执行

clamscan -r -i / &

检查所有用户 home 目录下文件

clamscan -r /home

检查用户 home 目录,并将感染的病毒移动到另外的文件夹

clamscan -r --move=/home/USER/VIRUS /home/USER

检查用户 home 目录并移除感染的文件

clamscan -r --remove /home/USER

查看帮助

clamscan --help

更多

相关

ClamTk 是 ClamAV 的 GUI 版本

sudo apt install clamtk

从 PPA 获取

sudo apt-add-repository ppa:landronimirc/clamtk
sudo apt-get update && sudo apt-get install clamtk

reference


2018-08-25 linux , clamav , virus

使用 Nemo 文件管理器

自从用上 mint 之后,我才发现原来 File Manager 能这么好用,Cinnamon 自带的文件管理叫做 Nemo ,至今用过 Windows,Mac,Ubuntu 还要各种发行版,但是唯有 Cinnamon 自带的这个 Nemo 的文件管理器让我用起来最舒服。至于为什么,我一一道来。

功能

双栏

Nemo 外观很简单,和大多数操作系统的 File Browser 都差不多,左边栏基本上是顶层导航栏,然后主体部分是文件浏览的功能。但是我非常喜欢的双栏设计,当时使用 Windows 的时候还需要借助 Total Commander,而 Mac 的 Finder 是层级的,基本上如果一层一层打开文件夹就会出现一连串的中间文件夹列表,依然不能使用双栏。

nemo file browser extra pane

Nemo 的双栏模式叫做 Extra pane,右边一栏可以通过快捷键 F3 快速启动和隐藏,这就使得文件移动复制变得异常简单。当然有人说 mv path1 path2 更快当然在有命令行的情况下就不是同一比较线了。

检查 md5

很多时候下载文件要检查文件的完整性,大部分情况下都 md5sum ~/Downloads/large.file.tar.gz 然后完成了,但是 Nemo extension 原生支持

nemo gtkhash md5

折叠文件夹

同一个层级的文件夹也可以类似树形展开

nemo folder

标签页

我可以说其他 Windows,Mac 都需要其他额外的软件来支持文件管理器中的标签页,而 Nemo Ctrl+T 就能支持,这和我 Chrome 的快捷键 是一致的。

nemo tab

批量修改文件名

有人说不存在,其实 Nemo 本身不带批量重命名功能,但是 Nemo 是支持调用 thunar 的。

sudo apt install thunar

然后在设置中 Edit->Preferences->Behaviour 标签下 ‘Bulk rename’ 空白栏中填入:

thunar -B

然后 nemo -q 重启,此时再多选,就可以批量重命名了

nemo bulk rename

扩展

下面就是最主要的部分了,记住如果要想使得 Nemo 在安装之后生效需要强制重启 Nemo nemo -q 来退出。

Dropbox 支持

Dropbox 同步标示

sudo apt install nemo-dropbox

Nextcloud 支持

Nextcloud 同步标示

sudo apt install nextcloud-client-nemo

nemo-gtkhash

Nemo gtkhash 就是用来显示文件 md5,sha 等等

sudo apt install nemo-gtkhash

nemo-fileroller

Nemo Fileroller 扩展就是用来在上下文菜单中管理压缩包,压缩 / 解压功能的,配合 Compress,几乎可以解压所有文件,压缩也支持非常多的格式。如果 Nemo 中右键没有压缩和解压缩的选项,不要慌一行命令就能解决。

sudo apt-get install nemo-fileroller
nemo -q

然后重启 nemo 即可。

nemo-share

能够快速在浏览文件时共享到 samba

sudo apt install nemo-share

nemo-compare

使用 meld 来比较两个文件夹,或者两个文件

sudo apt install nemo-compare

nemo-seahorse

PGP 加密和签名的工具

sudo apt install nemo-seahorse

nemo-terminal

在文件夹中显示嵌入的命令行

sudo apt install nemo-terminal

nemo-emblems

可以用来自定义文件夹图标

nemo-audio-tab

用来显示 mp3 的包含的 meta 信息,包括 title, artist, album 等等

nemo-pastebin

支持直接上传到 pastebin ,我不怎么用所以没有安装

Tips

Nemo Actions

Nemo 允许用户自己定义上下文菜单,文件 /usr/share/nemo/actions/sample.nemo_action 包含一个样例,存放自定义 actions 脚本的目录:

  • /usr/share/nemo/actions/ 系统级别
  • ~/.local/share/nemo/actions/ 用户级别脚本

actions 脚本必须以 .nemo_action 结尾

扫描病毒脚本 clamscan.nemo_action,需要提前安装 ClamAV

[Nemo Action]
Name=Clam Scan
Comment=Clam Scan
Exec=gnome-terminal -x sh -c "clamscan -r %F | less"
Icon-Name=bug-buddy
Selection=Any
Extensions=dir;exe;dll;zip;gz;7z;rar;

在比如检查 md5 或者 sha1 也可以直接放到右击菜单中

[Nemo Action]
Active=true
Name=Check SHA256
Name[fr]=Vérifier le SHA256
Comment=Check the SHA256 signature of the file
Comment[fr]=Vérifier la signature SHA256 de ce fichier
Exec=mint-sha256sum '%F'
Icon-Name=gtk-execute
Selection=S
Mimetypes=application/x-iso9660-image;image/png;image/jpeg;

再比如我写一个脚本将选中的文件或者文件夹中空格部分替换为 _

format_filename.nemo_action 如下

[Nemo Action]
Active=true
Name=Format filename %N
Comment=Replace filename space with - applied to %N
Exec=<format_filename.py %F>
Selection=any
Extensions=any;
EscapeSpaces=true

python 脚本名叫 format_filename.py

import sys
import os

command = sys.argv[0]
print("Running " + command)
print("With the following arguments:")
for arg in sys.argv:
    if command == arg:
        continue
    else:
        formated_path = arg.replace(' ', '_')
        os.rename(arg, formated_path)

nemo_action 文件中用到了一些内置的变量

  • %U - insert URI list of selection
  • %F - insert path list of selection
  • %P - insert path of parent (current) directory
  • %f or %N (deprecated) - insert display name of first selected file
  • %p - insert display name of parent directory
  • %D - insert device path of file (i.e. /dev/sdb1)

官方的样例可以查看本地的文件也可以看 GitHub

Nemo Actions 将 Nemo 文件管理器的功能上升了另外一个层面,如果 Python/Bash 能够做的事情,那么在 Nemo 中都能够完成。那几乎就是所有的任务都能够在文件管理器中右键完成了。再举个简单的例子,我经常用 ffmpeg 将 Mp4 中的音频提取出来,那么就可以直接用 Actions ,然后定义

Exec=gnome-terminal -x sh -c 'ffmpeg -i %F -f mp3 anyname.mp3'

当然其他的都可以完成了。

配置

默认情况下 Ubuntu 下面默认的文件管理器还是 nautilus.desktop , 可以使用如下命令查看默认的文件管理器

xdg-mime query default inode/directory

我电脑上返回的结果是 nautilus-folder-handler.desktop

如果本地机器已经安装了 nemo,那么可以使用如下命令将默认的文件管理器设置为 nemo

xdg-mime default nemo.desktop inode/directory application/x-gnome-saved-search

如果想要恢复之前的设置,将 nemo.desktop 设置回去即可

xdg-mime default nautilus.desktop inode/directory application/x-gnome-saved-search

然后可以使用 xdg-open $HOME 来验证有没有生效。

reference


2018-08-23 nemo , cinnamon , file-manager , ubuntu , linux-mint

Python 并发编程之 gevent

gevent 中最主要的是 greenlet,greenlet 是 Python 的 C 扩展,用来实现协程。

协程 Coroutine,就是可以暂时中断,之后再继续执行的程序

事实上 Python 就有最基础的 Coroutine,也就是生成器 generator

协程就是一种特殊的并发机制,其调度”就是指什么时候调用什么函数”完全由程序员指定

  • 进程拥有自己独立的堆和栈,既不共享堆,亦不共享栈,进程由操作系统调度。
  • 线程拥有自己独立的栈和共享的堆,共享堆,不共享栈,线程亦由操作系统调度(标准线程是的)。
  • 协程和线程一样共享堆,不共享栈,协程由程序员在协程的代码里显示调度。

greenlet

看一个最经典的生产者消费者模型。

from greenlet import greenlet
from time import sleep

def consumer():
    last= ''
    while True:
        receival = pro.switch(last)
        if receival is not None:
            print(f'Consume {receival}')
            last = receival
            sleep(1)


def producer(n):
    con.switch()
    x = 0
    while x < n:
        x += 1
        print(f'Produce {x}')
        last = con.switch(x)

pro = greenlet(producer)
con = greenlet(consumer)
pro.switch(10)

gevent

gevent 是一个并发网络库,他的协程是基于 greenlet 的。并基于 libev 实现快速事件循环(Linux 上是 epoll,FreeBSD 上是 kqueue,Mac OS X 上是 select)。

一个比较通俗的解释就是当 greenlet 遇到 IO 操作,比如访问网络时自动切换到其他 greenlet ,等 IO 操作完成,在适当的时候切换回来继续执行。由于 IO 操作非常耗时,经常使程序处于等待状态,所以 gevent 保证总是有 greenlet 在运行,而不是等待 IO。

import gevent

def foo():
    print('Running in foo')
    gevent.sleep(0)
    print('Explicit context switch to foo again')

def bar():
    print('Explicit context to bar')
    gevent.sleep(0)
    print('Implicit context switch back to bar')

gevent.joinall([
    gevent.spawn(foo),
    gevent.spawn(bar),
])

gevent.spawn() 方法会创建一个新的 greenlet 协程对象,gevent.joinall() 方法会等待所有传入的 greenlet 协程运行结束后再退出。

优缺点

gevent 的优点如下:

  • 执行效率高,子程序切换几乎没有开销,与多线程相比,线程越多,协程性能越明显
  • 不需要多线程的锁机制,因为只有一个线程,也不存在同时写变量冲突,在控制共享资源时也不需要加锁
  • I/O 多路复用是在一个进程内部处理多个逻辑流程,不用进行进程切换,性能较高,另外流程间共享信息简单。
  • 协程有编程语言提供,由程序员控制进行切换,所以没有线程安全问题,可以用来处理状态机,并发请求等 IO 密集型任务

gevent 缺点如下:

  • 不能利用 CPU 多核优势
  • 程序流程被事件处理切割成一个个小块,程序比较复杂,难于理解

所以,协程的适用场景,应该是一些I/O 密集型的并行程序,而对应的计算密集型,应当采用传统的多线程、多进程方案。

相关知识点

  • Threads
  • Processes
  • SubProcess (Or os.system calls)
  • concurrent.futures
  • gevent, greenlet etc
  • asyncio aiodns
  • cython (Disabling the GIL)
  • Writing C extensions

reference


2018-08-22 python , greenlet , gevent , thread , process , yield

电子书

最近文章

  • MySQL 中的日志配置和管理 MySQL 中默认是没有开启日志记录的,所以需要手动修改配置文件开启日志。而在 MySQL 中我们需要关心的有三类日志:
  • Java 查漏补缺之:ThreadLocal 使用 ThreadLocal 线程本地变量,每个线程保存变量的副本,对副本的改动,对其他的线程而言是透明的。
  • 为知笔记导出和备份 WizNote 已经用了好几年,虽然也一直在续费,但总感觉将死不死,基于整理这几年近 4000 条的笔记的目的,也一方面为迁移出 WizNote 的目的,研究一下 WizNote 笔记导出和备份的方法。
  • Nginx location 匹配规则 之前的关于 Nginx Config 的文章是当时看 Nginx 书记录下来的笔记,很大略,没有实际操作,等终究用到 location 的时候发现还是有很多需要注意的问题,比如匹配的优先顺序,比如 root 和 alias 的区别等等,所以单独拿一篇文章来记录一下目前遇到的问题,并来解决一下。
  • koajs 简单使用 Koa 是一个背靠 Express 的团队设计的全新的 Web 框架,旨在使之成为一个更轻量,更丰富,更加 robust 的基础框架。通过促进异步方法的使用,Koa 允许使用者抛弃 callback 调用,并且大大简化了错误处理。Koa 并没有将中间件绑定到核心代码,而是提供了一组优雅的方法让编写服务更加快速,通过很多第三方的扩展给 Koa 提供服务,从而实现更加丰富完整的 HTTP server。