¿Cómo decodifico entidades HTML en Swift?


121

Extraigo un archivo JSON de un sitio y una de las cadenas recibidas es:

The Weeknd ‘King Of The Fall’ [Video Premiere] | @TheWeeknd | #SoPhi

¿Cómo puedo convertir cosas como &#8216en los caracteres correctos?

He creado un Xcode Playground para demostrarlo:

import UIKit

var error: NSError?
let blogUrl: NSURL = NSURL.URLWithString("http://sophisticatedignorance.net/api/get_recent_summary/")
let jsonData = NSData(contentsOfURL: blogUrl)

let dataDictionary = NSJSONSerialization.JSONObjectWithData(jsonData, options: nil, error: &error) as NSDictionary

var a = dataDictionary["posts"] as NSArray

println(a[0]["title"])

Respuestas:


157

Esta respuesta se revisó por última vez para Swift 5.2 y iOS 13.4 SDK.


No hay una manera directa de hacerlo, pero puede usar la NSAttributedStringmagia para hacer que este proceso sea lo menos doloroso posible (tenga en cuenta que este método también eliminará todas las etiquetas HTML).

Recuerde iniciar solo NSAttributedStringdesde el hilo principal . Utiliza WebKit para analizar HTML debajo, por lo tanto, el requisito.

// This is a[0]["title"] in your case
let encodedString = "The Weeknd <em>&#8216;King Of The Fall&#8217;</em>"

guard let data = htmlEncodedString.data(using: .utf8) else {
    return
}

let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
    .documentType: NSAttributedString.DocumentType.html,
    .characterEncoding: String.Encoding.utf8.rawValue
]

guard let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) else {
    return
}

// The Weeknd ‘King Of The Fall’
let decodedString = attributedString.string
extension String {

    init?(htmlEncodedString: String) {

        guard let data = htmlEncodedString.data(using: .utf8) else {
            return nil
        }

        let options: [NSAttributedString.DocumentReadingOptionKey: Any] = [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ]

        guard let attributedString = try? NSAttributedString(data: data, options: options, documentAttributes: nil) else {
            return nil
        }

        self.init(attributedString.string)

    }

}

let encodedString = "The Weeknd <em>&#8216;King Of The Fall&#8217;</em>"
let decodedString = String(htmlEncodedString: encodedString)

54
¿Qué? Las extensiones están destinadas a extender los tipos existentes para proporcionar una nueva funcionalidad.
akashivskyy

44
Entiendo lo que estás tratando de decir, pero negar las extensiones no es el camino a seguir.
akashivskyy

1
@akashivskyy: para que esto funcione correctamente con caracteres no ASCII, debe agregar un NSCharacterEncodingDocumentAttribute, compare stackoverflow.com/a/27898167/1187415 .
Martin R

13
Este método es extremadamente pesado y no se recomienda en vistas de tabla o vistas de cuadrícula
Guido Lodetti

1
¡Esto es genial! Aunque bloquea el hilo principal, ¿hay alguna forma de ejecutarlo en el hilo de fondo?
MMV

78

La respuesta de @ akashivskyy es excelente y demuestra cómo utilizar NSAttributedStringpara decodificar entidades HTML. Una posible desventaja (como él dijo) es que también se elimina todo el marcado HTML, por lo que

<strong> 4 &lt; 5 &amp; 3 &gt; 2</strong>

se convierte

4 < 5 & 3 > 2

En OS X hay CFXMLCreateStringByUnescapingEntities()quien hace el trabajo:

let encoded = "<strong> 4 &lt; 5 &amp; 3 &gt; 2 .</strong> Price: 12 &#x20ac;.  &#64; "
let decoded = CFXMLCreateStringByUnescapingEntities(nil, encoded, nil) as String
println(decoded)
// <strong> 4 < 5 & 3 > 2 .</strong> Price: 12.  @ 

pero esto no está disponible en iOS.

Aquí hay una implementación pura de Swift. Decodifica referencias de entidades de caracteres como &lt;usar un diccionario, y todas las entidades de caracteres numéricos como &#64o &#x20ac. (Tenga en cuenta que no enumeré todas las 252 entidades HTML explícitamente).

Swift 4:

