【问题标题】:Unescape a universal character name to the corresponding character in C将通用字符名称转义为 C 中的相应字符
【发布时间】:2014-03-02 17:24:52
【问题描述】:

新编辑: 基本上我提供了一个不正确的例子。在我的实际应用程序中,字符串当然不会总是“C:/Users/Familjen-Styren/Documents/V\u00E5gformer/20140104-0002/text.txt”。相反,我将在 java 中有一个输入窗口,然后我将 "escape" 将 unicode 字符转换为通用字符名称。然后它将在 C 中 “未转义” (我这样做是为了避免将多字节字符从 java 传递到 c 时出现问题)。所以这里有一个例子,我实际上要求用户输入一个字符串(文件名):

#include <stdio.h>
#include <string.h>

int func(const char *fname);

int main()
{
   char src[100];
   scanf("%s", &src);
   printf("%s\n", src);
   int exists = func((const char*) src);
   printf("Does the file exist? %d\n", exists);
   return exists;
}

int func(const char *fname)
{
    FILE *file;
    if (file = fopen(fname, "r"))
    {
        fclose(file);
        return 1;
    }
    return 0;
}

现在它会认为通用字符名称只是实际文件名的一部分。那么如何“取消转义”输入中包含的通用字符名称?

第一次编辑: 所以我像这样编译这个例子:“gcc -std=c99 read.c”,其中“read.c”是我的源文件。我需要 -std=c99 参数,因为我使用前缀 '\u' 作为我的通用字符名称。如果我将它更改为 '\x' 它工作正常,我可以删除 -std=c99 参数。但在我的实际应用程序中,输入不会使用前缀“\x”,而是使用前缀“\u”。那么我该如何解决这个问题呢?

此代码给出了预期的结果,但对于我的实际应用程序,我不能真正使用 '\x':

#include <stdio.h>
#include <string.h>

int func(const char *fname);

int main()
{
   char *src = "C:/Users/Familjen-Styren/Documents/V\x00E5gformer/20140104-0002/text.txt";
   int exists = func((const char*) src);
   printf("Does the file exist? %d\n", exists);
   return exists;
}

int func(const char *fname)
{
    FILE *file;
    if (file = fopen(fname, "r"))
    {
        fclose(file);
        return 1;
    }
    return 0;
}

原文: 我找到了一些如何在其他编程语言(如 javascript)中执行此操作的示例,但我找不到任何有关如何在 C 中执行此操作的示例。这是一个产生相同错误的示例代码:

#include <stdio.h>
#include <string.h>

int func(const char *fname);

int main()
{
   char *src = "C:/Users/Familjen-Styren/Documents/V\u00E5gformer/20140104-0002/text.txt";
   int len = strlen(src); /* This returns 68. */
   char fname[len];
   sprintf(fname,"%s", src);
   int exists = func((const char*) src);
   printf("%s\n", fname);
   printf("Does the file exist? %d\n", exists); /* Outputs 'Does the file exist? 0' which means it doesn't exist. */
   return exists;
}

int func(const char *fname)
{
    FILE *file;
    if (file = fopen(fname, "r"))
    {
        fclose(file);
        return 1;
    }
    return 0;
}

如果我改用没有通用字符名称的相同字符串:

#include <stdio.h>
#include <string.h>

int func(const char *fname);

int main()
{
   char *src = "C:/Users/Familjen-Styren/Documents/Vågformer/20140104-0002/text.txt";
   int exists = func((const char*) src);
   printf("Does the file exist? %d\n", exists); /* Outputs 'Does the file exist? 1' which means it does exist. */
   return exists;
}

int func(const char *fname)
{
    FILE *file;
    if (file = fopen(fname, "r"))
    {
        fclose(file);
        return 1;
    }
    return 0;
}

它会输出 '文件存在吗? 1'。这意味着它确实存在。但问题是我需要能够处理通用字符。那么如何对包含通用字符名称的字符串进行转义呢?

提前致谢。

【问题讨论】:

  • 如果您只使用:char *src = "C:/Users/Familjen-Styren/Documents/V\u00E5gformer/20140104-0002/text.txt"; 而不使用 sprintf 会发生什么?... sprintf 不会进行编码转换,因此可能会丢弃字符。还有你目前使用的编码是什么?
  • @odedsh 它仍然输出文件不存在。我只是希望它是那么容易..
  • 你使用的是哪个编译器?
  • 您目前所做的与您要解决的问题无关。您的 源代码 包含通用字符名称,但实际程序中的实际字符串不包含。编译器已将其翻译为其他内容。如果您想在运行时翻译通用字符名称,您必须编写代码来执行此操作。
  • 这个例子不是“不是很好”。这比没有更糟糕。它促使一些人尝试为您解决一个完全错误的问题。 “我如何将字符串中的通用字符名称转换为相应的 unicode 字符串”---您必须检测 ` symbol, verify it is followed by u`,然后检测 4 个十六进制数字,然后将这些数字转换为编码中的 Unicode 字符选择(很可能是 UTF-8)。如果您还没有准备好解决这个问题,请考虑首先不要将字符转换为它们的通用名称,因为这没有任何意义。

