【问题标题】:Is there a simple way to convert Excel inline strings to shared strings table in Java?有没有一种简单的方法可以将 Excel 内联字符串转换为 Java 中的共享字符串表?
【发布时间】:2018-06-02 15:25:33
【问题描述】:

我正在尝试创建一个简单的 Java 程序来将 Excel 文件从内联字符串转换为共享字符串表以减小文件大小。

我知道 Apache POI 有一个 SXSSFWorkbook 类可以完成这项工作,但是使用 SAX XML 解析器读取带有内联字符串的大型 xlsx 文件仍然会失败。例如 150,000 行 x 50 列单元格。

是否有不使用 Apache POI 库来完成简单工作的简单解决方案?有谁知道吗?

【问题讨论】:

  • “...做简单的工作”:这与简单的工作相反 ;-)。使用内联字符串,所有字符串都位于工作表 XML 文件的单元格中。要创建sharedStrings.xml,需要:遍历工作表 XML 文件中的所有单元格以获取内联字符串。然后查找sharedStrings.xml 字符串是否已经存在。如果是,则获取 ID,否则在 sharedStrings.xml 中创建一个新字符串并获取 ID。然后将 ID 放入工作表 XML 文件的单元格中,而不是内联字符串值。然后是工作表 XML 文件中的下一个单元格。

标签: java excel


【解决方案1】:

虽然首先创建其中包含内联字符串的工作表,然后用共享字符串替换这些内联字符串会非常低效,但我将回答如何完成这个问题。

需要的是:遍历工作表的 XML 文件中的所有单元格以获取内联字符串。然后查找sharedStrings.xml 字符串是否已经存在。如果是,则获取 ID,否则在 sharedStrings.xml 中创建一个新字符串并获取 ID。然后将 ID 放入工作表 XML 文件的单元格中,而不是内联字符串值。

以下代码正在执行此操作。如果TestInlineStrings.xlsx 在第一张表中有内联字符串,则在此代码运行后,这些内联字符串将被替换为共享字符串。

import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackagePart;

import org.apache.poi.xssf.model.SharedStringsTable;

import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRst;

import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.XMLEvent;

import javax.xml.namespace.QName;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;

import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import java.util.Iterator;

class StaxReplaceInlineStrings {

