I'm using NSXMLParser to parse XML data from a remote server. I followed a tutorial to get up and running and everything is ok for any (NSString *) members I have in my objects that I'm creating. I also have integers that I need to set from the XML data, such as:
<root>
<child>
<name> Hello </name>
<number> 123 </number>
</child>
<child>
<name> World</name>
<number> 456 </number>
</child>
</root>
In this case I would be creating two "child" objects. I'm using:
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
...
[aChild setValue:currentElementValue forKey:elementName];
...
}
Delegate to set the values. This is where I get to my problem. The "NSString *name" member is set fine everytime, however if I use an NSInteger, then whenever I try to set "number" I get an EXC_BAD_ACCESS. So I tried using an "int" instead. But now I can't use key-value programming and need to look for the node manually:
if([elementName isEqualToString:#"number"]) {
aChild.number = [currentElementValue intValue]
}
This is okay, I can deal with that because I know what nodes I'll be getting, but it gets worse. When currentElementValue is an "NSMutableString *" as per the tutorial, it does not return the correct integer even though the string is correct. For instance:
NSLog(#"NSMutableString Value: %#, and intValue %d\n", currentElementValue, [currentElementValue intValue]);
// Value will be 123 but intValue will be 0
So I made currentElementValue an NSString instead of an NSMutableString and I can get the proper intValue. But I read online that the reason it is an NSMutableString is because the:
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
Delegate used to set the value can occur more than once, but typically does not. So my question is does anybody know what I'm doing wrong? This seems like a pretty trivial case for NSXMLParser so I'm sure it's something I'm misunderstanding.
I don't know where your code is failing, but here's the correct way to handle this:
NSMutableString *buffer = [[NSMutableString alloc] init];
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict {
[buffer setString:#""];
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
if([elementName isEqualToString:#"number"]) {
[aChild setNumber:[buffer intValue]];
[buffer setString:#""];
}
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
[buffer appendString:string];
}
intValue should work identically NSString and NSMutableString. Are you sure that Value was '123' and not '\n123' (\n means a new line character), if the string doesn't start with a decimal number then intValue will return 0.
Are you clearing the mutable string correctly at parser:didStartElement:? If you're cleaning only at parser:didEndElement: then parser:foundCharacters: will collect characters from parent element too. Which will prefix your string with newlines in this case and intValue will return 0.
You're correct in that parser:foundCharacters: can be called multiple times for a single element.
Couple things going on here.
First, you have obvious blanks in your XML character data, e.g.
<number> 456 </number>
You should really strip out that whitespace. That is likely what is causing the return value of [NSString intValue] to be wrong. If you can remove it at the source, great. If not, you can strip it out on the receiving end by doing:
currentElementValue = [currentElementValue stringByTrimmingCharactersInSet:
[NSCharacterSet whitespaceAndNewlineCharacterSet]];
The reason you couldn't use key/value is that you can't store an NSInteger value in an NSMutableDictionary. Both keys and values in the dictionary have to descend from NSObject, and NSInteger is (I'm surmising, here) just a platform-safe typedef of int. So you should use an NSNumber instead:
NSNumber *theInt = [NSNumber numberWithInt:[currentElementValue intValue]];
[aChild setObject:theInt forKey:elementName];
Related
I'm developing an application that needs to get data from an XML file. Some of the nodes have a lot of characters and I've a problem using this function :
- (void) parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
currentNodeContent = (NSMutableString *) [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
}
For example for the description node of an item I will get only the 20-30 last characters whereas I would get 200 or 300.
I checked this out with a NSLog and it appears the problem comes from here. Do you know what's wrong ?
Thanks for any advice.
SAX parsers do not guarantee to get all characters at once. You may get multiple calls with chunks of characters from any given block; your code should concatenate them into a single string.
The parser object may send the delegate several parser:foundCharacters: messages to report the characters of an element. Because string may be only part of the total character content for the current element, you should append it to the current accumulation of characters until the element changes.
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict {
if ([qualifiedName isEqualToString:#"myTag"]) {
buf = [NSMutableString string];
}
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
if ([qualifiedName isEqualToString:#"myTag"]) {
buf = [buf stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSLog(#"Got %#", buf);
}
}
- (void) parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
{
[buf appendString:string];
}
I'm currently parsing an xml file that has a string like this between two elements:
Hello & bye
When the foundCharacters delegate gets called it parses the information like this....
it parses:
Hello
&
bye
It makes 3 calls to parse that element and I end up with 3 strings as opposed to 1 string. Is there some way to detect this with out having to append a string together and keep a counter of how many times the delegate was called?
Any help is very appreciated...
Short answer : No. It's only chance that it's returning three strings, it might return 11 strings all one character long if it wanted to. It's up to you to join the strings together.
Long answer : You need to append a string. There's nothing you can do about it. however, I don't understand why you need to keep a count of the number of times the delegate has been called - I think that this code should do what you want :
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict {
// Make a new string to hold the data
[currentString release];
currentString = [NSMutableString alloc] init];
}
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
// Add the string to our current string
[currentString appendString:string];
}
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
// In here, current string should be it's entire contents,
NSLog(#"%#", currentString);
}
You will need a member in your class to hold the data - a mutable string is easiest one:
#interface MyClass : NSObject {
NSMutableString *currentString;
}
#end
(It doesn't need to be a property but you do need to remember to release it in your dealloc method)
I am working with an API where I get a response back this this, and I want to parse the integer ID out of it:
<?xml version="1.0"?>
<trip>328925</trip>
How would you parse this? I have some really fragile code I want to get rid of, and I'd appreciate some advice:
if ([[response substringWithRange:NSMakeRange(0, 21)]
isEqualToString: #"<?xml version=\"1.0\"?>"]) {
self.tripId = [response substringWithRange:NSMakeRange(28, response.length-35)];
}
I don't think I need an XML parsing library for this task!
I would use an XML parser. Using an XML parser really is the best way to parse XML.
It's not that hard to do either:
// Parser Delegate
#interface ParserDelegate : NSObject {
int inTripElements;
NSMutableString* trip;
}
#property (readonly) NSMutableString* trip;
#end
#implementation ParserDelegate
#synthesize trip;
- (id) init {
if (![super init]) {
[self release];
return nil;
}
trip = [#"" mutableCopy];
return self;
}
- (void) parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qualifiedName
attributes:(NSDictionary *)attributeDict {
if ([elementName isEqualToString:#"trip"]) {
++inTripElements;
}
}
- (void) parser:(NSXMLParser *)parser foundCharacters:(NSString *)string {
if (inTripElements > 0) {
[trip appendString:string];
}
}
- (void) parser:(NSXMLParser *)parser
didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName {
if ([elementName isEqualToString:#"trip"]) {
--inTripElements;
}
}
#end
// Parsing
NSString* toParse = #"<?xml version=\"1.0\"?>"
"<trip>328925</trip>";
NSData* data = [NSData dataWithBytes:[toParse UTF8String]
length:strlen([toParse UTF8String])];
ParserDelegate* parserDelegate = [[ParserDelegate alloc] init];
NSXMLParser* parser = [[NSXMLParser alloc] initWithData:data];
[parser setDelegate:parserDelegate];
[parser parse];
[parser release];
NSLog(#"trip=%#", parserDelegate.trip);
[parserDelegate release];
If you really don't want to use an XML Parser to create a proper object why not use the regular expression <trip>\d*</trip> to match the element? Then you can get the integer part by removing the start and end tag and parsing to ensure correct as with any string.
Here is a handy place for testing regular expressions; http://regexlib.com/RETester.aspx
Check out 'NSScanner'. It would be perfect for this.
You should use an XML library for this as there are many cases where the code will change
For example in this case what happens if
The <?xml declaration is not sent or the encoding changes from UTF-8
or somone adds a space before the trip element
In all these cases the provider of the file can say the file is correct
etc.
with the XML parsing all this has been done and your code is more robust
Also in this case the code to parse and find is quite simple.
I'm reading XML data and creating objects, but I need some of my object variables to be floats. And with the - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string it obviously becomes a string and my float variables will be set to 0.000000.
In the - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName I tried something like this when setting the value to the object, but it's apparently incompatible types as setValue wants a (id)key (and I just realized that temp was set to 0.000000 anyways, so the floatValue doesn't work either).
if([elementName isEqualToString:#"longitude"])
{
float temp = [currentElementValue floatValue];
[myObj setValue:temp forKey:elementName];
}
Does anyone have any idea how to solve it or do I just have to set it to NSStrings in my object and convert it to floats afterwards?
Just save it as a string when parsing. Then when you need it for calculations convert it to a
float with [NSString floatValue] as you do above.
However I think that float can't hold the value NSString returns so try it with your temp as a CGFloat instead.
There is no way for the xml to hold anything other that strings so this approach is OK.
In my iPhone application, I have the following NSString:
NSString *myxml=#"<students>
<student><name>Raju</name><age>25</age><address>abcd</address>
</student></students>";
How would I parse the XML content of this string?
Download:
https://github.com/bcaccinolo/XML-to-NSDictionary
Then you simply do :
NSDictionary *dic = [XMLReader dictionaryForXMLString:myxml error:nil];
Result is a NSDictionary *dic with dictionaries, arrays and strings inside, depending of the XML:
{
students = {
student = {
address = abcd;
age = 25;
name = Raju;
};
};
}
You should use the NSXMLParser class
Here's a link to the documentation for that class:
http://developer.apple.com/documentation/Cocoa/Reference/Foundation/Classes/NSXMLParser_Class/Reference/Reference.html
Your code should look something like this:
#implementation MyClass
- (void)startParsing {
NSData *xmlData = (Get XML as NSData)
NSXMLParser *parser = [[[NSXMLParser alloc] initWithData:xmlData] autorelease];
[parser setDelegate:self];
[parser parse];
}
- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qualifiedName attributes:(NSDictionary *)attributeDict {
NSLog(#"Started %#", elementName);
}
Another answer is: Don't use XML. Use a plist instead, which is written using XML but more easily parsable in Objective-C into distinct data types (NSArray for example has a method to convert a file or NSData plist into an NSArray).
Like #Jon Hess mentioned, just create a wrapping class for the "optional" methods of the NSXMLParserDelegate. These methods help you separate the tasks that you might find useful when you parse your xml.
One really good online journal file I found is Elegant XML parsing with Objective-C. Phil Nash really took his time to show the basics of the parsing options at your reach. It can take a new programmer and guide him/her through the whole setup.
Loading the xml can be a modification of #Jon Hess method.
You can setup the:
-(void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
attributes:(NSDictionary *)attributeDict{
}
to handle events on certain elements.
Also implement the:
-(void)parser:(NSXMLParser *)parser
foundCharacters:(NSString *)string {
}
to place the strings found into a collection of objects.
I think the best equivalent to XMLDocument is AbacigilXQ Library. You should look at it. I'm using it.
http://code.google.com/p/abacigilxq-library/