// Mapping from XML/HTML character entity reference to character
// From http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
private let characterEntities : [ Substring : Character ] = [
    // XML predefined entities:
    "&quot;"    : "\"",
    "&amp;"     : "&",
    "&apos;"    : "'",
    "&lt;"      : "<",
    "&gt;"      : ">",

    // HTML character entity references:
    "&nbsp;"    : "\u{00a0}",
    // ...
    "&diams;"   : "♦",
]

extension String {

    /// Returns a new string made by replacing in the `String`
    /// all HTML character entity references with the corresponding
    /// character.
    var stringByDecodingHTMLEntities : String {

        // ===== Utility functions =====

        // Convert the number in the string to the corresponding
        // Unicode character, e.g.
        //    decodeNumeric("64", 10)   --> "@"
        //    decodeNumeric("20ac", 16) --> "€"
        func decodeNumeric(_ string : Substring, base : Int) -> Character? {
            guard let code = UInt32(string, radix: base),
                let uniScalar = UnicodeScalar(code) else { return nil }
            return Character(uniScalar)
        }

        // Decode the HTML character entity to the corresponding
        // Unicode character, return `nil` for invalid input.
        //     decode("&#64;")    --> "@"
        //     decode("&#x20ac;") --> "€"
        //     decode("&lt;")     --> "<"
        //     decode("&foo;")    --> nil
        func decode(_ entity : Substring) -> Character? {

            if entity.hasPrefix("&#x") || entity.hasPrefix("&#X") {
                return decodeNumeric(entity.dropFirst(3).dropLast(), base: 16)
            } else if entity.hasPrefix("&#") {
                return decodeNumeric(entity.dropFirst(2).dropLast(), base: 10)
            } else {
                return characterEntities[entity]
            }
        }

        // ===== Method starts here =====

        var result = ""
        var position = startIndex

        // Find the next '&' and copy the characters preceding it to `result`:
        while let ampRange = self[position...].range(of: "&") {
            result.append(contentsOf: self[position ..< ampRange.lowerBound])
            position = ampRange.lowerBound

            // Find the next ';' and copy everything from '&' to ';' into `entity`
            guard let semiRange = self[position...].range(of: ";") else {
                // No matching ';'.
                break
            }
            let entity = self[position ..< semiRange.upperBound]
            position = semiRange.upperBound

            if let decoded = decode(entity) {
                // Replace by decoded character:
                result.append(decoded)
            } else {
                // Invalid entity, copy verbatim:
                result.append(contentsOf: entity)
            }
        }
        // Copy remaining characters to `result`:
        result.append(contentsOf: self[position...])
        return result
    }
}

Ejemplo:

let encoded = "<strong> 4 &lt; 5 &amp; 3 &gt; 2 .</strong> Price: 12 &#x20ac;.  &#64; "
let decoded = encoded.stringByDecodingHTMLEntities
print(decoded)
// <strong> 4 < 5 & 3 > 2 .</strong> Price: 12.  @

Swift 3:

// Mapping from XML/HTML character entity reference to character
// From http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
private let characterEntities : [ String : Character ] = [
    // XML predefined entities:
    "&quot;"    : "\"",
    "&amp;"     : "&",
    "&apos;"    : "'",
    "&lt;"      : "<",
    "&gt;"      : ">",

    // HTML character entity references:
    "&nbsp;"    : "\u{00a0}",
    // ...
    "&diams;"   : "♦",
]

extension String {

