NSString tokenizuje w Objective-C

144

Jaki jest najlepszy sposób na tokenizację / podział NSString w Objective-C?

Ned Batchelder
źródło

Odpowiedzi:

274

Znalazłem to na http://borkware.com/quickies/one?topic=NSString (przydatny link):

NSString *string = @"oop:ack:bork:greeble:ponies";
NSArray *chunks = [string componentsSeparatedByString: @":"];

Mam nadzieję że to pomoże!

Adam

Adam Alexander
źródło
39
Odnosząc się do przyszłych czytelników, chciałbym zauważyć, że jest odwrotnie [anArray componentsJoinedByString:@":"];.
Ivan Vučica
2
dzięki, ale jak podzielić NSString, który jest oddzielony większą liczbą tokenów? (Jeśli wiesz, co mam na myśli, mój angielski nie jest zbyt dobry) @Adam
11684
2
@Adam, myślę, że chciałeś componentsSeparatedByCharactersInSet. Zobacz odpowiedź poniżej.
Wienke
32

Wszyscy o tym wspominali, componentsSeparatedByString:ale możesz także użyć CFStringTokenizer(pamiętaj, że NSStringi CFStringsą wymienne), które również tokenizują języki naturalne (takie jak chiński / japoński, które nie dzielą słów na spacje).

Matt Gallagher
źródło
7
W systemie Mac OS X 10.6 i nowszych NSString ma metody, enumerateLinesUsingBlock:a enumerateSubstringsInRange:options:usingBlock:ten ostatni jest wersją CFStringTokenizer opartą na blokach. developer.apple.com/mac/library/documentation/Cocoa/Reference/… : developer.apple.com/mac/library/documentation/Cocoa/Reference/… :
Peter Hosey
1
Te enumeratesą dostępne w iOS 4 i nowszych metod, zbyt.
bugloaf
21

Jeśli chcesz tylko podzielić ciąg, użyj -[NSString componentsSeparatedByString:]. Aby uzyskać bardziej złożoną tokenizację, użyj klasy NSScanner.

Chris Hanson
źródło
7

Jeśli Twoje potrzeby w zakresie tokenizacji są bardziej złożone, zapoznaj się z moim zestawem narzędzi do tokenizacji / analizowania ciągu Cocoa String typu open source: ParseKit:

http://parsekit.com

W przypadku prostego dzielenia ciągów za pomocą znaku separatora (takiego jak „:”), ParseKit byłby zdecydowanie przesadą. Ale znowu, w przypadku złożonych potrzeb tokenizacji, ParseKit jest niezwykle wydajny / elastyczny.

Zobacz także dokumentację dotyczącą tokenizacji ParseKit .

Todd Ditchendorf
źródło
Czy to nadal działa? Wypróbowałem to i wyskoczyło kilka błędów, których nie chcę sam naprawić.
griotspeak
Hm? Żywy? Projekt ParseKit jest aktywnie utrzymywany, tak. Jednak komentarze tutaj nie są właściwym miejscem do zgłaszania błędów w projekcie. Jest dostępny zarówno w Google Code, jak i Github, jeśli chcesz zgłaszać błędy.
Todd Ditchendorf,
Brzmi dobrze, ale teraz nie mogę usunąć mojego głosu przeciw, dopóki w jakiś sposób nie zmienisz odpowiedzi (regulamin strony). Być może mógłbyś zauważyć, na jakich wersjach to działa, czy używa ARC itp.? Lub możesz po prostu dodać gdzieś spację, to zależy od ciebie :)
Dan Rosenstark
6

Jeśli chcesz tokenizować wiele znaków, możesz użyć NSString componentsSeparatedByCharactersInSet. NSCharacterSet ma kilka przydatnych gotowych zestawów, takich jak whitespaceCharacterSetiillegalCharacterSet . I ma inicjatory dla zakresów Unicode.

Możesz także łączyć zestawy znaków i używać ich do tokenizacji, na przykład:

// Tokenize sSourceEntityName on both whitespace and punctuation.
NSMutableCharacterSet *mcharsetWhitePunc = [[NSCharacterSet whitespaceAndNewlineCharacterSet] mutableCopy];
[mcharsetWhitePunc formUnionWithCharacterSet:[NSCharacterSet punctuationCharacterSet]];
NSArray *sarrTokenizedName = [self.sSourceEntityName componentsSeparatedByCharactersInSet:mcharsetWhitePunc];
[mcharsetWhitePunc release];

Należy pamiętać, że componentsSeparatedByCharactersInSetjeśli napotka więcej niż jeden element członkowski charSet w rzędzie, spowoduje to powstanie pustych ciągów, więc warto przetestować długość mniejszą niż 1.

