Подтвердить что ты не робот

Проблемы с кодировкой для файла CSV UTF8 при открытии Excel и TextEdit

Недавно я добавил кнопку CSV-загрузки, которая берет данные из базы данных (Postgres) из массива с сервера (Ruby on Rails) и превращает его в CSV файл на стороне клиента (Javascript, HTML5). В настоящее время я тестирую CSV файл, и я сталкиваюсь с некоторыми проблемами с кодировкой.

Когда я просматриваю файл CSV с помощью "less", файл выглядит нормально. Но когда я открываю файл в Excel или TextEdit, я начинаю видеть странные символы, например

â € ", â €, â € œ

появится в тексте. В основном, я вижу символы, которые описаны здесь: http://digwp.com/2011/07/clean-up-weird-characters-in-database/

Я читал, что такая проблема может возникнуть, когда параметр кодирования базы данных установлен неверным. НО, база данных, которую я использую, настроена на использование кодировки UTF8. И когда я отлаживаю коды JS, которые создают CSV файл, текст выглядит нормальным. (Это может быть способность Chrome и меньше возможностей)

Я чувствую себя расстроенным, потому что единственное, что я изучаю в своем онлайн-поиске, это то, что может быть много причин, по которым кодирование не работает, я не уверен, какая часть виновата (так извините, множество вещей), и ничто, что я пробовал, не пролило свет на мою проблему.

Для справки, здесь фрагмент JavaScript, который создает файл CSV!

$(document).ready(function() {
var csvData = <%= raw to_csv(@view_scope, clicks_post).as_json %>;
var csvContent = "data:text/csv;charset=utf-8,";
csvData.forEach(function(infoArray, index){
  var dataString = infoArray.join(",");
  csvContent += dataString+ "\n";
}); 
var encodedUri = encodeURI(csvContent);
var button = $('<a>');
button.text('Download CSV');
button.addClass("button right");
button.attr('href', encodedUri);
button.attr('target','_blank');
button.attr('download','<%=title%>_25_posts.csv');
$("#<%=title%>_download_action").append(button);
});
4b9b3361

Ответ 1

Как @jlarson обновил информацию о том, что Mac был самым большим виновником, мы могли бы получить еще немного. Office для Mac имеет, по крайней мере, 2011 год и обратно, довольно плохую поддержку для чтения форматов Unicode при импорте файлов.

Поддержка UTF-8, по-видимому, близка к несуществующей, прочитала небольшое количество комментариев о ее работе, в то время как большинство утверждают, что это не так. К сожалению, у меня нет Mac для тестирования. Итак, снова: сами файлы должны быть в порядке, как UTF-8, но импорт останавливает процесс.

Написал быстрый тест в Javascript для экспорта процентов экранированного UTF-16 маленького и большого endian, с/без спецификации и т.д.

Код, вероятно, должен быть рефакторирован, но должен быть в порядке для тестирования. Это может работать лучше, чем UTF-8. Конечно, это также обычно означает большие передачи данных, так как любой глиф представляет собой два или четыре байта.

Здесь вы можете найти скрипку:

Пример экспорта файла Unicode Скрипт

Обратите внимание, что он не обрабатывает CSV каким-либо определенным образом. Он в основном предназначен для чистого преобразования в URL-адрес данных, имеющий UTF-8, UTF-16 большой/маленький конец и +/- спецификацию. В скрипте есть один вариант заменить запятые на вкладки, но верьте, что это будет довольно хакерское и хрупкое решение, если оно работает.


Обычно используется как:

// Initiate
encoder = new DataEnc({
    mime   : 'text/csv',
    charset: 'UTF-16BE',
    bom    : true
});

// Convert data to percent escaped text
encoder.enc(data);

// Get result
var result = encoder.pay();

Существует два свойства результата объекта:

1.) encoder.lead

Это тип mime-типа, charset и т.д. для URL-адреса данных. Построен из параметров, переданных в инициализатор, или можно также сказать .config({ ... new conf ...}).intro() для повторной сборки.

data:[<MIME-type>][;charset=<encoding>][;base64]

Вы можете указать base64, но нет преобразования base64 (по крайней мере, не так далеко).

