如何在 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 文件的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中解析非常大的 XML 文件? [复制]

在 php 中解析非常大的 XML 文件

如何在 R 中读取大 (~20 GB) xml 文件?

用于非常大的 XML 文件的 SAX 解析器

java - 如何在java dom解析器中格式化xml? [复制]

如何解析这个xml文件里边的字符串,谁解答一下,加高分