 public static void main(String[] args) {
  try {

   File file = new File("TestInlineStrings.xlsx");
   OPCPackage opcpackage = OPCPackage.open(file);

   //if there are strings in the sheet data, we need the SharedStringsTable
   PackagePart sharedstringstablepart = opcpackage.getPartsByName(Pattern.compile("/xl/sharedStrings.xml")).get(0);
   SharedStringsTable sharedstringstable = new SharedStringsTable();
   sharedstringstable.readFrom(sharedstringstablepart.getInputStream());

   PackagePart sheetpart = opcpackage.getPartsByName(Pattern.compile("/xl/worksheets/sheet1.xml")).get(0);

   XMLEventReader reader = XMLInputFactory.newInstance().createXMLEventReader(sheetpart.getInputStream());
   XMLEventWriter writer = XMLOutputFactory.newInstance().createXMLEventWriter(sheetpart.getOutputStream());

   XMLEventFactory eventFactory = XMLEventFactory.newInstance();


   while(reader.hasNext()){ //loop over all XML in sheet1.xml

    boolean cellReplaced = false; //marker whether cell having inline string was replaced by cell having shared string

    XMLEvent event = (XMLEvent)reader.next();
    if(event.isStartElement()){
     StartElement startElement = (StartElement)event;
     QName startElementName = startElement.getName();
     if (startElementName.getLocalPart().equalsIgnoreCase("c")) { //start element of cell
      Attribute attribute;
      StartElement cellStart = startElement; //remember cell start
      Iterator attributeIterator = cellStart.getAttributes(); //get cell's attributes
      while (attributeIterator.hasNext()) {
       attribute = (Attribute)attributeIterator.next();
       if ("t".equals(attribute.getName().getLocalPart())) { //cell has type attribute
        String tvalue = attribute.getValue();
        if ("inlineStr".equals(tvalue)) { //cell type is inline string
         String inlineString = "";
         startElement = (StartElement)(XMLEvent)reader.next(); //read next start element - error if is not a start element
         startElementName = startElement.getName();
         if (startElementName.getLocalPart().equalsIgnoreCase("is")) { //start element of inline string     
          startElement = (StartElement)(XMLEvent)reader.next(); //read next start element - error if is not a start element
          startElementName = startElement.getName();
          if (startElementName.getLocalPart().equalsIgnoreCase("t")) { //start element of text
           Characters characters = (Characters)(XMLEvent)reader.next(); //read next characters element - error if is not a characters element   
           inlineString = characters.getData(); //get text data  
System.out.println(inlineString); 
          }
         }

         //create shared string in shared strings table
         CTRst ctstr = CTRst.Factory.newInstance();
         ctstr.setT(inlineString);
         int sRef = sharedstringstable.addEntry(ctstr);

         //we are replacing the cell element so skip elements until end element of cell
         while(reader.hasNext()) {
          event = (XMLEvent)reader.next();
          if(event.isEndElement()){
           EndElement endElement = (EndElement)event;
           QName endElementName = endElement.getName();
           if (endElementName.getLocalPart().equalsIgnoreCase("c")) { //end element of cell 
            break;
           }
          }
         }

         //create the new cell element having the shared string
         Attribute r = cellStart.getAttributeByName(new QName("r"));
         Attribute s = cellStart.getAttributeByName(new QName("s"));
         Attribute t = eventFactory.createAttribute("t", "s");
         List attributeList = Arrays.asList(new Attribute[]{t});
         if (r != null && s != null) {
          attributeList = Arrays.asList(new Attribute[]{r, s, t});
         } else if (r != null) {
          attributeList = Arrays.asList(new Attribute[]{r, t});
         } else if (s != null) {
          attributeList = Arrays.asList(new Attribute[]{s, t});
         }
System.out.println(attributeList);
         StartElement newCellStart = eventFactory.createStartElement(new QName("c"), attributeList.iterator(), null);
         writer.add(newCellStart);
         StartElement newCellValue = eventFactory.createStartElement(new QName("v"), null, null);
         writer.add(newCellValue);
         Characters value = eventFactory.createCharacters(Integer.toString(sRef));
         writer.add(value);         
         EndElement newCellValueEnd = eventFactory.createEndElement(new QName("v"), null);
         writer.add(newCellValueEnd);
         EndElement newCellEnd = eventFactory.createEndElement(new QName("c"), null);
         writer.add(newCellEnd);

         cellReplaced = true; // mark that cell was replaced
         break;
        }
       } 
      }
     }
    }
    if (!cellReplaced) {
     writer.add(event); //by default write each read event, except cell was replaced
    }
   }
   writer.flush();

   //write the SharedStringsTable
   OutputStream out = sharedstringstablepart.getOutputStream();
   sharedstringstable.writeTo(out);
   out.close();

   opcpackage.close();

  } catch (Exception ex) {
     ex.printStackTrace();
  }
 }
}

【讨论】:

  • 谢谢阿克塞尔。这是我一直在寻找的。在调用 XMLEventReader 迭代器(你的是 reader)之前,是否可以提前知道元素的数量?
  • @Trevor:不,这是基于事件的,这也是内存消耗非常低的原因。而StAX 仅转发。因此,如果需要知道事件的计数,那么这个必须至少遍历一次。另请参阅stackoverflow.com/questions/9720195/…
  • 阿克塞尔:是的,这是有道理的。现在这个 StAX 解决方案非常适合我的需求。谢谢你。用 116575 行 x 46 列(超过 530 万个单元格)替换大型 xlsx 电子表格中的内联字符串需要 59 秒(使用 Intel Core i5 CPU)。还不错。
  • @nantitv:使用内联字符串而不是共享字符串会更好地提高流性能,因为不需要使用额外的共享字符串表。但是,每个单独的字符串出现都需要单独存储在工作表中,即使它出现了 1000 次。使用共享字符串表可以避免这种情况。一个字符串只存储一次,如果需要,只有它的 id 会在工作表中存储 1000ds 次。
  • @nantitv:在poi.apache.org/components/spreadsheet/how-to.html#sxssf 中有解释。 “不再在窗口中的旧行变得不可访问,因为它们被写入磁盘。”意味着对于每个SXSSFSheet,都有一个临时文件,行数据将流入其中。在进程结束时,所有临时文件合并在一个工作簿中。
