在客户端用 JavaScript 逐行读取文件
Posted
技术标签:
【中文标题】在客户端用 JavaScript 逐行读取文件【英文标题】:Reading line-by-line file in JavaScript on client side 【发布时间】:2014-08-30 02:49:39 【问题描述】:您能帮我解决以下问题吗?
目标
在客户端逐行读取文件(在浏览器中通过 JS 和 html5 类),无需将整个文件加载到内存中。
场景
我正在处理应该在客户端解析文件的网页。目前,我正在阅读article 中描述的文件。
HTML:
<input type="file" id="files" name="files[]" />
$("#files").on('change', function(evt)
// creating FileReader
var reader = new FileReader();
// assigning handler
reader.onloadend = function(evt)
lines = evt.target.result.split(/\r?\n/);
lines.forEach(function (line)
parseLine(...);
);
;
// getting File instance
var file = evt.target.files[0];
// start reading
reader.readAsText(file);
问题在于 FileReader 一次读取整个文件,这会导致大文件(大小 >= 300 MB)的选项卡崩溃。使用reader.onprogress
并不能解决问题,因为它只会增加结果直到达到极限。
发明***
我在互联网上做了一些研究,但没有找到简单的方法来做到这一点(有很多文章描述了这个确切的功能,但在 node.js 的服务器端)。
作为解决它的唯一方法,我只看到以下内容:
-
按块分割文件(通过
File.split(startByte, endByte)
方法)
在该块中查找最后一个换行符 ('/n')
读取该块,除了最后一个换行符之后的部分,并将其转换为字符串并按行分割
从第 2 步中找到的最后一个换行符开始读取下一个块
但我最好使用已经存在的东西来避免熵增长。
【问题讨论】:
【参考方案1】:最终我创建了新的逐行阅读器,这与以前的阅读器完全不同。
特点是:
基于索引的文件访问(顺序和随机) 针对重复随机读取进行了优化(为过去已导航的行保存字节偏移的里程碑),因此在您读取所有文件一次后,访问第 43422145 行几乎与访问第 12 行一样快。 在文件中搜索:查找下一个和查找全部。 精确的索引、偏移量和匹配长度,以便您轻松突出显示它们查看此jsFiddle 以获取示例。
用法:
// Initialization
var file; // HTML5 File object
var navigator = new FileNavigator(file);
// Read some amount of lines (best performance for sequential file reading)
navigator.readSomeLines(startingFromIndex, function (err, index, lines, eof, progress) ... );
// Read exact amount of lines
navigator.readLines(startingFromIndex, count, function (err, index, lines, eof, progress) ... );
// Find first from index
navigator.find(pattern, startingFromIndex, function (err, index, match) ... );
// Find all matching lines
navigator.findAll(new RegExp(pattern), indexToStartWith, limitOfMatches, function (err, index, limitHit, results) ... );
性能与之前的解决方案相同。您可以在 jsFiddle 中调用“读取”来测量它。
GitHub:https://github.com/anpur/client-line-navigator/wiki
【讨论】:
npm 包即将推出【参考方案2】:更新:请检查我的第二个答案中的LineNavigator,那个阅读器要好得多。
我制作了自己的阅读器,满足了我的需求。
性能
由于问题仅与大文件有关,因此性能是最重要的部分。
如您所见,性能几乎与直接读取相同(如上所述)。
目前我正在努力让它变得更好,因为更大的时间消费者是异步调用以避免调用堆栈限制命中,这对于执行问题不是不必要的。性能问题已解决。
质量
测试了以下案例:
空文件 单行文件 文件末尾有换行符,没有 检查解析的行 在同一页面上多次运行 没有线路丢失,也没有订单问题代码和用法
HTML:
<input type="file" id="file-test" name="files[]" />
<div id="output-test"></div>
用法:
$("#file-test").on('change', function(evt)
var startProcessing = new Date();
var index = 0;
var file = evt.target.files[0];
var reader = new FileLineStreamer();
$("#output-test").html("");
reader.open(file, function (lines, err)
if (err != null)
$("#output-test").append('<span style="color:red;">' + err + "</span><br />");
return;
if (lines == null)
var milisecondsSpend = new Date() - startProcessing;
$("#output-test").append("<strong>" + index + " lines are processed</strong> Miliseconds spend: " + milisecondsSpend + "<br />");
return;
// output every line
lines.forEach(function (line)
index++;
//$("#output-test").append(index + ": " + line + "<br />");
);
reader.getNextBatch();
);
reader.getNextBatch();
);
代码:
function FileLineStreamer()
var loopholeReader = new FileReader();
var chunkReader = new FileReader();
var delimiter = "\n".charCodeAt(0);
var expectedChunkSize = 15000000; // Slice size to read
var loopholeSize = 200; // Slice size to search for line end
var file = null;
var fileSize;
var loopholeStart;
var loopholeEnd;
var chunkStart;
var chunkEnd;
var lines;
var thisForClosure = this;
var handler;
// Reading of loophole ended
loopholeReader.onloadend = function(evt)
// Read error
if (evt.target.readyState != FileReader.DONE)
handler(null, new Error("Not able to read loophole (start: )"));
return;
var view = new DataView(evt.target.result);
var realLoopholeSize = loopholeEnd - loopholeStart;
for(var i = realLoopholeSize - 1; i >= 0; i--)
if (view.getInt8(i) == delimiter)
chunkEnd = loopholeStart + i + 1;
var blob = file.slice(chunkStart, chunkEnd);
chunkReader.readAsText(blob);
return;
// No delimiter found, looking in the next loophole
loopholeStart = loopholeEnd;
loopholeEnd = Math.min(loopholeStart + loopholeSize, fileSize);
thisForClosure.getNextBatch();
;
// Reading of chunk ended
chunkReader.onloadend = function(evt)
// Read error
if (evt.target.readyState != FileReader.DONE)
handler(null, new Error("Not able to read loophole"));
return;
lines = evt.target.result.split(/\r?\n/);
// Remove last new line in the end of chunk
if (lines.length > 0 && lines[lines.length - 1] == "")
lines.pop();
chunkStart = chunkEnd;
chunkEnd = Math.min(chunkStart + expectedChunkSize, fileSize);
loopholeStart = Math.min(chunkEnd, fileSize);
loopholeEnd = Math.min(loopholeStart + loopholeSize, fileSize);
thisForClosure.getNextBatch();
;
this.getProgress = function ()
if (file == null)
return 0;
if (chunkStart == fileSize)
return 100;
return Math.round(100 * (chunkStart / fileSize));
// Public: open file for reading
this.open = function (fileToOpen, linesProcessed)
file = fileToOpen;
fileSize = file.size;
loopholeStart = Math.min(expectedChunkSize, fileSize);
loopholeEnd = Math.min(loopholeStart + loopholeSize, fileSize);
chunkStart = 0;
chunkEnd = 0;
lines = null;
handler = linesProcessed;
;
// Public: start getting new line async
this.getNextBatch = function()
// File wasn't open
if (file == null)
handler(null, new Error("You must open a file first"));
return;
// Some lines available
if (lines != null)
var linesForClosure = lines;
setTimeout(function() handler(linesForClosure, null) , 0);
lines = null;
return;
// End of File
if (chunkStart == fileSize)
handler(null, null);
return;
// File part bigger than expectedChunkSize is left
if (loopholeStart < fileSize)
var blob = file.slice(loopholeStart, loopholeEnd);
loopholeReader.readAsArrayBuffer(blob);
// All file can be read at once
else
chunkEnd = fileSize;
var blob = file.slice(chunkStart, fileSize);
chunkReader.readAsText(blob);
;
;
【讨论】:
更新,更快的版本即将推出(具有里程碑以加快对已读部分的随机访问)。 你可以在这里找到实际的、正确的版本:github.com/anpur/line-navigator【参考方案3】:为了同样的目的,我编写了一个名为 line-reader-browser 的模块。它使用Promises
。
语法(打字稿):-
import LineReader from "line-reader-browser"
// file is javascript File Object returned from input element
// chunkSize(optional) is number of bytes to be read at one time from file. defaults to 8 * 1024
const file: File
const chunSize: number
const lr = new LineReader(file, chunkSize)
// context is optional. It can be used to inside processLineFn
const context =
lr.forEachLine(processLineFn, context)
.then((context) => console.log("Done!", context))
// context is same Object as passed while calling forEachLine
function processLineFn(line: string, index: number, context: any)
console.log(index, line)
用法:-
import LineReader from "line-reader-browser"
document.querySelector("input").onchange = () =>
const input = document.querySelector("input")
if (!input.files.length) return
const lr = new LineReader(input.files[0], 4 * 1024)
lr.forEachLine((line: string, i) => console.log(i, line)).then(() => console.log("Done!"))
尝试以下代码 sn-p 以查看模块工作。
<html>
<head>
<title>Testing line-reader-browser</title>
</head>
<body>
<input type="file">
<script src="https://cdn.rawgit.com/Vikasg7/line-reader-browser/master/dist/tests/bundle.js"></script>
</body>
</html>
希望它可以节省某人的时间!
【讨论】:
以上是关于在客户端用 JavaScript 逐行读取文件的主要内容,如果未能解决你的问题,请参考以下文章