如何在 Java 中解析大 (50 GB) XML 文件
Posted
技术标签:
【中文标题】如何在 Java 中解析大 (50 GB) XML 文件【英文标题】:How to Parse Big (50 GB) XML Files in Java 【发布时间】:2014-12-06 06:47:22 【问题描述】:目前我正在尝试使用 SAX 解析器,但大约 3/4 的文件完全冻结了,我尝试分配更多内存等但没有得到任何改进。
有什么办法可以加快速度吗?更好的方法?
将其剥离为裸露的骨头,所以我现在有了以下代码,并且在命令行中运行时它仍然没有我想要的那么快。
使用“java -Xms-4096m -Xmx8192m -jar reader.jar”运行它,我得到超过文章 700000 附近的 GC 开销限制
主要:
public class Read
public static void main(String[] args)
pages = XMLManager.getPages();
XML 管理器
public class XMLManager
public static ArrayList<Page> getPages()
ArrayList<Page> pages = null;
SAXParserFactory factory = SAXParserFactory.newInstance();
try
SAXParser parser = factory.newSAXParser();
File file = new File("..\\enwiki-20140811-pages-articles.xml");
PageHandler pageHandler = new PageHandler();
parser.parse(file, pageHandler);
pages = pageHandler.getPages();
catch (ParserConfigurationException e)
e.printStackTrace();
catch (SAXException e)
e.printStackTrace();
catch (IOException e)
e.printStackTrace();
return pages;
页面处理程序
public class PageHandler extends DefaultHandler
private ArrayList<Page> pages = new ArrayList<>();
private Page page;
private StringBuilder stringBuilder;
private boolean idSet = false;
public PageHandler()
super();
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
stringBuilder = new StringBuilder();
if (qName.equals("page"))
page = new Page();
idSet = false;
else if (qName.equals("redirect"))
if (page != null)
page.setRedirecting(true);
@Override
public void endElement(String uri, String localName, String qName) throws SAXException
if (page != null && !page.isRedirecting())
if (qName.equals("title"))
page.setTitle(stringBuilder.toString());
else if (qName.equals("id"))
if (!idSet)
page.setId(Integer.parseInt(stringBuilder.toString()));
idSet = true;
else if (qName.equals("text"))
String articleText = stringBuilder.toString();
articleText = articleText.replaceAll("(?s)<ref(.+?)</ref>", " "); //remove references
articleText = articleText.replaceAll("(?s)\\\\(.+?)\\\\", " "); //remove links underneath headings
articleText = articleText.replaceAll("(?s)==See also==.+", " "); //remove everything after see also
articleText = articleText.replaceAll("\\|", " "); //Separate multiple links
articleText = articleText.replaceAll("\\n", " "); //remove new lines
articleText = articleText.replaceAll("[^a-zA-Z0-9- \\s]", " "); //remove all non alphanumeric except dashes and spaces
articleText = articleText.trim().replaceAll(" +", " "); //convert all multiple spaces to 1 space
Pattern pattern = Pattern.compile("([\\S]+\\s*)1,75"); //get first 75 words of text
Matcher matcher = pattern.matcher(articleText);
matcher.find();
try
page.setSummaryText(matcher.group());
catch (IllegalStateException se)
page.setSummaryText("None");
page.setText(articleText);
else if (qName.equals("page"))
pages.add(page);
page = null;
else
page = null;
@Override
public void characters(char[] ch, int start, int length) throws SAXException
stringBuilder.append(ch,start, length);
public ArrayList<Page> getPages()
return pages;
【问题讨论】:
您确定什么是“冻结”(想向我们提供更多关于这对您的情况意味着什么的详细信息吗?)是 SAX 解析器而不是您的代码中的某些东西?您是否在应用程序的任何地方都将对象保存在内存中? 目前我只是在对其进行一些测试,但我有一种感觉,它可能是 Eclipse 冻结了(将其剥离成裸露的骨头,它会冻结)。目前通过命令行运行它,请随时关注。 添加了一些基本代码,仅在 xml 文件中输出读者正在阅读的文章 在 endElement() 例程结束时清除 StringBuilder。您实际上需要一堆字符串构建器来正确处理嵌套元素。 不是 stringBuilder = new StringBuilder();在 startElement 中“清零”了吗? 【参考方案1】:您的解析代码可能工作正常,但您正在加载的数据量可能太大而无法在 ArrayList
的内存中保存。
您需要某种管道来将数据传递到其实际目的地,而无需任何时间 一次将其全部存储在内存中。
我有时针对这种情况所做的类似于以下情况。
创建处理单个元素的接口:
public interface PageProcessor
void process(Page page);
通过构造函数向PageHandler
提供此实现:
public class Read
public static void main(String[] args)
XMLManager.load(new PageProcessor()
@Override
public void process(Page page)
// Obviously you want to do something other than just printing,
// but I don't know what that is...
System.out.println(page);
) ;
public class XMLManager
public static void load(PageProcessor processor)
SAXParserFactory factory = SAXParserFactory.newInstance();
try
SAXParser parser = factory.newSAXParser();
File file = new File("pages-articles.xml");
PageHandler pageHandler = new PageHandler(processor);
parser.parse(file, pageHandler);
catch (ParserConfigurationException e)
e.printStackTrace();
catch (SAXException e)
e.printStackTrace();
catch (IOException e)
e.printStackTrace();
将数据发送到此处理器而不是将其放入列表中:
public class PageHandler extends DefaultHandler
private final PageProcessor processor;
private Page page;
private StringBuilder stringBuilder;
private boolean idSet = false;
public PageHandler(PageProcessor processor)
this.processor = processor;
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) throws SAXException
//Unchanged from your implementation
@Override
public void characters(char[] ch, int start, int length) throws SAXException
//Unchanged from your implementation
@Override
public void endElement(String uri, String localName, String qName) throws SAXException
// Elide code not needing change
else if (qName.equals("page"))
processor.process(page);
page = null;
else
page = null;
当然,您可以让您的界面处理多条记录的块,而不仅仅是一条记录,并让PageHandler
将页面本地收集到一个较小的列表中,并定期发送该列表进行处理并清除该列表。
或者(也许更好)您可以实现此处定义的PageProcessor
接口,并在此处构建用于缓冲数据并将其发送到块中以进一步处理的逻辑。
【讨论】:
这就是我最终所做的,一次 10,000 页效果很好。 我刚刚开始研究类似的任务。这是一个自定义的 Page 类吗?【参考方案2】:Don Roby 的方法有点让人想起我所遵循的创建代码生成器的方法,该代码生成器旨在解决这个特定问题(早期版本是在 2008 年构思的)。基本上每个complexType
都有其Java POJO
等效项,并且当上下文更改为该元素时,特定类型的处理程序被激活。我将这种方法用于 SEPA、交易银行和例如 discogs (30GB)。您可以使用属性文件以声明方式指定要在运行时处理的元素。
XML2J 一方面使用complexTypes
到Java POJO 的映射,但允许您指定要监听的事件。
例如
account/@process = true
account/accounts/@process = true
account/accounts/@detach = true
精髓在第三行。分离确保不会将单个帐户添加到帐户列表中。所以不会溢出。
class AccountType
private List<AccountType> accounts = new ArrayList<>();
public void addAccount(AccountType tAccount)
accounts.add(tAccount);
// etc.
;
在你的代码中你需要实现 process 方法(默认情况下代码生成器会生成一个空方法:
class AccountsProcessor implements MessageProcessor
static private Logger logger = LoggerFactory.getLogger(AccountsProcessor.class);
// assuming Spring data persistency here
final String path = new ClassPathResource("spring-config.xml").getPath();
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(path);
AccountsTypeRepo repo = context.getBean(AccountsTypeRepo.class);
@Override
public void process(XMLEvent evt, ComplexDataType data)
throws ProcessorException
if (evt == XMLEvent.END)
if( data instanceof AccountType)
process((AccountType)data);
private void process(AccountType data)
if (logger.isInfoEnabled())
// do some logging
repo.save(data);
请注意,XMLEvent.END
标记元素的结束标记。因此,当您处理它时,它就完成了。如果您必须将其(使用 FK)与其在数据库中的父对象相关联,您可以为父对象处理 XMLEvent.BEGIN
,在数据库中创建一个占位符并使用其密钥与其每个子对象一起存储。在最后的XMLEvent.END
中,您将更新父级。
请注意,代码生成器会生成您需要的一切。您只需要实现该方法,当然还有 DB 粘合代码。
有一些示例可以帮助您入门。代码生成器甚至会生成您的 POM 文件,因此您可以在生成后立即构建您的项目。
默认的处理方式是这样的:
@Override
public void process(XMLEvent evt, ComplexDataType data)
throws ProcessorException
/*
* TODO Auto-generated method stub implement your own handling here.
* Use the runtime configuration file to determine which events are to be sent to the processor.
*/
if (evt == XMLEvent.END)
data.print( ConsoleWriter.out );
下载:
https://github.com/lolkedijkstra/xml2j-core https://github.com/lolkedijkstra/xml2j-gen https://sourceforge.net/projects/xml2j/首先mvn clean install
核心(它必须在本地maven repo 中),然后是生成器。并且不要忘记按照用户手册中的说明设置环境变量XML2J_HOME
。
【讨论】:
以上是关于如何在 Java 中解析大 (50 GB) XML 文件的主要内容,如果未能解决你的问题,请参考以下文章