ABAQUS学习(教你学会看&写 input 文件)

教你学会看&写 input 文件

原链接:https://www.jianshu.com/p/8c4d45b089b6#fnref7

阅读指导

本文将会涉及到以下内容:

  • inp 文件的功能和作用(你是否真的需要继续阅读此文)
  • 如何读懂 inp 文件 (inp 文件结构解析)
  • 如何编写 inp 文件(在 manual 的帮助下完成 inp 的编写)
  • 如何使用 inp 文件(命令行 / CAE 提交作业 + 从 inp 文件导入模型)

1.阅读方式

2.阅读时长

  • 对于新手,完成阅读大概需要两个小时。

input 文件对于 ABAQUS 来讲是 native 的,因为在最初是没有 CAE 界面的,分析人员就只能把 input 提交给ABQ。这一历史就导致了目前一个比较常见的问题——某些关键字在 CAE 中无法使用。当然了,这只是部分原因,还有其他因素决定了 CAE 界面无法完全代替 inp 文件。

1. 什么是 inp 文件? 我需要了解它吗?

input 文件顾名思义就是用来输入信息的,是向 abaqus 的求解器提交信息的,提交 input 然后得到 output (通常是 .odb 文件)。不管我们使用什么方式来建立模型(也就是通常所说的前处理),最后要计算的时候都是使用一个叫做求解器的模块来进行的。通常我们对一款有限元软件的评价很重要的一方面就是求解器,我们大家都知道 ABAQUS 的非线性能力很强,其实就是他的非线性求解器很霸气。有点远了,回过来继续说 inp 文件。

inp 文件是我们向 ABAQUS 提交 job 的直接对象,无论通过什么方式进行提交都是直接把 inp 文件呈递给 ABQ (ABQ 在收到 job 任务之后会对 inp 进行预处理,本文不会涉及此部分内容)
input 文件包含了 几乎 所有的模型信息[^nealy all],包括模型的定义和求解以及输出要求。

[^nealy all]: 有时候我们还需要到用户子程序,那么 inp 就不是完整的

尽管 inp 如此重要,但是一般来讲我们是没有必要了解它的。因为对于很多工作,我们压根不需要碰这个文件,而且实际的情况是很多不了解它的人也能够做有限元分析。就像我们使用计算机却不必掌握计算机工作原理一样;但是当我们知道的足够多的时候很多东西变得简单明了。试想某一天你的电脑无法进入系统了,那么当你了解的足够多的时候你就可以清楚的知道是内存掉了,还是主板坏了,还是系统引导失败了。。。。。。

input 的重要还远不止于此,本文开篇就提到 input 对 Abaqus 来讲是 native 的,并且 CAE 中某些关键字是不能够使用的,而这些都只能交给 input 来做。

可以毫不含糊地讲,不会写 input 的厨师长都不是好司机。

对于那些没有清楚的编程概念的读者,我需要声明并且强调一下:写 inp 不是编程,也不是在写脚本。inp 只是为 ABQ 的求解器提供输入数据,我们写程序的时候经常需要读入外部文件,inp 就是这个角色——输入文件而已。

我在前面也提到了“ABQ 的预处理”,在这里稍微解释一下:将 input 中的数据结构(关键字 + 数据行)进行一些整理(按照 ABQ 事先约定的套路进行解释)然后传递给求解器(求解器就是解方程组用的),inp 就是一个传话的,我们想要让求解器干什么写在 inp 里面就行了(当然,远不止于此)。有一个非常明显的现象表明这个过程是确实会发生的,看一下面这个图,是不是很熟悉(或许你从来没有关注过这一部分的信息)。

img

这两个界面你熟悉吗?

2. inp 文件基础

Abaqus 的 doc 做的很好,至少有两个地方是可以参考的[1]

2.1. inp 文件的组织结构

An analysis in Abaqus is defined by an input file, which

  • contains keyword lines and data lines; and
  • is divided into model data and history data.
摘自 1.3.1 Defining a model in Abaqus

预处理器读取 inp 文件时候,总需要一些规则吧,我们自己写程序进行 I/O 操作的时候不同样要指定一些规则吗?正所谓“无规矩不成方圆”,inp 必须遵守 ABAQUS 制定的规则。

这个地方没什么好解释的,接着往下看吧。

2.1.1. inp 文件中的标记(功能属性)

