• ベストアンサー

文字列置換のアルゴリズムを教えてください。(Windowsプログラミング)

はじめまして。まだプログラミングを始めて間もない学生です。 現在Windowsプログラミングを学習しています。 学校で課題を出されたのですが、どうしても分からないので教えてください。 ある50MB程のテキストファイルがあります。それにはたくさんの個人情報が含まれており、 個人情報保護の観点から個人を特定できなくする必要があります。 そこで、以下のように8桁の患者IDを"*"(アスタリスク)に置換をしなければならないのですが、 その実現方法が分かりません。 ・"<受信歴>患者ID:"に続く0バイト目から8バイトを"*"に置換する。  例えば、「2005/01/01 09:00:00 <受診歴>患者ID:0000001234 内科受診」の行を 「2005/01/01 09:00:00 <受診歴>患者ID:********** 内科受診」とする必要があります。 ファイル内にはこのような該当箇所がたくさんあります。 簡単なウィンドウを作り、そこに配置したボタンを押下することによって指定ファイルが読み込まれ、 置換された新規ファイルが作成されるといった具合です。 ご教授の程、よろしくお願い致します。 <環境> ・OS:Windows2000 ・開発環境:VC++ 6.0 ・できればWin32APIを用いて。標準関数を用いてもよい。

質問者が選んだベストアンサー

  • ベストアンサー
  • Oh-Orange
  • ベストアンサー率63% (854/1345)
回答No.12

★??? ・課題の仕様は分かりました。 >そして、またまた追加で申し訳ないのですが、一行に同じタイプの置換該当箇所が二箇所以上あった場合、 >現在のソースでは初めの一箇所しか置換できないと思うのですが、どのように修正することで解決できるでしょうか。 >いろいろと試してはいるのですが、なかなか上手く行きません。  ↑  処理のアルゴリズムを整理すべきです。  ここがはっきりしていないと試行錯誤しても無意味です。時間が勿体無いです。 >何だか一番に最初ご質問させて頂いたことよりどんどん要求が高くなってきてしまい、 >申し訳ない気持ちであるのと同時に、自分能力では手に負えない領域に突入してしまった感がありますが、 >最後まで頑張りたいと思います。  ↑  回答No.1~No.11 までをよく読み、提示したソースがなぜそれで上手くいき、自力で作ったソースでは  上手くいかなかったり、無駄に2回ファイルを読んでいたりするかを比較して下さい。  よく読み理解すれば二箇所以上の置換も自力で解決できるはずです。  決して手に負えない領域に突入してはいないはずです。 >P.S. 全角アスタリスクへの置換、上手くできました。紹介していただいた関数のサンプルは、 >今からじっくり読んで理解したいと思います。 >ありがとうございました。  ↑  応援します。頑張って下さい。  上手く出来たらソースを見せてくれると嬉しいです。  それではこれが最後のアドバイスとなることを祈っています。 ・以上。

kenkenpo
質問者

補足

返信が遅くなってしまい、すみませんでした。 週末忙しかったため、メモリを動的に確保する部分にはまだ手を付けられていないのですが、 それ以外はなんとか思うような動きをするようになりました。 検索文字列のパターンが増えた場合にも対応できるように、検索&置き換えの機能を関数化しました。 完成版のソースを載せられるように、頑張ります。 いつも、親身なアドバイス本当にありがとうございます。

その他の回答 (11)

  • Oh-Orange
  • ベストアンサー率63% (854/1345)
回答No.11

★アドバイス >大変申し訳ないのですが、患者氏名の方は全角のアスタリスク("*")で置き換えたいのです。  ↑  全角なら次のようにすれば良い。    // 患者氏名の検索&書き換え  if ( (find = strstr(buff,search2)) != NULL ){   for ( find += strlen(search2)+1, i = 0 ; i < 10 ; i++ ){    memcpy( find, "*", 2 ); ←ここで氏名を(*)文字に書き換え    find += 2;   }  } >そして、実際に名前が入力されている部分だけを置換できる方法があれば、そちらの方法も教えていただけると幸いです。  ↑  名前部分以外が空白文字ならば    // 患者氏名の検索&書き換え  if ( (find = strstr(buff,search2)) != NULL ){   for ( find += strlen(search2)+1, i = 0 ; i < 10 ; i++ ){    if ( isspace(*find) ){ ←空白文字なら抜ける     break;    }    // 名前部分のみ書き換えるため    memcpy( find, "*", 2 );    find += 2;   }  } >追加で申し訳ないのですが、buffのサイズを256バイトで固定ではなく、一行の文字数によって動的に確保したいときは >どうすればよいのでしょうか。  ↑  これはなぜですか?  この場合は固定バイト(256)の配列に改行コードが現れるまで数回に分けて取得して行きます。  そして、malloc()、realloc() 関数で確保サイズを拡張しつつ固定バイトで読み取ったデータを  コピーしていけば良いと思います。この辺のサンプルは回答 No.9 の sakusaker7 さんのように  fgetc() 関数を使っても出来ますね。でも固定バイト(配列)で一括で fgets() した方が楽かな。 ・下にそのサンプルの載せておきます。 サンプル: char *fgets_alloc( FILE *fp ) {  char buff[ 4096 ]; // 固定バッファ  char *alloc; // malloc()用ポインタ  char *temp; // realloc()用ポインタ  long tell; // 読み込み前の位置  long size; // 読み込んだサイズ  long total; // 読み込んだ総サイズ    // 初期化  alloc = NULL;  total = 0;    // 改行が現れるまで読み込む  for ( tell = ftell(fp) ; fgets(buff,sizeof(buff),fp) != NULL ; tell = ftell(fp) ){   // 読み込んだサイズを算出   size = (ftell(fp) - tell);      // メモリ確保/拡張   if ( alloc == NULL ){    alloc = (char*)malloc( size + 1 ); // \0分も含む   }   else if ( (temp = realloc(alloc,total + size + 1)) != NULL ){    alloc = temp;   }   else{    free( alloc );    alloc = NULL;   }   if ( alloc == NULL ){    return NULL; // メモリ不足(*1)   }   // メモリへ追加コピー   strcpy( &alloc[total], buff );      // 改行のチェック   if ( buff[size - 1] == '\n' ){    return alloc; // 改行が含まれる場合   }   // 確保バッファのサイズ加算   total += size;  }  return alloc; // 改行が含まれなくても最後は返す(エラーまたは EOF なら NULL) } その他: ・上記のサンプルで改行が見つかるまで内部で繰り返してメモリに取得します。  また、改行が1つも発見できなくてファイルの最後(EOF)に到達した場合は今までに  読み込んだすべてのバッファにセットして返します。  ファイルの最後(EOF)やエラーが発生すると NULL を返します。 ・また malloc()、realloc() 関数でメモリが確保不足でも NULL を返す仕様になっています。  このエラーをすべて区別するには feof(fp)、ferror(fp) で調べて両方とも 0 のときなら  メモリ不足で NULL が返されたことになります。 ・以上。

