Ripple数据本地存储概览
1.数据文件及介绍
1.1 Sqlite数据
文件 | 包含表 | 内容 |
---|---|---|
Ledger.db | Ledgers | 区块信息 |
Ledger.db | Validations | 本地历史区块共识信息 |
Transaction.db | AccountTransactions | 账户交易表 |
Transaction.db | Transactions | 交易相关信息 |
Wallet.db | NodeIdentity | 存储当前节点的NodePublic与NodePrivate |
Wallet.db | PublisherManifests | 没什么用 |
Wallet.db | ValidatorManifests | 没什么用 |
1.2 序列化数据的存储
- NuDB 可用在各个平台上
- RocksDB 不可用在Windows平台上
2.数据结构
2.1 Sqlite表结构及说明
2.1.1 Ledgers
列名 | 类型 | 含义 |
---|---|---|
LedgerHash | CHARACTER | 哈希值 |
LedgerSeq | BIGINT UNSIGNED | Ledger序号 |
PrevHash | CHARACTER | 前个Ledger的Hash值 |
TotalCoins | BIGINT UNSIGNED | 当前网络上的XRP总数(交易会销毁XRP) |
ClosingTime | BIGINT UNSIGNED | 关闭时间 |
PrevClosingTime | BIGINT UNSIGNED | 前一个区块的关闭时间 |
CloseTimeRes | BIGINT UNSIGNED | ledger关闭时间的解决方案(2-120S) |
CloseFlags | BIGINT UNSIGNED | 标识这个ledger是怎么关闭的,一般都是0 |
AccountSetHash | CHARACTER | stateMap根结点hash |
TransSetHash | CHARACTER | txMap 根节点哈希 |
2.1.2 Validations
列名 | 类型 | 含义 |
---|---|---|
LedgerSeq | BIGINT UNSIGNED | Ledger序号 |
InitialSeq | BIGINT UNSIGNED | 共LedgerSeq一样 |
LedgerHash | CHARACTER | 共识过程中用到的LedgerHash |
NodePubKey | CHARACTER | 对ledger签名的节点公钥 |
SignTime | BIGINT UNSIGNED | 签名时间 |
RawData | BLOB | 与ledgerInfo类似的数据 |
2.1.3 AccountTransactions
列名 | 类型 | 含义 |
---|---|---|
TransID | CHARACTER | 交易hash |
Account | CHARACTER | 账户ID |
LedgerSeq | BIGINT UNSIGNED | ledger序号 |
TxnSeq | INTEGER | 交易Sequence号(是此账户的第几个交易) |
2.1.4 Transactions
列名 | 类型 | 含义 |
---|---|---|
TransID | CHARACTER | 交易hash |
TransType | CHARACTER | 交易类型 |
FromAcct | CHARACTER | 交易的发起账户 |
FromSeq | BIGINT UNSIGNED | 交易在账户中的序号 |
LedgerSeq | BIGINT UNSIGNED | 交易落在哪个区块上 |
Status | CHARACTER | 交易的状态V表示“共识过” |
RawTxn | BLOB | 交易序列化数据 |
TxnMeta | BLOB | 交易metaData的序列化数据 |
2.1.5 NodeIdentity
列名 | 类型 | 含义 |
---|---|---|
PublicKey | CHARACTER | 当前节点的NodePublic |
PrivateKey | CHARACTER | 储当前节点的NodePrivate |
2.1.6 PublisherManifests
列名 | 类型 | 含义 |
---|---|---|
RawData | BLOB |
2.1.7 ValidatorManifests
列名 | 类型 | 含义 |
---|---|---|
RawData | BLOB |
2.2 序列化数据
2.2.1 Ripple的数据序列化
以LedgerInfo为例:
void addRaw (LedgerInfo const& info, Serializer& s)
{s.add32 (info.seq);s.add64 (info.drops.drops ());s.add256 (info.parentHash);s.add256 (info.txHash);s.add256 (info.accountHash);s.add32 (info.parentCloseTime.time_since_epoch().count());s.add32 (info.closeTime.time_since_epoch().count());s.add8 (info.closeTimeResolution.count());s.add8 (info.closeFlags);
}int Serializer::add16 (std::uint16_t i)
{int ret = mData.size ();mData.push_back (static_cast<unsigned char> (i >> 8));mData.push_back (static_cast<unsigned char> (i & 0xff));return ret;
}
2.2.2 存到NuDB的数据及序列化
<big>需要序列化的数据分三种类型</big>
/** The types of node objects. */
enum NodeObjectType
{hotUNKNOWN = 0,hotLEDGER = 1,//hotTRANSACTION = 2 // Not usedhotACCOUNT_NODE = 3,hotTRANSACTION_NODE = 4
};
- LedgerInfo (对应hotLEDGER)
- LedgerSeq,LedgerHash等,具体可参见Ripple官网
- 区块之间是通过LedgerHash与PrevHash(上一个区块哈希)来产生顺序关联的
- LedgerInfo的序列化代码:
// Save the ledger header in the hashed object store {Serializer s (128);s.add32 (HashPrefix::ledgerMaster);addRaw(ledger->info(), s);app.getNodeStore ().store (hotLEDGER, std::move (s.modData ()), ledger->info().hash); }
- StateMap (包括各种SLE信息)
- SLE是STLedgerEntry的简写,是Ripple自定义的一种数据结构
- SLE种类:Account、Escrow、Fee、Amendment、PayChannel等
- 其中有一个比较重要的SLE是skipList,它包含前面256个区块的ledgerHash,这个SLE在无交易的情况下占用空间最大,每个区块hash占32字节,256个就是8k大小
- StateMap序列化代码:
void Ledger::rawInsert(std::shared_ptr<SLE> const& sle) {Serializer ss;sle->add(ss);auto item = std::make_shared<SHAMapItem const>(sle->key(),std::move(ss));// VFALCO NOTE addGiveItem should take ownershipif (! stateMap_->addGiveItem(std::move(item), false, false))LogicError("Ledger::rawInsert: key already exists"); }
- TxMap (交易信息)
- 包含交易信息txnData与交易的元数据metaData(包含交易影响的结构等)
- 最终txnData与metaDat合到一块存储,也叫metaData
- TxMap的序列化代码
void Ledger::rawTxInsert (uint256 const& key,std::shared_ptr<Serializer const> const& txn, std::shared_ptr<Serializer const> const& metaData) {assert (metaData);// low-level - just add to tableSerializer s(txn->getDataLength () +metaData->getDataLength () + 16);s.addVL (txn->peekData ());s.addVL (metaData->peekData ());auto item = std::make_shared<SHAMapItem const> (key, std::move(s));if (! txMap().addGiveItem(std::move(item), true, true))LogicError("duplicate_tx: " + to_string(key)); }
<big>说明:</big>
- StateMap与TxMap都是SHAMap类型的结构
- SHAMap既是基数树(Radix Tree)同时也是默克尔树(Merkle Tree)
- SHAMap中包含 SHAMapTreeNode 与 SHAMapInnerNode 两种节点类型
- SHAMapTreeNode SHAMap的非叶子节点
- SHAMapInnerNode SHAMap的叶子节点
- StateMap中的叶子节点对应hotACCOUNT_NODE,txMap中的叶子节点对应hotTRANSACTION_NODE类型
- SHAMap分支构造及查找算法代码:
最开始都是从root_节点找起,根据hash选择分支:
// Which branch would contain the specified hash
int SHAMapNodeID::selectBranch (uint256 const& hash) const
{int branch = * (hash.begin () + (mDepth / 2));if (mDepth & 1)branch &= 0xf;elsebranch >>= 4;assert ((branch >= 0) && (branch < 16));return branch;
}
如果对应分支没有节点,则直接插入
// easy case, we end on an inner node
auto inner = std::static_pointer_cast<SHAMapInnerNode>(node);
int branch = nodeID.selectBranch (tag);
assert (inner->isEmptyBranch (branch));
auto newNode = std::make_shared<SHAMapTreeNode> (item, type, seq_);
inner->setChild (branch, newNode);
否则,构造一个InnerNode,并连同原来的叶子节点一起作为新InnerNode的叶子节点
auto leaf = std::static_pointer_cast<SHAMapTreeNode>(node);
auto inner = std::make_shared<SHAMapInnerNodeV2>(seq_);
inner->setChildren(leaf, std::make_shared<SHAMapTreeNode>(item, type, seq_));
assert(!stack.empty());
auto parent = unshareNode(std::static_pointer_cast<SHAMapInnerNodeV2>(stack.top().first),stack.top().second);
stack.top().first = parent;
node = inner;
setChildren
void
SHAMapInnerNodeV2::setChildren(std::shared_ptr<SHAMapTreeNode> const& child1,std::shared_ptr<SHAMapTreeNode> const& child2)
{assert(child1->peekItem()->key() != child2->peekItem()->key());auto k1 = child1->peekItem()->key().begin();auto k2 = child2->peekItem()->key().begin();auto k = common_.begin();for (depth_ = 0; *k1 == *k2; ++depth_, ++k1, ++k2, ++k)*k = *k1;unsigned b1;unsigned b2;if ((*k1 & 0xF0) == (*k2 & 0xF0)){*k = *k1 & 0xF0;b1 = *k1 & 0x0F;b2 = *k2 & 0x0F;depth_ = 2*depth_ + 1;}else{b1 = *k1 >> 4;b2 = *k2 >> 4;depth_ = 2*depth_;}mChildren[b1] = child1;mIsBranch |= 1 << b1;mChildren[b2] = child2;mIsBranch |= 1 << b2;
}
序列化数据存储相关类图:
![](http://upload-images.jianshu.io/upload_images/1938630-91c04e8f89746a2b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/583)
序列化相关调用堆栈:
LedgerConsensusImp<Traits>::beginAccept ->
LedgerConsensusImp<Traits>::accept(855) ->
OpenView::apply ->
Ledger::rawReplace/Ledger::rawInsert(序列化) ->
SHAMap::addGiveItem/SHAMap::updateGiveItem
2.3 序列化数据存储到NuDB过程
- NodeObject的hash(uint256)元素取前4个字节得到key
- NodeObject 的 data经过lz4压缩算法压缩得到dataSize与dataCompressed
- dataSize,key,dataCompressed写入到文件
3.从DB中查找交易、区块、账户信息
3.1 查找区块信息
根据区块序号查找获得区块信息,如 ledger_data 命令:
- 直接从Ledger表读取LedgerHash或者读取SkipList获取LedgerHash(只对当前最大区块前256个区块或256整数倍的区块有效)
- 读取Ledger表,查找LedgerHash对应的记录信息并构造LedgerInfo
- 使用LedgerInfo及从配置文件中读取的db,nudb配置构造Ledger对象
- Ledger构造函数中
- 根据LedgerInfo中的初始化txMap
- 根据accountHash初始化stateMap
- 初始化Map过程中需要读取NuDB文件获取到root_节点信息
- 区块读取完毕
{"result": {"ledger": {"accepted": true,"account_hash": "EA4088D9B6FF34DA6102E4F6BCEC96DF860CF8006E66B544EEAD784635887514","close_flags": 0,"close_time": 561191060,"close_time_human": "2017-Oct-13 06:24:20","close_time_resolution": 20,"closed": true,"hash": "28980C1C7C407CE32FAE8A94984AEB1B69837F3F7F532F02D8773DAC7F218784","ledger_hash": "28980C1C7C407CE32FAE8A94984AEB1B69837F3F7F532F02D8773DAC7F218784","ledger_index": "8","parent_close_time": 561191044,"parent_hash": "C1EFB978755E1011376C4DEFA7B61BA3834C4F218B85DC4718F0C77A0CB446B2","seqNum": "8","totalCoins": "99999999999999990","total_coins": "99999999999999990","transaction_hash": "35098D46F21556B3496DC8409CD1F51AEA9568B6935D43736EB046278747769B"},"ledger_hash": "28980C1C7C407CE32FAE8A94984AEB1B69837F3F7F532F02D8773DAC7F218784","ledger_index": 8,"state": [{"Account": "rBuLBiHmssAMHWQMnEN7nXQXaVj7vhAv6Q","Balance": "10000000000","Flags": 0,"LedgerEntryType": "AccountRoot","OwnerCount": 0,"PreviousTxnID": "22F555EFF4F67BFF08C3AFBF00C830EFFCAE33C8C57F5DF1D471618E1AB3F4CC","PreviousTxnLgrSeq": 8,"Sequence": 1,"index": "079B5765FF6A6AD78F2C72D3CF6A96C6F862A5FE550567BB8CE3B31223D36A99"},{"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","Balance": "99999989999999990","Flags": 0,"LedgerEntryType": "AccountRoot","OwnerCount": 0,"PreviousTxnID": "22F555EFF4F67BFF08C3AFBF00C830EFFCAE33C8C57F5DF1D471618E1AB3F4CC","PreviousTxnLgrSeq": 8,"Sequence": 2,"index": "2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8"},{"Flags": 0,"Hashes": ["AB868A6CFEEC779C2FF845C0AF00A642259986AF40C01976A7F842B6918936C7","EF422714FD6900B2F5D69CA543FC3C67091E4178F514894CCDF11E30570FA90C","07EAC861B72B4F29F4825F695A203BAAD414E4A02DDAE7DB5C5F8B0B8A5ADFAE","847EA2FBAAB54AE6D084AC6A42DF1582907E0BC11C52A4E7D36F5A6424E832BA","69C031109212CB336A6E5A0D8BAB03E7A185711C48376EAFAC6B0458261D0CAF","1D3B71E935B8922E7FAD2D3E18EC36FD6884E1DBC6DE965A2BCFC27057DCAF68","C1EFB978755E1011376C4DEFA7B61BA3834C4F218B85DC4718F0C77A0CB446B2"],"LastLedgerSequence": 7,"LedgerEntryType": "LedgerHashes","index": "B4979A36CDC7F3D3D5C31A4EAE2AC7D7209DDA877588B9AFC66799692AB0D66B"}],"status": "success","validated": true}
}
<big>注:</big>
- transaction_hash 不为空说明此Ledger上存在交易
- state字段为此Ledger中包含的SLE信息
3.2 查找帐户信息
给出帐户地址,查找帐户信息,如account_info命令
- 查找参数列表中有无ledger_index参数,如果有,去获取对应的Ledger,参考3.1,如果没有,去获取最新共识过的Ledger
- 用1中获取到的Ledger去读取account参数对应的SLE节点信息,读取过程通过Ledger中的stateMap结构去查找SLE的key对应的叶子节点
- 根据Ledger信息与得到的SLE构造返回结果
{"result": {"account_data": {"Account": "rBuLBiHmssAMHWQMnEN7nXQXaVj7vhAv6Q","Balance": "10000000000","Flags": 0,"LedgerEntryType": "AccountRoot","OwnerCount": 0,"PreviousTxnID": "22F555EFF4F67BFF08C3AFBF00C830EFFCAE33C8C57F5DF1D471618E1AB3F4CC","PreviousTxnLgrSeq": 8,"Sequence": 1,"index": "079B5765FF6A6AD78F2C72D3CF6A96C6F862A5FE550567BB8CE3B31223D36A99"},"ledger_hash": "50EE9E422B7591BD0B7EE7126FAFF8437F785D137DEB60E3BD98294CBA1175D1","ledger_index": 14,"status": "success","validated": true}
}
3.3 查找交易信息
给出交易Hash,查找交易详情的过程,如tx命令
- 根据交易Hash,从Transaction表中读取交易信息得到Transaction对象,其中,RawTxn字段内容可以构造原始交易信息,LedgerSeq字段标识交易落在哪个区块上,Status字段标识交易的共识状态,V表示共识通过
- 根据Transaction表读取到的LedgerSeq去读取Ledger信息,参考3.1
- 根据读取到的Ledger信息去查对应txMap中Hash对应的Node,Node的中包含txnData与metaData
- 将1中得到的Transaction对象与3中得到的metaData构造json,返回json,结束
{"result": {"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","Amount": "10000000000","Destination": "rBuLBiHmssAMHWQMnEN7nXQXaVj7vhAv6Q","Fee": "10","Flags": 2147483648,"Sequence": 1,"SigningPubKey": "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020","TransactionType": "Payment","TxnSignature": "30440220249D1FA7DBB71A0EAE07AF84289136FB8F4B16B774E255A8E2B41F08EB7EEA5302201566B52AD408760EC940E8930A1402CCF0F3CCBCBDDAF356D9D399EDFA4F89B1","date": 561191060,"hash": "22F555EFF4F67BFF08C3AFBF00C830EFFCAE33C8C57F5DF1D471618E1AB3F4CC","inLedger": 8,"ledger_index": 8,"meta": {"AffectedNodes": [{"CreatedNode": {"LedgerEntryType": "AccountRoot","LedgerIndex": "079B5765FF6A6AD78F2C72D3CF6A96C6F862A5FE550567BB8CE3B31223D36A99","NewFields": {"Account": "rBuLBiHmssAMHWQMnEN7nXQXaVj7vhAv6Q","Balance": "10000000000","Sequence": 1}}},{"ModifiedNode": {"FinalFields": {"Account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","Balance": "99999989999999990","Flags": 0,"OwnerCount": 0,"Sequence": 2},"LedgerEntryType": "AccountRoot","LedgerIndex": "2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8","PreviousFields": {"Balance": "100000000000000000","Sequence": 1}}}],"TransactionIndex": 0,"TransactionResult": "tesSUCCESS","delivered_amount": "10000000000"},"status": "success","validated": true}
}
<big>注:</big>
其实Transaction表中也存有metaData字段,不知道这里为什么要读文件去取
3.4 查找帐户交易信息
查找一个帐户下的交易,可限定交易数量,最大一次查找200个交易
{"method": "account_tx","params": [{"account": "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","ledger_index_max": 1,"ledger_index_min": 1000,"limit": 20}]
}
这个过程完全从SqliteDB中进行,未涉及NuDB
- 从AccountTransactions表查找交易
- 根据查询结果构造交易信息
- 每个交易添加 validated:true,条件是给出的ledger_index_min 与ledger_index_max范围是当前共识过区块范围的子集
SELECT AccountTransactions.LedgerSeq,AccountTransactions.TxnSeq,Status,RawTxn,TxnMetaFROM AccountTransactions INNER JOIN TransactionsON Transactions.TransID = AccountTransactions.TransIDAND AccountTransactions.Account = '%s' WHEREAccountTransactions.LedgerSeq BETWEEN '%u' AND '%u'ORDER BY AccountTransactions.LedgerSeq DESC,AccountTransactions.TxnSeq DESCLIMIT %u;
返回结果
{"id" : 1,"result" : {"account" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","ledger_index_max" : 958,"ledger_index_min" : 1,"status" : "success","transactions" : [{"meta" : {"AffectedNodes" : [{"ModifiedNode" : {"FinalFields" : {"Account" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","Balance" : "99999989999999990","Flags" : 0,"OwnerCount" : 0,"Sequence" : 2},"LedgerEntryType" : "AccountRoot","LedgerIndex" : "2B6AC232AA4C4BE41BF49D2459FA4A0347E1B543A4C92FCEE0821C0201E2E9A8","PreviousFields" : {"Balance" : "100000000000000000","Sequence" : 1}}},{"CreatedNode" : {"LedgerEntryType" : "AccountRoot","LedgerIndex" : "81843E2DE3A90BADB1CA75B3C3781CFC72BDFD1584CA893240F317A7003FC93F","NewFields" : {"Account" : "rwqbtoxtmwEzCatTocFW8TcP3DYU18GtGg","Balance" : "10000000000","Sequence" : 1}}}],"TransactionIndex" : 0,"TransactionResult" : "tesSUCCESS","delivered_amount" : "10000000000"},"tx" : {"Account" : "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh","Amount" : "10000000000","Destination" : "rwqbtoxtmwEzCatTocFW8TcP3DYU18GtGg","Fee" : "10","Flags" : 2147483648,"Sequence" : 1,"SigningPubKey" : "0330E7FC9D56BB25D6893BA3F317AE5BCF33B3291BD63DB32654A313222F7FD020","TransactionType" : "Payment","TxnSignature" : "30450221008BC728BB5F58FE91E109BBEF1B3F098E17E49EF91EEF12CC7EB065483CF056700220450C9382096F3A2EEC8F054B47C48A91035985429C3F7594D9176D881B7AB7D4","date" : 560931910,"hash" : "3395C1AC406DB2974B72C1811008F8959D953802B88CB22E5B0231D6B0F8CE54","inLedger" : 20,"ledger_index" : 20},"validated" : true}]}
转自简书作者:SwordShield
博主QQ: 122209017
Ripple数据本地存储概览相关推荐
- 数据本地存储方法封装(笔记)localStorage、sessionStorage
数据本地存储方法封装(笔记)localStorage.sessionStorage 方法: import storage from 'good-storage'const SELLER_KEY = ' ...
- C#数据本地存储方案之SQLite
即使是做网络应用,在断线情况下,也需要考虑数据的本地存储.在SQLite出现之前,数据量大的情况下,我们一直使用ACCESS,数据量小,则文件存储.ACCESS不支持事务原子性,在断电情况下(这种情况 ...
- IOS数据本地存储的四种方式--
注:借鉴于:http://blog.csdn.net/jianjianyuer/article/details/8556024 在IOS开发过程中,不管是做什么应用,都会碰到数据保存问题.将数据保存到 ...
- vue存储数据的几种方法(Vuex与本地存储)
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 vue存储数据的几种方法(Vuex与本地存储) 前言 一.vuex 1.创建vuex 2.存入数据 3.取出数据 二.本地存储 1.存 ...
- 关于Unity中的本地存储
本地存储 在做游戏的时候,经常需要在本机存储一些数据,比如闯关类游戏要记录闯到第几关,做单机的时候要把数据保存到本地,下次启动的时候数据存在,就是把数据保存到磁盘里面或者手机的flash闪存里面. U ...
- 前端学习(1043):回车把数据存储到本地存储里面
<!DOCTYPE html> <html lang="en"><head><meta charset="UTF-8" ...
- ionic android 本地存储,ionic2/3本地数据存储storage
ionic2开始storage默认使用的是IndexedDB,而不是LocalStorage 存储 存储是存储键/值对和JSON对象的简单方法.存储使用下面的各种存储引擎,根据平台选择最佳的存储引擎. ...
- linux追加SQL结果到文件,RAC环境下误操作将数据文件添加到本地存储
今天碰到个有意思的事情,有客户在Oracle RAC环境,误操作将新增的数据文件直接创建到了其中一个节点的本地存储上.发现网上去搜的话这种问题还真不少,对应解决方案也各式各样,客户问我选择哪种方案可行 ...
- 【本地存储】将数据存储到本地 (sessionStorage、vuex)
数据存储本地 sessionStorage.setItem("nm",info.nm); //第一参数是key,第二个参数是valsessionStorage.setItem(&q ...
最新文章
- 我的世界java刷怪数量_我的世界Minecraft源码分析(1):刷怪逻辑
- 上海交通大学c语言章节作业,上海交通大学级C语言测试题.doc
- go interface转int_图解go反射实现原理
- MySQL从入门到精通50讲(一)-MySQL数据库操作创建数据库及删除数据库
- [系统安全] 二十三.逆向分析之OllyDbg动态调试复习及TraceMe案例分析
- 【批处理】windows环境将文件放置在虚拟盘
- sql查询月天数之和,函数相加
- windows server 2003 IIS 调试 ASP时路径问题
- oracle11g怎样进行闪回,模拟Oracle11g下用Flashback Data Archive进行恢复的若干场景
- 时间管理:战略时间块,缓冲时间块,逃离时间块
- CA SDK 使用简介
- 海康摄像头使用RTSP
- 汇编篇 :关于地址总线与数据总线的换算
- 测试用例设计正交试验法、功能图法
- 2022年上半年技术领域TOP 10高薪岗位出炉,第一名月薪4万
- 机器人图形变变变_中班数学活动——图形变变变 教案
- Python:实现jaccard similarity相似度无平方因子数算法(附完整源码)
- 用excel替换word里的文字,deepcopy
- 前端自动化 Jenkins/TravisCI/CiecleCi
- 【技术类】【ArcGIS对国产卫星的支持2:高分一号卫星】篇2、高分一号(GF-1)卫星影像数据介绍