Actian Zen によるコード ページとデータ エンコードの簡略化

情報をファイルに保存するとき、データのグローバル化をサポートするためにエンコードする必要があります。適切にエンコードされたデータは、さまざまなロケールのさまざまなユーザーが簡単に読み取れます。これは、データベースや単純なテキスト ドキュメントでも当てはまります。

右側の「このページの内容」では当ページで扱うトピックを示しています。

エンコード システム

ASCII は最も一般的なエンコード システムの 1 つです。最新の ASCII 標準では、文字 A は数値 65 として、B は 66 としてエンコードされます。初期の ASCII の形式では、127 文字のみ(大文字と小文字の英語の文字、数字、句読記号、およびその他の特殊文字)がサポートされていました。最近の ASCII の形式では 255 文字までサポートされるようになりましたが、さまざまな書記体系で使用されるすべての記号を表すには、これでも十分ではありませんでした。

さまざまな言語に必要な文字をサポートするために、同じ値が異なる書記体系で別の文字を表すために再利用されます。文字のエンコード方法のさまざまな体系はコード ページと呼ばれ、そのほとんどが ASCII に基づいています。

企業や組織が独自のコード ページを定義し、それらに名前を付けています。異なる組織が独自の名前付け規則を使用しているため、同じ体系のエンコードが複数の名前で存在していることがあります。コード ページのいくつかの例として、Microsoft Windows-1252(ラテン語ベースの西欧言語用)、コード ページ 932(日本語文字用)、および ISO-8859-1(Windows-1252 とほとんど同じ)があります。

フラット ファイルでは、どのエンコードが使用されているかは必ずしも明確ではありません。多くのエンコードは ASCII にあるラテン文字に対して同じ表記を使用しているため、大量のテキストを含むドキュメントを表示している場合には、一見したところ、正しいエンコードが使用されているように見える場合があります。しかし、時には、他の特定の文字が出てくるまで気付かないという問題もあります。

したがって、ファイルへの書き込みとファイルからの読み取りで同じエンコードが使用されるようにすることは重要です。残念ながら、プレーン テキスト ファイルでは、ファイルの作成に使用され、ファイルを正確に読み取るために必要となるデフォルトのコード ファイルを識別する標準の方法がありません。

対照的に、データベースではコード ページをデータに合わせて構成することができ、また構成する必要があります。保存されたデータのコード ページとアプリケーションが期待するコード ページとの間に違いがある場合は、データベースが 2 つの間で自動的に変換する必要があります。

コード ページとテキスト ファイル

適切なコード ページが指定されたフラット テキスト ファイルに保存されているデータをエンコードおよびデコードするという、一般的なシナリオを見てみましょう。国と通貨の一覧が CSV として書式設定されたファイルがあります。適切に表示された場合、ファイルの行は次のようになります。

このようなファイルは、上記のデータのフィールドを含む CurrencyRecord という名前の構造体があるものとして、以下のコード スニペットで読み書きすることができます。

"Australia","AUS","Dollar","$"
"Canada","CAD","Dollar","$"
"France","EUR","Euro","€"
"United Kingdom","GBP","Pound","£"
"United States","Dollar","$"

このようなファイルは、上記のデータのフィールドを含む CurrencyRecord という名前の構造体があるものとして、以下のコード スニペットで読み書きすることができます。

void writeCurrencyRecords()
{
       std::ofstream outputFile("CurrencyData.csv");
       for (auto it = currencyList.begin(); it < currencyList.end(); ++it)
       {
              outputFile << it->country << ", " << it->code << ", "
                    << it->name << ", " << it->symbol << std::endl;
       }
}
void readCurrencyRecords()
{
       std::ifstream inputFile("CurrencyData.csv");
       std::vector<std::string> fieldList;
       std::string line;
       if (!inputFile.good())
              return;
       while (std::getline(inputFile,line))
       {
              CurrencyRecord r = { 0 };
              std::string field;
          
              std::stringstream ss(line);
              fieldList.clear();
              while (std::getline(ss, field, ','))
                    fieldList.push_back(field);
              strncpy(r.country, fieldList[0].c_str(), 32);
              strncpy(r.code, fieldList[1].c_str(), 4);
              strncpy(r.name,fieldList[2].c_str(),32);
              r.symbol = fieldList[3][0];
              currencyList.push_back(r);
       }
       inputFile.close();
}

