Flask下载动态生成文件加一个简单的进度条

前一段码代码的时候碰见了这么一个情况,用户需要点击一个button然后下载一个比较大的动态生成的文件。flask里有一个send_file方法,可以把一个文件返回给客户端。可以先把动态生成的内容写到文件中,然后再用发送静态文件的方法,用send_file方法把这个文件返回,然后再把这个文件删除。不过这样的话就多了磁盘IO这一段时间,感觉很多此一举。

然后看了看send_file方法的代码,实际上是调用了一个FileWrapper的类,返回一个iterator,然后迭代返回块(默认的大小是4KB),和普通的Streaming的方法也没什么不一样的,只是包装了一下。所以直接用生成器构造Response即可。代码如下,关键的地方就只有添加一个Content-Length头,客户端每隔一段时间就可以计算一下当前的下载进度。

from flask import Flask
from flask import Response
from flask import render_template
from flask import make_response
from gevent.pywsgi import WSGIServer
app = Flask(__name__)
app.template_folder = "."

@app.route("/generate-on-fly")
def gen_on_fly():
def gen():
for i in xrange(200000):
yield "l"+"o"*100+"g\n"
resp = Response(gen())
resp.headers["Content-Length"] = resp.calculate_content_length()
return resp

@app.route("/")
def root():
resp = make_response(render_template("./root.html"))
resp.headers["Cache-Control"] = "no-store"
return resp

if __name__ == "__main__":
server = WSGIServer(('', 5000), app)
server.serve_forever()

根目录会返回一个html页面,页面上只有一个进度条和一个按钮。点击按钮会向/generate-on-fly发送请求,然后进度条开始滚动。root.html的内容如下。

<!DOCTYPE html>
<html>
<head>
<script src="https://code.jquery.com/jquery-1.12.4.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
<script>
function updateProgress(evt)
{
var percentComplete = (evt.loaded / evt.total)*100;
$('#progressbar').attr("aria-valuenow", percentComplete);
$('#progressbar').attr("style", "width: "+percentComplete+"%;");
}
function sendreq(evt)
{
var req = new XMLHttpRequest();
req.onprogress = updateProgress;
req.open('GET', 'http://localhost:5000/generate-on-fly', true);
req.send();
}
</script>
</head>
<body>
<div class="row" style="margin: 30px">
<div class="col-lg-6">
<div class="progress">
<div id="progressbar" class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0"
aria-valuemax="100"
style="width: 0%;">
</div>
</div>
</div>
<button type="button" class="btn" onclick="sendreq()">Download</button>
</div>
</body>
</html>

过程大致就是每隔一段时间,就会进入req的onprogress绑定的回调,回调里面计算一下当前下载的百分比,然后更新一下进度条的状态,也不是很复杂。

算法学习笔记(5):Dijkstra

在刷Hackerrank的时候碰到了这道题,其本身倒是十分普通的一道题,照本宣科的把Dijkstra实现一遍就可以做出来了。在讨论区和别人讨论发现的时间复杂度也是会被Accept的。

如果使用数组储存所有unvisited点的距离,在找当前最近点的时候,会需要遍历整个数组,而这是的时间复杂度。如果使用一个最小优先级队列,那么只需要将队首的元素取出,然后min-heapify一下,保持最小堆的性质,而这是的时间复杂度。能够显著减少时间消耗。

但是如果使用最小堆的话,降低堆中元素的优先级的操作就很麻烦了。首先如果想要找到堆中的某个元素就需要的时间复杂度,而修改了这个元素的优先级之后,想要维持最小堆的性质又要的时间复杂度。所以整体而言就变得和没有使用最小堆一样了。

为了解决这个问题有一个弥补的方法。就是在一个散列表中存储优先级队列中对应点的元素的handle。

如果想要降低某个点的优先级,那么可以通过查询散列表,获得最小堆中对应元素的handle,然后直接标志该点已经被删除(但是不实际从堆中删除),然后在储存堆的数组的最后新添加一个储存新优先级和该点的元素。标志删除的操作因为没有破坏最小堆的性质,只是 的时间复杂度,而最小堆中插入一个元素是 的时间复杂度。最后就实现了 的降低优先级的操作。

如果使用python实现的话,python的官方文档中简单介绍了使用heapq实现最小优先级队列的方法。

不过残念的是,add_task()方法的第一行就出现了个遍历dict,而这是的,所以推荐额外再使用一个set来维护entry_finder.keys(),这样查找和删除操作就都是 了。

另外一个有意思的事情是,heappush()原地的,所以pq会在原地修改。而pq中保存的是[distance, vertex]的list,而list是mutable的。所以在remove_vertex()把entry[-1]也就是vertex标志城REMOVED了之后,pq中的对应entry会自动修改。

最后挂上自己的代码。

# Enter your code here. Read input from STDIN. Print output to STDOUT
from heapq import *
 
def add_vertex(vertex, distance=0):
    if vertex in entry_set:
        remove_vertex(vertex)
    entry = [distance, vertex]
    entry_finder[vertex] = entry
    entry_set.add(vertex)
    heappush(pq, entry)
 
def remove_vertex(vertex):
    entry = entry_finder.pop(vertex)
    entry[-1] = REMOVED
    entry_set.remove(vertex)
 
def pop_vertex():
    while pq:
        priority, vertex = heappop(pq)
        if vertex is not REMOVED:
            del entry_finder[vertex]
            return priority, vertex
 
T = int(raw_input())
for T_ in range(T):
    pq = []
    entry_finder = {}
    REMOVED = "REMOVED"
 
    N, M = [int(i) for i in raw_input().split(" ")]
    connection = [[] for conn_ in range(N)]
    for M_ in range(M):
        x, y, r = [int(i)-1 for i in raw_input().split(" ")]
        connection[x].append([y, r+1])
        connection[y].append([x, r+1])
    S = int(raw_input())-1
     
    entry_set = set()
    unvisited = set()
    distance = {}
 
    for node in range(M):
        unvisited.add(node)
        if node!=S:
            distance[node] = float("Inf")
            add_vertex(vertex = node, distance = float("Inf"))
        else:
            distance[node] = 0
            add_vertex(vertex = node, distance = 0)
 
    while len(unvisited)!=0:
        current_distance, current_node = pop_vertex()
        if current_distance==float("Inf"):
            break
        for node, edge in connection[current_node]:
            if node in unvisited:
                new_distance = current_distance + edge
                if new_distance < distance[node]:
                    distance[node] = new_distance
                    add_vertex(node, new_distance)
                distance[node] = new_distance if new_distance < distance[node] else distance[node]
        unvisited.remove(current_node)
 
    for node in range(N):
        if node!=S:
            if distance[node]==float("Inf"):
                print -1,
            else:
                print distance[node],
    print

How to Install Pyeemd on Windows in 3 Steps

1. Prepare Compiling Environment

Download MSYS2 of your windows version from here.

Follow the instructions on the homepage to update the system. Run commands listed below.

$ update-core
$ pacman -Suy

If you have trouble connecting to the official repository of MSYS2, modify the configuration files of pacman, which are located at /etc/pacman.d/.

For those living in China, I recommend a MSYS2 mirror hosted by LUG@USTC. Just follow the instructions on their wiki.

We need to install the mingw64 toolchain (suppose we use 64-bit windows) and some tools like tar and make. The mingw64 toolchain contains compiler and other compiling-related tools. Just run the command below.

$ pacman -S mingw-w64-x86_64-toolchain tar make