input 文件是 ASCII 纯文本格式的,可以使用任意的文本编辑器打开。当我们打开一个 inp 文件的时候会看到有三种不同的行。而这三种行分别从 功能 上对应着:

  1. 注释;
  2. 关键字;
  3. 数据。
  • 注释以 ** 开始而且两颗星号必须占据前两列(因此不允许在某一行的后面进行注释)
  • 关键字以 * 开始,后面可以(有些是必须)跟参数
  • 数据前面则没有任何标识。
    另外 需要注意的是 input 文件中不允许出现空行(文件最后有一个空行,但是这个不影响,因为这个不会被读取到)

2.1.2. inp 文件的结构划分(内容属性)

内容 上来讲,如我们上面所说,分为 model 和 history。

  • model 包含了 node, element, material, initial condition 等信息
  • history 包含了 analysis type, loading, output requests 等信息

上面对两个部分必须包含的内容(在手册中以 required 进行标识)进行了加粗显示,也就是说一个完整的、正确的、可以被预处理器所接受的 inp 文件至少是应该包括这些信息的[2]。至于具体某一个关键字是属于 model 还是 history,要参考 Abaqus Keywords Reference Guide 的具体的条目,这些条目都以首字母排序出现在手册中。

2.2. inp 文件书写规则

参考 Abaqus Analysis User’s Guide >> 1.2.1 Input syntax rules

2.2.1. 关键字行

关键字行的基本规则:

  1. 关键字行以星号(*) 开头(其前面允许带空格,但是我个人不建议这么做)
  2. 如果关键字带有参数, 那么用分隔符 (此处是逗号 ,) 将两者隔开,如果参数带有值(value)使用赋值符号 (此处是等号 =) 为其赋值
  3. 如果某个关键字带有多个参数,各个参数之间加上分隔符 (此处是逗号 ,)

额外注意事项:

  • 不允许在一行中同时出现多次相同的参数
  • 所有关键字和参数都是 大小写不敏感的,你可以根据自己的习惯选择大小或者小写,甚至是混写
  • 通常来讲,参数的值也是大小写不敏感的[3],所以不要试图以不同大小写的形式来为参数取得不一样的值[^value case]
  • 关键字、参数、以及多数情况下参数的值 不必完全拼写出来(但是不建议这么做[^why not recommend])
  • 关键字行是以逻辑行进行识别的,续行在行末使用逗号 (,),因此避免关键字行行末多余的逗号
  • 关键字行的长度限制为256个字符,包括空格在内(尽管空格在关键字行中不起作用,只是视觉上的帮助而已)
  • 关键字的依赖和群组,很遗憾在手册中并没有指出哪些关键字之间有依赖关系

2.2.2. 数据行

首先声明,数据不一定是数字,也可以是字符串、内部变量等

基本规则:

  1. 数据行在语义上和逻辑上都从属于关键字行,没有单独的数据行
  2. 同一个逻辑行的数据之间以用分隔符进行隔断(此处是逗号 ,)
  3. 浮点数、整数、字符串都有各自对应的长度限制(基本不用考虑,很少能达到限制的长度)

数据行到底怎么写,这要看他的关键字(及其参数)。因为 没有独立的数据行

2.3. 最特殊的关键字 *heading

这个关键字特殊就特殊在每一个完整的 input 文件几乎都是以他开始的,这个关键字没有任何参数,他的数据行没有固定的格式,你可以写任意多行的数据,但是只有第一行的前 80 个字符起作用,这些字符将会显示在 OBD 的“Title Block”(默认位置在窗口下方,以左对齐方式显示)中,其他的只能自己看看。这个其实对应着 CAE 中的 job 编辑器的 description 栏。此处我们将展示出文本的第一个实例,该例题来自于 Abaqus Example Problems Guide >> 3.1.1 Symmetric results transfer for a static tire analysis。注意: title block 的第二行太长而特意拆分成了多行显示

img

*heading 关键字和 ODB 的 title

我们可以看到*heading*restart(即第二个关键字行)之间的部分全部为 *heading 的数据。而且只有第一行被写入了 ODB 的 title。

对这个关键字想要说明的是,你可以在这里写一些注释性的东西,就像这个例子一样,当我们时隔多日在看到这个文件的时候能够看一眼就知道他是干什么的。本例中 heading 区的内容包含:

  • 标题;
  • inp 文件名称;
  • 该分析的作用(目的);
  • 分析步的定义;
  • 模型的单位设置。

