Декодирование Base64 в R: полное руководство
В вашу сессию R приземляется строка. Она похожа на перетасованный алфавитный суп: буквы, цифры, изредка плюс или слеш, да пара знаков равно, привешенных к хвосту. Тот, кто её прислал, клянётся, что раньше это была совершенно обычная фраза, JPEG или JSON-документ. Эта строка - Base64, и эта страница - рецепт, как превратить её обратно в то, чем она была.
Короткое повторение, потому что домашняя страница этого сайта объясняет формат во всех деталях: Base64 записывает три входных байта четырьмя символами, взятыми из 64-символьного алфавита, и один-два завершающих символа = помечают место, где закончились настоящие данные. Обмен четыре-за-три - вот почему закодированный текст примерно на треть пухлее оригинала, а декодирование просто проворачивает этот обмен в обратную сторону. Удержим в голове форму проблемы - и откроем-ка наши конверты.
А вот неожиданный поворот, из-за которого R немного отличается от многих других языков: в базовом R нет Base64 вообще. В базовых пакетах не притаился ни base64_decode(), ни встроенная функция в одну строку, за которую можно было бы схватиться. Придётся приводить с собой пакет. Хорошая новость: экосистема предлагает несколько вариантов, у каждого свой характер, и к концу этой статьи вы точно будете знать, за какой тянуться - и к какому относиться с недоверием.
Декодеры одним взглядом
Тяжёлую работу делают пять пакетов, и они делятся на два лагеря: снисходительные, которые пожимают плечами на грязный ввод, и строгие, которые относятся к RFC как к договору. Вот состав, актуальный на 2026 год:
| Пакет | Версия (2026) | Точки входа для декодирования | Характер |
|---|---|---|---|
base64enc |
0.1-6 | base64decode() |
Снисходительный по умолчанию, в феврале 2026 обрёл режим strict |
openssl |
2.4.2 | base64_decode() |
Пропускает пробельные символы, но молча проваливается так, что заслуживает строгого взгляда |
b64 |
0.1.7 | decode(), decode_as_string() |
Строгий, векторизованный, написан на Rust, быстрый |
base64 |
2.0.2 | decode() |
Удобная обёртка из файла в файл вокруг openssl |
base64url |
1.4 | base64_urldecode() |
URL-безопасный алфавит, без заполнителя, тихий и прощающий |
Трое дублёров тоже заслуживают упоминания. Пакет jsonlite экспортирует собственные помощники, base64_enc, base64_dec и URL-безопасную пару, так что если вы уже разбираете JSON, декодер у вас, возможно, уже под рукой. Пакет jose везёт base64url_decode() для работы с JWT. А древний пакет RCurl до сих пор носит функцию base64(), которая оборачивает libcurl: она работает, она ориентирована на символы, и на неё скорее похоже на верного деда, чем на инструмент для нового кода.
Устанавливаем состав
Если R ещё нет на машине, его даёт ваша операционная система: r-base на Debian и Ubuntu, R на Fedora, пакет или установщик на macOS и Windows. Затем пакеты, прямо с CRAN:
install.packages("base64enc")
install.packages("openssl")
install.packages("b64")
install.packages("base64url")
Две ремарки по сборке, потому что именно на них установки уходят в сторону. Пакет openssl компилируется против вашего системного OpenSSL, так что голому Linux-коробке сначала могут понадобиться заголовочные файлы разработки:
sudo apt install libssl-dev
Пакет b64 - это Rust-движок, обёрнутый через extendr, так что сборка из исходников хочет Rust-инструментальную цепочку (sudo apt install cargo подтянет заодно и rustc). На Windows и macOS вы получаете готовые бинарники с CRAN, и всё это вас не касается. Если предпочитаете менеджеры пакетов, pak::pkg("base64enc") или remotes::install_cran("b64") сделают ту же работу и с меньшим количеством мнений о репозиториях.
Ваше первое декодирование: трёхстрочный ритуал
Девяносто процентов жизни декодирования умещаются в три строки. Вот канонический дымовой тест на знаменитой строке TWFu:
library(base64enc)
packed <- "TWFu"
bytes <- base64decode(packed)
bytes
#> [1] 4d 61 6e
rawToChar(bytes)
#> [1] "Man"
В этом крошечном обряде стоит обратить внимание на три вещи. Во-первых, base64decode() всегда выдаёт вам вектор типа raw, никогда не строку. Это фича, а не случайность: Base64 может нести фразу, JPEG или сертификат, и ни один из них до знакомства с содержимым не должен обрабатываться иначе. Во-вторых, путь от байтов обратно к тексту - отдельный, намеренный шаг через rawToChar(), и именно на этом шаге живёт решение про кодировку (об этом позже). В-третьих, TWFu декодируется в слово «Man»: три буквы, ноль драмы. Держите эту строку в запасном кармане. Если кусок кода декодирования превращает TWFu в Man, машина честная.
А поскольку вы когда-нибудь обязательно будете декодировать то, что закодировали сами, вот полный круговой рейс, доказывающий, что оба направления сходятся:
text <- "Hello, world!"
packed <- base64encode(charToRaw(text))
packed
#> [1] "SGVsbG8sIHdvcmxkIQ=="
identical(text, rawToChar(base64decode(packed)))
#> [1] TRUE
Граница вектора raw
R пересекает границу байты/текст двумя крошечными функциями, и когда вы их узнаете, остальная часть этой статьи покажется очевидной. charToRaw() превращает строку в её байты, rawToChar() делает обратное, а между ними - весь инструментарий raw: length() чтобы сосчитать байты, head() чтобы подсмотреть, writeBin() и readBin() чтобы носить их к файлам и из файлов. Каждый декодер в этой статье намеренно останавливается на той границе.
bytes <- base64decode("SGVsbG8s")
length(bytes)
#> [1] 6
head(bytes)
#> [1] 48 65 6c 6c 6f 2c
rawToChar(bytes)
#> [1] "Hello,"
Поэтому, когда вы видите результат вида [1] 48, читайте его как «один байт, значение 72, буква H» и остановитесь там, пока не узнаете, в какой кодировке одеты эти байты. Эта пауза - и есть весь дисциплинированный навык декодирования.
Грязный ввод: на чём декодеры не сходятся
Этот раздел спасёт вас в два часа ночи, потому что декодеры не сходятся во мнениях о том, что происходит, когда ввод немного грязный. Боевой Base64 приходит с пробелами внутри, с заполнителем, отброшенным нервным копипастом, с чужим символом, съеденным буфером обмена, или с мусором, привалявшимся хвостом. Вот как на одну и ту же семью провинившихся отвечает каждый декодер:
| Ввод | base64enc (по умолчанию) | base64enc (strict = TRUE) | openssl | b64 |
|---|---|---|---|---|
"SGVs bG8s IHdvcmxkIQ==" (пробел внутри) |
«Hello, world!» (пробел пропущен) | ошибка: невалидный символ, позиция указана | «Hello, world!» (пробел пропущен) | ошибка |
"SGVsbG8sIHdvcmxkIQ" (заполнитель отброшен) |
«Hello, world!» (отсутствующий заполнитель прощён) | ошибка: нет заполнителя | ошибка: не удалось декодировать | ошибка: невалидный заполнитель |
"SGVsbG8s!IHdvcmxkIQ==" (символ «!» внутри) |
«Hello, world!» (плохой символ пропущен) | ошибка: невалидный символ | пустой вектор raw, без ошибки | ошибка |
"SGVsbG8sIHdvcmxkIQ==xx" (хвостовой мусор) |
«Hello, world!» (мусор игнорирован) | ошибка: хвостовое содержимое | пустой вектор raw, без ошибки | ошибка |
"TQ==" (чистый) |
M | M | M | M |
Прочтите эту таблицу дважды, потому что в ней вся история. base64decode() по умолчанию - это доброжелательный таможенник: он пропускает символы вне алфавита, мирится с отсутствующим заполнителем и игнорирует хвостовое содержимое, так что строки почтового и буферного облика просто проходят. Режим strict = TRUE, который пришёл с релизом 0.1-5 в феврале 2026 после долгого молчания, - это судебный эксперт: он проверяет всю строку, называет точную позицию провинившегося и отказывается принимать всё, что не по учебнику. b64 по умолчанию строгий и никогда не добивается наполовину. А openssl сидит в неловкой середине: он охотно пропускает пробельные символы и честно ошибается на отсутствующем заполнителе, но когда сталкивается с по-настоящему незаконным символом или хвостовым мусором, возвращает пустой вектор raw и совершенно ничего не говорит. Этот тихий пустой результат - самое опасное поведение во всей этой статье, так что посмотрим, как это происходит:
dirty <- "SGVsbG8s!IHdvcmxkIQ=="
openssl::base64_decode(dirty)
#> raw(0) # пусто. ни ошибки. ни предупреждения. ни намёка.
Если вы пишете код против openssl, проверяйте длину полученного, прежде чем ему доверять. Это маленькая привычка, которая спасает от целого класса загадок «куда делись мои данные».
Для строгого лагеря - вот base64enc в точности, с теми самыми текстами ошибок, которые вы увидите в собственной консоли:
base64enc::base64decode("TWF u", strict = TRUE)
#> v=30000, pad=0, org='u'
#> Error: Invalid character (' ') at position 4 in base64 string (not allowed in strict mode)
base64enc::base64decode("TWF", strict = TRUE)
#> v=10000, pad=3, org=''
#> Error: Missing padding (1 characters) at the end of the base64 string (not allowed in strict mode)
base64enc::base64decode("TWF=xx", strict = TRUE)
#> v=20000, pad=0, org='xx'
#> Error: Trailing content 'xx' after padding at position 4 in base64 string (not allowed in strict mode)
Одна странность, которой стоит ждать: каждый строгий вызов, успешный или нет, бормочет в консоль C-уровневую строку состояния, вроде v=30000, pad=0, org='u'. Это прямой вывод из скомпилированного кода, а не предупреждение R, так что suppressWarnings() его не тронет. Это безвредно, но если ваши тесты сравнивают вывод консоли, теперь вы знаете, почему лишний рядок есть.
Особенности R: NA, векторы и тихая конкатенация
У Base64 есть свои причуды; R добавляет собственные. Первая кусается через NA. Когда вы передаёте NA_character_ декодеру, R тихо приводит его к строке "NA", прежде чем декодер что-либо увидит, а "NA" - совершенно валидная группа Base64. Результат - не ошибка и не пустой вектор. Это байт 0x34, буква "4":
base64decode(NA_character_)
#> [1] 34
rawToChar(base64decode(NA_character_))
#> [1] "4"
Так что колонка отсутствующих значений не декодируется в отсутствующие значения. Она декодируется в колонку букв 4, и ваш дальнейший код весело её обрабатывает. Если ваш ввод может содержать NA, отфильтруйте сначала:
values <- c("TWFu", NA_character_, "TQ==")
clean <- values[!is.na(values)]
out <- vapply(clean, function(x) rawToChar(base64decode(x)), character(1))
unname(out)
#> [1] "Man" "M"
Вторая причуда - векторный ввод, и декодеры делают совершенно разные повороты. base64decode() трактует символьный вектор как несколько кусков одной строки и склеивает их, так что два элемента возвращаются одним вектором raw. openssl::base64_decode() даже не доходит до этого: он схлопывает свой аргумент, а потом налетает на логическую проверку с двумя элементами, которая рождает одно из самых честных сообщений об ошибке в R. А b64::decode() - единственный по-настоящему векторизованный: по одному декодированному блоку на каждый вход:
base64decode(c("TWFu", "TQ=="))
#> [1] 4d 61 6e 4d
openssl::base64_decode(c("TWFu", "TQ=="))
#> Error in if (is.na(text)) : the condition has length > 1
out <- b64::decode(c("TWFu", "TQ=="))
rawToChar(out[[1]])
#> [1] "Man"
rawToChar(out[[2]])
#> [1] "M"
Вывод: крутите циклы или векторизуйте осознанно, и никогда не предполагайте, что вектор входов даёт вам вектор выходов.
URL-безопасный алфавит
Стандартный Base64 тратит последние две ячейки своего алфавита на + и /, и это ровно те символы, которых URL не любят. Плюс превращается в %2B, слеш - в %2F, а заполнитель - в %3D, так что токен, который должен вставляться куда угодно, начинает носить процентные знаки. URL-безопасный вариант, определённый в разделе 5 RFC 4648, заменяет эти два символа на - и _ и обычно отбрасывает и заполнитель. У R в этот мир ведут три двери.
Дверь первая - движки b64. Движок - это просто настроенный алфавит и политика заполнителя, и пакет везёт четыре нужных:
library(b64)
std <- encode(as.raw(c(0xfb, 0xef, 0xbe)))
std
#> [1] "++++"
url <- encode(as.raw(c(0xfb, 0xef, 0xbe)), engine("url_safe"))
url
#> [1] "----"
decoded <- decode(url, engine("url_safe"))[[1]]
toString(decoded)
#> [1] "fb, ef, be"
Движки - это "standard" (по умолчанию), "standard_no_pad", "url_safe" и "url_safe_no_pad", и один и тот же объект движка работает в обоих направлениях, что сохраняет ваш код симметричным. Они ещё и строгие к своему алфавиту: подайте стандартному движку "----", и он ответит "Invalid byte 45, offset 0.", что приятно.
Дверь вторая - отдельный пакет base64url, маленький и одноцелевой: URL-безопасный алфавит, без заполнителя, всегда. Одно предостережение: его декодер снисходительный, так что он охотно декодирует валидный префикс испорченной строки и останавливается там, не жалуясь. Если URL-безопасное значение пришло из недоверенного источника, предпочтите b64.
base64url::base64_urlencode("hello world")
#> [1] "aGVsbG8gd29ybGQ"
base64url::base64_urldecode("aGVsbG8g!!!")
#> [1] "hello " # остаток был тихо отброшен
Дверь третья - jose, который экспортирует base64url_encode() и base64url_decode() для работы с JWT и возвращает векторы raw, как и положено.
А если вам нужен лишь разовый случай и вы хотите остаться внутри base64enc, достаточно операции на строках и починки заполнителя:
url <- "----"
fixed <- gsub("_", "/", gsub("-", "+", url), fixed = TRUE)
padded <- switch(as.character(nchar(fixed) %% 4L),
"0" = fixed,
"2" = paste0(fixed, "=="),
"3" = paste0(fixed, "="))
rawToChar(base64decode(padded))
#> [1] "\xfb\xef\xbe"
JWT: подглядывать и доверять
Почему большинство людей встречаются с URL-безопасным Base64 - JSON Web Token. JWT - это три base64url-части, скреплённые точками: заголовок, описывающий, как он был подписан, пелод с претензиями и подпись, которая делает всё это заслуживающим доверия. Декодирование двух первых частей в R - дело пяти строк. Обратите внимание на fixed = TRUE в strsplit(): точка - это regex-подстановочный символ, и без этого флага R с удовольствием разрежет ваш токен на одиночные символы, а это самый частый Base64-баг в R-коде, который можно найти в дикой природе.
jwt <- jose::jwt_encode_hmac(jose::jwt_claim(sub = "1234567890", iat = 1516239022, name = "John Doe"), "0123456789abcdef")
parts <- strsplit(jwt, ".", fixed = TRUE)[[1]]
header <- base64url::base64_urldecode(parts[1])
header
#> [1] "{\"typ\":\"JWT\",\"alg\":\"HS256\"}"
payload <- jsonlite::fromJSON(base64url::base64_urldecode(parts[2]))
payload$name
#> [1] "John Doe"
Две честные оговорки. Первая: декодировать JWT - значит подглядывать, а не доверять: третья часть - это подпись, и она что-то значит только при проверке по ключу издателя. Для этого пакет jose берёт на себя всю семью. Его выпуск 2.0 (апрель 2026) добавляет поддержку ED25519 и делает заголовок typ необязательным; старые уроки, показывающие помощники jwk_read()/jwk_write() из 1.x (или их псевдонимы read_jwk()/write_jwk()), остаются в силе, потому что 2.0 по-прежнему везёт те же помощники для чтения/записи JWK:
library(jose)
claims <- jwt_decode_hmac(jwt, "0123456789abcdef")
claims$name
#> [1] "John Doe"
claims$sub
#> [1] "1234567890"
sp <- jwt_split(jwt)
sp$header
#> $alg
#> [1] "HS256"
#>
#> $typ
#> [1] "JWT"
sp$payload$name
#> [1] "John Doe"
jwt_decode_hmac() проверяет подпись и дополнительно применяет претензии exp и nbf, поднимая ошибку, если токен просрочен. И второе: base64url::base64_urldecode() и его друзья без проблем декодируют заголовок и пелод, но часть с подписью - бинарная, так что декодируйте её функцией, которая возвращает raw (например b64::decode() с движком "url_safe_no_pad"), а не в строку.
Кодировки: какой текст в этих байтах?
Каждый декодер в этой статье намеренно останавливается на границе вектора raw, потому что ответ на вопрос «какой это был текст?» зависит от кодировки, в которую упаковали байты. Хорошая новость заранее: если отправитель использовал UTF-8, а это большая часть современного веба, ваша жизнь коротка и счастлива. В base64enc даже есть фильтр для этого:
packed <- base64encode(charToRaw("caf\u00e9"))
bytes <- base64decode(packed)
checkUTF8(bytes, quiet = TRUE)
#> [1] TRUE
rawToChar(bytes)
#> [1] "café"
checkUTF8() спрашивает «могут ли эти байты читаться как UTF-8?», ни к чему не обязывая. С quiet = TRUE он возвращает TRUE или FALSE; по умолчанию он ещё более прямой и поднимает ошибку на невалидных байтах, называя провинившегося, как в INVALID byte 0xff at 0x0 (0, line 1): invalid start byte (FE/FF). Он также отчитывается FALSE для строк, содержащих NUL-байты, которые никогда не могут быть частью валидной R-строки UTF-8. Используйте его как фильтр, и остальное последует само.
Вот почему фильтр важен. В UTF-8-локали rawToChar() не бросает ошибку, когда байты не являются валидным UTF-8. Он выдаёт вам строку, которую R помечает как Encoding() == "unknown", и в зависимости от локали и сборки R тот же вызов вместо этого может умереть с ошибкой «invalid multibyte sequence», что хотя бы честно. В любом случае, rawToChar() без страховки на чужих байтах - так рождаются кракозябры:
broken <- as.raw(c(0xc3, 0x28)) # обрезанная пара UTF-8
checkUTF8(broken, quiet = TRUE)
#> [1] FALSE
rawToChar(broken) # ошибки нет. а это и есть проблема.
#> [1] "\xc3("
А когда отправитель использовал что-то совсем другое, Latin-1 или Windows-1252 или Shift-JIS, рецепт такой: декодируйте до raw, а потом переводите через iconv(), который понимает именованные кодировки:
latin1_bytes <- as.raw(c(0x63, 0x61, 0x66, 0xe9)) # «café» в Latin-1
iconv(rawToChar(latin1_bytes), from = "latin1", to = "UTF-8")
#> [1] "café"
Эмодзи заслуживают отдельного слова, потому что у R здесь есть своя история. Современный R хранит кодовые точки выше U+FFFF, где живут эмодзи, как настоящий UTF-8, так что круговой рейс работает, и количество байтов совпадает с тем, чего ожидают все остальные языки. Старые выпуски R хранили те же символы суррогатными парами, схемой, которую часто называют CESU-8, так что один эмодзи пересекал границу шестью байтами невалидного UTF-8. Если вы наследуете старый R-код, где эмодзи приходят по проводам сломанными, главный подозреваемый именно этот: проверьте nchar(x, type = "bytes") и сравните с тем, что произвёл бы язык отправителя.
emoji <- "\U0001F600"
nchar(emoji, type = "bytes")
#> [1] 4
round_trip <- base64decode(base64encode(charToRaw(emoji)))
identical(emoji, rawToChar(round_trip))
#> [1] TRUE
Правило большого пальца: предполагайте UTF-8, проверяйте через checkUTF8(), и тянитесь за iconv() только когда у вас есть точное знание о другой кодировке. Никогда не гадайте.
Файлы: от перенесённого текста к настоящим байтам
Строки - это легко; файлы - вот где Base64 окупается. Начните с ручного конвейера, который работает с любым пакетом: прочитайте закодированный текст, декодируйте, запишите байты.
cat("SGVsbG8sIHdvcmxkIQ==", file = "hello.b64")
bytes <- base64decode(paste(readLines("hello.b64"), collapse = ""))
writeBin(bytes, "hello.txt")
readLines("hello.txt")
#> [1] "Hello, world!"
Затем слой удобства. base64enc умеет читать и писать за вас: аргумент file указывает на файл с Base64-текстом, а output - на место, где должны оказаться декодированные байты. Возвращаемое значение - число записанных байтов, прекрасная бесплатная проверка здравомыслия:
n <- base64decode(file = "hello.b64", output = "hello.txt")
n
#> [1] 13
readLines("hello.txt")
#> [1] "Hello, world!"
Перенесённый на строки ввод, тот, что пережил почту или PEM-броню, - не проблема для снисходительных декодеров: они заглядывают прямо через переносы строк, где бы те ни падали, пока оставшееся является валидным Base64. MIME переносит на 76 символов с CRLF между строками; PEM-блоки - на 64. Строгий движок, конечно, отвергает переносы сразу.
wrapped <- paste0("VGhlIHF1aWNrIGJ", "\r\n",
"yb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==")
rawToChar(base64decode(wrapped))
#> [1] "The quick brown fox jumps over the lazy dog"
rawToChar(openssl::base64_decode(wrapped))
#> [1] "The quick brown fox jumps over the lazy dog"
b64::decode(wrapped)
#> Error: Invalid byte 13, offset 15.
Пакет b64 имеет файловую пару с самыми понятными именами, и поскольку он быстрый как Rust, это он, за кого тянуться, когда файл большой. Но есть одно острое ребро: decode_file() читает файл байт за байтом и не прощает ничего. Завершающий перенос строки - тот, что добавляет writeLines(), но которого никогда не делает cat() - отправляет Rust-движок в панику, проявляющуюся как перехватываемая ошибка вида User function panicked: decode_file_. Записывайте закодированный текст через cat() или writeBin(charToRaw(enc), path) - и ребро останется тупым.
writeBin(charToRaw("file payload bytes"), "payload.bin")
enc <- b64::encode_file("payload.bin")
cat(enc, file = "payload.b64")
bytes <- b64::decode_file("payload.b64")
rawToChar(bytes)
#> [1] "file payload bytes"
Пакет base64 - третий путь внутрь: чисто файловый, с парой соответствующих функций:
base64::encode("payload.bin", "payload.b64")
base64::decode("payload.b64", "payload.out")
readLines("payload.out")
#> [1] "file payload bytes"
Ещё одна файловая форма, с которой вы встретитесь на практике: колонка data frame, полная закодированных блоков, очень частая в дампах API. Для нескольких сотен строк простого векторизованного вызова достаточно:
df <- data.frame(content = c(b64::encode("alpha"), b64::encode("beta"),
b64::encode("gamma")))
decoded <- b64::decode(df$content)
df$decoded <- vapply(decoded, function(x) rawToChar(x), character(1))
df$decoded
#> [1] "alpha" "beta" "gamma"
API и веб-ответы
JSON-API любят запихивать бинарные данные в текст, и Base64 - их любимый чемодан. Паттерн в R один и тот же каждый раз: запросить, разобрать, декодировать и решить, что это за байты. Современный HTTP-клиент - httr2, а JSON-сторона - jsonlite:
library(httr2)
library(jsonlite)
res <- req_perform(request("https://httpbin.org/get?attachment=TWFuIGlzIGhlcmU%3D&name=man.txt"))
data <- fromJSON(rawToChar(res$body))
bytes <- base64decode(data$args$attachment)
writeBin(bytes, data$args$name)
length(bytes)
#> [1] 11
Три заметки в дорогу. Конструктор запросов - request(), и у него это имя с первого выпуска httr2. Тело ответа приходит вектором raw, так что rawToChar() до того, как разбирать его как JSON. И API может говорить на диалекте: проверьте, URL-безопасный ли его Base64 или у него отрезан заполнитель, прежде чем скармливать его декодеру, который ждёт учебный вариант. Некоторые API даже дважды кодируют, Base64 поверх Base64, и вы узнаете это, когда одно декодирование даст вам ещё один Base64 вместо ожидаемых байтов.
URI data: и самодостаточные документы
Схема URI data: (RFC 2397) позволяет документу нести своё собственное содержимое: MIME-тип, слово «base64», если нагрузка закодирована, и саму нагрузку. Вы будете постоянно встречать их в HTML, который скрапите, и декодирование одного - это строковая операция в два шага, за которой следует обычное декодирование:
img_tag <- "<img src=\"data:image/png;base64,iVBORw0KGgoAAA\" />"
uri <- regmatches(img_tag, regexec("src=\"([^\"]*)\"", img_tag))[[1]][2]
uri
#> [1] "data:image/png;base64,iVBORw0KGgoAAA"
payload <- sub("^data:[^,]*,", "", uri)
bytes <- base64decode(payload)
length(bytes)
#> [1] 10
Та же самая форма встречается в CSS, в аннотациях PDF и в самодостаточных отчётах R Markdown, где график сплющен в тег <img>, чтобы HTML ехал без файлов-спутников. Если вы рендерите такие документы и хотите вытащить встроенные изображения, вот весь трюк: найдите строку data:, отрежьте по первой запятой, декодируйте.
Базы данных, почта и конфигурация
Base64 возникает в базах данных, когда кому-то нужно бинарное внутри текстовой колонки, и сторона декодирования - это колонка строк обратно в байты. Вот круговой рейс против SQLite через DBI и RSQLite: сохраните закодированное значение, запросите обратно, декодируйте - и у вас есть ваши байты:
library(DBI)
library(RSQLite)
db <- dbConnect(SQLite(), ":memory:")
dbExecute(db, "CREATE TABLE files (name TEXT, payload TEXT)")
stored <- base64encode(charToRaw("stored in a database"))
dbExecute(db, paste0("INSERT INTO files VALUES ('note.txt', '", stored, "')"))
row <- dbGetQuery(db, "SELECT * FROM files")
rawToChar(base64decode(row$payload))
#> [1] "stored in a database"
SQLite также умеет хранить бинарные данные нативно как BLOB, тогда Base64 вообще не нужен и колонка возвращается в R вектором raw, как в class(dbGetQuery(db, "SELECT payload FROM blobs")$payload[[1]]) == "raw". Вариант Base64-в-TEXT существует ради переносимости: его можно осмотреть в текстовом редакторе, и любой другой язык прочитает его без бинарных драйверов.
Почта - вот откуда берётся перенесённый Base64. MIME-части переносит на 76 символов с CRLF между строками, и любая почтовая система, которую вы когда-либо читали, носила вложения ровно таким образом. У R нет первоклассного почтового клиента, но когда вы всё-таки получаете файл .eml, base64-часть вложения - это просто перенесённый на строки текст: снисходительные декодеры развернут его за вас, пока декодируют. Пакет mime помогает определить, что у вас в руках:
mime::guess_type("photo.jpg")
#> [1] "image/jpeg"
part <- "SGVsbG8s\r\nCnRoaXMgaXMgYW4g\r\nZW1haWwgYXR0YWNobWVudA=="
rawToChar(base64decode(part))
#> [1] "Hello,\nthis is an email attachment"
Завершают тур файлы конфигурации. Когда сертификат или блок хранится в Base64-кодировании в YAML- или JSON-конфигурации, или в переменной окружения, R читает его как обычную строку, и вы декодируете по требованию:
config_line <- "api_cert: TFRTU0VDUkVU"
cert_b64 <- sub("^api_cert: ", "", config_line)
raws <- base64decode(cert_b64)
rawToChar(raws)
#> [1] "LTSSECRET"
Крупные данные
У R-строк есть жёсткий потолок в 2^31 - 1 байт, и достаточно большой Base64-строке можно в него упереться, потому что закодированная форма примерно на треть крупнее оригинала. Пакет base64enc справляется с этим с 2022 года: дайте base64encode() ширину строки, и он вернёт вам вектор строк вместо одного огромного, а на стороне декодирования аргумент file = читает файл по строкам и декодирует, не заставляя вас держать одну гигантскую строку в переменной.
Для файлов в сотни мегабайт рабочий паттерн - читать согласованными кусками. Группы Base64 независимы, так что любой кусок, чья длина кратна четырём символам, декодируется сам по себе; вам нужен лишь небольшой буфер, чтобы нести неровный остаток:
chunk <- 65536L
con <- file("huge.b64", "rb")
on.exit(close(con))
leftover <- ""
out <- raw(0)
while (TRUE) {
piece <- readChar(con, chunk, useBytes = TRUE)
if (length(piece) == 0) break
piece <- gsub("[\r\n]", "", piece)
ready <- paste0(leftover, piece)
take <- floor(nchar(ready) / 4) * 4
if (take > 0) {
out <- c(out, base64decode(substr(ready, 1, take)))
leftover <- substr(ready, take + 1, nchar(ready))
} else {
leftover <- ready
}
}
if (nchar(leftover) > 0) out <- c(out, base64decode(leftover))
length(out)
Пакет b64 - чемпион скорости в этих краях: его Rust-движок декодирует 50-мегабайтный файл за долю секунды, и его векторизованный decode() обрабатывает колонку из многих закодированных значений одним вызовом, что драматически отличается от цикла по строкам. Если у вас много строк, сами прогоните быстрое сравнение через system.time(); разрыв между построчным циклом и одним векторизованным вызовом обычно достаточно велик, чтобы иметь значение.
Командная строка
Не всё требует полноценной сессии R. Классические Unix-инструменты говорят на Base64 нативно, и R может отдавать им работу или забирать её обратно. На Linux base64 -d декодирует (macOS и другие BSD-системы используют -D):
echo "SGVsbG8sIHdvcmxkIQ==" | base64 -d
#> Hello, world!
А однострочный Rscript делает ту же работу теми же пакетами, которыми вы пользуетесь в скриптах:
Rscript -e 'library(base64enc); writeLines(rawToChar(base64decode("TWFu")))'
#> Man
Используйте шелл для быстрых проверок и конвейеров; используйте R, когда результату нужно жить в data frame, в файле или в отчёте. Одна предосторожность: аргументы командной строки имеют предел размера (ARG_MAX), так что не вставляйте в терминал строки в несколько мегабайт. Пропускайте их через файл.
Ловушки, которые стоит знать
Вот короткий список того, как это кусает R-разработчиков; всё из этого родное экосистеме, а не Base64 вообще:
- openssl проваливается молча.
base64_decode()возвращает пустой вектор raw, без ошибки и предупреждения, когда встречает символ вне алфавита или хвостовой мусор. Всегда проверяйтеlength()результата, прежде чем ему верить. - NA - не отсутствующее значение для декодера.
NA_character_приводится к строке"NA", которая декодируется в байт0x34. ОтфильтруйтеNAдо декодирования. - Векторы ведут себя не так, как вы ждёте.
base64decode()склеивает вход в один вектор raw;openssl::base64_decode()умирает сthe condition has length > 1на многоэлементном входе; толькоb64::decode()по-настоящему векторизован. - rawToChar - тихий порчитель. Невалидные UTF-8 байты в UTF-8-локали не поднимают ошибку; они становятся строкой с пометкой
"unknown". Сначала прогоните черезcheckUTF8(). - Точка в JWT - regex-подстановочный символ.
strsplit(jwt, ".")безfixed = TRUEрежет по каждому символу. Это самый частый Base64-баг в R-коде. - Файлы для b64 не прощают. Завершающий перенос строки в закодированном файле отправляет
b64::decode_file()в панику. Пишите черезcat(), а неwriteLines(). - Снисходительность - не та снисходительность, которой можно доверять. Режим по умолчанию у
base64decode()пропускает плохие символы где угодно, так что испорченная строка может декодироваться в правдоподобно выглядящий мусор без единой ошибки. Используйтеstrict = TRUEна границах доверия. - Тексты ошибок b64 говорят по-руст. Ожидайте фразы вроде
Both cases of Either errored, когда подадите ему тип, который он не хочет. Сообщение бесполезно намеренно; это Rust-система типов, которая пожимает плечами.
Лучшие практики
- По умолчанию берите
base64enc::base64decode()для повседневной работы, и включайтеstrict = TRUEвезде, где ввод пересекает границу доверия: недоверенные API, загрузки пользователей, всё подписанное. - Тянитесь за
b64, когда нужна скорость, настоящая векторизация или URL-безопасные и беззаполнительные движки, и примите, что он строгий по замыслу. - Если
opensslуже есть в проекте - пользуйтесь, но считайте каждый результат подозрительным, пока не проверите его длину. - Решайте про кодировку осознанно: предполагайте UTF-8, проверяйте через
checkUTF8(), и используйтеiconv()только когда отправитель сказал, что иначе. - Для JWT подглядывайте через
strsplit()плюсfixed = TRUE, но доверяйте только после того, какjoseпроверит подпись. - Держите
TWFuв своих тестах: это три байта дымового теста для любого пути декодирования, который вы пишете. - Помните, чем Base64 не является: это не шифрование и не сжатие. Это упаковочный скотч, и любой, у кого есть эта статья, может развернуть всё, что он делает. Декодируйте свободно, доверяйте выборочно.
Краткая история Base64 в R
Формату уже много лет. Он был стандартизирован для протокола Privacy-Enhanced Mail в 1987 (RFC 989), принят MIME в 1993 (RFC 1521, затем итоговый RFC 2045 в 1996), подчищен в RFC 3548 в 2003 и обрёл современный облик, включая URL-безопасный алфавит, в RFC 4648 в 2006. История R гораздо короче и стремительнее. Пакет base64enc Саймона Урбанека впервые приземлился на CRAN в сентябре 2012 и с тех пор является рабочей лошадкой: в 2015 к нему добавился checkUTF8(), в 2022 - поддержка длинных векторов. В октябре 2024 старый пакет base64 переиздали явно как совместимую обёртку, и его собственное описание теперь направляет новые приложения к base64enc, openssl или jsonlite. Затем в начале 2024 пришёл b64 (текущий выпуск 0.1.7, июль 2025), Rust-движок, собранный на extendr, который принёс настоящую векторизацию и табун алфавитов. В феврале 2026 base64enc выпустил долгожданный строгий режим, а в апреле 2026 пакет jose перестроил себя вокруг функций jwt_*. Десять лет на то, что другие языки отгрузили в одной библиотеке, но результат - набор инструментов, где у каждого декодера чёткая работа и чёткий характер.
Весёлые факты
Потому что полное руководство должно закончиться улыбкой:
base64decode(NA_character_)возвращает букву"4", потому чтоNA_character_едет в декодер строкой"NA", а"NA"- валидная группа Base64. Самый R-специфичный однобайтовый сюрприз в языке.- Подайте
openssl::base64_decode()строку с одним незаконным символом, и он вернётraw(0)в полной тишине. Самая опасная тишина в экосистеме. - Каждый строгий вызов
base64decode()бормочет C-уровневую строку состояния вродеv=30000, pad=0, org='u'. Это не предупреждение. Это C-код прочищает горло. b64умеет декодировать алфавиты, которые вы никогда не видели: BinHex, IMAP modified UTF-7 и кастомные алфавиты bcrypt и crypt. R одним и тем же движком читает и вложение с Macintosh из 80-х, и современный хеш пароля.- R-строки обрываются на 2^31 - 1 байт, вот почему
base64encвыучился возвращать вектор строк для длинного входа. Формат уперся в стену языка, и пакет отрастил лестницу. - Слово «base64» кодируется в
YmFzZTY0. Формат, способный описать самого себя, - технический эквивалент зеркала, которое говорит на морзе. - Один-единственный NUL-байт по-прежнему стоит четырёх символов:
"AA==". В Base64 ничего - это всегда что-то. - У вашей сборки R есть прозвище. R 4.5.0 называется «How About a Twenty-Six», потому что версии R названы по комиксам и фильмам Peanuts, а полосы, откуда они берут, на десятилетия старше выпусков, которые ими названы. Даже у версии, с которой вы декодируете, есть чувство юмора.
Итоги
Выбирайте декодера по компании, которую вы собираете: base64enc для повседневной работы, с strict = TRUE на краях; openssl, если он уже в проекте, при условии, что вы проверяете результаты; b64, когда хотите скорости, векторов и URL-безопасных движков; и мелких специалистов base64 и base64url - для файловых дел и URL-безопасных строк. Стражуйте байты через checkUTF8(), прежде чем называть их текстом, используйте iconv(), когда известно, что кодировка другая, и помните, что вектор raw посередине - это фича: он заставляет вас решить, что значат байты, вместо того чтобы дать библиотеке гадать. Декодируйте всё, доверяйте только то, что верифицируется. А когда нужно пойти в обратную сторону, упаковать собственные байты в строку в дорогу, а не распаковывать чужую, сестринская статья подробно разбирает кодирование Base64 в R.
Последнее обновление: 2026-09-08
Связанная статья: Кодирование Base64 в R: полное руководство