2.) encoder.buf

Это строка с процентом экранированных данных.

Функция .pay() просто возвращает 1.) и 2.) как один.


Основной код:


function DataEnc(a) {
    this.config(a);
    this.intro();
}
/*
* http://www.iana.org/assignments/character-sets/character-sets.xhtml
* */
DataEnc._enctype = {
        u8    : ['u8', 'utf8'],
        // RFC-2781, Big endian should be presumed if none given
        u16be : ['u16', 'u16be', 'utf16', 'utf16be', 'ucs2', 'ucs2be'],
        u16le : ['u16le', 'utf16le', 'ucs2le']
};
DataEnc._BOM = {
        'none'     : '',
        'UTF-8'    : '%ef%bb%bf', // Discouraged
        'UTF-16BE' : '%fe%ff',
        'UTF-16LE' : '%ff%fe'
};
DataEnc.prototype = {
    // Basic setup
    config : function(a) {
        var opt = {
            charset: 'u8',
            mime   : 'text/csv',
            base64 : 0,
            bom    : 0
        };
        a = a || {};
        this.charset = typeof a.charset !== 'undefined' ?
                        a.charset : opt.charset;
        this.base64 = typeof a.base64 !== 'undefined' ? a.base64 : opt.base64;
        this.mime = typeof a.mime !== 'undefined' ? a.mime : opt.mime;
        this.bom = typeof a.bom !== 'undefined' ? a.bom : opt.bom;

        this.enc = this.utf8;
        this.buf = '';
        this.lead = '';
        return this;
    },
    // Create lead based on config
    // data:[<MIME-type>][;charset=<encoding>][;base64],<data>
    intro : function() {
        var
            g = [],
            c = this.charset || '',
            b = 'none'
        ;
        if (this.mime && this.mime !== '')
            g.push(this.mime);
        if (c !== '') {
            c = c.replace(/[-\s]/g, '').toLowerCase();
            if (DataEnc._enctype.u8.indexOf(c) > -1) {
                c = 'UTF-8';
                if (this.bom)
                    b = c;
                this.enc = this.utf8;
            } else if (DataEnc._enctype.u16be.indexOf(c) > -1) {
                c = 'UTF-16BE';
                if (this.bom)
                    b = c;
                this.enc = this.utf16be;
            } else if (DataEnc._enctype.u16le.indexOf(c) > -1) {
                c = 'UTF-16LE';
                if (this.bom)
                    b = c;
                this.enc = this.utf16le;
            } else {
                if (c === 'copy')
                    c = '';
                this.enc = this.copy;
            }
        }
        if (c !== '')
            g.push('charset=' + c);
        if (this.base64)
            g.push('base64');
        this.lead = 'data:' + g.join(';') + ',' + DataEnc._BOM[b];
        return this;
    },
    // Deliver
    pay : function() {
        return this.lead + this.buf;
    },
    // UTF-16BE
    utf16be : function(t) { // U+0500 => %05%00
        var i, c, buf = [];
        for (i = 0; i < t.length; ++i) {
            if ((c = t.charCodeAt(i)) > 0xff) {
                buf.push(('00' + (c >> 0x08).toString(16)).substr(-2));
                buf.push(('00' + (c  & 0xff).toString(16)).substr(-2));
            } else {
                buf.push('00');
                buf.push(('00' + (c  & 0xff).toString(16)).substr(-2));
            }
        }
        this.buf += '%' + buf.join('%');
        // Note the hex array is returned, not string with '%'
        // Might be useful if one want to loop over the data.
        return buf;
    },
    // UTF-16LE
    utf16le : function(t) { // U+0500 => %00%05
        var i, c, buf = [];
        for (i = 0; i < t.length; ++i) {
            if ((c = t.charCodeAt(i)) > 0xff) {
                buf.push(('00' + (c  & 0xff).toString(16)).substr(-2));
                buf.push(('00' + (c >> 0x08).toString(16)).substr(-2));
            } else {
                buf.push(('00' + (c  & 0xff).toString(16)).substr(-2));
                buf.push('00');
            }
        }
        this.buf += '%' + buf.join('%');
        // Note the hex array is returned, not string with '%'
        // Might be useful if one want to loop over the data.
        return buf;
    },
    // UTF-8
    utf8 : function(t) {
        this.buf += encodeURIComponent(t);
        return this;
    },
    // Direct copy
    copy : function(t) {
        this.buf += t;
        return this;
    }
};

