本文主要关注在 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,第一个参数是类名,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
参数。
实现多对多主要可以分为三个步骤:
比如:
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")
学习邮件服务器的一些笔记。
电子邮件相关的基本概念:
[[MUA]] 全称为 Mail User Agent 邮件用户代理。常见的 MUA 实例有: mutt, outlook, foxmail 等, 其主要任务是让用户能够收信,写信,发信。MUA 并非直接将 E-mail 发送到收件人手上,而是通过 MTA 代为传递。
MTA 全称 Mail Transfer Agent,MTA 仅仅负责邮件的传输。常见的 MTA 有 Postfix, sendmail, [[exim4]]
[[MDA]] 全称为 Mail Delivery Agent,负责投递本地邮件到适当的邮箱,一封邮件从 MUA 发出后,通过一个或者多个 MTA 最终到达 MDA。MDA 可以过滤邮件内容,依照规则,将邮件分类到适当的邮箱,甚至可以将邮件转回 MTA,以寄到另一个邮箱。一旦邮件到达邮箱,就原地等待用户通过 MUA 将其取走。
[[SMTP]] 全称 Simple Mail Transfer Protocol ,简单邮件传输协议。主要工作是把邮件信息从发件人邮件服务器中传送到接收人邮件服务器。SMTP 协议出现比较早,所有很多问题都没有考虑全面,比如信息内容需要是 ASCII 码,再比如 SMTP 没有对发送方进行身份验证,所有现在垃圾邮件非常多。
全称是 Local Mail Transfer Protocol,本地邮件传输协议,类似 SMTP,主要应用于非广域网的邮件网关。
全称是 Post Office Protocol Version 3,RFC1939,用于用户管理邮件服务器上面的电子邮件。当外来邮件发送到收件人的邮件服务器上时,收件人可以使用邮件客户端连接邮件服务器,把未阅读的邮件服务器以及部分信息拉取回本地进行处理。
[[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
全称是 Simple Authentication and Security Layer,因为 SMTP 协议没有验证用户身份的能力,虽然信封的寄件人地址已经隐含发信者身份,但是由于信封地址实在太容易造假,所以不能当成身份凭据。所以 SMTP 借助额外的机制 SASL 来验证客户端的身份,来决定谁有权使用转发服务。
SSL 全称是 Secure Socket Layer,加密传输层,TLS 是 Transport Layer Security 在 SSL 基础上提供更好的安全性。25 端口被设计用来转发邮件,并没有考虑认证和加密等问题,1997 年 465 端口被注册用于加密 (SMTPS)提交邮件,1998 年 STARTTLS 标准出现,规定使用 587 端口使用 STARTTLS 方式提交邮件。
假设用户 a@gmail.com 发送一封邮件到 b@qq.com,大致会执行流程:
安装 postfix
sudo apt install postfix
完成之后可以查看版本
sudo postconf mail_version
安装过程中可能出现这几个选项:
选择 Internet Site 即可。
postfix 配置文件主要集中在 /etc/postfix
目录下,最重要的两个文件是 master.cf
和 main.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 目录下。
之前通过 sendmail 命令能够发送邮件,查看 /var/mail
目录也能够查看收件的信息。通过 sudo apt install mailutils
中的 mail 命令能够方便的进行发信和收信操作。
发信
mail user@gmail.com
命令行会进入发信,自动出现 Cc, Subject 等等,填入主题和正文和使用 Ctrl + D 来发送邮件。
查看收件箱直接输入 mail 就行。如果要查看第一封邮件,输入数字 1。
master.cf 配置文件来决定如何启动 Postfix daemon,而 main.cf 配置文件则是配置 Postfix 的主要文件。Postfix 配置参数非常多,为了方便查找,通常在 main.cf 同目录下会附加一个 main.cf.proto 样例文件,里面有非常详细的注释。
Postfix 提供了一个命令行中修改 main.cf 配置文件的工具 —- postconf,但是如果熟悉 main.cf 也可以直接使用 vim 等编辑工具直接修改文件。
如果邮件服务器准备对外使用,需要接收来自其他邮件服务商的邮件,需要将域名 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 格式的邮件,以 From 接一个空格开始,空格之后是邮件地址,然后是收件日期
From name@example.com Wed Feb 25 16:04:34 2018
之后就是邮件正文内容,最后结束总是以一个空行结束。
安装
sudo apt-get install dovecot-core dovecot-imapd
sudo dovecot --version
Dovecot 的配置在 /etc/dovecot/
下
在接触爬虫的时候遇到了这个工具,看官网介绍应该可以归纳总结为:
设计哲学
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 安装需要经过如下的步骤:
JAVA_HOME
ANDROID_HOME
大致可以认为 k-v 的配置,具体可以参考官网
Android SDK 提供了一些辅助工具,大都在 SDK tools 目录下,不同系统可能命名方式有些差异,但基本都能够识别。
工具在 Sdk/tools/bin/uiautomatorviewer
下, 这是 android sdk 自带的工具可以用来查看控件的属性,id,class 等等,也可以用来查看 package name。
如果目标设备的 API Level 低于 18 则 uiautomatorviewer 不能获得对应的 sesource id,只有等于大于 18 的时候才能使用。
工具在 sdk/tools/monitor
查看包名的方式有很多,有很多 root 权限的应用都可以直接查看包名
上面提到的 uiautomatorviewer 是以一种方式,其他
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)
在抓包 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 建立时与服务端返回的证书对比一致性,从而识别中间人攻击后直接在客户端终止连接。
反编译高手可以拆解 apk 或者 iap 包,将客户端打包的证书替换掉,再签名,之后使用修改的包抓包
下面记录下 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
MDict 将字典定义(关键字,解释)放在 MDX(全称 Mobile Dictionary eXchange) 文件中,字典相关的其他资源样式文件,比如图片,发音,样式放在 MDD 文件中,虽然存放的内容是不一样的,但是两种文件的结构是一致的。
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 的内容进行显示,链接是可以多重嵌套的。
一个 Python 库用来将 mdx 转换为 txt 文本
一个 Python 库可以用来制作 mdx 词典
Java 库
更多
在之前总结 Nemo file browser 的时候接触到了 meld,作为一个 nemo-compare 插件可以用来轻松的比较两个文件夹,两个文件,甚至多个文件。在了解之后发现 meld 其实更加强大,结合 git 使用可以非常轻松地解决 git 的合并冲突问题。
虽然大部分情况我都是用 smartGit 来解决的,smartGit 的三路合并和 meld 能够达到的效果非常相似。
sudo apt install meld
打开 meld 从主界面就能看到应用的分工主要分为三块
前两个比较工具都可以选择两个或者三个输入源,后面的版本控制只需要一个版本控制的路径。meld 会自动对选中的内容进行比较。
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
自从用上 mint 之后,我才发现原来 File Manager 能这么好用,Cinnamon 自带的文件管理叫做 Nemo ,至今用过 Windows,Mac,Ubuntu 还要各种发行版,但是唯有 Cinnamon 自带的这个 Nemo 的文件管理器让我用起来最舒服。至于为什么,我一一道来。
Nemo 外观很简单,和大多数操作系统的 File Browser 都差不多,左边栏基本上是顶层导航栏,然后主体部分是文件浏览的功能。但是我非常喜欢的双栏设计,当时使用 Windows 的时候还需要借助 Total Commander,而 Mac 的 Finder 是层级的,基本上如果一层一层打开文件夹就会出现一连串的中间文件夹列表,依然不能使用双栏。
Nemo 的双栏模式叫做 Extra pane
,右边一栏可以通过快捷键 F3 快速启动和隐藏,这就使得文件移动复制变得异常简单。当然有人说 mv path1 path2
更快当然在有命令行的情况下就不是同一比较线了。
很多时候下载文件要检查文件的完整性,大部分情况下都 md5sum ~/Downloads/large.file.tar.gz
然后完成了,但是 Nemo extension 原生支持
同一个层级的文件夹也可以类似树形展开
我可以说其他 Windows,Mac 都需要其他额外的软件来支持文件管理器中的标签页,而 Nemo Ctrl+T
就能支持,这和我 Chrome 的快捷键 是一致的。
有人说不存在,其实 Nemo 本身不带批量重命名功能,但是 Nemo 是支持调用 thunar 的。
sudo apt install thunar
然后在设置中 Edit->Preferences->Behaviour 标签下 ‘Bulk rename’ 空白栏中填入:
thunar -B
然后 nemo -q
重启,此时再多选,就可以批量重命名了
下面就是最主要的部分了,记住如果要想使得 Nemo 在安装之后生效需要强制重启 Nemo nemo -q
来退出。
Dropbox 同步标示
sudo apt install nemo-dropbox
Nextcloud 同步标示
sudo apt install nextcloud-client-nemo
Nemo gtkhash 就是用来显示文件 md5,sha 等等
sudo apt install nemo-gtkhash
Nemo Fileroller 扩展就是用来在上下文菜单中管理压缩包,压缩 / 解压功能的,配合 Compress,几乎可以解压所有文件,压缩也支持非常多的格式。如果 Nemo 中右键没有压缩和解压缩的选项,不要慌一行命令就能解决。
sudo apt-get install nemo-fileroller
nemo -q
然后重启 nemo 即可。
能够快速在浏览文件时共享到 samba
sudo apt install nemo-share
使用 meld 来比较两个文件夹,或者两个文件
sudo apt install nemo-compare
PGP 加密和签名的工具
sudo apt install nemo-seahorse
在文件夹中显示嵌入的命令行
sudo apt install nemo-terminal
可以用来自定义文件夹图标
用来显示 mp3 的包含的 meta 信息,包括 title, artist, album 等等
支持直接上传到 pastebin ,我不怎么用所以没有安装
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
文件中用到了一些内置的变量
官方的样例可以查看本地的文件也可以看 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
来验证有没有生效。
gevent 中最主要的是 greenlet,greenlet 是 Python 的 C 扩展,用来实现协程。
协程 [[Coroutine]],就是可以暂时中断,之后再继续执行的程序
事实上 Python 就有最基础的 Coroutine,也就是生成器 generator
协程就是一种特殊的并发机制,其调度”就是指什么时候调用什么函数”完全由程序员指定
看一个最经典的生产者消费者模型。
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 是一个并发网络库,他的协程是基于 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 的优点如下:
gevent 缺点如下:
所以,协程的适用场景,应该是一些IO 密集型的并行程序,而对应的计算密集型,应当采用传统的多线程、多进程方案。