After the installation, close the MSYS2 window and run mingw64_shell.bat located at MSYS2 root directory. You may notice the word in purple before tilde changes from MSYS to
MINGW64. According the introduction of MSYS2, the MINGW64 shell is more natively oriented. Meanwhile, and the MSYS2 shell provides some linux features.

The mingw subsystems provide native Windows programs and are the main focus of the project. These programs are built to co-operate well with other Windows programs, independently of the other subsystems.

The msys2 subsystem provides an emulated mostly-POSIX-compliant environment for building software, package management, and shell scripting. These programs live in a virtual single-root filesystem (the root is the MSYS2 installation directory). Some effort is made to have the programs work well with native Windows programs, but it's not seamless.

2. Compile GSL and Libeemd

Libeemd depends on GNU Scientific Library (GSL). Run following command in MINGW64 shell to download GSL source code.

$ curl -o gsl-latest.tar.gz http://mirror.clarkson.edu/gnu/gsl/gsl-latest.tar.gz
$ tar zxvf gsl-latest.tar.gz

Change directory to the GSL directory and compile it. (Suppose we have GSL 2.1 here.)

$ cd gsl-2.1
$ ./configure
$ make
$ make install

GSL is installed to /usr/local/ without assigning --prefix flag. So we need to modify PKG_CONFIG_PATH variable before compiling libeemd so compiler may find GSL libraries.

$ export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/lib/pkgconfig

Then we clone libeemd source code from bitbucket and compile it. A makefile is included so we don't need to run a configure script.

$ git clone https://bitbucket.org/luukko/libeemd.git
$ cd libeemd
$ make

After compiling, you may use file command to check out file type of libeemd.so. It's a dll file! It sounds weird but I will continue with this convention. Otherwise you may modify Makefile and remake by yourself.

$ file libeemd.so
libeemd.so: PE32+ executable (DLL) (console) x86-64, for MS Windows

3. Install pyeemd module to Anaconda2

Anaconda is a great platform based on python for data analytic. It contains lots of modules and make it much simpler to do calculation.