标签: c file-io unicode-escapes


【解决方案1】:

我正在重新编辑答案,希望能更清楚。首先,我假设您对此很熟悉:http://www.joelonsoftware.com/articles/Unicode.html。处理字符编码时需要背景知识。

现在我开始使用我在我的 linux 机器上输入的一个简单的测试程序 test.c

#include <stdio.h>
#include <string.h>
#include <wchar.h>
#define BUF_SZ 255
void test_fwrite_universal(const char *fname)
{
    printf("test_fwrite_universal on %s\n", fname);
    printf("In memory we have %d bytes: ", strlen(fname));
    for (unsigned i=0; i<strlen(fname); ++i) {
        printf("%x ", (unsigned char)fname[i]);
    }
    printf("\n");
    
    FILE* file = fopen(fname, "w");
    if (file) {
        fwrite((const void*)fname, 1, strlen(fname),  file);        
        fclose(file);
        file = NULL;
        printf("Wrote to file successfully\n");
    }
}

int main()
{
    test_fwrite_universal("file_\u00e5.txt");
    test_fwrite_universal("file_å.txt");   
    test_fwrite_universal("file_\u0436.txt");   
    return 0;
}

文本文件被编码为 UTF-8。在我的 linux 机器上,我的语言环境是 en_US.UTF-8 所以我像这样编译并运行程序:

gcc -std=c99 test.c -fexec-charset=UTF-8 -o test

测试

test_fwrite_universal on file_å.txt
In memory we have 11 bytes: 66 69 6c 65 5f c3 a5 2e 74 78 74 
Wrote to file successfully
test_fwrite_universal on file_å.txt
In memory we have 11 bytes: 66 69 6c 65 5f c3 a5 2e 74 78 74 
Wrote to file successfully
test_fwrite_universal on file_ж.txt
In memory we have 11 bytes: 66 69 6c 65 5f d0 b6 2e 74 78 74 
Wrote to file successfully

文本文件是 UTF-8,我的语言环境是 UTF-8,char 的执行字符集是 UTF-8。 在 main 中,我用字符串调用函数 fwrite 3 次。该函数逐字节打印字符串。然后写入一个具有该名称的文件并将该字符串写入该文件。