好了,这个关键字不再解释了。


3. inp 文件实例分析——从一个实用的角度出发

先放上一个 inp 文件,这个是一个顶点在 (0,0,0) 的立方体,楞长是20 mm,划分了1个 3D 8 节点单元,并且使用了减缩积分。定义了一个静力分析步,所有设置均为默认。定义了一个固定约束的边界条件。

*Heading
** Job name: demo Model name: Model-1
** Generated by: Abaqus/CAE 6.13-4
*Preprint, echo=NO, model=NO, history=NO, contact=NO
** PARTS
*Part, name=Part-1
*Node
      1,          20.,          20.,          20.
      2,          20.,           0.,          20.
      3,          20.,          20.,           0.
      4,          20.,           0.,           0.
      5,           0.,          20.,          20.
      6,           0.,           0.,          20.
      7,           0.,          20.,           0.
      8,           0.,           0.,           0.
*Element, type=C3D8R
1, 5, 6, 8, 7, 1, 2, 4, 3
*Nset, nset=Set-1, generate
 1,  8,  1
*Elset, elset=Set-1
 1,
** Section: Section-1
*Solid Section, elset=Set-1, material=Material-1
,
*End Part
** ASSEMBLY
*Assembly, name=Assembly
*Instance, name=Part-1-1, part=Part-1
*End Instance
*Nset, nset=Set-1, instance=Part-1-1
  1,  2,  3, 10, 11, 12, 19, 20, 21
*Elset, elset=Set-1, instance=Part-1-1
 1, 2, 5, 6
*End Assembly
** MATERIALS
*Material, name=Material-1
*Density
 7.8e-09,
*Elastic
210000., 0.3
******************************************************************
** STEP: Step-1
*Step, name=Step-1, nlgeom=NO
*Static
1., 1., 1e-05, 1.
** BOUNDARY CONDITIONS
** Name: BC-1 Type: Symmetry/Antisymmetry/Encastre
*Boundary
Set-1, ENCASTRE
** OUTPUT REQUESTS
*Restart, write, frequency=0
** FIELD OUTPUT: F-Output-1
*Output, field, variable=PRESELECT
** HISTORY OUTPUT: H-Output-1
*Output, history, variable=PRESELECT
*End Step

3.1. inp 文件结构解析

把上面这个 input 文件的结构列一下就是这样的(为了显示嵌套关系进行了缩进处理),这个结构很清晰了吧。

img

inp 文件结构解析

对新手来讲可能这个还有点看不明白,那我就用下面这个图解释一下吧。

img

