epub 格式解析

epub 是一个电子书标准,最近在研究电子书解析,所以有了此文。

一个标准的未加密的 epub 电子书大致由以下三部分组成:

  • META-INF 文件夹,其中包含 container.xml 文件
  • OEBPS 文件夹,包含 images,xhtml 文件,css 样式和 content.opf 文件
  • mimetype 文件,内容为 application/epub+zip

META-INF 文件夹

META-INF 用于存放电子书信息,默认情况包含一个 container.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<container version="1.0" xmlns="urn:oasis:names:tc:opendocument:xmlns:container">
    <rootfiles>
        <rootfile full-path="OEBPS/content.opf" media-type="application/oebps-package+xml"/>
   </rootfiles>
</container>

该文件告诉电子书阅读器,文件的根文件路径和格式。除去 container.xml 文件外,标准还规定了其他可选文件:

  • manifest.xml 文件列表
  • metadata.xml 元数据
  • sigatures.xml 数字签名
  • encryption.xml 加密
  • rights.xml 权限管理

这些文件是可选的。

OEBPS 文件夹

OEBPS 文件夹用于存放真正的图书内容,包括 content.opf 文件,toc.ncx 目录文件,正文内容,css 样式文件,字体文件,封面,图片等等资源。

OPF 文件

opf 文件是 epub 最为重要的文件,是标准的 xml 文件,文件的根元素是 <package>

<package version="2.0" unique-identifier="BookId" xmlns="http://www.idpf.org/2007/opf">

此文件的主要内容由下面组成:

第一部分,<metadata> 元数据,包含书籍的出版信息,主要由两个子元素组成

  • dc:metadata 元素,使用 Dublin Core, 包含 15 项核心元素:

    • dc:title
    • dc:creator 责任者
    • dc:subject 主题关键词
    • dc:description
    • dc:publisher
    • dc:contributor
    • dc:date
    • dc:type
    • dc:format
    • dc:identifier
    • dc:source 来源
    • dc:language
    • dc:relation
    • dc:coverage 覆盖范围
    • dc:rights 权限描述
  • meta 标签,扩展元素,如果有信息在上面标签中无法描述,则扩展到该 meta 中

举例

<metadata xmlns:opf="http://www.idpf.org/2007/opf" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <dc:language>zh-CN</dc:language>
  <dc:title>圣殿春秋</dc:title>
  <dc:creator opf:role="aut" opf:file-as="雨浪飘零">『英』肯·福莱特</dc:creator>
  <dc:publisher>上海译文出版社</dc:publisher>
  <meta content="0.9.6" name="Sigil version" />
  <dc:date xmlns:opf="http://www.idpf.org/2007/opf" opf:event="modification">2016-07-16</dc:date>
  <dc:identifier opf:scheme="UUID" id="BookId">urn:uuid:97cabb7a-2ab9-4fe2-a56b-c075114f2187</dc:identifier>
  <meta name="cover" content="cover.jpg" />
</metadata>

第二部分为 <manifest> 文件列表,该列表中包含出版物的所有文件,每一行由一个 item 构成

<item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>

其中:

  • id 为文件 id
  • href 为文件相对路径
  • media-type 为文件的媒体类型

举例,文件内容有删减