    /// Returns a new string made by replacing in the `String`
    /// all HTML character entity references with the corresponding
    /// character.
    var stringByDecodingHTMLEntities : String {

        // ===== Utility functions =====

        // Convert the number in the string to the corresponding
        // Unicode character, e.g.
        //    decodeNumeric("64", 10)   --> "@"
        //    decodeNumeric("20ac", 16) --> "€"
        func decodeNumeric(_ string : String, base : Int) -> Character? {
            guard let code = UInt32(string, radix: base),
                let uniScalar = UnicodeScalar(code) else { return nil }
            return Character(uniScalar)
        }

        // Decode the HTML character entity to the corresponding
        // Unicode character, return `nil` for invalid input.
        //     decode("&#64;")    --> "@"
        //     decode("&#x20ac;") --> "€"
        //     decode("&lt;")     --> "<"
        //     decode("&foo;")    --> nil
        func decode(_ entity : String) -> Character? {

            if entity.hasPrefix("&#x") || entity.hasPrefix("&#X"){
                return decodeNumeric(entity.substring(with: entity.index(entity.startIndex, offsetBy: 3) ..< entity.index(entity.endIndex, offsetBy: -1)), base: 16)
            } else if entity.hasPrefix("&#") {
                return decodeNumeric(entity.substring(with: entity.index(entity.startIndex, offsetBy: 2) ..< entity.index(entity.endIndex, offsetBy: -1)), base: 10)
            } else {
                return characterEntities[entity]
            }
        }

        // ===== Method starts here =====

        var result = ""
        var position = startIndex

        // Find the next '&' and copy the characters preceding it to `result`:
        while let ampRange = self.range(of: "&", range: position ..< endIndex) {
            result.append(self[position ..< ampRange.lowerBound])
            position = ampRange.lowerBound

            // Find the next ';' and copy everything from '&' to ';' into `entity`
            if let semiRange = self.range(of: ";", range: position ..< endIndex) {
                let entity = self[position ..< semiRange.upperBound]
                position = semiRange.upperBound

                if let decoded = decode(entity) {
                    // Replace by decoded character:
                    result.append(decoded)
                } else {
                    // Invalid entity, copy verbatim:
                    result.append(entity)
                }
            } else {
                // No matching ';'.
                break
            }
        }
        // Copy remaining characters to `result`:
        result.append(self[position ..< endIndex])
        return result
    }
}

Swift 2:

// Mapping from XML/HTML character entity reference to character
// From http://en.wikipedia.org/wiki/List_of_XML_and_HTML_character_entity_references
private let characterEntities : [ String : Character ] = [
    // XML predefined entities:
    "&quot;"    : "\"",
    "&amp;"     : "&",
    "&apos;"    : "'",
    "&lt;"      : "<",
    "&gt;"      : ">",

    // HTML character entity references:
    "&nbsp;"    : "\u{00a0}",
    // ...
    "&diams;"   : "♦",
]

extension String {

    /// Returns a new string made by replacing in the `String`
    /// all HTML character entity references with the corresponding
    /// character.
    var stringByDecodingHTMLEntities : String {

        // ===== Utility functions =====

        // Convert the number in the string to the corresponding
        // Unicode character, e.g.
        //    decodeNumeric("64", 10)   --> "@"
        //    decodeNumeric("20ac", 16) --> "€"
        func decodeNumeric(string : String, base : Int32) -> Character? {
            let code = UInt32(strtoul(string, nil, base))
            return Character(UnicodeScalar(code))
        }

        // Decode the HTML character entity to the corresponding
        // Unicode character, return `nil` for invalid input.
        //     decode("&#64;")    --> "@"
        //     decode("&#x20ac;") --> "€"
        //     decode("&lt;")     --> "<"
        //     decode("&foo;")    --> nil
        func decode(entity : String) -> Character? {

            if entity.hasPrefix("&#x") || entity.hasPrefix("&#X"){
                return decodeNumeric(entity.substringFromIndex(entity.startIndex.advancedBy(3)), base: 16)
            } else if entity.hasPrefix("&#") {
                return decodeNumeric(entity.substringFromIndex(entity.startIndex.advancedBy(2)), base: 10)
            } else {
                return characterEntities[entity]
            }
        }

        // ===== Method starts here =====

        var result = ""
        var position = startIndex

        // Find the next '&' and copy the characters preceding it to `result`:
        while let ampRange = self.rangeOfString("&", range: position ..< endIndex) {
            result.appendContentsOf(self[position ..< ampRange.startIndex])
            position = ampRange.startIndex

            // Find the next ';' and copy everything from '&' to ';' into `entity`
            if let semiRange = self.rangeOfString(";", range: position ..< endIndex) {
                let entity = self[position ..< semiRange.endIndex]
                position = semiRange.endIndex

                if let decoded = decode(entity) {
                    // Replace by decoded character:
                    result.append(decoded)
                } else {
                    // Invalid entity, copy verbatim:
                    result.appendContentsOf(entity)
                }
            } else {
                // No matching ';'.
                break
            }
        }
        // Copy remaining characters to `result`:
        result.appendContentsOf(self[position ..< endIndex])
        return result
    }
}

