推荐 5 本书:2 季度中读到的好书

在第一季度,我在读过的书中选了 7 本我读着不错的进行记录,见【推荐 7 本书:1 季度中读到的好书】。如今第二季度过完了,延续上个季度的做法,在第二季度中读过的书中,有五本读起来不错,在这里推荐给大家。

-1- 《心若菩提》

第一本是曹德旺的自传《心若菩提》,这本书其实想看很久了,但一直没有翻开。我自己的感觉是读电子书所占的时间比较多时,看实体书就有些被遗忘,这本书电子版一直没上架,算是被其他电子书的阅读耽搁了。这个季度开始突然想起了在去年年底看过的《美国工厂》这个纪录片,然后就拿出这本书开始翻了起来,一通读下来之后,还挺有收获,对比电影我更喜欢这本书。

书中有不少地方蛮有启发,比如跟着父亲做生意,这在很早就有了的历练;在特殊时期解决各种配额那部分建立的人脉;还有在选择做什么的时候,利用标杆和与标杆之间的距离来丈量一番;在各地对老厂的收改中与地方的博弈等等。当然还有一些比较有趣的事,比如几次的求签问卜。整本书写的通俗易懂毫不晦涩,读起来很轻松,值得一读。

-2- 《中央帝国的军事密码》

第二本是郭建龙的《中央帝国的军事密码》,这是郭建龙帝国密码三部曲中的其中一本,另外两本分别是《中央帝国的财政密码》和《中央帝国的哲学密码》。我以每年一本的速度读完了这三本书。从书名可以看出,郭建龙从财政、哲学、军事三个方面对古代中国进行了解读,这三本书都值得一读。

读这本《中央帝国的军事密码》感觉没有另外两本书写的精彩,解读军事从地理与环境的角度出发但却没有提供地图让人读起来很吃力,我是配合在电脑大屏幕打开电子地图来一遍读一边对应到具体的地点。

虽然没有地图值得吐槽一下,但沿着地理的脉络一览古代中国的军事战争还是很有收获的。比如为什么是从关中开始,再到中原,再到南北对峙,在冷兵器时代,据险可守,从这点来看北方确实占据了很大的优势。

对于地图的吐槽另一面也表示当年地理知识学的是一塌糊涂,其实以前总是不知道学地理有什么用,现在想来是缺乏从整体的地理框架演变为具体的地域及地点,以及在这个框架下的国家和人民之间的关系,人毕竟生活在实际的地理空间,这些地理之间的纵横交错与环境的依托是不相同的,这之间的不同随着竞争在演变,到国家这一层的征战就依托这些具体的地理空间在进行。

-3- 《东晋门阀政治》

第三本是田余庆的《东晋门阀政治》,这本书我给打了五颗星,这是在第二季度我唯一给打了五颗星的书,对于两晋南北朝这段历史感兴趣的朋友,这本书不可错过。从书中的行文来看,田老师治学非常严谨,能够在阅读中感到田老师花费了很大的心血,对东晋这一百年的门阀政治阐述的非常清晰。

这本书我开始看是比较早的,但是看了开头一点后看不下去,断断续续尝试几次后就先把它放在一边了,这本书并不好读。为什么当时我选择读这本书呢?

很早以前,在我每次回忆起中学读历史的时候,记忆中对于两晋南北朝这段时间几乎没怎么提及,这让我一度认为这段时间非常短暂,但后来我才知道,两晋南北朝这段时间足足有 300 多年呢,这一点也不短暂,那么这段时间中国都在发生些什么呢?于是就产生了那么点好奇心,但这个好奇心并没有达到让我马上搞清楚它,但她就像埋在心里的一个引子,在读到的书中涉及到这段时期的内容时,这个引子就会浮现,在最近几年读的几本书中,郭建龙的帝国密码三部曲中从财政、哲学、军事三方面均有涉及到两晋南北朝,郭老师的书让我对这段事情大概有了个印象。

那这本书就是在读完《中央帝国的财政密码》后发现的,并准备看一看,但不知道为什么一直看不进去,直到最近又重新拿起来开始看,这一看就有点味道了,气味对了也就顺畅地一气读完了。随着阅读,对这段时间开始有了一些初步的认识了,就目前来看,这段时期是挺重要的一段时期,基本上在魏晋南北朝完成了民族和文化的大融合,为下一次在一起提供了基础。

东晋门阀政治从「王与马,共天下」始,终于「王与马,失天下」,只不过是此王非彼王,这一百年挺有意思,皇帝垂拱,士族当权,流民出力,能维持这么久也实属稀罕,只能说这三者刚好构成了一个平衡状态。在这种动态的平衡中,南方完成了广阔的土地开发和儒道释等文化的碰撞与扩展,北方则带来了各民族的普遍进步与大融合。这就为下一次的大一统做好了准备,历史在变化之中同时存在着历史的延续。

这本书给我的最大感触是作者并没有预设什么,他只是尽力的寻找历史内部真正存在的各种线索,细致考证,然后陈述事实,如果找不到那就先存疑放着,这种从细微的着手,层层推进的方式读起来虽然不好读但读进去也是酣畅淋漓。

-4- 《机遇之门》

第四本是多夫·莫兰的《机遇之门》,多夫·莫兰是以色列著名的企业家、发明家和投资人。USB 闪存盘就是他的发明。推荐这本书是因为作者写的小故事我挺喜欢。