kenkenpo
質問者

お礼

すみません。以下の部分なんですが、 >そして、またまた追加で申し訳ないのですが、一行に同じタイプの置換該当箇所が二箇所以上あった場合、 >現在のソースでは初めの一箇所しか置換できないと思うのですが、どのように修正することで解決できるでしょうか。 >いろいろと試してはいるのですが、なかなか上手く行きません。 問題なく動きました。失礼しました。

kenkenpo
質問者

補足

お世話になっております。 >>追加で申し訳ないのですが、buffのサイズを256バイトで固定ではなく、>一行の文字数によって動的に確保したいときは >>どうすればよいのでしょうか。 > ↑ > これはなぜですか? 今回の課題で出てくるテキストファイルは、患者管理システム(仮)から吐き出されるログということになっています。 基本的には一行何バイトまで出力されるか分っているので、固定長で確保しても問題ないのですが、 想定外の処理により、思わぬ出力結果になってしまうこともあるそうです。 例えば、改行文字が出力されず一行が膨大な文字数になってしまったり・・・ そういう場合でも個人情報の部分はきちんと全部置換して、新規ファイルに出力しなければいけないそうです。 もしくは、あまりも膨大なメモリが必要でその領域が確保できなかった場合は、 その行だけスキップして残りの処理をしなければならないそうです。 そして、またまた追加で申し訳ないのですが、一行に同じタイプの置換該当箇所が二箇所以上あった場合、 現在のソースでは初めの一箇所しか置換できないと思うのですが、どのように修正することで解決できるでしょうか。 いろいろと試してはいるのですが、なかなか上手く行きません。 何だか一番に最初ご質問させて頂いたことよりどんどん要求が高くなってきてしまい、 申し訳ない気持ちであるのと同時に、自分能力では手に負えない領域に突入してしまった感がありますが、 最後まで頑張りたいと思います。 いつもお世話になってばかりで誠に恐縮ですが、よろしくお願い致します。 P.S. 全角アスタリスクへの置換、上手くできました。紹介していただいた関数のサンプルは、 今からじっくり読んで理解したいと思います。 ありがとうございました。

  • sakusaker7
  • ベストアンサー率62% (800/1280)
回答No.10

> 大変申し訳ないのですが、患者氏名の方は全角のアスタリスク("*")で置き換えたいのです。 >そして、実際に名前が入力されている部分だけを置換できる方法があれば、 > そちらの方法も教えていただけると幸いです。 仕様が不明確な部分がありますがこういう結果が欲しい? #include <stdio.h> #include <string.h> #define UPPERBYTE (0x81) #define LOWERBYTE (0x40) enum State {FIRSTNAME, GIVENNAME}; int fill_with_fullwidthstar(unsigned char *str) { unsigned char *p = str; enum State s = FIRSTNAME; #define TARGETENTRY "患者氏名=" /* skip to head of name */ p = (unsigned char*)strstr((const char*)str, TARGETENTRY); if (!p) return 0; else p += sizeof TARGETENTRY - 1; for (;;p+=2) { if (!p[0] || !p[1]) break; if (p[0] == UPPERBYTE && p[1] == LOWERBYTE && s == FIRSTNAME) { //printf("-> GIVENNAME\n"); s = GIVENNAME; //continue; } else if (p[0] == UPPERBYTE && p[1] == LOWERBYTE && s == GIVENNAME) { //printf("-> break\n"); break; } p[0] = (unsigned char)'\x81'; p[1] = (unsigned char)'\x96'; } return 0; } int main() { unsigned char sampleline1[] = "患者ID=12345678"; unsigned char sampleline2[] = "患者氏名=山田 太郎     "; printf("before: %s\n", sampleline1); fill_with_fullwidthstar(sampleline1); printf("after: %s\n", sampleline1); printf("before: %s\n", sampleline2); fill_with_fullwidthstar(sampleline2); printf("after: %s\n", sampleline2); return 0; } 実行結果: before: '患者ID=12345678' after: '患者ID=12345678' before: '患者氏名=山田 太郎     ' after: '患者氏名=*****     ' そこかしこに判定が甘いところがありますので注意。

kenkenpo
質問者

補足

