Welcome to my blog. You can post whatever you like!
The blog post is using markdown, and here is a simple guide. Markdown Guide
LOGIN TO POST A BLOG
Author: Satori
Posted: 2018-01-17 18:05:34
Views: 833
Comments: 5
1月9号linode告诉我由于安全因素需要重启机器来升级,结果回来ssh登陆一看,uwsgi挂掉,同时supervisor也挂掉了,导致uwsgi没有重启,全部网站页面502。看原因应该是依赖的sock文件在/tmp里面被删了,导致supervisor不能启动。最后我想改默认sock的位置,可以说瞎操作一把,好像网站又能起了。结果今天想看注册功能会不会因为机器重启而挂掉,结果不出所料真的挂了,打开调试发现是注册email发不出去,又从代码上分析,发现没有什么问题。我就不用supervisor来起动,直接用uwsgi命令。结果发现注册能用了。感觉又是supervisor配置问题,最近太忙都没有时间去看。真的是太崩了。[[Monkey 1.gif]][[Monkey 1.gif]]
最近都没有时间来开发新功能了[[Onion 15.gif]],估计过年会有一些时间,顺便服务器和域名都需要续费了。从网站开发到现在都快一年了,时间真快。[[Onion 9.gif]]
Read More
Author: Satori
Posted: 2017-12-13 15:55:27
Category:
C4.5
决策树
技术
机器学习
Views: 837
Comments: 0
## 一、决策树概要
决策树是根据输入的属性进行决策,每次按照一个属性进行判断,走到对应的子节点,直到走到叶节点,也就是最后的输出标签。
## 二、C4.5概要
C4.5算法是生成决策树的一种算法,它在ID3算法上进行改进,在信息熵的基础上引入信息增益,每次决策树选择一个属性进行划分的时候,都计算按照这个属性进行划分得到的信息增益量,每次选择增益最多的方法进行划分,这样就能尽量少地减少决策树深度。
## 三、总体思路
主要是分为三个模块,一个是决策树的构建,这个模块又包含属性的选择这个模块,最后是预测模块。整体流程如下。
先构造树的根节点,然后根据数据的特征找到某个属性,根据这些属性把相同取值的数据项集合在一起,然后将这个属性值去掉。对于根据属性值划分得到的每一个数据集,构造子节点,再根据去掉划分属性后的数据进行子节点的构造,直到构造某个子节点没有属性的时候,即为叶子节点,然后根据数据的标签决定这个叶节点的最后标签。
![C4.5_sample][1]
如上图,对于这组数据,根据左边的数据,使用C4.5算法,最后选择的就是天气属性,天气有晴、阴、雨三个选择。根据这三种取值,将数据划分为三类,因此决策树当前节点有3个子节点,每个节点的数据都从原数据那里进行拷贝,只不过去掉了天气这个属性。对于这三个子节点,递归进行构造。
在预测的时候,从根节点开始,根据节点检查的属性走到对应的子节点上,重复以上过程直到走到叶节点上。
## 四、属性的选择
采取C4.5算法,目的就是对于当前的数据选择一个最优的划分属性,现在讲解如何选择这个属性,以上图的数据集为例。
首先计算信息熵,数据集D包含14个训练样本,其中属于类别“Yes”的有9个,属于类别“No”的有5个,则计算其信息熵。
```
Info(D) = -9/14 * log2(9/14) - 5/14 * log2(5/14) = 0.940
```
下面对属性集中每个属性分别计算信息熵,如下所示。
```
Info(OUTLOOK) = 5/14 * [- 2/5 * log2(2/5) - 3/5 * log2(3/5)] + 4/14 * [ - 4/4 * log2(4/4) - 0/4 * log2(0/4)] + 5/14 * [ - 3/5 * log2(3/5) - 2/5 * log2(2/5)] = 0.694
Info(TEMPERATURE) = 4/14 * [- 2/4 * log2(2/4) - 2/4 * log2(2/4)] + 6/14 * [ - 4/6 * log2(4/6) - 2/6 * log2(2/6)] + 4/14 * [ - 3/4 * log2(3/4) - 1/4 * log2(1/4)] = 0.911
Info(HUMIDITY) = 7/14 * [- 3/7 * log2(3/7) - 4/7 * log2(4/7)] + 7/14 * [ - 6/7 * log2(6/7) - 1/7 * log2(1/7)] = 0.789
Info(WINDY) = 6/14 * [- 3/6 * log2(3/6) - 3/6 * log2(3/6)] + 8/14 * [ - 6/8 * log2(6/8) - 2/8 * log2(2/8)] = 0.892
```
根据上面的数据,我们可以计算选择第一个根结点所依赖的信息增益值,计算如下所示。
```
Gain(OUTLOOK) = Info(D) - Info(OUTLOOK) = 0.940 - 0.694 = 0.246
Gain(TEMPERATURE) = Info(D) - Info(TEMPERATURE) = 0.940 - 0.911 = 0.029
Gain(HUMIDITY) = Info(D) - Info(HUMIDITY) = 0.940 - 0.789 = 0.151
Gain(WINDY) = Info(D) - Info(WINDY) = 0.940 - 0.892 = 0.048
```
接下来,我们计算分裂信息度量H(V)。属性OUTLOOK有3个取值,其中Sunny有5个样本、Rainy有5个样本、Overcast有4个样本。
```
H(OUTLOOK) = - 5/14 * log2(5/14) - 5/14 * log2(5/14) - 4/14 * log2(4/14) = 1.577
```
属性TEMPERATURE有3个取值,其中Hot有4个样本、Mild有6个样本、Cool有4个样本。
```
H(TEMPERATURE) = - 4/14 * log2(4/14) - 6/14 * log2(6/14) - 4/14 * log2(4/14) = 1.557
```
属性HUMIDITY有2个取值,其中Normal有7个样本、High有7个样本。
```
H(HUMIDITY) = - 7/14 * log2(7/14) - 7/14 * log2(7/14) = 1.0
```
属性WINDY有2个取值,其中True有6个样本、False有8个样本。
```
H(WINDY) = - 6/14 * log2(6/14) - 8/14 * log2(8/14) = 0.985
```
根据上面计算结果,我们可以计算信息增益率,如下所示。
```
IGR(OUTLOOK) = Info(OUTLOOK) / H(OUTLOOK) = 0.246/1.577 = 0.156
IGR(TEMPERATURE) = Info(TEMPERATURE) / H(TEMPERATURE) = 0.029 / 1.557 = 0.0186
IGR(HUMIDITY) = Info(HUMIDITY) / H(HUMIDITY) = 0.151/1.0 = 0.151
IGR(WINDY) = Info(WINDY) / H(WINDY) = 0.048/0.985 = 0.0487
```
最后发现是OUTLOOK属性的信息增益率最高,因此选择OUTLOOK属性进行划分。
## 五、优化剪枝
优化剪枝是比较重要的,我这里做了最简单的剪枝,就是当某个节点所有标签都是一样的值的时候,直接作为叶子节点,避免无意义地增加深度。
## 六、具体实现
### 1. 数据的初步处理
先定义输入数据的格式,是一个json文件。最外层是数组,每个元素是一个字典,字典的键有两个,一个是`attr`代表数据的属性集合,一个是`label`,代表这个数据的输出标签。对于`atrr`来说,后面接一个字典,字典的键就是每个属性名,值就是这个属性下的取值。
这一步主要是将输入数据进行处理,得到一个数据分布的字典`distribution`,字典的每一个键为属性的名称,每个属性名称之后接一个字典,这个字典的键是这个属性的所有取值,值也是一个字典,这个字典记录的是在每一种取值下每个标签出现的次数。
具体做法就是读每个数据的`attr`,根据这个数据的取值以及标签构造分布。同时顺便统计每个标签下数据的个数,用`label_count`表示。这部分代码如下。
```
# distribution: {"group_name1": {"group_val1": {"label1": x, "label2": y, ...}, "group_val2":...}, "group_name2": ...}
distribution = {}
# label_count: {"label1": x, "label2": y, ...}
label_count = {}
for single_data in data:
attrs = single_data["attrs"]
label = single_data["label"]
# add to label_count
if label_count.get(label):
label_count[label] += 1
else:
label_count[label] = 1
# add to distribution
for group_name, group_val in attrs.items():
if not distribution.get(group_name):
distribution[group_name] = {}
group_vals = distribution[group_name]
if not group_vals.get(group_val):
group_vals[group_val] = {}
val_labels = group_vals[group_val]
if val_labels.get(label):
val_labels[label] += 1
else:
val_labels[label] = 1
```
### 2. 计算信息熵
先是数据集的信息熵`info_label`。
```
# calculate info_label
info_label = 0
all_count = len(data)
for label, count in label_count.items():
info_label += (-count) / all_count * math.log(count / all_count) / math.log(2)
```
然后计算每一个属性的信息熵`info_attrs`
```
# calculate info_attrs, info_attrs: {"group_name1": x, "group_name2": y, ...}
info_attrs = {}
for group_name, group_vals in distribution.items():
info_attrs[group_name] = 0
for group_val, label_vals in group_vals.items():
cur_attr_count = functools.reduce(lambda x, y: x + y, label_vals.values())
cur_val = 0
for label, count in label_vals.items():
cur_val += -(count) / cur_attr_count * math.log(count / cur_attr_count) / math.log(2)
cur_val *= cur_attr_count / all_count
info_attrs[group_name] += cur_val
```
接着计算信息增益值`gain_attrs`。
```
gain_attrs = {}
for group_name, val in info_attrs.items():
gain_attrs[group_name] = info_label - val
```
接着计算分裂信息度量`h_attrs`。
```
# calculate the split info, h_attrs: {"group_name1": x, "group_name2": y, ...}
h_attrs = {}
for group_name, group_vals in distribution.items():
h_attrs[group_name] = 0
attr_val = {}
for attr, labels in group_vals.items():
attr_val[attr] = functools.reduce(lambda x, y: x + y, labels.values())
for attr, count in attr_val.items():
h_attrs[group_name] += -(count) / all_count * math.log(count / all_count) / math.log(2)
```
最后计算计算信息增益率`igr_attrs`,并返回最大值对应的属性。注意这里当分裂度为0,也就是只有一种分裂的时候,相当于属性只有一种取值,就直接返回那个属性(相当于igr无限大)。
```
igr_attrs = {}
for group_name, val in gain_attrs.items():
if abs(h_attrs[group_name]) < 1e-6:
return group_name
igr_attrs[group_name] = val / h_attrs[group_name]
return max(igr_attrs, key=igr_attrs.get)
```
### 3. 决策树的构造
首先定义决策树节点的结构。
```
class Node(object):
def __init__(self):
self.children = {} # {"attr1":Node1, "attr2":Node2, "attr3":Node3, attr:Node}
self.split_attr = None # select the split attribute
self.label = None
def is_leaf(self):
return len(self.children) == 0
```
`children`代表子节点,`split_attr`代表该节点划分子节点的属性,这两个属性对于中间节点有意义,同样`label`也只对叶节点有用,代表这个节点输出的标签是什么。
然后是具体构造过程,我采用广度优先的方法进行构造。有两个队列,`nodes`和`datas`表示待处理的节点以及待处理节点的数据。
对于每一个节点,拿到数据,判断这个数据属性是否被取光了,如果是,那就代表这就是叶子节点,这时候需要对数据的标签进行统计,得到最多的标签,作为该节点的标签。否则,先检查能否剪枝,即所有数据的标签是否一样,如果是一样的,也将这个节点作为叶子节点,标签就是那个标签。如果都不满足,则需要调用之前提到的方法来选择一个最佳划分属性,得到这个属性之后,根据这个属性的不同取值进行划分,对于每个划分的数据集,删去那个选择的属性。并根据数据集的个数构造相同个数的子节点入队,同时将前面构造的划分数据集入队,同时对于新构造的节点,将它和当前节点的`children`属性关联。当队列为空的时候,返回根节点即可。代码如下。
```
def make_nodes(data):
# single data: {"attrs": {"group_name1": val1, "group_name2": val2, ...}, "label": x}
# data: [single_data1, single_data2, ..., single_datak]
# datas: [data1, data2, ..., datan]
root = Node()
nodes = [root]
datas = [data]
while len(datas) > 0:
cur_data = datas[0]
cur_node = nodes[0]
del datas[0]
del nodes[0]
if len(cur_data) == 0:
# TODO: deal with no data
pass
else:
left_attrs = cur_data[0]["attrs"].keys()
if len(left_attrs) == 0:
# if reach a leaf node
# find the most common label
labels= [x["label"] for x in cur_data]
c = Counter(labels)
cur_node.label = c.most_common(1)[0][0]
else:
# cut tree if all data has the same label
labels = {x["label"] for x in cur_data}
if len(labels) == 1:
cur_node.label = cur_data[0]["label"]
continue
# else split the data by one attribute
split_attr = get_attr(cur_data)
cur_node.split_attr = split_attr
attr_vals = set([x["attrs"][split_attr] for x in cur_data])
for attr_val in attr_vals:
next_data = copy.deepcopy(list(filter(lambda x:x["attrs"][split_attr] == attr_val, cur_data)))
for ind in range(len(next_data)):
next_data[ind]["attrs"].pop(split_attr, None)
datas.append(next_data)
next_node = Node()
cur_node.children[attr_val] = next_node
nodes.append(next_node)
return root
```
### 4. 预测过程
这部分相对就简单很多了,对于新来的数据组,对于每一组数据,从root出发,看`split_attr`是根据什么属性划分子节点的,再找到自己的那个属性,根据`children`的键找到对应跳转的子节点。重复一直找到叶子节点。当然如果到某一步无路可走,那就直接返回空。代码如下。
```
def predict(root, data):
res = []
for cur_data in data:
cur_node = root
try:
while not cur_node.is_leaf():
split_attr = cur_node.split_attr
cur_attr = cur_data[split_attr]
cur_node = cur_node.children[cur_attr]
res.append(cur_node.label)
except:
res.append(None)
return res
```
然后是统计正确个数,然后和标准结果进行比较,代码如下。
```
root = make_nodes(train_data)
expect_results = [x["label"] for x in test_data]
predict_data = [x["attrs"] for x in test_data]
predict_results = predict(root, predict_data)
total = len(test_data)
right = 0
for ind in range(len(test_data)):
if expect_results[ind] == predict_results[ind]:
right += 1
acc = right / total
print("right:", right, "total:", total, "accuracy:", acc)
```
## 七、完整代码
详见[我的github页面][2]
## 八、总结
决策树以及C4.5算法看起来还是比较简单,但是自己写起来要计算信息增益之类的还是比较麻烦,难点在于数据结构的设计上,如何设计使代码写起来更轻松,同时让遍历数据代价降到最小,使运行时间降低。同时在一个nursing的数据集上跑,2秒左右得到了结果,正确率有97.9%,说明效果还不错。总之还是有很长一段路需要走。
[1]: https://chongliu.me/media/user_1/C4.5_sample.png
[2]: https://github.com/KomeijiSatori/C4.5
Read More
Author: Satori
Posted: 2017-12-10 14:53:37
Category:
Django
Javascript
技术
Views: 973
Comments: 0
## 一、背景
在微博以及微信等现代社交媒体上,@功能已经非常常见。在任意地方只需打一个@,后面接想要@的人,就能很轻松的让对方知道这条消息。
## 二、总体思路
虽然这个功能看起来非常简单,但是还是有不少问题需要解决。首先是原有的消息系统不能支撑这个业务。最大的问题就是之前对于消息的类型判断和`content_type`直接挂钩。如果是博客订阅,`content_type`就是博客,如果是消息回复,那`content_type`就是评论。但是,当要加入@功能的时候,这就会出问题。@人的地方有可能是博客,也有可能是评论。因此仅仅通过`content_type`是不足以支撑这个业务。
在多次考虑之后,我决定在原有Notification的基础上增加`type`字段,用于判断消息类型。如果是订阅,就是`Subscribe`,如果是评论,就是`Reply`,如果是@,就是`At`。这样根据`type`以及`content_type`就能确定消息的类型以及消息源。改进后的Notification如下。
```
class Notification(models.Model):
NOTIFICATION_TYPES = (
('Reply', 'Reply'),
('Subscribe', 'Subscribe'),
('At', 'At'),
)
unread = models.BooleanField(default=True)
type = models.CharField(max_length=max([len(x[0]) for x in NOTIFICATION_TYPES]), choices=NOTIFICATION_TYPES)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
object_id = models.PositiveIntegerField()
content_object = GenericForeignKey('content_type', 'object_id')
def __str__(self):
return str(self.type) + " from " + str(self.content_object)
```
有了Notification之后,就是在博客以及评论发表的时候,这则匹配@,然后提取@的用户,添加对应的Notification,这样就能实现@功能了。
## 三、具体实现
### 1. Notification Service的实现
就像Blog Service一样,添加一个Notification Service来处理和消息相关的操作,将之前的订阅以及评论的消息添加重写,主要是把type字段填上,同理在查询的时候,将type作为条件进行筛选,这部分代码就不展示了。
关键的部分在于@部分。这个主要是正则匹配,将发表的文字的@的用户全部抽离出来,然后添加Notification,当然没有的用户就会被过滤掉。代码如下。
```
@classmethod
def create_at_notification(cls, instance, text):
name_list = re.findall('@([^\s]+)', text)
res = []
for name in name_list:
user = User.objects.filter(username=name)
if len(user) > 0:
user = user[0]
notification = Notification.objects.create(type="At", content_object=instance, user=user)
res.append(notification)
return res
```
接着是定义两个函数得到由博客来的@消息以及由评论来的@消息,筛选条件就是type是at,用户是请求用户,来源是博客或者是评论,代码如下。
```
@classmethod
def get_at_blog_notifications(cls, user):
return user.notification_set.filter(type="At").filter(content_type__model='blog')
@classmethod
def get_at_comment_notifications(cls, user):
return user.notification_set.filter(type="At").filter(content_type__model='comment')
```
最后再补上未读的@消息数,也很简单了,就是筛选条件是未读,然后类型是At,代码如下。
```
@classmethod
def get_unread_at_count(cls, user):
res = user.notification_set.filter(unread=True).filter(type="At")
return len(res)
```
### 2. @功能使用Notification Service
这部分主要是要在博客或者评论发表的时候,制造Notification。这个主要是通过信号连接,在发出的时候进行触发。代码如下。
```
def post_save_comment_receiver(sender, instance, created, *args, **kwargs):
if created:
NotificationService.create_reply_notification(instance)
NotificationService.create_at_notification(instance, instance.text)
def post_save_blog_receiver(sender, instance, created, *args, **kwargs):
if created:
NotificationService.create_subscribe_notification(instance)
NotificationService.create_at_notification(instance, instance.text)
```
可以看到只需在原有基础上添加`create_at_notification`调用即可。
### 3. 后端传Notification对象
这部分主要是根据用户,将由评论和博客得到的@消息进行返回,并添加属性,让前端能识别是评论还是博客得到的消息,同时标注这条消息的id,让前端知道消息的id,并将消息按照时间排列,然后分页,最后返回,代码如下。
```
@login_required
def get_at_content(request):
if request.is_ajax():
user_id = request.GET.get("user_id")
user = get_object_or_404(User, id=user_id)
at_blog_notification_list = NotificationService.get_at_blog_notifications(user)
at_comment_notification_list = NotificationService.get_at_comment_notifications(user)
at_notifications = []
for ind in range(len(at_blog_notification_list)):
at_blog_notification_list[ind].src = "blog"
at_notifications.append(at_blog_notification_list[ind])
for ind in range(len(at_comment_notification_list)):
at_comment_notification_list[ind].src = "comment"
at_notifications.append(at_comment_notification_list[ind])
at_list = list()
# get obj list
for notification in at_notifications:
obj = notification.content_object
# add related notification field
obj.related_notification_id = notification.id
obj.unread = notification.unread
obj.src = notification.src
at_list.append(obj)
at_list = sorted(at_list, key=lambda x: x.publish_time, reverse=True)
page = request.GET.get('page')
items, page_list = BlogService.get_paginated_items(at_list, page)
context = {}
context['items'] = items
context['page_list'] = page_list
context['origin_ajax_url'] = request.get_full_path()
return render(request, "accounts/at_received.html", context)
else:
raise Http404("Page not found!")
```
### 4. 前端返回渲染后结果
从后端得到一系列消息之后,将它进行渲染,并根据是博客还是由评论得到的At消息进行不同的渲染,添加分页等内容,并添加按键监听,使得按下详情按钮就标记已读。还需处理分页之后不同页面的ajax请求。
```
<!--List a list of blogs-->
{% load static %}
{% load blog_tags %}
{% for item in items %}
<div class="blog-div well aos-init aos-animate" data-aos="flip-up">
{% if item.unread %}
<span class="label label-danger brand-new">New</span>
{% endif %}
<p class="lead"><b>{{ item.author }}</b>: {{ item.text|linebreaksbr|truncatewords:30 }}</p>
<p><span class="glyphicon glyphicon-time"></span> Posted on {{ item.publish_time }}</p>
{% if item.src == "comment" %}
<pre><a href="{% url 'blogs:archive' item.blog.id %}" target="_blank"><b><i>{{ item.blog.title }}</i></b></a> <span style="color:grey">written by {{ item.blog.author }} On {{ item.blog.publish_time }}</span></pre>
<a class="btn btn-primary read-at" id="notification_{{ item.related_notification_id }}" href="{% url 'blogs:archive' item.blog.id %}#comment_{{ item.id }}" target="_blank">Get Detail <span class="glyphicon glyphicon-chevron-right"></span></a>
{% elif item.src == "blog" %}
<pre><a href="{% url 'blogs:archive' item.id %}" target="_blank"><b><i>{{ item.title }}</i></b></a> <span style="color:grey">written by {{ item.author }} On {{ item.publish_time }}</span></pre>
<a class="btn btn-primary read-at" id="notification_{{ item.related_notification_id }}" href="{% url 'blogs:archive' item.id %}" target="_blank">Get Detail <span class="glyphicon glyphicon-chevron-right"></span></a>
{% endif %}
</div>
{% endfor %}
<!-- Pager -->
<ul class="pager">
{% if items.has_previous %}
<a class="btn btn-default page-link" id="prev-btn" href="{% url_replace page=items.previous_page_number %}"><</a>
{% endif %}
{% for i in page_list %}
{% if i != '...' %}
{% if i != items.number %}
<a class="btn btn-default page-link" href="{% url_replace page=i %}">{{ i }}</a>
{% else %}
<a class="btn btn-info page-link" href="{% url_replace page=i %}">{{ i }}</a>
{% endif %}
{% else %}
<span>...</span>
{% endif %}
{% endfor %}
{% if items.has_next %}
<a class="btn btn-default page-link" id="next-btn" href="{% url_replace page=items.next_page_number %}">></a>
{% endif %}
</ul>
<script>
$(".page-link").click(function(){
event.preventDefault();
var ajax_url = $(this).attr("href");
$.ajax({
type:"GET",
url: ajax_url,
success: function(result) {
$('#at-received').html(result);
},
error: function(jqXHR, exception) {
$('#at-received').html(exception);
}
});
});
$(".read-at").bind('click', function(event){
var notification = $(this).attr("id");
var notification_id = notification.split("_")[1];
$.ajax({
type: "GET",
url: "{% url 'read_notification' %}",
data: {"notification_id": notification_id},
success: function(result) {
ajax_unread_at_count();
get_notification_count();
ajax_at_received();
$(this).click();
}
});
});
</script>
```
### 5. 前端用户主页面的@消息JavaScript
这部分主要是在DashBoard页面上处理点击`@me`后前端的渲染,以及得到未读@消息个数的显示,都使用ajax请求,主要函数实现如下。
```
function ajax_unread_at_count() {
$.ajax({
type: "GET",
url: "{% url 'unread_at_count' %}",
success: function(result) {
var count = result['count'];
if (count > 0)
{
$('#unread-ats').html(count);
}
else
{
$('#unread-ats').html("");
}
}
});
}
function ajax_at_received() {
$.ajax({
type: "GET",
url: "{% url 'user_at_received' %}",
data: {"user_id": {{ request.user.id }} },
success: function(result) {
$('#at-received').html(result);
},
error: function(jqXHR, exception) {
$('#at-received').html(exception);
}
});
}
```
## 四、总结
这样@功能以及消息系统的更新也就讲完了。不得不感叹业务逻辑的重写和新功能的添加有分不开的关系,如果之前设计的时候多想想抽象的接口和逻辑,后期添加功能也会更容易。
Read More
Author: Satori
Posted: 2017-12-10 11:22:13
Category:
CSS
Django
HTML
JavaScript
技术
Views: 1376
Comments: 0
## 一、背景
在博客和评论里面插入各种表情是非常常见的,虽然网上有各种各样的包,但是普遍都不能自主添加表情以及动态添加分组。因此我需要自己实现一个表情系统。
表情系统的实现还是比较复杂的,难点在于我想要实现一个组件一样的表情栏,在网页相应的地方只需要简单的引用这个组件就能实现在这个组件所在的表单里面的输入框插入表情。
## 二、总体思路
### 1. 总体流程
实现表情功能主要分为两步,第一步是实现一个表情组件,按这个组件的某一个表情能插入一段对应该表情的文字,第二步是前端将这个表情文字进行转化,转换成表情图片。
### 2. 设计方法
对于表情组件而言,需要让它处在一个表单之中,而且这个表单有且仅有一个`textarea`元素,当表情组件某个表情被点击的时候,自动向这个`textarea`光标处插入对应该表情的文字。
对于前端转化而言,需要正则提取表情文字,并将它替换成对应的`img`标签图片。
## 三、具体实现
### 1. 表情组件的实现
使用纯后端的方法来实现。首先需要考虑表情的动态添加以及表情组的动态添加问题,因此需要把一类表情放到表情组文件夹里面,然后在每个文件夹添加对应的表情。后端只需要读取对应目录的文件就能动态地得到表情组以及每个表情。
而要实现处处能引用这个组件,我选择的是使用`template tag`来返回组件的html代码。这样每次只需要在其他template引用这个tag就可以得到表情组件了。
接着只需要拼接好html代码,将读到的文件及文件夹名字进行解析,就可以得到表情组名以及每个表情组下每个表情的名字。然后封装成`img`标签,同时为了方便之后插入表情文字,将表情所属的组名以及表情的名字装入`img`标签的数据段里面。
具体代码如下。
```
@register.simple_tag
def emoticon_bar():
cur_dir = os.path.join(settings.MEDIA_ROOT, "emoticon")
emoticon_set_names = [x for x in os.listdir(cur_dir) if os.path.isdir(os.path.join(cur_dir, x))]
# sort by the ascii code
emoticon_set_names = sorted(emoticon_set_names)
tab_list = ""
img_list = ""
for set_ind, emoticon_set_name in enumerate(emoticon_set_names):
emoticon_dir = os.path.join(cur_dir, emoticon_set_name)
files = [x for x in os.listdir(emoticon_dir) if os.path.isfile(os.path.join(emoticon_dir, x))]
# sort the file by ascii code
files = sorted(files)
urls = [settings.MEDIA_URL + "emoticon/{}/{}".format(emoticon_set_name, x) for x in files]
cur_img = ""
for ind, url in enumerate(urls):
cur_img += """<img src="{}" data-filename={}>""".format(url, files[ind])
cur_set = """
<div class="emoticon-set" data-emoticon_set_name="{emoticon_set_name}">
{img_str}
</div>
""".format(emoticon_set_name=emoticon_set_name, img_str=cur_img)
img_list += cur_set
tab_list += """
<a class="btn btn-default" data-ind={set_ind}>{set_name}</a>
""".format(set_ind=set_ind, set_name=emoticon_set_name)
string = """
<p>Emoticons:</p>
<div class="emoticon-div">
<div class="emoticon-tab">
{tab_list}
</div>
<div class="emoticon-img">
{img_list}
</div>
</div>
""".format(tab_list=tab_list, img_list=img_list)
return mark_safe(string)
```
这样在后面的模板里面,只要引用`{% emoticon_bar %}`就能得到表情组件了。
但只靠后端也不能实现组件的功能。因为前端需要控制按下表情后向`textarea`插入相应表情文字的工作,这需要用js实现,定义一个函数,传入一个textarea的jquery对象,再传入需要插入的文字。实现方法主要是先得到`textarea`光标的起始位置和终止位置,将这个位置的所有字符替换为text即可,同时还需要还原插入后光标的位置,只需要让新光标移动text的长度即可。代码如下。
```
function make_text(textarea, text) {
var s = textarea.get(0).selectionStart;
var e = textarea.get(0).selectionEnd;
var val = textarea.get(0).value;
textarea.get(0).value = val.substring(0, s) + text + val.substring(e);
textarea.get(0).selectionStart = e + text.length;
textarea.get(0).selectionEnd = e + text.length;
textarea.focus();
}
```
接下来是定义每个表情被按下的操作,主要是找到是哪个图片被按下来确定插入的表情文字,以及找到需要插入的`textarea`的位置。找插入文字可以利用之前后端传过来的`emoticon-set`的`data-emoticon_set_name`属性来确定类别,再用该图片的`data-filename`属性得到名称。再进行拼接。我使用的表情文字定义是`[[set_name空格file_name]]`,原因是尽量避免与markdown的`[][]`的冲突。然后是找到`textarea`的位置,具体做法是找到这个表情组件所属的form,再在form的后代里面找`textarea`。最后调用`make_text`函数即可实现文字的添加。代码如下。
```
$(".emoticon-set img").click(function(){
var set_name = $(this).parent().attr("data-emoticon_set_name");
var file_name = $(this).attr("data-filename");
var string = "[[" + set_name + " " + file_name + "]]";
// find the nearest ``textarea`` above the current bar
var textarea = $(this).closest("form").find("textarea");
make_text(textarea, string);
});
```
至于控制组件的其他点击,比如单击不同组,表情的切换控制不是非常核心的代码,简要思路就是使用一个data标签来确定哪个组被选定了,判断是第一次点击标签还是标签跳转还是点击同样标签,这三种情况对应处理一下即可。
关于css美化简要说要让用户快速得知自己正在点击的按钮,让鼠标悬停的时候将图片的不透明度改变,并调整大小,代码如下。
```
.emoticon-set img {
cursor: pointer;
transition: transform 0.5s;
opacity: 0.6;
}
.emoticon-set img:hover {
transform: scale(1.2, 1.2) rotate(18deg);
opacity: 1;
}
```
### 2. 表情转换的实现
这一部分相对就简单很多了,其实只需要将所有的字串正则替换为img标签,代码如下。
```
html = html.replace(/\[\[([^ \[\]]+?) ([^ \[\]]+?)]]/g, '<img src="/media/emoticon/$1/$2">');
```
同时注意为了跟原有markdown协同工作,需要抽象出一个统一的render接口,首先渲染markdown,然后渲染表情,函数如下。
```
function render_content(text)
{
// first default markdown render
var renderer = new marked.Renderer();
renderer.code = function (code, language) {
return '<pre class="prettyprint"><code class="hljs">' + hljs.highlightAuto(code).value +
'</code></pre>';
};
var html = marked(text, { renderer: renderer });
// render the emoji
html = html.replace(/\[\[([^ \[\]]+?) ([^ \[\]]+?)]]/g, '<img src="/media/emoticon/$1/$2">');
return html;
}
```
## 三、总结
表情这个功能总体实现难度还是不小的,要做到鲁棒性以及可重用性才是真正的难点。这种设计缺陷在于需要让组件置于一个form内,而且form内还需要只有一个textarea才能正常工作,因此还需要考虑一下更好的方法。
Read More
Author: yxp
Posted: 2017-12-07 20:32:36
Views: 590
Comments: 0
[[Emoji 9.png]][[Monkey 13.gif]]
Read More
Author: yxp
Posted: 2017-12-01 18:59:14
Views: 769
Comments: 11
@hxs2580 。。。
Read More
Author: LaoLi
Posted: 2017-11-02 23:47:37
Views: 890
Comments: 3
一本二十世纪的地缘政治书,通过历史,能量和支配三个概念分析了人类这几千年来的活动,正如几千年前运用海洋进行贸易的腓尼基人,以及后来的葡萄牙人荷兰人等等,作者创造性的运用物理学能量的最低消耗概念,认为人类的发展的本质是减少能量消耗,提高使用效率,因此在当时水路运输高于陆地的效率使掌控航海的族群获得极大的发展空间,在那个莱特兄弟还在制造飞机的时代,作者詹姆斯.费尔格里夫在本书中创造性的判断空中将成为重要的运输区域。这本书就为历史唯物主义提供了新的证据,只是后者更高明的在于将历史的发展上升到了矛盾的对立统一,对生产资料(书中的能量)的垄断是阶级斗争的重要特征。本书另一个创造性观点是“一个有效的国际联盟,通过消除某些浪费能量的渠道,必将在节省能量的道路上迈上一个新的台阶”这是作者在一战刚刚结束的环境下分析并得出的结论,对比现在,惊于作者明锐的洞察力和大胆的判断力,影响深远在今天来读也韵味十足。
Read More
Author: Satori
Posted: 2017-10-14 00:59:21
Views: 2006
Comments: 2
库安装
```
pip install pyahocorasick==1.1.4
```
```
import ahocorasick
A = ahocorasick.Automaton()
with open("pattern.txt") as f:
for ind, line in enumerate(f):
line = line.rstrip('\n')
A.add_word(line, (ind, line))
A.make_automaton()
with open("string.txt") as f:
s = f.read()
with open("res4.txt", "w") as f2:
for item in A.iter(s):
f2.write(str(item[1][1]) + " " + str(item[0] - len(item[1][1]) + 1) + "\n")
```
Read More
Author: Satori
Posted: 2017-10-11 22:57:29
Views: 7560
Comments: 8
1. 先下载Firefox浏览器(有了就不用)
2. 下载Firefox插件greasemonkey,[地址][1],并安装使用。
3. 下载用户脚本,[地址][2],点击安装此脚本。
4. 打开学习页面,就可以拖动进度条了。
### 注意,写这个脚本的是某个北邮前辈,不是我,仅供分享。
[1]: https://addons.mozilla.org/zh-cn/firefox/addon/greasemonkey/
[2]: https://greasyfork.org/zh-CN/scripts/28527-bupt-mooc-helper/
Read More
Author: Satori
Posted: 2017-10-07 12:25:41
Category:
CSS
HTML
JavaScript
vim键位
技术
Views: 912
Comments: 4
## 一、背景
这个功能的想法最早来源于北邮人论坛。当时我刚会用vim,在浏览论坛的时候我不禁试了一下,结果发现真的可以用vim键位,表示很欣喜。这个功能在我刚开始开发这个博客就考虑了,但是当时还有很多其它更重要的功能要开发,结果基本上拖了半年才真正实现了这个功能,算是完成了一个心愿。网页快捷键浏览在其他网站也有,比如twitter可以用j和k来上下切换浏览。
## 二、需求分析
因为后期开发了用户界面,因此用户界面的条目,包括发表的博客,发表的评论,回复我的等等都需要能使用vim键位浏览。同时,在博客详情页面,也能通过vim键位来浏览评论。
这里需要定义一下什么叫用vim键位浏览。就是说当使用vim键位的时候,会选中目标的博客,将它移动到屏幕中央,并在外边框加上颜色进行标注。在浏览博客条目的时候,按照vim键位,`j`就是向下移,`k`就是向上移,`h`就是翻上一页,`l`就是翻下一页。
在博客详情页面中,因为评论是树形结构,因此键位的意义也就稍有不同。`j`就是按照评论树的先序遍历顺序取下一个评论,同理`k`就是取上一个。`h`定义为评论树当前结点的前一个兄弟结点,同理`l`就是下一个兄弟节点。
在键位激活的时候,要做两件事情,一件是将目标窗口标红,另一件是将屏幕移动到目标窗口。这里也就分列表页面和详情页面来讲实现方法。不过在此之前先讲这两个功能都需要实现的逻辑,也就是移动窗口和给窗口加上边框。
## 三、移动窗口及边框着色的实现方法
### 1. 移动屏幕到目标窗口
这一部分主要是实现当用户按下按键之后移动到目标窗口的方法。这里将这个操作封装为一个函数。传入目标窗口以及移动时间。难点在于怎么计算目标窗口的位置。因为是要移动到屏幕中央,因此先判断能不能移到中央。这个取决于目标块的大小。如果目标窗口的高度都比屏幕高度高了,那就不能完整显示,就只能将它移动到屏幕顶端。如果不是的话,就能移动到中央。
首先需要明白怎么样使窗口移动。这里采用的做法是让**整个页面**移动,而不是让该窗口移动。可以理解为屏幕是绝对不动的,动的只有页面,页面在屏幕里面滑动,而屏幕只显示页面当前滑动的位置。使用`scrollTop`的动画来实现整个页面的移动。这个动画需要给出位移。在这种设计下,位移多少就是整个页面上滑动多少。
举个例子,如果位移为0的话页面不移动,位移为100的话页面向上移动100px,可以想象,如果计算出某元素顶部距离页面的距离,比如距离为x,位移x就能刚好移动到那个元素的顶部,也就是屏幕顶部和元素顶部重合。
因此剩下的事情就简单了,首先算出该窗口距离顶部的位移。如果直接让页面移动那么多的话那个窗口就在屏幕顶端了,因此需要页面移动少一点,移动到窗口居中就行了,下面就算少多少距离。这个就简单了,当前窗口需要移动到屏幕底端一半的位置,也就是屏幕高度减去窗口高度除以2。只要页面少往上移动这么多距离就能使窗口刚好居中。代码如下。
```
function move_to_ele(ele, time) {
// scroll to ele
if (ele.size() === 1)
{
var elOffset = ele.offset().top;
var elHeight = ele.height();
var windowHeight = $(window).height();
var offset;
if (elHeight < windowHeight) {
offset = elOffset - ((windowHeight / 2) - (elHeight / 2));
}
else {
offset = elOffset;
}
$('html, body').animate({
scrollTop: offset
}, time);
}
}
```
### 2. 给窗口边框着色
给边框着色就比较简单了,我这里也是封装成一个函数来处理。这个函数传两个参数,一个是当前已经被选中的窗口(没有的话就是空),以及下一个被选中的窗口(可能是下面一个也可能是上面一个窗口)。
着色方法比较简单,就是把当前窗口的`kb`类给移除,给下一个窗口加上`kb`类。而`kb`类定义了样式,也就是加边框。这样通过加上或者移除这个类来实现边框着色与否。`kb`样式如下。
```
.kb {
border: solid 4px rgba(255, 0, 0, 0.6);
}
```
函数实现如下。
```
function color_to_ele(curEle, next)
{
if (next.size() === 1)
{
if (curEle !== null)
{
curEle.removeClass("kb");
}
next.addClass("kb");
}
}
```
## 四、列表的定位
### 1. 找到当前激活的窗口
首先需要找是否已经有窗口被激活了,如果没有的话按`j`从第一个开始浏览,如果有的话就从当前位置开始。我们根据div的class属性来判断是否有激活窗口,有的话就返回那个窗口。代码如下。
```
function getCurrentElement()
{
var ele = $(".blog-div.kb");
if (ele.size() != 1)
{
return null;
}
else
{
return ele;
}
}
```
### 2. 四个键位的定位实现
vim键位的`j` `k` `h` `l`分别对应下移,上移,左翻一页,右翻一页。对于上下移动,调用next方法来获取下一个激活窗口的位置,使用prev方法得到上一个激活窗口的位置。得到目标位置后,再对新窗口的边框进行着色,并移动到对应位置。对于翻页操作,只需模拟点击左翻和右翻按钮来实现。同时需要注意边界情况,比如没有上一页或者下一页,没有上一个窗口或者下一个窗口等等,代码如下。
```
function movedown(time)
{
var curEle = getCurrentElement();
var next;
// no item selected
if (curEle === null)
{
next = $(".blog-div").first();
}
else
{
next = curEle.next(".blog-div");
}
// if not the last item
color_to_ele(curEle, next);
move_to_ele(next, time);
}
function moveup(time)
{
var curEle = getCurrentElement();
// no item selected
if (curEle !== null)
{
var next = curEle.prev(".blog-div");
color_to_ele(curEle, next);
move_to_ele(next, time);
}
}
function moveprev()
{
var btn = $("#prev-btn");
if (btn.size() === 1)
{
btn[0].click();
}
}
function movenext()
{
var btn = $("#next-btn");
if (btn.size() === 1)
{
btn[0].click();
}
}
```
### 3. 绑定键盘按键
最后一步就是将四个按键和刚才的四个函数相连接,设置一个移动时间即可。最后需要注意用户在输入框打字的时候不要监听这四个快捷键。代码如下。
```
var time = 100;
var vim_key_listener = new window.keypress.Listener;
vim_key_listener.simple_combo("j", function() {
movedown(time);
});
vim_key_listener.simple_combo("k", function() {
moveup(time);
});
vim_key_listener.simple_combo("h", function() {
moveprev();
});
vim_key_listener.simple_combo("l", function() {
movenext();
});
$("input[type=text], textarea")
.bind("focus", function() { vim_key_listener.stop_listening(); })
.bind("blur", function() { vim_key_listener.listen(); });
```
### 4. 用户界面的特殊处理
因为用户界面有多个tab,每个tab的窗口类名都一样,因此需要甄别是哪个窗口被激活,保证调用`getCurrentElement`函数的时候只会得到一个元素,就是当前tab的窗口。因此需要在用户界面点击tab的时候,将其他tab中的内容全部清空,再利用ajax请求得到自己tab的内容,这样就解决这个问题了,代码如下。
```
function clear_tabs() {
$("#blog-posts").empty();
$('#comment-posts').empty();
$('#comment-received').empty();
$("#blog-subscriptions-div").empty();
}
$('a[data-toggle="pill"]').on('shown.bs.tab', function (e) {
var target = $(e.target).attr("href");// activated tab
clear_tabs();
});
```
## 五、评论的定位
### 1. 找到当前激活的评论
大体和前面一样的做法,只是将类名改为`comment-body`,代码如下。
```
function getCurrentElement()
{
var ele = $(".kb .comment-body").first();
if (ele.size() != = 1)
{
return null;
}
else
{
return ele;
}
}
```
### 2. 四个键位的定位实现
vim键位的`j` `k`分别对应dom树的先序遍历的下、上一个结点。`h` `l`对应同级兄弟结点的上、下一个结点。
对于先序遍历的结果的获取,使用jQuery选取所有类为`comment-body`的元素,这样获取的列表默认就是dom树先序遍历的结果,然后只需找到当前激活窗口在这个列表的位置,选取上一个或者下一个位置就能得到先序遍历的前后结点。
对于上下兄弟结点的获取,直接找到它的父节点并在父节点调用next方法获取下一个list的位置,同理prev方法得到上一个list位置,也就是找到了兄弟结点的父亲结点。然后只需找那个父亲结点的孩子就能找到下一个窗口的位置。
得到目标位置后,再对新窗口的边框进行着色,并移动到对应位置。同时需要注意边界情况,比如没有上一个或下一个先序遍历的位置,没有上一个或下一个兄弟等等,代码如下。
```
function movenext(time)
{
var curEle = getCurrentElement();
var next;
// no item selected
if (curEle == = null)
{
next = $(".comment-body").first();
color_to_ele(null, next.parent());
}
else
{
next = curEle.parent().next("li").children(".comment-body");
color_to_ele(curEle.parent(), next.parent());
}
// if not the last item
move_to_ele(next, time);
}
function moveprev(time)
{
var curEle = getCurrentElement();
var next;
// no item selected
if (curEle != = null)
{
next = curEle.parent().prev("li").children(".comment-body");
color_to_ele(curEle.parent(), next.parent());
// if not the last item
move_to_ele(next, time);
}
}
function movedown(time)
{
var curEle = getCurrentElement();
// no item selected
if (curEle == = null)
{
var next = $(".comment-body").first();
color_to_ele(null, next.parent());
move_to_ele(next, time);
}
else
{
var all_items = $(".comment-body");
var cur_ind = all_items.index($(".kb .comment-body").first());
if (cur_ind + 1 <= all_items.size() - 1)
{
var next = $(all_items.get(cur_ind + 1));
color_to_ele(curEle.parent(), next.parent());
move_to_ele(next, time);
}
}
}
function moveup(time)
{
var curEle = getCurrentElement();
// no item selected
if (curEle != = null)
{
var all_items = $(".comment-body");
var cur_ind = all_items.index($(".kb .comment-body").first());
if (cur_ind - 1 >= 0)
{
var next = $(all_items.get(cur_ind - 1));
color_to_ele(curEle.parent(), next.parent());
move_to_ele(next, time);
}
}
}
```
### 3. 绑定键盘按键
最后一步就是将四个按键和刚才的四个函数相连接,设置一个移动时间即可。最后需要注意用户在输入框打字的时候不要监听这四个快捷键。代码如下。
```
$(document).ready(function() {
var time = 100;
var vim_key_listener = new window.keypress.Listener;
vim_key_listener.simple_combo("j", function() {
movedown(time);
});
vim_key_listener.simple_combo("k", function() {
moveup(time);
});
vim_key_listener.simple_combo("h", function() {
moveprev(time);
});
vim_key_listener.simple_combo("l", function() {
movenext(time);
});
$("input[type=text], textarea")
.bind("focus", function() { vim_key_listener.stop_listening(); })
.bind("blur", function() { vim_key_listener.listen();});
});
```
至此用vim键位浏览博客列表,用户列表,博客评论的实现方法也就全部实现了。
Read More