So konvertieren Sie Daten schnell in eine Hex-Zeichenfolge

80

Ich möchte die hexadezimale Darstellung eines Datenwerts in Swift.

Irgendwann möchte ich es so verwenden:

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

Antworten:

197

Eine einfache Implementierung (entnommen aus How to Hash NSString mit SHA1 in Swift ? , Mit einer zusätzlichen Option für die Ausgabe in Großbuchstaben) wäre

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()
    }
}

Ich habe eine hexEncodedString(options:)Methode im Stil der vorhandenen Methode gewählt base64EncodedString(options:).

Dataentspricht dem CollectionProtokoll, daher kann man map()jedes Byte der entsprechenden Hex-Zeichenfolge zuordnen. Das %02xFormat gibt das Argument in Basis 16 aus, das bei Bedarf bis zu zwei Ziffern mit einer führenden Null gefüllt ist. Der hhModifikator bewirkt, dass das Argument (das als Ganzzahl auf dem Stapel übergeben wird) als eine 1-Byte-Menge behandelt wird. Man könnte den Modifikator hier weglassen, da $0es sich um eine vorzeichenlose Zahl ( UInt8) handelt und keine Vorzeichenerweiterung auftritt, aber es schadet nicht, sie zu belassen.

Das Ergebnis wird dann zu einer einzelnen Zeichenfolge verbunden.

Beispiel:

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

Die folgende Implementierung ist um den Faktor 50 schneller (getestet mit 1000 zufälligen Bytes). Es ist inspiriert RenniePet-Lösung und Nick Moores Lösung , sondern nutzt String(unsafeUninitializedCapacity:initializingUTF8With:) die mit Swift 5.3 / Xcode 12 eingeführt wurde und auf macOS 11 und iOS 14 oder höher verfügbar.

Diese Methode ermöglicht es, einen Swift-String effizient aus UTF-8-Einheiten zu erstellen, ohne unnötiges Kopieren oder Neuzuweisen.

Eine alternative Implementierung für ältere MacOS / iOS-Versionen wird ebenfalls bereitgestellt.

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)
        }
    }
}
Martin R.
quelle
1
Während ich normalerweise keine extensionApple-Klasse mag, wenn eine funcverwendet werden kann, liebe ich die Symmetrie mit base64EncodedString.
Zaph
2
@reza_khalafi: Es gibt viele Lösungen für Objective-C, zum Beispiel hier: stackoverflow.com/questions/1305225/… .
Martin R
1
Können Sie auch eine umgekehrte Operation von Zeichenfolge zu Hex-Daten
bereitstellen
1
@ MiladFaridnia: UTF-16 wird von Swift-Strings intern verwendet. Die obige Funktion gibt eine Zeichenfolge zurück, die nur die Ziffern 0 ... 9 und die Buchstaben A ... F enthält, sodass dies kein Problem darstellen sollte.
Martin R
1
Tolle Antwort Martin
Woodstock
27

Dieser Code erweitert den DataTyp um eine berechnete Eigenschaft. Es durchläuft die Datenbytes und verkettet die Hex-Darstellung des Bytes mit dem Ergebnis:

extension Data {
    var hexDescription: String {
        return reduce("") {$0 + String(format: "%02x", $1)}
    }
}
Marius
quelle
1
Obwohl dieser Code zur Lösung des Problems beitragen kann, erklärt er nicht, warum und / oder wie er die Frage beantwortet. Die Bereitstellung dieses zusätzlichen Kontextes würde seinen langfristigen Wert erheblich verbessern. Bitte bearbeiten Sie Ihre Antwort, um eine Erklärung hinzuzufügen, einschließlich der Einschränkungen und Annahmen.
Toby Speight
6
Sie können es einfach in NSData umwandeln und seine Beschreibung erhaltenreturn (self as NSData).description
Leo Dabus
6
Mein Favorit (von stackoverflow.com/a/25762128/1187415 ):return map { String(format: "%02hhx", $0) }.joined()
Martin R
1
"Für jedes Byte wird eine Zeichenfolgenzuweisung und ein Kopiervorgang ausgeführt." Ich denke nicht, dass das richtig ist. Ich denke, Swift macht einen ziemlich guten Job bei der Optimierung der String-Manipulation. stackoverflow.com/a/36397955/253938
RenniePet
Ich bin mit Ihrer Bearbeitung einverstanden und entferne die Warnung vor schlechter Effizienz. Mein obiger Kommentar sollte also ignoriert werden, aber ich werde ihn der Nachwelt zuliebe dort lassen.
RenniePet
23

