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 수행 후 정확한 위치를 구해야 하는 문제에 봉착하셨다면 한번 시도해 보세요.
위의 문자열들에서 color_A 는 제가 아래 올린 코드의 color_a 에 대한 설정 값으로 사용되게 됩니다. 일일이 if 문을 이용하여 color_A == color_a 인지 color_b 인지 확인하지 않고 해당 클래스가 들고 있는 프라퍼티들의 이름 중에 같은 것이 있는지 찾는 방법이지요.
// 제일 상단에 지시문 추가
using System.Reflection;
// 본 클래스는 메인 클래스가 아니어도 상관 없습니다.
// 예를 들면 Form 클래스내에 기능적인 용도나 구조체로 사용할 클래스 여도 상관 없음
public class MyColorStyle
{
private string color_a;
private string color_b;
private string color_c;
private string color_f;
private string color_e;
private string color_f;
private string color_g;
// 외부에서 가져가고 세팅할 수 있도록 get set 함수 하나씩 준비
public string color_A {get { return color_a;} set { color_a= value; } }
public string color_B {get { return color_b;} set { color_b= value; } }
public string color_C {get { return color_c;} set { color_c= value; } }
public string color_D {get { return color_d;} set { color_d= value; } }
public string color_E {get { return color_e;} set { color_e= value; } }
public string color_F {get { return color_f;} set { color_f= value; } }
public string color_G {get { return color_g;} set { color_g= value; } }
public golfMapcolorStyle()
{
//생성자 부분에는 초기화 값 넣어 주시고요..
}
public void setColor(string clrString) // clrString : "color_A=#FFEECC" 형식의 설정 문자열이 줄단위로 기록되어 있는 파일 전체
{
string[] tmps = clrString.Split(Environment.NewLine.ToCharArray(), StringSplitOptions.RemoveEmptyEntries); // 줄바꿈 단위로 분리해준다.
foreach (string str in tmps)
{
string[] tmp = str.Split('='); // = 기호를 기준으로 앞의 문자열은 변수 명칭을 검색, 뒤의 문자열은 값으로 사용
foreach (PropertyInfo p in typeof(MyColorStyle).GetProperties())
{
if (tmp[0] == p.Name)
{
tmp[1] = tmp[1].Trim(); // 잘려진 문자열 뒤에 지저분하게 붙는 경우가 있어서 사용함
p.SetValue(this, tmp[1], null);
}
}
}
}
}
총 7개의 변수가 있지만 단 하나의 방법으로 설정을 할 수 있게 됩니다.
만약 변수가 100 개 이상이었다면 위와 같은 방식으로 사용하는게 개발 시간을 크게 단축할 수 있을 것입니다. 오류도 많이 줄어들 것이고요.
위 코드에서 제일 핵심이 되는 부분은 바로 아래 부분입니다.
foreach (PropertyInfo p in typeof(MyColorStyle).GetProperties())
{
// p 에 대하여 무었인가를 할 수 있음
}
클래스가 가지고 있는 변수를 프라퍼티로 보고 프라퍼티의 이름을 확인하여 뭔가를 할 수 있게 되는 것이죠.
p.SetValue 라는 명령어를 이용하여 p 라는 MyColorStyle 라는 클래스의 프라퍼티(변수) 의 값을 설정하는 것이 이번 포스트의 핵심 내용입니다.
만약 변수가 문자열 뿐만 아니라 int 형식도 존재한다고 하면 아래와 같이 간단하게 구현할 수도 있습니다.
저는 회사에서 디자인팀에 있고 전공도 디자인과 출신이며 심지어는 고등학교도 예체능계열 고등학교를 나왔습니다만 지금 회사에서 하는일의 95% 정도는 개발을 하고 있습니다. 실제로 제가 포토샵, 일러스트 및 기타 디자인툴을 다루는 시간을 다 합쳐도 비주얼 스튜디오를 사용하는 시간의 1/10 도 안될거에요.
요즘은 대부분의 개발을 c# 으로 진행하고 있습니다. 포토샵 스크립트를 이용해서 이미지 컨트롤은 할 수 있겠지만 좀더 복잡하고 다양한, 그리고 시스템 차원에서 뭔가를 하기에는 부족한 부분이 있습니다. 하지만 포토샵에는 레이어 컨트롤이나 layer effect 와 같은 놀라운 기능들이 있기 때문에 버리기는 아까운 부분이 있습니다.
그래서 저는 C# 을 이용하여 개발을 하되 포토샵을 이미지 자동화 편집 툴로 사용할때가 종종 있습니다. javascript 로 개발된 이미지 편집용 스크립트를 C# application 에서 포토샵으로 전달하여 자동화를 하는 것이지요.
??
이게 가능하냐고요?
예전에 제가 엑셀 비주얼 베이직을 이용하여 엑셀과 포토샵이 연동되는 것을 소개해 드린적이 있는데요, 개념적으로는 크게 다르지 않습니다.
개념적으로는 윈도우의 COM 오브젝트를 이용하는 것과 동일한데요. 연결해주는 방법에 약간 차이가 있고 C# 에서 포토샵 스크립트 작성이 쉽지 않으니 개발은 Extend Script Tool kit 으로 하고 실행만 C#이 하는 역할을 하는 겁니다.
이렇게 되면 C#이 다양한 파일 처리, 관리를 하는 동안 이미지 편집이 필요한 순간에 Photoshop 을 호출하여 이미지를 열고 자동화 편집 스크립트를 통해 이미지 편집을 진행하고 그 다음 나머지 파일 처리를 하는 방식 인 겁니다.
C# 에서 COM 오브젝트 선언하는 방법
Type ptsApp = Type.GetTypeFromProgID("Photoshop.Application");
dynamic psApp = Activator.CreateInstance(ptsApp);
psApp.doJavaScript("alert(\"ds\")"); // 여기에 수행되어야 할 포토샵 스크립트를 문자열로 작성하여 전달
dynamic 개체를 이용하여 선언이 가능하며 .NET4.0 이상의 플랫폼에서 지원합니다.
이렇게 했을 때 빌드를 하게 되면 아래와 같은 오류가 나타나는 경우가 있는데요.
Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo.Create' 멤버가 필요한 컴파일러가 없습니다
우측의 솔루션 탐색기 '참조' 부문에서 마우스 우클릭 후 '참조 추가' 로 참조할 개체를 추가해 주어야 하는데요.
Microsoft.CSharp
을 추가해주시면 정상적으로 빌드가 진행이 됩니다.
막상 포토샵 오브젝트를 추가해 주었지만 실제 실행될 때 C# 에서 포토샵 코드를 작성하는 것이 여간 귀찮은 일이 아닙니다. 그런 경우 미리 javascript 로 필요한스크립트를 작성하여 준뒤 아래와 같이 실행시킬 수 있습니다. 이렇게 하면 실행시 미리 변수를 전달 할 수 있기 때문에 상당히 유연한 개발을 진행할 수 있게 됩니다.
변수는 위에 보시는것 과 같이 dynamic 배열 개체를 생성한뒤 필요한 값을 입력해주고 스크립트와 함께 전달하는 방식을 사용하면 됩니다.
포토샵 스크립트 실행 완료 후 반환(리턴) 값이 필요한 경우라면?
무언가 일을 시켰다면 피드백이 있어야 하겠지요? 만들어진 개체의 파일 명이든 무엇이든 간에 어떤 피드백을 받아야 하는 경우 아래와 같이 작성합니다.
Javascript (포토샵에서 실행되어야 할 스크립트)
var value1 = arguments[0]; // C# 으로 부터 전달 받는 인자
var value2 = arguments[1]; // C# 으로 부터 전달 받는 인자
main(value1, value2)
function main (val1, val2)
{
string myString = "";
// 실제 필요한 계산, 동작을 작성한다.
myString = val1 + "," + val2 ;
return myString; // 이 값이 C# 으로 반환된다.
}
이렇게 해주면 myResult 라는 문자열 변수에 입력한 변수들을 합친 문자열이 반환이 되는것이죠.
어떠신가요? 어렵지 않죠?
이렇게 하면 C# 으로 빠르고 편리한 이미지 관리 프로그램을 만들어 포토샵으로 강력한 이미지 편집 기능을 함께 이용할 수 있는 기능을 개발 할 수 있습니다. 물론 ImageMagick 과 같은 강력한 이미지 편집 프로그램이 있긴 하지만 이미 만들어져 있는 PSD 파일 등의 레이어속성을 조회하거나 변경하고, 특정 레이어들을 이용하여 어떤 작업을 해야 한다면 ImageMagick 으로는 한계가 있습니다.
직접 한번 코딩을 해보시면서 테스트 해보시길 바랍니다.
궁금하신 부분은 뎃글로 남겨 주시면 답변 드릴 수 있도록 하겠습니다.
이만 포스팅을 마칩니다.
뎃글,공감은 블로그 작성자에게 큰 힘이 된답니다. 도움이 되었다 생각되시면클릭!! 부탁드려요~
프로그래밍을 이용하여 파일을 생성하거나 이동시킬때 md5 와 같은 무결성 검증을 위한 해시체크 파일을 생성해야 할때가 있습니다. 물론 간단한 유틸리티를 내려받아 윈도우 쉘 명령어로 만드는 방법도 있겠지만 굳이 프로그래밍에 의한 결과를 다시 사용자가 손으로 작업하는 것은 효율 적인 방법이라고 하기는 어렵겠죠.
그래서 이번에는 C# 프로그램에서 바로 md5 파일을 생성하는 방법을 소개해 드릴까 합니다.
일단 기본은 아래와 같습니다.
// 코드 최 상단에 아래와 같은 using 지시문을 넣어 줍니다.
using System.Security.Cryptography;
// 코드내에 아래의 함수를 추가하여 해시문자열을 받아올 수 있습니다.
// 해시 문자열을 스트링으로 받아오는 함수, 아래 str 은 파일의 경로를 지정한다.
private static string getMD5Hash(string str)
{
using (var md5 = MD5.Create())
{
using (var stream = File.OpenRead(str))
{
var hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}
}
이렇게 하면 getMD5Hash(파일경로) 라는 함수를 이용하여 해시체크용 문자열을 가져올 수 있습니다. 굳이 다른 유틸리티 없이도 본인이 개발중인 프로그램 내에서 md5 체크썸 파일을 만들 수 있는 것이죠.
md5 파일을 만드는것 까지 해보면 아래와 같습니다.
저의 경우에는 디렉토리 하위에 모든 파일에 대한 체크썸 파일이 필요했기 때문에 하위 디렉토리를 돌며 파일 및 폴더를 탐색하는 재귀함수 형태로 제작을 했습니다.
using System.Security.Cryptography;
.
.
.
private static void createmd5(string _path, string md5name)
{
string dirName = Directory.GetParent(md5name).ToString() + "\\";
string tmp = "";
chkDir_md5(ref tmp, _path, dirName);
File.WriteAllText(md5name, tmp);
}
// 지정된 폴더를 탐색하며 하위의 모든 폴더, 파일을 검사 후 문자열 기록
private static void chkDir_md5 (ref string _str, string _path, string _defPath)
{
FileAttributes att = File.GetAttributes(_path);
if ((att & FileAttributes.Directory) == FileAttributes.Directory)
{
// is a Directories
string[] tmpPath = Directory.GetDirectories(_path, "*");
foreach (string s in tmpPath)
{
chkDir_md5(ref _str, s, _defPath);
}
string[] tmpFiles = Directory.GetFiles(_path, "*.*");
foreach (string s in tmpFiles)
{
chkDir_md5(ref _str, s, _defPath);
}
}
else
{
// is a file
_str += getMD5Hash(_path);
_str += " *" + _path.Replace(_defPath, "");
_str += "\n";
}
}
private static string getMD5Hash(string str)
{
using (var md5 = MD5.Create())
{
using (var stream = File.OpenRead(str))
{
var hash = md5.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
}
}
찬찬히 보시면 전혀 어려울 것이 없으니 훓어 보시기 바랍니다.
md5 파일을 제작하여 원본파일과 함께 배포를 하게 되면 파일을 전달받는 위치에서 해당 파일이 원본과 동일하지를 판단할 수 있으며 압축/압축해제시 또는 웹서버에 올라간 데이터가 네트웍망을 타고 내려오는 동인 손실이 있었는지를 파악하는데 도움이 됩니다.
FileAttributes chkAtt = File.GetAttributes(_path);
if ((chkAtt & FileAttributes.Directory) == FileAttributes.Directory)
{
// 디렉토리일 경우
}
else
{
// 파일 일 경우
}
간단하죠 ?
이렇게 하면 _path 로 주어진 경로가 디렉토리인지 파일인지 구분할 수 있는데요. 폴더를 계속해서 탐색하는 기능을 만들때 유용하게 사용할 수 있답니다.
예를 들면 아래와 같은 방법으로 하위폴더의 모든 파일에 대한 액션을 할 수 있는 재귀 함수를 만들수 있는 것입니다.
private static void chkDir_recursive (string _path)
{
FileAttributes chkatt = File.GetAttributes(_path);
if ((chkatt & FileAttributes.Directory) == FileAttributes.Directory)
{
// is a Directories
string[] tmpPath = Directory.GetDirectories(_path, "*");
foreach (string s in tmpPath)
{
chkDir_recursive(_path); // 폴더를 만나면 하위로 계속 탐색을 진행
}
string[] tmpFiles = Directory.GetFiles(_path, "*.*");
foreach (string s in tmpFiles)
{
chkDir_recursive(_path); // 파일을 만나도 동일한 함수로 실행 가능함
}
}
else
{
// 파일을 만나게 되면 실제 해야할 액션을 코딩한다
}
}