Як швидко перетворити дані у шістнадцятковий рядок


81

Я хочу шістнадцяткове представлення значення даних у Swift.

Зрештою я хотів би використовувати його так:

let data = Data(base64Encoded: "aGVsbG8gd29ybGQ=")!
print(data.hexString)

Відповіді:


198

Проста реалізація (взята з " Як хеш NSString за допомогою SHA1 в Swift?" , З додатковою опцією для виводу великих літер) буде

extension Data {
    struct HexEncodingOptions: OptionSet {
        let rawValue: Int
        static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
    }

    func hexEncodedString(options: HexEncodingOptions = []) -> String {
        let format = options.contains(.upperCase) ? "%02hhX" : "%02hhx"
        return map { String(format: format, $0) }.joined()
    }
}

Я обрав hexEncodedString(options:)метод у стилі існуючого методу base64EncodedString(options:).

Dataвідповідає Collectionпротоколу, тому можна використати map()для зіставлення кожного байта з відповідним шістнадцятковим рядком. %02xФормат друкує аргумент в підставі 16, заповнений до двох цифр з ведучим нулем , якщо це необхідно. hhМодифікатор змушує аргумент (який передається як ціле число в стеку), що підлягає обробці , як кількість в один байт. Тут можна пропустити модифікатор, оскільки $0це непідписаний номер ( UInt8), і розширення знаку не відбудеться, але це не завдає шкоди, залишаючи його.

Потім результат об’єднується в один рядок.

Приклад:

let data = Data(bytes: [0, 1, 127, 128, 255])
print(data.hexEncodedString()) // 00017f80ff
print(data.hexEncodedString(options: .upperCase)) // 00017F80FF

Наступна реалізація є швидшою приблизно в 50 разів (перевіряється з 1000 випадковими байтами). Він натхненний рішення RenniePet в і рішення Ніка Мура , але має перевагу , String(unsafeUninitializedCapacity:initializingUTF8With:) яке було введено з Swift 5.3 / Xcode 12 і доступна на MacOS 11 і прошивкою 14 або пізнішої версії.

Цей метод дозволяє ефективно створювати рядок Swift з блоків UTF-8, без зайвого копіювання або перерозподілу.

Також пропонується альтернативна реалізація для старих версій macOS / iOS.

extension Data {
    struct HexEncodingOptions: OptionSet {
        let rawValue: Int
        static let upperCase = HexEncodingOptions(rawValue: 1 << 0)
    }

    func hexEncodedString(options: HexEncodingOptions = []) -> String {
        let hexDigits = options.contains(.upperCase) ? "0123456789ABCDEF" : "0123456789abcdef"
        if #available(macOS 11.0, iOS 14.0, watchOS 7.0, tvOS 14.0, *) {
            let utf8Digits = Array(hexDigits.utf8)
            return String(unsafeUninitializedCapacity: 2 * count) { (ptr) -> Int in
                var p = ptr.baseAddress!
                for byte in self {
                    p[0] = utf8Digits[Int(byte / 16)]
                    p[1] = utf8Digits[Int(byte % 16)]
                    p += 2
                }
                return 2 * count
            }
        } else {
            let utf16Digits = Array(hexDigits.utf16)
            var chars: [unichar] = []
            chars.reserveCapacity(2 * count)
            for byte in self {
                chars.append(utf16Digits[Int(byte / 16)])
                chars.append(utf16Digits[Int(byte % 16)])
            }
            return String(utf16CodeUnits: chars, count: chars.count)
        }
    }
}

1
Хоча мені зазвичай не подобається extensionклас на Apple, коли funcможна використовувати a, я люблю симетрію base64EncodedString.
zaph

2
@reza_khalafi: Є багато рішень для Objective-C, наприклад тут: stackoverflow.com/questions/1305225/… .
Мартін Р,

1
Чи можете ви також надати операцію зворотного перетворення з рядка на шістнадцяткові дані
nkp,

1
@MiladFaridnia: UTF-16 - це те, що рядки Swift використовують внутрішньо. Вищевказана функція повертає рядок, що містить лише цифри 0 ... 9 та літери A ... F, так що це не повинно бути проблемою.
Martin R

1
Чудова відповідь Мартін
Вудсток

28

Цей код розширює Dataтип із обчисленою властивістю. Він перебирає байти даних і об’єднує шістнадцяткове представлення байта з результатом:

extension Data {
    var hexDescription: String {
        return reduce("") {$0 + String(format: "%02x", $1)}
    }
}

1
Хоча цей код може допомогти вирішити проблему, він не пояснює, чому та / або як він відповідає на питання. Забезпечення цього додаткового контексту значно покращило б його довгострокову цінність. Будь ласка, відредагуйте свою відповідь, щоб додати пояснення, включаючи обмеження та припущення.
Тобі Спейт

6
Ви можете просто передати його NSData і отримати його описreturn (self as NSData).description
Лео Дабус

6
Мій улюблений (зі stackoverflow.com/a/25762128/1187415 ):return map { String(format: "%02hhx", $0) }.joined()
Martin R

1
"Для кожного байта виконується операція виділення та копіювання рядків." Я не думаю, що це правильно. Я думаю, що Свіфт досить добре справляється з оптимізацією маніпулювання рядками. stackoverflow.com/a/36397955/253938
RenniePet

Я погоджуюсь із вашою редакцією, видаляючи попередження про низьку ефективність. Тож мій вищезазначений коментар слід ігнорувати, але я залишу його там заради нащадків.
RenniePet

23

