Décodage de caractères HTML dans Objective-C / Cocoa Touch

103

Tout d'abord, j'ai trouvé ceci: Objective C HTML escape / unescape , mais cela ne fonctionne pas pour moi.

Mes caractères encodés (proviennent d'un flux RSS, btw) ressemblent à ceci: &

J'ai cherché partout sur le net et trouvé des discussions connexes, mais pas de correctif pour mon encodage particulier, je pense qu'ils sont appelés caractères hexadécimaux.

Treznik
la source
3
Ce commentaire intervient six mois après la question initiale, c'est donc plus pour ceux qui tombent sur cette question à la recherche d'une réponse et d'une solution. Une question très similaire s'est posée récemment à laquelle j'ai répondu à stackoverflow.com/questions/2254862/ ... Il utilise RegexKitLite et Blocks pour effectuer une recherche et remplacer le &#...;dans une chaîne avec son caractère équivalent.
johne
Qu'est-ce qui spécifiquement «ne fonctionne pas»? Je ne vois rien dans cette question qui ne fasse pas double emploi avec la question précédente.
Peter Hosey
C'est décimal. Hexadécimal est 8.
kennytm
La différence entre décimal et hexadécimal étant que le nombre décimal est en base 10, alors que l'hexadécimal est en base 16. «38» est un nombre différent dans chaque base; en base 10, c'est 3 × 10 + 8 × 1 = trente-huit, alors qu'en base-16, c'est 3 × 16 + 8 × 1 = cinquante-six. Les chiffres supérieurs sont des (multiples de) puissances supérieures de la base; le chiffre entier le plus bas est la base 0 (= 1), le chiffre suivant le plus élevé est la base 1 (= base), le suivant est la base ** 2 (= base * base), etc. C'est l'exposant à l'œuvre.
Peter Hosey

Réponses:

46

Celles-ci sont appelées références d'entités de caractères . Lorsqu'elles prennent la forme de, &#<number>;elles sont appelées des références d'entités numériques . Fondamentalement, c'est une représentation sous forme de chaîne de l'octet qui doit être remplacé. Dans le cas de &#038;, il représente le caractère avec la valeur 38 dans le schéma de codage de caractères ISO-8859-1, qui est &.

La raison pour laquelle l'esperluette doit être encodée en RSS est qu'il s'agit d'un caractère spécial réservé.

Ce que vous devez faire est d'analyser la chaîne et de remplacer les entités par un octet correspondant à la valeur entre &#et ;. Je ne connais pas d'excellents moyens de le faire dans l'objectif C, mais cette question de dépassement de pile pourrait être d'une certaine aide.

Edit: Depuis que j'ai répondu à cela il y a environ deux ans, il existe d'excellentes solutions; voir la réponse de @Michael Waterfall ci-dessous.

Ponts de Matt
la source
2
+1 J'étais sur le point de soumettre exactement la même réponse (y compris les mêmes liens, rien de moins!)
e.James
"Fondamentalement, c'est une représentation sous forme de chaîne de l'octet qui doit être remplacé." Plus comme un personnage. C'est du texte, pas des données; lors de la conversion du texte en données, le caractère peut occuper plusieurs octets, selon le caractère et le codage.
Peter Hosey le
Merci pour la réponse. Vous avez dit "il représente le caractère avec la valeur 38 dans le schéma de codage de caractères ISO-8859-1, qui est &". Êtes-vous sûr de cela? Avez-vous un lien vers une table de caractères de ce type? Parce que d'après ce dont je me souviens, il s'agissait d'une simple citation.
treznik
en.wikipedia.org/wiki/ISO/IEC_8859-1#ISO-8859-1 ou tapez simplement & # 038; dans google.
Matt Bridges
et que dire de & amp; ou & copy; symboles?
vokilam
162

Consultez ma catégorie NSString pour HTML . Voici les méthodes disponibles:

- (NSString *)stringByConvertingHTMLToPlainText;
- (NSString *)stringByDecodingHTMLEntities;
- (NSString *)stringByEncodingHTMLEntities;
- (NSString *)stringWithNewLinesAsBRs;
- (NSString *)stringByRemovingNewLinesAndWhitespace;
Michael Waterfall
la source
3
Mec, excellentes fonctions. Votre méthode stringByDecodingXMLEntities a fait ma journée! Merci!
Brian Moeskau
3
Pas de problème;) Heureux que vous l'ayez trouvé utile!
Michael Waterfall
4
Après quelques heures de recherche, je sais que c'est la seule façon de le faire qui fonctionne vraiment. NSString est en retard pour une méthode de chaîne qui peut le faire. Bien joué.
Adam Eberbach
1
J'ai trouvé (2) sur la licence de Michael trop restrictif pour mon cas d'utilisation, j'ai donc utilisé la solution de Nikita. L'inclusion de trois fichiers sous licence Apache-2.0 de google toolbox fonctionne très bien pour moi.
jaime
10
La mise à jour du code pour ARC serait pratique. Xcode lance une tonne d'erreurs et d'avertissements ARC lors de la compilation
Matej
52

