用java sax处理xml文件(DBLP数据集)
在java中,可以用多种方式处理xml文件。前一段时间因为要使用到dblp数据集,而且这个数据集比较大无法一次性加载到内存中解析成文档树再处理。所以只能用sax的方式边读边处理。
下面是dblp数据集的简介,在处理xml文件之前,对xml的结构的了解很重要:
DBLP是计算机领域内对研究的成果以作者为核心的一个计算机类英文文献的集成数据库系统,按年代列出了作者的科研成果。包括国际期刊和会议等公开发表的论文。DBLP没有提供对中文文献的收录和检索功能,国内类似的权威期刊及重要会议论文集成检索系统有C-DBLP。
这个项目是德国特里尔大学的Michael Ley负责开发和维护。它提供计算机领域科学文献的搜索服务,但只储存这些文献的相关元数据,如标题,作者,发表日期等。和一般流行的情况不同,DBLP并没有使用数据库而是使用XML存储元数据。
DBLP在学术界声誉很高,而且很多论文及实验都是基于DBLP的。所收录的期刊和会议论文质量较高,也比较全面。文献更新速度很快,能很好地反应了国外学术研究的前沿方向。
在下载DBLP数据集后要记得下载dblp.dtd文件,这个文件是对数据集结构的说明:
数据集的根节点是: dblp 二级节点有:article|inproceedings|proceedings|book|incollection|phdthesis|mastersthesis|www 这些节点标识了文献的类型。三级节点有:booktitle ,pages,year。。。。等等,分别标识文献的题目/页数/年份等属性。
我的解析目标是将这个xml文件的数据集解析出来存储到关系型数据库中,比如mysql。
下面简单介绍一下如何再java用sax的方式解析xml文档:
主要需要写一个类继承 org.xml.sax.helpers.DefaultHandler(注意这里并非实现接口)
重写如下方法,这里对方法的作用也进行了说明:
/*用来遍历xml的开始标签 参数说明: uri:标签的命名空间uri,如果标签没有命名空间或者不需要命名空间处理,则为空值 localName:本地名,不需要命名空间处理则为空 qName:当前遍历的标签名 attributes:标签的属性,可以为空 */ @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { // TODO Auto-generated method stub } //用来遍历xml的结束标签 @Override public void endElement(String uri, String localName, String qName) throws SAXException { // TODO Auto-generated method stub } /* 获取标签中间的字符数据。比如对于节点<year>1991</year>获取的即为1991 ch:字符数据(以字符数组的形式) start:字符数组中的起始位置 length:从起始位置开始要取用的字符个数 可以通过String的构造方法获取自己需要的字符串 */ @Override public void characters(char[] ch, int start, int length) throws SAXException { // TODO Auto-generated method stub } //用来标识解析开始 @Override public void startDocument() throws SAXException { // TODO Auto-generated method stub } //用来标识解析结束 @Override public void endDocument() throws SAXException { // TODO Auto-generated method stub }
基于上述框架,我的解析思路如下:
首先要对数据集进行预处理,由于论文的作者来自世界各地,所以名字会有26个字母以外的字符,这些字符在xml文件中定义成变量,并在dtd文档中做了说明,但是在实际解析时这些变量的解析会带来一些问题。为了方便起见,我们可以将用于标识变量的‘&’符号事先替换掉。我用的是linux下的sed命令
sed -i 's/&/,/g' datasets/dblp.xml
然后,创建一个用于保存需要的文档属性的类:
package model; public class Document { public String doc_type; public String authors; public String doc_title; public int doc_year; public Document(){ this.authors = ""; this.doc_title = ""; this.doc_year = 0; } public String getDoc_type() { return doc_type; } public void setDoc_type(String doc_type) { this.doc_type = doc_type; } public String getAuthors() { return authors; } public void setAuthors(String authors) { this.authors = authors; } public String getDoc_title() { return doc_title; } public void setDoc_title(String doc_title) { this.doc_title = doc_title; } public int getDoc_year() { return doc_year; } public void setDoc_year(int doc_year) { this.doc_year = doc_year; } public String toString(){ return "type "+doc_type+" #authors: " + authors+ " #title: " + doc_title + "#year: " + doc_year ; } }这个类可以根据自己的需求来改,如果一篇文档有多个作者,我将作者名拼接成一个字符串,名字之间以字符 ‘|’ 来分隔。
然后写handler:
package handler; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.HashSet; import java.util.Set; import org.xml.sax.Attributes; import org.xml.sax.SAXException; import org.xml.sax.helpers.DefaultHandler; import db.Connector; import model.Document; public class SAXParserHandler extends DefaultHandler { static Connection conn = Connector.getConn(); static PreparedStatement pstmt = null; int docIndex = 0; Document doc = null; String value = null; boolean isEnd = false; public static Set<String> types = new HashSet<String>(); static { try { conn.setAutoCommit(false); pstmt = conn.prepareStatement("INSERT INTO doc1(" + "doc_type, doc_authors, doc_title, doc_year) VALUES ( ?, ?, ?, ?)"); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } types.add("article"); types.add("inproceedings"); types.add("proceedings"); types.add("book"); types.add("incollection"); types.add("phdthesis"); types.add("mastersthesis"); types.add("www"); } //用来遍历xml的开始标签 @Override public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException { // TODO Auto-generated method stub super.startElement(uri, localName, qName, attributes); if(types.contains(qName)) { doc = new Document(); isEnd = false; doc.setDoc_type(qName); } } //用来遍历xml的结束标签 @Override public void endElement(String uri, String localName, String qName) throws SAXException { // TODO Auto-generated method stub super.endElement(uri, localName, qName); if(qName.equals("dblp")){ try { pstmt.executeBatch(); conn.commit(); } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println("last batch committed!"); }else if(types.contains(qName)){ isEnd = true; docIndex++; //将Document存入数据库 try { pstmt.setString(1, doc.getDoc_type()); pstmt.setString(2, doc.getAuthors()); pstmt.setString(3, doc.getDoc_title()); pstmt.setInt(4, doc.getDoc_year()); pstmt.addBatch(); if (docIndex%50000 == 0){ System.out.println(docIndex); pstmt.executeBatch(); conn.commit(); } } catch (SQLException e) { System.out.println("insert to mysql error!"); e.printStackTrace(); } }else if (qName.equals("author") && isEnd ==false) { if (doc.getAuthors().equals("")){ doc.setAuthors(value); }else doc.setAuthors(doc.getAuthors()+"|"+value); }else if (qName.equals("title")&& isEnd ==false) { doc.setDoc_title(value); }else if (qName.equals("year") && isEnd ==false) { doc.setDoc_year(Integer.parseInt(value)); } } @Override public void characters(char[] ch, int start, int length) throws SAXException { // TODO Auto-generated method stub super.characters(ch, start, length); value = new String(ch, start, length); } //用来标识解析开始 @Override public void startDocument() throws SAXException { // TODO Auto-generated method stub super.startDocument(); System.out.println("XML parse begin!"); } //用来标识解析结束 @Override public void endDocument() throws SAXException { // TODO Auto-generated method stub super.endDocument(); System.out.println("XML parse end!"); } }
再startElement方法和 characters方法中将数据存入模型,在endElement方法种将数据存入数据库。
说明一下:
我用 isend 来标识一篇文档是否已经遍历结束。在 startElement中遇到article|inproceedings|proceedings|book|incollection|phdthesis|mastersthesis|www 这些标签,将isend置为false,在endElement中遇到这些标签将isend置为true。
由于数据量非常大(500多W条记录),将数据存入数据库时使用批量插入的方式插入,否则会频繁的创建数据库链接,导致内存溢出。
主方法如下:
package parser; import java.io.IOException; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; import javax.xml.parsers.SAXParserFactory; import org.xml.sax.SAXException; import handler.SAXParserHandler; public class MyParser { public static void main(String[] args) { Long start = System.currentTimeMillis(); // sss SAXParserFactory factory = SAXParserFactory.newInstance(); //通过factory获取SAXParser实例 try { SAXParser parser = factory.newSAXParser(); //创建SAXParserHandler对象 SAXParserHandler handler = new SAXParserHandler(); parser.parse(args[0], handler); } catch (ParserConfigurationException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (SAXException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } Long end = System.currentTimeMillis(); System.out.println("Used: " + (end - start) / 1000 + " seconds"); } }
运行时需要添加JVM参数:-Xmx1G -DentityExpansionLimit=2500000
完整的代码已经放到github上: https://github.com/hunanatnjupt/DBLParser 在运行代码之前需要用sql脚本在数据库中创建数据库和表。