But there're no EMD related modules included in Anaconda by default. And I can't find any one in pypi or by conda search command. (There's one named emd in pypi actually. But there's no document and I don't think it reliable.) That's why I write this blog.

If you tried googling about libeemd and windows before reading this blog, you may have found this issue. Owner of libeemd stated that using libeemd on windows may be unpleasant.

Please also note that installing libeemd on Windows is currently quite unsupported – I haven't tested it, and I have no way to test it.

This unpleasantness may be caused by compiling, which we have already fixed, and dll loading. Library searching is different in linux and windows. I know little about os. So it may be appropriate to quote from tldp.org, rather than gibberishing by myself.

The interface used by Linux is essentially the same as that used in Solaris, which I'll call the ''dlopen()'' API. However, this same interface is not supported by all platforms; HP-UX uses the different shl_load() mechanism, and Windows platforms use DLLs with a completely different interface.

It means, instead of appointing library search work to os, that we need to load dll files libeemd.so depending on on our own. Luckily finding dll dependency and loading them in python is not hard.

You may use objdump to find dll dependency as shown below.

$ objdump -p libeemd.so | grep "\.dll"
        DLL Name: KERNEL32.dll
        DLL Name: msvcrt.dll
        DLL Name: libgomp-1.dll
        DLL Name: libgsl-19.dll

There's a handy tool named dependencywalker to find dll dependency as well. After opening the dll file, namely libeemd.so here, dependencies will be shown in the top-left column.

dll

We have known that libeemd.so depends on kernel32.dll, msvcrt.dll, libgomp-1.dll and libgsl-19.dll. Kernel32.dll and msvcrt.dll are located in C:/windows/system32 and we don't need to worry about them. We have to worry about libgomp-1.dll's and libgsl-19.dll's dependencies. (pretty messy, right?)

Do basically same to them as to libeemd.so. And we get this dependency tree. (You don't get it directly. I draw this manually.)

libeemd.so
|-KERNEL32.dll
|-msvcrt.dll
|-libgomp-1.dll
  |-KERNEL32.dll
  |-msvcrt.dll
  |-libwinpthread-1.dll
    |-KERNEL32.dll
    |-msvcrt.dll
  |-user32.dll
  |-libgcc_s_seh-1.dll
    |-KERNEL32.dll
    |-msvcrt.dll
    |-libwinpthread-1.dll
|-libgsl-19.dll
  |-KERNEL32.dll
  |-msvcrt.dll
  |-user32.dll
  |-libgslcblas-0.dll
    |-KERNEL32.dll
    |-msvcrt.dll

This tree ends in kernel32.dll, msvcrt.dll and user32.dll. They are all located in C:/windows/system32, which is in system searching path. Other dlls are located in MSYS2 directory. You can find them in MINGW64 shell by find / -name command.

After all these dlls being found, we may begin installing pyeemd to Anaconda.

Change directory to libeemd/pyeemd directory and install pyeemd module to Anaconda2/Lib/site-packages directory.

Make sure you're using Anaconda2's python when running the setup.py script. You can use absolute path in MINGW64 shell like /c/Users/juiceyang/Anaconda/python . (You should replace with your own username here.)

$ /c/Users/juiceyang/Anaconda2/python setup.py install

Change directory to Anaconda2/Lib/site-packages/pyeemd, you may find they are already there. But once you try to run pyeemd.py, a windows error 126 prompt out. That's because python can't find dlls libeemd.so depending on. We may fix this by insert some lines into pyeemd.py. It looks like this after inserting.

# Load libeemd.so
# ---------new lines start from here
ctypes.WinDLL("kernel32.dll")
ctypes.WinDLL("msvcrt.dll")
ctypes.WinDLL("user32.dll")
ctypes.WinDLL("C:\\msys64\\mingw64\\bin\\libwinpthread-1.dll")
ctypes.WinDLL("C:\\msys64\\mingw64\\bin\\libgcc_s_seh-1.dll")
ctypes.WinDLL("C:\\msys64\\mingw64\\bin\\libgomp-1.dll")
ctypes.WinDLL("C:\\msys64\\home\\YOUR_USERNAME\\gsl-2.1\\cblas\\.libs\\libgslcblas-0.dll")
ctypes.WinDLL("C:\\msys64\\home\\YOUR_USERNAME\\gsl-2.1\\.libs\\libgsl-19.dll")
# ctypes.WinDLL("C:\\msys64\\home\\YOUR_USERNAME\\libeemd\\libeemd.so") --> This line is commented out. Load libeemd.so in next 3 lines.
# ---------new lines end
_LIBDIR = os.path.dirname(os.path.realpath(__file__))
_LIBFILE = os.path.join(_LIBDIR, "libeemd.so")
_libeemd = ctypes.CDLL(_LIBFILE)

ctypes.WinDLL() function loads dlls. First three dlls are in system32 folder so we need no absolute path. Other dlls is located by absolute path.

Then run following snippet in Jupyter notebook to check out whether it's working!

%matplotlib inline
from pyeemd import ceemdan
from pyeemd.utils import plot_imfs
import matplotlib.pyplot as plt
import numpy as np

y = np.sin(2*np.pi*np.linspace(0,1,1000))+np.sin(20*np.pi*np.linspace(0,1,1000))
plt.plot(y)
plt.show()
imfs = ceemdan(y, S_number=4, num_siftings=50)
plot_imfs(imfs)
plt.show()

Here's my output. Have fun using pyeemd!

notebook

《undertale》——奥之细道,温暖人心的地下之旅

Undertale

之前玩osu!的时候偶然下载过一张谱面——toby fox - Battle Against a True Hero这张谱面的音乐听起来很赞,后来搜了一下发现是来自于一个叫做undertale的独立游戏。

之后又上steam看了一眼,发现游戏的好评如潮,预感是一个独立佳作。于是默默的放入愿望单,开始等打折(穷人过日子真是不容易)。猴年春节之时,垃圾平台steam在开放了国区之后,十分上道的开始了针对国区玩家开始了春节攻势特卖。值此有钱有闲的佳节,赶紧入手了这款游戏。迫不及待的通了关。

undertale的游戏画面

undertale的游戏画面十分『朴素』

1.画面

《undertale》是一个『像素风游戏』。玩过小霸王红白机的玩家大概都了解什么叫做『像素风』,小时候长时间对着20寸的一个小电视,看满屏幕的马赛克看到眼瞎的经历实在是太刻骨铭心。后来软磨硬泡之下家里终于买了电脑,玩到了第一个3D游戏——《极品飞车3》的时候,整个人都惊呆了。

然而最近『像素风』这个复古属性好像特别流行,举几个话题作品作为例子,移动平台上有《Knights of Pen & Paper》、《Pixel Dungeon》,PC平台上有《以撒的结合》、《盗贼遗产》。好像在这样的大环境下,『像素风』也逐渐的变成了一种视觉风格,而不是制作水平有限造成的缺陷。《undertale》最大的短板——画面,在这样的大环境下,好像也不是那么糟糕了。就好象吃多了大鱼大肉,偶尔吃一次野菜仿佛也别有风味一样,这年头玩多了3A大作,猛地玩一款像素游戏调节一下,也并不觉得瞎眼。反而觉得自己是一个有理想有情怀、脱离了低级趣味的人。(虽然我还想再次重申我认为画面就是游戏性的一部分,不服就是不客观)

2.音乐

在『像素到底』的指导思想下,游戏制作者也没有像《去月球》的作者一样,选择用正常的音乐来搭配游戏。《undertale》中的大量音乐都是8-bit风格的音乐,并且风格多种多样,例如在和遇到的第一个小boss——爱哭鬼Napstablook——的战斗中,背景音乐《ghost fight》有着明显的爵士乐特征。

制作者toby fox说到底本职工作还是作曲的,游戏的背景音乐对情感引导和氛围烘托起了很大作用,比如从ruins第一次到Toriel家的时候,节奏松缓的吉他曲让玩家立刻就放下戒备心,知道Toriel是个好人。各个角色的主题曲也对应着各个角色的特点。比如一听到下面这首sans的角色曲《sans.》就能认识到他平时示人的滑稽的一面,再听听另一首Papyrus的角色曲《Bonetrousle》,两首相似的曲风甚至能让人直接听出他们俩的兄弟关系。

3.游戏机制

//下有小部分剧透

《undertale》的游戏内容主要是随着叙事进行的动作(action)与解密(puzzle)。

游戏并非是典型的解密游戏,地图中的机关解谜不需要很高的智商(像我这种《时空幻境》都玩不下去的都可以很愉快的打通的程度)。

战斗是回合制动作,听起来这个类型好像挺奇葩的。简单来说就是怪物的回合中,他们会释放出弹幕攻击你,你需要躲避弹幕的攻击(类似东方等常见弹幕游戏)。玩家回合中则有许多种选择来结束战斗,这点类似于《博德之门》,具体的结束战斗的方法『因怪而异』,需要玩家探索。战斗中的弹幕十分鬼畜,玩家需要有很高水准的弹幕躲避技巧(东方系列我大概是能稳定打通Normal难度有一般概率打通Hard难度的沙包水平,玩本作感觉很十分吃力的程度),卡boss了打个几十次过不去是很正常的事情。所以手残的玩家还是不推荐买这个游戏了,看看别人的视频通关就好。

undertale-screenshot-2

十分典型的一个战斗画面,玩家需要操纵红心代表的玩家,躲避白色的弹幕。(画面实在是很有『风味』)

从游戏内容方面来讲,我觉得《undertale》相对于《去月球》强一些。这也许是跟玩家的视角有关系,《去月球》基本上都是作为旁观者的视角,相对来说平淡一些;而《undertale》是第一人称视角,需要让玩家切身的感受到许多困难来『stay determined』(这是游戏中的一句关键的话语),而这也只能通过高难度的动作内容来实现。玩家在玩完游戏之后才能更真切的感受到游戏制作者想要传达的意思。

4.幽默的文本

//有剧透

《undertale》目前还没有汉化,游戏中的文本都是英文的,所以不熟悉英语的玩家玩起来可能会经常get不到笑点。

角色的对白中有着大量的吐槽以及西方的双关语幽默,这让玩家在『和平线』以及『中立线』中能感受到更立体更真实的地下世界的居民们,而也能让玩家在『屠杀线』中面对着怪去楼空更为失落伤心。

记得最清晰的就是游戏中的科学家Alphys特别喜欢看一个类似于《光之美少女》的动画片,说起话来对于这个动画片中的种种细节如数家珍。而Alphys在社交网络上也表现的特别胆怯。而Alphys是一只带着眼镜的类似于三角龙的怪物。玩的时候就感觉死宅们被狠狠的吐槽了啊。

alphy_anime

这个就是科学家Alphy喜欢的动画片《Mew Mew: Kissy Cutie》,有些my little pony的感觉?

整个游戏的过程中,虽然躲弹幕十分虐人,由于这些幽默的对白和喜感的角色,玩家多少也能心中轻松一些。也算是对于游戏的气氛起到了一定的平衡作用。(就像『东方如果没有萌妹子谁还玩啊!』这样的效果)

5.叙事与角色

//下有剧透

终于到了我最想说的叙事部分。

《undertale》是我玩过的2015年发行的游戏中叙事最用心的游戏(本来想说是近期内玩过的叙事最用心的游戏,后来想想感觉要给《白色相簿2》让让路,不过拿它和一个gal比好像有些不公)。

《undertale》与多数用心叙事的游戏一样,有着大量的可探索的文本。玩家可以通过和各个NPC聊天,探索地图来还原这个地底世界的原貌,还原每个NPC的性格。游戏中基本没有什么绝对的路人角色,即使是战斗中的普通NPC也有着自己的特色,玩家需要通过了解这些特色来进行战斗。

这些可探索的文本和丰满的角色让玩家能感受到一个真实的地底世界。正常玩家在作出每个选择的时候,都会带入自己的感情。我相信每个正常玩家在『屠杀线』中对Toriel动手的时候,心里都十分难受;而在『中立线』最终选择是否原谅Flowey的时候,心里都会十分纠结。

游戏为玩家提供了三种路线。每种路线中,因为玩家的选择不同,NPC们的台词都不同,对玩家的态度也不同,甚至战斗的内容都会发生变化。NPC不是头顶问号只会收发任务的机器,玩家能感受到一个个鲜活的灵魂。而想要完整的了解地下世界以及各个角色的故事,玩家需要通关三条线。

三种路线中的『屠杀线』和『和平线』的完成条件十分苛刻,而大多数玩家在不看攻略的情况下,第一次玩游戏多数都会进入『中立线』。在结局之后,游戏中的角色Flowey会提醒玩家自己的种种选择所代表的含义,以及为什么进入了这个结局。玩家会在结局对自己有一个认识。

这个游戏对于地下世界和人物的塑造有多成功呢?在结局之后,玩家甚至会想没事回去看看游戏中的人物们过得怎么样。事实上游戏制作者也为他们准备了结局之后的台词。玩家回去之后会看到一个与之前不同的世界。好像整个地下世界就是真实在运作的世界一样。

With a highly satisfying, impactful, mysterious, and exciting story, I was constantly wanting to get right back to it when I wasn't busy.

——metacritic评论

这些用心之处也导致了游戏内容的膨胀。一开始游戏制作者只准备制作两个小时长度的内容,而随着开发的进行,游戏时长一延再延。而事实上我打通中立线花了将近13个小时。打通三条线大概需要25个小时左右(『屠杀线』实在不想自己玩,看得别人做的视频)。这样庞大的剧情,对于一个小品级的独立游戏来说,是十分难能可贵的。

6.后记

《undertale》的制作背景多少有些类似于《洞窟物语》,本作的作者toby fox也是基本一人独立完成了游戏的编剧、音乐、美术、制作等工作,游戏的制作前后花费了将近三年的时间。PC版发行于2015年9月,并在TGA2015中获得了最佳独立游戏、最佳角色扮演游戏、最具冲击性游戏三项提名。

第一次知道独立制作这种概念还不是从游戏中,是从动画中知道的。忘了是高中快毕业还是刚上大学的时候,从verycd上知道了一部叫做《李献计历险记》的动画(那个时候verycd还没被阉割啊),知道了这部制作者耗时两年多独自一人制作的『奇怪』的动画短片。具有鲜明个人风格的台词、略显文艺但并不脱离大众的选曲和十分特立独行的分镜——至少我当时那么认为,让这部作品十分出挑——我现在也这么认为。

想想这应该是我第一次有意识的接触到独立作品。虽然之后一直在看动画片,但是接触到的惊艳的动画作品倒是不多了。但是这两年,独立游戏的概念好像突然火了起来,steam上一大票独立游戏,其中不乏佳作。作为一个普通玩家,也只是玩过相对比较出名的比较老的《去月球》《洞窟物语》等几个作品(当然还有《Gone Home》这个新坑爹货)。

独立做一款游戏应该是一个十分困难的事情,我现在几乎无法想象这件事情的难度。而一个人能长时间顶着如此大的压力把作品做完,想必对于这部作品是真爱吧。

最后对这些独立游戏制作者致敬,希望他们以后能带来更多有特色的作品。

MongoDB上手笔记4——在很多数据中进行查询

上一次把简单的Restful API搭建起来了,接下来让我们往数据库里面填充一些数据。

俗话说的好,一天一个苹果,考不上博士。假设小明每天吃一个苹果,为了记录截止到某一天小明一共吃了多少个苹果,我在db logs的collection applesIAte中,记录了截止到某一天小明一共吃了多少苹果。

假设小明命中注定不会读博,所以小明骨骼惊奇的生下来就不吃奶,而是出生第一天就吃苹果,每天以苹果为生(好像一个并吃不饱的样子,请不要在意这些细节)。然后每天为了建设祖国而奋斗,一直活到了100岁,最后在100岁生日的当晚因为看到了祖国的建设成果过于兴奋(以及吃了太多的苹果)而含笑九泉。

按照这以上假设,我写了个脚本,记录了无私而强韧的小明的不平凡的一生中吃了的苹果数量。这段脚本中用了个insert_many()的方法,在一次性写入很多条document的时候比较快。

from flask_pymongo import MongoClient
mongo = MongoClient(host="localhost", port=27999)
db = mongo.logs
auth_res = db.authenticate("readwriter", "123456")
apples = [{
    "day": day,
    "apple": day
} for day in range(1, 100*365+1)]
db.applesIAte.insert_many(apples)

然后我们看一看小明这光辉的一生。

> db.applesIAte.stats()
{
        "ns" : "logs.applesIAte",
        "count" : 36500,
        "size" : 1752064,
        "avgObjSize" : 48,
        "numExtents" : 5,
        "storageSize" : 2793472,
        "lastExtentSize" : 2097152,
        "paddingFactor" : 1,
        "paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0. It remains hard coded to 1.0 for compatibility only.",
        "userFlags" : 1,
        "capped" : false,
        "nindexes" : 1,
        "totalIndexSize" : 1193696,
        "indexSizes" : {
                "_id_" : 1193696
        },
        "ok" : 1
}

恩,小明活了36500天(原谅我忘了闰年这事),真是了不起!

在回顾小明这光辉的一生(中吃了多少苹果)之前,让我们先做一些小手脚。这样我们才能看看我们能以多快的速度回顾(查询)了小明的一生(丫可是活了100年口阿!)

> db.setProfilingLevel(2)
{ "was" : 0, "slowms" : 100, "ok" : 1 }

上面这句话的意思是,我们所有的回顾小明一生的行为(查询)的相关信息都会被记录下来。

接下来我们看看小明在10000天的时候一共吃了多少苹果。(此处也可以用findOne({day: 10000}),不过并不影响结论)

> db.applesIAte.find({day: 10000})
{ "_id" : ObjectId("56a9c39cabf8322aa0828dec"), "day" : 10000, "apple" : 10000 }

显然他吃了10000个,awesome!那让我们看看我们花了多长时间发现他吃了这么多苹果。

> db.system.profile.find()
{
    "op":"query",
    "ns":"logs.applesIAte",
    "query":{
        "day":10000
    },
    "ntoreturn":0,
    "ntoskip":0,
    "nscanned":0,
    "nscannedObjects":36500,
    "keyUpdates":0,
    "writeConflicts":0,
    "numYield":285,
    "locks":{
        "Global":{
            "acquireCount":{
                "r":NumberLong(572)
            }
        },
        "MMAPV1Journal":{
            "acquireCount":{
                "r":NumberLong(286)
            }
        },
        "Database":{
            "acquireCount":{
                "r":NumberLong(286)
            }
        },
        "Collection":{
            "acquireCount":{
                "R":NumberLong(286)
            }
        }
    },
    "nreturned":1,
    "responseLength":62,
    "millis":20,
    "execStats":{
        "stage":"COLLSCAN",
        "filter":{
            "day":{
                "$eq":10000
            }
        },
        "nReturned":1,
        "executionTimeMillisEstimate":10,
        "works":36502,
        "advanced":1,
        "needTime":36500,
        "needFetch":0,
        "saveState":285,
        "restoreState":285,
        "isEOF":1,
        "invalidates":0,
        "direction":"forward",
        "docsExamined":36500
    },
    "ts":    ISODate("2016-01-28T07:53:57.857    Z"),
    "client":"127.0.0.1",
    "allUsers":[
        {
            "user":"boss",
            "db":"admin"
        },
        {
            "user":"readwriter",
            "db":"logs"
        }
    ],
    "user":"readwriter@logs"
}

上面这一大段基本上都没啥用,我们只需要两条——"millis":20,"docsExamined":36500。也就是查询一共花了20ms,查看了36500条document才找到了这条记录。(好慢!)

如果小明不是一个普通的人,是一个脱离了低级趣味的神仙,他活了一万年,甚至十万年前他就和猛犸象谈笑风生了(可他依旧不想读博)。那么我们的记录会越来越多,查询速度会越来越慢。

而我们为days加上索引可以有效的解决这个问题。因为day是单调增长的,所以参数是{day: 1}。

> db.applesIAte.createIndex({day: 1})
{
        "createdCollectionAutomatically" : false,
        "numIndexesBefore" : 1,
        "numIndexesAfter" : 2,
        "ok" : 1
}
> db.applesIAte.stats()
{
        "ns" : "logs.applesIAte",
        "count" : 36500,
        "size" : 1752064,
        "avgObjSize" : 48,
        "numExtents" : 5,
        "storageSize" : 2793472,
        "lastExtentSize" : 2097152,
        "paddingFactor" : 1,
        "paddingFactorNote" : "paddingFactor is unused and unmaintained in 3.0. It remains hard coded to 1.0 for compatibility only.",
        "userFlags" : 1,
        "capped" : false,
        "nindexes" : 2,
        "totalIndexSize" : 2117584,
        "indexSizes" : {
                "_id_" : 1193696,
                "day_1" : 923888
        },
        "ok" : 1
}

从stats()返回的结果来看,day的索引确实是加上了。那么结果如何呢?

{
    "op":"query",
    "ns":"logs.applesIAte",
    "query":{
        "day":20000
    },
    "ntoreturn":0,
    "ntoskip":0,
    "nscanned":1,
    "nscannedObjects":1,
    "keyUpdates":0,
    "writeConflicts":0,
    "numYield":0,
    "locks":{
        "Global":{
            "acquireCount":{
                "r":NumberLong(2)
            }
        },
        "MMAPV1Journal":{
            "acquireCount":{
                "r":NumberLong(1)
            }
        },
        "Database":{
            "acquireCount":{
                "r":NumberLong(1)
            }
        },
        "Collection":{
            "acquireCount":{
                "R":NumberLong(1)
            }
        }
    },
    "nreturned":1,
    "responseLength":62,
    "millis":0,
    "execStats":{
        "stage":"FETCH",
        "nReturned":1,
        "executionTimeMillisEstimate":0,
        "works":2,
        "advanced":1,
        "needTime":0,
        "needFetch":0,
        "saveState":0,
        "restoreState":0,
        "isEOF":1,
        "invalidates":0,
        "docsExamined":1,
        "alreadyHasObj":0,
        "inputStage":{
            "stage":"IXSCAN",
            "nReturned":1,
            "executionTimeMillisEstimate":0,
            "works":2,
            "advanced":1,
            "needTime":0,
            "needFetch":0,
            "saveState":0,
            "restoreState":0,
            "isEOF":1,
            "invalidates":0,
            "keyPattern":{
                "day":1
            },
            "indexName":"day_1",
            "isMultiKey":false,
            "direction":"forward",
            "indexBounds":{
                "day":[
                    "[20000.0, 20000.0]"
                ]
            },
            "keysExamined":1,
            "dupsTested":0,
            "dupsDropped":0,
            "seenInvalidated":0,
            "matchTested":0
        }
    },
    "ts":    ISODate("2016-01-28T08:05:46.904    Z"),
    "client":"127.0.0.1",
    "allUsers":[
        {
            "user":"boss",
            "db":"admin"
        },
        {
            "user":"readwriter",
            "db":"logs"
        }
    ],
    "user":"readwriter@logs"
}

结果发现"docsExamined":1,"millis":0。我们只查询了一条数据就直接找到了到20000天的时候小明吃了多少苹果,花的时间更是小到近似于0了!所以说当小明成仙之后,我们必须对day加上索引才能知道他为了不读博吃了多少苹果XD。

后记:最近看了一本书,作者比鸟哥还谐,整本书写的很像《春物》这种轻小说的风格。于是尝试着『学习』了一下这种风格,试过之后感觉有些浮夸啊XD

MongoDB上手笔记3——写个简单的Restful API读写数据库

上一篇中设置好了db logs的两个用户,并尝试往collection gen1中写入了一条简单的document。使用上一篇中的同样的方法,又写入了新的两条document,现在gen1中有了三条document。

> db
logs
> db.gen1.find()
{ "_id" : ObjectId("56a0daabcf03a917cb662aef"), "time" : 1, "value" : 1 }
{ "_id" : ObjectId("56a0dab3cf03a917cb662af0"), "time" : 2, "value" : 2 }
{ "_id" : ObjectId("56a0dabacf03a917cb662af1"), "time" : 3, "value" : 3 }

在命令行中写js语句来操作mongoDB自然是可行的,可是有很多情况需要使用别的语言来读取数据库。js可能缺少某些别人造好的轮子,比如我想用python来做科学计算,数据存在mongoDB里面,我就需要用python的驱动来操作数据库。

提供这个功能的轮子已经有很多了,mongoEngine和pymongo之类的。但是代码变多之后,感觉维护变得比较麻烦。代码中往往操作数据库和进行计算的部分混合在一起,看着条理不是很清晰。

所以想是不是可以将两部分分开来做,中间用个什么东西连接起来。后来知道了很Fancy(大概?)的Restful API,于是想着可以把各个部分分开,中间用http连接起来。

于是用flask和做了个十分简单的Restful API,功能就只有读取db logs的collection gen1中的最新一条数据以及向其中写数据。

api的验证部分使用了最简单的basic http authentication。request header中的auth部分就只是使用了base64加密而已(都不能算得上加密吧),十分简陋,完全没有安全性。每次请求都会创建一个MongoClient,简单测试了一下,目前没看出来有什么性能影响。

# 创建MongoClient
mongo = MongoClient(host="localhost", port=27999)
# 使用创建的对象验证身份
mongo["logs"].authenticate(request.authorization.username, request.authorization.password)

以下就是所有的代码了,一共只有50行。

simple_restful.py

from flask import request
from flask_restful import Resource
from bson.json_util import loads, dumps
from flask_pymongo import MongoClient
from pymongo.errors import OperationFailure
from flask import Flask
from flask_restful import Api


class Data(Resource):
    def get(self):
        mongo = MongoClient(host="localhost", port=27999)
        try:
            mongo["logs"].authenticate(request.authorization.username, request.authorization.password)
            last_log = mongo["logs"]["gen1"].find().sort("_id", -1)[0]
            return {
                       "err": "False",
                       "message": "Successfully get data",
                       "result": {"data": dumps(last_log)}
            }
        except OperationFailure, err:
            return {
                "err": "True",
                "message": "Failed getting data",
                "result": err.details
            }

    def post(self):
        mongo = MongoClient(host="localhost", port=27999)
        try:
            mongo["logs"].authenticate(request.authorization.username, request.authorization.password)
            data = request.data
            l = loads(data)
            result = mongo["logs"]["gen1"].insert_one(l["data"])
            return {
                "err": "False",
                "message": "Successfully post data",
                "result": {"data": dumps(l["data"]), "ack": str(result.acknowledged)}
            }
        except OperationFailure, err:
            return {
                "err": "True",
                "message": "Failed post data",
                "result": err.details
            }

app = Flask(__name__)
api = Api(app)
api.add_resource(Data, '/data')
app.run(debug=True)

使用curl简单测试一下吧。

>curl -v -H "Content-Type: application/json" --user readwriter:123456 http://localhost:5000/data
* Adding handle: conn: 0x140cf10
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x140cf10) send_pipe: 1, recv_pipe: 0
* About to connect() to localhost port 5000 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
* Server auth using Basic with user 'readwriter'
> GET /data HTTP/1.1
> Authorization: Basic cmVhZHdyaXRlcjoxMjM0NTY=
> User-Agent: curl/7.33.0
> Host: localhost:5000
> Accept: */*
> Content-Type: application/json
>
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Type: application/json
< Content-Length: 189
< Server: Werkzeug/0.11.3 Python/2.7.10
< Date: Thu, 21 Jan 2016 13:59:24 GMT
<
{
    "err": "False",
    "message": "Successfully get data",
    "result": {
        "data": "{\"_id\": {\"$oid\": \"56a0dabacf03a917cb662af1\"}, \"value\":
3.0, \"time\": 3.0}"
    }
}
* Closing connection 0

确实读出来了最新的一条document。再写入一条document试试。

C:\Users\Juiceyang\Desktop>curl -v -H "Content-Type: application/json" --user re
adwriter:123456 http://localhost:5000/data -X POST -d "{\"data\":{\"time\":4,\"v
alue\":4}}"
* Adding handle: conn: 0x85cf10
* Adding handle: send: 0
* Adding handle: recv: 0
* Curl_addHandleToPipeline: length: 1
* - Conn 0 (0x85cf10) send_pipe: 1, recv_pipe: 0
* About to connect() to localhost port 5000 (#0)
*   Trying 127.0.0.1...
* Connected to localhost (127.0.0.1) port 5000 (#0)
* Server auth using Basic with user 'readwriter'
> POST /data HTTP/1.1
> Authorization: Basic cmVhZHdyaXRlcjoxMjM0NTY=
> User-Agent: curl/7.33.0
> Host: localhost:5000
> Accept: */*
> Content-Type: application/json
> Content-Length: 29
>
* upload completely sent off: 29 out of 29 bytes
* HTTP 1.0, assume close after body
< HTTP/1.0 200 OK
< Content-Type: application/json
< Content-Length: 210
< Server: Werkzeug/0.11.3 Python/2.7.10
< Date: Thu, 21 Jan 2016 14:02:58 GMT
<
{
    "err": "False",
    "message": "Successfully post data",
    "result": {
        "ack": "True",
        "data": "{\"_id\": {\"$oid\": \"56a0e512612bf10dcce366d7\"}, \"value\":
4, \"time\": 4}"
    }
}
* Closing connection 0

结果来看也确实写入成功了。

《Armored Warfare》——一个说的过去的继承者

玩《Armored Warfare》(以下简称AW)也有一段时间了,从AW在内测的时候就在一直在官方主页和youtube上关注这个游戏,由于之前一直有不错的游戏玩,也没有花钱买内测资格(觉得这种设定挺傻的,不知道是不是国外的运营商跟着天朝的这一票学的,不学好净学坏)。之后记得10月左右的时候AW开始了公测,第一时间开始玩,中间又穿插着巫师3和辐射4,虽然是断断续续的玩,也没有像《War Thunder》那样一下子跳好几个版本似的断断续续。多少也算有些感慨吧。

AW是黑曜石工作室的作品。黑曜石开发了一系列rpg作品,传统的rpg作品有前些年的《无冬之夜2》系列,去年的《永恒之柱》,还有比较『奇葩』的rpg游戏《辐射:新维加斯》、《南方公园:真理之杖》等(《地牢围攻3》有些让人失望,归在等里好了)。之前虽然也做过《skyforge》这样的作品,可终归不是很流行。

后来我听说黑曜石竟然去做WoT-like了,心里一惊,赶紧上youtube看了个预告片,发现质量还不错,于是一直关注了下来。

是的,谈到这种慢节奏装甲战题材的射击游戏,坦克世界(《World of Tanks》,WoT)是个绕不开的坑。其虽然不是第一个吃螃蟹的人,也算得上是第一个把螃蟹做的能让大家吃得人。他就像别人家的孩子一样,所有的后来者都要和他比较一番,《War Thunder》如此、AW如此、甚至《红色管弦乐队2》都逃不过一劫。

上学的时候每周开班会,经常会有同学被要求介绍学习经验。我感觉游戏也是这样。有些经验比较容易照搬,比如MOBA游戏里的『锤子系』等技能(当然这种技能也不是只有MOBA里有,此处特指在MOBA中的应用),客观来讲,LOL从DotA里学了不少;当然有的经验照搬也是行不通的,CF里不卖火麒麟,卖点卡的话是肯定过不下去的,而CSGO里卖火麒麟的话估计玩家又要骂G胖了。

从这个角度来讲,我觉得黑曜石学的不错。AW无论从地图大小、地图类型、游戏模式都几乎是『照抄』WoT。

虽然是现代战争的背景,并没有让玩家像在红色管弦乐队2一样呆在车里看雷达射击。也没有走War Thunder那个无血条无点亮机制的老路。虽然WoT的这一套机制已经广受诟病许多年,不点亮就看不见,打观察塔掉血之类的槽点已经不再新鲜。然而事实就是事实,玩家再吐槽,依旧会乐此不疲的玩。

从这点来说,我认为黑曜石的选择是正确的,游戏就是游戏,不是艺术品,也不是战争模拟游戏。

网络游戏想赚钱,有一定的用户基数是必需的,其与《ARMA》系列、《闪点行动》系列的那一套『硬核』的游戏制作思路是完全矛盾的(即使是这样,《ARMA》和《闪点行动》最新作也都变得相对快餐化了)。在一个网络游戏当中,让玩家跑路半个小时,然后千辛万苦到了任务地点就立扑,这是无法想象的事情(也许60年代的魔兽世界是个例外?)。

AW因为慢节奏在线FPS的特质,注定就是一个快餐游戏。

玩家不可能花很多的时间学习复杂的游戏系统。反面例子就是《精英:危险》这种内容不多,系统还特别复杂的游戏,即使玩过了教学,刚上手的时候依旧不知道该怎么赚钱;而CF不得不说在这方面是个正面例子,首先有经典又流行的CS在前面做铺垫,其次系统简单到基本上不需要做铺垫,拿起枪就是干。AW类似于CF,前面有个谈不上经典但还算流行的WoT做铺垫,之后还有个说的过去的教学关。玩家是可以快速进入新手房玩个痛快的。

而游戏想有趣方法则很多了,内容充实是一方面,这种快餐网游想内容充实是比较困难了;而系统有趣则是另一方面。

从系统上说,AW是师承WoT,在WoT的基础上有些很小的改变。轮式车辆的引入、反坦克导弹的引入等特性的引入算是一些吧。

这导致AW也继承了WoT的许多问题。由于AW并没有中国公司代理,只好玩的北美服。美国玩家是个非常有趣的群体,他们游戏的心态好像和大陆玩家不太一样,不是特别认真。在这种情况下,WoT的一系列问题就被放大了,地图设计问题——玩家蹲的飞起(campers),出生点设置问题——一波流转一波蹲。

归根结底还是AW也好,WoT也好,匹配系统都太糟糕。不同水平的玩家被放到了一起,自然水平比较高的玩家嫌新手拖后腿,新手嫌压力大。这个道理已经在70年代之后的wow被验证了一次,相信接下来依旧会被一次又一次的验证。

当然,由于用了CryEngine,AW的画质还是十分感人的,场景的设置也很不错,天上飞机导弹不断,海里航母驱逐眼前。当然帧数也十分感人,i54590+非公970上次最高画质经常只有40帧左右,优化实在着急。

总的来说,如果你没玩过WoT,那么AW可以一玩;如果你WoT玩了有3000盘以上,就不要尝试这个游戏了,不会给你惊喜的。

MongoDB上手笔记2——新建用户并设置权限

上一篇说到了把MongoDB安装完毕,并设置了开机启动服务。接下来记录下怎么创建用户,并通过MongoDB的权限管理功能进行管理。

mongodb的用户权限管理的方式是分组的,不同的用户组有着不同的权限,对应的就是可以执行不同的命令。

形象地说就是警察用户组的只有抓捕的权力,医生用户组的只有开处方的权力等等。MongoDB的用户组的设置也秉承了这样的方法,在保证能正常使用的前提下,只给特定用户最少的权力。

首先,先启动mongod,此处使用配置文件的启动方法。

mongod --config E:/MongoDB/mongod.conf

然后连接mongodb。

mongo 127.0.0.1:27999
show dbs
use admin

这个时候可以看到成功的连接了进去,只有一个local的数据库。然后切换到admin数据库,准备创建用户。

MongoDB内置了许多类型的用户类型,大致分为了几类:

  • 单个数据库用户(Database User Roles)
  • 单个数据库管理员(Database Administration Roles)
  • 集群管理员(Cluster Administration Roles)
  • 备份恢复管理员(Backup and Restoration Roles)
  • 全部数据库的管理员(All-Database Roles)
  • 超级用户(Superuser Roles)

不同的用户类别下都有若干个用户类型,分别对应的这不同的权力。比如现在有一个数据库里记录着不同时间段的测量数据,我们想把特定时间段的测量数据读出来,然后在脚本里输出成csv文件,我们就需要一个read用户就够了,而不需要一个root用户(对应的就是上面说的『最少权力』的原则)。

回到正题,简单的举个例子,我们需要一个read用户用来读测量数据用来显示输出,一个readWrite用户用来写数据进入数据库,还有一个权力最大的用户来管理用户和数据库。然我就像建立一个公司一样,我们先任命一个boss,然后再由boss找员工(当然如果这个公司只有一个人的话,boss又要当老板,又要当苦力,那么在roles里面填多个role就可以了)。

db.createUser({user: "boss",pwd: "123456",roles:[{ role: "userAdminAnyDatabase", db: "admin" }]})

我们创建了一个叫boss的用户,他的角色是userAdminAnyDatabase,。从名字上也能看出来,userAdminAnyDatabase类型的用户可以管理所有数据库下的用户。(如果想在创建这个用户之前就开启权限管理,则需要在配置文件中开启localhost exception)

完成了用户管理员的创建之后就可以重新启动启用了权限管理的MongoDB了(此处在管理员权限的cmd中安装了mongoDB的服务)。

mongod --config E:/MongoDB/mongod.conf --auth --install

如果这个时候直接登陆的话,会发现有的命令是无法执行的,因为没有这个权限。比如最简单的显示所有的数据库都无法执行。

C:\Users\Juiceyang>mongo 127.0.0.1:27999/admin
MongoDB shell version: 3.0.6
connecting to: 127.0.0.1:27999/admin
> db
admin
> show dbs
2015-12-13T20:29:44.926+0800 E QUERY    Error: listDatabases failed:{
        "ok" : 0,
        "errmsg" : "not authorized on admin to execute command { listDatabases:
1.0 }",
        "code" : 13
}
    at Error (<anonymous>)
    at Mongo.getDBs (src/mongo/shell/mongo.js:47:15)
    at shellHelper.show (src/mongo/shell/utils.js:630:33)
    at shellHelper (src/mongo/shell/utils.js:524:36)
    at (shellhelp2):1:1 at src/mongo/shell/mongo.js:47
