テキストファイルでストリームリーダーの位置(行番号)を知る方法は?
-
06-07-2019 - |
質問
例(実生活ではないかもしれませんが、私のポイントを示すため):
public void StreamInfo(StreamReader p)
{
string info = string.Format(
"The supplied streamreaer read : {0}\n at line {1}",
p.ReadLine(),
p.GetLinePosition()-1);
}
GetLinePosition
は、streamreaderの想像上の拡張メソッドです。
これは可能ですか?
もちろん自分で数え続けることもできますが、それは問題ではありません。
解決
任意のTextReaderに行カウントラッパーを提供するのは非常に簡単です。
public class PositioningReader : TextReader {
private TextReader _inner;
public PositioningReader(TextReader inner) {
_inner = inner;
}
public override void Close() {
_inner.Close();
}
public override int Peek() {
return _inner.Peek();
}
public override int Read() {
var c = _inner.Read();
if (c >= 0)
AdvancePosition((Char)c);
return c;
}
private int _linePos = 0;
public int LinePos { get { return _linePos; } }
private int _charPos = 0;
public int CharPos { get { return _charPos; } }
private int _matched = 0;
private void AdvancePosition(Char c) {
if (Environment.NewLine[_matched] == c) {
_matched++;
if (_matched == Environment.NewLine.Length) {
_linePos++;
_charPos = 0;
_matched = 0;
}
}
else {
_matched = 0;
_charPos++;
}
}
}
欠点(簡潔にするため):
- コンストラクタの引数のnullをチェックしません
- 行を終了する別の方法を認識しません。生の\ rまたは\ nで区切られたファイルを読み取るときのReadLine()の動作と矛盾します。
- Read(char []、int、int)、ReadBlock、ReadLine、ReadToEndのような「ブロック」レベルのメソッドをオーバーライドしません。 TextReader実装は、他のすべてをRead()にルーティングするため、正しく機能します。ただし、パフォーマンスを向上させるには
- _innerへの呼び出しのルーティングを介してこれらのメソッドをオーバーライドします。ベースの代わりに。
- 読み取った文字をAdvancePositionに渡す。サンプルのReadBlock実装を参照してください。
public override int ReadBlock(char[] buffer, int index, int count) {
var readCount = _inner.ReadBlock(buffer, index, count);
for (int i = 0; i < readCount; i++)
AdvancePosition(buffer[index + i]);
return readCount;
}
他のヒント
特定の行にStreamReaderを探す必要がある同様の問題の解決策を探している間、この投稿に目を通します。最終的に、StreamReaderの位置を取得および設定する2つの拡張メソッドを作成しました。実際には行番号のカウントを提供しませんが、実際には、各ReadLine()の前の位置を取得し、行に関心がある場合は、後で設定するために開始位置を保持してそのように行に戻ります:
var index = streamReader.GetPosition();
var line1 = streamReader.ReadLine();
streamReader.SetPosition(index);
var line2 = streamReader.ReadLine();
Assert.AreEqual(line1, line2);
および重要な部分:
public static class StreamReaderExtensions
{
readonly static FieldInfo charPosField = typeof(StreamReader).GetField("charPos", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | BindingFlags.DeclaredOnly);
readonly static FieldInfo byteLenField = typeof(StreamReader).GetField("byteLen", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | BindingFlags.DeclaredOnly);
readonly static FieldInfo charBufferField = typeof(StreamReader).GetField("charBuffer", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | BindingFlags.DeclaredOnly);
public static long GetPosition(this StreamReader reader)
{
//shift position back from BaseStream.Position by the number of bytes read
//into internal buffer.
int byteLen = (int)byteLenField.GetValue(reader);
var position = reader.BaseStream.Position - byteLen;
//if we have consumed chars from the buffer we need to calculate how many
//bytes they represent in the current encoding and add that to the position.
int charPos = (int)charPosField.GetValue(reader);
if (charPos > 0)
{
var charBuffer = (char[])charBufferField.GetValue(reader);
var encoding = reader.CurrentEncoding;
var bytesConsumed = encoding.GetBytes(charBuffer, 0, charPos).Length;
position += bytesConsumed;
}
return position;
}
public static void SetPosition(this StreamReader reader, long position)
{
reader.DiscardBufferedData();
reader.BaseStream.Seek(position, SeekOrigin.Begin);
}
}
これは私にとって非常にうまく機能し、反射を使用するためのあなたの許容度に応じて、それはかなり簡単な解決策だと思います。
警告:
- さまざまなSystem.Text.Encodingオプションを使用して簡単なテストを行っていますが、これで使用するデータのほとんどは単純なテキストファイル(ASCII)です。
- StreamReader.ReadLine()メソッドのみを使用し、StreamReaderのソースの簡単なレビューでは、他の読み取りメソッドを使用してもこれが機能することを示しているようですが、実際にはそのシナリオをテストしていません。
いいえ、できません。 「行番号」の概念位置だけでなく、すでに読み取られた実際のデータに基づいています。たとえば、リーダーを任意の位置にSeek()した場合、そのデータを実際に読み取ることはないため、行番号を判別できません。
これを行う唯一の方法は、自分で追跡することです。
いいえ。
基礎となるストリームオブジェクト(任意の行の任意のポイントにある可能性がある)を使用して、任意の位置にシークできることを考慮してください。 次に、StreamReaderが保持するカウントに対して何を行うかを検討します。
StreamReaderは、現在どの行にあるのかを把握する必要がありますか? ファイル内の位置に関係なく、単に数行の読み取りを維持する必要がありますか?
これを実装するのが悪夢になるような質問だけではありません、私見です。
これは、ファイルの位置を登録するReadLine()メソッドを使用してStreamReaderを実装した男です。
http://www.daniweb.com/forums/thread35078.html
StreamReaderを継承し、特別なクラスにいくつかのプロパティ(_lineLength + _bytesRead)とともに追加のメソッドを追加する必要があると思います:
// Reads a line. A line is defined as a sequence of characters followed by
// a carriage return ('\r'), a line feed ('\n'), or a carriage return
// immediately followed by a line feed. The resulting string does not
// contain the terminating carriage return and/or line feed. The returned
// value is null if the end of the input stream has been reached.
//
/// <include file='doc\myStreamReader.uex' path='docs/doc[@for="myStreamReader.ReadLine"]/*' />
public override String ReadLine()
{
_lineLength = 0;
//if (stream == null)
// __Error.ReaderClosed();
if (charPos == charLen)
{
if (ReadBuffer() == 0) return null;
}
StringBuilder sb = null;
do
{
int i = charPos;
do
{
char ch = charBuffer[i];
int EolChars = 0;
if (ch == '\r' || ch == '\n')
{
EolChars = 1;
String s;
if (sb != null)
{
sb.Append(charBuffer, charPos, i - charPos);
s = sb.ToString();
}
else
{
s = new String(charBuffer, charPos, i - charPos);
}
charPos = i + 1;
if (ch == '\r' && (charPos < charLen || ReadBuffer() > 0))
{
if (charBuffer[charPos] == '\n')
{
charPos++;
EolChars = 2;
}
}
_lineLength = s.Length + EolChars;
_bytesRead = _bytesRead + _lineLength;
return s;
}
i++;
} while (i < charLen);
i = charLen - charPos;
if (sb == null) sb = new StringBuilder(i + 80);
sb.Append(charBuffer, charPos, i);
} while (ReadBuffer() > 0);
string ss = sb.ToString();
_lineLength = ss.Length;
_bytesRead = _bytesRead + _lineLength;
return ss;
}
実際の読み取りバイト数を使用する代わりに、文字列の長さがファイル位置の計算に使用されるため、コードに小さなバグがあると考えてください(UTF8およびUTF16エンコードファイルのサポートの欠如)。
簡単なものを探してここに来ました。 ReadLine()を使用しているだけで、Seek()などの使用を気にしない場合は、StreamReaderの単純なサブクラスを作成します
class CountingReader : StreamReader {
private int _lineNumber = 0;
public int LineNumber { get { return _lineNumber; } }
public CountingReader(Stream stream) : base(stream) { }
public override string ReadLine() {
_lineNumber++;
return base.ReadLine();
}
}
そして通常の方法で、たとえばfileというFileInfoオブジェクトから作成します
CountingReader reader = new CountingReader(file.OpenRead())
そして reader.LineNumber
プロパティを読むだけです。
BaseStreamに関して既に行われているポイントは有効かつ重要です。ただし、テキストを読み、テキストのどこにいるかを知りたい状況があります。再利用しやすくするために、それをクラスとして記述することは依然として有用です。
私は今、そのようなクラスを書き込もうとしました。正しく動作しているように見えますが、かなり遅いです。パフォーマンスが重要でない場合は問題ないはずです(それは遅くありません。以下を参照)。
私は同じロジックを使用して、一度に1文字ずつ、一度に1バッファー、または一度に1行ずつ、テキストの位置を追跡します。これを放棄することでパフォーマンスを向上させることができると確信していますが、実装がはるかに簡単になりました...そして、コードに従うことを望みます。
ReadLineメソッド(この実装の最も弱い点だと思います)とStreamReaderの非常に基本的なパフォーマンス比較を行いましたが、その差はほぼ1桁です。クラスStreamReaderExを使用して22 MB / sを取得しましたが、StreamSSDを直接使用して(SSD搭載のラップトップで)ほぼ9倍になりました。おもしろいかもしれませんが、適切な読解テストの作り方がわかりません。ディスクバッファよりも大きい2つの同一ファイルを使用し、それらを交互に読み取ります。少なくとも私の単純なテストは、複数回実行すると、どのクラスが最初にテストファイルを読み取るかに関係なく、一貫した結果を生成します。
NewLineシンボルはデフォルトでEnvironment.NewLineに設定されますが、長さ1または2の任意の文字列に設定できます。読者はこのシンボルのみを改行と見なします。少なくとも、Visual Studioによって、開いたファイルに「一貫性のない改行が含まれている」というかなりの回数のプロンプトが表示されることはわかっています。
Guardクラスを含めていないことに注意してください。これは単純なユーティリティクラスであり、コンテキストを置き換える方法についてはわかりにくいはずです。削除することもできますが、一部の引数チェックが失われるため、結果のコードは「正しい」から遠くなります。たとえば、Guard.NotNull(s、&quot; s&quot;)は、sがnullでないことを単純にチェックし、その場合はArgumentNullExceptionをスローします(引数名&quot; s&quot;の2番目のパラメーター)。
十分なせせらぎ、ここにコードがあります:
public class StreamReaderEx : StreamReader { // NewLine characters (magic value -1: "not used"). int newLine1, newLine2; // The last character read was the first character of the NewLine symbol AND we are using a two-character symbol. bool insideNewLine; // StringBuilder used for ReadLine implementation. StringBuilder lineBuilder = new StringBuilder(); public StreamReaderEx(string path, string newLine = "\r\n") : base(path) { init(newLine); } public StreamReaderEx(Stream s, string newLine = "\r\n") : base(s) { init(newLine); } public string NewLine { get { return "" + (char)newLine1 + (char)newLine2; } private set { Guard.NotNull(value, "value"); Guard.Range(value.Length, 1, 2, "Only 1 to 2 character NewLine symbols are supported."); newLine1 = value[0]; newLine2 = (value.Length == 2 ? value[1] : -1); } } public int LineNumber { get; private set; } public int LinePosition { get; private set; } public override int Read() { int next = base.Read(); trackTextPosition(next); return next; } public override int Read(char[] buffer, int index, int count) { int n = base.Read(buffer, index, count); for (int i = 0; i