在某些业务场景下,你可能需要把一个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中如何把单个元素转成数组相关推荐

  1. Android 解决XXX Layout leaked 使用Navigation 踩坑 XML内存泄漏

    Android 解决XXX Layout leaked 使用Navigation 踩坑 XML内存泄漏 报错日志 排查过程 泄漏原因 解决方案 最近维护一个项目,一个内存泄漏的的原因查了很久,这里记录 ...

  2. Android踩坑日记:RecyclerView中EditText和ImageView的ViewHolder复用坑

    RecyclerView中EditText和ImageView的ViewHolder复用坑 RecyclerView作为ListView的升级版,目前来讲讲开发过程遇到的坑. RecyclerView ...

  3. 【踩坑】Linux java中ftp下载文件,解压文件损坏,以及图片下载打开只显示下载路径的问题

    [踩坑]Linux java中ftp下载文件,解压文件损坏,以及图片下载打开只显示下载路径的问题 一. 问题重现 二. 问题解决思路 1. 确认是不是上传就导致数据出错了 2. 是不是平台问题 三. ...

  4. maxN - 返回数组中N个最大元素 minN - 返回数组中N个最小元素

    从提供的数组中返回 n 个最小元素.如果 n 大于或等于提供的数组长度,则返回原数组(按降序排列). 结合使用Array.sort() 与展开操作符(...) ,创建一个数组的浅克隆,并按降序排列. ...

  5. python怎么做自动化测试仪器经销商_Python自动化测试踩坑记录(企业中如何实施自动化测试)...

    企业中如何实施自动化测试 在我们读高中的时候, 是不是经常听老师说:学好数理化,走遍天下都不怕. 作为软件测试这个行业,在当下,你学好自动化,你去哪面试都不怕. 说是这么说,但是你想提前下班,自动化测 ...

  6. 【踩坑专栏】JSON parse error: Cannot deserialize value of type `java.util.Date` from String

    出现这种报错的原因是无法将Date字符串解析为Date类型,之前我的做法是在需要转换的字段上标注注解@DateFormatPatter和@JsonFormat 这一次我懒得一个个的标了,因为是自己做的 ...

  7. 踩坑了,JDK8中HashMap依然会产生死循环问题!

    点击上方蓝色"方志朋",选择"设为星标" 回复"666"获取独家整理的学习资料! 作者:Aaron_涛 blog.csdn.net/qq_3 ...

  8. java8 hashmap 死循环_踩坑了,JDK8中HashMap依然会死循环!

    作者:Aaron_涛 原文:blog.csdn.net/qq_33330687/article/details/101479385 是否你听说过JDK8之后HashMap已经解决的扩容死循环的问题,虽 ...

  9. vue踩坑记-在项目中安装依赖模块npm install报错

    在维护别人的项目的时候,在项目文件夹中安装npm install模块的时候,报错如下: 图片.png 图片.png npm ERR! path D:\ShopApp\node_modules\fsev ...

  10. 【踩坑】Win11 WSL2 中 meld 无法正常使用问题修复

    Win11 中已经默认使用 wslg 来显示 WSL2 中的 GUI,不再需要额外开启 Xserver,这个确实是很方便实用的功能,但目前(2021.12)似乎还不是特别完美.我在体验的过程中就遇到了 ...

最新文章

  1. content 内容生成技术2
  2. 【Flutter】Dart 面向对象 ( 命名构造方法 | 工厂构造方法 | 命名工厂构造方法 )
  3. C/C++经典程序训练3---模拟计算器_JAVA
  4. 在Spring中使用JTA事务管理
  5. 前端学PHP之文件操作(认真读读)
  6. 什么是异常 java 1615309028
  7. c调用其他类的方法_吊打面试官-类加载器
  8. 九龙擒庄指标源码破译_擒庄系列:庄家难逃该指标,散户屡试不爽的秘籍!(附公式)...
  9. sql alwayson群集 registerallprovidersip改为0_技术分享 | 从 MySQL 8.0 复制到 MySQL 5.7
  10. Javaweb项目中文乱码总结
  11. 聊聊eureka的preferSameZoneEureka参数
  12. python算法常用技巧与内置库
  13. 使用npm安装yarn
  14. 节日头像小程序源码,直接部署可用!
  15. 从键盘输入一个四位数,输出该四位数的个位,十位,百位和千位数分别是什么。
  16. 揭秘郭台铭兄弟开店计划 苹果中国渠道裂变
  17. 基于3dmax及Unity的虚拟博物展览馆
  18. ip被流量攻击怎么办
  19. 阿里云大学云计算专业欢迎加入
  20. 服务器零点信号,常见流量计的零点校验

热门文章

  1. revit打开服务器文件格式,Revit文件导出格式大全(下)
  2. mysql 长连接_使用mysql的长连接
  3. VM安装windows server 2008
  4. 2020最新Javaweb视频教程-Javaweb从入门到精通【JSP】
  5. Android ROS开发环境搭建
  6. html背景半透明 字不变,css实现背景半透明文字不透明的效果示例
  7. postgresql14编译安装参考手册(centos)
  8. redhat6静默安装oracle11g,redhat6.2静默安装oracle11gr2
  9. python图像文字识别 - PyTesser
  10. STAMP软件 输入文件准备