반응형

c# 을 이용하여 streamreader 개체로 텍스트를 읽는 방법은 매우 효율 적이고 손쉽게 작업을 수행하도록 해줍니다. 빠르고 간단하게 개발을 할 수 있어 저도 자주 사용하고는 합니다.

그런데 이번에 방대한 크기의 텍스트 파일을 이용해서 작업을 하는 중 고민 거리가 생겼습니다. 미리 텍스트 파일을 읽어서 중간중간 핵심이 되는 ID 별로 위치를 저장해놓고 나중에 ID 에 해당되는 위치로 seek 하여 읽어 들이는 방법을 사용하려고 했습니다.

그런게 steramreader 는 seek 기능이 없더군요. -_-

그래서 streamreader.basestream 으로 들어가보니 seek 도 있고 position 도 있길레 해당 메소드와 프라퍼티로 구현을 해보기로 하였습니다.

long filePos = -1;
using (StreamReader cini = new StreamReader(fs, Encoding.Default))
{
      do
      {
          filePos = cini.BaseStream.Position; //readLine 을 하기 전 위치를 저장한다
          string cline = readline();
          if (cline == "내가 원하는 정보")
          {
              break;
          }
      }while(cini.EndOfStream == false);
}

// filePos 에 저장된 값을 나중에 이용할 경우
using (StreamReader cini = new StreamReader(fs, Encoding.Default))
{
      cini.DiscardBufferedData();
      cini.BasePosition.Seek(filePos, SeekOrigin.Begin);
    
      // 자 이제 읽어 볼까!
      string myResult = cini.readLine();
}

이런 식으로 말이지요..

간단하잖아요? 상식적으로 저렇게 하면 될 것 같기도 하고요...

 

그런데 안됩니다.

 

 

엉터리 값이 나오게 됩니다.

라인 위치도 안맞고 알수 없는 위치의 값들이 튀어 나옵니다.

인터넷을 좀 뒤적거려보니 StreamReader 의 readline 이라는 녀석이 특이한 녀석이더군요.

우리가 생각하는 문자열에서 실제 한줄을 읽어서 반환해주는 게 아닌 어떤 블럭 단위로 데이터를 읽어 들인 뒤 newLine 에 해당하는 기호까지의 데이터를 돌려주는 것으로 블록 안에는 한줄이아닌 여러줄이 들어 있을 수도 있습니다.

 

어쨌든 여기저기 한참을 뒤진 후 완벽하게 구동하는 코드를 찾아 공유합니다.

stackoverflow.com/a/17457085

 

StreamReader and seeking

can you use streamreader to read a normal textfile and then in the middle of reading close the streamreader after saving the current position and then open streamreader again and start reading from...

stackoverflow.com

바로 요 링크에 있는 코드인데요. 사용하기도 편리하게 해당 개발자 분께서 함수로 깔끔하게 구현을 해주셨습니다.

바로 아래코드를 본인의 개발중인 클래스에 포함시킵니다.