Предыдущий ответ:


У меня нет настроек для репликации, но если ваш случай совпадает с @jlarson, то полученный файл должен быть правильным.

Этот ответ стал несколько длинным (интересная тема, которую вы говорите?), но обсудите различные аспекты вокруг вопроса, что (вероятно) происходит, и как на самом деле проверить, что происходит по-разному.

TL; DR:

Текст, скорее всего, импортируется как ISO-8859-1, Windows-1252 и т.п., а не как UTF-8. Принудительное приложение для чтения файла как UTF-8 с помощью импорта или других средств.


PS: UniSearcher - это хороший инструмент, доступный в этом путешествии.

Длинный путь

"Самый простой" способ быть на 100% уверенным, что мы смотрим, - это использовать hex-редактор для результата. В качестве альтернативы используйте hexdump, xxd или тому подобное из командной строки для просмотра файла. В этом случае последовательность байтов должна быть последовательностью байтов UTF-8, поставляемой из script.

В качестве примера, если мы берем script jlarson, он принимает массив data:

data = ['name', 'city', 'state'],
       ['\u0500\u05E1\u0E01\u1054', 'seattle', 'washington']

Этот объединяется в строку:

 name,city,state<newline>
 \u0500\u05E1\u0E01\u1054,seattle,washington<newline>

который преобразует Unicode в:

 name,city,state<newline>
 Ԁסกၔ,seattle,washington<newline>

Поскольку UTF-8 использует ASCII в качестве базы (байты с самым высоким битом не установлены, те же, что и в ASCII), единственная специальная последовательность в тестовых данных - "Ԁ ס ก ၔ", которая, в свою очередь, равна:

Code-point  Glyph      UTF-8
----------------------------
    U+0500    Ԁ        d4 80
    U+05E1    ס        d7 a1
    U+0E01    ก     e0 b8 81
    U+1054    ၔ     e1 81 94

Глядя на шестнадцатеричный дамп загруженного файла:

0000000: 6e61 6d65 2c63 6974 792c 7374 6174 650a  name,city,state.
0000010: d480 d7a1 e0b8 81e1 8194 2c73 6561 7474  ..........,seatt
0000020: 6c65 2c77 6173 6869 6e67 746f 6e0a       le,washington.

Во второй строке мы найдем d480 d7a1 e0b8 81e1 8194, которые совпадают с приведенным выше:

0000010: d480  d7a1  e0b8 81  e1 8194 2c73 6561 7474  ..........,seatt
         |   | |   | |     |  |     |  | |  | |  | |
         +-+-+ +-+-+ +--+--+  +--+--+  | |  | |  | |
           |     |      |        |     | |  | |  | |
           Ԁ     ס      ก        ၔ     , s  e a  t t

Ни один из других символов не искажен.

Выполняйте аналогичные тесты, если хотите. Результат должен быть аналогичным.


По предоставленному образцу â€", â€, “

Мы также можем взглянуть на образец, предоставленный в вопросе. Вероятно, предполагается, что текст представлен в Excel/TextEdit по кодовой странице 1252.

Процитировать Wikipedia на Windows-1252:

Windows-1252 или CP-1252 - это кодировка символов латинского алфавита, используемая по умолчанию в устаревших компонентах Microsoft Windows на английском и некоторых других Западные языки. Это одна из версий в группе кодовых страниц Windows. В пакетах LaTeX он упоминается как "ansinew".

Получение исходных байтов

Чтобы перевести его обратно в оригинальную форму, мы можем посмотреть макет кодовой страницы, из которого получим:

Character:   <â>  <€>  <">  <,>  < >  <â>  <€>  < >  <,>  < >  <â>  <€>  <œ>
U.Hex    :    e2 20ac 201d   2c   20   e2 20ac   9d   2c   20   e2 20ac  153
T.Hex    :    e2   80   94   2c   20   e2   80   9d*  2c   20   e2   80   9c
  • U не подходит для Unicode
  • T не подходит для Translated