> ^C
bye

而登录了之后,就获得了部分权限,比如可以看都有哪些数据库了。

C:\Users\Juiceyang>mongo 127.0.0.1:27999/admin -u boss -p 123456
MongoDB shell version: 3.0.6
connecting to: 127.0.0.1:27999/admin
> show dbs
admin  0.078GB
local  0.078GB

之后就可以开始创建其他的用户了,创建的方法和创建boss的方法一样。假设我们需要对logs数据库进行读写。

use logs
db.createUser({user: "reader",pwd: "123456",roles:[{ role: "read", db: "logs" }]})
db.createUser({user: "readwriter",pwd: "123456",roles:[{ role: "readWrite", db: "logs" }]})

之后尝试用reader用户往gen1集合写一次数据。

> db.gen1.insert({"time":1, "value":1});
WriteResult({
        "writeError" : {
                "code" : 13,
                "errmsg" : "not authorized on logs to execute command { insert:
\"gen1\", documents: [ { _id: ObjectId('566d691840d302a5c4e9cfc6'), time: 1.0, v
alue: 1.0 } ], ordered: true }"
        }
})

发现无法写入,原因是没有权限执行insert。

之后换readwriter尝试一次。

> db.auth("readwriter","123456")
1
> db.gen1.insert({"time":1, "value":1});
WriteResult({ "nInserted" : 1 })