<manifest>
  <item id="ncx" href="toc.ncx" media-type="application/x-dtbncx+xml"/>
  <item id="Preface03.xhtml" href="Text/Preface03.xhtml" media-type="application/xhtml+xml"/>
  <item id="Part00.xhtml" href="Text/Part00.xhtml" media-type="application/xhtml+xml"/>
  <item id="Part01.xhtml" href="Text/Part01.xhtml" media-type="application/xhtml+xml"/>
  <item id="Part02.xhtml" href="Text/Part02.xhtml" media-type="application/xhtml+xml"/>
  <item id="appendix.xhtml" href="Text/appendix.xhtml" media-type="application/xhtml+xml"/>
  <item id="main.css" href="Styles/main.css" media-type="text/css"/>
  <item id="Title.xhtml" href="Text/Title.xhtml" media-type="application/xhtml+xml"/>
  <item id="Intro1.xhtml" href="Text/Intro1.xhtml" media-type="application/xhtml+xml"/>
  <item id="Author.html" href="Text/Author.html" media-type="application/xhtml+xml"/>
  <item id="Preface01.xhtml" href="Text/Preface01.xhtml" media-type="application/xhtml+xml"/>
  <item id="cover.xhtml" href="Text/cover.xhtml" media-type="application/xhtml+xml"/>
  <item id="part01.jpg" href="Images/part01.jpg" media-type="image/jpeg"/>
  <item id="Part06_17.xhtml" href="Text/Part06_17.xhtml" media-type="application/xhtml+xml"/>
  <item id="chapter.png" href="Images/chapter.png" media-type="image/png"/>
  <item id="logo.png" href="Images/logo.png" media-type="image/png"/>
  <item id="cover.jpg" href="Images/cover.jpg" media-type="image/jpeg"/>
  <item id="cover_slim.jpg" href="Images/cover~slim.jpg" media-type="image/jpeg"/>
  <item id="Monarch.ttf" href="Fonts/Monarch.ttf" media-type="application/x-font-ttf"/>
  <item id="backcover.xhtml" href="Text/backcover.xhtml" media-type="application/xhtml+xml"/>
  <item id="backcover.jpg" href="Images/backcover.jpg" media-type="image/jpeg"/>
  <item id="backcover_slim.jpg" href="Images/backcover~slim.jpg" media-type="image/jpeg"/>
  <item id="Info.xhtml" href="Text/Info.xhtml" media-type="application/xhtml+xml"/>
</manifest>

第三部分为 <spine toc="ncx"> 提供图书线性阅读的次序,由子元素 itemref 组成

<itemref idref="cover.xhtml">

其中 idref 为 manifest 中列出的 id

<spine toc="ncx">
    <itemref idref="cover" />
    <itemref idref="copyright" />
</spine>

第四部分为 <guide> ,列出了电子书的特定页面,比如封面,目录,序言等等,属性值指向文件地址。该部分可选。

<guide>
  <reference type="cover" title="封面" href="Text/cover.xhtml"/>
</guide>

第五部分,<tour> 导读,根据读者的不同水平,按照一定次序选择电子书部分页面组成导读,可选。

NCX 文件

ncx 文件也是 epub 中非常重要的文件,该文件用于电子书的目录,文件命名通常为 toc.ncx,ncx 文件也是一个 xml 文件。ncx 全称为 Navigation Center eXtended。

ncx 文件中最主要的节点是 navMap,navMap 节点又由很多 navPoint 节点组成,navPoint 节点由 navLabel 和 content 节点组成。

<navMap>
  <navPoint id="navPoint-1" playOrder="1">
    <navLabel>
      <text>圣殿春秋</text>
    </navLabel>
    <content src="Text/cover.xhtml"/>
    <navPoint id="navPoint-2" playOrder="2">
      <navLabel>
        <text>作品简介</text>
      </navLabel>
      <content src="Text/Intro1.xhtml"/>
    </navPoint>
  ...
    <navPoint id="navPoint-7" playOrder="7">
      <navLabel>
        <text>前言</text>
      </navLabel>
      <content src="Text/Preface03.xhtml"/>
    </navPoint>
  </navPoint>
</navMap>
  • navPoint 节点中,playOrder 属性定义当前项在目录中的次序,text 子节点则定义了目录的名字
  • content 子节点 src 属性定义了章节文件的具体位置

navPoint 节点可以嵌套,形成了整本书的层级结构。

opf 文件定义了读者在顺序阅读时用到的章节和顺序,而 ncx 文件则定义了目录中用到的章节和顺序。如果存在附录形式的内容,希望在目录中出现,而不希望在正文中出现时,而已通过设置两个不同来达到目的。