Wienke
źródło
Nie dotyczy języków, w których białe znaki w ogóle nie oddzielają wszystkich tokenów logicznych. Złe rozwiązanie.
uchuugaka,
@uchuugaka W takim przypadku użyjesz innego zestawu lub zestawów znaków do tokenizacji. Używam tylko konkretnych przykładów, aby zilustrować ogólną koncepcję.
Wienke
5

Jeśli chcesz tokenizować ciąg znaków na wyszukiwane hasła, zachowując „cytowane frazy”, oto NSStringkategoria uwzględniająca różne typy par cudzysłowu:"" '' ‘’ “”

Stosowanie:

NSArray *terms = [@"This is my \"search phrase\" I want to split" searchTerms];
// results in: ["This", "is", "my", "search phrase", "I", "want", "to", "split"]

Kod:

@interface NSString (Search)
- (NSArray *)searchTerms;
@end

@implementation NSString (Search)

- (NSArray *)searchTerms {

    // Strip whitespace and setup scanner
    NSCharacterSet *whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet];
    NSString *searchString = [self stringByTrimmingCharactersInSet:whitespace];
    NSScanner *scanner = [NSScanner scannerWithString:searchString];
    [scanner setCharactersToBeSkipped:nil]; // we'll handle whitespace ourselves

    // A few types of quote pairs to check
    NSDictionary *quotePairs = @{@"\"": @"\"",
                                 @"'": @"'",
                                 @"\u2018": @"\u2019",
                                 @"\u201C": @"\u201D"};

    // Scan
    NSMutableArray *results = [[NSMutableArray alloc] init];
    NSString *substring = nil;
    while (scanner.scanLocation < searchString.length) {
        // Check for quote at beginning of string
        unichar unicharacter = [self characterAtIndex:scanner.scanLocation];
        NSString *startQuote = [NSString stringWithFormat:@"%C", unicharacter];
        NSString *endQuote = [quotePairs objectForKey:startQuote];
        if (endQuote != nil) { // if it's a valid start quote we'll have an end quote
            // Scan quoted phrase into substring (skipping start & end quotes)
            [scanner scanString:startQuote intoString:nil];
            [scanner scanUpToString:endQuote intoString:&substring];
            [scanner scanString:endQuote intoString:nil];
        } else {
            // Single word that is non-quoted
            [scanner scanUpToCharactersFromSet:whitespace intoString:&substring];
        }
        // Process and add the substring to results
        if (substring) {
            substring = [substring stringByTrimmingCharactersInSet:whitespace];
            if (substring.length) [results addObject:substring];
        }
        // Skip to next word
        [scanner scanCharactersFromSet:whitespace intoString:nil];
    }

    // Return non-mutable array
    return results.copy;

}

@end
Michael Waterfall
źródło
1

Jeśli szukasz podziału funkcji lingwistycznych w ciągu (słowa, akapity, znaki, zdania i wiersze), użyj wyliczania ciągów:

NSString * string = @" \n word1!    word2,%$?'/word3.word4   ";

[string enumerateSubstringsInRange:NSMakeRange(0, string.length)
                           options:NSStringEnumerationByWords
                        usingBlock:
 ^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
     NSLog(@"Substring: '%@'", substring);
 }];

 // Logs:
 // Substring: 'word1'
 // Substring: 'word2'
 // Substring: 'word3'
 // Substring: 'word4' 

Ten interfejs API działa z innymi językami, w których spacje nie zawsze są separatorem (np. Japoński). Używanie NSStringEnumerationByComposedCharacterSequencesjest również właściwym sposobem wyliczania znaków, ponieważ wiele znaków innych niż zachodnie ma więcej niż jeden bajt długości.

Robert
źródło
0

Miałem przypadek, w którym musiałem podzielić dane wyjściowe konsoli po zapytaniu LDAP za pomocą ldapsearch. Najpierw skonfiguruj i wykonaj NSTask (znalazłem tutaj dobry przykład kodu: Wykonaj polecenie terminala z aplikacji Cocoa ). Ale potem musiałem podzielić i przeanalizować dane wyjściowe, aby wyodrębnić tylko nazwy serwera wydruku z wyjścia zapytania Ldap. Niestety jest to raczej żmudna manipulacja ciągami znaków, która nie byłaby żadnym problemem, gdybyśmy mieli manipulować napisami / tablicami w języku C za pomocą prostych operacji na tablicach C. Oto mój kod wykorzystujący obiekty kakaowe. Jeśli masz lepsze sugestie, daj mi znać.

//as the ldap query has to be done when the user selects one of our Active Directory Domains
//(an according comboBox should be populated with print-server names we discover from AD)
//my code is placed in the onSelectDomain event code