返信がおそくなってしまい、すみませんでした。 ご丁寧にソースを全部載せてくださりありがとうございます。 所々分らない部分もありますが、頑張って解析してみようと思います。 本当に何度もありがとうございます。

  • sakusaker7
  • ベストアンサー率62% (800/1280)
回答No.9

> buffのサイズを256バイトで固定ではなく、 > 一行の文字数によって動的に確保したいときは > どうすればよいのでしょうか。 自前で一行取得の関数を作ってしまうとか。 #include <stdio.h> #include <stdlib.h> #include <string.h> #if DEBUG #define INITBUFFERSIZE 16 #else #define INITBUFFERSIZE 256 #endif char * myfgets(FILE *fp) { static char *mybuf = NULL; static size_t bufsize = 0; size_t idx; if (mybuf == NULL) { bufsize = 256; mybuf = (char *)malloc(bufsize); if (mybuf == NULL) return NULL; } for (idx=0; ;) { int ch; if (idx>=bufsize-1) { char *p; p = realloc(mybuf, bufsize*2); mybuf = p; bufsize *= 2; } ch = fgetc(fp); if (ch == EOF) { if (idx==0) return NULL; break; } mybuf[idx++] = (char)ch; if (ch == '\n') break; } mybuf[idx] = '\0'; return mybuf; } #if DEBUG int main() { FILE *fp; char *bufp; fp = fopen("sampleinput.txt", "r"); while ((bufp = myfgets(fp)) != NULL) { char *pos; pos = strchr(bufp, '\n'); if (pos) *pos = '\0'; printf("get linedata : '%s'\n", bufp); } return 0; } #endif やっつけで作ったものですので、そこかしこに 修正すべき点がありますが参考になれば。 たとえば話を簡単にするため fgetcで一文字ずつ ストリームからとってきていますが、 これは fread標準関数とかreadシステムコールを使って 読み込みはまとめてしまった方が良いでしょう。 fgetcでもその辺のことはやっていますけど。

  • Oh-Orange
  • ベストアンサー率63% (854/1345)
回答No.8

