Java SAX解析器进度监控

12

我在Java中编写了一个用于解析2.5GB维基百科文章XML文件的SAX解析器,有没有一种方法能够监视Java中解析进度呢?

5个回答

11

感谢EJP的建议使用ProgressMonitorInputStream,最终我扩展了FilterInputStream以便可以使用ChangeListener来监视当前读取的字节数。

通过这种方式,您可以更精细地控制进度,例如为大型XML文件的并行读取显示多个进度条。这正是我所做的。

因此,可监测流的简化版本如下:

/**
 * A class that monitors the read progress of an input stream.
 *
 * @author Hermia Yeung "Sheepy"
 * @since 2012-04-05 18:42
 */
public class MonitoredInputStream extends FilterInputStream {
   private volatile long mark = 0;
   private volatile long lastTriggeredLocation = 0;
   private volatile long location = 0;
   private final int threshold;
   private final List<ChangeListener> listeners = new ArrayList<>(4);


   /**
    * Creates a MonitoredInputStream over an underlying input stream.
    * @param in Underlying input stream, should be non-null because of no public setter
    * @param threshold Min. position change (in byte) to trigger change event.
    */
   public MonitoredInputStream(InputStream in, int threshold) {
      super(in);
      this.threshold = threshold;
   }

   /**
    * Creates a MonitoredInputStream over an underlying input stream.
    * Default threshold is 16KB, small threshold may impact performance impact on larger streams.
    * @param in Underlying input stream, should be non-null because of no public setter
    */
   public MonitoredInputStream(InputStream in) {
      super(in);
      this.threshold = 1024*16;
   }

   public void addChangeListener(ChangeListener l) { if (!listeners.contains(l)) listeners.add(l); }
   public void removeChangeListener(ChangeListener l) { listeners.remove(l); }
   public long getProgress() { return location; }

   protected void triggerChanged( final long location ) {
      if ( threshold > 0 && Math.abs( location-lastTriggeredLocation ) < threshold ) return;
      lastTriggeredLocation = location;
      if (listeners.size() <= 0) return;
      try {
         final ChangeEvent evt = new ChangeEvent(this);
         for (ChangeListener l : listeners) l.stateChanged(evt);
      } catch (ConcurrentModificationException e) {
         triggerChanged(location);  // List changed? Let's re-try.
      }
   }


   @Override public int read() throws IOException {
      final int i = super.read();
      if ( i != -1 ) triggerChanged( location++ );
      return i;
   }

   @Override public int read(byte[] b, int off, int len) throws IOException {
      final int i = super.read(b, off, len);
      if ( i > 0 ) triggerChanged( location += i );
      return i;
   }

   @Override public long skip(long n) throws IOException {
      final long i = super.skip(n);
      if ( i > 0 ) triggerChanged( location += i );
      return i;
   }

   @Override public void mark(int readlimit) {
      super.mark(readlimit);
      mark = location;
   }

   @Override public void reset() throws IOException {
      super.reset();
      if ( location != mark ) triggerChanged( location = mark );
   }
}

它不知道 - 或者说不关心 - 底层流的大小,因此您需要通过其他方式获取它,比如从文件本身获取。

因此,这里提供了简化的示例用法:

try (
   MonitoredInputStream mis = new MonitoredInputStream(new FileInputStream(file), 65536*4) 
) {

   // Setup max progress and listener to monitor read progress
   progressBar.setMaxProgress( (int) file.length() ); // Swing thread or before display please
   mis.addChangeListener( new ChangeListener() { @Override public void stateChanged(ChangeEvent e) {
      SwingUtilities.invokeLater( new Runnable() { @Override public void run() {
         progressBar.setProgress( (int) mis.getProgress() ); // Promise me you WILL use MVC instead of this anonymous class mess! 
      }});
   }});
   // Start parsing. Listener would call Swing event thread to do the update.
   SAXParserFactory.newInstance().newSAXParser().parse(mis, this);

} catch ( IOException | ParserConfigurationException | SAXException e) {

   e.printStackTrace();

} finally {

   progressBar.setVisible(false); // Again please call this in swing event thread

}
在我的案例中,进度条从左到右平稳地增长,没有异常跳跃。调整阈值可以在性能和响应之间取得最佳平衡。如果太小,在小型设备上读取速度可能会超过两倍;如果太大,进度条将不会平滑。 希望对你有所帮助。如果你发现了错误或者打字错误,请随意编辑,或者点赞来为我加油! :D

太好了!正是我想要的,我会加以改编,谢谢! :) - Matthieu

10

使用javax.swing.ProgressMonitorInputStream


我认为这已经足够接近了。谢谢! - Danijel
1
还有比这更简单的答案吗?! :) - Matthieu

2

通过覆盖 org.xml.sax.helpers.DefaultHandler/BaseHandler 的方法 setDocumentLocator,可以获取文件的当前行/列的估计值。当需要时,该方法会使用一个对象调用,您可以从中获得当前行/列的近似值。

编辑:据我所知,没有标准方法可以获取绝对位置。然而,我确信一些 SAX 实现提供了这种信息。


接近了,但我需要知道文件中的行数,对吧? - Danijel
确实。另一个想法可能是由神秘的EJP指出的。您可以通过使用输入流中的进展来估计进度。然而,由于潜在的缓冲和前瞻性,这也不是解析的进展。 - Po' Lazarus

1

假设您知道有多少篇文章,那么您不能在处理程序中保持一个计数器吗?例如:

public void startElement (String uri, String localName, 
                          String qName, Attributes attributes) 
                          throws SAXException {
    if(qName.equals("article")){
        counter++
    }
    ...
}

(我不知道你是否正在解析“文章”,这只是一个例子)

如果您事先不知道文章数量,您需要先进行计数。然后您可以打印状态nb tags read/total nb of tags,每100个标签说一次(counter % 100 == 0)。

甚至可以有另一个线程监视进度。在这种情况下,您可能希望同步访问计数器,但不必要,因为它不需要真正准确。

我的两分钱


我已经想到了解决方法,但我希望能够在不需要先计算文章数量的情况下完成。我认为也许有一种方法可以找出解析器在文件中的位置,因为我可以轻松地获取文件大小。 - Danijel

0
我会使用输入流位置。创建一个自己的微不足道的流类,它从“真实”的流中委托/继承并跟踪读取的字节数。正如你所说,获取总文件大小很容易。我不会担心缓冲、前瞻等等——对于这样的大文件来说,这都是小意思。另一方面,我会将位置限制在“99%”。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接