在了解了这些标准内容之后,解析 epub 格式就比较简单了,python 可以使用 ebooklib 这个库。他的使用相对比较简单,也就一个 epub 类,具体使用可以参考 GitHub,不过需要注意的是很多 epub 格式的书并没有完全按照标准生成,所以有些地方还得自己 hack。

  • [[epubcheck]]

reference


2018-09-19 epub , ebook , python-lib , parse

电子书常见格式及格式转换

最近因为写 kindle 推书的服务,所以不得不接触到了不同的电子书格式,mobi,epub, azw3,都比较常见,kindle 只支持 mobi 格式,所以亚马逊提供 了 KendleGen 这个工具来将 epub 转换为 mobi 格式。

也正是因为这个结果,所以现在基本上也只有 python 读写 epub 的库,mobi 的读写目前做的也不是很好。如果单纯的只是想要转格式 Calibre 已经做的很好了,UI 也有。这里只是谈论编程方式读写 epub 格式。

格式

最近几年因为 Kindle 的介入,图书电子化的趋势越来越明显,虽然各大厂都在推自己的协议,但通常流行的也无非就那几个,epub, mobi,azw3 等等

epub

epub 的全称是 Electronic Publication 是一个开放的标准化电子书格式,这个标注的格式针对阅读设备优化了字体显示,epub 文件能够被不同的设备打开。

需要注意的是,虽然 ePub 格式是一个开放的格式,但是不是所有的图书都按照标准的 epub 格式来生成。1

一个典型的 epub 格式资源包含如下:

.
├── chapter001.html
├── chapter002.html
├── chapter003.html
├── chapter004.html
├── content.opf
├── cover.jpeg
├── index.html
├── META-INF
│   ├── calibre_bookmarks.txt
│   └── container.xml
├── mimetype
├── postscript.html
├── preface.html
├── stylesheet.css
├── titlepage.xhtml
└── toc.ncx

解释:

  • content.opf 该文件包含书籍的 meta 信息,包括书名,介绍,作者,ISBN 等等,dc 是 Dublin Core metadata 的缩写,最小的 epub 要求 DC:identifierDC:titleDC:language 这三项

      <dc:title>失控:机器、社会与经济的新生物学</dc:title>
      <dc:publisher>新星出版社</dc:publisher>
      <dc:rights>2010, 新星出版社</dc:rights>
      <dc:identifier id="uuid_id" opf:scheme="uuid">6ae9aa9a-8077-4a21-9658-d3656c96810d</dc:identifier>
    
  • toc.ncx 就不说了 toc 很熟悉,目录
  • mimetype mimetype 文件
  • *.html 文件就是书的文本内容,不同的书可能命名不一样,但是总体都是 html 文件
  • *.css 样式文件
  • cover.jpg 封面图片

详细的 epub 结构可以参考这里

mobi 格式

mobi 格式最早是由法国 Mobipocket 公司为个人掌上设备推出的电子书格式,后来该公司被 Amazon 收购,所以 mobi 格式也成为了 Kindle 的默认支持格式。2

azw, azw3

看名字就知道这是 Amazon kindle 的私有格式了。

格式转换

使用 Calibre

如果不熟悉命令行,可以使用 Calibre 这个强大的电子书管理软件,epub 到 mobi,mobi 到 epub,pdf 到 mobi 多种格式任意切换,还能一键发送到 kindle

Calibre ebook-convert

如果熟悉 ebook-convert 可以直接使用命令行来转换

ebook-convert book_name.epub book_name.mobi

关于命令使用详情,可以参考 Calibre 官网

ebook-convert 支持非常多的格式转换,具体的格式可以参考官网

Python 代码