このファイルの保存と読み取りをコード ページ ISO-8859-1 を使用して行おうと、Windows-1252 を使用して行おうと、結果は同じです。プログラムでデータに対して追加の処理を実行せずにデータを表示しても、期待される文字が表示されます。

コード ページの混乱

通常、アプリケーションはオペレーティング システムにより設定されたコード ページを使用しますが、アプリケーションに独自の設定がある場合があります。ファイルは、同じ設定の同じプログラムによって読み書きされている限りは、適切に処理されます。問題が生じる可能性があるのは、異なるコード ページが設定されたコンピューターにファイルがコピーされた場合や、異なる設定のアプリケーションでファイルが使用された場合です。

参考までに、元のバージョンは次のとおりです。

"Australia","AUS","Dollar","$"
"Canada","CAD","Dollar","$"
"France","EUR","Euro","€"
"United Kingdom","GBP","Pound","£"
"United States","Dollar","$"

ファイルが ISO-8859-1 でエンコードされているのに対し、アプリケーションが Windows 1252 を使用して読み取った場合、結果は少し異なります。

"Australia","AUS","Dollar","$"
"Canada","CAD","Dollar","$"
"France","EUR","Euro","¤"
"United Kingdom","GBP","Pound","£"
"United States","Dollar","$"

ユーロ(€)の記号は別の文字に置き換わっていますが、その他の通貨記号は正しいです。これらの 2 つのコード ページは、異なる数値を使用してユーロ記号をエンコードしています。

これは、問題が起きる可能性のある優しい例です。エンコードの違いは、さらに深刻な問題を引き起こす可能性があります。次の例では、プレーン テキスト ファイルは間違ったエンコードで読み取られました。結果は完全に読み取り不可能で、これを読み取ろうとするプログラムでは破損したものとして扱われるかもしれません。

EÿþD Q ¹Ø-ØãØóØÐØÈØéØüغlQ_  ,gåeˆØŠØ↑Ø)YzznØþvØLØ

あるシステムから別のシステムへデータを渡すとき、エンコードの違いがある場合には変換が必要です。コード ページ間の変換に利用できる関数は、コンパイラ固有であってもプラットフォーム固有であってもかまいません。

Windows 1252 と ISO-8859-1 のコード ページの違いのみを変換するための 1 つの方法は、エンコードが異なる文字のマップを作成してそれらの文字を交換し、それ以外の文字は元のまま変換を通過できるようにすることです。

void populateMap()
{
    cp1252_To_iso85591.emplace(std::make_pair('\x80', '\0xa4'));
    cp1252_To_iso85591.emplace(std::make_pair('\x8A', '\0xa6'));
    cp1252_To_iso85591.emplace(std::make_pair('\x8c', '\0xbc'));
    cp1252_To_iso85591.emplace(std::make_pair('\x8e', '\0xb4'));
    cp1252_To_iso85591.emplace(std::make_pair('\x9a', '\0xa8'));
    cp1252_To_iso85591.emplace(std::make_pair('\x9c', '\0xbd'));
    cp1252_To_iso85591.emplace(std::make_pair('\x98', '\0xb8'));
    cp1252_To_iso85591.emplace(std::make_pair('\x9f', '\0xbe'));
    //make a reverse map for conversions in the other direction
    for (auto it = cp1252_To_iso85591.begin(); it != cp1252_To_iso85591.end(); ++it)
        iso85591_to_cp1252.emplace(std::make_pair(it->second, it->first));
}
std::string convert_85591_to_1252(std::string source)
{
    std::string returnValue;
    for (std::string::iterator it = source.begin(); it != source.end(); ++it)
    {
        char c = *it;
        //For characters encoded with values 0x00 to 0x7f the encodings are identical
        if (c <= 0x7f)
        {
            returnValue.push_back(c);
        }
        else {
            auto result = cp1252_To_iso85591.find(c);
            if (result != cp1252_To_iso85591.end())
                returnValue.push_back(result->second);
        }
    }
    return returnValue;
}