比如「百门模型」的故事,这个故事大概是这样的,你处在有 100 扇紧闭着的门的虚拟世界里,你十分想离开这个虚拟世界,打开每扇门的概率互不影响,对于每一扇门你有 1% 的机会能打开门,有 99% 的可能性会碰壁,…… ,那么逃离这个虚拟世界的概率是多少?

类似这样的小故事不少,这些小故事背后传递了作者对创业的理解,这个百门模型就是作者理解的创业世界。一旦开始创业,你就算是进入了一个包含 100 扇门的世界,你争分夺秒地寻找那扇能够向你敞开的门,从而突破并向前迈进…… 如何开启创业的机遇之门呢?

当然还有作者一路创业历程中的发生的各种故事,通过这些故事集,作者把他理解的企业家精神、市场、团队、困境以及各种心境完整地呈现给了读者。

整本书读起来既轻松,又很有启发,不管是不是创业我觉得都可以读一读。

-5- 《纳瓦尔宝典》

第五本是埃里克·乔根森汇编的《纳瓦尔宝典》,这本书最近比较火,我估计应该还会持续的火一阵子。这本书跟《穷查理宝典:查理·芒格智慧箴言录》有点类似。如书名,这本书的主要内容是整理汇编了一个叫纳瓦尔的人对于财富和幸福的一些观点,也可以理解为纳瓦尔在生活中提炼的智慧结晶。

关于纳瓦尔其人我就不作介绍了,感兴趣的人可以到网上探索一番。上面说到这本书最近比较火其实不大对,更确切的说应该是纳瓦尔的输出的内容先火起来了,以至于有人想把这些火热的内容集结成书。纳瓦尔的输出之所以火,在我看来有两方面的原因,一方面是纳瓦尔在网络上发表的观点和言论所涉及的内容正是如今大部分人所想要得到的财富和幸福。另一方面是纳瓦尔通过输出这些观点和言论告诉大家,你这么做就能得到财富并收获幸福,你一看说的还非常在理。

尽管每个人因自身境况不见的就能学会,但是纳瓦尔毕竟给出了获得财富和幸福的指南,而且说实在的,读完后确实给人带来启发。比如纳瓦尔对于杠杆的观点输出,如果早看到可能就会早思考和实践,在他眼里,人如果按照二分法的话,可以分为利用了杠杆的人和没利用杠杆的人,进一步指出劳动力、资本、复制边际成本为零的产品这些都是杠杆,而我们选择工作也优先要选择存在杠杆的工作,因为只有最大化地发挥杠杆的效应,才能利用有限的时间和体力产生巨大的影响力。

书中还有很多地方能够给人带来启发,在读的时候时不时心里会说,嗯说的真对,我怎么从来没这么想过,这可能就是人与人之间的差别所在,不管怎么说向有智慧的人或成功的人学习总是好的。

看这些智慧者的输出,有一个共同的地方,就是他们都非常重视基础性的知识,纳瓦尔也同样如此,我想在这方面也同样值得我们学习。

本文首发于我的微信公众账号「时间易逝」,欢迎订阅我的微信公众账号
在微信中搜索「doevents」或用微信扫描页面右上方二维码可订阅我的微信公众账号

npm 学习笔记

npm[1] 是 JavaScript 包管理工具,它类似 Java 中的 Maven , Gradle , Python 中的 pip 。

npm 随着 Node.js 一起发布,在安装 Node.js 的时候,npm 也会随着 Node.js 的安装一起安装到电脑上。 npm 作为一个包管理工具,其自身更新比较频繁,它的最新版本也可以独立进行更新。

# 在终端执行下面的命令即可更新到 npm 的最新版本
npm install npm@latest -g

npm 安装一个包都命令是 npm install package_name , 执行此命令后,会在当前目录安装 package_name 包,并创建一个 node_modules 的目录,下载要安装的包到这个目录中。

npm 是通过 package.json 这个文件管理包到,通过这个文件对包的信息进行定义,以便在团队协作的时候对包的使用上统一一致。使用 npm install 命令安装的是包的最新版本,我们日常在进行的项目实际上不一定总是使用最新的版本,这时候同样可以通过对 package.json 文件进行配置获取指定版本的包。

一个 package.json 至少要包括包名和版本信息。

{
    "name" : "package_name",
    "version" : "1.0.0"
}

如何创建一个 package.json 文件呢? 答案是使用 npm init 命令,然后逐一回答问题即可完成创建。如果不想回答问题,可以使用 npm init –yes 命令创建默认的 package.json ,然后再修改之。

{
    "name" : "npm",
    "version" : "1.0.0",
    "description" : "",
    "main" : "index.js",
    "scripts" : {
        "test" : "echo \\"Error: no test specified\\" && exit 1"
    },
    "keywords" : [],
    "author" : "",
    "license" : "ISC"
}

以上就是默认创建的文件。要安装包,就要有下载的地址,同样也可以在 package.json 中用字段 repository 指定。

{
    "type" : "git",
    "url" : "https://github.com/example/package_name.git"
}

还有一些其他的字段可供设置,比如主页地址,作者邮件等等。一个包的 name 命名有一定的规范,包名需要使用小写字母,不允许有空格,可以使用下划线和横线。对于常用不太变化的字段值,可进行初始化设置,这样就不用每次都输入这些信息了。

# 初始化设定作者 email
npm set init.author.email "author@example.com"
# 初始化设定作者姓名
npm set init.author.name "Eric"
# 初始化设定许可协议
npm set init.license "MIT"

在使用 npm install 命令安装包的时候,会读取 package.json 文件来安装模块。模块在有时候有两种类型,一种是面向生产环境的,另一种是面向开发环境的,这时候可通过在命令后面添加 --save--save-dev 参数指定安装。