Celui de Daniel est fondamentalement très sympa, et j'y ai corrigé quelques problèmes:

  1. supprimé le caractère de saut pour NSSCanner (sinon les espaces entre deux entités continues seraient ignorés

    [scanner setCharactersToBeSkipped: nil];

  2. correction de l'analyse quand il y a des symboles isolés '&' (je ne sais pas quelle est la sortie 'correcte' pour cela, je viens de la comparer à Firefox):

par exemple

    &#ABC DF & B&#39;  & C&#39; Items (288)

voici le code modifié:

- (NSString *)stringByDecodingXMLEntities {
    NSUInteger myLength = [self length];
    NSUInteger ampIndex = [self rangeOfString:@"&" options:NSLiteralSearch].location;

    // Short-circuit if there are no ampersands.
    if (ampIndex == NSNotFound) {
        return self;
    }
    // Make result string with some extra capacity.
    NSMutableString *result = [NSMutableString stringWithCapacity:(myLength * 1.25)];

    // First iteration doesn't need to scan to & since we did that already, but for code simplicity's sake we'll do it again with the scanner.
    NSScanner *scanner = [NSScanner scannerWithString:self];

    [scanner setCharactersToBeSkipped:nil];

    NSCharacterSet *boundaryCharacterSet = [NSCharacterSet characterSetWithCharactersInString:@" \t\n\r;"];

    do {
        // Scan up to the next entity or the end of the string.
        NSString *nonEntityString;
        if ([scanner scanUpToString:@"&" intoString:&nonEntityString]) {
            [result appendString:nonEntityString];
        }
        if ([scanner isAtEnd]) {
            goto finish;
        }
        // Scan either a HTML or numeric character entity reference.
        if ([scanner scanString:@"&amp;" intoString:NULL])
            [result appendString:@"&"];
        else if ([scanner scanString:@"&apos;" intoString:NULL])
            [result appendString:@"'"];
        else if ([scanner scanString:@"&quot;" intoString:NULL])
            [result appendString:@"\""];
        else if ([scanner scanString:@"&lt;" intoString:NULL])
            [result appendString:@"<"];
        else if ([scanner scanString:@"&gt;" intoString:NULL])
            [result appendString:@">"];
        else if ([scanner scanString:@"&#" intoString:NULL]) {
            BOOL gotNumber;
            unsigned charCode;
            NSString *xForHex = @"";

            // Is it hex or decimal?
            if ([scanner scanString:@"x" intoString:&xForHex]) {
                gotNumber = [scanner scanHexInt:&charCode];
            }
            else {
                gotNumber = [scanner scanInt:(int*)&charCode];
            }

            if (gotNumber) {
                [result appendFormat:@"%C", (unichar)charCode];

                [scanner scanString:@";" intoString:NULL];
            }
            else {
                NSString *unknownEntity = @"";

                [scanner scanUpToCharactersFromSet:boundaryCharacterSet intoString:&unknownEntity];


                [result appendFormat:@"&#%@%@", xForHex, unknownEntity];

                //[scanner scanUpToString:@";" intoString:&unknownEntity];
                //[result appendFormat:@"&#%@%@;", xForHex, unknownEntity];
                NSLog(@"Expected numeric character entity but got &#%@%@;", xForHex, unknownEntity);

            }

        }
        else {
            NSString *amp;

            [scanner scanString:@"&" intoString:&amp];  //an isolated & symbol
            [result appendString:amp];

            /*
            NSString *unknownEntity = @"";
            [scanner scanUpToString:@";" intoString:&unknownEntity];
            NSString *semicolon = @"";
            [scanner scanString:@";" intoString:&semicolon];
            [result appendFormat:@"%@%@", unknownEntity, semicolon];
            NSLog(@"Unsupported XML character entity %@%@", unknownEntity, semicolon);
             */
        }

    }
    while (![scanner isAtEnd]);

finish:
    return result;
}
Walty Yeung
la source
Cela devrait être la réponse définitive à la question !! Merci!
boliva
Cela a très bien fonctionné. Malheureusement, le code de la réponse la mieux notée ne fonctionne plus en raison de problèmes d'ARC, mais c'est le cas.
Ted Kulp
@TedKulp cela fonctionne très bien, il vous suffit de désactiver l'ARC par fichier. stackoverflow.com/questions/6646052/…
Kyle
Je vous ferais plaisir deux fois si je le pouvais.
Kibitz503
Traduction rapide pour les personnes qui consultent toujours cette question en 2016+: stackoverflow.com/a/35303635/1153630
Max Chuquimia
46

À partir d'iOS 7, vous pouvez décoder les caractères HTML de manière native en utilisant un NSAttributedStringavec l' NSHTMLTextDocumentTypeattribut:

NSString *htmlString = @"&#63743; &amp; &#38; &lt; &gt; &trade; &copy; &hearts; &clubs; &spades; &diams;";
NSData *stringData = [htmlString dataUsingEncoding:NSUTF8StringEncoding];

NSDictionary *options = @{NSDocumentTypeDocumentAttribute:NSHTMLTextDocumentType};
NSAttributedString *decodedString;
decodedString = [[NSAttributedString alloc] initWithData:stringData
                                                 options:options
                                      documentAttributes:NULL
                                                   error:NULL];

La chaîne attribuée décodée sera maintenant affichée comme suit:  & & <> ™ © ♥ ♣ ♠ ♦.

Remarque: cela ne fonctionnera que s'il est appelé sur le thread principal.

Bryan Luby
la source
6
meilleure réponse si vous n'avez pas besoin de prendre en charge iOS 6 et plus ancien
jcesarmobile
1
non, pas le meilleur si quelqu'un veut l'encoder sur un thread bg; O
badeleux
4
Cela fonctionnait pour décoder une entité, mais cela gâchait également un tiret non codé.
Andrew
Ceci est forcé de se produire sur le thread principal. Donc, vous ne voudrez probablement pas faire cela si vous n'êtes pas obligé.
Keith Smiley
Il bloque simplement l'interface graphique lorsqu'il s'agit de UITableView. Par conséquent, ne fonctionne pas correctement.
Asif Bilal
35

Personne ne semble mentionner l'une des options les plus simples: Google Toolbox pour Mac
(malgré le nom, cela fonctionne aussi sur iOS.)

https://github.com/google/google-toolbox-for-mac/blob/master/Foundation/GTMNSString%2BHTML.h

/// Get a string where internal characters that are escaped for HTML are unescaped 
//
///  For example, '&amp;' becomes '&'
///  Handles &#32; and &#x32; cases as well
///
//  Returns:
//    Autoreleased NSString
//
- (NSString *)gtm_stringByUnescapingFromHTML;

Et je devais inclure seulement trois fichiers dans le projet: en-tête, implémentation et GTMDefines.h.

Nikita Rybak
la source
J'ai inclus ces trois scripts, mais comment puis-je les utiliser maintenant?
Borut Tomazin
@ borut-t [myString gtm_stringByUnescapingFromHTML]
Nikita Rybak
2
J'ai choisi de n'inclure que ces trois fichiers, j'ai donc dû le faire pour le rendre compatible avec arc: code.google.com/p/google-toolbox-for-mac/wiki/ARC_Compatibility
jaime
Je dois dire que c'est de loin la solution la plus simple et la plus légère
lensovet
J'aurais aimé que cela fonctionne complètement. Il semble en sauter plusieurs dans mes cordes.
Joseph Toronto
17

Je devrais poster ceci sur GitHub ou quelque chose comme ça. Cela entre dans une catégorie de NSString, utilise NSScannerpour l'implémentation et gère les entités de caractères numériques hexadécimaux et décimaux ainsi que les entités symboliques habituelles.

En outre, il gère les chaînes mal formées (lorsque vous avez un & suivi d'une séquence de caractères non valide) de manière relativement gracieuse, ce qui s'est avéré crucial dans mon application publiée qui utilise ce code.

- (NSString *)stringByDecodingXMLEntities {
    NSUInteger myLength = [self length];
    NSUInteger ampIndex = [self rangeOfString:@"&" options:NSLiteralSearch].location;

    // Short-circuit if there are no ampersands.
    if (ampIndex == NSNotFound) {
        return self;
    }
    // Make result string with some extra capacity.
    NSMutableString *result = [NSMutableString stringWithCapacity:(myLength * 1.25)];

    // First iteration doesn't need to scan to & since we did that already, but for code simplicity's sake we'll do it again with the scanner.
    NSScanner *scanner = [NSScanner scannerWithString:self];
    do {
        // Scan up to the next entity or the end of the string.
        NSString *nonEntityString;
        if ([scanner scanUpToString:@"&" intoString:&nonEntityString]) {
            [result appendString:nonEntityString];
        }
        if ([scanner isAtEnd]) {
            goto finish;
        }
        // Scan either a HTML or numeric character entity reference.
        if ([scanner scanString:@"&amp;" intoString:NULL])
            [result appendString:@"&"];
        else if ([scanner scanString:@"&apos;" intoString:NULL])
            [result appendString:@"'"];
        else if ([scanner scanString:@"&quot;" intoString:NULL])
            [result appendString:@"\""];
        else if ([scanner scanString:@"&lt;" intoString:NULL])
            [result appendString:@"<"];
        else if ([scanner scanString:@"&gt;" intoString:NULL])
            [result appendString:@">"];
        else if ([scanner scanString:@"&#" intoString:NULL]) {
            BOOL gotNumber;
            unsigned charCode;
            NSString *xForHex = @"";

            // Is it hex or decimal?
            if ([scanner scanString:@"x" intoString:&xForHex]) {
                gotNumber = [scanner scanHexInt:&charCode];
            }
            else {
                gotNumber = [scanner scanInt:(int*)&charCode];
            }
            if (gotNumber) {
                [result appendFormat:@"%C", charCode];
            }
            else {
                NSString *unknownEntity = @"";
                [scanner scanUpToString:@";" intoString:&unknownEntity];
                [result appendFormat:@"&#%@%@;", xForHex, unknownEntity];
                NSLog(@"Expected numeric character entity but got &#%@%@;", xForHex, unknownEntity);
            }
            [scanner scanString:@";" intoString:NULL];
        }
        else {
            NSString *unknownEntity = @"";
            [scanner scanUpToString:@";" intoString:&unknownEntity];
            NSString *semicolon = @"";
            [scanner scanString:@";" intoString:&semicolon];
            [result appendFormat:@"%@%@", unknownEntity, semicolon];
            NSLog(@"Unsupported XML character entity %@%@", unknownEntity, semicolon);
        }
    }
    while (![scanner isAtEnd]);

finish:
    return result;
}
Daniel Dickison
la source
Morceau de code très utile, mais il présente quelques problèmes qui ont été résolus par Walty. Merci d'avoir partagé!
Michael Waterfall
connaissez-vous un moyen d'afficher les symboles lambda, mu, nu, pi en décodant leurs entités XML comme & micro; ... ect ????
chinthakad
Vous devriez éviter d'utiliser gotos comme style de code terrible. Vous devez remplacer la ligne goto finish;par break;.
Stunner
4

C'est ainsi que je le fais en utilisant le framework RegexKitLite :

-(NSString*) decodeHtmlUnicodeCharacters: (NSString*) html {
NSString* result = [html copy];
NSArray* matches = [result arrayOfCaptureComponentsMatchedByRegex: @"\\&#([\\d]+);"];

if (![matches count]) 
    return result;

for (int i=0; i<[matches count]; i++) {
    NSArray* array = [matches objectAtIndex: i];
    NSString* charCode = [array objectAtIndex: 1];
    int code = [charCode intValue];
    NSString* character = [NSString stringWithFormat:@"%C", code];
    result = [result stringByReplacingOccurrencesOfString: [array objectAtIndex: 0]
                                               withString: character];      
}   
return result;  

}

J'espère que cela aidera quelqu'un.

sucre réel
la source
4

vous pouvez utiliser uniquement cette fonction pour résoudre ce problème.

+ (NSString*) decodeHtmlUnicodeCharactersToString:(NSString*)str
{
    NSMutableString* string = [[NSMutableString alloc] initWithString:str];  // #&39; replace with '
    NSString* unicodeStr = nil;
    NSString* replaceStr = nil;
    int counter = -1;

    for(int i = 0; i < [string length]; ++i)
    {
        unichar char1 = [string characterAtIndex:i];    
        for (int k = i + 1; k < [string length] - 1; ++k)
        {
            unichar char2 = [string characterAtIndex:k];    

            if (char1 == '&'  && char2 == '#' ) 
            {   
                ++counter;
                unicodeStr = [string substringWithRange:NSMakeRange(i + 2 , 2)];    
                // read integer value i.e, 39
                replaceStr = [string substringWithRange:NSMakeRange (i, 5)];     //     #&39;
                [string replaceCharactersInRange: [string rangeOfString:replaceStr] withString:[NSString stringWithFormat:@"%c",[unicodeStr intValue]]];
                break;
            }
        }
    }
    [string autorelease];

    if (counter > 1)
        return  [self decodeHtmlUnicodeCharactersToString:string]; 
    else
        return string;
}
Krishna Gupta
la source
2

Voici une version Swift de la réponse de Walty Yeung :

extension String {
    static private let mappings = ["&quot;" : "\"","&amp;" : "&", "&lt;" : "<", "&gt;" : ">","&nbsp;" : " ","&iexcl;" : "¡","&cent;" : "¢","&pound;" : " £","&curren;" : "¤","&yen;" : "¥","&brvbar;" : "¦","&sect;" : "§","&uml;" : "¨","&copy;" : "©","&ordf;" : " ª","&laquo" : "«","&not" : "¬","&reg" : "®","&macr" : "¯","&deg" : "°","&plusmn" : "±","&sup2; " : "²","&sup3" : "³","&acute" : "´","&micro" : "µ","&para" : "¶","&middot" : "·","&cedil" : "¸","&sup1" : "¹","&ordm" : "º","&raquo" : "»&","frac14" : "¼","&frac12" : "½","&frac34" : "¾","&iquest" : "¿","&times" : "×","&divide" : "÷","&ETH" : "Ð","&eth" : "ð","&THORN" : "Þ","&thorn" : "þ","&AElig" : "Æ","&aelig" : "æ","&OElig" : "Œ","&oelig" : "œ","&Aring" : "Å","&Oslash" : "Ø","&Ccedil" : "Ç","&ccedil" : "ç","&szlig" : "ß","&Ntilde;" : "Ñ","&ntilde;":"ñ",]

    func stringByDecodingXMLEntities() -> String {

        guard let _ = self.rangeOfString("&", options: [.LiteralSearch]) else {
            return self
        }

        var result = ""

        let scanner = NSScanner(string: self)
        scanner.charactersToBeSkipped = nil

        let boundaryCharacterSet = NSCharacterSet(charactersInString: " \t\n\r;")

        repeat {
            var nonEntityString: NSString? = nil

            if scanner.scanUpToString("&", intoString: &nonEntityString) {
                if let s = nonEntityString as? String {
                    result.appendContentsOf(s)
                }
            }

            if scanner.atEnd {
                break
            }

            var didBreak = false
            for (k,v) in String.mappings {
                if scanner.scanString(k, intoString: nil) {
                    result.appendContentsOf(v)
                    didBreak = true
                    break
                }
            }

            if !didBreak {

                if scanner.scanString("&#", intoString: nil) {

                    var gotNumber = false
                    var charCodeUInt: UInt32 = 0
                    var charCodeInt: Int32 = -1
                    var xForHex: NSString? = nil

                    if scanner.scanString("x", intoString: &xForHex) {
                        gotNumber = scanner.scanHexInt(&charCodeUInt)
                    }
                    else {
                        gotNumber = scanner.scanInt(&charCodeInt)
                    }

                    if gotNumber {
                        let newChar = String(format: "%C", (charCodeInt > -1) ? charCodeInt : charCodeUInt)
                        result.appendContentsOf(newChar)
                        scanner.scanString(";", intoString: nil)
                    }
                    else {
                        var unknownEntity: NSString? = nil
                        scanner.scanUpToCharactersFromSet(boundaryCharacterSet, intoString: &unknownEntity)
                        let h = xForHex ?? ""
                        let u = unknownEntity ?? ""
                        result.appendContentsOf("&#\(h)\(u)")
                    }
                }
                else {
                    scanner.scanString("&", intoString: nil)
                    result.appendContentsOf("&")
                }
            }

        } while (!scanner.atEnd)

        return result
    }
}
Max Chuquimia
la source
1

En fait, le formidable framework MWFeedParser de Michael Waterfall (référé à sa réponse) a été bifurqué par rmchaara qui l'a mis à jour avec le support ARC!

Vous pouvez le trouver dans Github ici

Cela fonctionne vraiment très bien, j'ai utilisé la méthode stringByDecodingHTMLEntities et fonctionne parfaitement.

angelos.p
la source
Cela résout les problèmes d'ARC - mais introduit quelques avertissements. Je pense qu'il est prudent de les ignorer?
Robert J. Clegg
0

Comme si vous aviez besoin d'une autre solution! Celui-ci est assez simple et assez efficace:

@interface NSString (NSStringCategory)
- (NSString *) stringByReplacingISO8859Codes;
@end


@implementation NSString (NSStringCategory)
- (NSString *) stringByReplacingISO8859Codes
{
    NSString *dataString = self;
    do {
        //*** See if string contains &# prefix
        NSRange range = [dataString rangeOfString: @"&#" options: NSRegularExpressionSearch];
        if (range.location == NSNotFound) {
            break;
        }
        //*** Get the next three charaters after the prefix
        NSString *isoHex = [dataString substringWithRange: NSMakeRange(range.location + 2, 3)];
        //*** Create the full code for replacement
        NSString *isoString = [NSString stringWithFormat: @"&#%@;", isoHex];
        //*** Convert to decimal integer
        unsigned decimal = 0;
        NSScanner *scanner = [NSScanner scannerWithString: [NSString stringWithFormat: @"0%@", isoHex]];
        [scanner scanHexInt: &decimal];
        //*** Use decimal code to get unicode character
        NSString *unicode = [NSString stringWithFormat:@"%C", decimal];
        //*** Replace all occurences of this code in the string
        dataString = [dataString stringByReplacingOccurrencesOfString: isoString withString: unicode];
    } while (TRUE); //*** Loop until we hit the NSNotFound

    return dataString;
}
@end
mpemburn
la source
0

Si vous avez la référence d'entité de caractère sous forme de chaîne, par exemple @"2318", vous pouvez extraire une NSString recodée avec le caractère Unicode correct en utilisant strtoul;

NSString *unicodePoint = @"2318"
unichar iconChar = (unichar) strtoul(unicodePoint.UTF8String, NULL, 16);
NSString *recoded = [NSString stringWithFormat:@"%C", iconChar];
NSLog(@"recoded: %@", recoded");
// prints out "recoded: ⌘"
Henrik Hartz
la source
0

Version Swift 3 de la réponse de Jugale

extension String {
    static private let mappings = ["&quot;" : "\"","&amp;" : "&", "&lt;" : "<", "&gt;" : ">","&nbsp;" : " ","&iexcl;" : "¡","&cent;" : "¢","&pound;" : " £","&curren;" : "¤","&yen;" : "¥","&brvbar;" : "¦","&sect;" : "§","&uml;" : "¨","&copy;" : "©","&ordf;" : " ª","&laquo" : "«","&not" : "¬","&reg" : "®","&macr" : "¯","&deg" : "°","&plusmn" : "±","&sup2; " : "²","&sup3" : "³","&acute" : "´","&micro" : "µ","&para" : "¶","&middot" : "·","&cedil" : "¸","&sup1" : "¹","&ordm" : "º","&raquo" : "»&","frac14" : "¼","&frac12" : "½","&frac34" : "¾","&iquest" : "¿","&times" : "×","&divide" : "÷","&ETH" : "Ð","&eth" : "ð","&THORN" : "Þ","&thorn" : "þ","&AElig" : "Æ","&aelig" : "æ","&OElig" : "Œ","&oelig" : "œ","&Aring" : "Å","&Oslash" : "Ø","&Ccedil" : "Ç","&ccedil" : "ç","&szlig" : "ß","&Ntilde;" : "Ñ","&ntilde;":"ñ",]

    func stringByDecodingXMLEntities() -> String {

        guard let _ = self.range(of: "&", options: [.literal]) else {
            return self
        }

        var result = ""

        let scanner = Scanner(string: self)
        scanner.charactersToBeSkipped = nil

        let boundaryCharacterSet = CharacterSet(charactersIn: " \t\n\r;")

        repeat {
            var nonEntityString: NSString? = nil

            if scanner.scanUpTo("&", into: &nonEntityString) {
                if let s = nonEntityString as? String {
                    result.append(s)
                }
            }

            if scanner.isAtEnd {
                break
            }

            var didBreak = false
            for (k,v) in String.mappings {
                if scanner.scanString(k, into: nil) {
                    result.append(v)
                    didBreak = true
                    break
                }
            }

            if !didBreak {

                if scanner.scanString("&#", into: nil) {

                    var gotNumber = false
                    var charCodeUInt: UInt32 = 0
                    var charCodeInt: Int32 = -1
                    var xForHex: NSString? = nil

                    if scanner.scanString("x", into: &xForHex) {
                        gotNumber = scanner.scanHexInt32(&charCodeUInt)
                    }
                    else {
                        gotNumber = scanner.scanInt32(&charCodeInt)
                    }

                    if gotNumber {
                        let newChar = String(format: "%C", (charCodeInt > -1) ? charCodeInt : charCodeUInt)
                        result.append(newChar)
                        scanner.scanString(";", into: nil)
                    }
                    else {
                        var unknownEntity: NSString? = nil
                        scanner.scanUpToCharacters(from: boundaryCharacterSet, into: &unknownEntity)
                        let h = xForHex ?? ""
                        let u = unknownEntity ?? ""
                        result.append("&#\(h)\(u)")
                    }
                }
                else {
                    scanner.scanString("&", into: nil)
                    result.append("&")
                }
            }

        } while (!scanner.isAtEnd)

        return result
    }
}
Xzya
la source