10
Esto es genial, gracias Martin! Aquí está la extensión con la lista completa de entidades HTML: gist.github.com/mwaterfall/25b4a6a06dc3309d9555 También la he adaptado ligeramente para proporcionar las compensaciones de distancia realizadas por los reemplazos. Esto permite el ajuste correcto de cualquier atributo de cadena o entidad que pueda verse afectada por estos reemplazos (por ejemplo, índices de entidad de Twitter).
Michael Waterfall

3
@MichaelWaterfall y Martin, ¡esto es magnífico! ¡Funciona de maravilla! Actualizo la extensión para Swift 2 pastebin.com/juHRJ6au ¡Gracias!
Santiago

1
Convertí esta respuesta para que sea compatible con Swift 2 y la volqué en un CocoaPod llamado StringExtensionHTML para facilitar su uso. Tenga en cuenta que la versión Swift 2 de Santiago corrige los errores de tiempo de compilación, pero eliminarlo por strtooul(string, nil, base)completo hará que el código no funcione con entidades de caracteres numéricos y se bloquee cuando se trata de una entidad que no reconoce (en lugar de fallar con gracia).
Adela Chang

1
@AdelaChang: En realidad, ya había convertido mi respuesta a Swift 2 en septiembre de 2015. Todavía se compila sin advertencias con Swift 2.2 / Xcode 7.3. ¿O te refieres a la versión de Michael?
Martin R

1
Gracias, con esta respuesta resolví mis problemas: tuve serios problemas de rendimiento al usar NSAttributedString.
Andrea Mugnaini

27

Versión Swift 3 de la extensión de @ akashivskyy ,

extension String {
    init(htmlEncodedString: String) {
        self.init()
        guard let encodedData = htmlEncodedString.data(using: .utf8) else {
            self = htmlEncodedString
            return
        }

        let attributedOptions: [String : Any] = [
            NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
            NSCharacterEncodingDocumentAttribute: String.Encoding.utf8.rawValue
        ]

        do {
            let attributedString = try NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil)
            self = attributedString.string
        } catch {
            print("Error: \(error)")
            self = htmlEncodedString
        }
    }
}

Funciona genial. La respuesta original estaba causando un extraño accidente. Gracias por la actualización!
Geoherna

Para los caracteres franceses tengo que usar utf16
Sébastien REMY

23

Swift 4


  • Variable calculada de extensión de cadena
  • Sin guardia extra, hacer, atrapar, etc.
  • Devuelve las cadenas originales si falla la decodificación

extension String {
    var htmlDecoded: String {
        let decoded = try? NSAttributedString(data: Data(utf8), options: [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ], documentAttributes: nil).string

        return decoded ?? self
    }
}

1
Guauu ! ¡Funciona de inmediato para Swift 4! Uso // let encoded = "The Weeknd & # 8216; King Of The Fall & # 8217;" let finalString = encoded.htmlDecoded
Naishta

2
Me encanta la simplicidad de esta respuesta. Sin embargo, provocará bloqueos cuando se ejecute en segundo plano porque intenta ejecutarse en el hilo principal.
Jeremy Hicks

14

Versión Swift 2 de la extensión de @ akashivskyy,

 extension String {
     init(htmlEncodedString: String) {
         if let encodedData = htmlEncodedString.dataUsingEncoding(NSUTF8StringEncoding){
             let attributedOptions : [String: AnyObject] = [
            NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
            NSCharacterEncodingDocumentAttribute: NSUTF8StringEncoding
        ]

             do{
                 if let attributedString:NSAttributedString = try NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil){
                     self.init(attributedString.string)
                 }else{
                     print("error")
                     self.init(htmlEncodedString)     //Returning actual string if there is an error
                 }
             }catch{
                 print("error: \(error)")
                 self.init(htmlEncodedString)     //Returning actual string if there is an error
             }

         }else{
             self.init(htmlEncodedString)     //Returning actual string if there is an error
         }
     }
 }

Este código está incompleto y debe evitarse por todos los medios. El error no se está manejando correctamente. Cuando de hecho hay un código de error, se bloqueará. Debe actualizar su código para al menos devolver nil cuando hay un error. O simplemente puede iniciar con la cadena original. Al final deberías manejar el error. Que no es el caso. ¡Guauu!
oyalhi