然而细心的你可能已经发现 history 部分漏掉了一个 *restart 以及一个 *output,不过事实是“并没有这么简单”。这两个关键字都是有意略掉的。

  • *restart 写在 model 部分或者 history 部分没有影响,实际上我更喜欢写在 *heading 下面
  • *output 其实对于 inp 的完整性来讲是非必需的{但是从我们计算本身的意义来看是极其重要的[4]

3.2. 庖丁解牛——关键字实例分析

明白了 input 文件的结构之后我们来抽取出几个关键字仔细分析。

在这一部分,还会细致地简讲解如何使用 keywords manual
查阅 manual 是编写 inp 文件必不可少的步骤,而且 manual 是最权威的

3.2.1. model 部分的*elset

打开关键字手册,直接查找到 *elset,由于都是按照首字母进行排序的,非常好定位。

img

关键字 *elset

img

parameter.PNG

img

dataline.PNG

教你看懂 manual
  • 第一行为该关键字,注意关键字也可以是多个单词,此处为 *elset
  • 第二行用一个没有主语的短句简单明了的说明了该关键字的作用,此处是“设置/创建单元集合”[5],多数情况下第三行是第二行的细致版本,例如有些关键字在这里指出必须和哪个关键字合用,还有些关键字在此处指出应该学习哪一个章节等
  • product:适用范围,例如有些是隐式分析才能用的,有些是显式分析独有的,有些仅适用于 Aqua 模块。。。。。。
  • type:按照 model 和 history 进行分类,有许多关键字是同时占有两个分类的
  • level:依附对象和从属关系
  • ABQ/CAE:GUI界面[6]中对应的模块
  • Reference:很重要
  • parameters:分为两类,即 required (必备)和 optional [7](可选),从字面意思理解即可,每一个 parameter 下方都有简介
  • data line:数据行,数据行的具体要求都在这里列出来了,会针对每个具体的 parameter 给出其对应的 data 格式

接下来针对每一个参数进行解释:

  • elset:单元集的名字(注意这是一个与关键字同名的参数),这个参数是必备的,也就是关键字 *elset 后面必须要跟这个参数,另外这个关键字是有值的(必须赋值)。说如果我们想要定义一个叫做 demo 的单元集,那么 关键字行就是 *elset, elset = demo
  • generate:这个参数表示单元集中的单元编号将会采用自动生成等差数列的方式。等差数列没什么好说的,给定首项和末位数以及公差即可。
  • instance:该 单元集 所属的 instance,这也是一个有值的参数
  • internal:这个是 CAE 控制的,如果自己写 inp 文件用不着。
  • unsorted:从字面理解即可“就是不排序,不整理”,as is,后面还解释道,如果不添加该参数那么所创建的单元集中的单元编号都是按照升序排列的(无论你在数据行是以什么样的顺序给出)

数据行和参数(及其值 )是紧密联系的,后者的设置往往直接决定了数据行怎么写。来看看此处的数据行(图 dataline.PNG)吧。
此处的数据行分了两种情况:

  • if generate is ommited
  • if generate is included

不妨先看后者吧,比较简单一点。就是一个等差数列,第一行必须是三个数据,如果有需要就重复第一行的格式接着写,而数据行的行数没有限制。例如的 单元集 demo 要包含 58~175, 200350(仅取偶数标号),500550(每10位取一个) 号单元,那么

*elset, elset = demo, generate
58, 175, 1
200, 350, 2
500, 550, 10
提示一下:根据关键字及其参数的规则,generate 完全可以写成 gen。

再来看一下 使用 generate 参数时候对应的数据行。
第一行 把单元标号列出来就可以了,最多16个,但是你的单元集可能往往多余16个,没关系,重复第一行的格式跟着写。除了直接写单元标号之外也可以写 子单元集 的名字,也就说说我们前面 已经定义过的 单元集可以包含进来(原先的单元集仍保持不变)。假设我们要建立单元集 demo 中包含 subelset1 和 subelset2 两个单元集

*elset, elset = demo
subelset1, subelset2

img

DEMO-elset

注意事项:

  • 单元集 demo 在定义的时候使用了sub1 和 sub2,并且还有额外的指定单元
  • sub1 包含 6~40 的偶数号单元,共计18个单元
  • sub2 包含60、65、70、75、80 共计5个单元
  • demo 中以标号给出的单元是 1,4,1,由于 1号单元重复只计一次。demo中共包含 18 + 5 + 2 = 25 个单元
  • 如果 demo 的定义中,使用 generate 参数,那么1~4 号单元将会被使用,而 sub1, sub2 会排除,因为 此时的数据行并不支持 单元集 的 label(支持的 单元集 label 的情况是不实用 generate 参数)
  • 更多情况自行验证

3.2.2. history 部分的 *boundary

该关键字内容太多,不可能向前面一个关键字一样讲得那么完整。另一方面,doc 中涉及的方方面面我们能用到的太少了,所以也没有完全了解的必要。实用主义
该关键字包含 三大类参数(包含 空参数)和五大类 dataline(大类里面还有小类。。。。。。)。

img

keyword–*boundary

我们来看一下如何通过 inp 修改边界条件。回到第一个案例(立方体模型),我们原先设置的是
固定约束条件,现在想改为仅仅约束2~3两个自由度,而第一个自由度为指定位移 1 mm。(由于是实体应力单元没有更多的 DOF)。

** ------------------------------------ original
*Boundary
Set-1, ENCASTRE
** ------------------------------------ modified
*boundary, type = displacement
set-1, 1, 1, 1
set-1, 2, 3, 0

在改写的版本中
数据行第一行表示 DOF1~1,即 DOF1,1 个单位的 displacement(单位需要全局统一,本例为 mm)
数据行第二行表示 DOF2~3施加值为0的位移,也就是限制了y/z方向的位移。
而这两行数据他们的作用对象都是 set-1(这是一个节点集)

此部分改写依据的参数和数据行如图:

img

info for BC

至于如何从众多的参数和数据行类型中选取,没有什么好方法,这个需要一定的软件使用经验。

继续来看这个关键字的其他部分,在图 keyword–*boundary (我的每个图下方都是有标题的)中左下角分页的顶部看到 Optional, mutually exclusive parameters 这些就是互斥参数,在脚注10中已经解释过。 在上方第二个分页上存在 参数user,这是使用子程序时候用的。接下来的一节中就会讲到 inp 引用外部文件。

3.3. input 引用外部文件

3.3.1. 子程序

inp 引用外部文件的第一种情形在上一节结束时提到了——子程序,关于子程序的知识请参考 doc。

需要强调一下目前很多人对 fortran 程序的一些基本概念不清楚,例如 f77和固定格式你真的了解吗?(不管你是编写 fortran 程序的高手还是打算学习但未入门的新手,请移步 FAQ之 基本概念。)

3.3.2. 单元/节点 数据

在第一个例子中,我仅仅给整个模型划分了1个网格,而这一个网格拥有8个节点。如果模型中的网格数量巨大,inp 文件将会变得非常大,行数可能达到上百万行。这时候别说是编写或者查看 inp 了,就是打开一下可能对于某些机器上的某些编辑器(比如 Windows 上的记事本)就是非常艰巨的任务。ABQ 提供了这样一种机制就是允许使用 *include 关键字或者 inp 参数引入外部文件。doc 中的例题多数会采用这样的方式,将数量众多的单元和节点放入一个单独的文件中。

有两种方法可以使用:

  • *node, nset = all_node, inp = node.inp
  • *inclue, input = node.inp

inp 参数后面的 inp 文件里面仅仅包括 *node 的数据行(节点编号及其坐标)
这个 inp 文件是这么写的:

1, 20., 20., 20.
2, 20., 10., 20.
...
最后允许出现空行

*include 关键字后面的参数是 input,他后面的 inp 文件是完整的节点定义(带有关键字行)
这个 inp 文件是这么写的:

*node
1, 20., 20., 20.
2, 20., 10., 20.
...

把 *include 理解为复制粘贴比较简单一些,而其作用也正是如此。而 inp 参数就不一样了,他的作用不仅仅是复制粘贴,他所复制过来的内容要作为当前关键字的数据行。

4. inp 文件怎么用?

对于很多仅仅使用 CAE 的用户,他们可能根本没注意过 inp 的存在。实际上 inp 是非常方便和快捷的建模手段,而且很灵活。另一方面 inp 是纯文本,纯文本的好处就是走哪带哪,基本上没有什么限制,不必使用专用软件打开(想想 docx,xlsx,pptx,psd,eps …)【题外话,又绕远了】。另一方面 inp 的体积比较小的(尽管有时候高达上百MB),各个版本之间的兼容性(其实涉及到关键字)很好,而 cae 文件这方面不具有优势的。我们平常使用 CAE 来进行的那些操作怎么用 inp 来代替呢? 或者说 inp 还能提供哪些我们在 CAE 下所不能完成的任务呢? 我们一起来看看吧。

  • 快速修改模型
    如 BC (改变一下约束方式) /
    load (载荷大小方向等) /
    output request (修改,增删) /
    materials‘ properties (不就是改几个数字吗)/
    step/
    restart (r/w/rw) /
    interactions /
  • (从命令行和CAE)提交 inp 作业
    或许你还不知道可以直接提交 inp 作业,但是确实是可以的,而且很方便。
    最简单的方式就是 命令行提交 abaqus job=fileBaseName int cpus=40, 不过我喜欢直接指明版本号,例如我用的是 v6.13-4,那么 abq6134 job=fileBaseName int cpus=40。这两个命令的意思就是 提交名称为 fileBaseName 的 inp 文件进行计算,并且使用40个cpu以交互模式来运行。所谓交互模式就是在命令行会反馈一些信息,例如 隐式分析中的日志文件会带印在窗口上,显式分析的 日志文件和状态文件 会打印在窗口上。关于命令行中各个选项的使用,参考:分析手册 3.2.2. Command summary
    在 CAE 中 提交 inp 作业的方法:创建 job 时候选择作业来源为 inp 文件。(这个界面上 摁 F1 就知道了,写的很清楚)
  • 从 inp 读取模型
    如果我们有一个 inp 文件能不能看一下模型长什么样子呢? 完全可以,现在你已经知道可以提交这个 job 呀,得到 odb 文件就行了。当然了,不运行 job 也是可以的。通过 file >> import >> model (file type = inp) 或者在 模型数的 Model
    节点上看右键菜单。
    如果幸运的话那么你得到了完整的 模型,但是很多时候真的没那么幸运,由于部分关键字的不支持,或者是 inp 文件不完整(是那只是 inp 文件里面有错误,格式不正确等等)不管怎么样,导入之后先看窗口下方的提示信息,这个地方很重要。
  • inp 片段
    除了导入完整的 inp 文件得到模型之外,也可使用 inp 片段来存储一些常用数据代替繁琐的手动输入(手动输入很多数据是很容易出错的)。例如你可以将常用的 Material / Section 保存在 inp 文件中,通过上面介绍的方式导入模型。[^import model]
    不过呢,针对这个功能我更建议使用 python 来处理,而且不用我们写代码,我们在 CAE 操作的时候录制 宏 即可。录制的宏都保存在硬盘上(可以选择 主目录 或者 工作目录),都能干需要重复操作的时候运行宏即可。你可以对录制好的宏进行任意的修改(File>> Macro Manager 可以管理 宏,录制和播放都在这里)
  • 在不同的有限元之间交换模型数据
    比如 abaqus to nastranabaqus from ansys,这部分内容我没了解过,有兴趣的可以参考分析手册 3. Job Execution,不过有一点我是可以肯定的,这些都是用命令行来执行的,所以有必要了解命令行基础。

[^import model]: 但是需要知道的是,每一个被导入的 inp 文件都会创建一个 新的 Model。因此 你所导入的材料或者界面并没有导入你正在操作的模型。如果要把这些信息导入你当前工作模型,那么使用 model copy 功能即可,可以从模型树上 model 节点操作,或者菜单 model 操作。

5.最后说点啥呢

  • 学习 inp 是为了更好的建模,一切不从实用角度出发的 inp 都是不可取的
  • inp 是为了提高建模效率,如果做不到这一点,请果断使用 CAE(或者 python),正所谓条条大路通罗马
  • 不管有没有吸收本文的内容,请记住最基本的一点 inp 不是一种语言
  • 还有什么要补充的吗?欢迎留言探讨,若有错误,请一定指出来。

仔细阅读完本文之后你一定发现手册是这么强大的工具,有什么理由不安装手册呢?硬盘空间真的不够吗?才两个G而已。

手册分为 HTML 和 PDF(受保护) 两种版本,建议完全安装。两者不可互相替代。


  1. 本文中 Documentation 具体基于 v6.13-4,其他 版本具体编号可能有些许差异。

  2. 实际上对于常见的分析是这样的,某些特殊分析不遵守这样的规则,如果有兴趣,可以翻看例题手册中关于轮胎的部分。

  3. 既然说的是通常,那么就肯定有例外。这里有一个例外是参数的值是一个外部文件,而同时我们又工作在一个对大小写敏感的机器上(比如 Linux)

    [^value case]: 例如关键字 *material 的参数 name 的值就是字符串,abc 和 ABC 显然是两个不同的字符串,但是我们不能用这样的方式定义命名两种材料,你试试在 CAE 中允许你这么干吗?
    [^why not recommend]: 不用完全拼写出来的前提是已经提供的字符可以唯一确定这些 keyword,parameters,values,但是这些我们可能根本不确定。

  4. 只计算不输出这不是在做无用功吗?

  5. 某些关键字解释的不是很到位,比如此处的 “assign elements to an elset”,看上去像是把一堆单元加入到某个(已经存在的)单元集中,但实际上是建立单元集合,而单元集合的建立需要指定单元。

  6. 准确来讲就是 CAE(Complete Analysis Environment), 而不限于 GUI。注意 CAE 也可以没有界面(GUI)

  7. optional 类型的可能会遇到 “Optional, mutually exclusive parameters” 即“互斥的”。这时手册会同时给出一组参数,这些参数可能一个都用不上,但是如果要用,只能用一个(因为这些参数从语义或/和功能上是互斥的)。

猜你喜欢

转载自blog.csdn.net/a1099313374/article/details/86596521