Использование локального файла в качестве источника данных в JavaScript

Предыстория:

Я хочу создать «приложение», которое использует только JavaScript/HTML и может быть открыто браузером непосредственно из файловой системы. Это приложение должно иметь возможность считывать данные из другого файла. Затем я буду использовать JS для его анализа и отображения страниц. В качестве упрощенного примера представьте, что у меня есть CSV-файл (скачать здесь):

Mark Rodgers,[email protected],Accounting
[...]
Melissa Jones,[email protected],CEO

Я хочу иметь возможность читать файл с помощью JS и использовать данные в нем для создания моей страницы.

Что я уже сделал:

Демо (щелкните правой кнопкой мыши -> "Сохранить как", чтобы сохранить HTML на свой компьютер). Он также доступен на jsfiddle в частично сломанном виде (макет нарушен, но функционально он должен быть корректным). ).

Просто перетащите текстовый файл CSV в поле перетаскивания или выберите текстовый файл с помощью меню файлов, и JavaScript прочитает, проанализирует файл и заполнит таблицу.

Это зависит от API FileReader; большая часть тяжелой работы выполняется этой функцией:

function handleFileSelect(evt) {
    evt.stopPropagation();
    evt.preventDefault();

    var files = evt.target.files || evt.dataTransfer.files; // FileList object.
    var file = files[0];

    // this creates the FileReader and reads stuff as text
    var fr = new FileReader();
    fr.onload = parse;
    fr.readAsText(file);

    // this is the function that actually parses the file
    // and populates the table
    function parse()
    {
        var table = document.getElementById('emps');
        var employees = fr.result.split('\n'); var c = 0;
        for (var i in employees)
        {
            var employee = employees[i].split(',');
            if (employee.length == 3)
            {
                var row = document.createElement('tr');
                row.innerHTML = "<td>" + employee.join("</td><td>") + "</td>";
                table.appendChild(row);
                c++;
            }
        }
        document.getElementById('result').innerHTML = '<span>Added ' + c + ' employees from file: ' + file.name + '</span>';
    }
}

Это почти нормально, но пользователю неудобно загружать файл вручную. В идеале он должен иметь возможность загружать его автоматически, но из соображений безопасности ни один браузер этого не позволит... пока.

Требования к решению:

  • Должен работать в автономном режиме; то есть: он не может полагаться на какой-либо онлайн-сервис. Сюда также входят HTTP-серверы, работающие на локальном компьютере. Идея состоит в том, чтобы запустить его на любом компьютере, где установлен только браузер.

  • Должен работать, когда страница открыта с использованием протокола file:/// (т.е. HTML-страница на жестком диске).

  • Не следует не полагаться на сторонние дополнения (например, Flash, Java, вздрагивает ActiveX). Я почти уверен, что они, вероятно, все равно не сработают, если страница находится в file:///.

  • Он должен иметь возможность принимать произвольные данные. Это исключает загрузку файла в удобном для использования формате, таком как JSON.

  • Если он работает на любом (в идеале на обоих) Firefox или Chrome, все в порядке. Также можно полагаться на экспериментальные API.

Я заранее знаю имя файла, поэтому его можно закодировать в самом HTML. Любое решение, которое позволяет мне читать файл с диска, подходит, оно не должно использовать API FileReader.

Так что, если есть умный хак для загрузки файла на страницу, это тоже нормально (возможно, загрузить его в невидимый iframe и заставить JS получить содержимое); тоже норм.


