用MongoDB建立炉石传说卡牌数据库

The_Grand_Tournament_Banner
Standard

MongoDB 简介

MongoDB 是一个面向文档的数据库管理系统。与传统的SQL Server、MySQL等关系型数据库(Relational Database Management System)有所不同,它是非关系型的数据库,即 NoSQL。

NoSQL 的设计思维与传统的数据库设计还是有一点差别的。在传统的数据库设计思维中,在设计阶段就需要对表的字段名称、字段类型进行规定(DDL),如果插入的数据不符合要求,则数据库不会接受这条数据以保持完整性。

INSERT INTO TABLE members VALUES('小明', 'Hello MongoDB!')
INERT INTO TABLE members VALUES('John', 20, 'john@example.com')

-- 在关系型数据库中,上述两条插入语句不可能同时成功

而在 MongoDB 中,以文档表示每条记录,而集合则是一组文档。如果说 MongoDB 中的文档类似于关系型数据库中的行,那么集合就如同于表。

{"name": "小明", "greeting": "Hello MongoDB!"}
{"name": "John", "age": 20, "email": "john@example.com"}

集合是无模式的,意味着一个集合里面的文档可以是各式各样的。MongoDB 中的文档格式类似于JSON,每个文档包含着一些键值对。注意到,不仅是值的类型不同(字符串和整数),它们的键也是完全不一样的。在集合里面可以放置任何文档会带来很多好处,例如某些种类很多的字段就不必专门建表去存储了,减少了设计表时的成本。但随之而来的问题是:“还有必要使用多个集合吗?”问得好!为何要用多个集合呢?下面是一些理由:

  • 如果各种各样的文档混在一个集合里,无论是开发成本还是管理成本都会显著增加。设想你要查询博客文章,但还得去掉作者本身数据的文档,想想就很麻烦。

  • 把同类型的文档放在一个集合里,数据会更加集中,从一个写有“博客文章”的文件夹中找对应的文章,肯定比从一堆资料中找要快得多。

  • 索引可以同类文档的集合提供光环加成,尤其是有唯一索引的时候。

如你所见,确实有很多理由创建一个模式把相关类型的文档规整到一起。当然了,MongoDB 对此并不作强制要求,让开发者更有灵活性。

MongoDB 安装

  • 下载: MongoDB 提供了可用于32位和64位系统的预编译二进制包,可以从MongoDB官网下载安装。

  • 安装: 由于 MongoDB 是面向文档的,它会使用根目录的 X:\data\db 文件夹来存储数据库的文档。假设 Mongo 安装在D盘,我们则需要在D盘根目录下建立 data 目录,在里面再创建 db 目录

  • 启动服务: 进入 bin 目录,运行 mongod.exe 即可启动服务器。(cmd亦可)

  • 启动Shell: Mongo提供了一个Shell来进行对数据库的交互操作,运行 mongo.exe 即可。实际上,这个shell还封装了Javascript,js的相关语法都可以使用。

一个简单的示例:

$ mongo
MongoDB shell version: 3.0.5
connecting to: test
> 1+2
3
> db
test
> db.xiaoyc.insert({x:1, y:2})
WriteResult({ "nInserted" : 1 })
> db.xiaoyc.find()
{ "_id" : ObjectId("55c320c38da2ca5d4fe1381a"), "x" : 1, "y" : 2 }

回到正题,MongoDB 的简单介绍就到这里。更多相关的资料可以参加文末的参考链接。

关于炉石传说

炉石传说是暴雪推出的一款卡牌对战类游戏。没什么好讲的,可以自行百度。╮(╯_╰)╭

数据源获取

这或许是一个很大的问题。一般而言,对于游戏中的数据获取,很容易想到的便是去gamepedia.com找,上面的资料可谓是相当的全(比如参见这里)。啧啧,但是把资料变成结构化数据却是相当地麻烦。然后,你以为我要写个爬虫?No 没这个必要。因为一个已经出了一年多的游戏,怎么会连个资料库都没有呢,用现成的就好了哇。

于是,去某搜索引擎找“hearthstone cards database”,的确有很多发现。(不要问我是哪个搜索引擎,因为我都试了一遍 = =)然后发现不同的搜索引擎竟然给出的结果各不相同(你们串通好的么?),然而查到的结果都是一个个卡牌的检索网站啊,但我需要的是它背后的数据啊。想在页面上找个“下载”按钮无异于痴人做梦,难道又只能沦落到写爬虫了吗?

不!不能放弃!于是我继续找啊找,找到了好多spreadsheet,在忍着蜗牛的速度终于打开,看到的却是各种排版错乱的pdf后,真希望面前出现张桌子让我摔啊!(→桌子← 不知道你们看到是啥,反正我看到的是乱的)

然而,功夫不负有心人,最后还是找到了一个比较可靠的来源。这个网站提供了所有卡牌数据的JSON格式,而且还支持多种语言(包括中文),简直大赞啊!据作者说,这是从官方的游戏文件中直接导出的,所以准确度应该是有保证的。而且JSON的格式也很契合 MongoDB 的文档格式啊。

数据的格式

预处理当然是导入数据前很重要的一环,在此之前当然是要搞清楚数据的结构及内容了,比如以“AllSets.zhCN.json”为例,打开之后里面的结构大抵是这样的:

{
  "Basic": [
    {
      "id": "HERO_04",
      "name": "乌瑟尔·光明使者",
      "type": "Hero",
      "faction": "Neutral",
      "rarity": "Free",
      "health": 30,
      "collectible": true,
      "playerClass": "Paladin"
   },
   ...
   {
      "id": "CS2_182",
      "name": "冰风雪人",
      "type": "Minion",
      "faction": "Neutral",
      "rarity": "Common",
      "cost": 4,
      "attack": 4,
      "health": 5,
      "flavor": "他梦想着有一天能够下山开一间拉面店。但他没有那个勇气。",
      "artist": "Mauro Cascioli",
      "collectible": true,
      "howToGetGold": "战士达到55级后解锁。"
    },
    ...
  ],
  "Curse of Naxxramas": [
    {
      (省略内容)
    },
    ...(省略若干)
  ],
  "Goblins vs Gnomes": [
    ...(省略若干)
  ],
  ...(省略若干)
}

上面的示例有点长,但从中我们可以大体了解这个JSON的结构:它本身是个JSONObject,里面的键是一些基础的分类,其值则是一个JSONArray,里面包含了对每个对象的描述,包括随从牌、法术牌、甚至效果(仔细看的话,并非每个对象都是卡牌),每个对象又有一些属性,例如"id", "name", "health"等,但并非每个属性都存在(例如英雄就没有"cost"这个属性)。

面对这种奇怪的数据结构,应该怎么存储它呢?当然是 MongoDB 了!(如果你忘了 MongoDB 的某个重要特性,不妨回到开头再去看一下简介,可能会有更深的理解)

好,现在你也知道为什么 MongoDB 可以存储这样类型的数据了,接下来就是寻找将数据导入的方法。

我们通过文档找到有个命令叫做 mongoimport,它可以实现导入功能,来试一下。

$ mongoimport -d hearthstone -c cards pathtofile\\AllSets.zhCN.json
2015-08-07T16:00:22.693+0800    connected to: localhost
2015-08-07T16:00:22.986+0800    imported 1 document

看起来是成功了,但是怎么只导入了一个文档呢?不应该是有上百个吗?

如果你不仔细先观察文档的结果的话,可能会忽略这里的问题所在。实际上,数据是被导入了,但却是作为某个文档的属性被导入的,由于 MongoDB 的特性,这种方法竟然也是可以的!所以灵活性大也是有弊端的啊。我怀疑如果以后对它说“烧菜”,它可能真的会把菜放火里烧了。 Orz...

数据预处理

我们的想法是把数据前的“Basic”、“Curse of Naxxramas” 等去掉,只留下卡牌内容形成一个JSONArray的形式。这个应该用JavaScript本身是很简单的,可是我印象中也没想起来js怎么读写文件,于是很纠结。还好 python 可以比较方便的解决这个问题:

#!/usr /bin/env python
# -*- coding: utf-8 -*-

def flatten(filename, outfilename):
    import json, io
    with open(filename, 'r') as f:
        data = json.load(f, encoding="utf8")
        combine = []
        for key in data:
            combine += data[key]
        print combine[0]
        with io.open(outfilename, 'w', encoding='utf8') as g:
            d = json.dumps(combine, ensure_ascii=False, encoding='utf8')
            g.write(unicode(d))

if __name__ == "__main__":
    flatten("zh-CN\\AllSets.zhCN.json", "zh-CN\\AllSets.zhCN.flatten.json")

如果处理的是英文的话,大可不必也得这么奇怪,可是面对中文,python的编码问题大家也都是懂的。这段代码应该可以work。

然后再尝试运行下导入的命令,记得加上后面的选项。

$ mongoimport -d hearthstone -c cards pathtofile\\AllSets.zhCN.flatten.json --jsonArray
2015-08-07T16:12:01.662+0800    connected to: localhost
2015-08-07T16:12:01.696+0800    imported 1359 documents

大功告成了!

简单的数据分析

我觉得基于简单需求的统计是上手 MongoDB 的很好方式。比如说要统计下炉石已经出了多少张传说卡牌:

> db.cards.count({"rarity": "Legendary", "collectible": true})
67

> db.cards.aggregate([{$match : {"collectible": true} }, {$group: {_id : "$type", cnt: {$sum : 1} }}, {$sort: {"_id": 1}} ])
{ "_id" : "Hero", "cnt" : 12 }
{ "_id" : "Minion", "cnt" : 362 }
{ "_id" : "Spell", "cnt" : 186 }
{ "_id" : "Weapon", "cnt" : 18 }
> db.cards.aggregate([{$match : {"collectible": true, "type": "Minion"} }, {$group: {_id : "$cost", cnt: {$sum : 1} }}, {$sort: {"_id": 1}} ])
{ "_id" : 0, "cnt" : 2 }
{ "_id" : 1, "cnt" : 36 }
{ "_id" : 2, "cnt" : 67 }
{ "_id" : 3, "cnt" : 68 }
{ "_id" : 4, "cnt" : 55 }
{ "_id" : 5, "cnt" : 52 }
{ "_id" : 6, "cnt" : 36 }
{ "_id" : 7, "cnt" : 17 }
{ "_id" : 8, "cnt" : 11 }
{ "_id" : 9, "cnt" : 13 }
{ "_id" : 10, "cnt" : 2 }
{ "_id" : 12, "cnt" : 2 }
{ "_id" : 20, "cnt" : 1 }
> db.cards.find({"collectible": true, "type": "Minion", "cost":2, "mechanics": "Taunt"}, {_id: 0, "name":1, "attack": 1, "health": 1})
{ "name" : "蹒跚的食尸鬼", "attack" : 1, "health" : 3 }
{ "name" : "霜狼步兵", "attack" : 2, "health" : 2 }
{ "name" : "吵吵机器人", "attack" : 1, "health" : 2 }
{ "name" : "电镀机械熊仔", "attack" : 2, "health" : 2 }

注:此处的统计不包含最新的冠军的试炼。

  • 快帮忙计算一下现在哪个职业最厉害!

    • 必须是万佛朝宗骑啊!佛法无边,回头是岸。:)