def epub2mobi(book_path):
    import subprocess
    filename = os.path.basename(book_path)
    dirname = os.path.dirname(book_path)
    name, ext = os.path.splitext(filename)
    if ext != '.epub':
        return
    try:
        subprocess.call(['ebook-convert', book_path, os.path.join(dirname, name + '.mobi')])
    except Exception as e:
        logger.exception('convert error')

KindleGen

使用 Amazon 提供的二进制命令将 epub 转化为 mobi

*************************************************************
 Amazon kindlegen(Linux) V2.9 build 1028-0897292
 A command line e-book compiler
 Copyright Amazon.com and its Affiliates 2014
*************************************************************

Usage : kindlegen [filename.opf/.htm/.html/.epub/.zip or directory] [-c0 or -c1 or c2] [-verbose] [-western] [-o <file name>]
Note:
   zip formats are supported for XMDF and FB2 sources
   directory formats are supported for XMDF sources
Options:
   -c0: no compression
   -c1: standard DOC compression
   -c2: Kindle huffdic compression
   -o <file name>: Specifies the output file name. Output file will be created in the same directory as that of input file. <file name> should not contain directory path.
   -verbose: provides more information during ebook conversion
   -western: force build of Windows-1252 book
   -releasenotes: display release notes
   -gif: images are converted to GIF format (no JPEG in the book)
   -locale <locale option> : To display messages in selected language
      en: English
      de: German
      fr: French
      it: Italian
      es: Spanish
      zh: Chinese
      ja: Japanese
      pt: Portuguese
      ru: Russian
      nl: Dutch

工具库

Unpack mobi 库

下面的工具可以将 mobi 文件拆包,提取 azw3、mobi 的 epub。

https://github.com/kevinhendricks/KindleUnpack

Kindle 相关工具集

https://github.com/ywzhaiqi/MyKindleTools

Calibre 的 ebook-meta 工具

https://manual.calibre-ebook.com/generated/en/ebook-meta.html

利用该工具能够快速的获取各种格式电子书的 mata 信息,包括封面信息。3

reference

  1. http://sourcefabric.booktype.pro/booktype-23-for-authors-and-publishers/importing-a-book/ 

  2. https://en.wikipedia.org/wiki/Mobipocket 

  3. https://stackoverflow.com/a/35501566/1820217 


2018-09-18 ebook , epub , mobi , pdf , convert , kindlegen , calibre , python , python-lib , azw , azw3

如何配一副眼镜

在中国无论是购买什么东西,都需要对这一个行业进行深度的了解,不然就会被坑,眼镜同理。回想一下我当前戴的眼镜已经八年多,现在感觉是佩戴眼镜已经不舒服,所以就想要做个简单的了解。

从定下需求(寻找一副合适、舒服、能够长时间佩戴眼镜),到最后真正验光,下单购买也过去不下两个星期了。所以总结了这篇文章,如果能够帮助到其他人当然更好。

配眼镜相关的步骤无非就是验光,选镜架,选镜片,这主要的三个步骤,其中验光、镜片这两个步骤对我来说是最繁杂的部分,我上一次验光可能还是八、九年前,关于眼睛的度数散光也没怎么注意,但是验光的准确性对于配眼镜是非常重要的一步。另外就是镜片的选择,市场上的镜片种类太多,而普通消费者其实完全无从比较,对于眼镜这类必需的非消耗品来说,换镜的周期可能就是以年计,所以镜片与镜片之间的显微差异普通人是很难找准区别的。在加上眼镜店里面展示的镜片的各种系数,各种“功能”可能对于普通消费者来说就非常难以选择了。

镜架材质

对于镜架材质,其实个人的需求并不是很高,所以只做了简单的了解。市面上主流的眼镜架材质:

  • TR 又称记忆塑料,类似于金属的钛合金,算是比较高档的塑胶制品
  • 板材
  • 金属,不易发生变形

对于更加专业的镜架选择,可以参考 https://zhuanlan.zhihu.com/p/23626916 关于材质可以参考这篇

眼镜折射率