如果要更新包,使用 npm update 命令,卸载包用 npm uninstall package_name ,如果卸载的同时想要一并删除 package.json 文件,添加参数 --save

# 卸载 package_name 包,并删除 package.json 文件
npm uninstall --save package_name

上面说了,在默认情况下,包安装命令会将包安装在当前目录下,这只能在当前目录下使用。如果要使用一些全局的包,在安装的时候可以添加 -g 参数来安装,同理更新也是如此,卸载一个全局包也是如此。

# 安装 package_name 包为全局包
npm install -g package_name
# 更新全局包
npm update -g
# 卸载全局包
npm uninstall -g package_name

如果想知道当前都有哪些包需要更新,可以使用下面的命令查看。

# 查看哪些全局包需要更新
npm outdated -g --depth=0

包(package)是一个被 package.json 文件描述的文件或目录。模块(modules)是可以被 Node.js 引用的文件或目录。一个 Node.js 模块就是一个可以发布到 npm ,供其他开发者使用的模块。

如何创建一个 Node.js 模块呢?

  • 创建一个 package.json 文件,将模块信息添加到文件中;
  • 创建一个模块被引入就加载的文件,即 main 字段中指定的文件,默认为 index.js ,在此文件中将一个函数值赋给 exports 模块。
exports.printMsg = function() {
    console.log("This is a message from the first package.")
}

npm 的配置文件是 npmrc,项目级别的在项目所在目录下 .npmrc , 用户级别的在 ~/.npmrc , 全局的在 /etc/npmrc , 内置的配置文件在 npm 安装目录下。配置信息的优先级别为命令行参数级别最高,其次是环境变量 PATH ,再次是这些配置文件,要查看 npm 的配置信息,执行如下命令即可。

npm config list -l

默认下载包的源来自 npm 官网,如果感觉下载速度不理想,我们可以配置一些国内的镜像站点来加快下载速度。

我们可以在安装包的时候指定镜像源,也可以写到配置文件中。

# 在安装的时候指定「淘宝镜像」
npm install express --registr=https://registry.npmmirror.com
# 写到配置文件中
npm config set registry <https://registry.npmmirror.com>

# 查看配置是否成功
npm config get registry # npm info express 也可以

对于 npm 的学习暂时先告一段落,可能很长一段时间只是处于使用 npm 的阶段,了解以上的内容基本上应该可以开始使用了,使用中遇到问题随时查阅帮助解决即可。我看还涉及到发布自己的包等内容,这就是 long long after 以后的事情了。


本文中引用的资源
1:npm 的官方网站是:https://www.npmjs.com/

在 Ubuntu 20.04 LTS 上安装 SSL 证书

今天抽时间给自己的几个网站加上了 SSL 证书,SSL 证书是从腾讯云上申请的免费 SSL 证书,有效期为一年,从申请到配置比较顺利。

申请&下载

登录到腾讯云[1]上,搜索产品 SSL,找到 SSL 证书产品[2],进入该产品首页,也就是你的 SSL 证书概览,在这里左边是导航菜单,中间的内容区会显示证书申请状态和监控状态等概览信息,右边则是一些帮助信息,如下图所示。

在左边导航菜单中选择「我的证书」,会显示当前你申请的所有的证书,在中间功能按钮区选择「申请免费证书」按钮,如下图中红色箭头所指的按钮,即可开始进行免费证书的申请。

首先在弹出的页面上选择证书类型,免费版的目前只有一个,可以看到有效期只有一年,直接确定就行。

接下来是第一步提交材料,需要填写一些信息,要提交的资料主要是证书要绑定的域名以及申请邮箱,填写对应的信息即可,然后进入第二步「选择验证方式」。

第二步选择验证方式主要是验证你之前填写的绑定证书的域名是你的,使用推荐的「DNS验证」即可,选择下一步按钮进入第三步「验证域名」。

第三步验证域名需要你在你的域名中添加一条解析记录,在你的域名托管平台中找到你要绑定证书的域名,然后在域名解析中添加一条 CNAME 的解析记录,在解析记录中对应填写下图中的「主机记录」和「记录值」,解析记录添加完毕后,选择「查看域名验证状态」看看是否完成验证,如果完成验证,剩下就是等待证书的签发,基本上不到 1 分钟就能拿到发放的证书了。

这时候通过左边导航菜单回到「我的证书」菜单,如果上面操作无误,就能看到自己的证书了,在「我的证书」中签发下来的证书上选择「下载」,弹出如下图所示的页面。证书下来后我们需要安装到服务器上,所以我们可以先按照我们的服务器类型把证书下载下来,到这里我们的证书就已经申请完毕了,剩下的就是把下载的证书安装到服务器上。

上传&安装

我的服务器系统是 Ubuntu 20.04 ,Web 服务器软件使用的是 Apache ,所以在下载证书的时候选择服务器类型为 Apache 类型的下载,下载后解压缩,有四个文件,我们以申请证书的域名是 example.com 举例来说(在实际使用时将域名替换为自己到域名即可),那这四个文件分别是:

  • root_bundle.crt 证书文件
  • example.com.crt 证书文件
  • example.com.key 私钥文件
  • example.com.csr 系统在线生产文件,这个安装的时候不用

将证书文件和私钥文件拷贝到服务器上一个目录中,我是拷贝到了 /etc/apache2/ssl/example.com/ 这个目录下面,可以使用 scp 命令进行拷贝,也可以使用 scp 工具拷贝。

