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 에 해당하는 기호까지의 데이터를 돌려주는 것으로 블록 안에는 한줄이아닌 여러줄이 들어 있을 수도 있습니다.
어쨌든 여기저기 한참을 뒤진 후 완벽하게 구동하는 코드를 찾아 공유합니다.
바로 요 링크에 있는 코드인데요. 사용하기도 편리하게 해당 개발자 분께서 함수로 깔끔하게 구현을 해주셨습니다.
바로 아래코드를 본인의 개발중인 클래스에 포함시킵니다.
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# 문자열 읽어서 문자열에 해당하는 변수에 값 세팅하기
'DEV > c#' 카테고리의 다른 글
[c#] Resource 에 추가한 텍스트를 소스코드에서 불러오기 (0) | 2023.01.31 |
---|---|
[c# .net] 키움증권 API - 전체 종목 받아오기 (1) | 2021.03.31 |
[C#] 설정 저장하고 재실행 시 저장한 값 불러오기 (2) | 2020.07.29 |
C# 문자열 읽어서 문자열에 해당하는 변수에 값 세팅하기 (0) | 2020.07.21 |
C# .net으로 photoshop 연동하기 (0) | 2020.06.03 |