发现成功写入了。

再换回reader,读一次数据。

> db.auth("reader","123456")
1
> db.gen1.find()
{ "_id" : ObjectId("566d696a40d302a5c4e9cfc7"), "time" : 1, "value" : 1 }

发现能够成功读数据。

《巫师2》——progress with setback

搬家之前通关了《巫师1》,惊喜于游戏制作组精湛的故事叙述技巧,也对于游戏的一些缺陷颇有些惋惜,最后记录了一下,变成了上一篇评论。《巫师1》通关了之后,紧接着就通关了《巫师2》。

系列中的连续两部作品进行对比的话,不得不感叹一句游戏制作组十分用心,初上手时《巫师2》相较于《巫师1》颇有些脱胎换骨的感觉。不过实际通关之后,心里却有些与通关《巫师1》之后类似的感觉。

《巫师2》和前作不同,多了一个『国王刺客』的副标题。而故事也是接着《巫师1》末尾处白狼挫败了杀害Foltest的刺客。作为一个卖点在讲长篇故事上的RPG游戏,续作和前作的连续性的把握并不是很好掌控。

// 内有剧透

某些程度上来说,《巫师2》很好的兼顾了老玩家和新玩家。接着前作末尾的刺杀事件,自然可以联系上《巫师2》的开头。若是没有玩过前作,序章里的说明也能让玩家尽快了解故事进展,进入剧情。(其根本原因还是《巫师1》中的火蜥蜴内乱和《巫师2》的女巫集会所篡权事件没有什么直接关系。除了人物一定程度上有所延续,基本上就是两个故事。)

