【踩坑】XML转JSON中如何把单个元素转成数组
在某些业务场景下,你可能需要把一个XML 转换成一个 JSON,其实这个转换并不难,网上有很多现成的工具类。但这里都有一个问题:比如这个节点设计者想表达的是一个是数组 ,但由于XML语法在设计上的缺陷,当只有一个子节点 或者 没有子节点 的情况下,如果你尝试把它转成JSON时,你会发现他默认转成了一个JSON对象了,而不是数组。
当前XML中books节点下有多个book,设计者想表达的是books下有多个元素(多本书),这种场景下转换是没有问题的。
1、转换前的XML数据
<?xml version="1.0" encoding="utf-8"?>
<library><books><book><name>Java</name></book><book><name>C语言</name></book></books>
</library>
2、转换后的JSON数据
{"library": {"books": {"book": [{"name": "Java"},{"name": "C语言"}]}}
}
结合上面XML中books节点,我们删除其中一个子节点,其实设计者依然想表达的是books下有多个元素(多本书),当由于XML不能像JSON对象一样,能清晰的表达对象和数组,这种场景下转换成JSON对象就有问题了。
1、转换前的XML数据
<?xml version="1.0" encoding="utf-8"?>
<library><books><book><name>Java</name></book></books>
</library>
2、转换后的JSON数据
{"library": {"books": {"book": {"name": "Java"}}}
}
我们发现经过工具类转换后,book 被转换成一个JSON对象 {} 了,而不是一个JSON数组 [],因为此处XML的语义无法清晰的表达是对象还是数组,就导致转换的结果是一个对象。 其实你期待输出一个标准固定的格式给用户,不管几个节点,都是输出为JSON数组,而不是因为节点变化,导致数据格式发生变化,导致客户端无法用统一的格式解析数据。
网上有很多办法,提供了很多思路,比如(Convert XML to JSON and force array),它是一个.net语言的框架,但作为Java开发者真正解决这个问题的代码并不多。你需要自己写代码来解决这个问题。我在遇到这个问题后,也参考了部分开源代码的实现思路,你通过下面的代码可以轻松的解决这个问题。
package com.demo;
import java.util.ArrayList;
import java.util.HashMap;/*** @author youyun.xu* @Description: Tag* @date 2022/1/17 14:15*/
public class Tag {private String mPath;private String mName;private ArrayList<Tag> mChildren = new ArrayList<>();private String mContent;Tag(String path, String name) {mPath = path;mName = name;}void addChild(Tag tag) {mChildren.add(tag);}void setContent(String content) {// checks that there is a relevant content (not only spaces or \n)boolean hasContent = false;if (content != null) {for(int i=0; i<content.length(); ++i) {char c = content.charAt(i);if ((c != ' ') && (c != '\n')) {hasContent = true;break;}}}if (hasContent) {mContent = content;}}String getName() {return mName;}String getContent() {return mContent;}ArrayList<Tag> getChildren() {return mChildren;}boolean hasChildren() {return (mChildren.size() > 0);}int getChildrenCount() {return mChildren.size();}Tag getChild(int index) {if ((index >= 0) && (index < mChildren.size())) {return mChildren.get(index);}return null;}HashMap<String, ArrayList<Tag>> getGroupedElements() {HashMap<String, ArrayList<Tag>> groups = new HashMap<>();for(Tag child : mChildren) {String key = child.getName();ArrayList<Tag> group = groups.get(key);if (group == null) {group = new ArrayList<>();groups.put(key, group);}group.add(child);}return groups;}String getPath() {return mPath;}@Overridepublic String toString() {return "Tag: " + mName + ", " + mChildren.size() + " children, Content: " + mContent;}
}
package com.demo;import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;import static java.util.regex.Pattern.DOTALL;/*** @author youyun.xu* @Description: Xml转Json支持单个转数组(建造者模式)* @date 2022/1/17 14:15*/
public class XmlToJson {private static final String TAG = "XmlToJson";private static final String DEFAULT_CONTENT_NAME = "content";private static final String DEFAULT_ENCODING = "utf-8";private static final String DEFAULT_INDENTATION = " ";private String mIndentationPattern = DEFAULT_INDENTATION;// default values when a Tag is emptyprivate static final String DEFAULT_EMPTY_STRING = "";private static final int DEFAULT_EMPTY_INTEGER = 0;private static final long DEFAULT_EMPTY_LONG = 0;private static final double DEFAULT_EMPTY_DOUBLE = 0;private static final boolean DEFAULT_EMPTY_BOOLEAN = false;/*** Builder class to create a XmlToJson object*/public static class Builder {private StringReader mStringSource;private InputStream mInputStreamSource;private String mInputEncoding = DEFAULT_ENCODING;private HashSet<String> mForceListPaths = new HashSet<>();private HashSet<Pattern> mForceListPatterns = new HashSet<>();private HashMap<String, String> mAttributeNameReplacements = new HashMap<>();private HashMap<String, String> mContentNameReplacements = new HashMap<>();private HashMap<String, Class> mForceClassForPath = new HashMap<>(); // Integer, Long, Double, Booleanprivate HashSet<String> mSkippedAttributes = new HashSet<>();private HashSet<String> mSkippedTags = new HashSet<>();/*** Constructor** @param xmlSource XML source*/public Builder( String xmlSource) {mStringSource = new StringReader(xmlSource);}/*** Constructor** @param inputStreamSource XML source* @param inputEncoding XML encoding format, can be null (uses UTF-8 if null).*/public Builder( InputStream inputStreamSource, String inputEncoding) {mInputStreamSource = inputStreamSource;mInputEncoding = (inputEncoding != null) ? inputEncoding : DEFAULT_ENCODING;}/*** Force a XML Tag to be interpreted as a list** @param path Path for the tag, with format like "/parentTag/childTag/tagAsAList"* @return the Builder*/public Builder forceList( String path) {mForceListPaths.add(path);return this;}/*** Force a XML Tag to be interpreted as a list, using a RegEx pattern for the path** @param pattern Path for the tag using RegEx, like "*childTag/tagAsAList"* @return the Builder*/public Builder forceListPattern( String pattern) {Pattern pat = Pattern.compile(pattern, DOTALL);mForceListPatterns.add(pat);return this;}/*** Change the name of an attribute** @param attributePath Path for the attribute, using format like "/parentTag/childTag/childTagAttribute"* @param replacementName Name used for replacement (childTagAttribute becomes replacementName)* @return the Builder*/public Builder setAttributeName( String attributePath, String replacementName) {mAttributeNameReplacements.put(attributePath, replacementName);return this;}/*** Change the name of the key for a XML content* In XML there is no extra key name for a tag content. So a default name "content" is used.* This "content" name can be replaced with a custom name.** @param contentPath Path for the Tag that holds the content, using format like "/parentTag/childTag"* @param replacementName Name used in place of the default "content" key* @return the Builder*/public Builder setContentName( String contentPath, String replacementName) {mContentNameReplacements.put(contentPath, replacementName);return this;}/*** Force an attribute or content value to be a INTEGER. A default value is used if the content is missing.* @param path Path for the Tag content or Attribute, using format like "/parentTag/childTag"* @return the Builder*/public Builder forceIntegerForPath( String path) {mForceClassForPath.put(path, Integer.class);return this;}/*** Force an attribute or content value to be a LONG. A default value is used if the content is missing.* @param path Path for the Tag content or Attribute, using format like "/parentTag/childTag"* @return the Builder*/public Builder forceLongForPath( String path) {mForceClassForPath.put(path, Long.class);return this;}/*** Force an attribute or content value to be a DOUBLE. A default value is used if the content is missing.* @param path Path for the Tag content or Attribute, using format like "/parentTag/childTag"* @return the Builder*/public Builder forceDoubleForPath( String path) {mForceClassForPath.put(path, Double.class);return this;}/*** Force an attribute or content value to be a BOOLEAN. A default value is used if the content is missing.* @param path Path for the Tag content or Attribute, using format like "/parentTag/childTag"* @return the Builder*/public Builder forceBooleanForPath( String path) {mForceClassForPath.put(path, Boolean.class);return this;}/*** Skips a Tag (will not be present in the JSON)** @param path Path for the Tag, using format like "/parentTag/childTag"* @return the Builder*/public Builder skipTag( String path) {mSkippedTags.add(path);return this;}/*** Skips an attribute (will not be present in the JSON)** @param path Path for the Attribute, using format like "/parentTag/childTag/ChildTagAttribute"* @return the Builder*/public Builder skipAttribute( String path) {mSkippedAttributes.add(path);return this;}/*** Creates the XmlToJson object** @return a XmlToJson instance*/public XmlToJson build() {return new XmlToJson(this);}}private StringReader mStringSource;private InputStream mInputStreamSource;private String mInputEncoding;private HashSet<String> mForceListPaths;private HashSet<Pattern> mForceListPatterns = new HashSet<>();private HashMap<String, String> mAttributeNameReplacements;private HashMap<String, String> mContentNameReplacements;private HashMap<String, Class> mForceClassForPath;private HashSet<String> mSkippedAttributes = new HashSet<>();private HashSet<String> mSkippedTags = new HashSet<>();private JSONObject mJsonObject; // Used for caching the resultprivate XmlToJson(Builder builder) {mStringSource = builder.mStringSource;mInputStreamSource = builder.mInputStreamSource;mInputEncoding = builder.mInputEncoding;mForceListPaths = builder.mForceListPaths;mForceListPatterns = builder.mForceListPatterns;mAttributeNameReplacements = builder.mAttributeNameReplacements;mContentNameReplacements = builder.mContentNameReplacements;mForceClassForPath = builder.mForceClassForPath;mSkippedAttributes = builder.mSkippedAttributes;mSkippedTags = builder.mSkippedTags;mJsonObject = convertToJSONObject(); // Build now so that the InputStream can be closed just after}/*** @return the JSONObject built from the XML*/publicJSONObject toJson() {return mJsonObject;}privateJSONObject convertToJSONObject() {try {Tag parentTag = new Tag("", "xml");XmlPullParserFactory factory = XmlPullParserFactory.newInstance();factory.setNamespaceAware(true); // tags with namespace are taken as-is ("namespace:tagname")XmlPullParser xpp = factory.newPullParser();setInput(xpp);int eventType = xpp.getEventType();while (eventType != XmlPullParser.START_DOCUMENT) {eventType = xpp.next();}readTags(parentTag, xpp);unsetInput();return convertTagToJson(parentTag, false);} catch (XmlPullParserException | IOException e) {e.printStackTrace();return null;}}private void setInput(XmlPullParser xpp) {if (mStringSource != null) {try {xpp.setInput(mStringSource);} catch (XmlPullParserException e) {e.printStackTrace();}} else {try {xpp.setInput(mInputStreamSource, mInputEncoding);} catch (XmlPullParserException e) {e.printStackTrace();}}}private void unsetInput() {if (mStringSource != null) {mStringSource.close();}// else the InputStream has been given by the user, it is not our role to close it}private void readTags(Tag parent, XmlPullParser xpp) {try {int eventType;do {eventType = xpp.next();if (eventType == XmlPullParser.START_TAG) {String tagName = xpp.getName();String path = parent.getPath() + "/" + tagName;boolean skipTag = mSkippedTags.contains(path);Tag child = new Tag(path, tagName);if (!skipTag) {parent.addChild(child);}// Attributes are taken into account as key/values in the childint attrCount = xpp.getAttributeCount();for (int i = 0; i < attrCount; ++i) {String attrName = xpp.getAttributeName(i);String attrValue = xpp.getAttributeValue(i);String attrPath = parent.getPath() + "/" + child.getName() + "/" + attrName;// Skip Attributesif (mSkippedAttributes.contains(attrPath)) {continue;}attrName = getAttributeNameReplacement(attrPath, attrName);Tag attribute = new Tag(attrPath, attrName);attribute.setContent(attrValue);child.addChild(attribute);}readTags(child, xpp);} else if (eventType == XmlPullParser.TEXT) {String text = xpp.getText();parent.setContent(text);} else if (eventType == XmlPullParser.END_TAG) {return;} else if (eventType == XmlPullParser.END_DOCUMENT) {return;} else {//Log.i(TAG, "unknown xml eventType " + eventType);}} while (eventType != XmlPullParser.END_DOCUMENT);} catch (XmlPullParserException | IOException | NullPointerException e) {e.printStackTrace();}}private JSONObject convertTagToJson(Tag tag, boolean isListElement) {JSONObject json = new JSONObject();// Content is injected as a key/valueif (tag.getContent() != null) {String path = tag.getPath();String name = getContentNameReplacement(path, DEFAULT_CONTENT_NAME);putContent(path, json, name, tag.getContent());}try {HashMap<String, ArrayList<Tag>> groups = tag.getGroupedElements(); // groups by tag names so that we can detect lists or single elementsfor (ArrayList<Tag> group : groups.values()) {if (group.size() == 1) { // element, or list of 1Tag child = group.get(0);if (isForcedList(child)) { // list of 1JSONArray list = new JSONArray();list.put(convertTagToJson(child, true));String childrenNames = child.getName();json.put(childrenNames, list);} else { // stand alone elementif (child.hasChildren()) {JSONObject jsonChild = convertTagToJson(child, false);json.put(child.getName(), jsonChild);} else {String path = child.getPath();putContent(path, json, child.getName(), child.getContent());}}} else { // listJSONArray list = new JSONArray();for (Tag child : group) {list.put(convertTagToJson(child, true));}String childrenNames = group.get(0).getName();json.put(childrenNames, list);}}return json;} catch (JSONException e) {e.printStackTrace();}return null;}private void putContent(String path, JSONObject json, String tag, String content) {try {// checks if the user wants to force a class (Int, Double... for a given path)Class forcedClass = mForceClassForPath.get(path);if (forcedClass == null) { // default behaviour, put it as a Stringif (content == null) {content = DEFAULT_EMPTY_STRING;}json.put(tag, content);} else {if (forcedClass == Integer.class) {try {Integer number = Integer.parseInt(content);json.put(tag, number);} catch (NumberFormatException exception) {json.put(tag, DEFAULT_EMPTY_INTEGER);}} else if (forcedClass == Long.class) {try {Long number = Long.parseLong(content);json.put(tag, number);} catch (NumberFormatException exception) {json.put(tag, DEFAULT_EMPTY_LONG);}} else if (forcedClass == Double.class) {try {Double number = Double.parseDouble(content);json.put(tag, number);} catch (NumberFormatException exception) {json.put(tag, DEFAULT_EMPTY_DOUBLE);}} else if (forcedClass == Boolean.class) {if (content == null) {json.put(tag, DEFAULT_EMPTY_BOOLEAN);} else if (content.equalsIgnoreCase("true")) {json.put(tag, true);} else if (content.equalsIgnoreCase("false")) {json.put(tag, false);} else {json.put(tag, DEFAULT_EMPTY_BOOLEAN);}}}} catch (JSONException exception) {// keep continue in case of error}}private boolean isForcedList(Tag tag) {String path = tag.getPath();if (mForceListPaths.contains(path)) {return true;}for(Pattern pattern : mForceListPatterns) {Matcher matcher = pattern.matcher(path);if (matcher.find()) {return true;}}return false;}private String getAttributeNameReplacement(String path, String defaultValue) {String result = mAttributeNameReplacements.get(path);if (result != null) {return result;}return defaultValue;}private String getContentNameReplacement(String path, String defaultValue) {String result = mContentNameReplacements.get(path);/* if (result != null) {return result;}*/return result;}@Overridepublic String toString() {if (mJsonObject != null) {return mJsonObject.toString();}return null;}/*** Format the Json with indentation and line breaks** @param indentationPattern indentation to use, for example " " or "\t".* if null, use the default 3 spaces indentation* @return the formatted Json*/public String toFormattedString( String indentationPattern) {if (indentationPattern == null) {mIndentationPattern = DEFAULT_INDENTATION;} else {mIndentationPattern = indentationPattern;}return toFormattedString();}/*** Format the Json with indentation and line breaks.* Uses the last intendation pattern used, or the default one (3 spaces)** @return the Builder*/public String toFormattedString() {if (mJsonObject != null) {String indent = "";StringBuilder builder = new StringBuilder();builder.append("{\n");format(mJsonObject, builder, indent);builder.append("}\n");return builder.toString();}return null;}private void format(JSONObject jsonObject, StringBuilder builder, String indent) {Iterator<String> keys = jsonObject.keys();while (keys.hasNext()) {String key = keys.next();builder.append(indent);builder.append(mIndentationPattern);builder.append("\"");builder.append(key);builder.append("\": ");Object value = jsonObject.opt(key);if (value instanceof JSONObject) {JSONObject child = (JSONObject) value;builder.append(indent);builder.append("{\n");format(child, builder, indent + mIndentationPattern);builder.append(indent);builder.append(mIndentationPattern);builder.append("}");} else if (value instanceof JSONArray) {JSONArray array = (JSONArray) value;formatArray(array, builder, indent + mIndentationPattern);} else {formatValue(value, builder);}if (keys.hasNext()) {builder.append(",\n");} else {builder.append("\n");}}}private void formatArray(JSONArray array, StringBuilder builder, String indent) {builder.append("[\n");for (int i = 0; i < array.length(); ++i) {Object element = array.opt(i);if (element instanceof JSONObject) {JSONObject child = (JSONObject) element;builder.append(indent);builder.append(mIndentationPattern);builder.append("{\n");format(child, builder, indent + mIndentationPattern);builder.append(indent);builder.append(mIndentationPattern);builder.append("}");} else if (element instanceof JSONArray) {JSONArray child = (JSONArray) element;formatArray(child, builder, indent + mIndentationPattern);} else {formatValue(element, builder);}if (i < array.length() - 1) {builder.append(",");}builder.append("\n");}builder.append(indent);builder.append("]");}private void formatValue(Object value, StringBuilder builder) {if (value instanceof String) {String string = (String) value;// Escape special charactersstring = string.replaceAll("\\\\", "\\\\\\\\"); // escape backslashstring = string.replaceAll("\"", Matcher.quoteReplacement("\\\"")); // escape double quotesstring = string.replaceAll("/", "\\\\/"); // escape slashstring = string.replaceAll("\n", "\\\\n").replaceAll("\t", "\\\\t"); // escape \n and \tstring = string.replaceAll("\r", "\\\\r"); // escape \rbuilder.append("\"");builder.append(string);builder.append("\"");} else if (value instanceof Long) {Long longValue = (Long) value;builder.append(longValue);} else if (value instanceof Integer) {Integer intValue = (Integer) value;builder.append(intValue);} else if (value instanceof Boolean) {Boolean bool = (Boolean) value;builder.append(bool);} else if (value instanceof Double) {Double db = (Double) value;builder.append(db);} else {builder.append(value.toString());}}}
实现逻辑都写好了,集成到自己的项目中还是非常容易的,你只需要在你的项目中引入对应的Maven依赖
<dependencies><dependency><groupId>com.vaadin.external.google</groupId><artifactId>android-json</artifactId><version>RELEASE</version><scope>compile</scope></dependency><dependency><groupId>xmlpull</groupId><artifactId>xmlpull</artifactId><version>1.1.3.1</version></dependency><dependency><groupId>kxml2</groupId><artifactId>kxml2</artifactId><version>2.3.0</version></dependency></dependencies>
通过下面的工具类你就可以把 Single element XML to JSON Array。其中forceList()函数就是你需要设置强转成数组的节点绝对路径(xPath)
public class Test {public static void main(String [] args){String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" +"<library>" +" <books>" +"<book>" +"<name>Java</name>" +"</book>" +"</books>" +"</library>";XmlToJson xmlToJson2 = new XmlToJson.Builder(xml).forceList("/library/books/book").build();System.out.println(xmlToJson2);}
}
{"library": {"books": {"book": [{"name": "Java"}]}}
}
【踩坑】XML转JSON中如何把单个元素转成数组相关推荐
- Android 解决XXX Layout leaked 使用Navigation 踩坑 XML内存泄漏
Android 解决XXX Layout leaked 使用Navigation 踩坑 XML内存泄漏 报错日志 排查过程 泄漏原因 解决方案 最近维护一个项目,一个内存泄漏的的原因查了很久,这里记录 ...
- Android踩坑日记:RecyclerView中EditText和ImageView的ViewHolder复用坑
RecyclerView中EditText和ImageView的ViewHolder复用坑 RecyclerView作为ListView的升级版,目前来讲讲开发过程遇到的坑. RecyclerView ...
- 【踩坑】Linux java中ftp下载文件,解压文件损坏,以及图片下载打开只显示下载路径的问题
[踩坑]Linux java中ftp下载文件,解压文件损坏,以及图片下载打开只显示下载路径的问题 一. 问题重现 二. 问题解决思路 1. 确认是不是上传就导致数据出错了 2. 是不是平台问题 三. ...
- maxN - 返回数组中N个最大元素 minN - 返回数组中N个最小元素
从提供的数组中返回 n 个最小元素.如果 n 大于或等于提供的数组长度,则返回原数组(按降序排列). 结合使用Array.sort() 与展开操作符(...) ,创建一个数组的浅克隆,并按降序排列. ...
- python怎么做自动化测试仪器经销商_Python自动化测试踩坑记录(企业中如何实施自动化测试)...
企业中如何实施自动化测试 在我们读高中的时候, 是不是经常听老师说:学好数理化,走遍天下都不怕. 作为软件测试这个行业,在当下,你学好自动化,你去哪面试都不怕. 说是这么说,但是你想提前下班,自动化测 ...
- 【踩坑专栏】JSON parse error: Cannot deserialize value of type `java.util.Date` from String
出现这种报错的原因是无法将Date字符串解析为Date类型,之前我的做法是在需要转换的字段上标注注解@DateFormatPatter和@JsonFormat 这一次我懒得一个个的标了,因为是自己做的 ...
- 踩坑了,JDK8中HashMap依然会产生死循环问题!
点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 作者:Aaron_涛 blog.csdn.net/qq_3 ...
- java8 hashmap 死循环_踩坑了,JDK8中HashMap依然会死循环!
作者:Aaron_涛 原文:blog.csdn.net/qq_33330687/article/details/101479385 是否你听说过JDK8之后HashMap已经解决的扩容死循环的问题,虽 ...
- vue踩坑记-在项目中安装依赖模块npm install报错
在维护别人的项目的时候,在项目文件夹中安装npm install模块的时候,报错如下: 图片.png 图片.png npm ERR! path D:\ShopApp\node_modules\fsev ...
- 【踩坑】Win11 WSL2 中 meld 无法正常使用问题修复
Win11 中已经默认使用 wslg 来显示 WSL2 中的 GUI,不再需要额外开启 Xserver,这个确实是很方便实用的功能,但目前(2021.12)似乎还不是特别完美.我在体验的过程中就遇到了 ...
最新文章
- content 内容生成技术2
- 【Flutter】Dart 面向对象 ( 命名构造方法 | 工厂构造方法 | 命名工厂构造方法 )
- C/C++经典程序训练3---模拟计算器_JAVA
- 在Spring中使用JTA事务管理
- 前端学PHP之文件操作(认真读读)
- 什么是异常 java 1615309028
- c调用其他类的方法_吊打面试官-类加载器
- 九龙擒庄指标源码破译_擒庄系列:庄家难逃该指标,散户屡试不爽的秘籍!(附公式)...
- sql alwayson群集 registerallprovidersip改为0_技术分享 | 从 MySQL 8.0 复制到 MySQL 5.7
- Javaweb项目中文乱码总结
- 聊聊eureka的preferSameZoneEureka参数
- python算法常用技巧与内置库
- 使用npm安装yarn
- 节日头像小程序源码,直接部署可用!
- 从键盘输入一个四位数,输出该四位数的个位,十位,百位和千位数分别是什么。
- 揭秘郭台铭兄弟开店计划 苹果中国渠道裂变
- 基于3dmax及Unity的虚拟博物展览馆
- ip被流量攻击怎么办
- 阿里云大学云计算专业欢迎加入
- 服务器零点信号,常见流量计的零点校验
热门文章
- revit打开服务器文件格式,Revit文件导出格式大全(下)
- mysql 长连接_使用mysql的长连接
- VM安装windows server 2008
- 2020最新Javaweb视频教程-Javaweb从入门到精通【JSP】
- Android ROS开发环境搭建
- html背景半透明 字不变,css实现背景半透明文字不透明的效果示例
- postgresql14编译安装参考手册(centos)
- redhat6静默安装oracle11g,redhat6.2静默安装oracle11gr2
- python图像文字识别 - PyTesser
- STAMP软件 输入文件准备