std::string convert_1252_to_8559(std::string source)
{
    std::string returnValue;
    for (std::string::iterator it = source.begin(); it != source.end(); ++it)
    {
        char c = *it;
        //For characters encoded with values 0x00 to 0x7f the encodings are identical
        if (c <= 0x7f)
        {
            returnValue.push_back(c);
        }
        else {
            auto result = iso85591_to_cp1252.find(c);
            if (result != iso85591_to_cp1252.end())
                returnValue.push_back(result->second);
        }
    }
    return returnValue;
}

これには多くのコードが必要です。データを目的のコード ページに変換するために、読み取るまたは書き込むデータでこれらの関数を呼び出す必要があります。また、このコードはこれら 2 つの特定のコード ページ間の変換を実行することはできますが、他のコード ページを処理することはできません。

さらに、このコードはそもそも、どのエンコードを使用してデータが保存されたかがわからないという問題に対処していません。異なるコード ページが一部の文字をまったく同じにエンコードすることがあるため、ファイルを一見して、すべてのデータではないが一部のデータで機能すると想定してしまう可能性があります。

Actian Zen でのコード ページのサポート

Actian Zen データベースを使用する類似したシナリオを見てみましょう。ここでは、SQL アクセス方法の接続とネイティブの Btrieve 2 API を使用します。

ODBC(Open Database Connectivity)は、データベースにアクセスするための API です。ODBC を使用するアプリケーションでは、ドライバーはアプリケーションとデータベースの間に位置します。ドライバーはデータベースのアダプターとして機能し、アプリケーションからの要求をデータベース システムに適したものに翻訳します。

Actian Zen ODBC ドライバーは、データベースとの通信をサポートすることに加え、コード ページ間の変換も行えます。データベースとアプリケーションにはそれぞれ独自のコード ページ設定があり、ODBC ドライバーはそれらの設定を利用して行う変換を決定します。

Zen Control Center で新しいデータベースを作成するとき、日本語の Windows 版では、データベース コード ページは "サーバーのデフォルト" に設定されており、変更できません。この初期値は、オペレーティング システムで使用されているコード ページです。オペレーティング システムのデフォルトは言語設定に依存します。たとえば、Windows の英語版プラットフォームの場合は、一般的に Windows-1252 が設定されています。コード ページ設定の変更を希望される場合は、サポートまでお問い合わせください。ファイルを開く、ファイルの保存、データのインポート/エクスポート、スキーマのエクスポートの各ダイアログでは、コード ページを変更するオプションが提供されています。

同じ地域内でも、複数の言語が話されている地域では特に、コンピューターがさまざまな言語設定になっている可能性があります。データが複数のシステムで使用される場合は、Btrieve 2 API を使用して書き込むデータに一致するコード ページを選択する必要があります。データベースを作成するときにこれを行います。データベースが作成され、データが入力された後に設定を変更すると、既に書き込まれているデータは自動的に更新されません。いったん作成されると、テキスト エンコードはデータベースの属性となり、ドライバーはこの属性により、適用する変換を知ります。

次のコードは、既存のデータベースのテーブルから情報を読み取り、それをコンソールに出力します。コード ページの変換は ODBC によって処理されるため、次のコード内にコード ページを変換するためのものは何もありません。それは自動的に行われます。

#include <windows.h>
#include <iostream>
#include <sql.h>
#include <sqlext.h>
#include <stdio.h>