拷贝完成后,用 SSH 远程登录服务器,在虚拟主机配置文件中添加证书信息,比如在 /etc/apache2/sites-available 目录中,找到要配置 SSL 站点的配置文件,例如:example.com ,则对应到该目录下的 example.com.conf 文件,将配置信息修改为如下信息即可。

<VirtualHost *:443> 
	ServerAdmin admin@example.com 
	ServerName example.com 
	ServerAlias www.example.com 
	DocumentRoot /var/www/html/example.com/public_html/ 
	DirectoryIndex index.html 
	
	# SSL configuration 
	SSLEngine On 
	SSLCertificateFile /etc/apache2/ssl/example.com/example.com.crt 
	SSLCertificateKeyFile /etc/apache2/ssl/example.com/example.com.key
	SSLCACertificateFile /etc/apache2/ssl/example.com/root_bundle.crt 
	
	# Log files 
	ErrorLog /var/www/html/example.com/log/error.log 
	CustomLog /var/www/html/example.com/log/access.log combined 
</VirtualHost>

<VirtualHost *:80> 
	ServerName example.com 
	ServerAlias www.example.com 
	Redirect permanent / https://example.com/ 
</VirtualHost>

完成站点配置文件修改后, 在命令行运行 a2enmod ssla2ensite example.com 启用这个修改后的配置,紧接着 service apache2 restart 重启 Apache 服务器,这样证书就安装完了,再访问网站时域名前就能看见小锁了。


本文中引用的资源:

1: 腾讯云:https://cloud.tencent.com/
2: 腾讯云 SSL 证书产品:https://console.cloud.tencent.com/certoverview

– EOF –

阅读时眼睛疲劳怎么办,或许可以让设备朗读给我们听

随着年龄的增长,眼睛在阅读东西的时候可能就有些费劲,这时候对于电子屏幕上要读的文字可能就有通过听来获取的需要。前阵子一个朋友向我询问,问这种情况有没有好的解决方法,在大概聊了一下他的使用场景之后,我给他提供了一个解决办法,基本上解决了他的日常使用。

后来我又想了想,这就是在某种场景下,有将文字转成语音作为输入的需求。也就是将信息获取的方式由视觉系统转变为听觉系统,除了视障者之外,视觉正常的人在日常很多场景下也是有这个需求的,比如在通勤的路上、在开车的途中、在家务劳作的时候等等。

对于这方面的需求,目前一方面有专门做音频内容的产品,另一方面是很早就有的将文字转换成语音进行朗读的产品,它基于文字转语音(Text-To-Speech)这个系统,属于语音合成这个范畴,我最早接触的是微软开发的,如果没记错的话在 Windows 95 系统上就有,记忆中当时听来那是相当的生硬,早期的开发中在语音播报的场景中有一些应用。

专门基于音频内容的产品其音频内容是固定的,取决于音频内容本身和你的选择,如收音机、播客或使用 U 盘拷贝的音频内容,而这些音频内容一般由真人操作,实时或提前制作好供使用者选择。而文字转语音就比较个性化了,取决于你想让它读什么文字材料,然后针对这个文字材料进行实时的语音合成,通过声音设备输出,这就像直播中的主持人,只不过真人换成了机器设备。

早些年听机器朗读特别的生硬,基本上丝毫感受不到感情,近几年倒是没有尝试,趁着这次朋友的询问机会,最近断断续续的在各个地方再次做了一番尝试,针对它们提供的服务及其效果,在此做个记录。

我尝试的设备主要有手机和电脑,这也是目前大部分人日常使用的设备。手机两部,分别是 iPhone 和华为荣耀,两台笔记本电脑,分别是 Macbook Pro 和 Windows 笔记本,这也是我自己日常使用的设备。

1. 手机

1.1 利用手机自带功能
iPhone

在 iPhone 中,如要进行语音阅读屏幕文字,可通过 设置 – 辅助功能 – 朗读内容 进行设置,有两个选择,操作如图简洁明了,如下图所示。

当打开朗读所选项和朗读屏幕这两项功能后,如下图所示,即可按照其下方的提示操作进行内容的朗读,比如在选择文本时,会出现「朗读」按钮,双指从屏幕顶部向下轻扫就开始屏幕内容朗读了,我分别进行了尝试,比以前好很多,可能还有不小的提升空间,但听起来已经可以接受了。打开这两项功能后,进一步会出现「语音控制器」和「高亮显示内容」两个选项,打开前者会在屏幕上浮动一个小按钮,可在任何想要朗读的地方使用这个按钮来操作。高亮显示内容打开后则在朗读的时候以高亮的方式告诉你当前读到哪里了。

整体来讲 iPhone 上的朗读操作起来还是比较容易的。特别提一句,在声音中可以选择不同的语言和当下语言中的声音,在这里我选择的是中文下的「Siri 声音 2(中国大陆)」,可供选择的声音还有「婷婷、语舒和 Siri 声音1」。屏幕中间的工具条是「语音控制器」浮动按钮展开后的内容,可进行翻页和调整朗读语速。

总体来说,利用 iPhone 手机系统提供的功能,还是能够很容易地操作,并朗读屏幕上的内容,听起来也还算可以,如果你有这方面的需要,可以试着把玩一番。

Android 系列

这个就比较遗憾了,我在我的手机中找到了系统提供的功能,但我在我的手机上并没有试出来我所期待的功能,我通过「设置」->「智能辅助」->「无障碍」->「随选朗读」进行设置,但是我设置完后始终就是不能让它能够朗读起来,遂作罢。