很多人,比如我,可能到眼镜店或者网上查找相关材料的时候就被这个镜片后面带着的 1.56,1.67 这个系数唬到了,其实这个系数就是光学玻璃的两个重要参数之一的折射率,另外一个系数阿贝数在下面提到。

折射率越高,透光率越低,色散越厉害,从成像质量讲低折射率的镜片更好。但是折射率高,镜片越薄,也相对来说越轻。

蔡司的阿贝数,用来衡量介质的光线色散程度,阿贝数越大,色散就越小,反之,阿贝数越小,则色散就越大,其成像的清晰度就越差。镜片的阿贝数与折射率有一定关系,材料的折射率越大,色散越厉害,即阿贝数越低。

https://zh.wikipedia.org/wiki/%E9%98%BF%E8%B4%9D%E6%95%B0

镜片并非很多销售人员说的折射率越高越好,要根据自身的情况进行选择。

折射率选择参考:(仅作为参考,不要机械对照)

折射率 度数
1.56 0 — 400°(-4.00D)
1.60 300°(-3.00D)—600°(-6.00D)
1.67 400°(-4.00D)—800°(-8.00D)
1.74 800°(-8.00D)以上

球面和非球面镜片的区别

球面镜片是指从球体上切下一部分带有凸面或凹面形状的镜片。由于球面镜片难以避免的会出现像差(特别是像散像差,像弯像差)等等现象,所以出现了补正球面镜片缺点的镜片——非球面(单面非球)。

非球面的设计,将镜片边缘像差减底修正了影像,解决视界歪曲等问题,使成像更自然,视野更宽阔,可以满足顾客的需求,同时,使镜片更轻、更薄、更平。

非球面镜片就是指在所有子午线上的具有相同的非圆截面的旋转面 (如抛物线)。其最大优点就是边缘视野无物象扭曲现象,镜片边缘比普通球面镜片薄 1/3 左右。

就目前而言,非球面镜片的确更接近裸眼的视觉,镜片边沿的变形也小一些,视野更大一点,已经是主流的配制了。而且非球面镜片已经非常成熟,价格上的差异很小。但并非每个人都能适应,不排除有些人的眼睛就只能适应球面镜片。

渐进片

在和眼镜店职员聊天的时候,有推荐过抗疲劳镜片,多焦点渐进片,这部分内容我并没有关心,对于我来说大部分时间是近距离看电脑,多焦点渐进,其实对我而言并不是那么有用,所以这部分也就略过了。

镜片品牌

依视路,蔡司,豪雅,精工,凯米,罗敦司得,性价比高的,明月、柯达、康耐特、凯米 等等,镜片的选择就依据个人经济承受能力而定吧,各种各样,便宜昂贵的镜片都有,在自己的经济能力范围内选择一个合适的镜片即可。

好的镜片主要是有三个要素组成:镀膜、材料、设计。

耐划伤(硬度高)、防水防油污(光滑)、贴合性好(不脱膜)

蔡司

德国品牌,1846 年创立

蔡司是国际上公认的最好的眼镜镜片,无可挑剔的第一,而价格,还算是比较贵的,但是有一些眼镜店,蔡司的镜片价格可以卖的比较低,如果找到了,还算是比较好的,能打到 5-6 折就笑吧。但是蔡司入门的 A 系列莲花膜镜片,耐脏性挺好,耐磨性不好,要耐磨,选钻立方膜,价格马上上几个台阶。

依视路

世界五大镜片中排名前三,法国品牌,由创建于 1849 年的依视(Essel)和 1931 年的视路(Silor)两大视光学巨头合并而成。

镜片突出特点:

膜层硬度高,耐磨性能卓著,不易刮伤,防水防油污效果优越。

主打产品推荐:

爱赞数码生活系列,钻晶 A4 防蓝光系列,万里路渐进多焦点系列。

尼康

日本品牌的镜片,品质没得说,只是价格贵,一般门店也很少提供。

豪雅

