Pull to refresh

Открываем файлы формата Open XML Excel в JavaScript

Reading time 4 min
Views 20K
Для загрузки информации о торговых точках в наш логистический SaaS-сервис «Муравьиная логистика» из Excel я решил использовать web-браузер. Обычно проще загрузить файл на сервер и с помощью любой библиотеки залить в БД. Но мне было интересно загрузить его построчно для контроля целостности каждой строки на клиенте, ну и, конечно, опробовать так всеми рекламируемое HTML5 FileAPI и Drag and Drop.

Книга Exсel – это ZIP архив с каталогами и файлами XML в формате Open XML. Парсить XML отлично умеет jQuery, а вот зиппить нет. Для этого на просторах сети была найдена библиотека zip.js, которая прекрасно справилась с поставленной задачей.

Итак, попробуем посмотреть, что же находится внутри архива:
<div class="main">
    <progress id="progress"></progress>
    <div class="filedrag" id="comps">Перетащите файл <span class="red">сюда</span></div>
    <div class="result"></div>
</div>

var c = document.getElementById("comps"),
    FileDragHover = function (e) {
     e.stopPropagation();
     e.preventDefault();
     if(e.target.id==='comps')
       e.target.className = (e.type == "dragover" ? "filedrag hover" : "filedrag");
     else
       c.className = (e.type == "dragover" ? "filedrag hover" : "filedrag");
    }
    c.addEventListener("drop", function(e){
    e.preventDefault();

    c.className = "filedrag";
    var files = e.target.files || e.dataTransfer.files;
    for (var i = 0, f; f = files[i]; i++) {
        if(f.name.toLowerCase().indexOf('xlsx')<=0) {
            alert('Это не файл Excel');
        } else {
            zip.createReader(new zip.BlobReader(f), function(reader) {
                // Получаем все файлы архива
                reader.getEntries(function(entries) {
                   // В консоли появятся все внутренности архива Excel
                   console.info(entries)
                   return false;
               });
            }, function(error) {
                alert("Ошибка: " + error)
            });
        }
    }

    return false;
}, false);

c.addEventListener("dragover", FileDragHover, false);
c.addEventListener("dragleave", FileDragHover, false);


Результат можно посмотреть тут. Скачайте пример файла и перетащите его на форму.
В консоли появится список всех файлов архива книги Excel. Среди свойств объектов, появившихся в консоли, есть filename, по нему-то мы и будем искать необходимые нам файлы XML.

Нам понадобятся два файла из архива:
  • import.xlsx\xl\worksheets\sheet[N].xlsx
  • import.xlsx\xl\sharedStrings.xml

где:
sheet[N].xlsx — собственно лист Excel, N — его внутренний номер в книге.
sharedStrings.xml — ассоциативный массив строк, словарь листа.

Отфильтруем только нужные для нас файлы:
// Получаем все файлы архива
reader.getEntries(function(entries) {
        var a=[],st;
        for(var i in entries){
                var e=entries[i];
                var fn=e.filename.toLowerCase();
                if(fn.indexOf("sheet")>0){
                        a.push(e);
                }
                else if(fn.indexOf("sharedstring")>0){
                        st=e;
                }
        }
    // Массив всех листов книги Excel
    console.info(a)
    // Ассоциативный массив строк
    console.info(st)
    return false;
});


Результат можно посмотреть тут, закинув файл и посмотрев в консоль.

Далее нам необходимо извлечь данные простыми селекторами, для словаря строк это — st t, для записей таблицы с данными на листе это — sheetdata row.

Добавим функцию для вывода данных из листа Excel:
printExcelData = function(sheets, strings) {
        var unzipProgress = document.getElementById("progress");
    unzipProgress.style.display='block';

     strings.getData(new zip.TextWriter(), function(text) {
         // Получаем все строки листа для ассоциации с их кодами
         var i,st=$($.parseXML(decodeURIComponent(escape(text)))).find('si t');
         for(i=0;i<st.length;++i)
             st[i]=$(st[i]).text();

         // Перебираем листы в поисках нужного
         var parseSheet=function(sheet){
             var j,i,h,sh,d=[],s;
             sheet.getData(new zip.TextWriter(), function(text) {
                 // а вот и наши записи
                 sh=$($.parseXML(decodeURIComponent(escape(text)))).find('sheetdata row');

                 // делаем из строки объект
                 sh.each(function(e){
                     var c=$(this).find('c'),ci,v,o={};
                     for(i=0;i<c.length;++i){
                         ci=$(c[i]);
                         v=ci.find('v').text();
                         if(ci.attr('t'))
                             v=st[v];
                         j=ci.attr('r').charCodeAt(0)-65;
                         if(h)
                             o[h[j]]=v;
                         else
                             o[j]=v;
                     }
                     if(h){
                         d.push(o)
                     } else
                         h=o;
                 });

                 var id_name="";
                 for(i in h)
                     if(h[i]=='Comp_Id'){
                         id_name=h[i];
                         break;
                     }

                 // Если поле Comp_Id есть в записи, значит лист наш
                 if(id_name=='Comp_Id') {
                     unzipProgress.style.display='none';

                     // Это заголовок таблицы данных
                     s="";
                     for(i=0;i<Object.keys(h).length;i++)
                         s+='<th>'+h[i]+'</th>';
                     $('.result thead tr').append(s)

                     // Это данные
                     s="";
                     for(j=0; j<d.length; j++){
                         s+='<tr>';
                         for(i=0; i<Object.keys(h).length; i++){
                             s+='<td>'+d[j][h[i]].toString()+'</td>';
                         }
                         s+='</tr>';
                     }
                     $('.result tbody').append(s)
                     sheets=[];
                     return;
                 }

                 if(sheets.length>0)
                     parseSheet(sheets.pop());
             }, function(current, total) {
                 unzipProgress.value = current;
                 unzipProgress.max = total;
             });
         }
         parseSheet(sheets.pop());
     }, function(current, total) {
         unzipProgress.value = current;
         unzipProgress.max = total;
     });

 }


Так как Chrome считает преступлением использование HTML File API в кросс-домене (Uncaught SecurityError: An attempt was made to break through the security policy of the user agent.), последний пример выложил на Web-сервер.
Перетаскиваем файл и получаем стандартную таблицу HTML.

P.S.
Да, сейчас, как оказалось, есть Open XML SDK for JavaScript, но это тема для отдельной статьи…
Tags:
Hubs:
+4
Comments 2
Comments Comments 2

Articles