9

Versión Swift 4

extension String {

    init(htmlEncodedString: String) {
        self.init()
        guard let encodedData = htmlEncodedString.data(using: .utf8) else {
            self = htmlEncodedString
            return
        }

        let attributedOptions: [NSAttributedString.DocumentReadingOptionKey : Any] = [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ]

        do {
            let attributedString = try NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil)
            self = attributedString.string
        } 
        catch {
            print("Error: \(error)")
            self = htmlEncodedString
        }
    }
}

Obtengo "Error Domain = NSCocoaErrorDomain Code = 259" El archivo no se pudo abrir porque no está en el formato correcto "" cuando intento usar esto. Esto desaparece si ejecuto el full catch en el hilo principal. Encontré esto al consultar la documentación de NSAttributedString: "No se debe llamar al importador HTML desde un subproceso en segundo plano (es decir, el diccionario de opciones incluye documentType con un valor de html). Intentará sincronizar con el subproceso principal, fallará y se acabó el tiempo."
MickeDG

8
Por favor, la rawValuesintaxis NSAttributedString.DocumentReadingOptionKey(rawValue: NSAttributedString.DocumentAttributeKey.documentType.rawValue)y NSAttributedString.DocumentReadingOptionKey(rawValue: NSAttributedString.DocumentAttributeKey.characterEncoding.rawValue)es horrible. Reemplácelo con .documentTypey.characterEncoding
vadian

@MickeDG - ¿Puede explicar qué hizo exactamente para resolver este error? Lo estoy entendiendo esporádicamente.
Ross Barbish

@RossBarbish - Lo siento Ross, esto fue hace mucho tiempo, no puedo recordar los detalles. ¿Has probado lo que sugiero en el comentario anterior, es decir, ejecutar el do catch completo en el hilo principal?
MickeDG

7
extension String{
    func decodeEnt() -> String{
        let encodedData = self.dataUsingEncoding(NSUTF8StringEncoding)!
        let attributedOptions : [String: AnyObject] = [
            NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
            NSCharacterEncodingDocumentAttribute: NSUTF8StringEncoding
        ]
        let attributedString = NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil, error: nil)!

        return attributedString.string
    }
}

let encodedString = "The Weeknd &#8216;King Of The Fall&#8217;"

let foo = encodedString.decodeEnt() /* The Weeknd ‘King Of The Fall’ */

Re "The Weeknd" : ¿No es "The Weekend" ?
Peter Mortensen

El resaltado de sintaxis parece extraño, especialmente la parte de comentario de la última línea. ¿Puedes arreglarlo?
Peter Mortensen

"The Weeknd" es un cantante, y sí, así se escribe su nombre.
wLc

5

Estaba buscando una utilidad Swift 3.0 pura para escapar / no escapar de las referencias de caracteres HTML (es decir, para aplicaciones Swift del lado del servidor en macOS y Linux) pero no encontré ninguna solución integral, así que escribí mi propia implementación: https: //github.com/IBM-Swift/swift-html-entities