序章之后,到达浮港。玩家迅速就进入了两个分支——『蓝白条』和『松鼠党』两条线。本作中的故事分支做得比前作还要彻底。从进入支线之后,就是完全不同的任务,完全不同的剧情。在这点上《巫师2》作为一个不同于《老滚5》的传统的讲故事的游戏,任务链的深度上做得比《老滚5》的平面式任务链做得好了太多。不同的选项对影响巨大,直接影响剧情进展。

之后在弗根,知道了巨龙人外娘的真身之后,剧情又和序章联系上,让一向鸡肋的序章不再鸡肋,故事圆上之后完整性也好了很多。

然而我认为本作的最大的问题也就是在《巫师》系列最引以为傲的讲故事上。

《巫师1》的剧情并不是很紧凑。比如在Vizima城中的『找坏蛋』这个环节,玩家需要在Vizima城中的不同几个区之间来回跑路;之后到达沼泽之后也是在沼泽和城区之间来回跑,大量的时间都浪费在了路上,剧情十分拖沓。

可以看出来制作组是想解决这个问题的,然而用力过大,矫枉过正。首先每一章的地图都比原先的地图小。再也没有Vizima这样的巨无霸地图。玩家并不需要在跑路上耗费很多精力,注意力也能长时间保持在任务上。这一定程度上缓解了上一篇《巫师1》的评论中提到的开放式地图中常见的跑路问题。