虽然没能体验成功,我还是浏览了一番它使用的引擎,用的是讯飞语音引擎,坊间一直有说讯飞家的语音出类拔萃,可惜未能在此体验一番。

1.2 利用手机上的 App

系统提供的是全局的功能,但在我们平常使用的时候更多的是基于一些少量的场景,对于将文字转换成语音输出的这个需求,更多的是对应一个想要朗读的文档,比如 Word、PDF 文档,或者是电子书,这时候可以通过日常使用的 App 来解决这个问题,这里我推荐微信读书这个 App,微信读书支持导入文档,支持 txt,pdf,epub,doc,docx,mobi,azw3 等格式,将这些文档导入微信读书后,在阅读的时候,手指单点屏幕中央,在弹出的功能按钮中,选择听按钮就可以开启朗读功能。

这比较适合要朗读的文档篇幅比较长,因为在导入文档到手机中时大概率要在电脑上完成,这需要额外花费一些时间,但完成后使用就比较简单了。

在手机上还有其他的 App 也提供这个功能,比如在微信中,可以通过设置「关怀模式」,然后开启「听文字消息」功能,也可以听消息。

在系统提供的屏幕朗读和微信读书的结合下,我想基本上就可以解决将文字朗读出来的需要了,当然如果有更好的 App ,如果恰巧看到这篇记录,也请留言告诉我。

2. 电脑

电脑的操作系统同样也提供朗读的系统功能,比如在 MacBook Pro 中,可以通过「系统偏好设置」->「辅助功能」->「朗读内容」 来设置。而在 Windows 系统的电脑中,比如 Windows 10 提供系统自带有屏幕朗读功能,可通过 「Windows 设置 – 轻松使用 – 讲述人」 打开讲述人即可。

对于电脑这个强大的生产力工具来说,系统提供的功能我们可能用不到就解决了我们的问题,比如我们常用的浏览器(Chrome、Safari、Edge)基本上能打开大部分格式的文本文件,而这些浏览器本身或借助系统提供朗读的功能,就能完成我们需要的朗读功能。所以在电脑上大部分的将文档朗读出来都可以采用用浏览器打开文档,然后朗读即可。

这里面最为推荐的就是 Microsoft Edge 这个浏览器,右键鼠标单击,在弹出的菜单中选择「大声朗读」,就可以听到当前打开文档的文字朗读了,这个是我听的效果最好的(如下图中我打开了鲁迅先生的《呐喊》这个 txt 文件进行大声朗读,效果不错)。在 Mac 中我用浏览器(不论是其自带的 Safari 还是 Chrome)体验的效果都不理想,在这里的体验应该是借助了系统的功能,在 Chrome 中可以通过安装朗读服务插件来提高体验,这部分我没有尝试。

另外一些专用的软件本身也提供朗读功能,比如通常用的 Word 里面就有朗读功能。

一圈体验下来,除了 Android 手机上这个用讯飞语音引擎的我没体验到之外,体验最佳的就是用 Edge 进行的朗读,这也印证了我从旁听来的评价「微软的引擎好」,我觉得也理所应当,毕竟对于一个在 20 多年前就推出了这方面产品的企业,这么多年下来的积累与沉淀,理应提供不错的体验。

对于现有的工具或产品来说,如果要在手机上想把文字朗读出来,目前我比较推荐的是将文档导入到微信读书中,然后用微信读书中的听功能来听。如果是在 Windows 电脑上,我比较推荐直接使用 Edge 浏览器打开文档,使用其提供的大声朗读功能来听。

还有没有其他的方式呢,答案是有的,如果懂得编程,动手能力又强,可以使用微软提供的文字转语音应用程序编程接口(TTS API)[1] 用程序来实现自己的需求。不过对于大部分人来说,以上提供的方法已经足够自己使用,虽然跟真人还有些差距,但在眼睛疲劳又想听一听文档时,不妨一试。


[1]: https://azure.microsoft.com/en-us/services/cognitive-services/text-to-speech/#overview


本文首发于我的微信公众账号「时间易逝」,欢迎订阅我的微信公众账号
在微信中搜索「doevents」或用微信扫描页面右上方二维码可订阅我的微信公众账号

Delphi 7 操作 MySQL 数据库一例

Delphi 7 在现在生产环境中的开发基本已经绝迹,所剩不多的人员也主要用于旧系统的维护与小功能的升级,还有很少的一部分人沿用旧有的技能线升级到 XE 等后续版本继续完成日常的开发工作。更多的系统要么升级到了 .NET 体系 ,要么就是用了 Java 体系。但不可否认的是在 Windows 桌面软件的开发的某些场合中,其实用它还是蛮快的。

一个项目最初的需求跟数据库毫无关系,在考虑需求的基础上选择了 Delphi 7 ,这样发布一个独立的可执行程序,依赖很少,使用比较方便,但需求总是变化的,有了使用数据库的需求,把单机的应用变成了一个网络的应用,以往在 Windows 系统上一般用微软自家的 SQL Server 多一些,但现在机器上没有,MySQL 倒是现成的,于是决定用它了。

Delphi 7 开发的快速性在于其基于组件的丰富性,在早期好像用过 MySQL,用的是基于 ODBC 的 MySQL 驱动,但相当不好用,具体不好用的细节倒是忘记了,想着这么些年过去了,有没有与时俱进的组件呢,搜了一下还真发现了一个,名称是 ZeosLib ,看介绍挺强大,几乎支持所有的数据库,用了一下除了有一点通常都会出现的编码的坑之外还不错,把基本的使用做个记录归档以便以后使用。