【解决方案2】:

Adding a row to a large xlsx file (Out of Memory) 中,我提供了一种使用StAX 将行写入Excel 工作表的方法,而无需打开整个工作簿。但是使用了共享字符串表。

所以这里有一个稍微修改过的版本。

您将开始拥有这样的ReadAndWriteTest.xlsx

每次运行代码时,将添加 100,000 行,在列 A 中包含一个随机字符串,在列 B 中包含一个随机双精度值。字符串将由共享字符串表管理。因此,这个共享字符串表中的唯一字符串将比工作表中的字符串总和少得多。

我在生产性使用中使用了这种方法,当然,代码更复杂、更有条理,因为此代码示例仅应以简单代码显示该方法。它运行良好,比SXSSF 性能更高,并提供阅读写作。

import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.opc.PackagePart;

import org.apache.poi.xssf.model.SharedStringsTable;

import org.openxmlformats.schemas.spreadsheetml.x2006.main.CTRst;

import javax.xml.stream.XMLEventFactory;
import javax.xml.stream.XMLEventReader;
import javax.xml.stream.XMLEventWriter;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.events.Characters;
import javax.xml.stream.events.StartElement;
import javax.xml.stream.events.EndElement;
import javax.xml.stream.events.Attribute;
import javax.xml.stream.events.XMLEvent;

import javax.xml.namespace.QName;

import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;

import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;

import java.util.concurrent.ThreadLocalRandom;

class StaxReadAndWriteTest {

