1. MongoDB数据导出导入:先搞清楚你在哪个场景
做MongoDB开发和运维的这些年,我几乎每周都要和数据导出导入打交道。不管是把测试库的数据搬到本地、给客户交付一批脱敏后的样本数据,还是某个集合误删了需要从备份里捞回来,最终都会落到一个基本动作上:把一个库、一个集合,甚至几条文档,从一个环境弄到另一个环境。
很多人一开始都会纠结一个问题:导出导入到底用哪套命令?有mongoexport和mongoimport,还有mongodump和mongorestore,有时候还要加上mongosh里的find循环写入。光看名字好像都能干这件事,但实际上这四组工具有完全不同的适用场景。用错了,轻则导出的文件没法用,重则把整个库的数据结构弄乱,恢复的时候才知道什么叫痛苦。
在开始讲具体操作之前,我想先说一个经常被忽略的观点:MongoDB的“导出导入”从来不是一个单一动作,而是一套围绕数据形态变化的流程。你需要先想清楚三件事——数据要到哪里去、中间以什么格式存在、到了目的地之后要不要保持原来的类型。这三个问题定了,工具选型、参数配置、坑点规避就全部有方向了。
这篇文章我就基于实际项目里反复用到的场景,把MongoDB数据导出导入的完整链路拆开讲。会覆盖最近几个版本里命令行为和参数的变化、每个命令的真实适用边界、我在生产环境踩过的坑,以及可以直接抄走的操作方案。适合正在做数据迁移、环境初始化、备份恢复的开发者,也适合刚接触MongoDB但是不想在数据搬运上栽跟头的新手。
2. 导出导入工具选型:四种常用手段的真实边界
2.1 为什么会有“两套”导入导出方案
MongoDB官方提供的导入导出工具,其实一直在迭代。最早的时候,大家最熟悉的是mongoexport和mongoimport这对组合,它们处理的是人类可读的JSON、CSV文件。后来随着备份恢复需求越来越重,mongodump和mongorestore逐渐成了标配,因为它们产出的BSON文件是MongoDB内部存储格式的直接体现。
这两套方案并不是竞争关系,它们在设计上就是解决不同问题的。如果你要把数据交给别的系统去用,比如给数据分析团队一份CSV、给前端联调用一份JSON,那mongoexport就是最顺手的工具。如果你要做的是“把MongoDB的一个库完整搬去另一个环境”,甚至连索引、校验规则都要一并带走,那mongodump才是正确选择,因为JSON格式根本不保留索引定义,导进来还得重建。
我见过不少人在刚开始接触MongoDB的时候,习惯性用mongoexport导出全部集合,然后拿JSON文件去另一台机器上mongoimport。小数据量跑起来没问题,到单集合超过几千万条的时候就会发现:导出时间长、文件体积大、导入速度慢、索引丢失。这个问题的根源不是命令写得不对,而是工具本身选错了。
2.2 四种工具的能力边界对比
为了让选择更直观,我把实际项目中反复验证过的能力差异整理成了表格。
| 对比维度 | mongoexport / mongoimport | mongodump / mongorestore |
|---|---|---|
| 产出格式 | JSON、CSV | BSON(二进制JSON) |
| 是否保留索引 | 否,需要重建 | 是,备份时同步保存 |
| 是否保留用户权限 | 否 | 备份admin库时包含 |
| 单集合 vs 全库 | 主要面向单集合操作 | 全库、单库、单集合均可 |
| 数据量级 | 适合千万级以内的灵活筛选 | 亿级以上也能稳定处理 |
| 可读性 | 好,JSON/CSV可以直接打开看 | 差,二进制文件无法直接阅读 |
| 常用场景 | 数据交换、统计报表、定向导出 | 备份恢复、环境迁移、灾难重建 |
| 速度与资源占用 | 较慢,JSON解析开销高 | 更快,BSON更接近存储层格式 |
有一个细节值得重点关注:mongoexport导出的JSON文件里,_id字段会被保留为ObjectId结构,但如果你用了mongoexport给其他编程语言消费数据,比如Java后端、Python脚本,那么处理ObjectId就麻烦得要命。很多人在这一步踩坑,导出之后才发现JSON里面套了一层{"$oid": "..."},和自己预期的普通字符串完全不同。
2.3 版本差异带来的选择困惑
MongoDB的导出导入命令在不同的版本里有几个容易被忽略的变化。比如在5.0之前的版本里,mongodump和mongorestore还是随数据库发行版一起打包的独立程序。而从5.0开始,MongoDB把数据库工具抽成了独立的MongoDB Database Tools,需要单独下载。这就导致了一个很尴尬的情况:装了数据库本体,用mongosh连库正常,但执行mongodump --version却提示找不到命令。
所以在新手上路阶段,第一步其实是检查自己的工具链是否完整。执行mongodump --version和mongoexport --version看有没有正常输出版本信息。如果没有,去MongoDB官网下载对应版本的Database Tools,解压后把可执行文件加入系统PATH。这个问题不解决,后面的所有操作都无从谈起。
还有一点,老的mongoshell已经全面退役了,现在统一用mongosh。如果你在网上搜到的教程还在用mongo -host xxx,那大概率是老古董文章,操作方式已经不适配当前环境了。
3. mongoexport与mongoimport:灵活数据交换的正确打开方式
3.1 mongoexport核心参数拆解
mongoexport是我在需要“从MongoDB里抠出一批数据交给别人”时最常用的命令。它的基本结构很简洁,但参数组合起来能完成非常多定向操作。先来看一个实际例子。
mongoexport \ --uri="mongodb://user:pass@127.0.0.1:27017/admin" \ --db=shop \ --collection=orders \ --query='{"status": "paid", "pay_time": {"$gte": ISODate("2024-01-01T00:00:00Z")}}' \ --fields="_id,order_no,user_name,amount,pay_time" \ --type=json \ --out=/data/export/paid_orders.json这个命令做的事情是:连接指定MongoDB,在shop库的orders集合里,筛选出状态为paid且支付时间在2024年1月1日之后的所有订单,只保留_id、order_no、user_name、amount、pay_time这五个字段,导出为JSON文件。
其中--query参数是灵活性最强的部分。它接收的是一段MongoDB查询条件语法,可以加比较运算符、逻辑运算符、正则表达式、嵌套字段条件,几乎和db.collection.find()里写过滤器一样强大。但有个非常隐蔽的坑:在老版本中,--query里的日期对象必须写ISODate("..."),在较新版本的工具中又支持了ISODate写法,但如果你用的是新工具配合老库,有些参数格式会有兼容问题,所以建议在导出之前先小范围试一下。
--fields参数用来做字段裁剪。这里有几个容易被忽略的规则:
- 如果你传的是逗号分隔的字段列表,比如
_id,order_no,user_name,那就只导出这几个字段。 - 如果你写
_id,order_no,-user_name,表示排除指定字段。 - 如果是JSON格式并且没有传
--fields,就导出整个文档的所有字段。
导入到别的系统时,如果对方只需要部分字段,那你应该在导出阶段就完成裁剪,而不是等到下游再去处理。这能显著降低文件体积和下游解析成本。
3.2 CSV格式导出有哪些隐藏规则
CSV格式在导出报表类数据时非常好用,因为可以直接用Excel打开,给运营同事、数据分析同事都很方便。但CSV导出有两个非常折磨人的点。
第一个是字段顺序。CSV没有自描述能力,必须依赖第一行表头来约定每列的含义。如果用mongoexport --type=csv --fields=user_name,amount导出,那么文件的第一行就是user_name,amount。如果下游解析脚本写死了列顺序,建议在导出命令里明确指定--fields的顺序,不要指望数据库按文档字段顺序输出,因为文档内字段顺序在不同写入方式下可能完全不一致。
第二个是嵌套结构的处理。MongoDB的文档天然支持嵌套对象和数组,但CSV只有扁平的表格结构。当你导出含有嵌套字段的文档时,mongoexport会把整个子对象序列化成一个JSON字符串塞进单元格里。表面上能导出成功,但下游用SQL或者Pandas读取的时候,那些单元格里的JSON字符串就很尴尬。我通常的做法是,利用--fields里的点号语法直接展开嵌套字段,比如--fields="_id,user.profile.name,user.profile.age",这样导出后的CSV就是扁平化的多列,而不是一个嵌套JSON字符串。
另外,数组字段几乎是CSV的天敌。如果一个字段的值是数组并且长度不固定,那么CSV无法天然表示多值。实际项目里,如果导出时发现有数组字段,我一般会单独做一步展开,把数组元素拼成一个分隔符间隔的字符串,或者拆成多行再合并。这一步没有现成的命令参数支持,通常需要用脚本来配合。
3.3 mongoimport导入时的类型转换陷阱
导入方向和导出方向一样有各种坑。最典型的例子就是数字类型的退化。在MongoDB里,整数42会被存储为一个Int32,而mongoexport导出为JSON后,它就是一个普通的42。但如果你用文本编辑器打开,把它改成了带引号的"42",再mongoimport回去,MongoDB就会把它存成字符串"42"。这个变化对业务逻辑的影响很大:前端判断== 42也许还能通过,但如果用了严格等于=== 42,或者数据库聚合里的$sum,结果就完全不对了。
还有一个同样常见的问题是日期字符串。导出的BSON Date在JSON里会变成{"$date": "2024-03-15T08:00:00Z"}这种扩展JSON格式。如果你手动修改过,把它改成了"2024-03-15 08:00:00"这种普通字符串,导入之后就是一个没有任何语义的字符串,而不是MongoDB的Date类型。后续按时间范围查询,或者做聚合下钻,全部都会出问题。
为了规避这些类型问题,我的经验是:如果不是必须人工编辑,始终用mongodump的BSON方案来搬运数据;如果只能用JSON,那么就用脚本对导出文件做完整的类型检查,不要直接手动处理大文件。
3.4 mongoimport的mode参数与数据覆盖策略
mongoimport的核心参数--mode控制着导入时遇到相同_id记录的处理方式,取值有三个:insert、upsert、merge。
insert:默认模式。如果集合里已经存在相同_id的文档,直接报错,默认行为是跳过还是终止取决于--stopOnError。upsert:如果_id存在则更新整条文档,不存在则插入。适合增量同步场景。merge:只更新目标文档中与导入文档重叠的字段,原文档中的其他字段保留不动。适合修补部分字段值的场景。
我在实际项目里使用merge模式最多的一次,是帮业务线修复一批历史数据的tag字段。当时只需要往每条订单文档上追加一个标记字段,不能覆盖原有字段,于是导入文件里只放_id和tag两个字段,用--mode=merge执行。这样既避免了把所有数据全量覆盖,也避免了逐条写脚本更新的低效。
不过merge模式有一个需要注意的地方:如果原文档里的某个字段被导入文件显式设置为null,最终该字段会被置空,并不会自动忽略null值。所以在构造导入文件的时候,要把所有不打算修改的字段全部排除掉,只保留需要变更的字段。
4. mongodump与mongorestore:备份恢复场景里的主力方案
4.1 为什么备份恢复必须用BSON格式
mongodump和mongorestore这套组合,本质上就是MongoDB官方的物理逻辑备份与恢复方案。它产出的BSON文件是MongoDB存储引擎使用的二进制JSON格式,保留了文档原始的数据类型、内嵌结构以及可选的索引定义。
和JSON文字文件相比,BSON的优势非常明显:
- 类型保真。日期就是日期,ObjectId就是ObjectId,不会出现导出再导入后类型丢失的问题。
- 索引信息同步备份。如果导出的是整个库,那么各集合的索引也会一并备份到
metadata.json文件里,恢复的时候可以一并重建。 - 体积更小。BSON是二进制的,比可读文本的JSON紧凑很多,大文件场景下导出的时间更短、磁盘占用更少。
- 恢复效率高。
mongorestore在批量插入时能够利用内部更高效的写入路径,在很多实测场景中比mongoimport快数倍。
这些优点组合起来的核心结果就是:只要涉及完整的数据库迁移、环境重建、备份恢复,mongodump就是最安全的选择。就算你只想导出单集合,我也建议优先用mongodump --collection,而不是mongoexport,除非下游真的需要一份可读的JSON或CSV。
4.2 全库备份到恢复的标准命令实操
我平时做全库迁移最常用的是这种写法。
mongodump \ --uri="mongodb://backup_user:password@127.0.0.1:27017/admin" \ --db=shop \ --gzip \ --archive=/data/backup/shop_20250115.archive这里用了--gzip加--archive的组合。--archive会把所有备份内容写成一个独立归档文件,避免生成一长串的文件夹和文件结构;配合--gzip可以在归档的同时完成压缩,备份文件体积能减少到原来的三分之一甚至更少。当然,压缩也需要额外的CPU开销,如果数据库机器本身压力很高,可以先把--gzip拿掉,落盘后再用系统工具压缩。
恢复的时候同样很简单:
mongorestore \ --uri="mongodb://target_user:password@192.168.1.100:27017/admin" \ --gzip \ --archive=/data/backup/shop_20250115.archive \ --drop--drop参数非常关键,它的意思是恢复前先删除目标集合中已有的数据。如果没有加--drop,恢复时默认是往已有集合里追加数据,如果集合里已经存在相同_id的记录,就会产生冲突。我在恢复环境时通常都带--drop,保证恢复后的数据和备份完全一致。
如果目标库不存在,mongorestore会自动创建数据库和集合,不需要提前建库,这点对新手很友好。对于已经存在目标集合的情况,--drop的行为是直接删掉现有的集合再重建,而不是一条一条去重,所以速度也会更快。
4.3 过滤备份:只备份需要的子集
mongodump虽然重点是全量备份,但它同样支持按条件过滤部分数据。有一个参数--query可以作用于指定集合的过滤备份。
mongodump \ --uri="mongodb://user:pass@127.0.0.1:27017/admin" \ --db=shop \ --collection=orders \ --query='{"status": "cancelled", "cancel_time": {"$gte": ISODate("2023-01-01T00:00:00Z")}}' \ --out=/data/backup/cancelled_orders这个场景适合冷数据归档。比如把三年前取消的订单单独备份出来,然后从生产库中删除,减少在线库的存储压力。注意这里的--query和mongoexport一样支持MongoDB查询语法,但在多个版本里对日期的写法兼容性不同,使用前最好先小范围验证。
4.4 mongorestore的性能调优参数
当恢复的数据量特别大的时候,mongorestore有一些参数可以显著提升速度,但也要付出一定的代价。我在恢复几十亿条数据的场景里常用到以下配置:
mongorestore \ --uri="mongodb://user:pass@127.0.0.1:27017/admin" \ --gzip \ --archive=/data/backup/huge.archive \ --drop \ --numInsertionWorkersPerCollection=4 \ --maintainInsertionOrder=false \ --writeConcern=0--numInsertionWorkersPerCollection:每个集合启用多少个并发写入线程。增加这个值可以提升写入吞吐,但也会让目标库的CPU和磁盘IO压力显著上升。恢复时建议监控目标库的系统负载,过高的并发有可能把集群节点压垮。--maintainInsertionOrder=false:关闭插入顺序保证。因为外部按BSON存储顺序读取时,本来就是按_id索引顺序遍历的,乱序插入对最终数据没有影响。--writeConcern=0:写入不等待确认。这是最激进的选择,只在恢复可容忍数据丢失的临时环境时使用,生产环境恢复绝对不能这么干。
这些参数组合起来,在一些大集群恢复场景里可以把整体耗时缩短超过40%。但也别盲目堆并发,建议先在小范围数据上测试,再逐步加到标准配置。
5. 生产经验:大集合导出导入的进阶策略
5.1 全量导出会拖垮生产库怎么办
很多初入行的同学第一次在生产环境执行全量导出的时候,都会遇到一个很直观的后果:业务响应变慢了,监控曲线出现毛刺。这是因为全量导出本质上是一次大范围的集合扫描,会消耗大量的IO和内存。
我的建议是:除非数据量很小,否则不要在业务高峰期直接对生产库执行全量导出。更合理的做法是使用从节点进行导出。在复制集架构中,mongoexport和mongodump都支持指定readPreference=secondary,让命令读取从节点数据,从而避免给主节点增加压力。
mongodump \ --uri="mongodb://user:pass@host1:27017,host2:27017,host3:27017/admin?replicaSet=rs0&readPreference=secondary" \ --gzip \ --archive=/data/backup/from_secondary.archive这一招在实际生产环境非常实用。从节点上执行备份,相当于把备份的压力从主节点剥离了出去,对业务的影响几乎降到了零。副作用是如果是从节点读取,备份的数据可能存在轻微延迟,所以对实时性要求不高的备份场景完全适用。
如果整个复制集所有节点的负载都不低,还可以考虑把--archive写到本地磁盘,避免通过网络写远程文件。有一些新版本工具支持--archive配合FIFO或者管道的方式二次转发,但普通场景下直接写本地磁盘最省事。
5.2 超大数据集的分批导出思路
当集合规模大到单次导出把内存和磁盘都干满的时候,就要改变策略了。分批导出的思路是:利用一个业务上有意义的时间字段作为切分点,把导出范围拆成若干个小段,逐一执行。
比如一个集合里有1亿条交易记录,我不想一次性全导,就按create_time的年份分批:
mongoexport \ --uri="mongodb://user:pass@127.0.0.1:27017/admin" \ --db=trade \ --collection=records \ --query='{"create_time": {"$gte": ISODate("2023-01-01T00:00:00Z"), "$lt": ISODate("2024-01-01T00:00:00Z")}}' \ --type=json \ --out=/data/export/records_2023.json分完年再分季度、月、日,灵活度很高。这样做的价值有两点:第一,每次导出的数据量可控,不会因为单次任务过大导致内存溢出或长时间占用数据库连接;第二,如果某个分片导出失败,只需要重跑这一小段,不需要重新来一遍所有数据。
分批的时候有个细节:时间切分的边界要严格对齐,避免出现重叠或遗漏。$gte和$lt的组合是最稳妥的,别写$lte加$gt,容易导致边界数据重复。
5.3 数据脱敏导出:不能把生产数据直接交付
数据导出还有一个高频场景是给测试环境或合作方提供数据。如果直接把生产库的完整用户信息导出去,会带来很大的数据合规风险。我自己处理这类需求时,通常会先做脱敏再交付。
脱敏没有现成的MongoDB命令,一般是用脚本做。大致的流程是:导出需要的数据到JSON文件,然后用脚本对敏感字段做处理,比如把手机号中间四位打码、把姓名改成随机假名、把地址字段替换成虚拟地址,处理完成之后再做导入。这一步不能用简单的shell替换,因为JSON文件里有些字段可能是嵌套的,对格式做外科手术式的字符串替换非常容易出错。
我常用Python脚本处理脱敏,比如读取JSON文件,遍历每一条文档,对指定的敏感字段调用脱敏函数,再输出为新的JSON文件。整个过程并不复杂,但一定要在输出前对脱敏后的字段做一次抽样人工复核,看看是否符合预期,同时确认没有遗漏的敏感字段。
6. 常见问题与排查技巧实录
6.1 中文乱码与字符集问题
mongoexport导出CSV时,如果文件用Excel直接打开发现中文乱码,问题通常出在文件编码上。MongoDB里的字符串是UTF-8编码,但Windows下的Excel默认用ANSI或者GBK读取CSV,两者的字符集对不上。
解决办法有两个。一个是导出后把CSV文件转成带BOM的UTF-8格式,用编辑器或者脚本转换都行;另一个是干脆导出JSON,然后用相关数据处理工具转成Excel,中间过程由工具统一管理编码。我个人的习惯是尽量避免用CSV承载中文数据,除非下游明确要求CSV格式。
6.2 导入报错:E11000 duplicate key
这个错误出现的原因很直观:导入的数据里包含的目标集合已经存在的_id,并且导入命令没有处理重复数据的逻辑。解决方案有三种。
一是加上--mode=upsert,让导入变成更新语义;二是先删除目标集合再导入;三是通过--filter配合脚本把重复的_id过滤掉。
其中最容易在操作时产生迷惑的是--mode=upsert的行为。它会根据_id判断文档是否存在,存在则整条更新,不存在则插入。如果导入文件中存在同一条_id的多条记录,最终以文件里最后一条为准,因为处理顺序是线性的。这点和数据库的upsert语义基本一致,不用花太多时间去纠结。
6.3 导出的JSON里出现了$oid和$date包裹结构
如果在mongoexport导出的JSON里看到{"$oid": "..."}或者{"$date": "..."}这种嵌套结构,说明导出时使用了MongoDB的扩展JSON规范。这种格式并不适合直接交给非MongoDB的消费方,因为它增加了一层包装,下游解析成本很高。
解决办法也很简单:导出时指定--jsonFormat=canonical或--jsonFormat=relaxed,两个选项分别对应不同的输出规范。其中relaxed模式更接近阅读友好型,数字和日期都以可读的形式输出,但要注意relaxed模式在某些情况下会丢失某些类型的精确表示,如64位整数的精度。如果对精度要求很高,就老老实实保留canonical格式,然后在下游做解包处理。
6.4 mongorestore之后数据可用但索引丢了
有同学遇到过这样一种情况:用mongorestore恢复了几个集合,数据都正常,但查询性能变得很差,执行计划显示全集合扫描。大部分情况下,这是因为恢复后的集合索引没建上。
只在恢复单集合备份时,如果当初的备份文件只包含BSON数据而没有metadata.json,那么索引信息就不会被恢复。解决方法是:在备份时保证mongodump输出的目录结构完整,恢复时也使用目录方式或者完整归档文件,不要手动丢掉metadata.json。如果数据已经恢复完了才发现索引丢了,那只能按照业务需要的查询模式重新手工创建索引,这是一个比较耗时的操作,最好提前避免。
6.5 导出超大集合时内存溢出和连接中断
在集合数据量特别大、文档结构又复杂的时候,导出工具的进程内存可能飙升,甚至直接OOM。这个问题在绝大多数情况下,不是MongoDB本身的问题,而是导出工具默认把数据批处理时缓冲在内存中的机制影响的。
处理思路是限制导出的批处理大小。mongoexport有一个--batchSize参数,可以控制每次从服务端读取的文档数量。调低批大小可以显著降低内存占用。在遇到导出中断的问题时,通常我用这种处理:第一次按照失败时的数据位置,二分定位到具体的时间段或_id范围,之后按范围分批重试。逐条排查不太现实,因为海量的数据无法人工逐条检查。
6.6 常用排查命令备忘
在实际排查中,有几个命令非常高效,建议收藏:
# 查看导出的BSON文件信息(类似文档头解析) bsondump /data/backup/shop/orders.bson | head -n 5 # 检查归档包能否正常读取 mongorestore --archive=/data/backup/shop_20250115.archive --dryRun # 查看目标库集合数据量是否与备份一致 mongosh --quiet --eval "db.getSiblingDB('shop').orders.countDocuments({})" # 检查恢复后的索引是否完整 mongosh --quiet --eval "db.getSiblingDB('shop').orders.getIndexes()"bsondump是我非常喜欢的一个调试命令。它能直接查看BSON文件的内部结构,在备份文件打不开、怀疑文件损坏的时候,可以用它先看看文件头部有没有正常内容。--dryRun参数则是检查归档包完整性的利器,恢复前跑一遍,能极大降低中途失败的概率。
7. 我推荐的一套稳妥操作模板
有了前面这些基础认知,在实际项目里可以从下面这个模板入手。这套模板我不只在测试环境用过,生产环境的常规迁移任务也基本按这个套路走。
第一步,确认两个环境的版本兼容性。比较低的MongoDB版本备份出来的BSON,在高版本恢复通常没问题,但反过来高版本备份到低版本恢复就可能出现不兼容的情况。稳妥做法是在动手前查一下两个环境的版本号。
mongod --version mongosh --eval "db.version()"第二步,选择备份方式。如果只涉及单个或几个集合的定向数据交换,用mongoexport;如果要做整个库的迁移或备份,用mongodump。数据量超过千万级的场景,尽量直接放弃JSON方案,选择BSON方案。
第三步,先做小批验证。任何一次导出导入都不要直接跑全量,先导出几百条或几千条到本地,确认文件结构正确、类型正常、目标库能正常导入,再放开全量执行。这个习惯帮我避免过很多次返工。
第四步,关注数据校验。恢复完成后不要只看行数,要抽样比对几条关键文档,检查金额、时间、状态字段是否和备份一致。如果涉及金额统计,可以在备份前和目标库分别跑一下聚合求和,对比输出是否一致。
在实际项目里,我见过太多的数据迁移事故并不是因为通晓原理困难,而是因为操作顺序错了或者跳过验证直接全量执行。小批验证加事后校验,这套组合是最便宜也是最保险的安全带。
这几台机器上反复跑了无数遍导出导入之后,我自己最深的体会是:数据搬运从来不是“把文件拷过去”那么简单,类型保真、索引恢复、性能控制、数据安全,每一项都需要在动手之前就想清楚。不要等到恢复完成之后才发现类型不对、索引缺失、数据翻倍——到那个阶段,每一秒的损失都是实打实的。