Meine Version. Es ist ungefähr zehnmal schneller als die [ursprüngliche] akzeptierte Antwort von Martin R.

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)])
        })
    }
}
Nick Moore
quelle
1
Es ist elegant, Nick. Vielen Dank!
Vadim
1
Konnte dies zu einem Leckerbissen machen, wenn Sie meine Änderungen in diese Antwort einfließen lassen
möchten
12

Swift 4 - Von Daten zu Hex-Zeichenfolgen
Basierend auf der Lösung von Martin R, aber noch ein bisschen schneller.

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 - Von Hex-String zu Daten
Ich habe auch eine schnelle Lösung zum Konvertieren eines Hex-Strings in Daten hinzugefügt (basierend auf einer C-Lösung ).

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)
  }
}

Hinweis: Diese Funktion validiert die Eingabe nicht. Stellen Sie sicher, dass es nur für hexadezimale Zeichenfolgen mit (einer geraden Anzahl von) Zeichen verwendet wird.

Zyphrax
quelle
5

Dies beantwortet die Frage des OP nicht wirklich, da es auf einem Swift-Byte-Array und nicht auf einem Datenobjekt funktioniert. Und es ist viel größer als die anderen Antworten. Es sollte jedoch effizienter sein, da die Verwendung von String (Format :) vermieden wird.

Wie auch immer, in der Hoffnung, dass jemand dies nützlich findet ...

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
   }
}

Testfall:

  // Test the byteArrayToHexString() method
  let byteArray : [UInt8] = [ 0x25, 0x99, 0xf3 ]
  assert(StringMisc.byteArrayToHexString(byteArray) == "2599F3")
RenniePet
quelle
Hier ist eine schnellere Version dieser Antwort: gist.github.com/BenLeggiero/ce1e62fe0194ca969eb7cdda6639a011
Ben Leggiero
0

Vielleicht nicht der schnellste, macht aber data.map({ String($0, radix: 16) }).joined()den Job. Wie in den Kommentaren erwähnt, war diese Lösung fehlerhaft.

DeFrenZ
quelle
1
Dies ist problematisch, da für einstellige Hex-Zahlen keine führenden Nullen eingefügt werden. Beispiel: Dafür Data(bytes: [0x11, 0x02, 0x03, 0x44])wird die Zeichenfolge "112344" anstelle von "11020344" zurückgegeben.
Martin R
0

Ein bisschen anders als andere Antworten hier:

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()
    }
}

In meinem grundlegenden XCTest + -Mess-Setup war dies jedoch das schnellste der 4, die ich ausprobiert habe.

Durchlaufen von 1000 Bytes (derselben) Zufallsdaten jeweils 100 Mal:

Oben: Zeitdurchschnitt: 0,028 Sekunden, relative Standardabweichung: 1,3%

MartinR: Zeitdurchschnitt: 0,037 Sekunden, relative Standardabweichung: 6,2%

Zyphrax: Zeitdurchschnitt: 0,032 Sekunden, relative Standardabweichung: 2,9%

NickMoore: Zeitdurchschnitt: 0,039 Sekunden, relative Standardabweichung: 2,0%

Das Wiederholen des Tests ergab die gleichen relativen Ergebnisse. (Nick und Martins tauschten manchmal)

Justin Oroz
quelle