 public static void main(String[] args) {
  try {

   String loremipsum = "Lorem ipsum dolor sit amet ne mei euismod interpretaris est te iusto causae doctus.";

   File file = new File("ReadAndWriteTest.xlsx");
   OPCPackage opcpackage = OPCPackage.open(file);

   //if there are strings in the sheet data, we need the SharedStringsTable
   PackagePart sharedstringstablepart = opcpackage.getPartsByName(Pattern.compile("/xl/sharedStrings.xml")).get(0);
   SharedStringsTable sharedstringstable = new SharedStringsTable();
   sharedstringstable.readFrom(sharedstringstablepart.getInputStream());

   PackagePart sheetpart = opcpackage.getPartsByName(Pattern.compile("/xl/worksheets/sheet1.xml")).get(0);

   XMLEventReader reader = XMLInputFactory.newInstance().createXMLEventReader(sheetpart.getInputStream());
   XMLEventWriter writer = XMLOutputFactory.newInstance().createXMLEventWriter(sheetpart.getOutputStream());

   XMLEventFactory eventFactory = XMLEventFactory.newInstance();

   int rowsCount = 0;

   while(reader.hasNext()){ //loop over all XML in sheet1.xml
    XMLEvent event = (XMLEvent)reader.next();
    writer.add(event); //by default write each readed event

    if(event.isStartElement()){
     StartElement startElement = (StartElement)event;
     QName startElementName = startElement.getName();
     if(startElementName.getLocalPart().equalsIgnoreCase("row")) { //start element of row
      boolean rowStart = true;
      rowsCount++;
      do {
       event = (XMLEvent)reader.next(); //find this row's end
       writer.add(event); //by default write each readed event

       if(event.isEndElement()){
        EndElement endElement = (EndElement)event;
        QName endElementName = endElement.getName();
        if(endElementName.getLocalPart().equalsIgnoreCase("row")) { //end element of row
         rowStart = false;
         //we assume that there is nothing else (character data) between end element of row and next element 
         XMLEvent nextElement = (XMLEvent)reader.peek();
         QName nextElementName = null;
         if (nextElement.isStartElement()) nextElementName = ((StartElement)nextElement).getName();
         else if (nextElement.isEndElement()) nextElementName = ((EndElement)nextElement).getName();
         if(!nextElementName.getLocalPart().equalsIgnoreCase("row")) { //next is not start element of row
          //we have the last row, so we write new rows now 

          for (int i = 0; i < 100000; i++) {

           StartElement newRowStart = eventFactory.createStartElement(new QName("row"), null, null);
           writer.add(newRowStart);

//start cell A
           Attribute attribute = eventFactory.createAttribute("t", "s");
           List attributeList = Arrays.asList(attribute);
           StartElement newCellStart = eventFactory.createStartElement(new QName("c"), attributeList.iterator(), null);
           writer.add(newCellStart);

           CTRst ctstr = CTRst.Factory.newInstance();

           //create a random string from loremipsum
           int length = ThreadLocalRandom.current().nextInt(5, 20);
           int index = ThreadLocalRandom.current().nextInt(0, loremipsum.length() - length);
           //set randoom string in CTRst
           ctstr.setT(loremipsum.substring(index, index + length).trim());
           //update SharedStringsTable with CTRst and get sRef as the ID of this string
           int sRef = sharedstringstable.addEntry(ctstr);

           StartElement newCellValue = eventFactory.createStartElement(new QName("v"), null, null);
           writer.add(newCellValue);

           //set sRef of the string as content of cell A
           Characters value = eventFactory.createCharacters(Integer.toString(sRef));
           writer.add(value);         

           EndElement newCellValueEnd = eventFactory.createEndElement(new QName("v"), null);
           writer.add(newCellValueEnd);

           EndElement newCellEnd = eventFactory.createEndElement(new QName("c"), null);
           writer.add(newCellEnd);
//end cell A
//start cell B
           newCellStart = eventFactory.createStartElement(new QName("c"), null, null);
           writer.add(newCellStart);

           newCellValue = eventFactory.createStartElement(new QName("v"), null, null);
           writer.add(newCellValue);

           //set random double value as content of cell B
           value = eventFactory.createCharacters(""+ThreadLocalRandom.current().nextDouble((double)length));
           writer.add(value);         

           newCellValueEnd = eventFactory.createEndElement(new QName("v"), null);
           writer.add(newCellValueEnd);

           newCellEnd = eventFactory.createEndElement(new QName("c"), null);
           writer.add(newCellEnd);
//end cell B

           EndElement newRowEnd = eventFactory.createEndElement(new QName("row"), null);
           writer.add(newRowEnd);

           rowsCount++;
          }
         }
        }
       }
      } while (rowStart);
     }
    }
   }

   writer.flush();

   //write the SharedStringsTable
   OutputStream out = sharedstringstablepart.getOutputStream();
   sharedstringstable.writeTo(out);
   out.close();

   opcpackage.close();

  } catch (Exception ex) {
     ex.printStackTrace();
  }
 }
}

【讨论】:

  • 感谢阿克塞尔的想法。我注意到代码创建了一系列来自loremipsum 字符串的随机子字符串。我的目标是将基于内联字符串的 xlsx 转换为基于共享字符串表的 xlsx 输出,其中每个单词都不是随机的。我认为您的代码只是作为示例展示如何创建共享字符串表?那么如何一次性更新sharedStrings.xmlsheet1.xml
  • @Trevor:代码显示了如何将行写入 Excel 工作表,而无需打开整个工作簿。但是使用了共享字符串表。因此内存使用量被最小化而没有内联字符串的缺点。首先创建其中包含内联字符串的工作表,然后用共享字符串替换这些内联字符串将是非常低效的。这不是代码的用途。但请参阅我的其他答案。
猜你喜欢
  • 2010-09-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-05
  • 2021-12-30
  • 2011-02-27
  • 1970-01-01
相关资源
最近更新 更多