public static long GetActualPosition(StreamReader reader)
{
    System.Reflection.BindingFlags flags = System.Reflection.BindingFlags.DeclaredOnly | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.GetField;

    // The current buffer of decoded characters
    char[] charBuffer = (char[])reader.GetType().InvokeMember("charBuffer", flags, null, reader, null);

    // The index of the next char to be read from charBuffer
    int charPos = (int)reader.GetType().InvokeMember("charPos", flags, null, reader, null);

    // The number of decoded chars presently used in charBuffer
    int charLen = (int)reader.GetType().InvokeMember("charLen", flags, null, reader, null);

    // The current buffer of read bytes (byteBuffer.Length = 1024; this is critical).
    byte[] byteBuffer = (byte[])reader.GetType().InvokeMember("byteBuffer", flags, null, reader, null);

    // The number of bytes read while advancing reader.BaseStream.Position to (re)fill charBuffer
    int byteLen = (int)reader.GetType().InvokeMember("byteLen", flags, null, reader, null);

    // The number of bytes the remaining chars use in the original encoding.
    int numBytesLeft = reader.CurrentEncoding.GetByteCount(charBuffer, charPos, charLen - charPos);

    // For variable-byte encodings, deal with partial chars at the end of the buffer
    int numFragments = 0;
    if (byteLen > 0 && !reader.CurrentEncoding.IsSingleByte)
    {
        if (reader.CurrentEncoding.CodePage == 65001) // UTF-8
        {
            byte byteCountMask = 0;
            while ((byteBuffer[byteLen - numFragments - 1] >> 6) == 2) // if the byte is "10xx xxxx", it's a continuation-byte
                byteCountMask |= (byte)(1 << ++numFragments); // count bytes & build the "complete char" mask
            if ((byteBuffer[byteLen - numFragments - 1] >> 6) == 3) // if the byte is "11xx xxxx", it starts a multi-byte char.
                byteCountMask |= (byte)(1 << ++numFragments); // count bytes & build the "complete char" mask
            // see if we found as many bytes as the leading-byte says to expect
            if (numFragments > 1 && ((byteBuffer[byteLen - numFragments] >> 7 - numFragments) == byteCountMask))
                numFragments = 0; // no partial-char in the byte-buffer to account for
        }
        else if (reader.CurrentEncoding.CodePage == 1200) // UTF-16LE
        {
            if (byteBuffer[byteLen - 1] >= 0xd8) // high-surrogate
                numFragments = 2; // account for the partial character
        }
        else if (reader.CurrentEncoding.CodePage == 1201) // UTF-16BE
        {
            if (byteBuffer[byteLen - 2] >= 0xd8) // high-surrogate
                numFragments = 2; // account for the partial character
        }
    }
    return reader.BaseStream.Position - numBytesLeft - numFragments;
}

 

그런 다음 위에서 제가 작성했던 코드에 position 을 기록하는 부분을 위에 소개한 함수를 이용해서 찾는 것이지요.

GetActualPosition 이라는 함수를 이용해서요. 그럼 아래와 같이 되겠죠.

long filePos = -1;
using (StreamReader cini = new StreamReader(fs, Encoding.Default))
{
	do
	{
		filePos = GetActualPosition(cini); //readLine 을 하기 전 위치를 저장한다
		string cline = readline();
		if (cline == "내가 원하는 정보")
		{
			break;
		}
	}while(cini.EndOfStream == false);
}

// filePos 에 저장된 값을 나중에 이용할 경우
using (StreamReader cini = new StreamReader(fs, Encoding.Default))
{
	cini.DiscardBufferedData();
	cini.BasePosition.Seek(filePos, SeekOrigin.Begin);

	// 자 이제 읽어 볼까!
	string myResult = cini.readLine();
}

 

이렇게 해서 결과를 확인해보면 아주 완벽하게 동작이 됩니다.

해당 개발자 분께서 페이지에 그리고 주석으로 상세하게 소개를 하고 있으니 관심 있으신 분께서는 한번 찬찬히 분석해보시는 것도 큰 공부가 될 것 같습니다.

c# 을 이용해 StreamReader 를 사용하시는 분들~

readline 수행 후 정확한 위치를 구해야 하는 문제에 봉착하셨다면 한번 시도해 보세요.

 

그럼 이만~

2023.01.31 - [DEV/c#] - [c#] Resource 에 추가한 텍스트를 소스코드에서 불러오기

2020/07/29 - [DEV/c#] - [C#] 설정 저장하고 재실행 시 저장한 값 불러오기

2020/07/21 - [DEV/c#] - C# 문자열 읽어서 문자열에 해당하는 변수에 값 세팅하기

2020/06/29 - [DIY/Arduino] - C# 에서 아두이노로 시리얼 통신 하기

2020/06/03 - [DEV/c#] - C# .net으로 photoshop 연동하기

반응형

+ Recent posts