El paquete, HTMLEntitiesfunciona con referencias de caracteres con nombre HTML4, así como referencias de caracteres numéricos hexadecimales / dec, y reconocerá referencias de caracteres numéricos especiales según la especificación W3 HTML5 (es decir, no &#x80;debe escapar como el símbolo del Euro (unicode U+20AC) y NO como el unicode carácter para U+0080, y ciertos rangos de referencias de caracteres numéricos deben reemplazarse con el carácter de reemplazo U+FFFDal desempacar).

Ejemplo de uso:

import HTMLEntities

// encode example
let html = "<script>alert(\"abc\")</script>"

print(html.htmlEscape())
// Prints ”&lt;script&gt;alert(&quot;abc&quot;)&lt;/script&gt;"

// decode example
let htmlencoded = "&lt;script&gt;alert(&quot;abc&quot;)&lt;/script&gt;"

print(htmlencoded.htmlUnescape())
// Prints<script>alert(\"abc\")</script>"

Y para el ejemplo de OP:

print("The Weeknd &#8216;King Of The Fall&#8217; [Video Premiere] | @TheWeeknd | #SoPhi ".htmlUnescape())
// prints "The Weeknd ‘King Of The Fall’ [Video Premiere] | @TheWeeknd | #SoPhi "

Editar: HTMLEntitiesahora admite referencias de caracteres con nombre HTML5 a partir de la versión 2.0.0. También se implementa el análisis que cumple con las especificaciones.


1
Esta es la respuesta más genérica que funciona todo el tiempo y no requiere ejecutarse en el hilo principal. Esto funcionará incluso con las cadenas Unicode escapadas de HTML más complejas (como (&nbsp;͡&deg;&nbsp;͜ʖ&nbsp;͡&deg;&nbsp;)), mientras que ninguna de las otras respuestas lo logran.
Stéphane Copin el

5

Swift 4:

La solución total que finalmente funcionó para mí con código HTML y caracteres de nueva línea y comillas simples

extension String {
    var htmlDecoded: String {
        let decoded = try? NSAttributedString(data: Data(utf8), options: [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
            ], documentAttributes: nil).string

        return decoded ?? self
    }
}

Uso:

let yourStringEncoded = yourStringWithHtmlcode.htmlDecoded

Luego tuve que aplicar algunos filtros más para deshacerme de las comillas simples (por ejemplo, no , no , es , etc.) y nuevos caracteres de línea como \n:

var yourNewString = String(yourStringEncoded.filter { !"\n\t\r".contains($0) })
yourNewString = yourNewString.replacingOccurrences(of: "\'", with: "", options: NSString.CompareOptions.literal, range: nil)

Esto es esencialmente una copia de esta otra respuesta . Todo lo que hiciste fue agregar algo de uso que es bastante obvio.
rmaddy

alguien ha votado esta respuesta y la ha encontrado realmente útil, ¿qué le dice eso?
Naishta

@Naishta Te dice que todos tienen opiniones diferentes y eso está bien
Josh Wolff

3

Este sería mi enfoque. Puede agregar el diccionario de entidades desde https://gist.github.com/mwaterfall/25b4a6a06dc3309d9555 que menciona Michael Waterfall.

extension String {
    func htmlDecoded()->String {

        guard (self != "") else { return self }

        var newStr = self

        let entities = [
            "&quot;"    : "\"",
            "&amp;"     : "&",
            "&apos;"    : "'",
            "&lt;"      : "<",
            "&gt;"      : ">",
        ]

        for (name,value) in entities {
            newStr = newStr.stringByReplacingOccurrencesOfString(name, withString: value)
        }
        return newStr
    }
}

Ejemplos utilizados:

let encoded = "this is so &quot;good&quot;"
let decoded = encoded.htmlDecoded() // "this is so "good""

O

let encoded = "this is so &quot;good&quot;".htmlDecoded() // "this is so "good""

1
No me gusta mucho, pero todavía no encontré nada mejor, así que esta es una versión actualizada de la solución Michael Waterfall para Swift 2.0 gist.github.com/jrmgx/3f9f1d330b295cf6b1c6
jrmgx

3

Elegant Swift 4 Solution

Si quieres una cuerda,

myString = String(htmlString: encodedString)

agregue esta extensión a su proyecto:

extension String {

    init(htmlString: String) {
        self.init()
        guard let encodedData = htmlString.data(using: .utf8) else {
            self = htmlString
            return
        }

        let attributedOptions: [NSAttributedString.DocumentReadingOptionKey : Any] = [
           .documentType: NSAttributedString.DocumentType.html,
           .characterEncoding: String.Encoding.utf8.rawValue
        ]

        do {
            let attributedString = try NSAttributedString(data: encodedData,
                                                          options: attributedOptions,
                                                          documentAttributes: nil)
            self = attributedString.string
        } catch {
            print("Error: \(error.localizedDescription)")
            self = htmlString
        }
    }
}

Si desea una cadena NSAttributedString con negrita, cursiva, enlaces, etc.,

textField.attributedText = try? NSAttributedString(htmlString: encodedString)

agregue esta extensión a su proyecto:

extension NSAttributedString {

    convenience init(htmlString html: String) throws {
        try self.init(data: Data(html.utf8), options: [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
            ], documentAttributes: nil)
    }

}

2

Versión var calculada de la respuesta de @yishus

public extension String {
    /// Decodes string with HTML encoding.
    var htmlDecoded: String {
        guard let encodedData = self.data(using: .utf8) else { return self }

        let attributedOptions: [String : Any] = [
            NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType,
            NSCharacterEncodingDocumentAttribute: String.Encoding.utf8.rawValue]

        do {
            let attributedString = try NSAttributedString(data: encodedData,
                                                          options: attributedOptions,
                                                          documentAttributes: nil)
            return attributedString.string
        } catch {
            print("Error: \(error)")
            return self
        }
    }
}

1

Swift 4

func decodeHTML(string: String) -> String? {

    var decodedString: String?

    if let encodedData = string.data(using: .utf8) {
        let attributedOptions: [NSAttributedString.DocumentReadingOptionKey : Any] = [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ]

        do {
            decodedString = try NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil).string
        } catch {
            print("\(error.localizedDescription)")
        }
    }

    return decodedString
}