具体的操作系统是 Windows 10 家庭中文版,版本号是 20H2,操作系统内部版本是 19042.928,用的数据库是 MySQL Community Server 8.0.17 for Win64 on x86_64 。

我用的是 ZeosLib 是 7.2.10,可通过网址 https://sourceforge.net/projects/zeoslib/ 下载获得。

1. 组件的安装

这里只说 Delphi 7 的安装,下载组件压缩文件后解压缩 zeosdbo-7.2.10-stable.zip ,解压缩后的目录下有三个文件夹和两个文件,在其中的找到 packages 文件夹,进入该文件夹后找到 delphi7 文件夹进入,鼠标左键双击 ZeosDbo.bpg 文件,在 Delphi 7 打开后选择菜单栏 Project 中的 Compile All Projects 菜单项,编译完成后选择 OK 按钮。在 Library Path 中增加该组件的路径,路径指向 delphi7 文件夹下的 build 文件夹。在 delphi7 文件夹中找到 ZComponentDesign.dpk 文件,鼠标双击打开,选择 install ,这样组件就会安装好了。

2. 数据库的访问与操作

MySQL 数据库的安装就不再详述了,在 Windows 系统上安装也比较简单,通常一路「下一步」即可安装完毕。我们这里写一个简单的登录示例,成功登录后记录一条登录信息,大概梳理一下流程。

1. 用户打开程序;
  1.1. 如果发现没有数据库配置信息,启动数据库配置界面;
  1.2 如果有数据库配置信息,启动用户登录界面;
2. 用户完成数据库连接配置
	……
3. 用户完成登录
	……
4. 记录登录信息

数据库的准备工作

# 数据库脚本

# 创建 examples 数据库
CREATE DATABASE IF NOT EXISTS `examples` 
DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

# 创建用户数据表
CREATE TABLE `users` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `loginname` varchar(50) COLLATE utf8mb4_general_ci NOT NULL,
  `pwd` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,
  `isadmin` tinyint(1) NOT NULL DEFAuLT '0',
  PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