★アドバイス >多分、数字の置換が終わった時点でバッファの先頭にポインタを戻さなければならないとは思うんですが、 >その実現方法がわかりません。  ↑  バッファの先頭に戻さなくても buff が先頭ですので strstr(buff,"検索文字列") と続けて  検索すれば良いだけですけど。ポインタにバッファの先頭を戻すのなら find = buff; で可能。 >ところで、またまた機能追加といいますか、今度は"患者氏名 ="に続く名前の部分も"*"(全角アスタリスク)で >置換しなければならなくなりました。  ↑  機能追加なら前回のサンプルにもう一つ検索と書き換えの部分を機能追加すればよい。  ファイルを2度読まなくても1行バッファを2回検索すればよい。そして書き換える。 ・つまり、最初は『患者ID:』の部分を書き換えた後に『患者氏名=』を検索して書き換えるように  続ければよい。→順番が逆でも1行バッファを2回先頭から検索するため問題ない。 ・下に前回のサンプルに機能追加した内容を載せておきます。  どう機能追加すべきか下のサンプルで分かるはずです。  次にまた機能追加するときの参考にして下さい。 機能追加のサンプル: char *search1 = "患者ID:"; char *search2 = "患者氏名="; char *find; int i; while ( fgets(buff,sizeof(buff),fp) != NULL ){  // 患者IDの検索&書き換え  if ( (find = strstr(buff,search1)) != NULL ){   for ( find += strlen(search1) ; isdigit(*find) ; find++ ){    *find = '*'; ←ここで数字を(*)文字に書き換え   }  }  // 患者氏名の検索&書き換え  if ( (find = strstr(buff,search2)) != NULL ){   for ( find += strlen(search2)+1, i = 0 ; i < 20 ; i++ ){ ←20バイトの固定ならこれでOK    *find++ = '*'; ←ここで氏名を(*)文字に書き換え   }  }  fputs( buff, fo ); ←書き出しはここ1つで良い } その他: ・ファイルを "r+" の読み書き両用で処理する前に入力用、出力用で動作確認しましょう。  上記のサンプルを少し修正するだけで読み書き両用でオープンして書き換えの処理ができます。  ただし、バッファ内容を書き出すときには fseek() 関数などで出力位置をちゃんと設定する  必要があります。ここを忘れるとデータを壊します。読み書き両用での処理は参考書などで  あまり触れないためか使いこなせていないプログラマーがいます。身近に。誰? ・以上。

kenkenpo
質問者

お礼

追加で申し訳ないのですが、buffのサイズを256バイトで固定ではなく、一行の文字数によって動的に確保したいときは どうすればよいのでしょうか。 一行の文字数を知るためには、まず一行読み込まないといけないとおもうんですが、 fgets()関数では、2番目の引数に必ずサイズか必要でもんね・・・ いつも一度にたくさんのことをお聞きしてしまい、申し訳ありません。 そして、Oh-Orange様のご好意に甘えてばかりですみません。 どうぞよろしくお願い致します。

kenkenpo
質問者

補足

いつも簡潔なアドバイスありがとうございます。 大変申し訳ないのですが、患者氏名の方は全角のアスタリスク("*")で置き換えたいのです。 そして、実際に名前が入力されている部分だけを置換できる方法があれば、そちらの方法も教えていただけると幸いです。 何度もお手数をお掛けしてしまい、大変申し訳ないのですが、アドバイスの程よろしくお願い致します。

  • Oh-Orange
  • ベストアンサー率63% (854/1345)
回答No.7

★アドバイス >例えば、fgetc()関数などでは、ストリームからは読み込めますが文字列「line」からは読み込めませんよね・・・ >バッファから一文字ずつ読み込んでいく関数があるのでしょうか。  ↑  関数ではなくポインタで移動していけば良いと思います。  もちろん関数にすることも出来ますが…。  関数にすると下のようなものになります。  // バッファから1文字取得(漢字非対応,シフトJIS版)  int buff_getc( const char *buff[] )  {   const char *head = *buff;      if ( *head != '\0' ){    *buff = (head + 1);    return *head;   }   return EOF;  }  // 使い方  char buff[ 256 ];  char *seek;  int ch;    /* 行単位で処理 */  while ( (seek = fgets(buff,sizeof(buff),fp)) != NULL ){   while ( (ch = buff_getc(&seek)) != EOF ){    /* 1文字ずつ処理 */   }   /* 1行の終わり */  } >これで目的の機能は実現できたのですが、仕様変更といいますか、今度は患者IDが10桁(最大10桁)と定まっておらず、 >桁数に応じてその桁数だけ"*"に置換しなければならなくなりました。  ↑  可変長なら isdigit() 関数で数字の部分だけ(*)文字に書き換えるようにすれば楽です。  下にそのサンプルを載せておきます。→あまり複雑に考えなくても良いと思います。 サンプル: #include <ctype.h> ←ファイルの頭に追加。 // switch 文の一部 case ID_BUTTON: {  FILE *fp = fopen("置換前ファイル.txt","r"); ←エラーチェックしましょう。  FILE *fo = fopen("置換後ファイル.txt","w"); ←同上  char buff[ 256 ];  char *find;    while ( fgets(buff,sizeof(buff),fp) != NULL ){   if ( (find = strstr(buff,"患者ID:")) != NULL ){    for ( find += 7 ; isdigit(*find) ; find++ ){     *find = '*'; ←ここで数字を(*)文字に書き換え    }   }   fputs( buff, fo );  }  fclose( fo );  fclose( fp );  break; } その他: >ちなみに、課題とは関係ないことなのですが、ここのサイトでソースにインデントを付けて表示させるには、 >どのようにすれば良いのでしょうか。  ↑  全角の空白文字でインデントできます。 ・以上。

kenkenpo
質問者

お礼

(続き) ちなみに、患者氏名だけ置換できるコードは以下のようになりました。 FILE *fp; FILE *fo; char buff[ 256 ]; char *seek, *find; char *search1 = "患者ID:"; char *search2 = "患者氏名 ="; char *string = "'**********'"; // switch 文の一部 case ID_BUTTON: {  if((*fp = fopen("置換前ファイル.txt","r")) != NULL){   if((*fo = fopen("置換後ファイル.txt","w")) != NULL){    while ( fgets(buff,sizeof(buff),fp) != NULL ){     if ( (find = strstr(buff, search1)) != NULL ){      for ( find += 7 ; isdigit(*find) ; find++ ){       *find = '*';      }     }     fputs( buff, fo );    }    rewind(fp);    rewind(fo);    while(fgets(buff, sizeof(buff), fp) != NULL){     for(seek = buff; (find = strstr(seek, search2)) != NULL; seek = find){      fprintf(fp_write, "%.*s", (find - seek), seek);      fputs(search2, fo);      fputs(string, fo);      find += (strlen(search2) + strlen(string));     }     if (find == NULL){      fputs(seek, fo);     }    }    fclose( fo );    fclose( fp );    break; } もう一度「fp」から読み出してるのでダメなのが一目瞭然ですが。 「fo」を"r+"でオープンすると言うのもやってみたのですが、やはりポインタの関係からか、上手くいきません。 今回も長くなってしまいましたが、ご指導の程よろしくお願い致します。 最後まで読んで頂き、誠にありがとうございました。

kenkenpo
質問者

補足

毎度、大変参考になるアドバイスの数々本当にありごがとうございます。 Oh-Orange様に教えて頂いた、isdigit() 関数を使うやり方でうまく行きました。当たり前かもしれませんが、 IsCharAlpha()という英字かどうかを判断する関数もあるんですね。今後どんどん利用していきたいと思います。 ところで、またまた機能追加といいますか、今度は"患者氏名 ="に続く名前の部分も"*"(全角アスタリスク)で 置換しなければならなくなりました。 一例をあげますと、患者氏名 ='山田 太郎     'といった感じで、 「'」から「'」までは22バイトあり、名前格納範囲は20バイトあります。 純粋に名前の部分(この例で言えば全角4文字)だけ、アスタリスクに変えられればいいのですが、 今回はとりあえず20バイトを全角アスタリスク10個の文字列で置き換えることによって上手く置換できました。 ところが、「患者ID」と「患者氏名」の両方を置換できるようなプログラムを作ることが出来ません。  while ( fgets(buff,sizeof(buff),fp) != NULL ){   if ( (find = strstr(buff,"患者ID:")) != NULL ){    for ( find += 7 ; isdigit(*find) ; find++ ){     *find = '*';    }   }   fputs( buff, fo );  } の部分で、一行バッファに読み出すごとに、両方の部分の置換を行えたらと思っているのですが、 ポインタの理解が足りないためかポインタの位置の整合性と取ることができません。 多分、数字の置換が終わった時点でバッファの先頭にポインタを戻さなければならないとは思うんですが、 その実現方法がわかりません。 他の方法として、while文の中では片方の置換しか行わず、とりあえず両ファイルともクローズして、 新たに書き込み用のファイルをもう1つ作成して読み書きするということも考えましたが、大変効率が悪いような気がします。 (文字数が足りないため、後半はお礼の欄方へ続けさせていただきます。)

  • sakusaker7
  • ベストアンサー率62% (800/1280)
回答No.6

fgets 等で一行ずつ取り出してくるときは、ファイルの大きさにもよりますが 内容すべてを一度にメモリに読み込んでくるということはしません。 この辺は細かい説明をしだすととても字数が足りないのではしょりますが、 fgetsを呼び出すことで何段階にも下請け関数やシステムコールが 呼び出されます。 最もハードウェアよりの処理ではディスクを管理するときの「セクタ」とか 「クラスタ」といった単位のブロックを基準に読み込んで行きます。 が、普通はそんなことは気にしないでいいです。

kenkenpo
質問者

補足

何度もご丁寧ご回答していただき、誠にありがとうございます。 fgets等がそのような仕組みになっているのは、全くもって知りませんでした。今すぐには難しいとは思いますが、将来的には細かい部分まで理解して関数を使いこなせるようになれればとい思います。 大変参考になりました。

  • Oh-Orange
  • ベストアンサー率63% (854/1345)
回答No.5

★アドバイス >ちなみに、ファイル(HDD)アクセスは時間が掛かる処理のため、一度に全てバッファに読み込み、 >そこから一行ずつ読み込んでいくという流れでいいのでしょうか?  ↑  確かにそうですが setvbuf() 関数で入出力バッファを 4KB に設定すると巨大なバッファを確保して  行単位で処理するのとあまり処理速度面で変化が無いようです。よって setvbuf() 関数で 4KB ~ 10KB  の範囲でバッファを設定すればメモリの節約にもなります。速度面もそこそこです。 ・あと CreateFileMapping() 関数でメモリマップドファイルを利用するとメモリにアクセスする感じで  入出力関数を利用しないでよいため楽です。ちょうどポインタと同じ要領で文字列を書き換えたり、  取得したり出来ます。ただし、文字列として扱うためにファイルサイズの後に1つの NULL 文字を書き込んで  置かないとファイルの最後の境界の判定が面倒になります。NULL 文字を書き込んでおけばそれを見て  ファイルの終わり EOF と出来ます。 ・その他、GlobalAlloc() でも 50MB のメモリは確保できたりしますね。  まぁ、とにかくいろいろと試して見て下さい。  (1)char buff[ 256 ]; のバッファで fopen()、fgets() 関数で処理。  (2)GlobalAlloc() 関数で巨大バッファをポインタで処理。  (3)CreateFileMapping() 関数でメモリマップドファイルで処理。  3つ試して速度など比較してみるのも面白いかもしれません。 ・一番楽なのは (1) の方法だと思います。  やはり高水準入出力関数ですので使いやすいです。 ・以上。→下の『参考URL』もどうぞ。

参考URL:
http://www.bohyoh.com/CandCPP/C/Library/setvbuf.html,http://homepage2.nifty.com/DSS/WinSys/Win/FileMapping.htm
kenkenpo
質問者

補足

度々、ご丁寧なご回答ありがとうございます。 ただただ感謝の気持ちでいっぱいです。 昨日の内に、経過をご報告できなくてすみませんでした。 ところで、Oh-Orange様が以前回答されていた質問(QNo.2920993)をたまたま見つけまして、 それを参考させて頂き以下のようなプログラムを作成することが出来ました。(該当部分のみ抜粋) FILE *fp_read, *fp_write; char line[256]; char *find, *seek; char *search = "患者ID="; char *string = "**********"; case WM_COMMAND: // ボタンがクリックされたときの処理 switch(LOWORD(wp)){ case ID_BUTTON: fp_read = fopen("置換前ファイル.txt", "r"); fp_write = fopen("置換後ファイル.txt", "w"); while(fgets(line, sizeof(line), fp_read) != NULL){ for(seek = line; (find = strstr(seek, search)) != NULL ; seek = find){ fprintf(fp_write, "%.*s", (find - seek), seek); fputs(search, fp_write); fputs(string, fp_write); find += (strlen(search) + strlen(string)); } if (find == NULL){ fputs( seek, fp_write); } } これで目的の機能は実現できたのですが、仕様変更といいますか、今度は患者IDが10桁(最大10桁)と定まっておらず、 桁数に応じてその桁数だけ"*"に置換しなければならなくなりました。 そこで自分の考えとしましては、forループを10回まわし、そのforループの中で文字列「line」から一文字ずつ読み込み、 その文字が改行文字('\n')だったらbreakし、そうじゃなかったらファイル「fr_write」に書き込む というような流れでできるのではと考えたのですが、なかなか上手く行きません。 入出力関数がたくさんあって、今回はどれを使うべきなのかが分りません。 例えば、fgetc()関数などでは、ストリームからは読み込めますが文字列「line」からは読み込めませんよね・・・ バッファから一文字ずつ読み込んでいく関数があるのでしょうか。 ちなみに、課題とは関係ないことなのですが、ここのサイトでソースにインデントを付けて表示させるには、 どのようにすれば良いのでしょうか。 分らないことだらけで大変申し訳ないのですが、どのような関数を使えばいいのか、 また基本的には上記のソースを使用し少し修正することで実現できるのか、教えていただけると幸いです。

  • sakusaker7
  • ベストアンサー率62% (800/1280)
回答No.4

> ちなみになんですが、mallocやcalloc等を用いて、読み込むバッファの領域を確保する必要などはないのでしょうか? それはもちろん必須です。 一行読むのに十分な大きさの領域が必要です。 あらかじめ一番長い行でどのくらいなのかが わかると楽なのですが、 学校の課題ということなので、適当な大きさ(2Kバイトとか4Kバイト)の配列を あらかじめ宣言しておいてそれを使うというのでも 良いと思います。 問題の特記事項になにかあるのならそれに従うべきですが。 あと、50Mバイトくらいのファイルというのであれば、 ファイルの内容を丸々メモリにマッピングしてしまうというてもありでしょう。 CreateFileMapping http://msdn.microsoft.com/library/ja/jpmemory/html/_win32_createfilemapping.asp?frame=true この辺のAPIを使います。 > ・重ねての質問で申し訳ないのですが、やはり50MB程度のファイルを読み込むときは、 >GetFileSize→GlobalAlloc→GlobalLockと行った流れでは問題でしょうか? >MSDNによると、VirtualAllocを使用したほうが良いとのことですが、引数の設定の仕方などがあまりよく分らないもので・・・ GlobalAllocはWin16時代から引きずってきたAPIなので VirtulAllocとはHeapAllocなどの方がいいといえばそうなのですが、 決定的にまずい点があるというわけでもないので とりあえずはそれでやってみてもいいんじゃないでしょうか。 あ、まさか実行するPCの実装メモリが128MBしかない とかいう条件はないですよね?(笑)

kenkenpo
質問者

お礼

ご回答ありがとうございます。大変勉強になります。 >学校の課題ということなので、適当な大きさ(2Kバイトとか4Kバイト)の配>列を >あらかじめ宣言しておいてそれを使うというのでも >良いと思います。 ファイル全体を見渡してみて、一行256MBあれば良いと判断したので、ウィンドウプロシージャのローカル変数に、 文字列格納用の変数char line[256];を用意しました。 >あと、50Mバイトくらいのファイルというのであれば、 >ファイルの内容を丸々メモリにマッピングしてしまうというてもありでしょ>う。 CreateFileMappingに目を通しましたが、マッピングというものをまだ理解できていない為難しそうです。 ちなみに、ファイル(HDD)アクセスは時間が掛かる処理のため、一度に全てバッファに読み込み、 そこから一行ずつ読み込んでいくという流れでいいのでしょうか? >あ、まさか実行するPCの実装メモリが128MBしかない >とかいう条件はないですよね?(笑) はい、そういう条件はありません。 何度もお手数お掛けして申し訳ありませんが、よろしくお願い致します。

  • Oh-Orange
  • ベストアンサー率63% (854/1345)
回答No.3

★回答者 No.1 です。 ・どうも寝ぼけていました。簡単なウインドウを…。って書いていますね。それじゃ GUI ですね。 >C言語のランタイムライブラリを使用した方が効率が良いのであれば、そちらの使用も考えています。  ↑  CreateFile、CloseHandle 関数よりも fopen、fclose の方が効率が良いと思います。  それに行単位で処理するには API 関数は不便です。  理由は行単位の処理を行う関数を用意する必要があるので。   >重ねての質問で申し訳ないのですが、やはり50MB程度のファイルを読み込むときは、 >GetFileSize→GlobalAlloc→GlobalLockと行った流れでは問題でしょうか?  ↑  GlobalAlloc() 関数は低速でしかも >このヒープマネージャは、4MB 未満のメモリブロックを想定して設計されています。  と MSDN マニュアルに載っているため 50 MB なら別の方法をとった方がよいかもしれません。 >MSDNによると、VirtualAllocを使用したほうが良いとのことですが、引数の設定の仕方などがあまりよく分らないもので・・・  ↑  VirtualAlloc を使っても良いですが、C 関数も使って良さそうなので fopen、fclose の利用を  お勧めします。 ・それでは簡単なアルゴリズムを紹介します。  (1)fopen() 関数で "r+" の読み書き両用モードでオープンします。  (2)ftell() 関数で現在の読み込み位置を取得しておきます。  (3)fgets() 関数で行単位で文字列を取得します。  (4)strstr() 関数で "<受診歴>患者ID:" の文字列を検索します。  (5)見つかったら strstr() 関数で得たポインタ(文字列)に +15 してそこから 10 桁の数字を   『*』文字で書き換えます。見つからなかったら(2)の処理へジャンプ。  (6)書き換えた文字列を出力します。このとき、fseek() 関数で書き込み位置を変更します。   変更する位置は (2) で取得した読み込み位置、つまり fgets() する前の位置に戻すわけです。  (7)ファイルの終わりに到達したら fclose() 関数でクローズします。  こんな感じになります。これで一度プログラムしてみて下さい。 ・上手くいかない場合は、読み書き両用モードでオープンしないで読み込み用、書き込み用で  オープンして処理すれば確実です。文字列の検索は strstr() 関数で十分でしょう。  この読み込み用、書き込み用で処理する方法は回答者 No.2 さんを参考に。 ・以上。

kenkenpo
質問者

補足

ひとつひとつ丁寧に教えて下さりありがとうございます。 大変分りやすいです。 初心者の私でも、なんとか流れは掴むことができましたので、 まずは実際に作って、動かしてみたいと思います。 そして、本日中に状況の説明を書き込ませていただきたいと 思います。その際には、またアドバイス頂けると幸いです。 ちなみに、回答者No.2の「sakusaker7」さんにも伺ったのですが、 mallocやcallocを用いて、読み込むバッファの領域を確保する必要などはないのでしょうか? 何度もお手数お掛けしてしまいますが、よろしくお願い致します。

  • sakusaker7
  • ベストアンサー率62% (800/1280)
回答No.2

質問が「文字列置換のアルゴリズムを教えてください」で、 > 簡単なウィンドウを作り、そこに配置したボタンを押下することによって とあるので、作成するのはGUIプログラムで ファイルの作成手順は今回の質問の範囲にはないと思いますがいいですか? とりあえず、「文字列置換」では有名なアルゴリズムは なかったと思うので、力技でやるしかないでしょう。 ただし、置換対象を見つける段階において工夫する余地はあります。 ただ、高速な文字列検索アルゴリズムって > "<受信歴>患者ID:"に続く 多バイト文字とは相性がよくなかったりするんですよね。 ということでとりあえず結果ガでりゃアいいだろうというレベルで (工夫の余地はありますのでその辺はがんばってください)、 ・読み込みのためにファイル1をオープンする ・書き込みのためのファイル2をオープンする ・ファイル1から一行読み込む ・読み込んだ行の中に、「<受信歴>患者ID:」 があるか検索する →とりあえず strstr (もしくは_mbsstr)を使う ・みつからなかったらそのままファイル2に出力 ・見つかった場合、 その直後がIDと見なせるものかチェックして、 もしIDらしいのなら'*'で行の内容を上書き。 ・出力 でいいんじゃないですかね。 '*'で書き潰す前後でデータの長さが変わるわけじゃないので 単純に上書きでいいでしょう。 面倒なので細かい注意事項は書きませんが、 また引っかかるようならその辺補足ででても書いてください。 親切な誰かが事細かに説明してくれるかもしれませんけど。

kenkenpo
質問者

補足

ご丁寧に回答してくださりありがとうございます。 完全には理解しきれていませんが、参考にさせていただき なんとか完成させたいと思います。 ちなみになんですが、mallocやcalloc等を用いて、読み込むバッファの領域を確保する必要などはないのでしょうか? 基本的な質問ばかりですみませんが、ご回答頂けると幸いです。

関連するQ&A

  • フォルダ内のファイルを全て開き文字列置換

    こんばんは。Perl超初心者です(プログラミングの初心者でもあります)。 フォルダ内にあるすべてのファイルに対して、正規表現を使った文字列置換をしたいのですが、うまくいきません。アドヴァイスをいただけないでしょうか。 具体的には、あるファルダの中に100個程のファイルがあって、その中の改行が3回連続している部分を、「改行+[SAMPLE]+改行」に置換をしたいと表います。 Windows XP Professional SP3 / ActivePerl 5.10 の環境で、以下のように記述したのですが、「Missing $ on loop variable at insert.pl line 5.」とエラーになってしまいます。どこで、間違ったのでしょうか(というか、まるでダメなスクリプトかもしれませんが…)。 use strict; use warnings; my @filename = glob "*.txt"; foreach open(FILE, "$filename(@filename)") { my @content =<FILE>; @content =~ s/\n{3}/\n[SAMPLE]\n/g; print FILE @content; close(FILE); } どなたか、ご教示いただけると非常助かります。 よろしくお願い致します。

    • ベストアンサー
    • Perl
  • 文字列一括置換ソフトを探してます

    文字列一括置換ソフトを探してます・・・って書くと、Vectorに行けばいっぱいあると言われそうですが、ちょっと条件が特殊です。 ・複数(多分500~1000個)のテキストファイルの指定文字列を一括置換したい(ここまでは普通) ・対象の文字列の置換パターンが3万件(苦笑) ワイルドカードなどではなく、3万の単語を別の3万の単語に置換したいのです。 (プログラムの変数を、ネーミングルールを変えたために総入れ替えする・・・と考えて頂ければ分かっていただけるでしょうか) 10件くらいのパターンを連続して置換するソフトは見つかったのですが、3万件となると・・・。 (そもそも3万件のリストをよく作ったもんだ) UNIXにsedというコマンドがあるというのは聞いたことがあるのですが、Windows(&DOS)しか触ったことがないので、どうすれば良いのか分かりません(私はプログラマじゃありません)。 なお、同じネタでエクセルのワークシートのセルを置換する必要があるのですが、こちらは自作マクロで対応しています。 もし適当なソフトが見つからなければ、エクセルマクロの作成を検討しています。 (マクロでテキストファイルの読み込みはやったことがあるのですが、書き出しは多分やったことがない) 置換リストはエクセルで作成しているので、リストはソフトに応じた形式で準備できると思います。 以上、よろしくお願いいたします。

  • 複数行に渡る文字列の置換

    こんにちは、Perl初心者です(プログラミング全般の初心者です)。 カレントフォルダ内のテキストファイルに対して、文字列置換をするスクリプトを書こうとしています。具体的には、以下のようにストリングIDの直下に改行のみの場合(ストリングがない)は、[BLANK]という文字列を挿入したいと思っています。 TEXT_STRING_ID_001<改行> <改行> <改行> TEXT_STRING_ID_002<改行> 入門書やこのサイトの皆さまのお力を借りて、なんとか以下のようなリストを書きエラーなく置換処理ができるところまでは確認できました。 しかし、この方法だと結局1行ずつ処理していることになるので、「s/\n{3}/\n[en]\n/gm」のような置換ができません(mオプションをつけてもダメなようです)。 この問題を解決する良い方法はないものでしょうか。 (もしかすると、処理の仕方を根本から変えないといけないのでしょうか) 以下、現状のリスト: use strict; use warnings; my $dirname = '.'; opendir(DIR, $dirname) or die "$dirname: $!"; while (my $dir = readdir(DIR)) { next unless (-f $dir); next unless ($dir =~ /\.txt$/); print $dir, "\n"; open(FILE, $dir) or die "$dir: $!"; my @file = <FILE>; close(FILE); foreach my $line (@file) { $line =~ s/\n{3}/\n[BLANK]\n/gm; } open(NEWFILE, "> $dir") or die "$dir: $!"; print NEWFILE @file; close(NEWFILE); } closedir(DIR);

    • ベストアンサー
    • Perl
  • 今プログラミングを始めるなら何がいいでしょうか?

    ずばり。今、プログラミングを始めるなら何がいいでしょうか? Windowsで動いて、これから成長していきそうな そんな言語はなんですか? 必要なものなど、簡単に教えていただけるとなお良いです どの言語で何ができて、どうやったら環境が整うかとかも知りたいので プログラミング言語について詳しいサイト、本、雑誌などあれば そちらの紹介もお願いします 適度に硬くなく、読みやすいければさらに良いです よろしくお願いします

  • Windowsプログラミングで

    当方、10年程前に、某ゲーム系専門学校に通ってまして プログラミングの方を専攻しておりました。 当時の学校の開発環境はPC-9801やX68000で学校では C言語や65816の授業がありました。 また当時は私はX68000が好きだったこともあり68000 のアセンブラ等も勉強してました。 当時挫折を味わい学校も辞め二年ほどプログラムという よりはパソコンから完全に離れてしまいました。 でもパソコンとかを触ったりするのはやはり好きだったので Windows95搭載パソコンを買ってしまいました。 CPUがPentium2とかMMX Pentiumとか言われてもちんぷんかんぷん でしたしDirectXが出てきて、3Dアクセラレーターボード等が 普及してきてましたが二年間程離れていたせいで3Dゲームを プレイするために必要なマシンとかも分からなかったくらい です。 VC++5.0とかも買ってしまいましたが、学校で勉強したのは 概ねシングルタスク環境での開発ですので異様な違和感を 覚えました。イベントドリブン型プログラミングに馴染めない というかMFCやWindowsAPIの関数の複雑さに圧倒されたというか そんな感じです。 二年間離れていたから対応できなくなったとは思っていませんが Windows95発売から10年以上経過した今、専門学校に入学するのが 数年遅かったら・・・もしくは数年生まれるのが遅かったら等とは 思ったりします。 結局、今では自分にプログラミングにセンスが無かったと思ってますが 「MS-DOS等では開発したことがあるがWindowsでは出来ません」 というような人がいるということをプログラム関係の本で 読んだり、あるいは専門学校の講師とかソフト開発会社の人 に聞いたことがあります。 長々と書きましたが、自分のように実務経験が無く、 あくまで趣味の域でやってる人ならともかく、職業として プログラマーをしている人はどうなのでしょうか? プログラマーとして飯食ってるひとがWindowsプログラム等の イベントドリブン型プログラミングに対応出来なかったら 死活問題だと思うのですがどうなのでしょうか? それとも実務経験のある人にはそもそもそういう人は いないのでしょうか? 長年の疑問だったので何方か答えて頂ければうれしいです。

  • プログラミング

    ソフトウェアに対して、初心者がプログラミングを始めたいのですが、独学でやるために必要な、環境とは、どのようなものでしょうか? 基本的に、ハイスペックPC(ゲーム用に使っていた)は、持っています。 個人的に調べた結果 Microsoft visual stadio .net シリーズ が、あればいいのかと思ったのですが、いかがでしょう? また、独学が出来る本も先ず何がいいのかも教えて下さい。

  • csvデータの置換について

    お世話になります。 batファイルを利用してcsvファイルの数値の置換をしたのですが、ご教授下さい。 詳細は下記になります。 【環境】 Windows2008 Server 【対象ファイル】 test.csv 【実施内容】 ファイルの内容は下記になります。 A,111,aaa B,222,bbb C,333,ccc D,444,ddd E,555,eee 上記の内容で数値を下記のように置換したい。 111は111F 222は222G 333は333H 444は444I 555は555J よろしくお願い致します。

  • プログラミングの勉強

    業務などで必要なソフトを作成できるようになりたいので プログラミングの勉強をしようと思うのですが、 たくさんの種類があってどれがいいのかわかりません フリー環境がそろい 習得のための参考書などが充実していて 広い範囲の用途に対応できるプログラミング言語は何でしょうか? 主に Windowsで動くGUIのソフトを作成したいと思っています。 Javaの関係でも Java Beans とEclipseとかあり、わからなくなりました。 よろしくお願いします

  • マックでwindowプログラミングするには?

    マック(MacOSX)でWindowプログラミングするにはどのような環境が必要でしょうか? MS-WindowsにはVisualC++やVisualBasicなどがありますが、マックに関しては調べてもよくわからないです。 Windowではないgccでのコンソールレベルのプログラムならネット上で多数見つかりましたが、Windowだけがなかなか見つからないです。 マックでWindowプログラミングができる環境にはどんなものがあるのでしょうか?無料、有料(予算2万円以下)どちらでもいいので教えてください。 #Javaは除くものとします。

  • プログラミングの開発環境構築の知識を知りたいです。

    プログラミング勉強し始めたばかりです。 ローカルで環境構築をスムーズにできるようなりたいので、体系的にどんな知識が必要なのかを教えてください。 持っているパソコンがMacなのですが、仕事ではWindowsを使うことが多いためMac・Windowsの両方、かつ、プライベートで利用する前提でのローカルでの環境構築を体系的に知りたいです。 Macでの環境構築を試みているのですが、Home brewを入れて開発に必要なパッケージを入れることくらいしか分からず、MacのOSやアップデートが原因のエラーが全く対処できないです。 WindowsでもこのようなOSやOSのアップデートによるエラーがあると思います。 知識が浅く抽象的な質問で申し訳ございませんが、どこが原因なのか見当がつけるようにしたいです。 ※ 仮想環境を作ってのプログラミング環境の構築を学習しているため、仮想環境構築でのプログラミング開発環境構築に関する回答は今のところは不要です。