Например:

â => Unicode 0xe2   => CP-1252 0xe2
" => Unicode 0x201d => CP-1252 0x94
€ => Unicode 0x20ac => CP-1252 0x80

Специальные случаи, такие как 9d, не имеют соответствующей кодовой точки в CP-1252, они просто копируются напрямую.

Примечание. Если вы посмотрите на искаженную строку, скопировав текст в файл и сделав шестнадцатеричный дамп, сохраните файл, например, кодировку UTF-16, чтобы получить значения Unicode, представленные в таблице. Например. в Vim:

set fenc=utf-16
# Or
set fenc=ucs-2

Байты для UTF-8

Затем мы объединяем результат, строку T.Hex, в UTF-8. В последовательностях UTF-8 байты представлены ведущим байтом, сообщающим нам, сколько последующих байтов делает глиф. Например, если у байта есть двоичное значение 110x xxxx, мы знаем, что этот байт и следующий представляют одну кодовую точку. Всего два. 1110 xxxx говорит нам, что это три и так далее. Значения ASCII не имеют большого битового набора, поэтому любое сопоставление байтов 0xxx xxxx является автономным. Всего один байт.

0xe2 = 1110 0010bin => 3 bytes => 0xe28094 (em-dash)  —
0x2c = 0010 1100bin => 1 byte  => 0x2c     (comma)    ,
0x2c = 0010 0000bin => 1 byte  => 0x20     (space)   
0xe2 = 1110 0010bin => 3 bytes => 0xe2809d (right-dq) "
0x2c = 0010 1100bin => 1 byte  => 0x2c     (comma)    ,
0x2c = 0010 0000bin => 1 byte  => 0x20     (space)   
0xe2 = 1110 0010bin => 3 bytes => 0xe2809c (left-dq)  "

Вывод; Оригинальная строка UTF-8:

—, ", "

Переплетение обратно

Мы также можем сделать обратное. Исходная строка в виде байтов:

UTF-8: e2 80 94 2c 20 e2 80 9d 2c 20 e2 80 9c

Соответствующие значения в cp-1252:

e2 => â
80 => €
94 => "
2c => ,
20 => <space>
...

и т.д., результат:

â€", â€, “

Импорт в MS Excel

Другими словами: проблема заключается в том, как импортировать текстовые файлы UTF-8 в MS Excel и некоторые другие приложения. В Excel это можно сделать по-разному.

  • Метод один:

Не сохраняйте файл с расширением, распознанным приложением, например .csv или .txt, но опустите его полностью или сделайте что-нибудь.

В качестве примера сохраните файл как "testfile", без расширения. Затем в Excel откройте файл, подтвердите, что мы действительно хотим открыть этот файл, а voilà мы получаем с опцией кодирования. Выберите UTF-8, и файл должен быть правильно прочитан.

  • Метод второй:

Используйте данные импорта вместо открытого файла. Что-то вроде:

Data -> Import External Data -> Import Data

Выберите кодировку и продолжите.

Убедитесь, что Excel и выбранный шрифт действительно поддерживают глиф

Мы также можем протестировать поддержку шрифтов для символов Юникода, используя, иногда, более дружественный буфер обмена. Например, скопируйте текст с этой страницы в Excel:

Если существует поддержка кодовых точек, текст должен выглядеть отлично.


Linux

В Linux, который является, прежде всего, UTF-8 в userland, это не должно быть проблемой. Используя Libre Office Calc, Vim и т.д., Отобразите файлы правильно.


Почему он работает (или должен)

encodeURI из состояний spec (также читайте sec-15.1.3):

Функция encodeURI вычисляет новую версию URI, в которой каждый экземпляр определенных символов заменяется одной, двумя, тремя или четырьмя управляющими последовательностями, представляющими кодировку UTF-8 символа.

Мы можем просто протестировать это в нашей консоли, например:

>> encodeURI('Ԁסกၔ,seattle,washington')
<< "%D4%80%D7%A1%E0%B8%81%E1%81%94,seattle,washington"

Когда мы регистрируем escape-последовательности, равные единицам в шестнадцатеричном дампе выше:

%D4%80%D7%A1%E0%B8%81%E1%81%94 (encodeURI in log)
 d4 80 d7 a1 e0 b8 81 e1 81 94 (hex-dump of file)

или, проверяя 4-байтовый код:

>> encodeURI('󱀁')
<< "%F3%B1%80%81"

Если это не соответствует

Если ничего из этого не применимо, это может помочь, если вы добавили

  • Пример ожидаемого ввода и искаженного вывода (копирование).
  • Пример hex-дампа исходных данных и файла результата.

Ответ 2

Я столкнулся именно с этим вчера. Я разрабатывал кнопку, которая экспортирует содержимое таблицы HTML в качестве загрузки CSV. Функциональность самой кнопки почти идентична вашей - при щелчке я читаю текст из таблицы и создаю URI данных с содержимым CSV.

Когда я попытался открыть полученный файл в Excel, стало ясно, что символ "£" читается неправильно. Представление 2 байта UTF-8 обрабатывалось как ASCII, что приводило к нежелательному символу мусора. Некоторый Googling указал, что это была известная проблема с Excel.

Я попытался добавить отметку порядка байтов в начале строки - Excel просто интерпретировал ее как данные ASCII. Затем я попробовал разные вещи, чтобы преобразовать строку UTF-8 в ASCII (например, csvData.replace('\u00a3', '\xa3')), но я обнаружил, что в любое время, когда данные будут принудительно введены в строку JavaScript, он снова станет UTF-8. Хитрость состоит в том, чтобы преобразовать его в двоичный, а затем Base64 закодировать его, не преобразовывая обратно в строку на этом пути.

У меня уже было CryptoJS в моем приложении (используется для аутентификации HMAC против REST API), и я смог использовать его для создания ASCII закодированную последовательность байтов из исходной строки, тогда Base64 закодирует ее и создаст URI данных. Это сработало, и полученный файл при открытии в Excel не отображает никаких нежелательных символов.

Существенным битом кода, который выполняет преобразование, является:

var csvHeader = 'data:text/csv;charset=iso-8859-1;base64,'
var encodedCsv =  CryptoJS.enc.Latin1.parse(csvData).toString(CryptoJS.enc.Base64)
var dataURI = csvHeader + encodedCsv

Где csvData - ваша строка CSV.

Вероятно, есть способы сделать то же самое без CryptoJS, если вы не хотите вносить эту библиотеку, но это, по крайней мере, показывает, что это возможно.

Ответ 3

Excel любит Unicode в кодировке UTF-16 LE с спецификацией. Выведите правильную спецификацию (FF FE), затем преобразуйте все ваши данные из UTF-8 в UTF-16 LE.

Windows использует UTF-16 LE внутренне, поэтому некоторые приложения работают лучше с UTF-16, чем с UTF-8.

Я не пытался это делать в JS, но в Интернете есть различные сценарии для преобразования UTF-8 в UTF-16. Преобразование между вариантами UTF довольно легко и занимает всего дюжину строк.

Ответ 4

У меня была аналогичная проблема с данными, которые были втянуты в Javascript из списка Sharepoint. Это оказалось чем-то вроде "Zero Width Space" , и оно отображалось как "когда был переведен в Excel. По-видимому, Sharepoint вставляет их иногда, когда пользователь нажимает" backspace".

Я заменил их этим quickfix:

var mystring = myString.replace(/\u200B/g,'');

Похоже, у вас могут быть другие скрытые персонажи. Я нашел код для символа нулевой ширины в шахте, посмотрев строку вывода в инспекторе Chrome. Инспектор не смог отобразить персонажа, чтобы он заменил его красной точкой. Когда вы наводите указатель мыши на эту красную точку, она дает вам код (например,\u200B), и вы можете просто добавить в различные коды к невидимым символам и удалить их таким образом.

Ответ 5

Это может быть проблемой в вашей серверной кодировке.

Вы можете попробовать (при условии, что locale english US), если вы используете Linux:

sudo locale-gen en_US en_US.UTF-8
dpkg-reconfigure locales

Ответ 6

button.href = 'data:' + mimeType + ';charset=UTF-8,%ef%bb%bf' + encodedUri;

это должно сделать трюк