我们可以看到“file_\u00e5.txt”和“file_å.txt”是一样的:66 69 6c 65 5f c3 a5 2e 74 78 74 果然(http://www.fileformat.info/info/unicode/char/e5/index.htm)代码点 +00E5 的 UTF-8 表示是:c3 a5 在最后一个示例中,我使用了 \u0436,它是一个俄语字符 ж (UTF-8 d0 b6)

现在让我们在我的 Windows 机器上尝试同样的方法。这里我使用 mingw 并执行相同的代码:

C:\test>gcc -std=c99 test.c -fexec-charset=UTF-8 -o test.exe

C:\test>测试

test_fwrite_universal on file_å.txt
In memory we have 11 bytes: 66 69 6c 65 5f c3 a5 2e 74 78 74
Wrote to file successfully
test_fwrite_universal on file_å.txt
In memory we have 11 bytes: 66 69 6c 65 5f c3 a5 2e 74 78 74
Wrote to file successfully
test_fwrite_universal on file_╨╢.txt
In memory we have 11 bytes: 66 69 6c 65 5f d0 b6 2e 74 78 74
Wrote to file successfully

所以看起来出现了可怕的错误 printf 没有正确写入字符并且磁盘上的文件看起来也有问题。 有两件事值得注意:就字节值而言,文件名在 linux 和 windows 中是相同的。用notepad++之类的东西打开文件的内容也是正确的

问题的原因是 Windows 和语言环境上的 C 标准库。在 linux 上系统语言环境是 UTF-8 在 Windows 上我的默认语言环境是 CP-437。当我调用诸如 printf fopen 之类的函数时,它假定输入在 CP-437 中,而 c3 a5 实际上是两个字符。

在我们查看合适的 Windows 解决方案之前,让我们尝试解释一下为什么 file_å.txtfile_\u00e5.txt 的结果不同。 我相信关键是你的文本文件的编码。如果我在 CP-437 中写同样的test.c

C:\test>iconv -f UTF-8 -t cp437 test.c > test_lcl.c

C:\test>gcc -std=c99 test_lcl.c -fexec-charset=UTF-8 -o test_lcl.exe

C:\test>test_lcl

test_fwrite_universal on file_å.txt
In memory we have 11 bytes: 66 69 6c 65 5f c3 a5 2e 74 78 74
Wrote to file successfully
test_fwrite_universal on file_å.txt
In memory we have 10 bytes: 66 69 6c 65 5f 86 2e 74 78 74
Wrote to file successfully
test_fwrite_universal on file_╨╢.txt
In memory we have 11 bytes: 66 69 6c 65 5f d0 b6 2e 74 78 74
Wrote to file successfully

我现在了解了 file_å 和 file_\u00e5 之间的区别。文件中的字符å实际上编码为0x86。请注意,这次第二个字符串是 10 个字符而不是 11 个字符。 如果我们查看文件并告诉 Notepad++ 使用 UTF-8,我们会看到一个有趣的结果。写入文件的实际数据也是如此。

最后如何让这该死的东西在 Windows 上运行。不幸的是,似乎不可能将标准库与 UTF-8 编码的字符串一起使用。在 Windows 上,您不能将 C 语言环境设置为此。见:What is the Windows equivalent for en_US.UTF-8 locale?

但是我们可以使用宽字符来解决这个问题:

#include <stdio.h>
#include <string.h>
#include <windows.h>
#define BUF_SZ 255
void test_fopen_windows(const char *fname)
{
    wchar_t buf[BUF_SZ] = {0};
    int sz = MultiByteToWideChar(CP_UTF8, 0, fname, strlen(fname), (LPWSTR)buf, BUF_SZ-1);
    wprintf(L"converted %d characters\n", sz);
    wprintf(L"Converting to wide characters %s\n", buf);
    FILE* file =_wfopen(buf, L"w");
    if (file) {
        fwrite((const void*)fname, 1, strlen(fname),  file);        
        fclose(file);
        wprintf(L"Wrote file %s successfully\n", buf);
    }
}


int main()
{
    test_fopen_windows("file_\u00e5.txt");
    return 0;
}

编译使用:

gcc -std=gnu99 -fexec-charset=UTF-8 test_wide.c -o test_wide.exe

_wfopen 不符合 ANSI,并且 -std=c99 实际上意味着 STRICT_ANSI,因此您应该使用 gnu99 来获得该功能。

【讨论】:

  • 好的,但实际上,当我尝试硬编码的 UTF-8 字符串时,fopen() 似乎确实有效。如果您想回答有关如何通过 JNI 传递 UTF-8 字符串的问题,我已经问过问题here。这个答案将永远被接受,因为你已经帮助我安静了很多,但我担心我会在这个问题上停留一段时间......谢谢。
  • 我认为文字“Vågformer”会起作用的唯一原因是,如果您在 Windows ANSI 代码页 437 中并且 å 实际上是 0x86 而不是 utf-8
  • @Linus 我用更多信息重写了答案,希望这次有用。
  • 这是一些非常有用的信息,但是如果您使用另一个字符集(如 ISO/IEC 8859-1 或 windows 默认字符集:Windows-1252)会发生什么?
  • 然后你需要在那个字符集中对字符串进行编码,你会得到不同的结果。在代码页 1252 中,0x86 的代码是一把小匕首/十字架。你的角色是0xE5。然而,一些可能的字符会明显丢失
【解决方案2】:

错误的数组大小(忘记了 .txt 和 \0 以及编码的非 ASCII 字符占用超过 1 个字节。)

// length of the string without the universal character name. 
// C:/Users/Familjen-Styren/Documents/Vågformer/20140104-0002/text
// 123456789012345678901234567890123456789012345678901234567890123
//          1         2         3         4         5         6
// int len = 63;

// C:/Users/Familjen-Styren/Documents/Vågformer/20140104-0002/text.txt
int len = 100;


char *src = "C:/Users/Familjen-Styren/Documents/V\u00E5gformer/20140104-0002/text.txt";
char fname[len];
// or if you can use VLA
char fname[strlen(src)+1];

sprintf(fname, "%s", src);

【讨论】:

  • 啊,对不起。我很快就做了这个例子,错过了那部分,不过我会更新这个问题。
  • @Linus 请在缓冲区大小足够后重新验证您是否仍有å 问题。
  • 我对我的问题进行了新的更新(请参阅编辑标签)。
  • @Linus 63+4+1 太小了。使用更大。尝试打印strlen(src)。可疑代码至少需要 63+4+1+1。
猜你喜欢
  • 1970-01-01
  • 2023-03-11
  • 1970-01-01
  • 2023-03-16
  • 1970-01-01
  • 2023-03-08
  • 2016-02-06
  • 1970-01-01
相关资源
最近更新 更多