之后任务链不再像前作中那么冗长,不再需要不停找人。最明显的例子就是之前说的《巫师1》中『找坏蛋』这样的任务不见了,玩家也不需要在同一张地图中停留过长时间。(说起来《巫师1》里最后找了半天坏蛋,最后发现坏蛋就在眼前的时候,我的心情真是...)

这带来的直接好处就是剧情简单干练,玩家玩起来能时刻保持注意力,紧紧跟随剧情,十分畅快。

然而也带来了另一个问题,就是制作经费就那么多,能做出来的内容也就那么多,不拖时间,篇幅保证不了呀。

所以故事在玩家走出弗根之后急转直下,剧情进展快的像是坐了火箭。以弗根为分界点,前半部分故事和后半部分故事的节奏完全不同。我一开始想着女巫大清洗之后还有其他剧情,结果就蹦出来了制作人员表。怅然若失之后,突然反应过来,《巫师2》这不是加上序章和尾声也才5章么?要知道《巫师1》光是中间的正篇就有整整5章,还每章都比巫师2的长...

主线剧情都保证不了,支线剧情就自然不必说了。根本就不会有前作中支线干扰主线的问题。

剧情的过于短小精悍给本作减分不少。

说了半天剧情。看看相对于《巫师1》的进步。

最大的进步当然还是画面的进步,那光影效果变化太大了(画面是游戏性的一部分,不服就是不客观)。这个自不必说,隔了好几年,画面不进步说不过去(不过人物都画风变了什么鬼)