person NullUserException    schedule 17.11.2012    source источник
comment
В Chrome вы должны указать параметр командной строки --allow-file-access-from-files, чтобы разрешить доступ к схеме file://. Я не уверен, что Chrome позволит сделать это каким-либо другим, более удобным способом, из соображений безопасности.   -  person Stan    schedule 26.11.2012
comment
‹идентификатор сценария=тип файла=текст/html src=file.txt›‹/сценарий›   -  person Bart Calixto    schedule 26.11.2012
comment
Не уверен, но проект node-webkit может быть вам интересен. github.com/rogerwang/node-webkit   -  person net.uk.sweet    schedule 27.11.2012
comment
если вам нужна только mozilla (firefox/seamonkey) или chrome, вы можете использовать XMLHttpRequest в файле:// (хотя хром потребует, чтобы эта функция была включена), если веб-страница также находится в файле:// - я обновил свой ответьте с примером (без проверки браузера, для этого уже есть много примеров). Я почти уверен, что это не сработает, если сама страница не находится также в файле:// (по крайней мере, не должна, если это произойдет, это огромная дыра в безопасности, о которой следует немедленно сообщить)   -  person technosaurus    schedule 28.11.2012
comment
Есть ли причина, по которой вы не просто используете html5 offline API? В основном то же самое, за исключением того, что вам даже не нужно ничего сохранять на компьютере. Кроме того, с помощью FileSystem API входной файл должен запрашиваться только один раз.   -  person Esailija    schedule 30.11.2012
comment
@Esailija Большим недостатком этого является то, что теперь вы привязаны к одному браузеру и одному компьютеру: эти данные невозможно перенести (конечно, вы можете создать функции экспорта и импорта, но это противоречит цели того, что я пытаюсь сделать) . Не говоря уже о том, что на него накладываются всевозможные ограничения, когда HTML запускается из file:///.   -  person NullUserException    schedule 30.11.2012
comment
@NullUserException Я имел в виду использование API файловой системы с автономным API, чтобы у вас не было ограничений file:///. Когда компьютер подключен к сети, можно ли перенести содержимое виртуальной файловой системы?   -  person Esailija    schedule 30.11.2012
comment
@Esailija Для этого потребуется, чтобы страница размещалась где-то (например, в Интернете, на локальном HTTP-сервере), верно?   -  person NullUserException    schedule 30.11.2012
comment
@NullUserException да, страницу изначально нужно откуда-то скачать. Но требуется только статический файл, обслуживающий http-сервер, и он может работать на локальном хосте.   -  person Esailija    schedule 30.11.2012
comment
@Esailija Это именно то, чего я пытаюсь избежать.   -  person NullUserException    schedule 30.11.2012
comment
Я вижу, я просто не понимаю, как вы собираетесь распространять html-файл, если не через интернет. Пользователю изначально также необходимо загрузить файл в file:/// case.   -  person Esailija    schedule 30.11.2012
comment
@Esailija Подумайте об электронной почте и онлайн-хранилищах, таких как Dropbox или Google Drive. В случае с Google Диском, поскольку он синхронизируется автоматически, я хочу просто открыть HTML-файл, и содержимое страницы волшебным образом появится для меня.   -  person NullUserException    schedule 30.11.2012


Ответы (6)


Вот код, который я использовал для Firefox, который не переносим, ​​но работает:

Как прокомментировал OP, enablePrivilege() устарел, его следует считать пригодным для использования. Но поскольку мой Firefox, использующий предыдущий профиль, все еще работает с моим кодом, поэтому я немного покопался в prefs.js (поскольку about:config скрывает эти настройки). И вот настройки, которые вам нужны, чтобы они заработали.

user_pref("capability.principal.codebase.p0.granted", "UniversalXPConnect");
user_pref("capability.principal.codebase.p0.id", "file://");  // path to the html file.
user_pref("capability.principal.codebase.p0.subjectName", "");

И вот код:

var File = function(file) {
  netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
  var ios = Components.classes["@mozilla.org/network/io-service;1"]
                            .getService(Components.interfaces.nsIIOService);
  if (!File.baseURI) {
    File.baseURI = ios.newURI(location.href.substring(0, location.href.lastIndexOf('/')+1), null, null);
    File.baseFolder = File.baseURI.QueryInterface(Components.interfaces.nsIFileURL).file.path;
  }
  var URL = ios.newURI(file, null, File.baseURI);
  this.fptr = URL.QueryInterface(Components.interfaces.nsIFileURL).file;
}