//the following variables are declared in the interface .h file as globals
@protected NSArray* aDomains;//domain combo list array
@protected NSMutableArray* aPrinters;//printer combo list array
@protected NSMutableArray* aPrintServers;//print server combo list array

@protected NSString* sLdapQueryCommand;//for LDAP Queries
@protected NSArray* aLdapQueryArgs;
@protected NSTask* tskLdapTask;
@protected NSPipe* pipeLdapTask;
@protected NSFileHandle* fhLdapTask;
@protected NSMutableData* mdLdapTask;

IBOutlet NSComboBox* comboDomain;
IBOutlet NSComboBox* comboPrinter;
IBOutlet NSComboBox* comboPrintServer;
//end of interface globals

//after collecting the print-server names they are displayed in an according drop-down comboBox
//as soon as the user selects one of the print-servers, we should start a new query to find all the
//print-queues on that server and display them in the comboPrinter drop-down list
//to find the shares/print queues of a windows print-server you need samba and the net -S command like this:
// net -S yourPrintServerName.yourBaseDomain.com -U yourLdapUser%yourLdapUserPassWord -W adm rpc share -l
//which dispalays a long list of the shares

- (IBAction)onSelectDomain:(id)sender
{
    static int indexOfLastItem = 0; //unfortunately we need to compare this because we are called also if the selection did not change!

    if ([comboDomain indexOfSelectedItem] != indexOfLastItem && ([comboDomain indexOfSelectedItem] != 0))
    {

        indexOfLastItem = [comboDomain indexOfSelectedItem]; //retain this index for next call

    //the print-servers-list has to be loaded on a per univeristy or domain basis from a file dynamically or from AN LDAP-QUERY

    //initialize an LDAP-Query-Task or console-command like this one with console output
    /*

     ldapsearch -LLL -s sub -D "cn=yourLdapUser,ou=yourOuWithLdapUserAccount,dc=yourDomain,dc=com" -h "yourLdapServer.com" -p 3268 -w "yourLdapUserPassWord" -b "dc=yourBaseDomainToSearchIn,dc=com" "(&(objectcategory=computer)(cn=ps*))" "dn"

//our print-server names start with ps* and we want the dn as result, wich comes like this:

     dn: CN=PSyourPrintServerName,CN=Computers,DC=yourBaseDomainToSearchIn,DC=com

     */

    sLdapQueryCommand = [[NSString alloc] initWithString: @"/usr/bin/ldapsearch"];


    if ([[comboDomain stringValue] compare: @"firstDomain"] == NSOrderedSame) {

      aLdapQueryArgs = [NSArray arrayWithObjects: @"-LLL",@"-s", @"sub",@"-D", @"cn=yourLdapUser,ou=yourOuWithLdapUserAccount,dc=yourDomain,dc=com",@"-h", @"yourLdapServer.com",@"-p",@"3268",@"-w",@"yourLdapUserPassWord",@"-b",@"dc=yourFirstDomainToSearchIn,dc=com",@"(&(objectcategory=computer)(cn=ps*))",@"dn",nil];
    }
    else {
      aLdapQueryArgs = [NSArray arrayWithObjects: @"-LLL",@"-s", @"sub",@"-D", @"cn=yourLdapUser,ou=yourOuWithLdapUserAccount,dc=yourDomain,dc=com",@"-h", @"yourLdapServer.com",@"-p",@"3268",@"-w",@"yourLdapUserPassWord",@"-b",@"dc=yourSecondDomainToSearchIn,dc=com",@"(&(objectcategory=computer)(cn=ps*))",@"dn",nil];

    }


    //prepare and execute ldap-query task

    tskLdapTask = [[NSTask alloc] init];
    pipeLdapTask = [[NSPipe alloc] init];//instead of [NSPipe pipe]
    [tskLdapTask setStandardOutput: pipeLdapTask];//hope to get the tasks output in this file/pipe

    //The magic line that keeps your log where it belongs, has to do with NSLog (see /programming/412562/execute-a-terminal-command-from-a-cocoa-app and here http://www.cocoadev.com/index.pl?NSTask )
    [tskLdapTask setStandardInput:[NSPipe pipe]];

    //fhLdapTask  = [[NSFileHandle alloc] init];//would be redundand here, next line seems to do the trick also
    fhLdapTask = [pipeLdapTask fileHandleForReading];
    mdLdapTask  = [NSMutableData dataWithCapacity:512];//prepare capturing the pipe buffer which is flushed on read and can overflow, start with 512 Bytes but it is mutable, so grows dynamically later
    [tskLdapTask setLaunchPath: sLdapQueryCommand];
    [tskLdapTask setArguments: aLdapQueryArgs];

#ifdef bDoDebug
    NSLog (@"sLdapQueryCommand: %@\n", sLdapQueryCommand);
    NSLog (@"aLdapQueryArgs: %@\n", aLdapQueryArgs );
    NSLog (@"tskLdapTask: %@\n", [tskLdapTask arguments]);
#endif

    [tskLdapTask launch];

    while ([tskLdapTask isRunning]) {
      [mdLdapTask appendData: [fhLdapTask readDataToEndOfFile]];
    }
    [tskLdapTask waitUntilExit];//might be redundant here.

    [mdLdapTask appendData: [fhLdapTask readDataToEndOfFile]];//add another read for safety after process/command stops

    NSString* sLdapOutput = [[NSString alloc] initWithData: mdLdapTask encoding: NSUTF8StringEncoding];//convert output to something readable, as NSData and NSMutableData are mere byte buffers

#ifdef bDoDebug
    NSLog(@"LdapQueryOutput: %@\n", sLdapOutput);
#endif

    //Ok now we have the printservers from Active Directory, lets parse the output and show the list to the user in its combo box
    //output is formatted as this, one printserver per line
    //dn: CN=PSyourPrintServer,OU=Computers,DC=yourBaseDomainToSearchIn,DC=com

    //so we have to search for "dn: CN=" to retrieve each printserver's name
    //unfortunately splitting this up will give us a first line containing only "" empty string, which we can replace with the word "choose"
    //appearing as first entry in the comboBox

    aPrintServers = (NSMutableArray*)[sLdapOutput componentsSeparatedByString:@"dn: CN="];//split output into single lines and store it in the NSMutableArray aPrintServers

#ifdef bDoDebug
    NSLog(@"aPrintServers: %@\n", aPrintServers);
#endif

    if ([[aPrintServers objectAtIndex: 0 ] compare: @"" options: NSLiteralSearch] == NSOrderedSame){
      [aPrintServers replaceObjectAtIndex: 0 withObject: slChoose];//replace with localized string "choose"

#ifdef bDoDebug
      NSLog(@"aPrintServers: %@\n", aPrintServers);
#endif

    }

//Now comes the tedious part to extract only the print-server-names from the single lines
    NSRange r;
    NSString* sTemp;

    for (int i = 1; i < [aPrintServers count]; i++) {//skip first line with "choose". To get rid of the rest of the line, we must isolate/preserve the print server's name to the delimiting comma and remove all the remaining characters
      sTemp = [aPrintServers objectAtIndex: i];
      sTemp = [sTemp stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];//remove newlines and line feeds

#ifdef bDoDebug
      NSLog(@"sTemp: %@\n", sTemp);
#endif
      r = [sTemp rangeOfString: @","];//now find first comma to remove the whole rest of the line
      //r.length = [sTemp lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
      r.length = [sTemp length] - r.location;//calculate number of chars between first comma found and lenght of string
#ifdef bDoDebug
      NSLog(@"range: %i, %i\n", r.location, r.length);
#endif

      sTemp = [sTemp stringByReplacingCharactersInRange:r withString: @"" ];//remove rest of line
#ifdef bDoDebug
      NSLog(@"sTemp after replace: %@\n", sTemp);
#endif

      [aPrintServers replaceObjectAtIndex: i withObject: sTemp];//put back string into array for display in comboBox

#ifdef bDoDebug
      NSLog(@"aPrintServer: %@\n", [aPrintServers objectAtIndex: i]);
#endif

    }

    [comboPrintServer removeAllItems];//reset combo box
    [comboPrintServer addItemsWithObjectValues:aPrintServers];
    [comboPrintServer setNumberOfVisibleItems:aPrintServers.count];
    [comboPrintServer selectItemAtIndex:0];

#ifdef bDoDebug
    NSLog(@"comboPrintServer reloaded with new values.");
#endif


//release memory we used for LdapTask
    [sLdapQueryCommand release];
    [aLdapQueryArgs release];
    [sLdapOutput release];

    [fhLdapTask release];

    [pipeLdapTask release];
//    [tskLdapTask release];//strangely can not be explicitely released, might be autorelease anyway
//    [mdLdapTask release];//strangely can not be explicitely released, might be autorelease anyway

    [sTemp release];

    }
}
Rosario Carcò
źródło
0

Sam natrafiłem na przypadek, w którym nie wystarczyło po prostu oddzielić ciąg znaków przez składnik wielu zadań, takich jak
1) Kategoryzowanie tokenu na typy
2) Dodawanie nowych tokenów
3) Oddzielanie ciągu między niestandardowymi zamknięciami, jak wszystkie słowa między "{" i "} „
W przypadku takich wymagań zestaw Parse Kit uratował mi życie.

Użyłem go do analizowania plików .PGN (prtable gaming notation) z powodzeniem, jest bardzo szybki i lekki.

amar
źródło