这是一个我写博客的地方,旧版本的博客依然在http://shellbye.github.io, 本来的域名是shellbye.com,后来就没有续费了,所以直接用了github page自带的子域名。
我也有一个自己的微信公众号(写代码的李白),时不时更新一下,没啥内容。
以后新的更新都在issue里面了,更方便。
my blog --> see https://github.com/Shellbye/Shellbye.github.io/issues for recent update
这是一个我写博客的地方,旧版本的博客依然在http://shellbye.github.io, 本来的域名是shellbye.com,后来就没有续费了,所以直接用了github page自带的子域名。
我也有一个自己的微信公众号(写代码的李白),时不时更新一下,没啥内容。
以后新的更新都在issue里面了,更方便。
本文旨在记录我在使用ELK技术栈的过程中用的一些东西,和对ELK技术栈的一些内容的理解。因为我想让这篇文章能够对未来的像我一样的新手有一定的帮助,所以我会尽量【从零开始】记录,并把技术细节记录到我所能做到的最细。鉴于我在使用中使用的Ubuntu操作系统,所以我在本文中所有的系统环境都默认是Ubuntu 1604,且假设是一个新系统,这样所有依赖的东西就都需要单独安装。
Elasticsearch(业界简称ES),是一个基于Java和Lucene的分布式、可扩展、实时的搜索与数据分析引擎,所以在安装使用ES之前,你需要安装Java。
安装Java比较简单,具体可以参考另一篇博客 #4 。
我第一次使用ES的时候,是去官网下载的压缩包,然后设置好相应的配置之后,手动启动的。这样的方式的一个好处是可以清晰的体验ES的整个流程,对ES有一个大概的认识,但是这样做也是有比较多的问题的。比如你启动ES的时候是前台运行还是后台运行呢?如果前台运行,那你可能需要多个窗口协同工作(tmux),如果你后台运行(命令行末尾加-d
),那么每次需要重启时又比较麻烦,所以经过一段时间的折腾之后,我再后面从5.x到6.x迁移的时候,就依照官方文档直接用Ubuntu的包管理工具的安装的,并直接用systemd来托管ES,方便了很多。
我把ES的Ubuntu安装方式也总结了一篇简单的博客 #5 。
相比我之前做搜索时短暂调研过的whoosh和sphinx,ES我比较喜欢的一点是它的基于http的访问接口,非常的“解耦”,而且在初期可以非常轻松的进行一些简单的操作。当你的ES安装好并且已经启动之后,就可以进行一些简单的增删改查了,具体可以参考这篇博客 #6 。
ES被认为是一种NoSQL,而MySQL是传统关系型数据的典型代表,因为NoSQL的日渐普及,以及ES的诸多传统关系型数据库不具备的功能,所以有很多需要从MySQL导数据到Elasticsearch的场景,这部分的相关介绍可以参考这篇专门记录从MySQL导数据到Elasticsearch的博客 #8 。
# https://www.elastic.co/guide/en/logstash/current/installing-logstash.html
sudo apt update
# Download and install the public signing key
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
# install the apt-transport-https package on Debian before proceeding
sudo apt-get install -y apt-transport-https
# Save the repository definition to /etc/apt/sources.list.d/elastic-6.x.list
echo "deb https://artifacts.elastic.co/packages/6.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-6.x.list
# install the logstash Debian package
sudo apt-get update && sudo apt-get install -y logstash
# configure logstash to start automatically when the system boots up
sudo /bin/systemctl daemon-reload
sudo /bin/systemctl enable logstash.service
# logstash can be started and stopped as follows:
sudo systemctl start logstash.service
# sudo systemctl stop logstash.service
# information see /var/log/logstash/
sudo journalctl --unit logstash
# https://www.rosehosting.com/blog/how-to-install-python-3-6-4-on-centos-7/
sudo yum install -y https://centos7.iuscommunity.org/ius-release.rpm
sudo yum update
sudo yum install -y python36u python36u-libs python36u-devel python36u-pip
python3.6 -V
sudo apt-get update
sudo apt-get install -y mysql-server
sudo mysql_secure_installation
# for install msyql-python
sudo apt-get install libmysqlclient-dev
# ref https://www.digitalocean.com/community/tutorials/how-to-install-mysql-on-ubuntu-14-04
被上一个题目( #43 )暴虐之后,我变得谦虚了不少,虽然我是从难到易的在做,但是这只是一个从通过率的角度来衡量的,而且不同的题目解题思路差异还挺大,真不能掉以轻心。
一条包含字母 A-Z 的消息通过以下方式进行了编码:
'A' -> 1
'B' -> 2
...
'Z' -> 26
给定一个只包含数字的非空字符串,请计算解码方法的总数。
示例 1:
输入: "12"
输出: 2
解释: 它可以解码为 "AB"(1 2)或者 "L"(12)。
示例 2:
输入: "226"
输出: 3
解释: 它可以解码为 "BZ" (2 26), "VF" (22 6), 或者 "BBF" (2 2 6) 。
这也是一个动态规划的题目,根据其特性,首先想到的就是把它的情况分成包含第一个字符和不包含第一个字符两类。这里的每一个字符还不是等价的,其中的1, 2, 0
三个字符比较特殊,前两者和后面的部分或全部组合之后会有其他含义,而最后一个0
则是不能和后面的字符组合,且自己单独也不能独立,所以这三个字符需要特殊考虑。
1
,那么只要后面不是0
,则解码方法就会多一种,因为有1
独立、合并后面两种方法;2
,那么只要后面不是7, 8, 9, 0
,则解码方法也会多一种,原因同上。121
这种情况,中间的2
是只能够利用一次的,不能重复计算。隔了几天之后,有突然根据一个评论里面说到的“爬楼梯”想到了另一个思路,就是每次分析到某一个字符时,都看一下这个字符是否把后面的字符分成了两类,具体的分类方式和上面还是一样的。比如字符串192756
,当分析到1
的时候,就可以发现1
之后有两种走法,一种是把92756
当做一个全新的去继续分析,另一种就是把2756
当作一个全新的去分析,其中的9
和1
合并了。
上面的两个思路都是站在当前字符向后看,但是我在网上搜索了一些资料,发现大家都是站在当前字符向前看,想想也对,后面的都是不知道的,但是不能向后看了,前面因为已经处理过了,所以是知道的,因此应该向前看。具体怎么向前看呢?参考一下对1212
的处理逻辑
1
第一种:1
12
第一种:1,2 DL
第二种:12 HT
121
第一种:1,2,1 DL
第二种:1,21 HT
第三种:12,1 DL
1212
第一种:1,2,1,2 DL
第二种:1,2,12 HT
第三种:1,21,2 DL
第四种:12,12 HT
第五种:12,1,2 DL
12121
第一种:1,2,1,2,1 DL
第二种:1,2,1,21 HT
第三种:1,2,12,1 DL
第四种:1,21,2,1 DL
第五种:1,21,21 HT
第六种:12,12,1 DL
第七种:12,1,2,1 DL
第八种:12,1,21 HT
通过以上的几种详细分析,我们可以发现处理新出现的字符,主要有两种方法:
1.新字符“独立”为一种解码方法(标记DL)
2.新字符尝试贴在原有解码方法中最后一个字符的后面,与之“合体”(标记HT)
经过以上的分析,当字符串是由1
和2
构成的时候,这种情况比较简单,我们能得出以下状态转移方程:
dp[i]=dp[i-1]+dp[i-2]
关于为什么是以上方程,我纳闷了好一阵,肯定不是因为这是斐波那契数列,因为有更深层次的原因,再次看了一遍上面的五个详细分析之后,我明白过来了。就拿12121
中情况来说,为什么它是1212
和121
两种方式的和呢?因为12121
有两种方式构成,一种是直接复制1212
下来,然后在末尾加上新出现的字符1
(“独立”DL);另一种就是把新出现的字符1
,与前一个字符“合体”(HT),那有几种“合体”的方式呢?理论上讲应该是1212
中五种全部,因为他们都是2
结尾的,但是真实情况不允许,为什么呢?因为1212
中的五种,有三种“独立”和两种“合体”,而这三种“独立”,恰好来自121
的三种方式。所以就刚好是 5+3
,也就是我们上面的状态转移方程dp[i]=dp[i-1]+dp[i-2]
。
因为我们上面分析的这个输入字符串1212
太特殊,所以我们再来分析一个19210
1
第一种:1
19
第一种:1,9 DL
第二种:19 HT
192
第一种:1,9,2 DL
第二种:19,2 DL
1921
第一种:1,9,2,1 DL
第二种:1,9,21 HT
第三种:19,2,1 DL
第四种:19,21 HT
19210
第一种:1,9,2,10 HT
第二种:19,2,10 HT
通过上面的分析我们可以发现,上面提到的“独立”和“合体”这两种方法并不是总会发生,有些时候只能“独立”:
1
或2
2
,当前字符为7
,8
,9
有些时候只能“合体”(当前字符为0
),还有一些时候,比如上面的这个情况,解码方法不仅仅没有增加,还急剧减少了。。。
因为自己算法方面比较差,所以很多题目苦思冥想之后依然没有思路时,我就会去网上看别人的解析,但是,我在看很多算法解析的时候,常常只是看到了完美执行的代码,却搞不懂是怎么来的,所以我自己写的时候,就会尽量避免直接丢一段代码的模式,针对这道题,我准备尝试一种把整个过程写下来的新方法。
针对上面 思路3 的情况,我们可以写出如下的V0
版本
public int numDecodingsV0(String s) {
if (s == null || s.length() == 0 || s.charAt(0) == '0') return 0;
int[] dp = new int[s.length()];
dp[0] = 1;
dp[1] = 2;
for (int i = 2; i < s.length(); i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[s.length() - 1];
}
以上代码针对完全由1
和2
构成的字符串是没问题的,这也是最简单的版本。它虽然简单,但是也麻雀虽小五脏俱全,代码主体有三大块,第一块是第一行,表示一些对异常输入的检查和快速返回;第二块是接下来三行,是一些初始化工作,类似递归证明的1
和n-1
;第三块是主体循环,几乎在所有的算法题中,这个主体循环都是核心,这里也是时间复杂度的决定性部分;最后就是返回答案了,这个没啥需要说的。
上面的代码之所以只能处理1
和2
构成的字符串,是因为它认为所有的新字符都可以进行“合体”(HT)和“独立”(DL),所以当其中出现其他字符时,就不行了。那么为了兼容其他字符,我们需要进行一些调整。
字符1
和2
和其他字符有什么不同之处呢?这个不同就在与能否随时随地的自由的“独立”和“合体”,比如字符8
,它只有在前面是1
的时候才能“合体”,如果前面不是1
就不行了,那么怎么判断当前字符和前面的字符能不能“合体”呢?这个时候结合题目信息我们可以判断,当两个数字小于等于26
时,就是可以的(0
比较特殊,稍后重点讨论)。于是我们的代码就更新成了下面这样
public int numDecodingsV1(String s) {
if (s == null || s.length() == 0 || s.charAt(0) == '0') return 0;
int[] dp = new int[s.length()];
dp[0] = 1;
if (Integer.parseInt(s.substring(0, 2)) <= 26) {
dp[1] = 2;
} else {
dp[1] = 1;
}
for (int i = 2; i < s.length(); i++) {
String a = s.substring(i - 1, i + 1);
if (Integer.parseInt(a) <= 26) {
// 独立+合体数目
dp[i] = dp[i - 1] + dp[i - 2];
} else {
// 仅独立数目
dp[i] = dp[i - 1];
}
}
return dp[s.length() - 1];
}
其中比较关键的新增内容就是Integer.parseInt(s.substring(0, 2)) <= 26
这个判断,并根据其结果选择不同的路径,如果为真,则可以有“合体”和“独立”两种选择,与1
,2
类似,如果为假,则只能“独立”,不能“合体”。
截止目前,在不包含0
的情况下,我们都可以轻易处理了,接下来就要重点考虑包含0
的情况了。
0
也考虑进去字符0
的特殊之处我们在上面已经见识过了,它是不能“独立”只能“合体”的,而且有些时候它的“合体”还会导致解码方法的减少(比如110
,从两种变成一种),还有时候它甚至会导致整个字符串无法解码,即算法返回0
(比如9912302144
)。总结起来,关于0
有以下几种情况:
0
在开头,返回 0;0
前面的字符不是1
或者2
,字符串非法,返回 0;0
前面是1
或者2
,此时解码方法要缩减,缩减的部分为0
前面的部分“合体”形成的,也就是0
前面的字符再前面的字符所带来的数目(相见上面的分析)。26
比较其实是有点粗粒度了,因为其中可能包含05
这种情况,05
虽然符合小于26
但是它却不能作答自由的“独立”或者“合体”。能完全真正作答的,还需要满足大于10
。1
, 20
),我们的代码应该是如下的样子 public int numDecodings(String s) {
if (s == null || s.length() == 0 || s.charAt(0) == '0')
return 0;
if (s.length() == 1)
return 1;
int[] dp = new int[s.length()];
dp[0] = 1;
String a0 = s.substring(0, 2);
if (Integer.parseInt(a0) <= 26 && Integer.parseInt(a0) > 10 && s.charAt(1) != '0') {
dp[1] = 2;
} else if (s.charAt(1) == '0' && !(s.charAt(0) == '1' || s.charAt(0) == '2')) {
return 0;
} else {
dp[1] = 1;
}
for (int i = 2; i < s.length(); i++) {
String a = s.substring(i - 1, i + 1);
// 计算独立数
if (s.charAt(i) != '0') {
// 只要当前的字符不是 0 ,就表示通过独立可以实现继承前面的解码数
dp[i] = dp[i - 1];
}
// 计算合体数
if (Integer.parseInt(a) <= 26 && Integer.parseInt(a) >= 10) {
// 有合体的可能性,加上潜在合体数,即前一个的前一个
dp[i] += dp[i - 2];
}
// 0 前面不是 1 或者 2 就返回 0
if (s.charAt(i) == '0' && !(s.charAt(i - 1) == '1' || s.charAt(i - 1) == '2'))
return 0;
}
return dp[s.length() - 1];
}
这是一个可以通过的版本,🎉🎉🎉,但是我可以看到,虽然中间的循环逻辑稍微清楚一点,但是第一部分的初始化做的实在是惨不忍睹,明显就是针对各种特殊测试用例打的补丁。
因为我的初始化做的实在是太差了,所以查了一些其他人的做法,
public int numDecodings(String s) {
int n = s.length();
if (n == 0) {
return 0;
}
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = (s.charAt(0) == '0') ? 0 : 1;
for (int i = 2; i <= n; i++) {
if (s.charAt(i - 1) != '0') {
dp[i] = dp[i - 1];
}
int twoDigits = Integer.parseInt(s.substring(i - 2, i));
if (twoDigits >= 10 && twoDigits <= 26) {
dp[i] += dp[i - 2];
}
}
return dp[s.length()];
}
以上是别人提供的一个解法,这个解法的初始化方法是非常简洁明了的,而且循环内部也逻辑更简单,总之是各个方面都比我的原始方法要好。我对比了一下两个解法的差异,发现我们的差异的关键点在于赋给dp
中每一个结点的意义是不一样的,在我的解法中,dp[i]
表示输入字符分析到第i
个结点(包含)时,可能的解码方法总数;而上面的这个更简洁的解法中,dp[i]
表示的却不包含当前的第i
个结点,比如同样的一组输入数据(12121
),最后的dp
数组分别如下:
[1, 2, 3, 5, 8] // 我的
[1, 1, 2, 3, 5, 8] // 别人的
通过上面的输出我们可以看到,在别人的解法中,它的第一位其实是没有严格的物理意义的,那么问题来了:为什么要加这么一个没有物理意义的点呢?(这个点貌似有一个算法领域的专有名字,我想不起来了)虽然这个点没有物理意义,但是它却有一个重要的作用,那就是可以把第二个元素纳入到循环中。什么意思呢,其实我们的初始化代码之所以混乱,就是因为我在循环之前不仅仅处理了第一个字符,而且也处理了第二个字符,所以才会有那么多的和循环里面逻辑差不多的代码。为什么会这样呢,因为这个循环是一个依赖两个元素的,如果只初始化一个元素,还不够进入循环的条件,如果初始化两个,就会显得比较乱(比如我的),这时,引入一个无物理意义的点就像引入了辅助线一样,就可以把这个问题解决了。
经过上面的思考,最终优化如下
public int numDecodings(String s) {
if (s == null || s.length() == 0 || s.charAt(0) == '0')
return 0;
int[] dp = new int[s.length() + 1];
dp[0] = 1; // 辅助线
dp[1] = 1;
for (int i = 2; i < s.length() + 1; i++) {
String a = s.substring(i - 2, i);
// 计算独立(DL)数
if (s.charAt(i - 1) != '0') {
// 只要当前的字符不是 0 ,就表示通过独立可以实现继承前面的解码数
dp[i] = dp[i - 1];
}
// 计算合体(HT)数
if (Integer.parseInt(a) <= 26 && Integer.parseInt(a) >= 10) {
// 有合体的可能性,加上潜在合体数,即前一个的前一个
dp[i] += dp[i - 2];
}
}
return dp[s.length()];
}
其中唯一需要注意的就是下标的变化,其他逻辑基本上没啥变化。
最近新开发一个项目(demo),有大量的数据批量保存,用到了JPA
自带的saveAll
方法,但是在压测的时候,却发现一个致命的问题,就是特别慢。用MySQL
的show PROCESSLIST;
查看之后,发现有大量的如下语句
select next_val as id_val from hibernate_sequence for update
原来是因为在model
中的id
是由Hibernate
生成的,
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
而它的生成机制就是在hibernate_sequence
这个表中存储了下一个id
,并在每一次取的时候都自增,所有的新建请求都需要在这里排队取id
,那可不是就慢了,这就导致saveAll
和重复调用save
一样了,批量操作失去了批量的意义,避免这种假批量的方式比较多,比较简单快捷的一个方式就是在配置文件中显示的关闭这种策略
spring.jpa.hibernate.use-new-id-generator-mappings=false
这样配置之后,同样的model
代码
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Integer id;
其生成id
的功能将由数据库来完成,这样就会快很多,起码是多线程,而不是排队的单线程了。
安装ES之前需要安装Java,在CentOS上安装Java可以参考 #12
# https://www.elastic.co/guide/en/elasticsearch/reference/current/rpm.html
rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
cat >> /etc/yum.repos.d/elasticsearch.repo << EOF
[elasticsearch-6.x]
name=Elasticsearch repository for 6.x packages
baseurl=https://artifacts.elastic.co/packages/6.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md
EOF
sudo yum install elasticsearch
cat >> /etc/security/limits.conf << EOF
elasticsearch soft memlock unlimited
elasticsearch hard memlock unlimited
elasticsearch - nofile 65536
elasticsearch soft nproc 65535
elasticsearch hard nproc 65535
EOF
# https://support.cafex.com/hc/en-us/articles/202508492-Increasing-the-number-of-threads-available-on-Linux
sudo chkconfig --add elasticsearch
sudo -i service elasticsearch start
# sudo -i service elasticsearch stop
最近在Spring Boot,顺便温习Java相关的东西,看到的相关的资料和书籍中,经常提到控制反转(IoC Inversion of Control),看的比较模糊,于是花了点时间找了一些资料,对控制反转有了一个简单的理解,在这里简短的记录一下。
控制反转其实是一个比较古老的概念,现在其实已经不怎么提及这个事儿了,为什么呢?在我看来,主要是因为软件领域的工作模式已经发生了一些变化。在软件发展的上古时期,也就是大师辈出的上个时间50、60年代前后,那时的程序员应该叫做计算机科学家,他们大多对软件的诸多领域都有深入的认识,很多人都可以独立完成从硬件到软件到交付的全过程,所有代码都是自己(或者团队)独立完成,基本上不依赖第三方(那时似乎也没有第三方)。比如那个时候写个博客网站,很多人直接就是些html了,一些网页应用,背后也是通过cgi部署在服务上的一些perl脚本。
在这个时期,软件的作者控制着软件的一切,他决定着软件的信息流,软件里的一切几乎都是可控的。假设我们在那个时期,要写一个“把大象装冰箱”的程序,我们要自己找到冰箱的API,自己控制程序打开冰箱,针对不同的冰箱,可能还得写不同的代码;然后在自己设计方法把大象装进去,最后把冰箱门关上。在这个过程中,冰箱怎么打开是我们控制的,冰箱怎么关上也是我们自己控制的,把大象装进去这个事儿,当然更是由我们自己控制。这个过程可以抽象为以下控制关系:
后来,随着时间的推移,大家发现彼此写了很多功能几乎一样的软件,再全人类这个角度看,这是一种对智力资源的浪费,于是一群大牛们聚到一起,倡导开源,倡导不要重复造轮子(DRY,Dont Repeat Yourself),于是,就有了一系列的公用的软件框架。比如像Spring这种。那“把大象装冰箱”来说,这个时候,我们可能有了一个框架,叫做ElephantRefrigerator,这个框架兼容了市面上所有的主流冰箱,可以通过简单的调用open、close就完成冰箱的打开和关闭,我们使用这个冰箱的话,就只需要实现一个方法就好(put_elephant_in),其他的事儿都由框架完成了。这种情况下,整个软件的基本流程就不是我们自己控制了,而是由框架控制,此时此刻,就发生了控制反转。这个新的过程可以抽象为以下控制关系:
控制反转的过程,我认为就是软件写作越来越快速、越来越模块化的一个过程,所以很多年轻的程序员都不知道什么是控制反转,因为他们从开始接触编程,就是各种框架各种飞,根本没控制过啥,都是控制反转的一代人,所以就不知道控制反转是啥了,就像鱼不知水一样。
安装Logstash需要先安装Java,CentOS安装Java可以参考 #12
# https://www.elastic.co/guide/en/logstash/current/installing-logstash.html
rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
cat >> /etc/yum.repos.d/elasticsearch.repo << EOF
[elasticsearch-6.x]
name=Elasticsearch repository for 6.x packages
baseurl=https://artifacts.elastic.co/packages/6.x/yum
gpgcheck=1
gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearch
enabled=1
autorefresh=1
type=rpm-md
EOF
sudo yum install logstash
# https://www.elastic.co/guide/en/logstash/6.2/running-logstash.html#running-logstash-upstart
sudo initctl start logstash
#include <iostream>
#include <stdio.h>
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
typedef signed short BitDepth; // 16bit audio
template <typename _Ty>
void write(std::ofstream &stream, const _Ty &ty)
{
stream.write((const char *)&ty, sizeof(_Ty));
}
void writeWaveFile(const char *filename, BitDepth *buffer, size_t sz)
{
const int samplerate = 16000;
const int channels = 1;
std::ofstream stream(filename, std::ios::binary);
stream.write("RIFF", 4);
write<int>(stream, 0); // todo
stream.write("WAVEfmt ", 8);
write<int>(stream, 16);
write<short>(stream, 1);
write<unsigned short>(stream, channels);
write<int>(stream, samplerate);
write<int>(stream, samplerate * channels * sizeof(BitDepth));
write<short>(stream, channels * sizeof(BitDepth));
write<short>(stream, sizeof(BitDepth) * 8);
stream.write("data", 4);
write<int>(stream, sz * sizeof(BitDepth));
stream.write((const char *)&buffer[0], sz * sizeof(BitDepth));
stream.close();
}
int main()
{
std::vector<BitDepth> b;
std::string line;
std::ifstream myfile("wave_data.txt");
if (myfile.is_open())
{
while (getline(myfile, line))
{
b.push_back(std::stof(line));
}
myfile.close();
}
writeWaveFile("./e2.wav", b.data(), b.size());
return 0;
}
之前写Web一直是用Python+Django,短暂得写过一小段时间的Java,那时的Java Web真的是很麻烦,需要各种各样的配置,一会儿是注解,一会儿又是XML,写一个CRUD要先后在好几个不同的文件里写各种配置,有DAO,有TAO,还有XML,还有Entity,总之是非常复杂。相比之下,Django就方便的多,基本上是零配置,只需要专心写业务逻辑就好。最近又开始看了Java Web相关的Spring Boot 2.0,感觉好像好了很多,很多之前的麻烦东西都不存在了,越来越像Django了。
这次重拾Spring,又遇到了当年也没有搞明白的依赖注入(Dependency Injecting),借着这次机会,终于是搞懂了。
首先,依赖注入(Dependency Injecting)是一种保持代码解耦的程序设计方式。在正式理解依赖注入之前,需要先搞明白两个概念,一个是client(管控某些流程、业务逻辑),一个是service(提供某种服务、功能)。依赖注入,说的就是把client所依赖的service,注入到client中。下面我们举例说明:
public class AK47 {
public void fire() {
System.out.println("Fire!!!!")
}
}
public class Soldier {
private AK47 ak47;
Soldier() {
this.ak47 = new AK47();
}
void fire() {
this.ak47.fire()
}
public static void main(String[] args) {
Soldier soldier = new Soldier();
soldier.fire();
}
}
上面的脚本中,我们的client就是Soldier,它依赖于AK47,然后它就在自己内部new一个AK47,然后在fire里面使用。这是没有依赖注入的写法,这样写的问题是当后期士兵的武器升级之后,武器的代码肯定得改,但是逻辑上讲士兵是不需要改的,但是按照我们上面的写法,士兵也得改,这就是两个类之间耦合度太高,需要解耦一下。
public interface Weapon {
public void fire();
}
public class AK47 implements Weapon {
public void fire() {
System.out.println("Fire!!!!")
}
}
public class Soldier {
private Weapon weapon;
Soldier(Weapon weapon1) {
this.weapon = weapon1;
}
void fire() {
this.weapon.fire()
}
public static void main(String[] args) {
Weapon ak47 = new AK47();
Soldier soldier = new Soldier(ak47);
soldier.fire();
Weapon m1 = new M1();
Soldier soldier2 = new Soldier(m1);
soldier2.fire();
}
}
在新的方法中,我们把武器抽象出来了一个接口,然后在士兵初始化的时候传入武器,这样都有武器更新换代的时候,士兵的代码是不用动的,这样就实现了一定程度的士兵与武器的解耦。注意,这时,依赖(武器)就是通过Client(士兵)的构造方法被注入到Client中的,这就是一种依赖注入的方式。
上面这样通过构造方法注入的方式,有一定的缺点,那就是一旦Client创建好,就无法再更改了,于是还有一种通过setter方法进行注入,如下所示:
public interface Weapon {
public void fire();
}
public class AK47 implements Weapon {
public void fire() {
System.out.println("Fire!!!!")
}
}
public class Soldier {
private Weapon weapon;
Soldier() {
}
void setWeapon(Weapon weapon1) {
this.weapon = weapon1;
}
void fire() {
this.weapon.fire()
}
public static void main(String[] args) {
Weapon ak47 = new AK47();
Soldier soldier = new Soldier();
soldier.setWeapon(ak47);
soldier.fire();
}
}
上面的代码中,具体的实现一直在变,但是main方法一直其实没啥大的变化,在实际的工程项目中,Spring之类的框架,就是用来完成本文中main方法所做的事儿的,它们负责new各种个样的实例(术语Bean),然后按照一定的指引(XML或者是注解),讲service功能的Bean注入到client功能的Bean中。所以Spring这类的框架一般都叫做Injector,其与代码的关系,可以简单的用下图来展示:
在之前的一篇博客中( #8 ),我记录了怎么从MySQL中,利用Logstash导数据到ES中,但是导出的过程比较粗暴,是一次性全部导完的。但是在实际使用中,我们常常需要根据某些字段值实时的导入数据,比如数据库条目中的create_time
或update_time
等,这样做的好处是显而易见的,因为这样可以避免大量已经导入的数据重复的导入,对于远程导入,这种方式还比较节约带宽,而且高效。本文就记录了利用Logtash来完成这种形式的导入。
从MySQL导数据到Elasticsearch,分别需要MySQL(安装参考 #9 )、Elasticsearch(安装参考 #5 )和中间件Logstash(安装参考 #7 )。
创建并切换数据库
mysql> CREATE DATABASE demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
mysql> use demo;
创建demo需要的表
mysql> CREATE TABLE `demo_questions_001` (`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `subject` varchar(31) DEFAULT NULL, `question` varchar(300) DEFAULT NULL, created_at timestamp default current_timestamp) DEFAULT CHARSET=utf8;
Query OK, 0 rows affected (0.05 sec)
插入数据
mysql> INSERT INTO demo_questions_001(subject, question) VALUES('math', 'x+y=z+1');
Query OK, 1 row affected (0.01 sec)
mysql> select * from demo_questions_001;
+----+---------+----------+---------------------+
| id | subject | question | created_at |
+----+---------+----------+---------------------+
| 1 | math | x+y=z+1 | 2018-05-07 20:30:56 |
+----+---------+----------+---------------------+
1 row in set (0.00 sec)
Logstash有专门真对MySQL的input插件,官方文档在这里,这里唯一需要注意的是下载mysql-connector
的jar
包,并在配置文件中使用绝对路径配置其位置。jar
包可以从这里下载。与上一篇博客( #8 )不同的地方在于这次用到了sql_last_value
,这是一个用来记录上一次执行的位置的值,官方文档如下:
The value used to calculate which rows to query. Before any query is run, this is set to Thursday, 1 January 1970, or 0 if use_column_value is true and tracking_column is set. It is updated accordingly after subsequent queries are run.
该值默认是一个1970年1月1日的时间戳,也可以根据用户的定制设置为其他值。如果想要重置sql_last_value
,需要用到last_run_metadata_path
(如下),可以修改这个值,或者将旧值对应的文件删除。
具体的demo.conf
如下:
input {
jdbc {
jdbc_driver_class => "com.mysql.jdbc.Driver"
jdbc_driver_library => "/Users/shellbye/Downloads/mysql-connector-java-5.1.32/mysql-connector-java-5.1.32-bin.jar"
jdbc_connection_string => "jdbc:mysql://localhost:3306/demo"
jdbc_user => "root"
jdbc_password => ""
schedule => "* * * * *"
jdbc_paging_enabled => true
jdbc_page_size => 10000
last_run_metadata_path => "/tmp/logstash.file"
statement => "SELECT id, subject, question from demo_questions_001 where created_at > :sql_last_value"
columns_charset => { "question" => "UTF-8" }
}
}
output {
elasticsearch {
index => "question_index_from_mysql_2"
hosts => ["localhost:9200"]
document_id => "%{id}"
}
}
...
[2018-05-07T20:43:25,950][INFO ][logstash.pipeline ] Pipeline started successfully {:pipeline_id=>"main", :thread=>"#<Thread:0x6dabc970 run>"}
[2018-05-07T20:43:26,093][INFO ][logstash.agent ] Pipelines running {:count=>1, :pipelines=>["main"]}
[2018-05-07T20:44:01,709][INFO ][logstash.inputs.jdbc ] (0.009032s) SELECT version()
[2018-05-07T20:44:01,998][INFO ][logstash.inputs.jdbc ] (0.000736s) SELECT version()
[2018-05-07T20:44:02,088][INFO ][logstash.inputs.jdbc ] (0.006108s) SELECT count(*) AS `count` FROM (SELECT id, subject, question from demo_questions_001 where created_at > '1970-01-01 08:00:00') AS `t1` LIMIT 1
[2018-05-07T20:44:02,154][INFO ][logstash.inputs.jdbc ] (0.000719s) SELECT * FROM (SELECT id, subject, question from demo_questions_001 where created_at > '1970-01-01 08:00:00') AS `t1` LIMIT 10000 OFFSET 0
[2018-05-07T20:45:00,293][INFO ][logstash.inputs.jdbc ] (0.001596s) SELECT version()
[2018-05-07T20:45:00,307][INFO ][logstash.inputs.jdbc ] (0.003129s) SELECT version()
[2018-05-07T20:45:00,330][INFO ][logstash.inputs.jdbc ] (0.007516s) SELECT count(*) AS `count` FROM (SELECT id, subject, question from demo_questions_001 where created_at > '2018-05-07 20:44:01') AS `t1` LIMIT 1
[2018-05-07T20:46:00,295][INFO ][logstash.inputs.jdbc ] (0.000376s) SELECT version()
[2018-05-07T20:46:00,305][INFO ][logstash.inputs.jdbc ] (0.000409s) SELECT version()
[2018-05-07T20:46:00,313][INFO ][logstash.inputs.jdbc ] (0.002396s) SELECT count(*) AS `count` FROM (SELECT id, subject, question from demo_questions_001 where created_at > '2018-05-07 20:45:00') AS `t1` LIMIT 1
[2018-05-07T20:46:00,332][INFO ][logstash.inputs.jdbc ] (0.002503s) SELECT * FROM (SELECT id, subject, question from demo_questions_001 where created_at > '2018-05-07 20:45:00') AS `t1` LIMIT 10000 OFFSET 0
[2018-05-07T20:47:00,328][INFO ][logstash.inputs.jdbc ] (0.000395s) SELECT version()
[2018-05-07T20:47:00,352][INFO ][logstash.inputs.jdbc ] (0.000892s) SELECT version()
[2018-05-07T20:47:00,364][INFO ][logstash.inputs.jdbc ] (0.002741s) SELECT count(*) AS `count` FROM (SELECT id, subject, question from demo_questions_001 where created_at > '2018-05-07 20:46:00') AS `t1` LIMIT 1
从日志中可以看到这样每次读取数据库时,就不是全量的读取了,而是只读取那些未读取的,这样做显然是对的。😘
ES的安装参考这里 #5 ,Logstash的安装参考这里 #7
注意这里有一个问题需要注意一下,就是如果你一直在尝试导入一个(比较小的)文件(就像我在这个demo中做的),那么logstash后面就不会继续去导入了,因为logtash会通过sincedb_path所指定的位置去记录当前文件的导入位置,如果你已经导入完成了,那么后续再导入就不会执行了(别问我怎么知道的😂)。
shellbye@localhost:~$ cat text.txt
apple $2 01
banana $4 02
book $44 03
desk $55 04
与上一篇 #10 导入json文件不同,导入文本文件需要使用到grok
这个filter
,用来描述文本文件的结构。这里有一个比较方便的调试工具 grokdebug
input {
file {
path => "/home/shellbye/text.txt"
start_position => "beginning"
}
}
filter {
grok {
match => { "message" => "%{DATA:name} %{DATA:price} %{WORD:no}" }
}
}
output {
stdout {}
elasticsearch {
index => "demo_index002"
hosts => ["localhost:9200"]
}
}
/path/to/bin/logstash -f /home/shellbye/f.conf
在上一篇文章 #46 中,介绍了一种当必填参数没有传入时的比较友好的提示方式,今天在顺着同样的思路,介绍另外一种情况:参数类型错误时的处理情况。
当你使用Spring Boot
时,如果接口的输入参数有误,后端的默认提示是这个样子的:
是不是一种不知所措的感觉呢?相比与上一篇的默认提示,这个提示简直就是太不友好了,我们想要的其实是这个样子的:
与上一篇的思路简直是如出一辙,依然是找到那个正确的异常(TypeMismatchException
),然后对它进行一些相应的处理就好。
项目目录结构
关键代码
Controller.java
package com.example.demo;
import org.springframework.web.bind.annotation.*;
@RestController
public class Controller {
@RequestMapping(value = "/demo", method = RequestMethod.GET)
public @ResponseBody
String demo(@RequestParam(value = "p1") Integer p1) {
return "OK";
}
}
TypeMismatchExceptionHandler.java
package com.example.demo;
import org.springframework.beans.TypeMismatchException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class TypeMismatchExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleTypeMismatch(
TypeMismatchException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String msg = String.format("{\"code\":\"400202\",\"data\":{},\"message\":\"参数%s类型错误,要求是%s\"}\n",
((MethodArgumentTypeMismatchException) ex).getName(), ex.getRequiredType());
return new ResponseEntity<>(msg, HttpStatus.OK);
}
}
在将Python
代码转为C++
的过程中,需要一步一步的查看以确保转化过程的正确性,对于常见的数据类型,基本打印出来就可以进行对比查验,但是对于模型输出的Tensor
,本以为它就是个多维向量而已,循环打印之就好,但是缺也是费了很大的周折,最终还是通过看源码找到了解决方案。
因为我也刚开始接触TenserFlow
,所以很多东西还没有搞特别明白,比如C++
模型的输出,在我们的项目中,输出是一个std::vector<Tensor>
,我想做的事儿就是遍历这个Tensor
,查看里面的数据是否与我们的Python
版一致。
在尝试了各种方法之后,最后通过通读文档,凭感觉觉得这个vec
可能就是我要找的东西,然后发现了Eigen
这么一个概念,继续顺藤摸瓜,又找到了这篇博客,最终确定我需要的应该是tensor
这个方法,
代码如下:
Status run_status = session_->Run(input, { "BiasAdd:0", }, {}, &outputs);
if (run_status.ok())
{
auto f = outputs[0];
auto t0 = f.tensor<float, 3>(); // 3来自f.shape()
std::cout << "shape " << f.shape() << std::endl; // [1,255,43]
for (int i = 0; i < 1; i++) {
for (int j = 0; j < 255; j++) {
for (int k = 0; k < 43; k++) {
std::cout << " " << t0(i, j, k);
}
std::cout << std::endl;
}
}
}
RapidJSON是一款由腾讯开源的json处理包,我在项目中使用的时候,还有点不太熟悉,所以简单记录一些东西。
#include "rapidjson/document.h"
#include "rapidjson/writer.h"
#include "rapidjson/stringbuffer.h"
using namespace rapidjson;
int main() {
// official example to parse json
const char* json = "{\"project\":\"rapidjson\",\"stars\":10}";
Document d;
d.Parse(json);
// 2. 利用 DOM 作出修改。
Value& s = d["stars"];
s.SetInt(s.GetInt() + 1);
// 3. 把 DOM 转换(stringify)成 JSON。
StringBuffer buffer;
Writer<StringBuffer> writer(buffer);
d.Accept(writer);
// Output {"project":"rapidjson","stars":11}
std::cout << buffer.GetString() << std::endl;
// Build json string
Document jsondoc;
jsondoc.SetObject();
Document::AllocatorType& allocator = jsondoc.GetAllocator();
Value s2;
s2 = "This is value";
jsondoc.AddMember("key2", s2, allocator);
StringBuffer buffer2;
Writer<StringBuffer> writer2(buffer2);
jsondoc.Accept(writer2);
std::cout << buffer2.GetString() << std::endl;
// Build json use already exists string
Document jsondoc3;
jsondoc3.SetObject();
std::string a = "A old string";
Document::AllocatorType& allocator3 = jsondoc3.GetAllocator();
Value s3;
s3.SetString(StringRef(a.c_str())); // This is the special place
jsondoc3.AddMember("test", s3, allocator3);
StringBuffer buffer3;
Writer<StringBuffer> writer3(buffer3);
jsondoc3.Accept(writer3);
std::cout << buffer3.GetString() << std::endl;
return 0;
}
The output is,
{"project":"rapidjson","stars":11}
{"key2":"This is value"}
{"test":"A old string"}
ES安装好之后(参考 #5 ),可以使用以下命令进行简单的操作。
ES的索引,在6.x之后,可以初略的理解为相当于关系型数据库概念里面的表。与关系型数据库不一样的地方在于ES的索引并不强制要求结构化。所以最简单的一个存储题目的索引(question_index)就可以创建如下:
shellbye@localhost:~$ curl -X PUT "localhost:9200/question_index?pretty"
以下为输出
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "question_index"
}
创建完成之后可以通过以下命令查看索引列表(相当于MySQL里面的show tables
):
shellbye@localhost:~$ curl "localhost:9200/_cat/indices?v"
以下为输出
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open question_index sunRjkjmRYGuwg5-CLJokA 5 1 0 0 1.1kb 1.1kb
有了索引之后,写下来就可以向索引里面写入数据(增)了。
shellbye@localhost:~$ curl -X PUT "localhost:9200/question_index/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
"id": "10001",
"subject": "math",
"question": "calculate 1+1"
}
'
以下为输出
{
"_index" : "question_index",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
通过返回数据里面的"result" : "created"
我们可以看到这个新的条目创建成功了,这里需要注意以下请求url里面的1
、我输入的数据里面的{"id": "1001"}
,和返回的"_id" : "1"
。其中输入的url里面的1表示我指定了这个新增的题目在ES内部的id,我输入的数据里面的{"id": "1001"}
则是数据本身的一个标志id,对于ES来说,它和别的字段都是一视同仁的,那么返回的"_id" : "1"
,即对应请求url里面的1
。
做个实验会更加明白:
shellbye@localhost:~$ curl -X PUT "localhost:9200/question_index/_doc/this_id_can_be_any_thing?pretty" -H 'Content-Type: application/json' -d'
{
"id": "10002",
"subject": "math",
"question": "calculate 1+2"
}
'
以下为输出
{
"_index" : "question_index",
"_type" : "_doc",
"_id" : "this_id_can_be_any_thing",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
好了,添加数据之后,我们就可以进行接下来的修改数据(改)了。
通过修改数据,也可以更加清晰的理解前面的url里面的和返回的_id
了.
shellbye@localhost:~$ curl -X PUT "localhost:9200/question_index/_doc/this_id_can_be_any_thing?pretty" -H 'Content-Type: application/json' -d'
{
"id": "10002",
"subject": "math",
"question": "calculate 1+2222"
}
'
以下为输出
{
"_index" : "question_index",
"_type" : "_doc",
"_id" : "this_id_can_be_any_thing",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
如上所示,只要提供的id是已经有的,那么就是相当于是一次更新操作("result" : "updated",
)。当然也可以显示的在请求的url后面加上_update
,但是这个时候要注意和不加_update
的操作的http方法不一样,而且参数外面也多了一层doc
,如下所示:
shellbye@localhost:~$ curl -X POST "localhost:9200/question_index/_doc/this_id_can_be_any_thing/_update?pretty" -H 'Content-Type: application/json' -d'
{
"doc": {
"id": "10002",
"subject": "math",
"question": "calculate 1+3"
}
}
'
以下为输出
{
"_index" : "question_index",
"_type" : "_doc",
"_id" : "this_id_can_be_any_thing",
"_version" : 5,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 1
}
ES里的“查找”,有两层含义
shellbye@localhost:~$ curl -X GET "localhost:9200/question_index/_doc/1?pretty"
以下为输出
{
"_index" : "question_index",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"found" : true,
"_source" : {
"id" : "10001",
"subject" : "math",
"question" : "calculate 1+1"
}
}
shellbye@localhost:~$ curl -X GET "localhost:9200/question_index/_doc/this_id_can_be_any_thing?pretty"
以下为输出
{
"_index" : "question_index",
"_type" : "_doc",
"_id" : "this_id_can_be_any_thing",
"_version" : 5,
"found" : true,
"_source" : {
"id" : "10002",
"subject" : "math",
"question" : "calculate 1+3"
}
}
shellbye@localhost:~$ curl -X GET 'localhost:9200/question_index/_search?pretty' -H 'Content-Type: application/json' -d'
{
"size":1,
"query" : {
"match" : {
"question" : "calculate"
}
}
}'
以下为输出
{
"took" : 3,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 0.2876821,
"hits" : [
{
"_index" : "question_index",
"_type" : "_doc",
"_id" : "1",
"_score" : 0.2876821,
"_source" : {
"id" : "10001",
"subject" : "math",
"question" : "calculate 1+1"
}
}
]
}
}
如上所示的搜索,输入的关键字是calculate
,正确的返回了包含该关键字的数据。
最后是删除数据,之所以最后写删除数据,是因为一旦删除了之后,还得重新添加,比较麻烦,所以就把删除数据的demo写到最后了。
shellbye@localhost:~$ curl -X DELETE "localhost:9200/question_index/_doc/1?pretty"
以下为输出
{
"_index" : "question_index",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
删除数据之后可以通过查看总数来确认删除:
shellbye@localhost:~$ curl "localhost:9200/_cat/indices?v"
以下为输出
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open question_index sunRjkjmRYGuwg5-CLJokA 5 1 1 0 10kb 10kb
可以看到我们创建并添加了两条数据、删除了一条数据的索引目前只有一条数据了。
删除索引和删除数据的接口基本一致,就是末尾少了几个参数
shellbye@localhost:~$ curl -X DELETE "localhost:9200/question_index/?pretty"
以下为输出
{
"acknowledged" : true
}
shellbye@localhost:~$ curl "localhost:9200/_cat/indices?v"
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
可以看到此时已经没有任何索引了。🙂
2015-09-08
漫漫长夜,淅淅沥沥的雨
一切都是那么的熟悉
我又一次从噩梦中醒来
想到的还是熟悉的你
熟悉的场景里熟悉的疼痛
我又一次回到了过去的自己
爱上你,
让我还是那么的手足无措
原来我没有想象中的坚强
还是十六岁那个懦弱的自己
我躲在墙角默默地哭泣
祈祷着这一切尽快过去
好想去黑色的狂野中奔跑
融入老友的怀抱
此时的我已缺乏勇气
只想象着漫漫长夜,听着淅淅沥沥的雨
2014-03-12
那个酒吧的女子
抱着双肩静静地坐在那里
这是她第一次来到这里么?
她看起来是那么的惊慌失措
那个酒吧的女子
优雅地举着装有红酒的高脚杯
她那性感的红唇
暗淡了她身边的所有存在
那个酒吧的女子
幸福地坐在她的爱人身旁
他看她的那温柔的眼神
几乎要把她融化
那个酒吧的女子
独自坐在灯光打不到的角落
烈酒一杯杯由喉入腹
面无表情
那个酒吧的女子
在也没有在酒吧出现过
她的故事会有怎样的结局?
我好想了解
2014-02-23
——献给我的宝贝,李燕
在最初的夜里
我看似无所畏惧甘之若素
但黑暗中那未知一切却一直使我惧怕
深渊之下的回响也一次次地攫取着我的心
恐惧不单单是因为所恐惧的东西
更是因为不得不独自面对
你的到来,是我漫长的夜的破晓
我看到东方既白的天空
把所有的黑暗与恐惧埋在了身后
我的太阳,你终于来了
你带给我的,是全新的力量
让我更加勇敢的面对整个世界
注:“每段恋情都在你心里培植了一种力量或一个弱点” ——迈克尔·默多克
2013-12-21
当我和你在一起的时候
我希望整个世界忘掉我
因为这样
我就可以也忘掉整个世界
专心陪你
2013-10-25
这是一片决绝的叶子
已不再留恋过往
找到了自己的真正的归宿之后
便义无反顾
2013-10-06
像一颗颗义无反顾的坠落的流星
她的眼泪顺着她的脸颊迅速落下
一道浅浅的泪痕留在了她的脸上
就像流星的轨迹停留在夜空之上
2013-09-26
盛夏常思秋,怎奈热难消。
倒是初秋乍凉时,又嫌秋料峭。
秋意渐浓日,思乡情甚时。
正说深秋迟不到,雨把屋檐敲。
2013-09-24
雾霭迷离的南国清晨
你肩倚一只淡色的伞
似有似无地出现在河的对岸
沿着河道似充满忧伤的走着
你那一袭红衣牢牢地攥住了我的心
我已忘记为何来到河边
如今只呆呆地立在那里
埋怨河水太急河道太宽
2013-09-17
Long time no see, my friend
Since the last time we met
It seems a whole lifetime have passed
And I almost forget you
But don't worry, my friend
The moment we hug each other
The moment I heard your voice
All the wonderful memories have found themselves
2013-09-14
来吧,夜,我的情人
把我拥入你的怀里
在我的耳边轻轻地说着爱我
让我踏实地睡在你的腿上
来吧,夜,我的情人
让我们一起在没有人注视的时候狂奔
我们的欢笑将吵醒已经入睡的人们
他们将会嫉妒我们放浪的笑声
来吧,夜,我的情人
就这样依偎在我的肩膀吧
让我感觉的到自己的存在
请不要让我无故的消失掉
来吧,夜,我的情人
请深情地望着我
就像我深情的望着你一样
只有你的眼神能让我无所畏惧
夜啊,我深爱的情人
请告诉我
我该怎样挽留你
你才不会在破晓时离去?
2013-08-15
有时候我不想被拆开
因为我想保守一个秘密
2013-08-10
我飞的慢
是为等待花开
2013-07-21
那美好的回忆啊
总出现在夏日的梦里
让我沉溺其中
不愿醒来面对忙碌的世界
那美好的回忆啊
犹如一首熟悉的老歌
总在我耳畔响起
让我的独孤显得如此刺耳
那美好的回忆啊
就像浅滩里的海草
我已被紧紧地缠绕
无法脱离这过去的美好幻象
放我走吧,回忆
我们的世界里其实只有我
你是那虚无的梦幻
是一种不存在的影像
放我走吧,回忆
我不愿永远停留在你的世界里
那里荒芜的从草不生
那里寂寥的有如旷野
放我走吧,回忆
请打开这美丽的牢笼
请解开这温柔的镣铐
请放我走
2013-07-14
下雨了
我便是上帝所塑的泥人
担心融化在淅沥的雨中
从此无声无息的消失掉
就仿佛从来没有存在过
骄阳下
我又是那荷叶上一滴水
害怕暴晒之下渐渐蒸发
永远地悬浮于虚无之上
再也无法被瞩目被关注
我焦虑着,我辗转反侧着
我痛苦着,我心力交瘁着
只因害怕自己的突然消失
也畏惧在这绚烂的世界里
没有留下属于自己的痕迹
2013-06-24
毕业季里的每一场雨
都是一种认真的挽留
它希望推迟离别的到来
它想多留你哪怕一场雨的时间
毕业季里的每一朵云
都是一弯浅浅的暗恋
它看似卑微的虚无缥缈
其实都是一个个浪漫的挂念
毕业季里的每一阵风
都传唱着青春的旋律
它是青春不朽的美丽赞歌
它是真爱不消的永恒祝福
毕业季里的每一缕光
都是一次真实的见证
它为每一段真挚的情谊
盖上亘古不变的印章
===============================
毕业季里的每一片叶
都写着一个沉睡的秘密
它在风中犹犹豫豫的落下
最后化作被遗忘的尘埃
毕业季里的每一颗树
都是一枝忧伤的遗憾
它使得所有的梦幻变得真实
它填补了无悔年华里最后的空白
毕业季里的每一朵花
都是一回小小的误会
不同于永远伫立在那里的遗憾之树
它曾盛开但终将迎来凋零
毕业季里的每一苗草
都有着伟大的不羁梦想
虽然前路荆棘密布
但依然无法阻挡成长的渴望
===============================
毕业季里的每一杯酒
都是一幕仔细的回顾
它从一切开始的地方起
慢慢地诉说着过往的那些回忆
毕业季里的每一滴泪
都有着它朴实的原因
它为青葱岁月的每个故事
都划上了一个或悲或喜的句点
毕业季里的每一阵笑
背后似乎都隐藏着烦恼
这干净的烦恼啊
总是会在以后的日子里化作诗句
毕业季里的每一首歌
都是为离别吹响的号角
只是这号角声啊
远航的鼓励里却有着丝丝的不舍
==============================
毕业季里的每一张照片
将一个个瞬间定格为永恒
它是我们送给未来的一份礼物
一份贵重程度随时间不减反增的礼物
毕业季里的每一声叹息
都纯粹的不含伪装
它是最简单的无奈
为清晰的毕业照里留下几个的噪点
毕业季里的每一次回首
看到的都是不离去的理由
但也正是这些理由
让你不得不得踏上追逐未来的旅程
毕业季里的每一声再见
都饱含着约定与期待
它是一句短暂的誓言
它给充满离愁的毕业季一丝温暖
2013-05-04
随着醉意一起涌来的你
让我在醺醺的状态中不想清醒
我想我的人生太过理智
理智使我不得不远离你
理智的生活并不是我所厌恶的
但是它束缚了我的欲望
束缚了我不羁的天空
阻碍了我欲振翅远去
文明使我偏离了最初的自我
离自己最初的梦想越来越远
修身让我抛弃了原始的野心
我离你越近,就离佛越远
我想抛弃整个世界去拥抱你
但是我不敢,因为我怕
我怕当我抛弃了整个世界之后
你抛弃了我
所以我卑微地蜷缩在没有光照的角落
我没有死,我只是在等待
等待你来
点亮我的世界
2013-04-23
听到你的名字
我就觉得很美
它让我对你的美再一次充满幻想
它就这样轻易地把我拖入了美梦
不需要见到你
不需要拥有你
仅你的名字就可以填满我的世界
只回忆就足够我熬过思念的煎熬
我不会爱上你
也不会忘记你
你是我广袤天空中掠过的一片云
你是我深蓝大海里游弋的一只鱼
为你写下诗句
因我不能永生
2012-12-01
街道转角
我用力跑去
只为拥抱阳光
以及高墙另一边的世界
转身之后的世界
没有来得及细细分辨
一辆疾驰的什么
便把我拥入怀里
我感觉颧骨内嵌
肋骨深深扎入我的心脏
胫骨裂缝了
暖暖的血顺着我的脸颊流过
疼动感一闪而过
取而代之的
是一种前所未有的
放松
2012-11-02
那是曾经的伤口
暗示着过往岁月里的不羁
那是成长的路标
记录着青葱年华中的狂妄
那是旅行的印记
揭示了来时路的崎岖坎坷
那是英雄的勋章
它总不被想起,却也难以忘记
抚摸着它,满是回忆与故事
2012-10-27
青色的月光擦亮了墙壁
布满血迹的墙壁上满是划痕
阴森的走廊尽头传来慑人的声响
仿佛来自地狱的狂笑
万物毛骨悚然
是镣铐
是枷锁
拖着最沉重的罪孽
走向地狱最深处
2012-10-20
光明到来了
那等待着其施舍的大地
歌舞升平
它却静静的蜷缩着
目睹着肮脏和丑陋
歌功颂德
整个世界已将它遗忘
不理不睬
不闻不问
但它知道
光明褪去之后
它将坐拥整个世界
2012-10-19
黑暗无孔不入的渗了进来
绝望被加深了
四壁在收缩,地面在下沉
一切摇摇欲坠
2012-10-16
皮,包裹着丑陋的身躯
面纱,遮掩着美丽的皮
春风中摇曳着的面纱
唤起了多少次的征战
隐去了多少英雄的梦
皮紧张的收缩着
躲避着冰冷的刃
我剥去你美丽的皮
只是为了释放你渴求自由的灵魂
2012-10-15
诗人把女子写入诗中
用爱慕点缀着其韵律
用仰慕装饰着其结构
毫不掩饰
女子轻声品读着这诗
紧缩的眉心初见朱红
几滴眼泪化入诗句中
不敢相信
美貌遥远地击中诗人
诗歌轻轻地揭开面纱
如若女子可以承受
诗人便可以续写
诗中的女子不会老去
诗人却会
2012-09-10
我听不到野性的呼唤
只因有太多的安慰萦绕于耳
我看不到残酷的美
只因我们被遮蔽了双眼
我感觉不到坠落的快感
只因有过多的世俗缠绕于身
我舔不到自己的伤口
只因它被冗余的纱布覆盖
我难以体会自然的壮美
只因被钢筋水泥深深的囚禁
我无法真正地敬畏生命
只因它们总被无情的践踏
2012-08-29
双眉紧锁
你解不开心中的结
辗转反侧
我放不下对你的思念
暴晒在夏日里的时光
就这样带着你远走
我站在喧嚣的暴雨中
凝视着你窗上的影子
秋风采携着最美的花儿
为你写下最美的诗句
我执笔踟躇于半空之中
无法描绘最深的爱意
2012-07-05
我静坐在江南的竹椅上
独自享受深巷的一缕清幽
深夜也不愿睡去
却爱凝视夜空中华丽的圆月
她静静地走过
却悄然卷走了我的思绪
把它渐渐拉长,再拉长
我隐隐听到了思绪断裂的声音
2012-06-27
我在你走过的街边写下情诗
只待你惊鸿一瞥
我把自己融化在诗中
惟愿凝视你哪怕一瞬
我在你坐过的石凳旁点燃烟花
只想换来你片刻的笑靥
我无悔地燃烧自己
但求片刻垂青
2012-06-16
The lady on horseback
She remind me of the princess
For each girl is a princess inside
Only some prince did not recognise
2012-06-08
I wake up from a nice dream
In which I told you that you're in my dream
And you said you know that already
Because I told you this in your dream
2012-05-23
我才刚从寒冷的冬夜醒来
你却已收拾好了你的行囊
当我准备要捕获你的美时
你却早已转过身只留背影
我正渴望去花园与你约会
才得知那些花儿早已凋谢
在我写好情诗大声朗读时
你却已走出我声音的国度
我想最后再看一眼你的眸
你只给我一个淡淡的微笑
错过原本应是痛彻心扉的
但你的回眸融化了我的心
2012-05-20
我听到了大海的声音
虽然闭着眼
但是我却清晰地闻到
大海的味道
期待回归大海
因生命源于此
渴望遇见大海
等待那次初见
还只是闻得到海的气息
也似乎听到了海的欢笑
但是睁不开眼
在黑暗中静待
2012-05-13
我躬着身
跪倒在你的面前
额头紧贴地皮
双手无法控制地颤抖着
你看着我蜷缩的躯体
脚踩着我颤栗的肩膀
一句你嘴里的“懦夫”
直直地插进我的脊梁
流星从天上划落
夜渐渐终结
我怀揣着我的卑微
跌如谷底
2012-05-13
我在黑暗中醒来
发现自己站在峭壁之巅
回身环顾周边
我意识到我是世界之王
日出之后
我的国,便沉没了
他们夺走了我的王冠
我被囚禁在深渊之中
他们不知道
深渊之低,尽是黑暗
我的国
在黑暗中生长
2011-12-10
I thought I was dead already
I thought I would be gone forever
I thought I would be forgotten
I thought I should vanished
Untill I saw the Moon,tonight
Untill I saw warmth, I saw hope
which I once thought I don't deserve
which I once dare not to persue
She is right there in front of me
Soft and bright
Her image disappeared in the air
Hid above the cloud
Tonight, I was saved by the Moon
Tomorrow, I will fight for mine own
Only by staring at her can I
Obtain the inner peace
2011-11-14
She just stands over there
as if she's staring at future
wind was allowed to touch her hair
while the sunshine to kiss her
I stand right here
doing nothing but stare
she's the only one I care
and spend all my admire
2011-11-09
I shall not feel lonely
The day I become crazy
For it's sure and clear
I have a date in the future
I have no time to wIpe my tears
And no one really cares
I won't clean the blood on my T-shirt
Even I have a date in the future
2011-11-07
我是黑暗中的因纽特人
你是一道魔幻般的极光
被你炫丽的光芒照亮的
不仅仅是我孤单的现在
还有安稳静待你的过往
以及注定会有你的未来
To Rossi
From Inuit
2011-11-04
Haven't see you since yesterday
Where are you now?
Are you happy like I wished,
Or alone like I am?
Haven't see you since yesterday
Can you feel my missing now?
How I wish I could tell you I love you,
But I can't, 'cause the history hurts me.
Haven't see you since yesterday
And the image of you are killing me.
I hope I can hold your hand,
And ring it with all my heart.
2011-09-19
To my dear parents, to the one I loved, and to myself.
There is a moment that you suddenly noticed you are all alone in the world with no one around you.
There is a day that you start to knew there is not so many persons who cares about you.
There is a night that you have to face the darkness all by your owe with the wind crying outside
There is a period that you finally realised life do harm you and it really hurt.
2011-07-26
青青子衿,悠悠我心。
但为君故,沉吟至今。
竹马高歌,青梅羞涩。
华丽其珠,辛辛其苦。
巾帼不让,须眉浅伤。
年少轻狂,不掩锋芒。
口出狂言,动辄十年。
委屈其泪,使之惭愧。
璀璨其世,闪烁其词。
天地之间,西海岸边。
珠之华丽,贝为其衣。
转瞬四季,人生如戏。
昔我往矣,杨柳依依。
今我来思,雨雪霏霏。
融融盘阳,冰雨作殇。
其雨濛濛,其思浓浓。
如梦人生,能不琦行?
杜康解忧?难去离愁!
佳人北立,难寻其迹。
斐然文采,娉婷舞姿。
人世三载,木成其材。
关关雎鸠,在河之洲。
窈窕淑女,君子好逑。
蔚然其木,花为其子。
其位弥高,其品弥雅。
累累其果,荣荣其林。
凤舞其间,凰翔九天。
嘤其鸣矣,求其友声。
结发出行,其思匪轻。
南国都城,琴瑟互闻。
梅雨绵绵,所悟田田。
踏向而立,汲汲欲易。
青春不止,幻化作诗。
岁月匆匆,白驹无踪。
一滴眼泪,诗人已醉。
转身落幕,舟行不顾。
我喜欢你
我也喜欢你
让我睡着吧!
不管这个动荡不安的世界
让我躺在震颤的大地上
听着遍野的哀嚎,入睡
让我趴下吧!
不管肩上有多么大的重担
让我静静地蜷缩起来
保持着生命最初的形状
让我放弃吧!
不管那有多么的美好灿烂
让我可以裸身奔跑在旷野
无牵无挂
让我飞翔吧!
自由的翱翔在蓝天中
俯视着我深爱的土地
在喜悦中落下泪水
让我死亡吧!
躺在那个每个生命最终的归宿里
静静地
不用考虑醒来之后
让我失败吧!
让我沉沦吧!
让我犯错吧!
让我疯狂吧!
让我被嘲笑吧!
让我被侮辱吧!
让我被折磨吧!
让我被抛弃吧!
我闭上眼
双手合十
轻声祈祷
让我!
嘲笑小时候的自己
不敢独自面对天黑
无知带来的恐惧
终将褪去
嘲笑当年的自己
不敢面对自己喜欢的女孩
羞赧而致的心跳
渐渐平息
嘲笑现在的自己
不敢为了理想放下一切
与生俱来的懦弱
没有勇气
我轻轻拨动琴弦
熟悉的琴音再一次响起
我的眼泪掉在琴上
琴声变的沉重了许多
并不是每一次出发都有目的
但是每一次旅行却总有收获
我们抛开时间离开原点
只为拥有故事留下印记
回来的路上满载疲惫
就像启程时充满期待
旅行是一种投资
用时间去换故事
我们付出了一样的岁月
就看谁能换来
更好的回忆
站在卢浮宫
你爱上了蒙娜丽莎
我却爱上了达芬奇
我把自己嫁给了未来的你
她是一个可以抚平忧伤的巫师
只想被你想起
不想被你忘记
在遇见你之前的日子里
我觉得回声是一份礼物
因为在我孤单的岁月里
它代表着我坚强的存在
我和爸爸妈妈搬入新家
我在新家里洒满了欢笑
听着洁净的墙壁的回声
感受从头开始的新鲜感
我和小伙伴们爬上高山
冲着连绵的大青山呼喊
听着被大山放大的回声
感觉有一种征服的豪迈
走入凌乱的大学新宿舍
妈妈的话在安慰着失落
短暂急促的回声徘徊着
离家的自由感被淹没了
游荡在燥热喧嚣的街头
汽车的喇叭声尖叫而过
余音在我脑中变成回声
撞击着我昏沉沉的思绪
在遇见你之后的日子里
回声遂变成了一把尖刀
因为二人世界里的回声
代表着一种深深地孤独
是有人说起思念么?
说长久的别离才引发思念?
我想我是不能赞同的
因为我觉得思念是一朵昙花
思念是一朵昙花
只要短暂的瞬间
就会迎来盛开
这思念的盛开
你从樱花树下走过
樱花盛开了
我拾起飘落在地花瓣
把它夹到我的日记本里
天神都嫉妒我
我中了宙斯了诅咒
生命变成了花期
我成了一朵樱花
早春时节
我与伙伴们一起开放
但是我知道
我们有着不同的期许
它们把美献给天空
把浪漫献给大地
而我则躲在枝头
默默地等待着
春天很快就消失了
就像她来的那样突然
同伴们一个个离我而去
留给我等待的祝福
我忍耐着孤独寂寞
等待着
我熬到了炎热的盛夏
远远地看到了你的到来
你从樱花树下走过
最后一朵樱花落下
我拜倒在你的面前
你拾起了我
你把我夹到了那本日记中
我看到了自己文字
读到了自己的情诗
我看到了那片樱花
如果我鲁莽的表白
使你难堪,请告诉我
我可以为你擦去
我写在阿尔卑斯山上的诗文
如果我所谓的真诚
被你厌恶,请告诉我
我愿意为了爱情
埋葬我的引以为傲
如果我嘶声力竭的歌声
打扰了你,请告诉我
我愿意为了你的宁静
让巫师把我变成哑巴
如果我随心撒就的文字
不能被你所理解,请告诉我
我愿意为了得到你的肯定
让上帝拿走我所有的才华
甚至
如果我晃动的名字
不小心出现在了你的脑海
请告诉我
我愿意诅咒我的名字
只怪身不由已
我爱你
2010-12-27
为什么你要独自在街道上漫步,
丝毫不顾及这寒冷的夹杂着旧报纸的风?
只是因为思念吗?
还是因为无家可归?
为什么你独自一人坐在公交车上,
任由它把你带到任何地方?
你已经看不到未来了吗?
所以这样的随波逐流?
为什么你独自跑上楼梯,
躺在肮脏的满是灰尘的屋顶?
只因这里没人看到你的脆弱?
还是这样站在蓝天下才觉得自由?
2010-11-19
在她纯洁外表的掩盖之下
她迷惑了很多行路人
他们徘徊在她温柔的身体里
迷失了原来的方向
2010-11-17
我想在你面前说话
以展示自己可怜的聪明才智
可是我却不敢
因为我担心自己词不达意
于是我默默地站在
距离你一个微笑的地方
期待你的微笑 可以融化我的腼腆
2010-11-12
我娇羞地低着头
因为我感觉到了你的靠近
你距离我是那么的近
以至于我都可以感受到你等待的气息
我眼角的余光
看到了你红色的皮鞋
你轻轻地弯下腰
可是却并没有说一个字
你的安静的等待
终于打动了我
我抬起头问道:
老师,真的要收卷了吗?
2010-11-12
你在等待着什么
却不知道我在等待着你
我双手托腮
心怦怦的跳着
你没有注意到这一切
依然在左顾右盼
我紧紧注视着你
期待与你的目光相遇
终于,我们四目相对
我惊喜之情溢于言表
我轻声说:
老师,你TMD怎么还不发卷?
2010-11-01
好想告诉你
我在追逐着最美的东西
我行走在彩虹护卫的蓝天下
看到了最美的落日
我登上了挡住太阳的大山
闻到了最清爽的空气
我看到两个太阳在海的尽头融合
然后一同消失在海天交接之处
我乘着小舟向蓝色的远处划去
划呀划,黑夜就这样来临了
我抬头看着一颗流星
她落了下来,就在不远处
我跳下海奋力地向她游去
游啊游,黑夜就这样过去了
我看到太阳藏在云彩里
娇羞地看着我
我抬头享受着温暖的阳光
然后向白云飞去
躺在软软的云彩里
我睡着了
我醒来,看到太阳又轻轻划走
我纵身跳下云端,掉在蚂蚁的背上
我指挥着它向着太阳的方向前行
它只频频点着头,不说什么
我们遇到了蝴蝶
她说这样赶不上太阳
蝴蝶载着我
我们一起飞向远方
飞着飞着,蝴蝶累了
于是我们就停在花园休息
好想告诉你
我依旧在追逐着最美的东西
太阳再次躲在了那座山后
我想要马上离开花园
可是,蝴蝶爱上了玫瑰
于是,我开始一个人的征程
黑夜里,我不害怕
因为有萤火虫陪伴着我
当深蓝的月亮游入星海
萤火虫们就都随着召唤走了
月亮悄悄告诉我
“太阳在你后面”
我不相信,因为我看见
太阳从我前面落下
月亮摇着头,叹息着
她走了,也带走了星星
真正的夜突然降临
我开始害怕黑夜的魂
一柄白色的剑割破了黑夜的魂
我睁开了紧闭的双眼
另一个太阳
从我身后懒懒地爬起
她昏昏的,懒懒地
不是我一直追逐的那个
于是我柔柔眼睛
向着日落的方向,继续赶路
2010-07-07
像个哲学家一样忧伤的思考人生,却
像个艺术家一样疯狂的热爱生活。
像个文学家一样执著的记录感受,也
像个摄影家一样痴迷于定格画面。
像个探险家一样探索未知的领域,还
像个音乐家一样沉迷于古典的美。
像个战士一样无畏死亡,却
像个诗人一样且吟且泣。
像个学者一样追求知识,也
像个乞丐一样乞讨爱情。
像个绅士一样待人处事,还
像个幼童一样心直口快。
2009-10-24
几个小时之前,我还在寝室里和室友在一起,他们为我践行,几个小时之后,我来到了这个万里之外的城市……
寂静的校园的小路
昏黄的迷漫水雾的灯光
寂寞的路灯静静的守候一隅黑暗
我带着一顶红色的鸭舌帽
微微的低着头
故意挡着自己的脸
我站在你必然经过的路灯下
与寂寞的路灯静静守候你的到来
希望你可以在即使分开很久之后依然可以辨认我
你的声音从不远处传来
我收紧了灵魂
听着你的渐渐靠近的脚步声
你的声音是如此的熟悉
因为我不止一次的在梦中温习
当你从我身边走过时
我甚至可以看的到你耳后零乱的绒发
我甚至可以闻的到你身上淡淡的熏衣草香味
可是
即使是如此近的距离
你都不曾感受到我的存在
我默默的背起丢在草丛里包
抚摸着寂寞的路灯
不忍再看它斑驳的身体
又经过了几个小时,走在自己的校园里,路灯下,没有人……
2009-07-02
芸芸众生,我急驶而过
匆匆一瞥,我抛出不懈的目光
盛世角落,我看到一个女孩哭泣
繁华背后,肮脏的现实让我不忍细看
浮躁的快乐终将归于空虚
狂欢的结束曲往往是孤独
狂欢是因为害怕分离
可狂欢后的分离更让人难以接受
当现在只为等待将来时
往昔岁月里的点点滴滴就会闪现在脑海
回忆中的一切都比现实美好
所以喜欢回忆的人都是理想主义者
干净的痛苦一定会沉淀
沉淀成悠闲
付出总该有回报
那些或痛苦或美好的回忆就是回报
总有些东西是你无法摆脱的
即使你不想不愿让我进入你的世界
但是我知道,在你的回忆中
一个不懂事的男孩,你已经无法躲避
生命就是这样,你无法安排自己的回忆
只能接受,然后不经意的想起
心中也许会暗暗感慨:
我最美好的回忆,怎么会有他突然闪现?
回忆总是由意外组成
你无法为自己创造回忆
你总是记起你在躲避的
苦思冥想也找不到当初的美好
三年好短暂,刚想要珍惜你
却不得不对着你的背影说再见
不在一起的日子,总在幻想
未来的岁月里,我的名字会闪现在你的脑海里吗?
三年好漫长,分离拉长了思念
长长的思念,注定会缠绕我的一生
谁都没想都居然是这样的结局
一路保重,我的同学们,我会默默地祈祷
2009-03-28
我爱你,
在你面前我没有自己,
每次站在你的附近,
我感觉自己仿佛溶解了,
溶解在了空气中,
溶解在了对你的迷恋中,
然后围绕着你旋转。
我爱你,
我的世界里只有你。
突然找到的那种诗人描述的感觉:
世界很大,
你们周围有很多的人,
可是你却对这繁华世界与茫茫人海毫不在意,
你只能看到她一个人,
你感觉你的世界里再什么都没有了。
我爱你,
我想为你写歌,
写世界上最动听的情歌,
可是我发现我才华不够,
于是我在现有的歌曲中寻找,
找那个最适合我唱给你听的歌,
可是找不到。
我爱你,
在你的世界里,
我甘愿做个稻草人,
没有**,
没有要求,
只愿默默为你守候一片麦田,
静静等待你的爱。
2009-03-15
我爱你,
爱的不是你的容貌,
所以不会因为你渐渐老去而抹去对你的爱;
我爱你,
爱的不是你的财富,
所以不会因为你穷困潦倒而丢失对你的爱;
我爱你,
爱的不是你的地位,
所以不会因为你一落千张而抛弃对你的爱;
我爱你,
爱的不是你的笑容,
所以不会因为你痛哭流涕而揉碎对你的爱;
我爱你,
爱的不是你的歌喉,
所以不会因为你声音嘶哑而埋葬对你的爱;
我爱你,
爱的不是你的爱我,
所以不会因为你不理不睬而放弃对你的爱;
我爱你,
爱的不是你的舞姿,
所以不会因为你停止跳舞而终止对你的爱;
我爱你,
爱的是你善良的心灵,
所以不会在意外在的一切;
我爱你,
爱的是你高贵的灵魂,
所以不会受任何杂念影响;
我爱你,
爱的是完完全全的你,
所以会永远爱下去
2008-01-29
如果敌人让你生气,那说明你还没有胜他的把握。
如果朋友让你生气,那说明你仍然在意他的友情。
在写上一篇博客( #43 )的时候,想要用一种暴力的解法去完成,但是遇到一个问题,就是求一个数组里面所有数的组合,于是就先来看组合了。
给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。
示例:
输入: n = 4, k = 2
输出:
[
[2,4],
[3,4],
[2,3],
[1,2],
[1,3],
[1,4],
]
动态规划解法的关键,依然是找到递推公式,也就是拆分子问题的方式,在本题中,我们可以利用组合的计算公式
C(n,k)=C(n-1,k-1)+C(n-1,k)
这个公式的物理意义:n个数中选k个,可以分为“包含n这个数字(剩下的n-1
个中再取剩下的k-1
)”和“不包含n这个数字(剩下的n-1
个中全完全部k
个)”两类,分别表示为C(n-1,k-1)
和`C(n-1,k)·。有了上面的公式,那么代码也相对简单了许多
class Solution {
public List<List<Integer>> combine(int n, int k) {
if (n == k) {
List<Integer> row = new ArrayList<>();
for (int i = 1; i <= n; i++) {
row.add(i); // select all
}
List<List<Integer>> ret = new LinkedList<>();
ret.add(row);
return ret;
}
if (k == 0) {
List<Integer> row = new ArrayList<>();
List<List<Integer>> ret = new LinkedList<>();
ret.add(row);
return ret;
}
List<List<Integer>> ret = this.combine(n - 1, k - 1);
for (List<Integer> list : ret) {
list.add(n);
}
ret.addAll(this.combine(n - 1, k));
return ret;
}
}
这里需要稍微说一下n == k
内部的for
循环里面,是把这次的组合的结果存储了起来,我刚开始看的时候,以为应该是row.add(1);
,理解其实有误,这里并不是求有几种组合方法,而是要展示具体的组合结果。
还有就是最后一个循环内部的list.add(n);
,因为this.combine(n - 1, k - 1);
里面的所有组合都没有n
,这里需要都给补上,这个公式C(n,k)=C(n-1,k-1)+C(n-1,k)
只是保证了数量上的相等,所以这里需要单独添加n
;但是在this.combine(n - 1, k)
中,因为本身就没有包含n
,所以就不需要额外的添加工作了。
相比于方便快捷的Python,C++的常用操作确实是匮乏很多,最近项目需要分割中文字符串,我这个C++新手在网上找了好长时间都没有结果,最后还是歪打正着的找到了这个SO的问答,才有了眉目。
#include <iostream>
#include <string>
#include <vector>
std::vector<std::string> split_chinese(std::string s) {
std::vector<std::string> t;
for (size_t i = 0; i < s.length();)
{
int cplen = 1;
// 以下的几个if,要参考这里 https://en.wikipedia.org/wiki/UTF-8#Description
if ((s[i] & 0xf8) == 0xf0) // 11111000, 11110000
cplen = 4;
else if ((s[i] & 0xf0) == 0xe0) // 11100000
cplen = 3;
else if ((s[i] & 0xe0) == 0xc0) // 11000000
cplen = 2;
if ((i + cplen) > s.length())
cplen = 1;
t.push_back(s.substr(i, cplen));
i += cplen;
}
return t;
}
int main(int argc, char *argv[])
{
std::string s = "这是一组中文";
std::vector<std::string> t = split_chinese(s);
for(auto a : t) {
std::cout << a << std::endl;
}
return 0;
}
偷得浮生半日闲,最近外派归来,新的任务还没下来,刚好赋闲几天。
前几天在论坛里看到一个不错的创业公司的招聘贴,里面有一个加分项,即个人博客,让我想起来了我拿域名都过期了的博客,于是改了下Github Page的CNAME,想着直接用shellbye.github.io就好,结果整来整去,每次对shellbye.github.io的访问都被重定向了到老域名,折腾了好久,实在没看出来哪里有问题,于是给Github Page发了邮件,在客服人员的回复下才知道,原来浏览器还会缓存域名(301),于是清空了缓存,一切就正常了。
说起来收拾博客,还有一个原因,就是因为一直找不到合适的读源码的相关的书籍或者博客,恰好我年前读了几天bottle
,于是我就冒出来一个想法,是不是可以尝试写本书,记录读源码的过程,一方面激励自己读源码,一方面也可以解决市面上目前没有读源码相关书籍这样一个问题。不过我也知道,这肯定不是一个一蹴而就的事儿,真要做起来,一定是需要很大的付出,否则难成。
写书这个事儿,除了需要很大的付出外,其实好处真的是非常多的,
首先,这是一个非常好的提高自己代码能力的机会。就我目前看来,还真很难找到合适的途径,能够快速的提高代码水平和能力的途径。我之前有过一个小小的尝试,读了howdoi,收获其实就非常大,只可惜自己没有长期坚持下来,否则一定是有长足的进步的
其次,这也是一个提升知名度的机会。阮一峰就是通过一系列的博客教程,进而在业界有了一定的知名度,这不仅在工作中有一定好处,就是寻找下一个工作机会或者创业,也是有很多的好处的。
最后,说不定还可以赚点钱。😊
目前还仅仅只是一个idea,而且产生这个idea很大程度上也是自己最近比较闲,说不定过几天忙起来,也就彻底没这个念头了,哈哈哈哈
在JPA
和Hibernate
的帮助下,Spring
用起来也和Django
很像,可以自动的从model
生成数据库表。我最近在项目中遇到一个需要给所有的表都加一个tb_
的前缀,从网上找了找,果然可以做到,下面简单记录一下。
本项目的整个工程链接在Github,下面是最核心的文件PrefixTbNamingStrategy.class
package com.example.demo;
import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;
import org.springframework.context.annotation.Configuration;
import java.util.Locale;
@Configuration
public class PrefixTbNamingStrategy implements PhysicalNamingStrategy {
@Override
public Identifier toPhysicalCatalogName(Identifier name,
JdbcEnvironment jdbcEnvironment) {
return apply(name, jdbcEnvironment);
}
@Override
public Identifier toPhysicalSchemaName(Identifier name,
JdbcEnvironment jdbcEnvironment) {
return apply(name, jdbcEnvironment);
}
@Override
public Identifier toPhysicalTableName(Identifier name,
JdbcEnvironment jdbcEnvironment) {
Identifier identifier = apply(name, jdbcEnvironment);
String tbName = "tb_" + identifier.toString();
return new Identifier(tbName, identifier.isQuoted());
}
@Override
public Identifier toPhysicalSequenceName(Identifier name,
JdbcEnvironment jdbcEnvironment) {
return apply(name, jdbcEnvironment);
}
@Override
public Identifier toPhysicalColumnName(Identifier name,
JdbcEnvironment jdbcEnvironment) {
return apply(name, jdbcEnvironment);
}
private Identifier apply(Identifier name, JdbcEnvironment jdbcEnvironment) {
if (name == null) {
return null;
}
StringBuilder builder = new StringBuilder(name.getText().replace('.', '_'));
for (int i = 1; i < builder.length() - 1; i++) {
if (isUnderscoreRequired(builder.charAt(i - 1), builder.charAt(i),
builder.charAt(i + 1))) {
builder.insert(i++, '_');
}
}
return getIdentifier(builder.toString(), name.isQuoted(), jdbcEnvironment);
}
/**
* Get an identifier for the specified details. By default this method will return an
* identifier with the name adapted based on the result of
* {@link #isCaseInsensitive(JdbcEnvironment)}
*
* @param name the name of the identifier
* @param quoted if the identifier is quoted
* @param jdbcEnvironment the JDBC environment
* @return an identifier instance
*/
protected Identifier getIdentifier(String name, boolean quoted,
JdbcEnvironment jdbcEnvironment) {
if (isCaseInsensitive(jdbcEnvironment)) {
name = name.toLowerCase(Locale.ROOT);
}
return new Identifier(name, quoted);
}
/**
* Specify whether the database is case sensitive.
*
* @param jdbcEnvironment the JDBC environment which can be used to determine case
* @return true if the database is case insensitive sensitivity
*/
protected boolean isCaseInsensitive(JdbcEnvironment jdbcEnvironment) {
return true;
}
private boolean isUnderscoreRequired(char before, char current, char after) {
return Character.isLowerCase(before) && Character.isUpperCase(current)
&& Character.isLowerCase(after);
}
}
这个文件基本上copy自org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
,只在方法toPhysicalTableName
上做了一个最简单的修改,实现了想要的功能。代码应该是还可以进一步精简的(比如直接继承SpringPhysicalNamingStrategy
),但是这里只是记录一种思路,就不进行精简了。
这里要特别说明一下顶部的注解@Configuration
,如果不加这个注解,就需要在application.properties
里面添加如下一行
spring.jpa.hibernate.naming.physical-strategy=com.example.demo.PrefixTbNamingStrategy
related_name
首先是related_name
,官网是这么说明的:
The name to use for the relation from the related object back to this one.
这个名字是用来反向查找与其有关的对象时使用的
我看过好几次,但是一直没搞明白是啥,知道实际再别人的代码看到具体的使用,才明白。举例说假设你有如下的models.py
文件,用来定义老师与学生:
class Teacher(models.Model):
name = models.CharField(max_length=50)
class Student(models.Model):
name = models.CharField(max_length=50)
tch = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True)
通过学生获取老师是比较直观的,
stu = Student.objects.get(id=1)
tch = stu.tch
反过来要通过老师获取学生,Django官方有相应的操作,但是不是特别直观,
tch = Teacher.objects.get(id=1)
students = tch.student_set.all()
这样的有一个问题就是当一个学生表里有多个老师的外健时,如下:
class Student(models.Model):
name = models.CharField(max_length=50)
tch = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True)
headmaster = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True)
这个反向查找就会报错,因为有两个外健,Django不知道在逆向查找时该找哪一个,这个时候就是related_name
发挥作用的时候了:
class Student(models.Model):
name = models.CharField(max_length=50)
tch = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True, related_name='class_stu')
headmaster = models.ForeignKey(Teacher, on_delete=models.SET_NULL, null=True, related_name='school_stu')
这时,班级老师要查找学生就可以通过设置好的related_name
进行了:
tch = Teacher.objects.get(id=1)
students = class_stu
而校长则是通过另一个来获取本校的所有学生
tch = Teacher.objects.get(id=1)
students = school_stu
prefetch_related
有了related_name
,可以方便的反向查找与其关联的对象,但是因为要多次方法数据库,效率不够高,所以就有了prefetch_related
最近在做语音合成的相关工作,其中用的一个功能点就是获取汉字的拼音,这个在Python
中可以通过pypinyin
来完成,
>>> from pypinyin import pinyin
>>> pinyin('中心')
[['zhōng'], ['xīn']]
但是在我们的项目中,需要的是C++
版本的,所以需要把这部分功能转化为C++
代码,看了一下pypinyin
的代码,大概就是先把汉字转化成code point
,然后在一个超级大的字典里找这个汉字的拼音。于是,把这个超级大的字典拷贝出来放到C++``里就基本大功告成了,但是还缺一步,就是汉字到code point
的过程。在Python
中,汉字到code point
就是一行代码,
ord('白')
30333
但是就这样一个简单的操作,把它翻译到C++
缺用了我很长时间(很大程度上也是因为我对C++
不熟),最终还是在万能的SO
上找到了解决的代码:
#include <iostream>
#include <string>
#include <codecvt>
#include "data_pinyin.hpp"
int main(int argc, char *argv[])
{
if (argc < 2){
std::cerr << "Usage: " << argv[0]
<< " chararters" << std::endl;
return 1;
}
std::string name = argv[1];
std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> cv;
auto str32 = cv.from_bytes(name);
int name_code_point = 0;
for (auto c : str32)
name_code_point = uint_least32_t(c);
std::cout << name_code_point << std::endl;
std::cout << pinyin_dict[name_code_point] << std::endl;
return 0;
}
其中的data_pinyin.hpp
就是我从pypinyin
拷贝出来的那个超级大的字典,并经过简单的处理(16进制转为了10进制)转成了C++
相应的格式,就放几行看一下吧,pypinyin
的原始文件在这里,
#include <map>
std::map<int, std::string> pinyin_dict = {
{12295, "líng,yuán,xīng"},
{13312, "qiū"},
{13313, "tiàn"},
{13316, "kuà"},
{13317, "wǔ"},
// 中间省略
{183944, "yǐ"},
{183955, "chǔ"},
};
完成之后就是编译运行了
$ g++ -std=c++11 -g main.cpp -o main.out
$ ./main.out 白
30333
bái,bó
缩写 | 全写 | 含义 | 参考 |
---|---|---|---|
JRE | Java Runtime Environment | It is a package of everything necessary to run a compiled Java program, including the Java Virtual Machine (JVM), the Java Class Library, the java command, and other infrastructure. However, it cannot be used to create new programs. | 1 |
JDK | Java Development Kit | is an implementation of either one of the Java Platform, Standard Edition, Java Platform, Enterprise Edition, or Java Platform, Micro Edition platforms[1] released by Oracle Corporation in the form of a binary product aimed at Java developers on Solaris, Linux, macOS or Windows. | 1, 2 |
JPA | Java Persistence API | The most famous JPA provider is Hibernate. is a standard for ORM |
1 |
JDBC | Java Database Connectivity | The Java Database Connectivity (JDBC) API is the industry standard for database-independent connectivity between the Java programming language and a wide range of databases | 1 |
JSF | JavaServer Faces | JavaServer Faces (JSF) is a Java specification for building component-based user interfaces for web applications[1] and was formalized as a standard through the Java Community Process being part of the Java Platform, Enterprise Edition. | 1 |
IoC | Inversion of Control | In software engineering, inversion of control (IoC) is a design principle in which custom-written portions of a computer program receive the flow of control from a generic framework. | 1, #36 |
DI | Dependency Injection | In software engineering, dependency injection is a technique whereby one object supplies the dependencies of another object. A dependency is an object that can be used (a service). An injection is the passing of a dependency to a dependent object (a client) that would use it. The service is made part of the client's state. | 1, 2 |
POJO | Plain Old Java Object | In software engineering, a Plain Old Java Object (POJO) is an ordinary Java object, not bound by any special restriction and not requiring any class path. | 1 |
JavaBeans | JavaBeans | A JavaBean is a POJO that is serializable, has a no-argument constructor, and allows access to properties using getter and setter methods that follow a simple naming convention. | 1 |
EJB | Enterprise JavaBeans | EJB is a server-side software component that encapsulates business logic of an application. An EJB web container provides a runtime environment for web related software components, including computer security, Java servlet lifecycle management, transaction processing, and other web services. The EJB specification is a subset of the Java EE specification. | 1 |
DTO | Data Transfer Object | is an object that carries data between processes. The difference between data transfer objects and business objects or data access objects(DAO) is that a DTO does not have any behavior except for storage, retrieval, serialization and deserialization of its own data (mutators, accessors, parsers and serializers) | 1 |
DAO | Data Access Object | is an object that provides an abstract interface to some type of database or other persistence mechanism. | 1 |
ORM | Object-Relational Mapping | Object-relational mapping (ORM, O/RM, and O/R mapping tool) in computer science is a programming technique for converting data between incompatible type systems using object-oriented programming languages. | 1 |
CRUD | Create, Read, Update, Delete | 1 | |
Java SE | Standard Edition | For general-purpose use on desktop PCs, servers and similar devices. | 1 |
Java EE | Enterprise Edition | Java SE plus various APIs which are useful for multi-tier client–server enterprise applications. Java EE built on top of Java SE, it provides libraries for database access (JDBC, JPA), remote method invocation (RMI), messaging (JMS), web services, XML processing, and defines standard APIs for Enterprise JavaBeans, servlets, portlets, Java Server Pages, etc. |
1, 2 |
JNDI | Java Naming and Directory Interface | The Java Naming and Directory Interface (JNDI) is a Java API for a directory service that allows Java software clients to discover and look up data and resources (in the form of Java objects) via a name. | 1,2,3 |
Java RMI | Java Remote Method Invocation | In computing, the Java Remote Method Invocation (Java RMI) is a Java API that performs remote method invocation, the object-oriented equivalent of remote procedure calls (RPC), with support for direct transfer of serialized Java classes and distributed garbage-collection. | 1 |
JMH | Java Microbenchmark Harness | JMH is a Java harness for building, running, and analysing nano/micro/milli/macro benchmarks written in Java and other languages targetting the JVM. | 1, 2 |
AOP | Aspect Oriented Programming | Aspect-Oriented Programming (AOP) complements Object-Oriented Programming (OOP) by providing another way of thinking about program structure. The key unit of modularity in OOP is the class, whereas in AOP the unit of modularity is the aspect. Aspects enable the modularization of concerns such as transaction management that cut across multiple types and objects. (Such concerns are often termed crosscutting concerns in AOP literature.) | 1 |
HATEOAS | Hypermedia as the Engine of Application State | is a means of creating self-describing APIs wherein resources returned from an API contain links to related resources. This enables clients to navigate an API with minimal understanding of the API’s URLs. | |
DO | Data Object | 与数据库表结构一一对应,通过DAO层向上传输数据源对象 | 1 |
DTO | Data Transfer Object | 数据传输对象,Service或Manager向外传输的对象 | |
BO | Business Object | 业务对象。 由Service层输出的封装业务逻辑的对象 | |
AO | Application Object | 应用对象。 在Web层与Service层之间抽象的复用对象模型,极为贴近展示层,复用度不高 | |
VO | View Object | 显示层对象,通常是Web向模板渲染引擎层传输的对象 | |
DAO | Data Access Object | 数据存取对象,位于业务逻辑和持久化数据之间,实现对持久化数据的访问 | |
EDA | Event Driven Architecture |
作为技术人员,一个良好的技术环境是非常重要的,一个人闭门造车往往容易误入歧途。比如在之前的业务中,需要使用Logstash从MySQL中读取数据( #8 )并导入到ElasticSearch中,由于数据量非常大(7千万),没法一次全部读取到内存中,所以就只能使用limit一次一点的去读取(一次一万),使用的是大约如下的SQL
sql> select * from table_name limit 1000000,2;
然后经过简单处理之后导入到MySQL中,程序刚启动时,读取一次非常快,后面就渐渐变慢了,
[2018-12-27 11:43:56] 2 rows retrieved starting from 1 in 7m 25s 157ms (execution: 7m 25s 99ms, fetching: 58ms)
不过因为这个整体的导入是一个非常低频的操作,所以也就没有深究。
后来因为人员变动,我的工作要交接给另一个大牛,他发现了这个问题,然后把我的limit换成了between,产生了奇效!
sql> select * from table_name where id BETWEEN 1457581 and 1457582
[2018-12-27 11:44:03] 2 rows retrieved starting from 1 in 92ms (execution: 88ms, fetching: 4ms)
不得不说虽然基本上实现了同样的功能,但是他们之间的差距真的是非常的大,原来limit并不是我想象的那种根据索引直接查询,而且要全表走一遍。以下是两个sql语句的详细分析
mysql> explain select * from table_name limit 1000,1;
+----+-------------+------------+------------+------+---------------+------+---------+------+-----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------------+------+---------------+------+---------+------+-----------+-------+
| 1 | SIMPLE | table_name | NULL | ALL | NULL | NULL | NULL | NULL | 23730731 | NULL |
+----+-------------+------------+------------+------+---------------+------+---------+------+-----------+-------+
1 row in set, 1 warning (0.01 sec)
mysql> explain select * from table_name where id between 1000 and 1010;
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | table_name | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 1 | Using where |
+----+-------------+------------+------------+-------+---------------+---------+---------+------+------+-------------+
1 row in set, 1 warning (0.00 sec)
对比上面的expalin的输出结果,我们可以看到不同的地方主要是type
和rows
。下面就结合官方文档来看看不一样的地方到底有啥差别:
The type column of EXPLAIN output describes how tables are joined.
range
Only rows that are in a given range are retrieved, using an index to select the rows. The key column in the output row indicates which index is used. The key_len contains the longest key part that was used.
ALL
A full table scan is done for each combination of rows from the previous tables.
The rows column indicates the number of rows MySQL believes it must examine to execute the query.
For InnoDB tables, this number is an estimate, and may not always be exact.
从上面的解释可以看出来,当type
是range的时候,只有符合条件的数据才会被拉取,而且还利用到了索引,但是如果是ALL,需要做一个全表扫描。所以就慢了非常非常多。而且rows
也给出了大约的需要访问的数据条数,这也是一个重要的参考指标。
当你按照完MySQL
之后( #18 ),要想用Django
连接MySQL
,需要mysqlclient
,其安装之前,依赖mariadb-devel
sudo yum install mariadb-devel -y
pip install mysqlclient
最近在公司内部转岗到了AI lab,开始做模型工程化的工作,也开始接触C++了,估计会有不少新的笔记产生,毕竟是从头开始学一个全新的语言。
今天尝试了从零开始在VS2015中配置boost,网络上的中文资料帮了一些忙,但是依然没有完全配置好,最终还是参考了官方文档,才解决了遇到的问题。
从官网下载最新的包即可,解压之后进入目录中,依次执行
bootstrap
.\b2
这里可能需要很久,完成之后就需要打开VS2015进行如下三项配置:
在工程中右键-->属性
然后就可以啦
yum -y update
yum groupinstall -y development
yum install -y zlib-dev openssl-devel sqlite-devel bzip2-devel
yum install xz-libs -y
wget https://www.python.org/ftp/python/3.6.5/Python-3.6.5.tar.xz
xz -d Python-3.6.5.tar.xz
tar -xvf Python-3.6.5.tar
cd Python-3.6.5
./configure --prefix=/usr/local
make && make altinstall
python3.6 -V
接上回( #32 ),可以把转成vector<char>
的二进制文件进一步转成std::string
:
std::string str = "This is a test";
std::cout << str << std::endl;
std::vector<char> vc(str.begin(), str.end());
for (auto c : vc) {
std::cout << c << std::endl;
}
std::string str2(vc.begin(), vc.end());
std::cout << str2 << std::endl;
The output is:
This is a test
T
h
i
s
i
s
a
t
e
s
t
This is a test
这次的这个动态规划题目,我打算先自己来,把自己的思考过程记录下来。
帮派里有 G 名成员,他们可能犯下各种各样的罪行。
第 i 种犯罪会产生 profit[i] 的利润,它要求 group[i] 名成员共同参与。
让我们把这些犯罪的任何子集称为盈利计划,该计划至少产生 P 的利润。
有多少种方案可以选择?因为答案很大,所以返回它模 10^9 + 7 的值。
示例 1:
输入:G = 5, P = 3, group = [2,2], profit = [2,3]
输出:2
解释:
至少产生 3 的利润,该帮派可以犯下罪 0 和罪 1 ,或仅犯下罪 1 。
总的来说,有两种方案。
示例 2:
输入:G = 10, P = 5, group = [2,3,5], profit = [6,7,8]
输出:7
解释:
至少产生 5 的利润,只要他们犯其中一种罪就行,所以该帮派可以犯下任何罪行 。
有 7 种可能的计划:(0),(1),(2),(0,1),(0,2),(1,2),以及 (0,1,2) 。
提示:
1 <= G <= 100
0 <= P <= 100
1 <= group[i] <= 100
0 <= profit[i] <= 100
1 <= group.length = profit.length <= 100
题目的条件翻译成数学语言应该是
groug(1)+groug(2)+groug(i)+...+groug(n) <= G
profit(1)+profit(2)+profit(i)+...+profit(n) >= P
因为这个题目是关于动态规划的,那么就考虑用动态规划的套路来解,首先就是考虑怎么把问题拆分成子问题,首先想到的就是这个分组的方法,把group
里面的数划分成两部分(其中一部分为X
个),那么其盈利方式sep(G)
就可以表示成两个子问题的和
sep(G) = sep(G-X)+sep(X)
这样看起来似乎没问题,但是这里有一个问题,就是拆分的这两部分单独都无法满足条件,但是他们加起来是可以的,所以这样的划分方法是不对的。既然想不到一些优雅的方法,那就看看有没有暴力解法,这个题目里的暴力解法还是比较明显,就是求group
的所有组合(参考求组合的方法 #44 ),然后分别计算这些组合的盈利能力。
class Solution {
public int profitableSchemes(int G, int P, int[] group, int[] profit) {
int count = 0;
List<List<Integer>> allCom = this.combine(group.length, 1);
for (int i = 2; i <= group.length; i++) {
List<List<Integer>> t = this.combine(group.length, i);
allCom.addAll(t);
}
for (List list : allCom) {
int total_g = 0, total_p = 0;
for (Object i : list) {
int idx = (Integer)i - 1;
// check profit
total_g += group[idx];
total_p += profit[idx];
}
if (total_g <= G && total_p >= P)
count++;
}
return count;
}
public List<List<Integer>> combine(int n, int k) {
if (n == k) {
List<Integer> row = new ArrayList<>();
for (int i = 1; i <= n; i++) {
row.add(i); // select all
}
List<List<Integer>> ret = new LinkedList<>();
ret.add(row);
return ret;
}
if (k == 0) {
List<Integer> row = new ArrayList<>();
List<List<Integer>> ret = new LinkedList<>();
ret.add(row);
return ret;
}
List<List<Integer>> ret = this.combine(n - 1, k - 1);
for (List<Integer> list : ret) {
list.add(n);
}
ret.addAll(this.combine(n - 1, k));
return ret;
}
}
很显然,这是一个开销太大的方法,它不分青红皂白的检查了所有的组合(关键是还计算了所有组合),其中其实很有大一部分是可以省略的。
在探索动态规划的解法过程中,看到这篇文章的一个代码量特别小的解法,但是我还没搞懂其中
if(G >= group[k])
{
int tp = P - profit[k];
if(tp < 0) tp = 0;
ans = profitableSchemes(flag, G-group[k], tp, group, profit, k+1);
}
ans = (ans + profitableSchemes(flag, G, P, group, profit, k+1))%1000000007;
其中的两个profitableSchemes
,难道不是都考虑了包含k
的情况?第一种是显然包含k
的,所以需要再考虑除了k
之外剩下有多少中方法;第二种,讨论k + 1
,难道不是也相当于把k
考虑进去了? 🤔
在看了两边这个Youtube的视频讲解之后,我终于搞懂了上面的代码的含义,然后我用了一个相对简单的代码实现了一遍,因为没有存储中间的计算结果,所以超时了,但是逻辑起码是对的
class Solution {
public int profitableSchemes(int G, int P, int[] group, int[] profit) {
return profitableSchemesV1(G, P, group, profit, 0);
}
public int profitableSchemesV1(int G, int P, int[] group, int[] profit, int k) {
// 退出条件,类似斐波那契数列里面的Fib(0)和Fib(1)
if (k >= group.length && P <= 0 && G >= 0)
return 1;
if (G < 0)
return 0;
if (k >= group.length)
return 0;
int count = 0;
if (G >= group[k]) {
// 做第k个任务的情况下,后面的情况就是G和P都减少了
int tp = P - profit[k];
if (tp < 0)
tp = 0;
count += this.profitableSchemesV1(G - group[k], tp, group, profit, k + 1);
}
// 第k个任务不错,所有的G和P都留给后面消耗
count += this.profitableSchemesV1(G, P, group, profit, k + 1);
return count;
}
}
注释里已经写了我最新的理解,这次应该是没有问题了,之前的问题在于我在“反向理解”,总把profitableSchemesV1(G, P, group, profit, k + 1)
理解成“前”k
次的结果,但其实应该按照注释里理解的那样,而是后k + 1
的结果。那么我们最初的公式就需要修改一下了
sep(G) = sep(G - K)+sep(G + K)
这里公式的物理意义就是:G
这么多人,要实现P
这么多的利润,有两种方法,即做K
这个方案和不做K
这个方案。
上面的解法虽然逻辑上是对的,但是因为大量的重复计算导致效率非常低,我没仔细算,但是我估计应该是和递归版本的斐波那契数列是一样的,即O(2^N)。
为了避免重复计算,这次我们用一个数组把中间结果都记录下来
class Solution {
public int profitableSchemes(int G, int P, int[] group, int[] profit) {
int n = 101;
int[][][] flag = new int[n][n][n];
return profitableSchemesV2(flag, G, P, group, profit, 0);
}
public int profitableSchemesV2(int[][][] flag, int G, int P, int[] group, int[] profit, int k) {
if (k >= group.length && P <= 0 && G >= 0)
return 1;
if (G < 0)
return 0;
if (k >= group.length)
return 0;
if (flag[G][P][k] != 0)
return flag[G][P][k];
int count = 0;
if (G >= group[k]) {
// 做第k个任务的情况下
int tp = P - profit[k];
if (tp < 0)
tp = 0;
count += profitableSchemesV2(flag, G - group[k], tp, group, profit, k + 1); // 前k个任务,不能使用后面的任务
}
count = (count + profitableSchemesV2(flag, G, P, group, profit, k + 1)) % 1000000007;
flag[G][P][k] = count;
return count;
}
}
https://blog.csdn.net/yanglingwell/article/details/81302278
https://www.youtube.com/watch?v=MjOIR61txFc
# https://www.elastic.co/guide/en/elasticsearch/reference/current/deb.html
sudo apt update
# Download and install the public signing key
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
# install the apt-transport-https package on Debian before proceeding
sudo apt-get install -y apt-transport-https
# Save the repository definition to /etc/apt/sources.list.d/elastic-6.x.list
echo "deb https://artifacts.elastic.co/packages/6.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-6.x.list
# install the Elasticsearch Debian package
sudo apt-get update && sudo apt-get install -y elasticsearch
# configure Elasticsearch to start automatically when the system boots up
sudo /bin/systemctl daemon-reload
sudo /bin/systemctl enable elasticsearch.service
# Elasticsearch can be started and stopped as follows:
sudo systemctl start elasticsearch.service
# sudo systemctl stop elasticsearch.service
# information see /var/log/elasticsearch/
sudo journalctl --unit elasticsearch
ulimit -n 65536
cat >> /etc/security/limits.conf << EOF
elasticsearch - nofile 65536
EOF
# add follow lines into /usr/lib/systemd/system/elasticsearch.service [SERVICE]
# # add according https://www.elastic.co/guide/en/elasticsearch/reference/current/setting-system-settings.html#sysconfig
LimitMEMLOCK=infinity
sudo systemctl daemon-reload
前几天尝试了一下leetcode中文版通过率最低的一个题( #41 ),后来发现是关于动态规划的题目,然后听几个人说动态规划是算法题里面比较难的一类,于是就又找了一个动态规划的题目,自己思考了很久,最后依然是通过理解网络上的资源才有了着手之处。
你将获得 K 个鸡蛋,并可以使用一栋从 1 到 N 共有 N 层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。
你知道存在楼层 F ,满足 0 <= F <= N 任何从高于 F 的楼层落下的鸡蛋都会碎,从 F 楼层或比它低的楼层落下的鸡蛋都不会破。
每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X 扔下(满足 1 <= X <= N)。
你的目标是确切地知道 F 的值是多少。
无论 F 的初始值如何,你确定 F 的值的最小移动次数是多少?
示例 1:
输入:K = 1, N = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,我们肯定知道 F = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,我们肯定知道 F = 1 。
如果它没碎,那么我们肯定知道 F = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 F 是多少。
示例 2:
输入:K = 2, N = 6
输出:3
示例 3:
输入:K = 3, N = 14
输出:4
提示:
1 <= K <= 100
1 <= N <= 10000
计算机这个领域,在成为真正的大神之前,大多数人的第一感觉都是不准的,因为这是一个极度理性的领域,至于那些已经封神的大牛,他们几乎已经通过长久的练习与见多识广把很多理性的耗时的分析都转化成了O(1)
的感觉。
在最初看这个题目的时候,我的第一反应就是二分查找,简单来看,这个题目就是通过二分查找来确定F
的索引,于是第一反应就是logN
。但是仔细一看就发现是不对的,那必须啊,因为还有已知条件(K
)还没用上呢!根据初高中的理科经验可知,如果题目中有一些已知条件你没用就把题目做完了,那很大概率是你题目做错了(也有可能你是天才)。那么在这个题目中,问题出在哪里呢?这个题目与二分查找不一样的地方在于它的每一次比较大小——即扔鸡蛋——都是有代价的,这种代价就是因为鸡蛋摔破而导致的后面比较机会的减少。下面我就来看看“正确”的解法。
相比上一个题目( #41 ),这个题目暴力解法也不是很简单就能想到的——起码我是没想到。当然,看过答案之后就会觉得“很明显”了,这样的流程其实是不科学的,下面我就尝试引导读者一步步走向答案。
首先,我们来看扔鸡蛋这个事儿本身,假设在N
层的高楼中有K
个鸡蛋,这个时候我们在n
层扔了一个鸡蛋,那么这一次动作,把整个高楼其实就分成了两部分,一部分是1楼到n
楼,这是一个层高为n
的新楼,我们暂时叫它一号楼;另一部分是n+1
到N
楼,这是一栋新产生的层高为N-(n+1)+1=N-n
的新楼,我们叫他二号楼。
然后,我们来看刚扔下去的鸡蛋,如果它碎了,说明楼层太高(起码高于F
),那么F
应该是在一号楼,那么我们就带着剩下的K-1
个鸡蛋去一号楼继续,当我们站在一号楼的某一层的时候,其实和最开始是一样的(递归的信号)。如果鸡蛋没碎,说明楼层不够高(低于F
),此时我们要去二号楼,但是这里有一点点需要注意的,就是我们在二号楼的某一层的时候,其实该层在原始的楼里是要比当前楼层高n
层的,其他同理。
最后,因为我们是要找无论 F 的初始值如何
的条件下的查找次数,所以我们要在一号楼和二号楼各自的查找次数中选择那个最大的值,用计算机语言描述整个过程,就是
searchTime(K, N) = max( searchTime(K-1, X-1), searchTime(K, N-X) )
其中的X
就是我们刚才扔鸡蛋做在的楼层,它具体是哪一层并不重要。对于从1到N
的每一个X
,都可以计算出一个对应的searchTime
的值,在这N
个值中,最小的那个就是本题的答案了!
class Solution {
public int superEggDrop(int K, int N) {
return Solution.recursive(K, N);
}
public static int recursive(int K, int N) {
if (N == 0 || N == 1 || K == 1) {
return N;
}
int minimun = N;
for (int i = 1; i <= N; i++) {
int tMin = Math.max(Solution.recursive(K - 1, i - 1), Solution.recursive(K, N - i));
minimun = Math.min(minimun, 1 + tMin);
}
return minimun;
}
}
上面的这个方法有多暴力呢?通过推导式我们发现它和斐波那契数列第N项的计算方法(Fib(n) = Fib(n-1) + Fib(n-2)
)类似,那么,这个东西的时间复杂度又怎么计算呢?对于第N项 Fib(n) = Fib(n-1) + Fib(n-2)
,这个计算本身就是一个加法,其时间复杂度是O(1)
,但是其中包含了第N-1和第N-2两项,他们又同理本身需要一个O(1)
,且也分别包含了第N-1-1、第N-1-2和第N-2-1、第N-2-2四项,以此类推,这就是一个满二叉树的节点总数求和(如图),即O(2^n)
,同样的,其空间复杂度很低,仅仅是O(1)
。
上面的计算之所以时间复杂度高,与递归版本的斐波那契数列一样,就是因为重复计算了很多遍底部节点的值,为了加快这个计算过程,一个简单的提升方法就是拿空间换时间,把计算的中间结果都存储起来,后面直接查表即可。
class Solution {
public int superEggDrop(int K, int N) {
int[][] middleResults = new int[K + 1][N + 1];
for (int i = 1; i <= N; i++) {
middleResults[1][i] = i; // only one egg
middleResults[0][i] = 0; // no egg
}
for (int i = 1; i <= K; i++) {
middleResults[i][0] = 0; // zero floor
}
for (int k = 2; k <= K; k++) { // start from two egg
for (int n = 1; n <= N; n++) {
int tMinDrop = N * N;
for (int x = 1; x <= n; x++) {
tMinDrop = Math.min(tMinDrop, 1 + Math.max(middleResults[k - 1][x - 1], middleResults[k][n - x]));
}
middleResults[k][n] = tMinDrop;
}
}
return middleResults[K][N];
}
}
这个解法利用了一个二维数组存储了部分计算结果(空间复杂度O(KN)),使得时间复杂度降低到了O(KN^2)
。但是依然是一个平方级别的时间复杂度,不够快,还能优化吗?
最开始的时候,我们就想到了二分查找,但是因为发现不对,就果断抛弃了,事实上它还是有利用价值的。在上一个O(KN^2)
的算法中,我们拿着K
个鸡蛋检查了每一个楼层来寻找F
,但是事实上这并不是必须的,为什么呢?我们来看我们上面总结的这个递归的等式
searchTime(K, N) = max( searchTime(K-1, X-1), searchTime(K, N-X) )
现在我们令T1 = searchTime(K-1, X-1)
,T2 = searchTime(K, N-X)
,其中,T1
是随着X
的增长而增长的,T2
是随着X
的增长而降低的,如下图
其中蓝色描出来的部分,就是searchTime(K, N)
了,我们可以看出来它是局部有序的,所以可以考虑二分查找之!这里有一些与简单二分查找不一样的地方,就是在简单二分查找中,我们拿到输入数组A
和它的下标low
,high
,mid
之后,比较大小直接就是用下标读取数组的值A[low] < A[mid]
,但是在我们这个二分查找中,这个值是需要依赖与本题逻辑相关的一些计算的,具体看代码
class Solution {
Map<Integer, Integer> cache = new HashMap<>();
public int superEggDrop(int K, int N) {
if (N == 0)
return 0;
else if (K == 1)
return N;
Integer key = N * 1000 + K; // K <= 100
if (cache.containsKey(key))
return cache.get(key);
int low = 1, high = N;
while (low + 1 < high) {
int middle = (low + high) / 2;
int lowVal = superEggDrop(K - 1, middle - 1);
int highVal = superEggDrop(K, N - middle);
if (lowVal < highVal)
low = middle;
else if (lowVal > highVal)
high = middle;
else
low = high = middle;
}
int minimum = 1 + Math.min(
Math.max(superEggDrop(K - 1, low - 1), superEggDrop(K, N - low)),
Math.max(superEggDrop(K - 1, high - 1), superEggDrop(K, N - high))
);
cache.put(key, minimum);
return cache.get(key);
}
}
在while
循环中,我们利用了上文中提到的T1
和T2
的单调性,计算出了它们内部分别的最大值,因为在while
循环之后我们是要先取最大值,然后在众多的最大值中取最小值,所以while
内部是通过二分查找的**找到最大值。这个版本的空间复杂度依然是O(KN)
,但是因为利用到了二分查找,所以其时间复杂度就降到了O(KNlogN)
,这在很多的算法中,已经是一个可以接受的时间复杂度了,只是并不是最优雅的而已。
这个方法是上一个方法的延续,并把时间复杂度从O(KNlogN)
降到了O(KN)
,下面我们看一下它的思路。
首先令Xa = opt(K,N)
是能够找最小移动次数的最小的X
,根据上一个方法中我们分析T1
和T2
的单调性的方法,我们可以再次分析,并可以最终得出opt(K,N)
是随着N
的增长而增长的,这个我们也可以从下图中看到
随着N
的增长,T2
在向上移动,他们的交叉点Xa
也在向上移动,那么在上一个方法中需要遍历从1
到X
的循环就可以改成从Xa
到X
了,因为小于Xa
的都找不到最小移动次数。
与上一个方法自顶向下的方法不同,这次的解法是自底向上的,它每次的计算都是会先找到Xa
,然后就可以直接计算出结果。
class Solution {
public int superEggDrop(int K, int N) {
// only one egg situation
int[] dp = new int[N + 1];
for (int i = 0; i <= N; ++i)
dp[i] = i;
// two and more eggs
for (int k = 2; k <= K; ++k) {
int[] dp2 = new int[N + 1];
int x = 1; // start from floor 1
for (int n = 1; n <= N; ++n) {
// start to calculate from bottom
// Notice max(dp[x-1], dp2[n-x]) > max(dp[x], dp2[n-x-1])
// is simply max(T1(x-1), T2(x-1)) > max(T1(x), T2(x)).
while (x < n && Math.max(dp[x - 1], dp2[n - x]) > Math.max(dp[x], dp2[n - x - 1]))
x++;
// The final answer happens at this x.
dp2[n] = 1 + Math.max(dp[x - 1], dp2[n - x]);
}
dp = dp2;
}
return dp[N];
}
}
这里需要特别注意一下dp
,在for
循环中,它代表是上一次循环解出来的最小值,也就是比当前楼层低一层的情况下的最优解。所以while
条件中的
max(dp[x - 1], dp2[n - x]) > max(dp[x], dp2[n - x - 1])
,
带入T1 =dp(K−1,X−1)
,T2 =dp(K,N−X)
,就会发现其实是
max(T1(x-1), T2(x-1)) > max(T1(x), T2(x))
( 🤔)。
上面的方法的思路,都还是顺着题目的思路的进行的,其实我们可以换一个思路来想:“求k个鸡蛋在m步内可以测出多少层”。我们令dp[k][m]
表示k个鸡蛋在m步内可以测出的最多的层数,那么当我们在第X层扔鸡蛋的时候,就有两种情况:
N - X + dp[k-1][m-1]
层,X
和它上面的N-X
层已经通过这次扔鸡蛋确定大于F
;X + dp[k][m-1]
,X
层及其以下已经通过这次扔鸡蛋确定不会大于F
;也就是说,我们每一次扔鸡蛋,不仅仅确定了下一次扔鸡蛋的楼层的方向,也确定了另一半楼层与F
的大小关系,所以在下面的关键代码中,使用的不再是max
,而是加法(这里是重点)。评论里有人问到为什么是相加,其实这里有一个惯性思维的误区,上面的诸多解法中,往往求max
的思路是“两种方式中较大的那一个结果”,其实这里的相加,不是鸡蛋碎了和没碎两种情况的相加,而是“本次扔之后可能测出来的层数 + 本次扔之前已经测出来的层数”。
class Solution {
public int superEggDrop(int K, int N) {
int[][] dp = new int[K + 1][N + 1];
for (int m = 1; m <= N; m++) {
dp[0][m] = 0; // zero egg
for (int k = 1; k <= K; k++) {
dp[k][m] = dp[k][m - 1] + dp[k - 1][m - 1] + 1;
if (dp[k][m] >= N) {
return m;
}
}
}
return N;
}
}
这个算法的空间复杂度没有啥疑问,就是O(KN)
,关于时间复杂度,原文说是O(KlogN)
,但是说实话我还没完全搞懂为啥,因为在我看来两层循环一个K
,一个N
,应该是O(KN)
才对呀,如果有人知道这个是什么回事,还麻烦赐教下了。
从MySQL导数据到Elasticsearch,分别需要MySQL(安装参考 #9 )、Elasticsearch(安装参考 #5 )和中间件Logstash(安装参考 #7 )。
首先创建demo要用的数据库
mysql> CREATE DATABASE demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
切换数据库
mysql> use demo;
创建demo需要的题目表
mysql> CREATE TABLE `demo_questions` (`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `subject` varchar(31) DEFAULT NULL, `question` varchar(300) DEFAULT NULL) DEFAULT CHARSET=utf8;
向表里插入几条数据
mysql> INSERT INTO demo_questions(subject, question) VALUES('math', 'x+y=z+1');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO demo_questions(subject, question) VALUES('math', 'a+b=c+d');
Query OK, 1 row affected (0.00 sec)
mysql> INSERT INTO demo_questions(subject, question) VALUES('math', '1+2+3+4=');
Query OK, 1 row affected (0.00 sec)
OK,到这里数据就构造好了,如下
mysql> select * from demo_questions;
+----+---------+----------+
| id | subject | question |
+----+---------+----------+
| 1 | math | x+y=z+1 |
| 2 | math | a+b=c+d |
| 3 | math | 1+2+3+4= |
+----+---------+----------+
3 rows in set (0.00 sec)
Logstash有专门真对MySQL的input插件,官方文档在这里,这里唯一需要注意的是下载mysql-connector
的jar
包,并在配置文件中使用绝对路径配置其位置。jar
包可以从这里下载。具体的demo.conf
如下:
input {
jdbc {
jdbc_driver_class => "com.mysql.jdbc.Driver"
jdbc_driver_library => "/Users/shellbye/Downloads/mysql-connector-java-5.1.32/mysql-connector-java-5.1.32-bin.jar"
jdbc_connection_string => "jdbc:mysql://localhost:3306/demo"
jdbc_user => "root"
jdbc_password => ""
schedule => "* * * * *"
jdbc_paging_enabled => true
jdbc_page_size => 10000
statement => "SELECT id, subject, question from demo_questions"
columns_charset => { "question" => "UTF-8" }
}
}
output {
elasticsearch {
index => "question_index_from_mysql"
hosts => ["localhost:9200"]
document_id => "%{id}"
}
}
shellbye@localhost:/etc/logstash/conf.d$ /usr/share/logstash/bin/logstash -f m.conf
WARNING: Could not find logstash.yml which is typically located in $LS_HOME/config or /etc/logstash. You can specify the path using --path.settings. Continuing using the defaults
Could not find log4j2 configuration at path /usr/share/logstash/config/log4j2.properties. Using default config which logs errors to the console
[INFO ] 2018-04-26 16:43:26.406 [main] scaffold - Initializing module {:module_name=>"netflow", :directory=>"/usr/share/logstash/modules/netflow/configuration"}
[INFO ] 2018-04-26 16:43:26.435 [main] scaffold - Initializing module {:module_name=>"fb_apache", :directory=>"/usr/share/logstash/modules/fb_apache/configuration"}
[WARN ] 2018-04-26 16:43:27.145 [LogStash::Runner] multilocal - Ignoring the 'pipelines.yml' file because modules or command line options are specified
[INFO ] 2018-04-26 16:43:27.484 [LogStash::Runner] runner - Starting Logstash {"logstash.version"=>"6.2.4"}
[INFO ] 2018-04-26 16:43:27.716 [Api Webserver] agent - Successfully started Logstash API endpoint {:port=>9600}
[INFO ] 2018-04-26 16:43:28.707 [Ruby-0-Thread-1: /usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/stud-0.0.23/lib/stud/task.rb:22] pipeline - Starting pipeline {:pipeline_id=>"main", "pipeline.workers"=>1, "pipeline.batch.size"=>125, "pipeline.batch.delay"=>50}
[INFO ] 2018-04-26 16:43:29.214 [[main]-pipeline-manager] elasticsearch - Elasticsearch pool URLs updated {:changes=>{:removed=>[], :added=>[http://localhost:9200/]}}
[INFO ] 2018-04-26 16:43:29.223 [[main]-pipeline-manager] elasticsearch - Running health check to see if an Elasticsearch connection is working {:healthcheck_url=>http://localhost:9200/, :path=>"/"}
[WARN ] 2018-04-26 16:43:29.392 [[main]-pipeline-manager] elasticsearch - Restored connection to ES instance {:url=>"http://localhost:9200/"}
[INFO ] 2018-04-26 16:43:29.813 [[main]-pipeline-manager] elasticsearch - ES Output version determined {:es_version=>6}
[WARN ] 2018-04-26 16:43:29.815 [[main]-pipeline-manager] elasticsearch - Detected a 6.x and above cluster: the `type` event field won't be used to determine the document _type {:es_version=>6}
[INFO ] 2018-04-26 16:43:29.824 [[main]-pipeline-manager] elasticsearch - Using mapping template from {:path=>nil}
[INFO ] 2018-04-26 16:43:29.837 [[main]-pipeline-manager] elasticsearch - Attempting to install template {:manage_template=>{"template"=>"logstash-*", "version"=>60001, "settings"=>{"index.refresh_interval"=>"5s"}, "mappings"=>{"_default_"=>{"dynamic_templates"=>[{"message_field"=>{"path_match"=>"message", "match_mapping_type"=>"string", "mapping"=>{"type"=>"text", "norms"=>false}}}, {"string_fields"=>{"match"=>"*", "match_mapping_type"=>"string", "mapping"=>{"type"=>"text", "norms"=>false, "fields"=>{"keyword"=>{"type"=>"keyword", "ignore_above"=>256}}}}}], "properties"=>{"@timestamp"=>{"type"=>"date"}, "@version"=>{"type"=>"keyword"}, "geoip"=>{"dynamic"=>true, "properties"=>{"ip"=>{"type"=>"ip"}, "location"=>{"type"=>"geo_point"}, "latitude"=>{"type"=>"half_float"}, "longitude"=>{"type"=>"half_float"}}}}}}}}
[INFO ] 2018-04-26 16:43:29.880 [[main]-pipeline-manager] elasticsearch - New Elasticsearch output {:class=>"LogStash::Outputs::ElasticSearch", :hosts=>["//localhost:9200"]}
[INFO ] 2018-04-26 16:43:30.193 [Ruby-0-Thread-1: /usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/stud-0.0.23/lib/stud/task.rb:22] pipeline - Pipeline started successfully {:pipeline_id=>"main", :thread=>"#<Thread:0x21336b3c@/usr/share/logstash/logstash-core/lib/logstash/pipeline.rb:247 run>"}
[INFO ] 2018-04-26 16:43:30.242 [Ruby-0-Thread-1: /usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/stud-0.0.23/lib/stud/task.rb:22] agent - Pipelines running {:count=>1, :pipelines=>["main"]}
[INFO ] 2018-04-26 16:44:01.600 [Ruby-0-Thread-13: /usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/rufus-scheduler-3.0.9/lib/rufus/scheduler/jobs.rb:284] jdbc - (0.015967s) SELECT version()
[INFO ] 2018-04-26 16:44:01.660 [Ruby-0-Thread-13: /usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/rufus-scheduler-3.0.9/lib/rufus/scheduler/jobs.rb:284] jdbc - (0.032411s) SELECT count(*) AS `count` FROM (SELECT id, subject, question from demo_questions) AS `t1` LIMIT 1
[INFO ] 2018-04-26 16:44:01.673 [Ruby-0-Thread-13: /usr/share/logstash/vendor/bundle/jruby/2.3.0/gems/rufus-scheduler-3.0.9/lib/rufus/scheduler/jobs.rb:284] jdbc - (0.004498s) SELECT * FROM (SELECT id, subject, question from demo_questions) AS `t1` LIMIT 10000 OFFSET 0
因为我们这里数据比较少,所以基本上第一行select
的日志出来时,数据就已经同步完了。
这个时候可以看到ES里面对应的索引里有了3条数据
shellbye@localhost:/var/log/logstash$ curl localhost:9200/_cat/indices?v
health status index uuid pri rep docs.count docs.deleted store.size pri.store.size
yellow open question_index_from_mysql lcZyx7rvQvmVoeFdwjqn7Q 5 1 3 0 15.4kb 15.4kb
算法工程师们的给出的demo往往是用Python写的,但是在实际的项目中,考虑的性能的因素,我们常常需要使用C++来对demo进行一次重写,TF官方的文档中,这部分东西比较少,所以我对自己项目中用到的一些东西进行一点简单的记述。下面的代码也许不能直接拿来运行,但是大致意思是对的。
with gfile.FastGFile('model.pb', 'rb') as f:
graph_def = tf.GraphDef()
graph_def.ParseFromString(f.read())
g = tf.Graph()
tf.import_graph_def(graph_def, name='')
with tf.Session(graph=g) as sess:
_in = sess.graph.get_tensor_by_name("data/in:0")
_out = sess.graph.get_tensor_by_name("out:0")
in_batch = np.zeros((1, 16)) # np array (1, 16) as input
feed_dict = {inp: in_batch}
results = sess.run(_out, feed_dict=feed_dict)
以上为Python版本的加载模型和运行启动代码,下面是C++版本的
tensorflow::GraphDef graph_def;
Status load_graph_status = ReadBinaryProto(tensorflow::Env::Default(), “./model.pb”, &graph_def);
if (load_graph_status.ok()) {
//session config
std::unique_ptr<tensorflow::Session> session;
tensorflow::SessionOptions session_options;
session_options.config.mutable_gpu_options()->set_allow_growth(true);
session->reset(tensorflow::NewSession(session_options));
Status session_create_status = (*session)->Create(graph_def);
if (!session_create_status.ok()) {
// error
}
tensorflow::Tensor _in(tensorflow::DT_INT32, tensorflow::TensorShape({1, 16}));
auto inp_mapped = inp.tensor<int, 2>(); // 这里是C++版本比较奇特的地方,要通过这种方式才能完成输入数据的准备
for (int i = 0; i < 1; i++) {
for (int j = 0; j < 16; j++) {
inp_mapped(i, j) = 0; // input data
}
}
std::vector<std::pair<std::string, tensorflow::Tensor>> feed_dict = {
{"data/in:0", _in}
};
std::vector<Tensor> _out;
Status run_status = session_->Run(feed_dict, {"out:0",}, {}, _out);
}
在模运算中,同余是一个很重要的定义,比如最简单的求余,我们有
表示5除以3之后余2,同样的,11除以3之后也余2
即5和11的余数是相同的,这在数学上叫做同余,可以表示为
这个表示5和11除以3之后,他们的余数是相同的(都是2)。
假设现在有三个人小a、小b和小c住在一个小区里,现在考试结束了,小a和小b想互相告诉对方自己的成绩,但是他们只能通过隔着窗户互相喊话的形式来交换,但是他们又不想让小c知道,于是他们俩在学校时就偷偷约定了一个数字 32(别问为什么没在学校告知对方成绩),作为他们之间通信的密钥,然后回家之后,他们把自己的真实成绩乘以32,然后大声喊出来,比如小a喊3200,小b喊2880,然后他们在听到对方的成绩之后,再除以32,就知道了对方的成绩。于此同时,就算有人听到了他们喊的3200和2880,也不知道他们在说什么,这就实现了对称加密,这里的对称,指的是小a和小b的手里的密钥是一样的,都是32。但是这样有一个问题,就是两人必须得在学校见面,并提前约定好密钥,万一他们在偷偷约定的时候被小c听到了,那么他们后面所有的加密过程就都没有意义了。有没有其他方法呢?当然有,那就是非对称加密,又叫公开密钥加密
非对称加密的后半部分就是对称加密,它的关键点是如何通过公开的方式,而不是偷偷的方式,来确定他们的公共密钥,也就是上面的32。
从小a的角度分析,
从小b的角度分析,
所以他们得到的结果当然就是一样的喽。
那么问题来了,既然小c知道N, P, J, K,也知道其中的计算规则,那么为什么他不能从
中反解出A呢?
实际上,这是一个已知的非常难的问题(discrete logarithm problem 离散对数难题),而如果要提前计算所有N并存储起来的话,需要的存储空间大到完全无法存储,所以就认为它是不可破解的了。
那么既然计算这么难,小a和小b互相之间是怎么计算的呢?因为其中包含了取模运算,且取模运算有上面我们提到的性质,所以实际用的计算量是非常小的。下面我们用
来做一个简单的demo
其中的替换过程,使用的就是互余的性质。最后我们就可以轻松算出:
wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie;" http://download.oracle.com/otn-pub/java/jdk/8u171-b11/512cd62ec5174c3487ac17c61aaa89e8/jdk-8u171-linux-x64.tar.gz
tar -xf jdk-8u171-linux-x64.tar.gz
sudo mkdir -p /usr/lib/jvm/
sudo mv jdk1.8.0_171 /usr/lib/jvm/
sudo update-alternatives --install "/usr/bin/java" "java" "/usr/lib/jvm/jdk1.8.0_171/bin/java" 1010
sudo update-alternatives --install "/usr/bin/javac" "javac" "/usr/lib/jvm/jdk1.8.0_171/bin/javac" 1010
sudo update-alternatives --install "/usr/bin/javaws" "javaws" "/usr/lib/jvm/jdk1.8.0_171/bin/javaws" 1010
java -version
cat >> /etc/profile.d/javajdk.sh << EOF
export JAVA_HOME=/usr/lib/jvm/jdk1.8.0_171
EOF
source /etc/profile.d/javajdk.sh
Bottle支持在开发过程中自动监听文件改动,并重启服务。本文通过阅读其源码,来探究它是如何做到这一点的。
本文使用0.12
的稳定版本,完整源码见这里。
以下代码就可以启动一个WEB服务,并监听文件改动。
# -*- coding:utf-8 -*-
# Created by shellbye on 2018/5/24.
from bottle import run, route
@route('/')
def hello():
return "Hello World! "
run(host='localhost', port=8050, reloader=True)
修改该文件并重新刷新浏览器,会看到如下执行日志,证明服务重启了
Bottle v0.12.13 server starting up (using WSGIRefServer())...
Listening on http://localhost:8050/
Hit Ctrl-C to quit.
127.0.0.1 - - [24/May/2018 11:57:12] "GET / HTTP/1.1" 200 13
Bottle v0.12.13 server starting up (using WSGIRefServer())...
Listening on http://localhost:8050/
Hit Ctrl-C to quit.
127.0.0.1 - - [24/May/2018 11:57:23] "GET / HTTP/1.1" 200 16
官方对于自动重启的文档介绍如下:
How it works: the main process will not start a server, but spawn a new child process using the same command line arguments used to start the main process. All module-level code is executed at least twice! Be careful.
The child process will have
os.environ['BOTTLE_CHILD']
set toTrue
and start as a normal non-reloading app server. As soon as any of the loaded modules changes, the child process is terminated and re-spawned by the main process.
意思就是主进程启动时,并没有启动服务本身,而是派生出了一个和主进程本身一摸一样的子进程,并由该子进程完成服务的提供。所有模块级别的代码都会因为这个原因执行两次。子进程中环境变量os.environ['BOTTLE_CHILD']
设置为True
,任何模块文件的改动都会导致子进程被销毁重建。
下面我们来一下看看源码,主进程判断是否开始自动重启机制,以及如果开始就派生子进程的程序是在服务一开始就要进行的,由下面这段代码完成以上工作:
if reloader and not os.environ.get('BOTTLE_CHILD'): #主进程才会进入到以下内容,子进程则直接跳过了
try:
lockfile = None
fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock')
os.close(fd) # We only need this file to exist. We never write to it
while os.path.exists(lockfile):
args = [sys.executable] + sys.argv
environ = os.environ.copy()
environ['BOTTLE_CHILD'] = 'true' #标识子进程
environ['BOTTLE_LOCKFILE'] = lockfile
p = subprocess.Popen(args, env=environ)
while p.poll() is None: # Busy wait... #子进程正常执行中,每隔interval时间更新lockfile的读取时间戳
os.utime(lockfile, None) # I am alive!
time.sleep(interval)
if p.poll() != 3: # 若子进程返回码不是3,则不是文件改动的重启,就需要彻底关闭了
if os.path.exists(lockfile): os.unlink(lockfile)
sys.exit(p.poll())
except KeyboardInterrupt:
pass
finally:
if os.path.exists(lockfile):
os.unlink(lockfile)
return
以上代码中,p.poll()
是一个比较重要的操作,它是主进程采集子进程状态的一个方法,用来判断子进程当前的执行状态,如果返回是None
,则表示子进程还在执行中,目前并没有返回任何结果。在以上的代码中,子进程执行的过程中,主进程一直在用utime
更新着lockfile
的时间,这个更新的时间会在后面判断子进程状态时使用。
上面的代码执行完毕之后,主进程就进入了不断检查子进程或在文件做了改动之后重新派生子进程的循环之中。而子进程则没有进到上面的if
,继续执行了下去,然后是下面这段代码:
if reloader:
lockfile = os.environ.get('BOTTLE_LOCKFILE')
bgcheck = FileCheckerThread(lockfile, interval)
with bgcheck:
server.run(app) # 执行到这里就阻塞了,直到bgcheck主动interrupt(发现文件改动,见下)
if bgcheck.status == 'reload': # bgcheck发现有文件进行了改动,标记reload,这里子进程退出,并返回code 3,主进程通过这个3再重新派生子进程,如上所述
sys.exit(3)
这里的with
是一个比较巧妙的用法,它确保在文件修改时间检查的同时去启动服务,并在服务终止时做一些收尾工作,就像经典的文件读取一样:
with open('output.txt', 'w') as f:
f.write('Hi there!')
下面就需要重点关注这个FileCheckerThread
了:
class FileCheckerThread(threading.Thread):
''' Interrupt main-thread as soon as a changed module file is detected,
the lockfile gets deleted or gets to old. '''
def __init__(self, lockfile, interval):
threading.Thread.__init__(self)
self.lockfile, self.interval = lockfile, interval
#: Is one of 'reload', 'error' or 'exit'
self.status = None
def run(self):
exists = os.path.exists # 函数重命名,可以简化部分大量使用的函数的代码
mtime = lambda path: os.stat(path).st_mtime
files = dict()
for module in list(sys.modules.values()): # 将所有需要监听的文件都加入检测字典,并记录其最后一次修改的时间
path = getattr(module, '__file__', '')
if path[-4:] in ('.pyo', '.pyc'): path = path[:-1]
if path and exists(path): files[path] = mtime(path)
while not self.status:
if not exists(self.lockfile)\
or mtime(self.lockfile) < time.time() - self.interval - 5: # 如果lockfile不存在了,或者其已经超过5秒没有更新时间,则是发生了某种错误,标记错误,并interrupt
self.status = 'error'
thread.interrupt_main()
for path, lmtime in list(files.items()): # 依次检测所有的需要检测的文件的最近修改时间,如果符合重启的条件,则中断调用其的进程(子进程)
if not exists(path) or mtime(path) > lmtime:
self.status = 'reload'
thread.interrupt_main()
break
time.sleep(self.interval)
def __enter__(self):
self.start()
def __exit__(self, exc_type, exc_val, exc_tb):
if not self.status: self.status = 'exit' # silent exit
self.join()
return exc_type is not None and issubclass(exc_type, KeyboardInterrupt)
至此就完成了一次重启机制的介绍。🤓
wget --no-check-certificate --no-cookies --header "Cookie: oraclelicense=accept-securebackup-cookie;" http://download.oracle.com/otn-pub/java/jdk/8u171-b11/512cd62ec5174c3487ac17c61aaa89e8/jdk-8u171-linux-x64.rpm
sudo yum localinstall jdk-8u171-linux-x64.rpm
wget http://repo.mysql.com/mysql-community-release-el7-5.noarch.rpm
sudo rpm -ivh mysql-community-release-el7-5.noarch.rpm
yum update
sudo yum install mysql-server -y
在多年不接触Java
系,尤其是Spring
之后,最近因为转岗需要用到Spring Boot
,才突然发现原来Java
系的Web开发现在也这么友好了,终于不用在配置各种个样的xml
文件和各种XXO.java
文件了。但是因为对Spring Boot
不是很熟,所以有些东西还是需要记录一下。
比如最近遇到的一个小问题,就是当GET
请求时,如果有一些url
参数是必填而用户忘记填写的话,Spring Boot
默认的返回是直接400 Bad Request
(如下图)
这个原则上来说是没有问题的,但是当你写的是restful
格式的接口时,这个提示就不是很友好了,需要改成比如这个样式的:
{"code":"400200","data":{},"message":"parameter p1 is required"}
其实从上面的图中可以发现,Spring Boot
其实做完了我们需要的工作,只是它的展示样式不是我们需要的,那么我们需要做的,其实就是把Spring Boot
的工作封装一下就好了。具体的思路就是找到参数缺失的异常(MissingServletRequestParameterException
),然后自己捕捉到它就好了。
项目目录结构
关键代码
Controller.java
package com.example.demo;
import org.springframework.web.bind.annotation.*;
@RestController
public class Controller {
@RequestMapping(value = "/demo", method = RequestMethod.GET)
public @ResponseBody
String demo(@RequestParam(value = "p1") String p1,
@RequestParam(value = "p2") String p2,
@RequestParam(value = "p3") String p3,
@RequestParam(value = "p4") String p4,
@RequestParam(value = "p5") String p5) {
return "OK";
}
}
MissingServletRequestParameterExceptionHandler.java
package com.example.demo;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
@ControllerAdvice
public class MissingServletRequestParameterExceptionHandler extends ResponseEntityExceptionHandler {
@Override
protected ResponseEntity<Object> handleMissingServletRequestParameter(
MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
String msg = String.format("{\"code\":\"400200\",\"data\":{},\"message\":\"parameter %s is required\"}", ex.getParameterName());
return new ResponseEntity<>(msg, HttpStatus.OK);
}
}
如上代码,MissingServletRequestParameterExceptionHandler
扩展了ResponseEntityExceptionHandler
并重写了其中处理参数缺失的方法handleMissingServletRequestParameter
,然后就进入到了我们自己的代码中,然后就效果如下图
最近因为老项目迁移,在从Mysql往Elasticseaerch里导数据( #8 )时,Logstash有一下关键报错内容:
Mixing up field types: class org.elasticsearch.index.mapper.KeywordFieldMapper$KeywordFieldType != class org.elasticsearch.index.mapper.TextFieldMapper$TextFieldType on field XXX
事后回看,这个报错信息还是比较精准的,但是当时还是一脸懵逼。因为建索引时我们指定了XXX字段就是text
{
"mappings":{
"doc":{
"properties":{
"id":{
"type":"keyword"
},
"XXX":{
"type":"text"
}
}
}
}
}
然后Logstash的关键配置如下
input {
jdbc {
type => "business_type_001",
statement => "select id, XXX from my_table"
}
}
就是以上教科书式的标准配置,但是却一直在报错
Mixing up field types: class org.elasticsearch.index.mapper.KeywordFieldMapper$KeywordFieldType != class org.elasticsearch.index.mapper.TextFieldMapper$TextFieldType on field XXX
经过一段时间的调查,我们终于找到了原因。究其根源,是因为Elasticsearch在6.X版本中,去掉了原来类似关系型数据库表的概念的type
,这里需要稍微科普一下:在Elasticsearch 5.X中,index类似关系型数据库里面的database,一个index里面可以有多个type,这个type相当于关系型数据库里面的table。但是在实际的开发过程中,其实type是比较鸡肋的,就我个人而言,我是基本上没有用过的。因为没必要非要把不同的数据强行塞到一个index里面,再去通过type区分。实际工作中要么就是放到不同的index里面,要么也是通过自己的某个分类关键字去区分不同类型的数据,type长期处于默认值。
问题恰恰就出在这个地方。在Elasticsearch 6.X中,官方也终于去掉了type。但是因为我们是在迁移项目(很不幸,从6.X到5.X,因为云厂商还没部署6.X),所以导致建索引时用的是5.X的方法(指定了type是doc),但是在Logstash中,type这个关键字,又恰好被我们用来区分不同的数据(旧版本是6.X),最终导致的问题就是:我们手动建索引的时候,相当于建了一个叫doc的表,但是从Logstash导数据时,却是往business_type_001
(见上)这个不存在的表里灌数据,因为表不存在,所以Logstash非常智能的给我们创建了一个同名表,然后按照Elasticsearch的默认配置为每个字段都设计了keyword这样的类型,于是引发了上面所说的问题。
Linux系统的权限问题,常常折磨着我们这些开发人员,所以我在启动Django
项目的uwsgi
时,也总是把pid
和socket
文件放到/tmp
下面,但是,今后如果要用CentOS(运维的最爱,dont know why)系统来部署的话,就不行了,因为你会遇到如下的502
connect() to unix:/tmp/xxx.sock failed (2: No such file or directory) while connecting to upstream
然后你不断的问自己“我到底哪里错了”,并持续数个小时,然后偶然发现如下的信息:
it appears that programs that create files in /tmp (or /var/tmp as I have discovered) are the only programs that are able to see the files in that directory.
意思是,tmp
不再是那个tmp
了,某个程序(uwsgi
)在里面创建的文件以后只能该程序自己看到,其他程序(nginx
)就是睁眼瞎了(No such file or directory
),所以会出现上面描述的问题,解决方法就是:放到别的文件夹里。🙂
参考:
https://stackoverflow.com/questions/22272943/nginx-cannot-find-unix-socket-file-with-unicorn-no-such-file-or-directory
https://serverfault.com/questions/463993/nginx-unix-domain-socket-error/464025#464025
http://fedoraproject.org/wiki/Features/ServicesPrivateTmp
Flag还是要立的,虽然有被疯狂打脸的风险,但是当你知道有人看到了你的Flag之后,你会比平常更加努力。元旦的时候,和老婆一起定了2019的目标,我的其中就包含了leetcode上按照通过率排序最低的50个题,这不是第一个就遇到了很大问题,看答案都看的模模糊糊的,好在互联网上总有各种先人的足迹,只要下功夫,总能通过网络上的蛛丝马迹找到想要的东西,包括自己的失踪的女儿,参考《网络谜踪》。好了,闲话就到这里,下面看原题:
返回 A 的最短的非空连续子数组的长度,该子数组的和至少为 K 。
如果没有和至少为 K 的非空子数组,返回 -1 。
示例 1:
输入:A = [1], K = 1
输出:1
示例 2:
输入:A = [1,2], K = 4
输出:-1
示例 3:
输入:A = [2,-1,2], K = 3
输出:3
提示:
1 <= A.length <= 50000
-10 ^ 5 <= A[i] <= 10 ^ 5
1 <= K <= 10 ^ 9
很多算法在算力足够的情况下,都是可以用暴力循环的方式求解,只要能够用计算机语言描述问题,就可以用暴力方式求解,比如这个题,我们就可以用下面的三层循环来求解
class Solution {
public int shortestSubarray(int[] A, int K) {
int minLen = A.length + 1;
for (int i = 0; i < A.length; i++) {
for (int j = i; j < A.length; j++) {
int sum = 0;
for (int k = i; k <= j; k++) {
sum += A[k];
}
if (sum >= K && (j - i + 1) < minLen) {
minLen = j - i + 1;
}
}
}
return minLen == A.length + 1 ? -1 : minLen;
}
}
当然暴力解法也不一定就非常容易,也是有很多地方需要注意的,比如上面的第二层循环即j
的那一层,我就花了点时间的,刚开始的初始条件是int j = i + 1
,但是对于上面的示例1就通过不了,因为这种写法默认输入是包含两个及以上元素的,但是实际情况是我们常常会遇到1个甚至是空的输入这种边界条件,而且对这些条件的处理往往才是实现代码时的关键点。
暴力解法最大的问题就在于时间开销太大,在一些输入规模比较大的情况下,基本上没有用,相当于这个问题没有解决,比如我们上面这种暴力解法,其时间负责度就是O(n^3)
,但是它又一个好处是空间复杂度比较低,只有O(1)
,相比与下面要提到的各种快速算法,它的空间占用率是最低的。
在上面的暴力解法中,我们可以看到,它之所以暴力,是因为它的很多计算都是重复的,比如在最内层的循环中求和,前k
个元素的和在求前k+1
,k+2
... k+n
个元素的和时又重复计算了一边,这种有重叠子问题的方法,是典型的可以用动态规划(Dynamic programming)的**来解决的,这个重复计算的过程是可以省略的,前k+1
个元素的和就是前k
个元素的和再加第k+1
个元素就可以了。基于这样的想法,我们可以考虑先构造这样一个数组preSum
,它的第i
个元素preSum[i]
表示输入数组A
的前i
个元素(0
到i-1
,不包含i
)的和,即
preSum[i] = A[0] + A[1] + ... + A[i-1]
这里要格外注意一下数组preSum
的起点,我在最初实现的时候,preSum[0]
定义成了A[0]
,这样看起来似乎是合情合理,但是这样无形之中忽视了A
为空的情况,在处理上面的示例1的时候就遇到了问题,这就是边界条件的处理问题,一定要多加小心。最终的实现版本中,我把preSum[0]
定义成了0
,即表示A
是空的情况下(和A
中前0个元素的情况下)其和为0
。
当我们计算好preSum
之后,问题就转化成了满足preSum[x2] - preSum[x1] >= K
条件下的最小的x2 - x1
。因为此时preSum[x2] - preSum[x1]
就表示A
中第x1
到第x2
之间元素的和(其中x1 < x2
)。
class Solution {
public int shortestSubarray(int[] A, int K) {
int minLen = A.length + 1;
int[] preSum = new int[A.length + 1];
preSum[0] = 0;
for (int i = 0; i < A.length; i++) {
preSum[i + 1] = preSum[i] + A[i];
}
for (int i = 0; i < A.length; i++) {
for (int j = i + 1; j < A.length + 1; j++) {
if ((preSum[j] - preSum[i]) >= K) {
if ((j - i) < minLen) {
minLen = j - i;
}
}
}
}
return minLen == A.length + 1 ? -1 : minLen;
}
}
相比与暴力解法,这个方法把时间复杂度降到了O(n^2)
(代价是空间复杂度升到了O(n)
,这也是拿空间换时间,不过这个例子不够典型),看起来已经非常不错了,但是我们能满足吗?不能,好,接下来看时间复杂度O(n)
的解法。
在上面没那么暴力解法中的那个双层循环中,还有一定的优化空间的。
preSum[x2] <= preSum[x1]
(其中x1 < x2
)时,表明x1
到x2
之间的元素的和是负数或0,那么就是当preSum[xn] - preSum[x1] >= K
则必然有preSum[xn] - preSum[x2] >= K
,那么这个时候我们只计算xn - x2
即可(x1
到x2
之间的元素可以全部跳过了,耶!),就不需要计算xn - x1
了,因为后者一定是更大的,不满足我们要选最小的条件。preSum[x2] - preSum[x1] >= K
时,x1
就可以跳过了,为什么呢?因为x1
到x2
已经满足了大于K
,再继续从x1
开始向后再早,也不会再有比x2
距离x1
更近的了,毕竟我们要求的是最小的x2 - x1
。以上的两种分析,情况1是把位于末尾没用的x1
扔掉,情况2是把指向前面的已经满足条件的x1
的指针向后移动1位,这是就需要比较当前最小值与此时刚符合条件的值的大小了。
class Solution {
public int shortestSubarray(int[] A, int K) {
int minLen = A.length + 1;
int[] preSum = new int[A.length + 1];
preSum[0] = 0;
for (int i = 0; i < A.length; i++) {
preSum[i + 1] = preSum[i] + A[i];
}
Deque<Integer> deque = new LinkedList<>();
for (int i = 0; i < A.length + 1; i++) {
while (!deque.isEmpty() && preSum[i] <= preSum[deque.getLast()]) {
// 1.
deque.pollLast();
}
while (!deque.isEmpty() && preSum[i] - preSum[deque.getFirst()] >= K) {
// 2.
int new_len = i - deque.pollFirst();
if (new_len < minLen) {
minLen = new_len;
}
}
deque.addLast(i);
}
return minLen == A.length + 1 ? -1 : minLen;
}
}
其中第二个for循环中,终止条件是i < A.length + 1
,因为我们自己构造的数组preSum
其长度是A.length + 1
。
1.https://blog.csdn.net/yanglingwell/article/details/80875790
2.https://www.cnblogs.com/f91og/p/9494922.html
3.https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/solution/
4.https://leetcode.com/problems/shortest-subarray-with-sum-at-least-k/discuss/143726/C%2B%2BJavaPython-O(N)-Using-Deque
在尝到过自动化测试的甜头之后,我基本所有的项目都会有配套的自动化测试,之所这里没有说测试驱动开发(Test Driven Development
),因为我目前确实还没做到TDD
,我目前还是先Develop
,然后才写Test
,当然我还是希望以后真的能切实做到TDD
,因为我一直相信前人经过很多弯路之后留下了的一些经验。
之前我的项目没太多的对第三方服务的依赖,所以写测试的时候,也没有太操心和第三方接口的一些问题,但是这次一个新项目对第三方服务严重依赖,因为基本所有的内容都来自三方服务,但是因为第三方服务也在开发中,就导致他们的接口很不稳定,这也让我的测试用例时通时不通,于是经过一些调研,找到了一些合适的Spring Boot
下面基于MockRestServiceServer
的模拟第三方服务接口的方法。本项目所有代码都在这里。
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
public class MainController {
private RestTemplate restTemplate;
@Autowired
public MainController(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@RequestMapping(value = "/", method = RequestMethod.GET)
public @ResponseBody
String demo() {
String result = restTemplate.getForObject("http://some_third_api_service.com", String.class);
return String.format("{\"result\": %s}", result);
}
}
上面基本上就是这个demo项目的所有业务代码了,这里之所以把DemoApplication.class
也贴出来,是为了强调这里的RestTemplate
这个Bean
,因为如果想要用后面的MockRestServiceServer
,这里就需要有一个这样的Bean
,用于在测试的代码里替换它,如果Controller
里面的restTemplate
是在demo()
方法里new
出来的,就无法实现模拟了,读者可以自己尝试一下。
package com.example.demo;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.client.MockRestServiceServer;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.client.support.RestGatewaySupport;
import static org.springframework.test.web.client.ExpectedCount.once;
import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MainControllerTest {
@Autowired
RestTemplate restTemplate;
@Autowired
private MockMvc mvc;
private MockRestServiceServer mockServer;
@Before
public void setUp() {
RestGatewaySupport gateway = new RestGatewaySupport();
gateway.setRestTemplate(restTemplate);
mockServer = MockRestServiceServer.createServer(gateway);
}
@Test
public void demoTest() throws Exception {
mockServer.expect(once(), requestTo("http://some_third_api_service.com"))
.andRespond(withSuccess("ok123", MediaType.APPLICATION_JSON));
mvc.perform(MockMvcRequestBuilders
.get("/")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json("{\"result\":\"ok123\"}"));
mockServer.verify();
}
}
从这里的setUp()
就可以明白为什么上面的业务代码要用Bean
了,否则这里的reset
也就没有了意义。这也设置之后,我们就可以在withSuccess
写任何我们期望的来自第三方的返回,这也不仅仅可以脱离测试时对第三方的依赖,也可以加快测试的执行速度。
参考资料:
ES的安装参考这里 #5 ,Logstash的安装参考这里 #7
注意这里有一个问题需要注意一下,就是如果你一直在尝试导入一个(比较小的)文件(就像我在这个demo中做的),那么logstash后面就不会继续去导入了,因为logtash会通过sincedb_path
所指定的位置去记录当前文件的导入位置,如果你已经导入完成了,那么后续再导入就不会执行了(别问我怎么知道的😂)。
shellbye@localhost:~$ cat data.txt
{"qus": "xyz", "ocr": "xxx", "name": "o001"}
{"qus": "xyz", "ocr": "xxx", "name": "o002"}
{"qus": "xyz", "ocr": "xxx", "name": "o003"}
{"qus": "xyz", "ocr": "xxx", "name": "o004"}
input {
file {
path => "/home/shellbye/data.txt"
discover_interval => 1
start_position => "beginning"
}
}
filter {
json{
source => "message"
}
}
output {
elasticsearch {
index => "demo_index001"
hosts => ["localhost:9200"]
}
}
/path/to/bin/logstash -f /home/shellbye/f.conf
wget -O boost_1_66_0.tar.gz https://sourceforge.net/projects/boost/files/boost/1.66.0/boost_1_66_0.tar.gz/download
tar xzvf boost_1_66_0.tar.gz
cd boost_1_66_0/
sudo apt-get update
sudo apt-get install build-essential g++ python-dev autotools-dev libicu-dev build-essential libbz2-dev
./bootstrap.sh --prefix=/usr/
./b2
sudo ./b2 install
之前有若干博客分别介绍了Logstash
的安装( #7 #14 )、导入文本文件( #11 )、导入json
文件( #10 )以及从MySQL
导入文件( #8 #16 )。以上导的数据都是从一个地方,一次性到了另一个地方,虽然来源不一样,但是整体逻辑还是比较简单的,今天记录一个比较复杂的逻辑,即从MySQL
(或其他来源)获取数据之后,将拿到的数据发送到远程restful
接口(感谢JSONPlaceholder),然后将返回的内容经过处理之后输出到output
。
创建并切换数据库,然后创建demo需要的表,插入3条数据
mysql> CREATE DATABASE demo CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
mysql> use demo;
mysql> CREATE TABLE user_id_tb (`id` int(11) NOT NULL AUTO_INCREMENT PRIMARY KEY, `user_id` int(11) ) DEFAULT CHARSET=utf8;
mysql> INSERT INTO user_id_tb(user_id) VALUES (2);
mysql> INSERT INTO user_id_tb(user_id) VALUES (1);
mysql> select * from user_id_tb;
+----+---------+
| id | user_id |
+----+---------+
| 1 | 2 |
| 2 | 1 |
+----+---------+
2 rows in set (0.00 sec)
与之前的文章不同的是,这次配置文件中需要用到一个第三方的filter
插件,logstash-filter-rest
,这个插件是本文的关键,它是用来发送http请求并处理返回值的插件,具体的安装方法如下:
$LS_HOME/bin/logstash-plugin install logstash-filter-rest
接下来是配置文件
input {
jdbc {
jdbc_driver_class => "com.mysql.jdbc.Driver"
jdbc_driver_library => "/Users/shellbye/Downloads/mysql-connector-java-5.1.32/mysql-connector-java-5.1.32-bin.jar"
jdbc_connection_string => "jdbc:mysql://localhost:3306/demo"
jdbc_user => "root"
jdbc_password => ""
schedule => "* * * * *"
jdbc_paging_enabled => true
jdbc_page_size => 10000
last_run_metadata_path => "/tmp/logstash_last_run_demo.file"
statement => "SELECT id, user_id from user_id_tb"
columns_charset => { "question" => "UTF-8" }
}
}
# logstash-6.2.4/bin/logstash-plugin install logstash-filter-rest
# https://github.com/lucashenning/logstash-filter-rest
# https://github.com/elastic/logstash/issues/3489
filter {
rest {
request => {
url => "https://jsonplaceholder.typicode.com/users/%{user_id}"
method => "GET"
}
target => "user_info"
}
mutate {
add_field => { "city" => "%{[user_info][address][city]}" }
}
mutate {
remove_field => [ "user_info"]
}
}
output {
elasticsearch {
index => "user_index"
hosts => ["localhost:9200"]
document_id => "%{user_id}"
}
}
注意上面的remove_field
,这里因为信息比较多,所以我就把完整的返回都删除了,只保留了我需要的字段(city
),这里还能顺便看到logstash nested json
即嵌套的json
的读取方法。还有一个需要注意的地方,就是当你使用POST
方法时,参数是默认在body
里的,这是restful
的约定,而不在form
里。
curl 'localhost:9200/user_index/_search?pretty'
{
"took" : 6,
"timed_out" : false,
"_shards" : {
"total" : 5,
"successful" : 5,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 1.0,
"hits" : [
{
"_index" : "user_index",
"_type" : "doc",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"id" : 1,
"city" : "Wisokyburgh",
"@timestamp" : "2018-05-11T02:01:02.231Z",
"user_id" : 2,
"@version" : "1"
}
},
{
"_index" : "user_index",
"_type" : "doc",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"id" : 2,
"city" : "Gwenborough",
"@timestamp" : "2018-05-11T02:01:02.261Z",
"user_id" : 1,
"@version" : "1"
}
}
]
}
}
对于我这种C++新人来说,用C++干个啥都很困难,似乎都需要记录。比如近日有这么一个需求,把一个二进制文件(old.wav)读取到内存里,返回给调用方,然后调用方在把它写入到一个新的二进制文件(new.wav)中。
#include <iostream>
#include <vector>
#include <fstream>
std::vector<char> read_return() {
std::ifstream file("old.wav", std::ios::binary | std::ios::ate);
std::streamsize size = file.tellg();
file.seekg(0, std::ios::beg);
std::vector<char> buffer(size);
if (file.read(buffer.data(), size)) {
std::cout << "Read OK!" << std::endl;
return buffer;
} else {
std::cout << "Read Failed!" << std::endl;
return buffer;
}
}
int main() {
auto wav = read_return();
std::ofstream stream("save.wav", std::ios::binary);
stream.write((const char *) &wav[0], wav.size());
stream.close();
return 0;
}
参考:
开始接触C++,才更加发现了Python第三方库之多,多到写Python几乎就是不停的找库,经验丰富的人知道有哪些库,而新手则只能冒着bug百出的风险自己动手DIY。项目需要一个把汉字转成带音调的功能,本以为随手就是,结果调研好久都没有发现合适的第三方项目,倒是发现了不错的Python项目(python-pinyin
),无奈之下,只得照着python-pinyin
的核心代码进行了部分C++化,好在这个功能如果只是简单用用,还算比较简单,大致分为以下几步。
就是就是把输入的语句拆分成一个个汉字,可以参考我之前写的 #27 ,也是给予前人的东西自己学习了一下。
找到汉字对应的code point
,参考我之前写的 #25 。
项目中包含了一个很大的map,这是整个项目的核心,找到code point之后就是在这个map里面找拼音了。在python-pinyin
中,还对多音字进行了判断和处理,这部分我就没搞了,遇到多音字直接去第一个就是了(^_^)。
在python-pinyin
中,还可以按照不同的需求对输出就行各种各样的定制化输出,因为我这个比较简单,所以只是一种固定的输出格式(语音 ==> yu4 yin1 .)。
整个项目没啥特别复杂的技术,正好适合我学习C++和cmake,地址在这里:hanzi2pinyin
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.