File.prototype = {
  write: function(data) {
    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
    var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
                             .createInstance(Components.interfaces.nsIFileOutputStream);
    foStream.init(this.fptr, 0x02 | 0x08 | 0x20, 0666, 0);
    var converter = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
                              .createInstance(Components.interfaces.nsIConverterOutputStream);
    converter.init(foStream, null, 0, 0);
    converter.writeString(data);
    converter.close();
  },
  read: function() {
    netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
    var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]
                            .createInstance(Components.interfaces.nsIFileInputStream);
    var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
                            .createInstance(Components.interfaces.nsIConverterInputStream);
    fstream.init(this.fptr, -1, 0, 0);
    cstream.init(fstream, null, 0, 0);
    var data = "";
    // let (str = {}) { // use this only when using javascript 1.8
    var str = {};
      cstream.readString(0xffffffff, str);
      data = str.value;
    // }
    cstream.close();
    return data;
  }
};
person xiaoyi    schedule 17.11.2012
comment
Какой аргумент принимает File()? - person NullUserException; 17.11.2012
comment
@NullUserException путь к файлу относительно текущего файла. нравится var file = new File('xxx.csv'); - person xiaoyi; 17.11.2012
comment
Хм, Firefox задыхается от этой строки: let (str = {}) { - person NullUserException; 17.11.2012
comment
@NullUserException Я уже обновил свое решение. Это было в спецификациях Javascript 1.8. Просто используйте мой последний код. - person xiaoyi; 17.11.2012
comment
Черт, теперь FF говорит мне Error: A script from "file://" was denied UniversalXPConnect privileges., что я попытаюсь поэкспериментировать с настройками позже, но, судя по тому, что я читаю здесь, это выглядит не очень хорошо: bugzilla.mozilla.org/show_bug.cgi?id=790913 - person NullUserException; 17.11.2012

Вот пример использования данных JSON во внешнем файле, который работает локально или на сервере. В этом примере просто используется настройка языка браузера для загрузки скрипта ‹ > с локализованным HTML, а затем обрабатывается его объект json для сброса данных в указанных тегах с локализованным содержимым.

<html><meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<head>
<script>
    function setLang(){
        for (var i=0;i<items.length;i++){
            term=document.getElementById(items[i].id)
            if (term) term.innerHTML=items[i].value
        }
    }
    var lang=navigator.userLanguage || navigator.language;
    var script=document.createElement("script");
    script.src=document.URL+"-"+lang.substring(0,2)+".js"
    var head = document.getElementsByTagName('head')[0]
    head.insertBefore(script,head.firstChild)
</script>
</head>
<body onload='setLang()'>
<div id="string1" class="txt">This is the default text of string1.</div> 
<div id="string2" class="txt">This is the default text of string2.</div>
</body></html>

Файлы данных для этого выглядят так:

items=[
{"id":"string1","value":"Localized text of string1."},
{"id":"string2", "value":"Localized text of string2."}
];

но вы можете использовать любой параметр для условной загрузки соответствующего файла (он будет вставлен как первый тег в ‹ head >, поэтому его можно будет использовать где угодно), а формат JSON способен обрабатывать большое количество данных. Возможно, вы захотите переименовать функцию setLang во что-то более подходящее и изменить ее в соответствии с вашими потребностями, например... для каждого я добавляю строку, а затем добавляю поля с данными (похоже, у вас уже есть дескриптор этой части) и ваш JSON будет выглядеть так:

items=[
{"fname":"john","lname":"smith","address":"1 1st St","phone":"555-1212"},
{"fname":"jane","lname":"smith","address":"1 1st St","phone":"555-1212"}
];

если вам нужно предварительно обработать ваши данные, awk очень удобен - это будет что-то вроде: (непроверенная оценка)

awk 'BEGIN{FS=",";print "items=[\n"}
{printf "{\"fname\":\"%s\",\"lname\":\"smith\",\"address\":\"1 1st St\",\"phone\":\"555-1212\"},\n", $1, $2, $3, $4}
END{print "];"}' file.csv > file.js

Редактировать: теперь, когда OP более ясен, только браузеры Mozilla разрешают XMLHttpRequest для файла: // из коробки, а chrome (возможно, другие браузеры на основе webkit) можно настроить, чтобы разрешить это. Зная, что он может НЕ работать в IE‹10, вы можете:

var filePath = "your_file.txt";
xmlhttp = new XMLHttpRequest();
xmlhttp.open("GET",filePath,false);
xmlhttp.overrideMimeType('text/plain');
xmlhttp.send(null);
//maybe check status !=404 here
var fileContent = xmlhttp.responseText;
var fileArray = fileContent.split('\n')
var n = fileArray.length;
//process your data from here probably using split again for ','

Я оставляю первоначальный вариант json-p для других, у которых может быть аналогичная проблема, но они имеют некоторый контроль над своим форматом данных, поскольку он будет работать во всех браузерах, поддерживающих javascript. Однако, если кто-нибудь знает способ заставить его работать для IE (кроме запуска небольшого веб-сервера), пожалуйста, отредактируйте.

Редактировать 2:

В браузерах Mozilla вы также можете использовать iframes.

<html>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<head>
<script>
function showContents(frameObject){
    alert(frameObject.contentDocument.body.innerHTML);
    //replace with your code
}
</script>
</head>
<body onload='showContents()'>
<iframe id="frametest" src="data.txt" onload="showContents(this);" 
    style="visibility:hidden;display:none"></iframe>
</body></html>
person technosaurus    schedule 27.11.2012
comment
Извините, но я не контролирую формат файлов и не могу предварительно обработать файлы перед их загрузкой с помощью JS. Иначе я бы не задавал этот вопрос, потому что это самый простой выход. Он должен иметь возможность обрабатывать произвольные данные. - person NullUserException; 27.11.2012
comment
На самом деле это не файл JSON, а скрипт JSONP. - person Bergi; 27.11.2012
comment
правда, я только сказал, что это файл, содержащий данные json, потому что я не хотел подразумевать, что ему нужна оболочка функции обратного вызова в стиле json-p. В основном он действует как включаемый файл, который может содержать переменные, функции и т. д. ... но поскольку OP обновил формат, фиксированный как csv, XMLHttpRequest действительно является единственным способом, а не переносимым, если нет это какой-то хак, чтобы получить innerHTML внешнего скрипта... метод iframe работает по умолчанию только в браузерах Mozilla - person technosaurus; 28.11.2012
comment
+1 Это было первое, что я попробовал, и у меня ничего не вышло из-за слишком строгих политик безопасности Chrome. Работал из коробки на ФФ. Работает в Chrome, если вы используете --allow-file-access-from-files, а также работает в IE10 (появляется всплывающее окно с вопросом, хотите ли вы разрешить заблокированный контент). В отличие от FileReader, Ajax может немного запутаться при работе с бинарными файлами, но я думаю, что смогу с этим смириться. Кажется, это лучшее решение на данный момент. - person NullUserException; 30.11.2012
comment
@NullUserException Ajax может немного запутаться при обработке двоичных файлов. Это вызвано нормализацией. Если вы ожидаете двоичные ответы, используйте соответствующий атрибут responseType экземпляра XHR, например. "arraybuffer" или "blob". - person Rob W; 11.12.2012
comment
@RobW Ах да. Я не знал об этом, когда писал комментарий. Похоже, это относительно новое дополнение к XHR. - person NullUserException; 18.01.2013

Предполагая, что файл csv находится в том же каталоге, что и приложение, я бы загрузил файл с помощью AJAX. Насколько я знаю, можно получить файл в текстовом формате, а затем разобрать его. Это должно работать в IE и Firefox, но не работает в Chrome (если только вы не запустите chrome с настройкой командной строки --allow-file-access-from-files).

person Inkbug    schedule 03.12.2012

Это можно сделать довольно легко, используя класс javascript XMLHttpRequest():

function FileHelper()
{}
{
    FileHelper.readStringFromFileAtPath = function(pathOfFileToReadFrom)
    {
        var request = new XMLHttpRequest();
        request.open("GET", pathOfFileToReadFrom, false);
        request.send(null);
        var returnValue = request.responseText;

        return returnValue;
    }
}

...

var text = FileHelper.readStringFromFileAtPath ( "mytext.txt" );
person user3375451    schedule 03.03.2014

Я так понимаю, содержимое файла полностью под вашим контролем, и это не обязательно должен быть какой-то определенный формат? А нужен только способ чтения?

Вы можете объявить глобальную функцию "handleFile". В вашем внешнем файле содержимое должно быть таким:

handleFile('Mark Rodgers,[email protected],Accounting');

Чтобы «прочитать» файл, просто добавьте элемент сценария с соответствующим атрибутом src. В вашей функции «handleFile» вы получаете свое содержимое.

Местоположение файла, вероятно, изначально должно быть установлено пользователем, но после этого вы можете сохранить местоположение в localStorage или что-то в этом роде.

person dave    schedule 26.11.2012
comment
К сожалению, формат файла не находится под моим контролем. - person NullUserException; 26.11.2012

Убедившись, что файл находится в том же каталоге или в подкаталоге, загрузите файл с помощью AJAX.

В отличие от тега script, вы получите доступ к содержимому.

person Gerard Sexton    schedule 27.11.2012