the-witcher-2-screenshot

这画面变化太大了,随便从网上找张图可见一斑

战斗系统大改进。玩家再也不用看着鼠标打拍子了。相信每个在浮港杀触手怪的玩家都对此深有感触。本作的打击感不逊于《真三》等传统的动作游戏,拳拳到肉,刀刀见血,很是畅快。即使是简单难度下,也不像是在《巫师1》中那样,随便一阵乱点就能把怪打死了。打怪需要一些简单的操作地滚,和一些简单的战斗规划嗑药。然而战斗中不可嗑药的问题还是没解决,没事就要跪地上喝瓶药的感觉很不爽。

收集系统还是一般,没有很丰富的装备系统。玩家还是偶尔没事升级一下即可。和一代基本持平。

另外由于本作是第二作,所以出现了初作中不会出现的问题——存档继承问题。哪怕你在《巫师1》中再厉害,《巫师2》中刚上来也个菜鸟狩魔猎人。《巫师》并不是暗黑天梯,每次都需要重新练级还是有些令人不爽。

总的来说《巫师2》还是改进了不少《巫师1》的缺点,然而却冒出了许多新的问题。然而捡起的只是无足轻重的芝麻,本身最大的闪光点丢掉了让人颇为惋惜。下一篇《巫师3》的评价里再见吧。

MongoDB上手笔记1——在windows desktop上安装并设置MongoDB服务

最近一个活儿要从设备那里每秒取一些数据回来,出于查询简单的想法,想把数据存在数据库里。之前不知道是因为错觉还是什么,一直觉得node.js非常的酉告(cooooool好像只能这样翻译了?),所以花了几天的时间学了学mongodb。大致整理成了如下这个类似于quick start之类的东西。

之所以注明是在windows desktop上安装,是因为这个活儿里,数据从设备到远程的PC中间数据通过GPRS传到第三方的服务器上,再通过第三方提供的软件从服务器上将数据取回,通过一个虚拟串口将数据读取出来。而第三方提供的软件只能运行在windows上,又只有盗版windows desktop,反正也只是做做样子只能凑活用下了。

1.简单启动MongoDB

从mongodb网站上下载完安装文件之后自行安装完毕。在此不表。

进入mongodb的安装路径中的放exe的文件夹下,可以看到里面有两个exe文件,分别叫mongo和mongod。

mongod就是启动数据库所需要的可执行文件。如果没有理解错误的话,mongod的名字应该是指mongo daemon process的意思,他会运行在后台,默默的完成数据库中的操作。

mongodb在安装的时候并不会把bin文件夹放到环境变量下,所以首先要将bin文件夹对应的路径放到系统环境变量PATH里。这样在cmd中输入mongo和mongod时,它才会知道这两个东西在哪里。

运行数据库,自然需要对数据库进行一系列的配置。在cmd中运行mongod的时候可以完成一些简单的配置,比如数据库存储数据的路径、日志文件的路径、端口的设置等等。例如如下的配置方式会在D:/dbs/data下存储数据文件,将日志文件存储在D:/dbs/log.txt中。

mongod --dbpath d:/dbs/data --logpath d:/dbs/log.txt

2.配置文件启动MongoDB

不过因为在cmd中输入一大长串字符串十分的不方便;而且会在桌面上留下一个cmd大黑框,很不美观。所以还是推荐使用配置文件进行数据库的配置。(当然如果不觉得cmd大黑框无所谓的话,桌面上放个bat文件也可以)

先建立一个mongodb的配置文件,假设放在了D:/dbs/mongo.conf这个路径下。随便用个什么编辑器打开mongo.conf。数据库配置文件使用的是YAML格式,简单使用的话,YAML和JSON没什么不一样的。不过需要注意的一个地方就是YAML不支持tab缩进,所以需要使用空格。以下是我的配置文件,简单配置的话大概有这么几个参数。

systemLog:
    destination: file
    path: "e:/MongoDB/log.txt"
    logAppend: true
storage:
    dbPath: "e:/MongoDB/data"
    journal:
        enabled: true
net:
    bindIp: 127.0.0.1
    port: 27999

其中,各个选项的意思如下。

systemLog.destination的值可以为file或是syslog,因为之后我们会将mongod设置为一个系统服(也就是不会出现cmd大黑框,并可以设置为开机启动)。所以设置为syslog没有意义,此处选择file。

当systemLog.destination设置为file的时候,systemLog.path也必须被确定(也就是上面对应的--logpath的参数)此参数设置了日志文件的路径。

systemLog.logAppend设置为true之后,日志文件就会每次都在现有的log.txt之后续写。默认的是false,则每次mongod重启之后都会同文件夹下备份上次的日志文件,同时新建一个日志文件。

storage.dbPath对应的就是之前--dbpath之后的参数,设置数据文件的存储目录。

storage.journal.enabled对应是否启用mongodb的日志机制。简单的可以理解为启用之后,mongodb崩溃之后可以快速恢复,即可以满足ACID中的一致性。

net.bindIp这个参数是设置绑定ip用的,默认使用本地地址。

net.port这个参数是设置绑定的端口的,默认使用是27017,此处改了改,改成了27999。

假设配置文件mongo.conf放在了d:/dbs/mongo.conf路径下。在这之后,就可以win+r打开cmd,然后输入如下命令启动mongodb。

mongod --config d:/dbs/mongo.conf

3.安装MongoDB服务

按照上面的做法,还是会出现一个cmd大黑框,而且每次都要输入一遍这个命令,还是很麻烦。

这个问题可以通过设置为MongoDB服务来解决(推荐在设置完用户权限之后在安装服务)。

首先以管理员权限打开cmd,然后输入如下命令。

mongod --config d:/dbs/mongo.conf --install

之后就可以在计算机管理->服务和应用程序->服务中看到MongoDB服务了。也可以修改是否要开机自动启动。这样就不会出现cmd的大黑框了。

如果需要修改配置文件,需要先删除现有的MongoDB服务,命令如下。

mongod --remove

之后修改配置文件,并重新安装MongoDB服务即可。