如何用Java解析CSV文件

时间:2023-05-05 22:07:26

首先看一下csv文件的规则:
csv(Comma Separate Values)文件即逗号分隔符文件,它是一种文本文件,可以直接以文本打开,以逗号分隔。windows默认用excel打开。它的格式包括以下几点(它的格式最好就看excel是如何解析的。):

①每条记录占一行;
②以逗号为分隔符;
③逗号前后的空格会被忽略;
④字段中包含有逗号,该字段必须用双引号括起来;
⑤字段中包含有换行符,该字段必须用双引号括起来;
⑥字段前后包含有空格,该字段必须用双引号括起来;
⑦字段中的双引号用两个双引号表示;
⑧字段中如果有双引号,该字段必须用双引号括起来;
⑨第一条记录,可以是字段名;

⑩以上提到的逗号和双引号均为半角字符。

下面给出一种解析方法,来自:http://blog.csdn.net/studyvcmfc/article/details/6232770,原文中有一些bug,经过修改,测试ok的代码如下:

该解析算法的解析规则与excel或者wps大致相同。另外包含去掉注释的方法。

构建方法该类包含一个构建方法,参数为要读取的csv文件的文件名(包含绝对路径)。

普通方法:

① getVContent():一个得到当前行的值向量的方法。如果调用此方法前未调用readCSVNextRecord方法,则将返回Null。
② getLineContentVector():一个得到下一行值向量的方法。如果该方法返回Null,则说明已经读到文件末尾。
③ close():关闭流。该方法为调用该类后应该被最后调用的方法。
④ readCSVNextRecord():该方法读取csv文件的下一行,如果该方法已经读到了文件末尾,则返回false;
⑤ readAtomString(String):该方法返回csv文件逻辑一行的第一个值,和该逻辑行第一个值后面的内容,如果该内容以逗号开始,则已经去掉了该逗号。这两个值以一个二维数组的方法返回。
⑥ isQuoteAdjacent(String):判断一个给定字符串的引号是否两两相邻。如果两两相邻,返回真。如果该字符串不包含引号,也返回真。
⑦ readCSVFileTitle():该方法返回csv文件中的第一行——该行不以#号开始(包括正常解析后的#号),且该行不为空
解析接口代码:

  1. import java.io.BufferedReader;
  2. import java.io.FileNotFoundException;
  3. import java.io.FileReader;
  4. import java.io.IOException;
  5. import java.util.Vector;
  6. public class CsvParse {
  7. //声明读取流
  8. private BufferedReader inStream = null;
  9. //声明返回向量
  10. private Vector<String> vContent = null;
  11. /**
  12. * 构建方法,参数为csv文件名<br>
  13. * 如果没有找到文件,则抛出异常<br>
  14. * 如果抛出异常,则不能进行页面的文件读取操作
  15. */
  16. public CsvParse(String csvFileName) throws FileNotFoundException {
  17. inStream = new BufferedReader(new FileReader(csvFileName));
  18. }
  19. /**
  20. * 返回已经读取到的一行的向量
  21. * @return vContent
  22. */
  23. public Vector<String> getVContent() {
  24. return this.vContent;
  25. }
  26. /**
  27. * 读取下一行,并把该行的内容填充入向量中<br>
  28. * 返回该向量<br>
  29. * @return vContent 装载了下一行的向量
  30. * @throws IOException
  31. * @throws Exception
  32. */
  33. public Vector<String> getLineContentVector() throws IOException, Exception {
  34. if (this.readCSVNextRecord()) {
  35. return this.vContent;
  36. }
  37. return null;
  38. }
  39. /**
  40. * 关闭流
  41. */
  42. public void close() {
  43. if (inStream != null) {
  44. try {
  45. inStream.close();
  46. } catch (IOException e) {
  47. // TODO Auto-generated catch block
  48. e.printStackTrace();
  49. }
  50. }
  51. }
  52. /**
  53. * 调用此方法时应该确认该类已经被正常初始化<br>
  54. * 该方法用于读取csv文件的下一个逻辑行<br>
  55. * 读取到的内容放入向量中<br>
  56. * 如果该方法返回了false,则可能是流未被成功初始化<br>
  57. * 或者已经读到了文件末尾<br>
  58. * 如果发生异常,则不应该再进行读取
  59. * @return 返回值用于标识是否读到文件末尾
  60. * @throws Exception
  61. */
  62. public boolean readCSVNextRecord() throws IOException, Exception {
  63. //如果流未被初始化则返回false
  64. if (inStream == null) {
  65. return false;
  66. }
  67. //如果结果向量未被初始化,则初始化
  68. if (vContent == null) {
  69. vContent = new Vector<String>();
  70. }
  71. //移除向量中以前的元素
  72. vContent.removeAllElements();
  73. //声明逻辑行
  74. String logicLineStr = “”;
  75. //用于存放读到的行
  76. StringBuilder strb = new StringBuilder();
  77. //声明是否为逻辑行的标志,初始化为false
  78. boolean isLogicLine = false;
  79. try {
  80. while (!isLogicLine) {
  81. String newLineStr = inStream.readLine();
  82. if (newLineStr == null) {
  83. strb = null;
  84. vContent = null;
  85. isLogicLine = true;
  86. break;
  87. }
  88. if (newLineStr.startsWith(“#”)) {
  89. // 去掉注释
  90. continue;
  91. }
  92. if (!strb.toString().equals(“”)) {
  93. strb.append(“/r/n”);
  94. }
  95. strb.append(newLineStr);
  96. String oldLineStr = strb.toString();
  97. if (oldLineStr.indexOf(“,”) == -1) {
  98. // 如果该行未包含逗号
  99. if (containsNumber(oldLineStr, “\”") % 2 == 0) {
  100. // 如果包含偶数个引号
  101. isLogicLine = true;
  102. break;
  103. } else {
  104. if (oldLineStr.startsWith(“\”")) {
  105. if (oldLineStr.equals(“\”")) {
  106. continue;
  107. } else {
  108. String tempOldStr = oldLineStr.substring(1);
  109. if (isQuoteAdjacent(tempOldStr)) {
  110. // 如果剩下的引号两两相邻,则不是一行
  111. continue;
  112. } else {
  113. // 否则就是一行
  114. isLogicLine = true;
  115. break;
  116. }
  117. }
  118. }
  119. }
  120. } else {
  121. // quotes表示复数的quote
  122. String tempOldLineStr = oldLineStr.replace(“\”\”", “”);
  123. int lastQuoteIndex = tempOldLineStr.lastIndexOf(“\”");
  124. if (lastQuoteIndex == 0) {
  125. continue;
  126. } else if (lastQuoteIndex == -1) {
  127. isLogicLine = true;
  128. break;
  129. } else {
  130. tempOldLineStr = tempOldLineStr.replace(“\”,\”", “”);
  131. lastQuoteIndex = tempOldLineStr.lastIndexOf(“\”");
  132. if (lastQuoteIndex == 0) {
  133. continue;
  134. }
  135. if (tempOldLineStr.charAt(lastQuoteIndex - 1) == ’,') {
  136. continue;
  137. } else {
  138. isLogicLine = true;
  139. break;
  140. }
  141. }
  142. }
  143. }
  144. } catch (IOException ioe) {
  145. ioe.printStackTrace();
  146. //发生异常时关闭流
  147. if (inStream != null) {
  148. inStream.close();
  149. }
  150. throw ioe;
  151. } catch (Exception e) {
  152. e.printStackTrace();
  153. //发生异常时关闭流
  154. if (inStream != null) {
  155. inStream.close();
  156. }
  157. throw e;
  158. }
  159. if (strb == null) {
  160. // 读到行尾时为返回
  161. return false;
  162. }
  163. //提取逻辑行
  164. logicLineStr = strb.toString();
  165. if (logicLineStr != null) {
  166. //拆分逻辑行,把分离出来的原子字符串放入向量中
  167. while (!logicLineStr.equals(“”)) {
  168. String[] ret = readAtomString(logicLineStr);
  169. String atomString = ret[0];
  170. logicLineStr = ret[1];
  171. vContent.add(atomString);
  172. }
  173. }
  174. return true;
  175. }
  176. /**
  177. * 读取一个逻辑行中的第一个字符串,并返回剩下的字符串<br>
  178. * 剩下的字符串中不包含第一个字符串后面的逗号<br>
  179. * @param lineStr 一个逻辑行
  180. * @return 第一个字符串和剩下的逻辑行内容
  181. */
  182. public String[] readAtomString(String lineStr) {
  183. String atomString = “”;//要读取的原子字符串
  184. String orgString = “”;//保存第一次读取下一个逗号时的未经任何处理的字符串
  185. String[] ret = new String[2];//要返回到外面的数组
  186. boolean isAtom = false;//是否是原子字符串的标志
  187. String[] commaStr = lineStr.split(“,”);
  188. while (!isAtom) {
  189. for (String str : commaStr) {
  190. if (!atomString.equals(“”)) {
  191. atomString = atomString + “,”;
  192. }
  193. atomString = atomString + str;
  194. orgString = atomString;
  195. if (!isQuoteContained(atomString)) {
  196. // 如果字符串中不包含引号,则为正常,返回
  197. isAtom = true;
  198. break;
  199. } else {
  200. if (!atomString.startsWith(“\”")) {
  201. // 如果字符串不是以引号开始,则表示不转义,返回
  202. isAtom = true;
  203. break;
  204. } else if (atomString.startsWith(“\”")) {
  205. // 如果字符串以引号开始,则表示转义
  206. if (containsNumber(atomString, “\”") % 2 == 0) {
  207. // 如果含有偶数个引号
  208. String temp = atomString;
  209. if (temp.endsWith(“\”")) {
  210. temp = temp.replace(“\”\”", “”);
  211. if (temp.equals(“”)) {
  212. // 如果temp为空
  213. atomString = “”;
  214. isAtom = true;
  215. break;
  216. } else {
  217. // 如果temp不为空,则去掉前后引号
  218. temp = temp.substring(1, temp
  219. .lastIndexOf(“\”"));
  220. if (temp.indexOf(“\”") > -1) {
  221. // 去掉前后引号和相邻引号之后,若temp还包含有引号
  222. // 说明这些引号是单个单个出现的
  223. temp = atomString;
  224. temp = temp.substring(1);
  225. temp = temp.substring(0, temp
  226. .indexOf(“\”"))
  227. + temp.substring(temp
  228. .indexOf(“\”") + 1);
  229. atomString = temp;
  230. isAtom = true;
  231. break;
  232. } else {
  233. // 正常的csv文件
  234. temp = atomString;
  235. temp = temp.substring(1, temp
  236. .lastIndexOf(“\”"));
  237. temp = temp.replace(“\”\”", “\”");
  238. atomString = temp;
  239. isAtom = true;
  240. break;
  241. }
  242. }
  243. } else {
  244. // 如果不是以引号结束,则去掉前两个引号
  245. temp = temp.substring(1, temp.indexOf(‘\“‘, 1))
  246. + temp
  247. .substring(temp
  248. .indexOf(‘\”‘, 1) + 1);
  249. atomString = temp;
  250. isAtom = true;
  251. break;
  252. }
  253. } else {
  254. // 如果含有奇数个引号
  255. // TODO 处理奇数个引号的情况
  256. if (!atomString.equals(“\“”)) {
  257. String tempAtomStr = atomString.substring(1);
  258. if (!isQuoteAdjacent(tempAtomStr)) {
  259. // 这里做的原因是,如果判断前面的字符串不是原子字符串的时候就读取第一个取到的字符串
  260. // 后面取到的字符串不计入该原子字符串
  261. tempAtomStr = atomString.substring(1);
  262. int tempQutoIndex = tempAtomStr
  263. .indexOf(“\”");
  264. // 这里既然有奇数个quto,所以第二个quto肯定不是最后一个
  265. tempAtomStr = tempAtomStr.substring(0,
  266. tempQutoIndex)
  267. + tempAtomStr
  268. .substring(tempQutoIndex + 1);
  269. atomString = tempAtomStr;
  270. isAtom = true;
  271. break;
  272. }
  273. }
  274. }
  275. }
  276. }
  277. }
  278. }
  279. //先去掉之前读取的原字符串的母字符串
  280. if (lineStr.length() > orgString.length()) {
  281. lineStr = lineStr.substring(orgString.length());
  282. } else {
  283. lineStr = “”;
  284. }
  285. //去掉之后,判断是否以逗号开始,如果以逗号开始则去掉逗号
  286. if (lineStr.startsWith(“,”)) {
  287. if (lineStr.length() > 1) {
  288. lineStr = lineStr.substring(1);
  289. } else {
  290. lineStr = “”;
  291. }
  292. }
  293. ret[0] = atomString;
  294. ret[1] = lineStr;
  295. return ret;
  296. }
  297. /**
  298. * 该方法取得父字符串中包含指定字符串的数量<br>
  299. * 如果父字符串和字字符串任意一个为空值,则返回零
  300. * @param parentStr
  301. * @param parameter
  302. * @return
  303. */
  304. public int containsNumber(String parentStr, String parameter) {
  305. int containNumber = 0;
  306. if (parentStr == null || parentStr.equals(“”)) {
  307. return 0;
  308. }
  309. if (parameter == null || parameter.equals(“”)) {
  310. return 0;
  311. }
  312. for (int i = 0; i < parentStr.length(); i++) {
  313. i = parentStr.indexOf(parameter, i);
  314. if (i > -1) {
  315. i = i + parameter.length();
  316. i–;
  317. containNumber = containNumber + 1;
  318. } else {
  319. break;
  320. }
  321. }
  322. return containNumber;
  323. }
  324. /**
  325. * 该方法用于判断给定的字符串中的引号是否相邻<br>
  326. * 如果相邻返回真,否则返回假<br>
  327. *
  328. * @param p_String
  329. * @return
  330. */
  331. public boolean isQuoteAdjacent(String p_String) {
  332. boolean ret = false;
  333. String temp = p_String;
  334. temp = temp.replace(“\”\”", “”);
  335. if (temp.indexOf(“\”") == -1) {
  336. ret = true;
  337. }
  338. // TODO 引号相邻
  339. return ret;
  340. }
  341. /**
  342. * 该方法用于判断给定的字符串中是否包含引号<br>
  343. * 如果字符串为空或者不包含返回假,包含返回真<br>
  344. *
  345. * @param p_String
  346. * @return
  347. */
  348. public boolean isQuoteContained(String p_String) {
  349. boolean ret = false;
  350. if (p_String == null || p_String.equals(“”)) {
  351. return false;
  352. }
  353. if (p_String.indexOf(“\”") > -1) {
  354. ret = true;
  355. }
  356. return ret;
  357. }
  358. /**
  359. * 读取文件标题
  360. *
  361. * @return 正确读取文件标题时返回 true,否则返回 false
  362. * @throws Exception
  363. * @throws IOException
  364. */
  365. public boolean readCSVFileTitle() throws IOException, Exception {
  366. String strValue = “”;
  367. boolean isLineEmpty = true;
  368. do {
  369. if (!readCSVNextRecord()) {
  370. return false;
  371. }
  372. if (vContent.size() > 0) {
  373. strValue = (String) vContent.get(0);
  374. }
  375. for (String str : vContent) {
  376. if (str != null && !str.equals(“”)) {
  377. isLineEmpty = false;
  378. break;
  379. }
  380. }
  381. // csv 文件中前面几行以 # 开头为注释行
  382. } while (strValue.trim().startsWith(“#”) || isLineEmpty);
  383. return true;
  384. }
  385. }

其实呢。。。。作为一个标准,CSV的解析应该更简单,这里大放送,JVM的CSV解析结合:

http://ostermiller.org/utils/CSV.html

经测试,如下实例是最好用的:
https://github.com/segasai/SAI-CAS/tree/master/src/sai_cas/input/csv

from: http://jacoxu.com/?p=1490