Una explicación estaría en orden. Por ejemplo, ¿en qué se diferencia de las respuestas anteriores de Swift 4?
Peter Mortensen

1

Swift 4.1 +

var htmlDecoded: String {


    let attributedOptions: [NSAttributedString.DocumentReadingOptionKey : Any] = [

        NSAttributedString.DocumentReadingOptionKey.documentType : NSAttributedString.DocumentType.html,
        NSAttributedString.DocumentReadingOptionKey.characterEncoding : String.Encoding.utf8.rawValue
    ]


    let decoded = try? NSAttributedString(data: Data(utf8), options: attributedOptions
        , documentAttributes: nil).string

    return decoded ?? self
} 

Una explicación estaría en orden. Por ejemplo, ¿en qué se diferencia de las respuestas anteriores? ¿Qué características de Swift 4.1 se utilizan? ¿Funciona solo en Swift 4.1 y no en versiones anteriores? ¿O funcionaría antes de Swift 4.1, digamos en Swift 4.0?
Peter Mortensen

1

Swift 4

extension String {
    var replacingHTMLEntities: String? {
        do {
            return try NSAttributedString(data: Data(utf8), options: [
                .documentType: NSAttributedString.DocumentType.html,
                .characterEncoding: String.Encoding.utf8.rawValue
            ], documentAttributes: nil).string
        } catch {
            return nil
        }
    }
}

Uso simple

let clean = "Weeknd &#8216;King Of The Fall&#8217".replacingHTMLEntities ?? "default value"

Ya puedo escuchar a personas quejándose de mi fuerza sin envolver opcional. Si está investigando la codificación de cadenas HTML y no sabe cómo tratar con las opciones Swift, está muy por delante de usted.
quemeful

Sí, allí es era ( editado 1 de noviembre a las 22:37 y declaró el "uso simple" mucho más difícil de comprender)
quemeful

1

Swift 4

Realmente me gusta la solución usando documentAttributes. Sin embargo, puede ser demasiado lento para analizar archivos y / o uso en celdas de vista de tabla. No puedo creer que Apple no proporcione una solución decente para esto.

Como solución alternativa, encontré esta extensión de cadena en GitHub que funciona perfectamente y es rápida para decodificar.

Entonces, para situaciones en las que la respuesta dada es lenta , vea la solución sugerida en este enlace: https://gist.github.com/mwaterfall/25b4a6a06dc3309d9555

Nota: no analiza las etiquetas HTML.


1

Respuesta actualizada trabajando en Swift 3

extension String {
    init?(htmlEncodedString: String) {
        let encodedData = htmlEncodedString.data(using: String.Encoding.utf8)!
        let attributedOptions = [ NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType]

        guard let attributedString = try? NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil) else {
            return nil
        }
        self.init(attributedString.string)
   }

0

C objetivo