using namespace std;
const WCHAR* CONNECTION_STRING = L"Driver={Pervasive ODBC Unicode Interface};DSN=MyZenSrc";

 int main()
{

       SQLWCHAR* SELECT_STATEMENT = (SQLWCHAR*)L"SELECT ID,Country, CurrencyCode,CurrencyName, Symbol FROM CurrencyInfo ";
       SQLWCHAR* INSERT_STATEMENT = (SQLWCHAR*)L"INSERT INTO CurrencyInfo (Country,CurrencyCode, CurrencyName, Symbol) values (?, ?, ?, ?)";
       SQLRETURN sret;
       SQLHENV       hEnvironment = nullptr;
       SQLHDBC hDB; //database handle
       SQLHSTMT hSqlStatement;
       //The return value for each call should be checked for errors. 
       //Checks are omitted here for clarity.

       sret = SQLAllocHandle(SQL_HANDLE_ENV, SQL_NULL_HANDLE, &hEnvironment);
       sret = SQLSetEnvAttr(hEnvironment,SQL_ATTR_ODBC_VERSION,reinterpret_cast<SQLPOINTER>(SQL_OV_ODBC3),0);

       sret = SQLAllocHandle(SQL_HANDLE_DBC, hEnvironment, &hDB);
       sret = SQLDriverConnect(hDB, NULL, (SQLWCHAR*)CONNECTION_STRING, SQL_NTS, NULL, 0, NULL, SQL_DRIVER_COMPLETE);
       sret = SQLAllocHandle(SQL_HANDLE_STMT, hDB, &hSqlStatement);

       //Buffers for parameters
       WCHAR CountryName[51] = { 0 };
       WCHAR CurrencyCode[4] = { 0 };
       WCHAR CurrencyName[17] = { 0 };
       WCHAR CurrencySymbol[3] = { 0 };
       SQLLEN CountryLen, CurrencyNameLen, CurrencyCodeLen, CurrencySymbolLen, IDLen;
       
       //Copy values into buffers
       wcscpy_s(CountryName, L"Japan");
       wcscpy_s(CurrencyCode, L"JPY");
       wcscpy_s(CurrencyName, L"Yen");
       wcscpy_s(CurrencySymbol, L"¥");

       //Set those buffers as parameters for next statement
       CountryLen = CurrencyCodeLen = CurrencyNameLen = CurrencySymbolLen = SQL_NTSL;
       sret = SQLBindParameter(hSqlStatement, 1, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_VARCHAR, 50, 0, CountryName, 50, &CountryLen);
       sret = SQLBindParameter(hSqlStatement, 2, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_VARCHAR, 4,  0, CurrencyCode, 4, &CurrencyCodeLen);
       sret = SQLBindParameter(hSqlStatement, 3, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_VARCHAR, 17, 0, CurrencyName, 16, &CurrencyNameLen);
       sret = SQLBindParameter(hSqlStatement, 4, SQL_PARAM_INPUT, SQL_C_WCHAR, SQL_VARCHAR, 3,  0, CurrencySymbol, 50, &CurrencySymbolLen);

       //Insert record
       sret = SQLExecDirect(hSqlStatement, (SQLWCHAR*)INSERT_STATEMENT, SQL_NTS);
       //Free previous statement handle and create new one to use for retrieval statement
       SQLFreeHandle(SQL_HANDLE_STMT, hSqlStatement);
       sret = SQLAllocHandle(SQL_HANDLE_STMT, hDB, &hSqlStatement);

       if (sret != SQL_SUCCESS)
       {

       SQLWCHAR      SqlState[6], SQLStmt[100], Msg[SQL_MAX_MESSAGE_LENGTH];
       SQLINTEGER    NativeError;
       SQLSMALLINT   i, MsgLen;
       SQLRETURN     rc1, rc2;
       SQLLEN numRecs = 0;
       SQLGetDiagField(SQL_HANDLE_STMT, hSqlStatement, 0, SQL_DIAG_NUMBER, &numRecs, 0, 0);

       if (sret == SQL_SUCCESS)
       {
          bool moreData = true; 
          int ID;
          //Bind columns to our variables. Every time SQLFetch is called, 
          // these variables will be populated with values of next row.
          sret = SQLSetStmtAttr(hSqlStatement, SQL_ATTR_PARAM_BIND_TYPE,
              SQL_PARAM_BIND_BY_COLUMN, 0);
          sret = SQLBindCol(hSqlStatement, 1, SQL_C_SLONG, &ID, sizeof(ID), &IDLen);
          sret = SQLBindCol(hSqlStatement, 2, SQL_C_TCHAR, CountryName, sizeof(CountryName), &CountryLen);
          sret = SQLBindCol(hSqlStatement, 3, SQL_C_TCHAR, CurrencyCode, sizeof(CurrencyCode), &CurrencyCodeLen);
          sret = SQLBindCol(hSqlStatement, 4, SQL_C_TCHAR, CurrencyName, sizeof(CurrencyName), &CurrencyNameLen);|
          sret = SQLBindCol(hSqlStatement, 5, SQL_C_TCHAR, CurrencySymbol, sizeof(CurrencySymbol), &CurrencySymbolLen);
          sret = SQLExecDirect(hSqlStatement, SELECT_STATEMENT, SQL_NTS);
          do {
                sret = SQLFetch(hSqlStatement);
                if (sret == SQL_NO_DATA)
                   moreData = false;
            else
                wcout << ID << L", " << CountryName << L", " << CurrencyCode << ", " << CurrencyName << ", " << CurrencySymbol << endl;
           } while (moreData);
       }

       if (hSqlStatement)
            SQLFreeHandle(SQL_HANDLE_STMT, hSqlStatement);
       if (hDB)
       {
            SQLDisconnect(hDB);
            SQLFreeHandle(SQL_HANDLE_DBC, hDB);
       }
       if (hEnvironment)
       SQLFreeHandle(SQL_HANDLE_ENV, hEnvironment);
       return 0;
}

Btrieve 2 API を使用して Zen データ ファイル(データベースの一部であるかどうかは任意)と通信する場合、Btrieve 2 を介して渡されたデータは、それが渡されたのと同じフォームで書き込まれます。これが、ODBC と Btrieve 2 を使用する違いの重要点です。Btrieve 2 では、データ アクセスがより直接的で、書き込む前に翻訳や変換を行わないため、ファイルへの高速アクセスが提供されます。データ アクセスに ODBC を使用すると、直接的な方法ほど速くありませんが、ドライバーが提供する追加機能に伴って生じる利便性があります。

Btrieve ファイルがデータベースの一部である場合に自動コード ページ変換を利用するには、データにアクセスする他のアプリケーションは ODBC 経由で接続することができます。そうでない場合、アプリケーションは自身のコード ページ変換を実行する必要があります。

次のコードでは、Btrieve 2 API を使用して、Btrieve ファイルからレコードのコレクションを読み取っています。テキストは、アプリケーションと同じコード ページを使用してファイルに書き込まれます。

status = btrieveClient.FileOpen(&btrieveFile, FILE_NAME, NULL, Btrieve::OPEN_MODE_NORMAL);
if (status != Btrieve::STATUS_CODE_NO_ERROR)
   return -1;
int bytesRead = btrieveFile.RecordRetrieveFirst(sortIndex, (char*)&record, sizeof(record));
if (bytesRead > 0)
{
    while (status == Btrieve::STATUS_CODE_NO_ERROR )
    {
        currencyList.push_back(record);
        btrieveFile.RecordRetrieveNext((char*)&record, sizeof(record));
        status = btrieveFile.GetLastStatusCode();   
    }
}
btrieveClient.FileClose(&btrieveFile);
btrieveClient.Reset();

次に、レコードのリストをデータベースに追加します。

Btrieve::StatusCode  status = btrieveClient.FileOpen(&btrieveFile, FILE_NAME, NULL, Btrieve::OPEN_MODE_NORMAL);
for (int i=0;i<currencyList.size();++i)
{
    btrieveFile.RecordCreate((char*)&currencyList[i],
        sizeof(CurrencyRecord));
}
btrieveClient.FileClose(&btrieveFile);
btrieveClient.Reset();

Btrieve 2 ベースのコードははるかに少なく、SQL の使用が必要となりません。また、Btrieve 2 のコードは ODBC を使用するコードに比べ、データ ファイルの読み書きに必要となる呼び出しが少ないため、より高速になります。

まとめ

アプリケーション共有データは、同じコード ページを使用するか、またはあるコード ページから別のコード ページへデータを変換する確実な方法を使用する必要があります。SQL メソッドを使用してデータにアクセスする(上記の例では、ODBC ドライバー経由の)アプリケーションの場合、コード ページ変換はデータベース ドライバーを介して既に利用できるようになっています。

独自のファイルを直接管理するアプリケーションの場合、コード ページ変換は自動ではありません。それらのデータは、特定のアプリケーションで使用されるエンコードを使用して書き込まれます。Zen データベースの一部である Btrieve ファイルの場合は、データを生成するアプリケーションが SQL を使用しない場合でも、情報にアクセスする他のアプリケーションは SQL メソッドを使用してデータをインポートすることができます。

この既存のコード ページ変換ソリューションを使用すると、システム間でデータを渡す必要があるときに、データの破損を防ぐのに役立ちます。Btrieve の詳細は、Btrieve 2 API ドキュメントで確認できます。また、[各種例]タブに他の C++ サンプルがあります。