# 创建用户登录信息表
CREATE TABLE `user_logins` (
  `id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `login_desc` varchar(150) COLLATE utf8mb4_general_ci NOT NULL,
  `login_time` datetime NOT NULL,
  PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

在 Delphi 7 中新建一个 Application ,再添加一个 Data Module 和两个 Form,将 Data Module 命名为 DataBox,将 Application 自带的 Form 和添加的两个 Form 分别命名为 frm_main 、 frm_login 、frm_dbset,保存工程文件为 LoginExample ,代码如下。

LoginExample.dpr 工程文件

program LoginExample;

uses
	Forms,
	u_main in 'u_main.pas' {frm_main},
	u_databox in 'u_databox.pas' {DataBox : TDataModule},
	u_login in 'u_login.pas' {frm_login},
	u_dbset in 'u_dbset.pas' {frm_dbset};

{$R *.res}

begin
	Application.Initialize;
	Application.CreateForm(TDataBox, DataBox);
	if show_FormLogin then
	begin
		Application.CreateForm(Tfrm_main, frm_main);
	end;
	Application.Run;
end.

u_databox.pas 单元文件

unit u_databox;

interface

uses
	Windows,SysUtils, Classes, ZAbstractConnection, ZConnection, DB,
  ZAbstractRODataset, ZAbstractDataset, ZDataset,Registry,
  Forms, DCPcrypt2, DCPsha256,Dialogs;

type
  TDataBox = class(TDataModule)
    ZQOper: TZQuery;
    ZC: TZConnection;
    ZQLogs: TZQuery;
    DCP_sha2561: TDCP_sha256;
    procedure DataModuleCreate(Sender: TObject);
  private
    { Private declarations }
    FDBHost : String;
    FDBPort : Integer;
    FDBName : String;
    FDBConUser : String;
    FDBConPwd : String;
  public
    { Public declarations }
    procedure GetDBConStr;
    function GetSha256(s:String):String;
  end;

var
  DataBox: TDataBox;
  OperID: Integer;
  Operator: string;
  LogsID:string;
  CurDir:string;
  isAdmin:Boolean;

procedure WriteLogs(sOper:Integer;funid:Integer;funName:string);


implementation

uses u_dbset, DateUtils;

{$R *.dfm}

procedure WriteLogs(sOper:Integer;login_desc:string);
begin
  with DataBox.ZQLogs do
  begin

    Close;
    SQL.Clear;
    SQL.Add('INSERT INTO user_logins(user_id,login_desc,log_time) VALUES(:uid,:desc,:logtime)');
    ParamByName('uid').Value:=sOper;
    ParamByName('desc').Value:=funName;
    ParamByName('logtime').Value:=Now;
    ExecSQL;
  end;
end;


procedure TDataBox.GetDBConStr;
var
  myReg : TRegistry;
begin
  myReg := TRegistry.Create;
  with myReg do
  try
    RootKey := HKEY_CURRENT_USER;
    if OpenKey('SOFTWARE\ExampleProg\DBConParam\', False) then
    begin
      FDBHost := ReadString('DBIP');
      FDBPort := StrToInt(ReadString('DBPort'));
      FDBName := ReadString('DBName');
      FDBConUser := ReadString('DBUser');
      FDBConPwd := ReadString('DBPwd');
    end;
  finally
    myReg.CloseKey;
    myReg.Free;
  end;
end;

procedure TDataBox.DataModuleCreate(Sender: TObject);
begin
  GetDBConStr;
  if FDBHost = '' then
  begin
    Application.CreateForm(TfrmDbSet, frmDbSet);
    frmDbSet.ShowModal;
  end;
  GetDBConStr;
  with ZC do
  begin
    Disconnect;
    Protocol := 'mysql';
    LibraryLocation := ExtractFilePath(Application.ExeName)+'libmysql.dll';
    HostName := FDBHost;
    Port := FDBPort;
    User := FDBConUser;
    Password := FDBConPwd;
    Database := FDBName;
    Connect;
  end;
  with ZQOper do
  begin
    Close;
    SQL.Text := 'SELECT Count(*) as OperCount FROM users';
    Open;
    if FieldByName('OperCount').Value = 0 then
    begin
      Close;
      SQL.Text := 'INSERT INTO users(loginname,name,pwd,isAdmin) VALUES(:loginname,:name,:pwd,:isAdmin)';
      ParamByName('loginname').Value := 'admin';
      ParamByName('name').Value := 'admin';
      ParamByName('pwd').Value := GetSha256('admin'); //sha256 admin
      ParamByName('isAdmin').Value := 1;
      try
        ExecSQL;
      finally
        Close;
      end;
    end;
  end;
end;

function TDataBox.GetSha256(s: String): String;
var
  Hash : TDCP_sha256;
  Digest : array[0..31] of byte;  
  Source : String;
  i : Integer;
  str1 : String;
begin
  Source := s;  //get s string sha256

  if Source <> '' then
  begin
    Hash := TDCP_sha256.Create(nil);  //create the hash
    Hash.Init;
    Hash.UpdateStr(Source);
    Hash.Final(Digest);
    str1 := '';
    for i:=0 to 31 do
      str1 := str1 + IntToHex(Digest[i],2);
  end;
  Result := str1;
end;

end.

u_dbset.pas 单元文件

unit u_dbset;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ExtCtrls, Mask, Registry,ZAbstractConnection, ZConnection;

type
  TfrmDbSet = class(TForm)
    Label2: TLabel;
    Label3: TLabel;
    Label4: TLabel;
    Label5: TLabel;
    Bevel1: TBevel;
    Label6: TLabel;
    lblStatus: TLabel;
    edt_ServerName: TEdit;
    edt_DBName: TEdit;
    edt_ConnUser: TEdit;
    edt_Pwd: TEdit;
    BitBtn1: TBitBtn;
    BitBtn2: TBitBtn;
    BitBtn3: TBitBtn;
    Panel1: TPanel;
    Label1: TLabel;
    Image1: TImage;
    Panel2: TPanel;
    Label1: TLabel;
    edt_Port: TEdit;
    ZC: TZConnection;
    procedure FormKeyUp(Sender: TObject; var Key: Word;
      Shift: TShiftState);
    procedure BitBtn3Click(Sender: TObject);
    procedure FormShow(Sender: TObject);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure BitBtn1Click(Sender: TObject);
    procedure ZCAfterConnect(Sender: TObject);
    procedure ZCAfterDisconnect(Sender: TObject);
    procedure RzBitBtn2Click(Sender: TObject);
  private
    { Private declarations }
    DBConStr: string;
    FMacineName: string;
    FDBName: string;
    FPwd: string;
    FConnUser: string;
    procedure SetDBConStr;
  public
    { Public declarations }
  end;

var
  frmDbSet: TfrmDbSet;

implementation

uses u_databox,comobj;

{$R *.dfm}

{ TfrmDbSet }



procedure TfrmDbSet.SetDBConStr;
var
  myReg:TRegistry;
begin
  myReg:=TRegistry.Create;
  with myReg do
  try
    RootKey:=HKEY_CURRENT_USER;
    if OpenKey('SOFTWARE\ExampleProg\DBConParam\',True) then
    begin
      WriteString('DBIP',edt_ServerName.Text);
      WriteString('DBPort',edt_Port.Text);
      WriteString('DBName',edt_DBName.Text);
      WriteString('DBUser',edt_ConnUser.Text);
      WriteString('DBPwd', edt_Pwd.Text);
    end;
  finally
    myReg.CloseKey;
    myReg.Free;
  end;
end;

procedure TfrmDbSet.FormKeyUp(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin
  if (Key = VK_F8) and (ssCtrl in Shift) then
  begin
    edt_ConnUser.Color := clWhite;
    edt_ConnUser.Enabled := True;
    edt_ConnUser.SetFocus;
  end;
end;

procedure TfrmDbSet.BitBtn3Click(Sender: TObject);
begin
  Close;
end;

procedure TfrmDbSet.FormShow(Sender: TObject);
begin
  edt_ServerName.SetFocus;
end;

procedure TfrmDbSet.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  ZC.Disconnect;
  Action:=caFree;
  frmDbSet:=nil;
end;

procedure TfrmDbSet.BitBtn1Click(Sender: TObject);
begin
  with ZC do
  begin
    Disconnect;
    Protocol := 'mysql';
    LibraryLocation := ExtractFilePath(Application.ExeName)+'libmysql.dll';
    HostName := edt_ServerName.Text;
    Port := StrToInt(edt_Port.Text);
    User := edt_ConnUser.Text;
    Password := edt_Pwd.Text;
    Database := edt_DBName.Text;
    Connect;
    RzBitBtn2.Enabled := True;
  end;
  lblStatus.Caption := '连接状态:测试连接成功,已连接!';
end;

procedure TfrmDbSet.ZCAfterConnect(Sender: TObject);
begin
  lblStatus.Caption := '连接状态:已连接!';
end;

procedure TfrmDbSet.ZCAfterDisconnect(Sender: TObject);
begin
  lblStatus.Caption := '连接状态:未连接!';
end;

procedure TfrmDbSet.BitBtn2Click(Sender: TObject);
begin
  SetDBConStr;
  Close;
end;

end.

u_login.pas 单元文件

unit u_login;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, DB, ZAbstractRODataset, ZAbstractDataset, ZDataset;

type
  Tfrm_login = class(TForm)
    Label1: TLabel;
    edt_pwd: TEdit;
    cbo_username: TComboBox;
    Label2: TLabel;
    btnLogin: TButton;
    ZQUser: TZQuery;
    procedure FormCreate(Sender: TObject);
    procedure btnLoginClick(Sender: TObject);
    procedure FormCloseQuery(Sender: TObject; var CanClose: Boolean);
    procedure cbo_usernameKeyPress(Sender: TObject; var Key: Char);
    procedure edt_pwdKeyPress(Sender: TObject; var Key: Char);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure ZQUserAfterOpen(DataSet: TDataSet);
    procedure edt_pwdEnter(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

function Show_FormLogin:Boolean;

implementation

uses u_databox;

var
  PasswordOK : Boolean;

{$R *.dfm}

function Show_FormLogin:Boolean;
var
  frm_login : Tfrm_login;
begin
  PasswordOK := False;
  frm_login := Tfrm_login.Create(Application);
  try
    frm_login.ShowModal;
  finally
    frm_login.Free;
  end;
  Result := PasswordOK;
end;

procedure Tfrm_login.FormCreate(Sender: TObject);
begin
  ZQUser.Close;
  ZQUser.Open;
end;

procedure Tfrm_login.btnLoginClick(Sender: TObject);
begin
  if ZQUser.Locate('loginname;pwd', 
	VarArrayOf([cbo_username.Text, DataBox.GetSha256(edt_pwd.Text)]), [loCaseInsensitive]) then
  begin
    OperID := ZQUser.FieldByName('id').AsInteger;
    Operator := cbo_username.Text;
    isAdmin := (ZQUser.FieldByName('isAdmin').AsInteger=1);
    Application.MessageBox(PChar(Operator + '登录成功'), '登录提示', MB_OK+MB_ICONINFORMATION);
    PasswordOK := True;
    WriteLogs(OperID,'登录');  
    if PasswordOK then
      Close;
  end
  else
    Application.MessageBox('用户名或密码不正确,登录失败,如忘记密码,请联系管理员!', '错误提示', MB_OK+MB_ICONWARNING);
end;

procedure Tfrm_login.FormCloseQuery(Sender: TObject;
  var CanClose: Boolean);
begin
  if not PasswordOK then
  begin
    CanClose := Application.MessageBox('你真的要退出该软件吗?', '信息提示', MB_YESNO+MB_ICONQUESTION)=IDYES;
    WriteLogs(OperID, '退出系统');
  end;
end;

procedure Tfrm_login.cbo_usernameKeyPress(Sender: TObject; var Key: Char);
begin
  if Key = #13 then
  begin
    edt_pwd.Text := '';
    edt_pwd.SetFocus;
  end;
end;

procedure Tfrm_login.edt_pwdKeyPress(Sender: TObject; var Key: Char);
begin
  if Key = #13 then
  begin
    btnLoginClick(Self);
  end;
end;

procedure Tfrm_login.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  Action := caFree;
end;

procedure Tfrm_login.ZQUserAfterOpen(DataSet: TDataSet);
begin
  cbo_username.Items.Clear;
  with ZQUser do
  begin
    First;
    while not Eof do
    begin
      cbo_username.Items.Add(FieldByName('loginname').AsString);
      Next;
    end;
  end;
end;

procedure Tfrm_login.edt_pwdEnter(Sender: TObject);
begin
  edt_pwd.Text := '';
end;

end.

完成上述的步骤,这个小的 Demo 就算完成了,有三个地方需要注意一下:

第一,我看我的 IED 中安装了 DCPcrypt2 加密解密组件,随手引用了对密码做哈希处理,这部分如果用于练习的时候可以去掉。

第二,访问 MySQL 数据库需要动态链接库 libmysql.dll ,这个需要注意一下,不管你用 32 位的操作系统还是 64 位的操作系统,同时它也跟你安装的 MySQL 是 32 位的还是 64 位的也没有关系,在 Delphi 7 中使用 MySQL 的时候只能使用这个动态链接库的 32 位版本。

第三,编码问题,Delphi 7 会碰到编码问题,比如向 MySQL 数据库中写入中文会显示乱码,此时在 TZConnectionProperties 中添加 codepage=gbk 。另一种处理方式是在使用 TZQuery 等时,在运行 SQL 插入数据语句前,要先运行 set names gbk

begin
{这里的 zq1 是一个 TZQuery 控件}
	with zq1 do
	begin
		Close;
		SQL.Text := 'set names gbk';
		ExecSQL;
		SQL.Text := 'INSERT INTO Test(UName) VALUES("张三")';
		ExecSQL;
	end;
end;

因为是直接在项目中使用后,手工直接在 Notion 中码出来的,并没有写这个 Demo ,所以无法附上 Demo 的源码,对于现在还使用 Delphi 的人来说应该是比较简单的,我其实更多的是给自己做个笔记。但如果有任何问题,可直接通过「关于我」中提供的联系方式与我联系。

– EOF –