+(NSString *) decodeHTMLEnocdedString:(NSString *)htmlEncodedString {
    if (!htmlEncodedString) {
        return nil;
    }

    NSData *data = [htmlEncodedString dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *attributes = @{NSDocumentTypeDocumentAttribute:     NSHTMLTextDocumentType,
                             NSCharacterEncodingDocumentAttribute:     @(NSUTF8StringEncoding)};
    NSAttributedString *attributedString = [[NSAttributedString alloc]     initWithData:data options:attributes documentAttributes:nil error:nil];
    return [attributedString string];
}

0

Versión Swift 3.0 con conversión de tamaño de fuente real

Normalmente, si convierte directamente contenido HTML en una cadena atribuida, el tamaño de la fuente aumenta. Puede intentar convertir una cadena HTML en una cadena atribuida y viceversa para ver la diferencia.

En cambio, aquí está la conversión del tamaño real que asegura que el tamaño de la fuente no cambie, aplicando la proporción de 0,75 en todas las fuentes:

extension String {
    func htmlAttributedString() -> NSAttributedString? {
        guard let data = self.data(using: String.Encoding.utf16, allowLossyConversion: false) else { return nil }
        guard let attriStr = try? NSMutableAttributedString(
            data: data,
            options: [NSDocumentTypeDocumentAttribute: NSHTMLTextDocumentType],
            documentAttributes: nil) else { return nil }
        attriStr.beginEditing()
        attriStr.enumerateAttribute(NSFontAttributeName, in: NSMakeRange(0, attriStr.length), options: .init(rawValue: 0)) {
            (value, range, stop) in
            if let font = value as? UIFont {
                let resizedFont = font.withSize(font.pointSize * 0.75)
                attriStr.addAttribute(NSFontAttributeName,
                                         value: resizedFont,
                                         range: range)
            }
        }
        attriStr.endEditing()
        return attriStr
    }
}

0

Swift 4

extension String {

    mutating func toHtmlEncodedString() {
        guard let encodedData = self.data(using: .utf8) else {
            return
        }

        let attributedOptions: [NSAttributedString.DocumentReadingOptionKey : Any] = [
            NSAttributedString.DocumentReadingOptionKey(rawValue: NSAttributedString.DocumentAttributeKey.documentType.rawValue): NSAttributedString.DocumentType.html,
            NSAttributedString.DocumentReadingOptionKey(rawValue: NSAttributedString.DocumentAttributeKey.characterEncoding.rawValue): String.Encoding.utf8.rawValue
        ]

        do {
            let attributedString = try NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil)
            self = attributedString.string
        }
        catch {
            print("Error: \(error)")
        }
    }

Por favor, la rawValuesintaxis NSAttributedString.DocumentReadingOptionKey(rawValue: NSAttributedString.DocumentAttributeKey.documentType.rawValue)y NSAttributedString.DocumentReadingOptionKey(rawValue: NSAttributedString.DocumentAttributeKey.characterEncoding.rawValue)es horrible. Reemplácelo con .documentTypey.characterEncoding
vadian

El rendimiento de esta solución es horrible. Tal vez esté bien para caes separados, no se recomienda analizar archivos.
Vincent

0

Eche un vistazo a HTMLString: una biblioteca escrita en Swift que permite que su programa agregue y elimine entidades HTML en cadenas

Para completar, copié las características principales del sitio:

  • Agrega entidades para codificaciones ASCII y UTF-8 / UTF-16
  • Elimina más de 2100 entidades con nombre (como &)
  • Admite eliminar entidades decimales y hexadecimales
  • Diseñado para admitir grupos de grafemas extendidos Swift (→ 100% a prueba de emoji)
  • Unidad completamente probada
  • Rápido
  • Documentado
  • Compatible con Objective-C

0

Versión Swift 5.1

import UIKit

extension String {

    init(htmlEncodedString: String) {
        self.init()
        guard let encodedData = htmlEncodedString.data(using: .utf8) else {
            self = htmlEncodedString
            return
        }

        let attributedOptions: [NSAttributedString.DocumentReadingOptionKey : Any] = [
            .documentType: NSAttributedString.DocumentType.html,
            .characterEncoding: String.Encoding.utf8.rawValue
        ]

        do {
            let attributedString = try NSAttributedString(data: encodedData, options: attributedOptions, documentAttributes: nil)
            self = attributedString.string
        } 
        catch {
            print("Error: \(error)")
            self = htmlEncodedString
        }
    }
}

Además, si desea extraer la fecha, las imágenes, los metadatos, el título y la descripción, puede usar mi pod llamado:

] [1].

Kit de legibilidad


¿Qué es lo que no lo haría funcionar en algunas versiones anteriores, Swift 5.0, Swift 4.1, Swift 4.0, etc.?
Peter Mortensen

Encontré un error al decodificar una cadena usando collectionViews
Tung Vu Duc

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.