Моя версія. Це приблизно в 10 разів швидше, ніж [оригінальна] прийнята відповідь Мартіна Р.

public extension Data {
    private static let hexAlphabet = Array("0123456789abcdef".unicodeScalars)
    func hexStringEncoded() -> String {
        String(reduce(into: "".unicodeScalars) { result, value in
            result.append(Self.hexAlphabet[Int(value / 0x10)])
            result.append(Self.hexAlphabet[Int(value % 0x10)])
        })
    }
}

1
Це елегантно, Нік. Дякую!
Вадим

1
Був у змозі покращити це, якщо ви хочете внести мої зміни у цю відповідь: gist.github.com/BenLeggiero/916bf788000736a7c0e6d1cad6e54410
Бен Ледж'єро,

12

Swift 4 - від даних до шістнадцяткової рядка на
основі рішення Мартіна Р, але навіть крихітно швидше.

extension Data {
  /// A hexadecimal string representation of the bytes.
  func hexEncodedString() -> String {
    let hexDigits = Array("0123456789abcdef".utf16)
    var hexChars = [UTF16.CodeUnit]()
    hexChars.reserveCapacity(count * 2)

    for byte in self {
      let (index1, index2) = Int(byte).quotientAndRemainder(dividingBy: 16)
      hexChars.append(hexDigits[index1])
      hexChars.append(hexDigits[index2])
    }

    return String(utf16CodeUnits: hexChars, count: hexChars.count)
  }
}

Swift 4 - від шістнадцяткового рядка до даних
Я також додав швидке рішення для перетворення шістнадцяткового рядка в дані (на основі рішення C ).

extension String {
  /// A data representation of the hexadecimal bytes in this string.
  func hexDecodedData() -> Data {
    // Get the UTF8 characters of this string
    let chars = Array(utf8)

    // Keep the bytes in an UInt8 array and later convert it to Data
    var bytes = [UInt8]()
    bytes.reserveCapacity(count / 2)

    // It is a lot faster to use a lookup map instead of strtoul
    let map: [UInt8] = [
      0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, // 01234567
      0x08, 0x09, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 89:;<=>?
      0x00, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x00, // @ABCDEFG
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00  // HIJKLMNO
    ]

    // Grab two characters at a time, map them and turn it into a byte
    for i in stride(from: 0, to: count, by: 2) {
      let index1 = Int(chars[i] & 0x1F ^ 0x10)
      let index2 = Int(chars[i + 1] & 0x1F ^ 0x10)
      bytes.append(map[index1] << 4 | map[index2])
    }

    return Data(bytes)
  }
}

Примітка: ця функція не перевіряє введені дані. Переконайтесь, що він використовується лише для шістнадцяткових рядків із (парною кількістю) символів.


5

Це насправді не відповідає на питання OP, оскільки воно працює на байтовому масиві Swift, а не на об'єкті Data. І це набагато більше, ніж інші відповіді. Але це повинно бути більш ефективним, оскільки воно уникає використання рядка (format:).

У будь-якому випадку, в надії хтось вважає це корисним ...

public class StringMisc {

   // MARK: - Constants

   // This is used by the byteArrayToHexString() method
   private static let CHexLookup : [Character] =
      [ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F" ]


   // Mark: - Public methods

   /// Method to convert a byte array into a string containing hex characters, without any
   /// additional formatting.
   public static func byteArrayToHexString(_ byteArray : [UInt8]) -> String {

      var stringToReturn = ""

      for oneByte in byteArray {
         let asInt = Int(oneByte)
         stringToReturn.append(StringMisc.CHexLookup[asInt >> 4])
         stringToReturn.append(StringMisc.CHexLookup[asInt & 0x0f])
      }
      return stringToReturn
   }
}

Тестовий приклад:

  // Test the byteArrayToHexString() method
  let byteArray : [UInt8] = [ 0x25, 0x99, 0xf3 ]
  assert(StringMisc.byteArrayToHexString(byteArray) == "2599F3")

Ось більш швидка версія цієї відповіді: gist.github.com/BenLeggiero/ce1e62fe0194ca969eb7cdda6639a011
Бен Ледж'єро,

0

Можливо, не найшвидший, але data.map({ String($0, radix: 16) }).joined()робить свою роботу. Як зазначалося в коментарях, це рішення було недосконалим.


1
Це проблематично, оскільки для одноцифрових шістнадцяткових чисел не вставляються початкові нулі. Приклад: для Data(bytes: [0x11, 0x02, 0x03, 0x44])нього повертається рядок "112344" замість "11020344".
Martin R

0

Дещо відрізняється від інших відповідей тут:

extension DataProtocol {
    func hexEncodedString(uppercase: Bool = false) -> String {
        return self.map {
            if $0 < 16 {
                return "0" + String($0, radix: 16, uppercase: uppercase)
            } else {
                return String($0, radix: 16, uppercase: uppercase)
            }
        }.joined()
    }
}

Однак у моїй базовій установці міри XCTest + це було найшвидше з 4-х, які я спробував.

Перебираючи 1000 байт (однакових) випадкових даних по 100 разів кожен:

Вгорі: Середнє за часом: 0,028 секунди, відносне стандартне відхилення: 1,3%

MartinR: Середнє значення часу: 0,037 секунди, відносне стандартне відхилення: 6,2%

Зифракс: Середній час: 0,032 секунди, відносне стандартне відхилення: 2,9%

NickMoore: Середнє значення часу: 0,039 секунди, відносне стандартне відхилення: 2,0%

Повторення тесту дало ті самі відносні результати. (Нік і Мартінс іноді міняються місцями)

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.