整合營銷服務商

          電腦端+手機端+微信端=數(shù)據(jù)同步管理

          免費咨詢熱線:

          使用JavaScript寫爬蟲

          比Python,JavaScript才是更適合寫爬蟲的語言。原因有如下三個方面:

          • JavaScript異步IO機制適用于爬蟲這種IO密集型任務。JavaScript中的回調非常自然,使用異步網(wǎng)絡請求能夠充分利用CPU。
          • JavaScript中的jQuery毫無疑問是最強悍的HTML解析工具,使用JavaScript寫爬蟲能夠減少學習負擔和記憶負擔。雖然Python中有PyQuery,但終究還是比不上jQuery自然。
          • 爬取結果多為JSON,JavaScript是最適合處理JSON的語言。

          一、任務:爬取用戶在Github上的repo信息

          通過實例的方式學習爬蟲是最好的方法,先定一個小目標:爬取github repo信息。入口URL如下,我們只需要一直點擊next按鈕就能夠遍歷到用戶的所有repo。

          https://github.com/{{username}}?tab=repositories

          獲取repo之后,可以做什么?

          • 統(tǒng)計用戶最常使用的語言,統(tǒng)計用戶語言使用分布情況統(tǒng)計用戶所獲取的star數(shù),fork數(shù)

          二、爬蟲雙股劍:axios和jQuery

          axios是JavaScript中很常用的異步網(wǎng)絡請求庫,相比jQuery,它更輕量、更專業(yè)。既能夠用于瀏覽器端,也可以用于Node。它的語法風格是promise形式的。在本任務中,只需要了解如下用法就足夠了:

          axios.get(url).then((resp) => {
           請求成功,處理resp.data中的html數(shù)據(jù)
          }).catch((err) => {
           請求失敗,錯誤處理
          })
          

          請求之后需要處理回復結果,處理回復結果的庫當然是用jQuery。實際上,我們有更好的選擇:cheerio。

          在node下,使用jQuery,需要使用jsdom庫模擬一個window對象,這種方法效率較低,四個字形容就是:笨重穩(wěn)妥。

          如下代碼使用jQuery解析haha.html文件

          fs = require("fs")
          jquery=require('jquery')
          jsdom=require('jsdom') //fs.readFileSync()返回結果是一個buffer,相當于byte[] 
          html = fs.readFileSync('haha.html').toString('utf8') 
          dom= new jsdom.JSDOM(html) 
          $=jquery(dom.window) console.log($('h1'))
          

          cheerio只實現(xiàn)了jQuery中的DOM部分,相當于jQuery的一個子集。cheerio的語法和jQuery完全一致,在使用cheerio時,幾乎感覺不到它和jQuery的差異。在解析HTML方面,毫無疑問,cheerio是更好的選擇。如下代碼使用cheerio解析haha.html文件。

          cheerio=require('cheerio')
          html=require('fs').readFileSync("haha.html").toString('utf8')
          $=cheerio.load(html)
          console.log($('h1'))
          

          只需20余行,便可實現(xiàn)簡單的github爬蟲,此爬蟲只爬取了一頁repo列表。

          var axios = require("axios")
          var cheerio = require("cheerio")
          axios.get("https://github.com/weiyinfu?tab=repositories").then(resp => {
           var $ = cheerio.load(resp.data)
           var lis = $("#user-repositories-list li")
           var repos = []
           for (var i = 0; i < lis.length; i++) {
           var li = lis.eq(i)
           var repo = {
           repoName: li.find("h3").text().trim(),
           repoUrl: li.find("h3 a").attr("href").trim(),
           repoDesc: li.find("p").text().trim(),
           language: li.find("[itemprop=programmingLanguage]").text().trim(),
           star: li.find(".muted-link.mr-3").eq(0).text().trim(),
           fork: li.find(".muted-link.mr-3").eq(1).text().trim(),
           forkedFrom: li.find(".f6.text-gray.mb-1 a").text().trim()
           }
           repos.push(repo)
           }
           console.log(repos)
          })
          

          三、更豐富的功能

          爬蟲不是目的,而是達成目的的一種手段。獲取數(shù)據(jù)也不是目的,從數(shù)據(jù)中提取統(tǒng)計信息并呈現(xiàn)給人才是最終目的。

          在github爬蟲的基礎上,我們可以擴展出更加豐富的功能:使用echarts等圖表展示結果。

          要想讓更多人使用此爬蟲工具獲取自己的github統(tǒng)計信息,就需要將做成一個網(wǎng)站的形式,通過搜索頁面輸入用戶名,啟動爬蟲立即爬取github信息,然后使用echarts進行統(tǒng)計展示。網(wǎng)站肯定也要用js作為后端,這樣才能和js爬蟲無縫銜接,不然還要考慮跨語言調用。js后端有兩大web框架express和koa,二者API非常相似,并無優(yōu)劣之分,但express更加流行。

          如上設計有一處用戶體驗不佳的地方:當啟動爬蟲爬取github信息時,用戶可能需要等待好幾秒,這個過程不能讓用戶干等著。一種解決思路是:讓用戶看到爬蟲爬取的進度或者爬取過程。可以通過websocket向用戶推送爬取過程信息并在前端進行展示。展示時,使用類似控制臺的界面進行展示。

          如何存儲爬取到的數(shù)據(jù)呢?使用MongoDB或者文件都可以,最好實現(xiàn)兩種存儲方式,讓系統(tǒng)的存儲方式變得可配置。使用MongoDB時,用到js中的連接池框架generic-pool。

          整個項目用到的庫包括:

          • express:后端框架
          • cheerio+axios:爬蟲
          • ws:websocket展示爬取過程
          • webpack:打包工具
          • less:樣式語言
          • echarts:圖表展示
          • vue:模板渲染
          • jquery:DOM操作
          • mongodb:存儲數(shù)據(jù)
          • generic-pool:數(shù)據(jù)庫連接池

          試用地址:

          https://weiyinfu.cn/githubstatistic/search.html?

          案例地址:https://github.com/weiyinfu/GithubStatistic

          原文鏈接:https://zhuanlan.zhihu.com/p/53763115


          oSQL,全稱 Not Only SQL,意為不僅僅是 SQL,泛指非關系型數(shù)據(jù)庫。NoSQL 是基于鍵值對的,而且不需要經過 SQL 層的解析,數(shù)據(jù)之間沒有耦合性,性能非常高。

          非關系型數(shù)據(jù)庫又可細分如下:

          • 鍵值存儲數(shù)據(jù)庫:其代表有 Redis、Voldemort 和 Oracle BDB 等。
          • 列存儲數(shù)據(jù)庫:其代表有 Cassandra、HBase 和 Riak 等。
          • 文檔型數(shù)據(jù)庫:其代表有 CouchDB 和 MongoDB 等。
          • 鍵值存儲數(shù)據(jù)庫:其代表有 Redis、Voldemort 和 Oracle BDB 等。
          • 圖形數(shù)據(jù)庫:其代表有 Neo4J、InfoGrid 和 Infinite Graph 等。

          對于爬蟲的數(shù)據(jù)存儲來說,一條數(shù)據(jù)可能存在某些字段提取失敗而缺失的情況,而且數(shù)據(jù)可能隨時調整。另外,數(shù)據(jù)之間還存在嵌套關系。如果使用關系型數(shù)據(jù)庫存儲,一是需要提前建表,二是如果存在數(shù)據(jù)嵌套關系的話,需要進行序列化操作才可以存儲,這非常不方便。如果用了非關系型數(shù)據(jù)庫,就可以避免一些麻煩,更簡單、高效。

          本節(jié)中,我們主要介紹 MongoDB 存儲操作。

          MongoDB 是由 C++ 語言編寫的非關系型數(shù)據(jù)庫,是一個基于分布式文件存儲的開源數(shù)據(jù)庫系統(tǒng),其內容存儲形式類似 JSON 對象,它的字段值可以包含其他文檔、數(shù)組及文檔數(shù)組,非常靈活。在這一節(jié)中,我們就來看看 Python 3 下 MongoDB 的存儲操作。

          1. 準備工作

          在開始之前,請確保已經安裝好了 MongoDB 并啟動了其服務,安裝方式可以參考:https://setup.scrape.center/mongodb。

          除了安裝好 MongoDB 數(shù)據(jù)庫,我們還需要安裝好 Python 的 PyMongo 庫,如尚未安裝,可以使用 pip3 來安裝:

          pip3 install pymongo

          更詳細的安裝說明可以參考:https://setup.scrape.center/pymongo。

          安裝好 MongoDB 數(shù)據(jù)庫和 PyMongo 庫之后,我們便可以開始本節(jié)的學習了。

          2. 連接 MongoDB

          連接 MongoDB 時,我們需要使用 PyMongo 庫里面的 MongoClient。一般來說,傳入 MongoDB 的 IP 及端口即可,其中第一個參數(shù)為地址 host,第二個參數(shù)為端口 port(如果不給它傳遞參數(shù),默認是 27017):

          import pymongo
          client = pymongo.MongoClient(host='localhost', port=27017)

          這樣就可以創(chuàng)建 MongoDB 的連接對象了。

          另外,MongoClient 的第一個參數(shù) host 還可以直接傳入 MongoDB 的連接字符串,它以 mongodb 開頭,例如:

          client = MongoClient('mongodb://localhost:27017/')

          這也可以達到同樣的連接效果。

          3. 指定數(shù)據(jù)庫

          在 MongoDB 中,可以建立多個數(shù)據(jù)庫,接下來我們需要指定操作哪個數(shù)據(jù)庫。這里我們以 test 數(shù)據(jù)庫為例來說明,下一步需要在程序中指定要使用的數(shù)據(jù)庫:

          db = client.test

          這里調用 client 的 test 屬性即可返回 test 數(shù)據(jù)庫。當然,我們也可以這樣指定:

          db = client['test']

          這兩種方式是等價的。

          4. 指定集合

          MongoDB 的每個數(shù)據(jù)庫又包含許多集合(collection),它們類似于關系型數(shù)據(jù)庫中的表。

          下一步需要指定要操作的集合,這里指定一個集合名稱為 students。與指定數(shù)據(jù)庫類似,指定集合也有兩種方式:

          collection = db.students
          collection = db['students']

          這樣我們便聲明了一個集合對象。

          5. 插入數(shù)據(jù)

          接下來,便可以插入數(shù)據(jù)了。對于 students 這個集合,新建一條學生數(shù)據(jù),這條數(shù)據(jù)以字典形式表示:

          student = {
              'id': '20170101',
              'name': 'Jordan',
              'age': 20,
              'gender': 'male'
          }

          這里指定了學生的學號、姓名、年齡和性別。接下來,直接調用 collection 的 insert 方法即可插入數(shù)據(jù),代碼如下:

          result = collection.insert(student)
          print(result)

          在 MongoDB 中,每條數(shù)據(jù)其實都有一個 _id 屬性來唯一標識。如果沒有顯式指明該屬性,MongoDB 會自動產生一個 ObjectId 類型的 _id 屬性。insert 方法會在執(zhí)行后返回 _id 值。

          運行結果如下:

          5932a68615c2606814c91f3d

          當然,我們也可以同時插入多條數(shù)據(jù),只需要以列表形式傳遞即可,示例如下:

          student1 = {
              'id': '20170101',
              'name': 'Jordan',
              'age': 20,
              'gender': 'male'
          }
          
          student2 = {
              'id': '20170202',
              'name': 'Mike',
              'age': 21,
              'gender': 'male'
          }
          
          result = collection.insert([student1, student2])
          print(result)

          返回結果是對應的 _id 的集合:

          [ObjectId('5932a80115c2606a59e8a048'), ObjectId('5932a80115c2606a59e8a049')]

          實際上,在 PyMongo 3.x 版本中,官方已經不推薦使用 insert 方法了。當然,繼續(xù)使用也沒有什么問題。官方推薦使用 insert_one 和 insert_many 方法來分別插入單條記錄和多條記錄,示例如下:

          student = {
              'id': '20170101',
              'name': 'Jordan',
              'age': 20,
              'gender': 'male'
          }
          
          result = collection.insert_one(student)
          print(result)
          print(result.inserted_id)

          運行結果如下:

          <pymongo.results.InsertOneResult object at 0x10d68b558>
          5932ab0f15c2606f0c1cf6c5

          與 insert 方法不同,這次返回的是 InsertOneResult 對象,我們可以調用其 inserted_id 屬性獲取 _id。

          對于 insert_many 方法,我們可以將數(shù)據(jù)以列表形式傳遞,示例如下:

          student1 = {
              'id': '20170101',
              'name': 'Jordan',
              'age': 20,
              'gender': 'male'
          }
          
          student2 = {
              'id': '20170202',
              'name': 'Mike',
              'age': 21,
              'gender': 'male'
          }
          
          result = collection.insert_many([student1, student2])
          print(result)
          print(result.inserted_ids)

          運行結果如下:

          <pymongo.results.InsertManyResult object at 0x101dea558>
          [ObjectId('5932abf415c2607083d3b2ac'), ObjectId('5932abf415c2607083d3b2ad')]

          該方法返回的是 InsertManyResult 類型的對象,調用 inserted_ids 屬性可以獲取插入數(shù)據(jù)的 _id 列表。

          6. 查詢

          插入數(shù)據(jù)后,我們可以利用 find_one 或 find 方法進行查詢,其中 find_one 查詢得到的是單個結果,find 則返回一個生成器對象。示例如下:

          result = collection.find_one({'name': 'Mike'})
          print(type(result))
          print(result)

          這里我們查詢 name 為 Mike 的數(shù)據(jù),它的返回結果是字典類型,運行結果如下:

          <class 'dict'>
          {'_id': ObjectId('5932a80115c2606a59e8a049'), 'id': '20170202', 'name': 'Mike', 'age': 21, 'gender': 'male'}

          可以發(fā)現(xiàn),它多了 _id 屬性,這就是 MongoDB 在插入過程中自動添加的。

          此外,我們也可以根據(jù) ObjectId 來查詢,此時需要使用 bson 庫里面的 objectid:

          from bson.objectid import ObjectId
          
          result = collection.find_one({'_id': ObjectId('593278c115c2602667ec6bae')})
          print(result)

          其查詢結果依然是字典類型,具體如下:

          {'_id': ObjectId('593278c115c2602667ec6bae'), 'id': '20170101', 'name': 'Jordan', 'age': 20, 'gender': 'male'}

          當然,如果查詢結果不存在,則會返回 None。

          對于多條數(shù)據(jù)的查詢,我們可以使用 find 方法。例如,這里查找年齡為 20 的數(shù)據(jù),示例如下:

          results = collection.find({'age': 20})
          print(results)
          for result in results:
              print(result)

          運行結果如下:

          <pymongo.cursor.Cursor object at 0x1032d5128>
          {'_id': ObjectId('593278c115c2602667ec6bae'), 'id': '20170101', 'name': 'Jordan', 'age': 20, 'gender': 'male'}
          {'_id': ObjectId('593278c815c2602678bb2b8d'), 'id': '20170102', 'name': 'Kevin', 'age': 20, 'gender': 'male'}
          {'_id': ObjectId('593278d815c260269d7645a8'), 'id': '20170103', 'name': 'Harden', 'age': 20, 'gender': 'male'}

          返回結果是 Cursor 類型,它相當于一個生成器,我們需要遍歷取到所有的結果,其中每個結果都是字典類型。

          如果要查詢年齡大于 20 的數(shù)據(jù),則寫法如下:

          results = collection.find({'age': {'$gt': 20}})

          這里查詢的條件鍵值已經不是單純的數(shù)字了,而是一個字典,其鍵名為比較符號 $gt,意思是大于,鍵值為 20。

          這里將比較符號歸納為表 5-3。

          表 5-3 比較符號

          符 號

          含 義

          示 例

          $lt

          小于

          {'age': {'$lt': 20}}

          $gt

          大于

          {'age': {'$gt': 20}}

          $lte

          小于等于

          {'age': {'$lte': 20}}

          $gte

          大于等于

          {'age': {'$gte': 20}}

          $ne

          不等于

          {'age': {'$ne': 20}}

          $in

          在范圍內

          {'age': {'$in': [20, 23]}}

          $nin

          不在范圍內

          {'age': {'$nin': [20, 23]}}

          另外,還可以進行正則匹配查詢。例如,查詢名字以 M 開頭的學生數(shù)據(jù),示例如下:

          results = collection.find({'name': {'$regex': '^M.*'}})

          這里使用 $regex 來指定正則匹配,^M.* 代表以 M 開頭的正則表達式。

          這里將一些功能符號再歸類為下表。

          符 號

          含 義

          示 例

          示例含義

          $regex

          匹配正則表達式

          {'name': {'$regex': '^M.*'}}

          name 以 M 開頭

          $exists

          屬性是否存在

          {'name': {'$exists': True}}

          name 屬性存在

          $type

          類型判斷

          {'age': {'$type': 'int'}}

          age 的類型為 int

          $mod

          數(shù)字模操作

          {'age': {'$mod': [5, 0]}}

          年齡模 5 余 0

          $text

          文本查詢

          {'$text': {'$search': 'Mike'}}

          text 類型的屬性中包含 Mike 字符串

          $where

          高級條件查詢

          {'$where': 'obj.fans_count == obj.follows_count'}

          自身粉絲數(shù)等于關注數(shù)

          關于這些操作的更詳細用法,可以在 MongoDB 官方文檔找到: https://docs.mongodb.com/manual/reference/operator/query/。

          7. 計數(shù)

          要統(tǒng)計查詢結果有多少條數(shù)據(jù),可以調用 count 方法。比如,統(tǒng)計所有數(shù)據(jù)條數(shù):

          count = collection.find().count()
          print(count)

          或者統(tǒng)計符合某個條件的數(shù)據(jù):

          count = collection.find({'age': 20}).count()
          print(count)

          運行結果是一個數(shù)值,即符合條件的數(shù)據(jù)條數(shù)。

          8. 排序

          排序時,直接調用 sort 方法,并在其中傳入排序的字段及升降序標志即可。示例如下:

          results = collection.find().sort('name', pymongo.ASCENDING)
          print([result['name'] for result in results])

          運行結果如下:

          ['Harden', 'Jordan', 'Kevin', 'Mark', 'Mike']

          這里我們調用 pymongo.ASCENDING 指定升序。如果要降序排列,可以傳入 pymongo.DESCENDING。

          9. 偏移

          在某些情況下,我們可能想只取某幾個元素,這時可以利用 skip 方法偏移幾個位置,比如偏移 2,就忽略前兩個元素,得到第三個及以后的元素:

          results = collection.find().sort('name', pymongo.ASCENDING).skip(2)
          print([result['name'] for result in results])

          運行結果如下:

          ['Kevin', 'Mark', 'Mike']

          另外,還可以用 limit 方法指定要取的結果個數(shù),示例如下:

          results = collection.find().sort('name', pymongo.ASCENDING).skip(2).limit(2)
          print([result['name'] for result in results])

          運行結果如下:

          ['Kevin', 'Mark']

          如果不使用 limit 方法,原本會返回三個結果,加了限制后,會截取兩個結果返回。

          值得注意的是,在數(shù)據(jù)庫數(shù)量非常龐大的時候,如千萬、億級別,最好不要使用大的偏移量來查詢數(shù)據(jù),因為這樣很可能導致內存溢出。此時可以使用類似如下操作來查詢:

          from bson.objectid import ObjectId
          collection.find({'_id': {'$gt': ObjectId('593278c815c2602678bb2b8d')}})

          這時需要記錄好上次查詢的 _id。

          10. 更新

          對于數(shù)據(jù)更新,我們可以使用 update 方法,指定更新的條件和更新后的數(shù)據(jù)即可。例如:

          condition = {'name': 'Kevin'}
          student = collection.find_one(condition)
          student['age'] = 25
          result = collection.update(condition, student)
          print(result)

          這里我們要更新 name 為 Kevin 的數(shù)據(jù)的年齡:首先指定查詢條件,然后將數(shù)據(jù)查詢出來,修改年齡后調用 update 方法將原條件和修改后的數(shù)據(jù)傳入。

          運行結果如下:

          {'ok': 1, 'nModified': 1, 'n': 1, 'updatedExisting': True}

          返回結果是字典形式,ok 代表執(zhí)行成功,nModified 代表影響的數(shù)據(jù)條數(shù)。

          另外,我們也可以使用 $set 操作符對數(shù)據(jù)進行更新,代碼如下:

          result = collection.update(condition, {'$set': student})

          這樣可以只更新 student 字典內存在的字段。如果原先還有其他字段,則不會更新,也不會刪除。而如果不用 $set 的話,則會把之前的數(shù)據(jù)全部用 student 字典替換;如果原本存在其他字段,則會被刪除。

          另外,update 方法其實也是官方不推薦使用的方法。這里也分為 update_one 方法和 update_many 方法,用法更加嚴格,它們的第二個參數(shù)需要使用 $ 類型操作符作為字典的鍵名,示例如下:

          condition = {'name': 'Kevin'}
          student = collection.find_one(condition)
          student['age'] = 26
          result = collection.update_one(condition, {'$set': student})
          print(result)
          print(result.matched_count, result.modified_count)

          這里調用了 update_one 方法,其第二個參數(shù)不能再直接傳入修改后的字典,而是需要使用 {'$set': student} 這樣的形式,其返回結果是 UpdateResult 類型。然后分別調用 matched_count 和 modified_count 屬性,獲得匹配的數(shù)據(jù)條數(shù)和影響的數(shù)據(jù)條數(shù)。

          運行結果如下:

          <pymongo.results.UpdateResult object at 0x10d17b678>
          1 0

          我們再看一個例子:

          condition = {'age': {'$gt': 20}}
          result = collection.update_one(condition, {'$inc': {'age': 1}})
          print(result)
          print(result.matched_count, result.modified_count)

          這里指定查詢條件為年齡大于 20,然后更新條件為 {'$inc': {'age': 1}},也就是年齡加 1,執(zhí)行之后會將第一條符合條件的數(shù)據(jù)年齡加 1。

          運行結果如下:

          <pymongo.results.UpdateResult object at 0x10b8874c8>
          1 1

          可以看到匹配條數(shù)為 1 條,影響條數(shù)也為 1 條。

          如果調用 update_many 方法,則會將所有符合條件的數(shù)據(jù)都更新,示例如下:

          condition = {'age': {'$gt': 20}}
          result = collection.update_many(condition, {'$inc': {'age': 1}})
          print(result)
          print(result.matched_count, result.modified_count)

          這時匹配條數(shù)就不再為 1 條了,運行結果如下:

          <pymongo.results.UpdateResult object at 0x10c6384c8>
          3 3

          可以看到,這時所有匹配到的數(shù)據(jù)都會被更新。

          11. 刪除

          刪除操作比較簡單,直接調用 remove 方法指定刪除的條件即可,此時符合條件的所有數(shù)據(jù)均會被刪除。示例如下:

          result = collection.remove({'name': 'Kevin'})
          print(result)

          運行結果如下:

          {'ok': 1, 'n': 1}

          另外,這里依然存在兩個新的推薦方法 —— delete_one 和 delete_many。示例如下:

          result = collection.delete_one({'name': 'Kevin'})
          print(result)
          print(result.deleted_count)
          result = collection.delete_many({'age': {'$lt': 25}})
          print(result.deleted_count)

          運行結果如下:

          <pymongo.results.DeleteResult object at 0x10e6ba4c8>
          1
          4

          delete_one 即刪除第一條符合條件的數(shù)據(jù),delete_many 即刪除所有符合條件的數(shù)據(jù)。它們的返回結果都是 DeleteResult 類型,可以調用 deleted_count 屬性獲取刪除的數(shù)據(jù)條數(shù)。

          12. 其他操作

          另外,PyMongo 還提供了一些組合方法,如 find_one_and_delete、find_one_and_replace 和 find_one_and_update,它們是查找后刪除、替換和更新操作,其用法與上述方法基本一致。

          另外,還可以對索引進行操作,相關方法有 create_index、create_indexes 和 drop_index 等。

          關于 PyMongo 的詳細用法,可以參見官方文檔:http://api.mongodb.com/python/current/api/pymongo/collection.html。

          另外,還有對數(shù)據(jù)庫和集合本身等的一些操作,這里不再一一講解,可以參見官方文檔:http://api.mongodb.com/python/current/api/pymongo/。

          13. 總結

          本節(jié)講解了使用 PyMongo 操作 MongoDB 進行數(shù)據(jù)增刪改查的方法,后面我們會在實戰(zhàn)案例中應用這些操作進行數(shù)據(jù)存儲。

          本節(jié)代碼:https://github.com/Python3WebSpider/MongoDBTest。


          .MongDB 簡介

          MongoDB(來自于英文單詞“Humongous”,中文含義為“龐大”)是可以應用于各種規(guī)模的企業(yè)、各個行業(yè)以及各類應用程序的開源數(shù)據(jù)庫。作為一個適用于敏捷開發(fā)的數(shù)據(jù)庫,MongoDB 的數(shù)據(jù)模式可以隨著應用程序的發(fā)展而靈活地更新。與此同時,它也為開發(fā)人員 提供了傳統(tǒng)數(shù)據(jù)庫的功能:二級索引,完整的查詢系統(tǒng)以及嚴格一致性等等。MongoDB 能夠使企業(yè)更加具有敏捷性和可擴展性,各種規(guī)模的企業(yè)都可以通過使用 MongoDB 來創(chuàng)建新的應用,提高與客戶之間的工作效率,加快產品上市時間,以及降低企業(yè)成本。

          MongoDB 是專為可擴展性,高性能和高可用性而設計的數(shù)據(jù)庫。它可以從單服務器部署擴展到大型、復雜的多數(shù)據(jù)中心架構。利用內存計算的優(yōu)勢,MongoDB 能夠提供高性能的數(shù)據(jù)讀寫操作。MongoDB 的本地復制和自動故障轉移功能使您的應用程序具有企業(yè)級的可靠性和操作靈活性。

          以上內容摘自官網(wǎng):

          1.1 文檔型數(shù)據(jù)庫

          簡而言之,MongoDB是一個免費開源跨平臺的 NoSQL 數(shù)據(jù)庫,與關系型數(shù)據(jù)庫不同,MongoDB 的數(shù)據(jù)以類似于 JSON 格式的二進制文檔存儲:

          {
              name: "jack",
              age: 22,
          }

          文檔型的數(shù)據(jù)存儲方式有幾個重要好處:

          1. 文檔的數(shù)據(jù)類型可以對應到語言的數(shù)據(jù)類型,如數(shù)組類型(Array)和對象類型(Object);
          2. 文檔可以嵌套,有時關系型數(shù)據(jù)庫涉及幾個表的操作,在 MongoDB 中一次就能完成,可以減少昂貴的連接花銷;
          3. 文檔不對數(shù)據(jù)結構加以限制,不同的數(shù)據(jù)結構可以存儲在同一張表;
          4. MongoDB 的文檔數(shù)據(jù)模型和索引系統(tǒng)能有效提升數(shù)據(jù)庫性能;
          5. 復制集功能提供數(shù)據(jù)冗余,自動化容災容錯,提升數(shù)據(jù)庫可用性;
          6. 分片技術能夠分散單服務器的讀寫壓力,提高并發(fā)能力,提升數(shù)據(jù)庫的可拓展性;
          7. MongoDB 高性能,高可用性、可擴展性等特點,使其至 2009 年發(fā)布以來,逐漸被認可,并被越來越多的用于生產環(huán)境中。AWS、GCP、阿里云等云平臺都提供了十分便捷的 MongoDB 云服務。

          1.2 MongoDB 基礎概念

          可以使用我們熟悉的 MySQL 數(shù)據(jù)庫來加以對比:

          MySQL 基礎概念MongoDB 對應概念數(shù)據(jù)庫(database)容器(database)表(table)集合(collection)行(row)文檔(document)列(column)域(filed)索引(index)索引(index)

          也借用一下菜鳥教程)的圖來更加形象生動的說明一下:

          這很容易理解,但是問題在于:我們?yōu)槭裁匆胄碌母拍钅兀?/strong>(也就是為什么我們要把“表”替換成“集合”,“行”替換成“文檔”,“列”替換成“域”呢?)原因在于,其實在 MySQL 這樣的典型關系型數(shù)據(jù)中,我們是在定義表的時候定義列的,但是由于上述文檔型數(shù)據(jù)庫的特點,它允許文檔的數(shù)據(jù)類型可以對應到語言的數(shù)據(jù)類型,所以我們是在定義文檔的時候才會定義域的。

          也就是說,集合中的每個文檔都可以有獨立的域。因此,雖說集合相對于表來說是一個簡化了的容器,而文檔則包含了比行要多得多的信息。

          2 搭建環(huán)境

          怎么樣都好,搭建好環(huán)境就行,這里以 OS 環(huán)境為例,你可以使用 OSX 的 brew 安裝 mongodb:

          brew install mongodb

          在運行之前我們需要創(chuàng)建一個數(shù)據(jù)庫存儲目錄 /data/db:

          sudo mkdir -p /data/db

          然后啟動 mongodb,默認數(shù)據(jù)庫目錄即為 /data/db(如果不是,可以使用 --dbpath 指令來指定):

          sudo mongd

          過一會兒你就能看到你的 mongodb 運行起來的提示:

          具體的搭建過程可以參考菜鳥的教程:http://www.runoob.com/mongodb/mongodb-window-install.html

          3 基于 Shell 的 CRUD

          3.1 連接實例

          通過上面的步驟我們在系統(tǒng)里運行了一個 mongodb 實例,接下來通過 mongo 命令來連接它:

          mongo [options] [db address] [file names]

          由于上面運行的 mongodb 運行在 27017 端口,并且滅有啟動安全模式,所以我們也不需要輸入用戶名和密碼就可以直接連接:

          mongo 127.0.0.1:27017

          或者通過 --host 和 --port 選項指定主機和端口。一切順利的話,就進入了 mongoDB shell,shell 會報出一連串權限警告,不過不用擔心,這并不會影響之后的操作。在添加授權用戶和開啟認證后,這些警告會自動消失。

          3.2 CRUD 操作

          在進行增刪改查操作之前,我們需要先了解一下常用的 shell 命令:

          • db 顯示當前所在數(shù)據(jù)庫,默認為 test
          • show dbs 列出可用數(shù)據(jù)庫
          • show tables show collections 列出數(shù)據(jù)庫中可用集合
          • use 用于切換數(shù)據(jù)庫

          mongoDB 預設有兩個數(shù)據(jù)庫,admin 和 local,admin 用來存放系統(tǒng)數(shù)據(jù),local 用來存放該實例數(shù)據(jù),在副本集中,一個實例的 local 數(shù)據(jù)庫對于其它實例是不可見的。使用 use 命令切換數(shù)據(jù)庫:

          > use admin
          > use local
          > use newDatabase

          可以 use 一個不存在的數(shù)據(jù)庫,當你存入新數(shù)據(jù)時,mongoDB 會創(chuàng)建這個數(shù)據(jù)庫:

          > use newDatabase
          > db.newCollection.insert({x:1})
          WriteResult({ "nInserted" : 1 })

          以上命令向數(shù)據(jù)庫中插入一個文檔,返回 1 表示插入成功,mongoDB 自動創(chuàng)建 newCollection 集合和數(shù)據(jù)庫 newDatabase。下面將對增查改刪操作進行一個簡單的演示。

          3.2.1 創(chuàng)建(Create)

          MongoDB 提供 insert 方法創(chuàng)建新文檔:

          • db.collection.inserOne() 插入單個文檔WriteResult({ "nInserted" : 1 })
          • db.collection.inserMany() 插入多個文檔
          • db.collection.insert() 插入單條或多條文檔

          我們接著在剛才新創(chuàng)建的 newDatabase 下面新增數(shù)據(jù)吧:

          db.newCollection.insert({name:"wmyskxz",age:22})

          根據(jù)以往經驗應該會覺得蠻奇怪的,因為之前在這個集合中插入的數(shù)據(jù)格式是 {x:1} 的,而這里新增的數(shù)據(jù)格式確是 {name:"wmyskxz",age:22} 這個樣子的。還記得嗎,文檔型數(shù)據(jù)庫的與傳統(tǒng)型的關系型數(shù)據(jù)的區(qū)別就是在這里!

          并且要注意,age:22 和 age:"22" 是不一樣的哦,前者插入的是一個數(shù)值,而后者是字符串,我們可以通過 db.newCollection.find() 命令查看到剛剛插入的文檔:

          > db.newCollection.find()
          { "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
          { "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }

          這里有一個神奇的返回,那就是多了一個叫做 _id 的東西,這是 MongoDB 為你自動添加的字段,你也可以自己生成。大部分情況下還是會讓 MongoDB 為我們生成,而且默認情況下,該字段是被加上了索引的。

          3.2.2 查找(Read)

          MongoDB 提供 find 方法查找文檔,第一個參數(shù)為查詢條件:

          > db.newCollection.find() # 查找所有文檔
          { "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
          { "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
          > db.newCollection.find({name:"wmyskxz"}) # 查找 name 為 wmyskxz 的文檔
          { "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
          > db.newCollection.find({age:{$gt:20}}) # 查找 age 大于 20 的文檔
          { "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }

          上述代碼中的$gt對應于大于號>的轉義。

          第二個參數(shù)可以傳入投影文檔映射數(shù)據(jù):

          > db.newCollection.find({age:{$gt:20}},{name:1})
          { "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz" }

          上述命令將查找 age 大于 20 的文檔,返回 name 字段,排除其他字段。投影文檔中字段為 1 或其他真值表示包含,0 或假值表示排除,可以設置多個字段位為 1 或 0,但不能混合使用。

          為了測試,我們?yōu)檫@個集合弄了一些奇奇怪怪的數(shù)據(jù):

          > db.newCollection.find()
          { "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
          { "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
          { "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30 }

          然后再來測試:

          > db.newCollection.find({age:{$gt:20}},{name:1,x:1}) 
          { "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz" }
          { "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "x" : 1 }
          > db.newCollection.find({age:{$gt:20}},{name:0,x:0}) 
          { "_id" : ObjectId("5cc102fb33907ae66490e46d"), "age" : 22 }
          { "_id" : ObjectId("5cc108fb33907ae66490e46e"), "age" : 22, "y" : 30 }
          > db.newCollection.find({age:{$gt:20}},{name:0,x:1})
          Error: error: {
              "ok" : 0,
              "errmsg" : "Projection cannot have a mix of inclusion and exclusion.",
              "code" : 2,
              "codeName" : "BadValue"
          }

          從上面的命令我們就可以把我們的一些想法和上面的結論得以驗證,perfect!

          除此之外,還可以通過 count、skip、limit 等指針(Cursor)方法,改變文檔查詢的執(zhí)行方式:

          > db.newCollection.find().count()
          3
          > db.newCollection.find().skip(1).limit(10).sort({age:1})
          { "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 22 }
          { "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30 }

          上述查找命令跳過 1 個文檔,限制輸出 10 個,以 age 子段正序排序(大于 0 為正序,小于 0 位反序)輸出結果。最后,可以使用 Cursor 方法中的 pretty 方法,提升查詢文檔的易讀性,特別是在查看嵌套的文檔和配置文件的時候:

          > db.newCollection.find().pretty()
          { "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
          {
              "_id" : ObjectId("5cc102fb33907ae66490e46d"),
              "name" : "wmyskxz",
              "age" : 22
          }
          {
              "_id" : ObjectId("5cc108fb33907ae66490e46e"),
              "name" : "wmyskxz-test",
              "age" : 22,
              "x" : 1,
              "y" : 30
          }

          3.2.3 更新(Update)

          MongoDB 提供 update 方法更新文檔:

          • db.collection.updateOne() 更新最多一個符合條件的文檔
          • db.collection.updateMany() 更新所有符合條件的文檔
          • db.collection.replaceOne() 替代最多一個符合條件的文檔
          • db.collection.update() 默認更新一個文檔,可配置 multi 參數(shù),跟新多個文檔

          以 update() 方法為例。其格式:

          > db.collection.update(
              <query>,
              <update>,
              {
                  upsert: <boolean>,
                  multi: <boolean>
              }
          )

          各參數(shù)意義:

          • query 為查詢條件
          • update 為修改的文檔
          • upsert 為真,查詢?yōu)榭諘r插入文檔
          • multi 為真,更新所有符合條件的文檔

          下面我們測試把 name 字段為 wmyskxz 的文檔更新一下試試:

          > db.newCollection.update({name:"wmyskxz"},{name:"wmyskxz",age:30})
          WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

          要注意的是,如果更新文檔只傳入 age 字段,那么文檔會被更新為{age: 30},而不是{name:"wmyskxz", age:30}。要避免文檔被覆蓋,需要用到 $set 指令,$set 僅替換或添加指定字段:

          > db.newCollection.update({name:"wmyskxz"},{$set:{age:30}})

          如果要在查詢的文檔不存在的時候插入文檔,要把 upsert 參數(shù)設置真值:

          > db.newCollection.update({name:"wmyskxz11"},{$set:{age:30}},{upsert:true})

          update 方法默認情況只更新一個文檔,如果要更新符合條件的所有文檔,要把 multi 設為真值,并使用 $set 指令:

          > db.newCollection.update({age:{$gt:20}},{$set:{test:"A"}},{multi:true})
          WriteResult({ "nMatched" : 3, "nUpserted" : 0, "nModified" : 3 })
          > db.newCollection.find()
          { "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
          { "_id" : ObjectId("5cc102fb33907ae66490e46d"), "name" : "wmyskxz", "age" : 30, "test" : "A" }
          { "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30, "test" : "A" }
          { "_id" : ObjectId("5cc110148d0a578f03d43e81"), "name" : "wmyskxz11", "age" : 30, "test" : "A" }

          3.2.4 刪除(Delete)

          MongoDB 提供了 delete 方法刪除文檔:

          • db.collection.deleteOne() 刪除最多一個符合條件的文檔
          • db.collection.deleteMany() 刪除所有符合條件的文檔
          • db.collection.remove() 刪除一個或多個文檔

          以 remove 方法為例:

          > db.newCollection.remove({name:"wmyskxz11"})
          > db.newCollection.remove({age:{$gt:20}},{justOne:true})
          > db.newCollection.find()
          { "_id" : ObjectId("5cc1026533907ae66490e46c"), "x" : 1 }
          { "_id" : ObjectId("5cc108fb33907ae66490e46e"), "name" : "wmyskxz-test", "age" : 22, "x" : 1, "y" : 30, "test" : "A" }

          MongoDB 提供了 drop 方法刪除集合,返回 true 表面刪除集合成功:

          > db.newCollection.drop()

          3.2.5 小結

          相比傳統(tǒng)關系型數(shù)據(jù)庫,MongoDB 的 CURD 操作更像是編寫程序,更符合開發(fā)人員的直覺,不過 MongoDB 同樣也支持 SQL 語言。MongoDB 的 CURD 引擎配合索引技術、數(shù)據(jù)聚合技術和 JavaScript 引擎,賦予 MongoDB 用戶更強大的操縱數(shù)據(jù)的能力。

          參考文章:簡明 MongoDB 入門教程 -

          https://segmentfault.com/a/1190000010556670

          4 MongoDB 數(shù)據(jù)模型的一些討論

          前置申明:這一部分基于以下鏈接整理 https://github.com/justinyhuang/the-little-mongodb-book-cn/blob/master/mongodb.md#%E8%AE%B8%E5%8F%AF%E8%AF%81

          這是一個抽象的話題,與大多數(shù)NoSQL方案相比,在建模方面,面向文檔的數(shù)據(jù)庫算是和關系數(shù)據(jù)庫相差最小的。這些差別是很小,但是并不是說不重要。

          4.1 沒有連接(Join)

          您要接受的第一個也是最基本的一個差別,就是 MongoDB 沒有連接(join)。我不知道MongoDB不支持某些類型連接句法的具體原因,但是我知道一般而言人們認為連接是不可擴展的。也就是說,一旦開始橫向分割數(shù)據(jù),最終不可避免的就是在客戶端(應用程序服務器)使用連接。且不論MongoDB為什么不支持連接,事實是數(shù)據(jù)是有關系的,可是MongoDB不支持連接。(譯者:這里的關系指的是不同的數(shù)據(jù)之間是有關聯(lián)的,對于沒有關系的數(shù)據(jù),就完全不需要連接。)

          為了在沒有連接的MongoDB中生存下去,在沒有其他幫助的情況下,我們必須在自己的應用程序中實現(xiàn)連接。

          基本上我們需要用第二次查詢去找到相關的數(shù)據(jù)。找到并組織這些數(shù)據(jù)相當于在關系數(shù)據(jù)庫中聲明一個外來的鍵。現(xiàn)在先別管什么獨角獸了,我們來看看我們的員工。首先我們創(chuàng)建一個員工的數(shù)據(jù)(這次我告訴您具體的_id值,這樣我們的例子就是一樣的了):

          db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d730"), name: 'Leto'})

          然后我們再加入幾個員工并把 Leto 設成他們的老板:

          db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d731"), name: 'Duncan', manager: ObjectId("4d85c7039ab0fd70a117d730")});
          db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d732"), name: 'Moneo', manager: ObjectId("4d85c7039ab0fd70a117d730")});

          (有必要再強調一下,_id可以是任何的唯一的值。在實際工作中你很可能會用到ObjectId, 所以我們在這里也使用它)

          顯然,要找到Leto的所有員工,只要執(zhí)行:

          db.employees.find({manager: ObjectId("4d85c7039ab0fd70a117d730")})

          沒什么了不起的。在最糟糕的情況下,為彌補連接的缺失需要做的只是再多查詢一次而已,該查詢很可能是經過索引了的。

          4.1.1 數(shù)組和嵌入文檔(Embedded Documents)

          MongoDB 沒有連接并不意味著它沒有其他的優(yōu)勢。還記得我們曾說過 MongoDB 支持數(shù)組并把它當成文檔中的一級對象嗎?當處理多對一或是多對多關系的時候,這一特性就顯得非常好用了。用一個簡單的例子來說明,如果一個員工有兩個經理,我們可以把這個關系儲存在一個數(shù)組當中:

          ({name: 'Siona', manager: [ObjectId("4d85c7039ab0fd70a117d730"), ObjectId("4d85c7039ab0fd70a117d732")] })

          需要注意的是,在這種情況下,有些文檔中的 manager 可能是一個向量,而其他的卻是數(shù)組。在兩種情況下,前面的 find 還是一樣可以工作:

          db.employees.find({manager: ObjectId("4d85c7039ab0fd70a117d730")})

          很快您就會發(fā)現(xiàn)數(shù)組中的值比起多對多的連接表(join-table)來說要更容易處理。

          除了數(shù)組,MongoDB 還支持嵌入文檔。嘗試插入含有內嵌文檔的文檔,像這樣:

          db.employees.insert({_id: ObjectId("4d85c7039ab0fd70a117d734"), name: 'Ghanima', family: {mother: 'Chani', father: 'Paul', brother: ObjectId("4d85c7039ab0fd70a117d730")}})

          也許您會這樣想,確實也可以這樣做:嵌入文檔可以用‘.’符號來查詢:

          db.employees.find({'family.mother': 'Chani'})

          就這樣,我們簡要地介紹了嵌入文檔適用的場合以及您應該怎樣使用它。

          4.1.2 DBRef

          MongoDB 支持一個叫做 DBRef 的功能,許多 MongoDB 的驅動都提供對這一功能的支持。當驅動遇到一個 DBRef 時它會把當中引用的文檔讀取出來。DBRef 包含了所引用的文檔的 ID 和所在的集合。它通常專門用于這樣的場合:相同集合中的文檔需要引用另外一個集合中的不同文檔。例如,文檔 1 的 DBRef 可能指向 managers 中的文檔,而文檔 2 中的 DBRef 可能指向 employees 中的文檔。

          4.1.3 范規(guī)范化(Denormalization)

          代替連接的另一種方法就是反規(guī)范化數(shù)據(jù)。在過去,反規(guī)范化是為性能敏感代碼所設,或者是需要數(shù)據(jù)快照(例如審計日志)的時候才應用的。然而,隨著NoSQL的日漸普及,有許多這樣的數(shù)據(jù)庫并不提供連接操作,于是作為規(guī)范建模的一部分,反規(guī)范化就越來越常見了。這樣說并不是說您就需要為每個文檔中的每一條信息創(chuàng)建副本。與此相反,與其在設計的時候被復制數(shù)據(jù)的擔憂牽著走,還不如按照不同的信息應該歸屬于相應的文檔這一思路來對數(shù)據(jù)建模。

          比如說,假設您在編寫一個論壇的應用程序。把一個 user 和一篇 post 關聯(lián)起來的傳統(tǒng)方法是在 posts 中加入一個 userid 的列。這樣的模型中,如果要顯示 posts 就不得不讀取(連接)users。一種簡單可行的替代方案就是直接把 name 和 userid 存儲在 post中。您甚至可以用嵌入文檔來實現(xiàn),比如說 user: {id: ObjectId('Something'), name: 'Leto'}。當然,如果允許用戶更改他們的用戶名,那么每當有用戶名修改的時候,您就需要去更新所有的文檔了(這需要一個額外的查詢)。

          對一些人來說改用這種方法并非易事。甚至在一些情況下根本行不通。不過別不敢去嘗試這種方法:有時候它不僅可行,而且就是正確的方法。

          4.1.4 應該選擇哪一種?

          當處理一對多或是多對多問題的時候,采用id數(shù)組往往都是正確的策略。可以這么說,DBRef并不是那么常用,雖然您完全可以試著采用這項技術。這使得新手們在面臨選擇嵌入文檔還是手工引用(manual reference)時猶豫不決。

          首先,要知道目前一個單獨的文檔的大小限制是 4MB,雖然已經比較大了。了解了這個限制可以為如何使用文檔提供一些思路。目前看來多數(shù)的開發(fā)者還是大量地依賴手工引用來維護數(shù)據(jù)的關系。嵌入文檔經常被使用,but mostly for small pieces of data which we want to always pull with the parent document。一個真實的例子,我把 accounts 文檔嵌入存儲在用戶的文檔中,就像這樣:

          db.users.insert({name: 'leto', email: 'leto@dune.gov', account: {allowed_gholas: 5, spice_ration: 10}})

          這不是說您就應該低估嵌入文檔的作用,也不是說應該把它當成是鮮少用到的工具并直接忽略。將數(shù)據(jù)模型直接映射到目標對象上可以使問題變得更加簡單,也往往因此而不再需要連接操作。當您知道 MongoDB 允許對嵌入文檔的域進行查詢并做索引后,這個說法就尤其顯得正確了。

          4.2 集合:少一些還是多一些?

          既然集合不強制使用模式,那么就完全有可能用一個單一的集合以及一個不匹配的文檔構建一個系統(tǒng)。以我所見過的情況,大部分的 MongoDB 系統(tǒng)都像您在關系數(shù)據(jù)庫中所見到的那樣布局。換句話說,如果在關系數(shù)據(jù)庫中會用表,那么很有可能在 MongoDB 中就要用集合(多對多連接表在這里是一個不可忽視的例外)

          當把嵌入文檔引進來的時候,討論就會變得更加有意思了。最常見的例子就是博客系統(tǒng)。是應該分別維護 posts 和 comments 兩個集合,還是在每個 post 中嵌入一個 comments 數(shù)組?暫且不考慮那個 4MB 的限制(哈姆雷特所有的評論也不超過200KB,誰的博客會比他更受歡迎?),大多數(shù)的開發(fā)者還是傾向于把數(shù)據(jù)劃分開。因為這樣既簡潔又明確。

          沒有什么硬性的規(guī)定(呃,除了 4MB 的限制)。做了不同的嘗試之后您就可以憑感覺知道怎樣做是對的了。

          總結

          至此已經對 MongoDB 有了一個基本的了解和入門,但是要運用在實際的項目中仍然有許多實踐需要自己去完成

          - END -


          主站蜘蛛池模板: 中文无码AV一区二区三区| 日韩一区二区三区视频久久| 日亚毛片免费乱码不卡一区| 一区二区三区视频在线| 夜夜嗨AV一区二区三区| 色一情一乱一伦一区二区三区| 久久精品免费一区二区三区| 日本精品少妇一区二区三区| 福利电影一区二区| 日本视频一区二区三区| 国产高清在线精品一区小说| 国产精品成人一区二区三区| 久久精品免费一区二区三区 | 亚洲大尺度无码无码专线一区 | 亚洲国产精品一区二区九九| 天堂一区二区三区精品| 精品国产一区二区二三区在线观看| 在线免费视频一区二区| 国产伦精品一区二区免费| 国产一区二区三区在线看| 亚洲熟妇成人精品一区| 国产一区二区三区国产精品| 天天爽夜夜爽人人爽一区二区| 在线精品动漫一区二区无广告| 亚洲AV无码一区二区二三区入口 | 99精品高清视频一区二区| 免费萌白酱国产一区二区三区| 久久精品免费一区二区三区| 日韩av片无码一区二区不卡电影| 精品在线视频一区| 亚洲AV本道一区二区三区四区| 无码aⅴ精品一区二区三区| 国产天堂一区二区综合| 亚洲Av无码国产一区二区| 中文字幕无线码一区| 日韩视频在线一区| 在线精品视频一区二区| 国产精品特级毛片一区二区三区| 亚洲国产综合无码一区二区二三区| 亚洲毛片不卡av在线播放一区| 亚欧免费视频一区二区三区|