日本品牌,始创于 1941 年,世界五大镜片排名第二,第三的位置,日本对于工业做工,细节的把控是真的好。

镜片突出特点:

品质稳定,耐磨性能强,防反光性能优越。

主打产品推荐:

兰御膜防蓝光系列,锐美抗疲劳系列。

罗敦司得

德国品牌,始创于 1877 年。这个品牌有点贵,就略过了。

凯米

凯米属于韩国品牌,是韩国一家专业生产树脂镜片厂商品牌,相较于蔡司,依视路等要便宜很多,性价比高

镜片突出特点: 性价比高,品类齐全。

主打产品推荐:

U6 紫外线蓝光双防护系列,Magic Form 渐进多焦点系列。

总结

所以在了解这些知识之后,也就不会在配镜的时候被那么多的名词唬到了。针对自己的情况选择一款合适的眼镜也就没有那么的困难了,这里要感谢互联网上那么多无私分享的人,以上大部分内容都是在 Google 和知乎上搜索出来的内容,当然也有部分是我在路过眼镜店,和在配眼镜的时候和店员聊天所得。

有的时候买卖就是这么一个你情我愿的事情,原本就不是那么麻烦,但是总有些怀着鬼胎的人,让本来一个很普通的事情变得那么曲折。镜片好坏差异程度太大,所以也总有不良商家会以次充好,我之所以会总结这么多的内容,完全就是因为我对市场失去信心所致,我愿意花钱,但是我想要我花的钱值得,这也就是我说的买卖最平等的部分,配镜的店员根据经验给出一个在价格合理的范围内最合理的搭配,而配镜的人依据自己的能力支付并获得一个最值得的眼镜,这就完成了一笔交易。

最后,验光,镜片,加工都是一副好眼镜必不可少的环节,如果可能就找一些品质比较好的医院,或者眼科检查店进行检查。

reference


2018-09-15 眼镜 , 经验 , methodology

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]]
  • [[MTA]]
  • [[MDA]]

MUA

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

MTA

MTA 全称 Mail Transfer Agent,MTA 仅仅负责邮件的传输。常见的 MTA 有 Postfix, sendmail, [[exim4]]

MDA

[[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

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

Postfix 使用的默认端口为 25

Dovecot

[[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 , mailgun , email

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

电子书

最近文章

  • 在日本生活必备的应用收集整理 在上一篇梳理了一下如何 注册日区的 Apple ID ,其实主要是为了这一篇整理做准备,有很多的应用,如果不是日区就安装不了,并且如果要在日本生活有一些应用还是非常重要的,所以就提前梳理一下需要用到的应用。
  • 如何注册日区 Apple ID 以及初次登录日区 App Store 之前一直使用美区的账号,但突然发现有一些 App 在美区也没有,比如去日本经常需要用到的「乗換案内」就搜不到,美区里面尽是一些冒牌的,还穿插各种广告的应用,还有很多日本本地的一些应用也几乎都没有上架美区。
  • 《工作、消费主义和新穷人》读书笔记 怎么知道的这一本书 前些日子去线下逛书店的时候,在书店刚进门的架子上一眼就看到了这一本,或许是这本书的名字起得太吸引眼球了,但是拿起来之后看到作者才意识到这本书是之前刚刚读过的 [[现代性与大屠杀]] 的作者 [[齐格蒙 鲍曼]] 的著作,这更让我提起了兴趣。
  • 《活法》读书笔记 怎么知道的这一本书 这本书一直非常出名,[[稻盛和夫]] 的名字一直非常响亮,但我似乎对其没有任何了解,这一本书也是非常机缘巧合地在楼下的图书馆里面看到了。
  • Mastodon 站点管理:导入自定义表情包 [[Mastodon]] 实例可以允许站点管理员自定义整站上的表情包,管理的地址在 首选项-管理(Administration)-自定义表情(custom emojis) , 具体的页面地址是 https://instance.domain/admin/custom_emojis。