Script to replace XML attribute value in multiple elements?

Forgive my ignorance, maybe this is simple but... I have an XML file containing info on multiple clips listed as follows:

<clip name="CLIP 1" duration="2615800/2400s" start="17990s" format="r1" tcFormat="NDF">

<video offset="17990s" ref="r6" duration="2615800/2400s" start="17990s">

<audio lane="-3" offset="17990s" ref="r6" srcID="3" duration="784740000/720000s" start="17990s" role="dialogue" srcCh="1, 2"/>

<audio lane="-2" offset="17990s" ref="r6" srcID="2" duration="784740000/720000s" start="17990s" role="dialogue" srcCh="1, 2"/>

<audio lane="-1" offset="17990s" ref="r6" duration="784740000/720000s" start="17990s" role="dialogue" srcCh="1, 2"/>

</video>

<keyword start="17990s" duration="2615800/2400s" value="Rough Mix 4_10"/>

</clip>


<clip name="CLIP 2".... etc.


Every clip (could be 4, could be 100) has the same "audio lane" attributes (1,2,3), and the same "role" attributes (dialogue). all other attributes are different for each clip in the XML.


What I'd like to do is, for each audio lane in every clip in the XML, change the the "role" attribute, and leave everything else alone. In a perfect world, I'd have a dialog pop up the listed the audio lanes, and a text box to enter the text I'd like to replace the existing role attribute in every instance of each lane. So all the audio lanes for every clip would change. Using etc Edit, Text Wrangler... something like that? I'd end up with something like the following:


<audio lane="-3" offset="17990s" ref="r6" srcID="3" duration="784740000/720000s" start="17990s" role="ROLE_C" srcCh="1, 2"/>

<audio lane="-2" offset="17990s" ref="r6" srcID="2" duration="784740000/720000s" start="17990s" role="ROLE_B" srcCh="1, 2"/>

<audio lane="-1" offset="17990s" ref="r6" duration="784740000/720000s" start="17990s" role="ROLE_A" srcCh="1, 2"/>


If the role attributes were all different, then I could just do a find and replace for the attributes, but since; they're the same for every audio lane, I'm clueless. Knowing that I'm clueless, I'm guessing there's a way to do this? I can do it manually, but if I had, say 100 clips in the XML, it'd be nice to automate it somehow. Thanks!

OS X Yosemite (10.10.4)

Posted on Jul 14, 2015 11:09 AM

Reply
24 replies

Jul 14, 2015 2:43 PM in response to CAIV

Hello


You may try something like the following shell script.



#!/bin/bash perl -w <<'EOF' - in.xml > out.xml use strict; use XML::LibXML; # audio lane => role mappings my %roles = ( -1 => 'ROLE_A', -2 => 'ROLE_B', -3 => 'ROLE_C', ); my $parser = XML::LibXML->new(); my $doc = $parser->parse_file(shift); for my $k (keys %roles) { my $rr = $doc->find(qq(//audio[\@lane="$k"]/\@role)); for my $r (@{$rr}) { $r->setValue($roles{$k}); } } print $doc->toString(0); EOF




And here's an AppleScript wrapper if it helps.



set infile to (choose file of type {"xml"} with prompt "Choose input xml file")'s POSIX path set outfile to (choose file name default name "out.xml" with prompt "Specify output xml file name and location")'s POSIX path do shell script "perl -w <<'EOF' - " & infile's quoted form & " > " & outfile's quoted form & " use strict; use XML::LibXML; # audio lane => role mappings my %roles = ( -1 => 'ROLE_A', -2 => 'ROLE_B', -3 => 'ROLE_C', ); my $parser = XML::LibXML->new(); my $doc = $parser->parse_file(shift); for my $k (keys %roles) { my $rr = $doc->find(qq(//audio[\\@lane=\"$k\"]/\\@role)); for my $r (@{$rr}) { $r->setValue($roles{$k}); } } print $doc->toString(0); EOF"




Hope this may help,

H

Jul 14, 2015 4:25 PM in response to Hiroto

Thank you! It works great. I'll see if I can figure out how to get a dialog to pop up after it opens the XML to let the user specify the text that should be filled in for the attribute values (ROLE_A, ROLE_B, AND ROLE_C were examples).


What I'm trying to accomplish in the end, would be to either enter the number of lanes in the files, (or have the script figure it out), and then enter the role value for each lane. The end result would be to be able to use it for files with varying lane counts. If you have any pointers that would be great, but this is very helpful! At least I have a place to start now. Thanks!

Jul 14, 2015 6:40 PM in response to CAIV

Hello


Perhaps something like the following AppleScript script might work?


It retrieves audio lane attribute values from the 1st clip element, lets user specify role for each lane and edits the role attribute values accordingly. If every clip element has the same audio lane structure, this might work.



set infile to (choose file of type {"xml"} with prompt "Choose input xml file")'s POSIX path set outfile to (choose file name default name "out.xml" with prompt "Specify output xml file name and location")'s POSIX path -- retrieve audio lane attribute values from the 1st clip element in input file do shell script "perl -w <<'EOF' - " & infile's quoted form & " use strict; use XML::LibXML; my $parser = XML::LibXML->new(); my $doc = $parser->parse_file(shift); my $rr = $doc->find(qq(//clip[1]//audio/\\@lane)); for my $r (@{$rr}) { print $r->value, qq(\\n); } EOF" set lanes to result's paragraphs -- build lane => role mappings set mappings to "" repeat with i in lanes display dialog "Enter audio role for audio lane " & i default answer "" set mappings to mappings & i & tab & result's text returned & linefeed end repeat (* mappings holds lane => role mappings of which each mapping is denoted by lane \t role \n e.g. -1 \t ROLE_A \n -2 \t ROLE_B \n -3 \t ROLE_C \n *) -- edit the audio role attribute values according to the mappings and yield output file do shell script "perl -w <<'EOF' - " & mappings's quoted form & " " & infile's quoted form & " > " & outfile's quoted form & " use strict; use XML::LibXML; my %roles = (shift =~ /^(\\S+)\\t(\\S+)$/omg) ; my $parser = XML::LibXML->new(); my $doc = $parser->parse_file(shift); for my $k (keys %roles) { my $rr = $doc->find(qq(//audio[\\@lane=\"$k\"]/\\@role)); for my $r (@{$rr}) { $r->setValue($roles{$k}); } } print $doc->toString(0); EOF"




Good luck,

H

Jul 15, 2015 12:40 PM in response to CAIV

Hello


The script uses Perl's XML::LibXML module which implements interface to libxml2 library. It enables us to use XPath (1.0) notation to select target nodes in tree. This tree-based XML parsing by XPath is very handy.


Here's some learning resources.


XPath

http://www.w3schools.com/xpath/


XPath 1.0

http://www.w3.org/TR/xpath/


libxml2

http://www.xmlsoft.org/


XML::LibXML (Perl module)

http://search.cpan.org/~shlomif/XML-LibXML-2.0121/LibXML.pod

http://search.cpan.org/~shlomif/XML-LibXML-2.0121/lib/XML/LibXML/Parser.pod

http://search.cpan.org/~shlomif/XML-LibXML-2.0121/lib/XML/LibXML/Node.pod

http://search.cpan.org/~shlomif/XML-LibXML-2.0121/lib/XML/LibXML/Attr.pod



I used Perl but you can also use other scripting languages such as Ruby and Python. Or even AppleScript with free XMLLib.osax by Satimage:


http://www.satimage.fr/software/en/downloads/downloads_companion_osaxen.html



Happy scripting! 🙂

H

Jul 17, 2015 10:39 AM in response to Hiroto

Thanks again for this... I've started reading the material linked above. I have a *lot* to learn. 😮 I've run into an issue with a type of file I hadn't anticipated using the script with and I wonder if you'd mind looking at it if you have the time. Let me know and I'll post here. In any case, thanks so much for your help. No way I could have figured this out on my own. 🙂

Jul 22, 2015 11:25 PM in response to Hiroto

No need to apologize! The script has been really useful. :-) There’s a tiny issue, and a more challenging problem, feel free to ignore either one :-D

The tiny issue is that, as I guess the script uses the xml’s lane ordering, it presents the dialogs for for the user to enter role assignments in descending order, (if there are 4 lanes, it prompts for lane 4 first, then 3,2,1). Not a huge problem, and maybe not too difficult to sort out.


The larger issue arises when working with xml made from clips with no Video component. I’m hesitant to even ask, as I have figured out a workaround, but if you like a challenge, here is a summary. This is really long, and I apologize in advance, I just figured I'd put it all in on giant post and be done. :-)


Some background… I’m using the script with fcpxml files and thanks to your help, it works great! My intent was to use this script with fcpxml which defines A/V clips comprised of one Video component and multiple Audio components(channels) as shown below.


(this A/V clip contains 1 Video component and 4 interleaved stereo Audio components(channels) )


<clip name="TR_R1_V10_SPLIT" duration="2862400/2400s" start="3590s" format="r1" tcFormat="NDF">

<video offset="3590s" ref="r2" duration="2862400/2400s" start="3590s">

<audio lane=“-4” offset="3590s" ref="r2" srcID=“4” duration="858720000/720000s" start="3590s" role=“ROLE_D“ srcCh="1, 2"/>

<audio lane="-3" offset="3590s" ref="r2" srcID="3" duration="858720000/720000s" start="3590s" role=“ROLE_C“ srcCh="1, 2"/>

<audio lane="-2" offset="3590s" ref="r2" srcID="2" duration="858720000/720000s" start="3590s" role=“ROLE-B” srcCh="1, 2"/>

<audio lane="-1" offset="3590s" ref="r2" duration="858720000/720000s" start="3590s" role=“ROLE_A” srcCh="1, 2"/>

</video>

</clip>


So as all the <audio> elements have lane=“-X” parameters, the script finds and modifies the role parameter in each audio element perfectly.


But, for clips that are audio-only, the fcpxml is constructed slightly differently. For example:


(this audio clip contains 4 mono audio components(channels) )


<clip name="COACHVPRET01" duration="169369185/720000s" start="2515120609/48000s" format="r1" tcFormat="NDF">

<audio offset="37726809135/720000s" ref="r2" duration="169369185/720000s" start="37726809135/720000s" role="Role_A” srcCh="1">

<audio lane="-3" offset="37726809135/720000s" ref="r2" duration="169369185/720000s" start="37726809135/720000s" role="Role_D” srcCh="4"/>

<audio lane="-2" offset="37726809135/720000s" ref="r2" duration="169369185/720000s" start="37726809135/720000s" role="Role_C” srcCh="3"/>

<audio lane="-1" offset="37726809135/720000s" ref="r2" duration="169369185/720000s" start="37726809135/720000s" role=“Role_B” srcCh="2"/>

</audio>

</clip>


As there is no <video> element, the first audio component(channel) is defined in the parent <audio> element and has no “lane” parameter. (it is effectively lane=“0” , but the lane parameter is not required for the parent component in fcpxml).


So, as the script is looking for <audio> elements containing lane parameters to build the role mappings, it misses the first (parent) component(channel). Subsequent components are then mis-identified in the Role setting dialogs. Since the <audio lane> parameters start at “-1” for the second component, the prompt for lane -1 sets the role for the second component, lane -2 prompt sets the role for the third component, etc.


The challenge is that I can’t figure out how to get the script to see the role parameter in the parent element. It was suggested to me that I could have the script first replace all occurrences of <audio offset with <audio lane=“0” offset and then run the mapping bit. Other than manually in BBedit or something, I have no clue how to do that, so here I am…


The only unique search term for component(channel) 1 will be the <audio offset> text string as it will only occur, and always be first, in the parent element. Additional channels will always be in child audio elements with lane parameters as before. Would I need a separate Audio Only script? could there be an if/then routine triggered by user input, check box, radio button or something? I have no clue. :-(


As I said above, I have a workaround. The user can set the Role they want on component(channel) to all channels easily before creating the original fcpxml., then just use the script to batch change the individual child elements. as long as they also know that the role prompt for lane -1 sets component 2, -2 sets 3 etc., it works fine.


If this is too much, just say so. I only ask because, though I have no problem at all with the workaround, I’ve made the script available to others for free here, and I’d like to make it less confusing when used with Audio Only clips. I really would like to do it myself, but based on what I’ve learned from the material you posted above, I think it will be quite some time before I can do that.


In any case, Thanks very much for your time. This script was something that a lot of people, including myself, have been clamoring for. :-)

Jul 23, 2015 2:49 PM in response to CAIV

Hello


Here's my attempt to tackle the case. The revised script listed below will a) ask for the role in descending order of lane attribute values such as -1, -2, -3 and b) add lane="0" attribute to /clip/audio element.


Actually it has separate logic to process /clip/video and /clip/audio. It can handle XML data containing both /clip/video and clip/audio elements provided that every /clip/video element has the same audio lane structure and every /clip/audio element has the same audio lane structure which can be different from that of /clip/video element. It will ask separate mappings for /clip/video and /clip/audio if file contains both.


By the way, the standard "display dialog" command in AppleScript is a sad device to ask for this sort of structured data. More sophisticated interface could have been made by using Cocoa via RubyCocoa or PyObjC. But it is not essential and yet requires some work. So I leave that part of the script as it is.


Hope this may be of some help.

Hiroto



set infile to (choose file of type {"xml"} with prompt "Choose input xml file")'s POSIX path set outfile to (choose file name default name "out.xml" with prompt "Specify output xml file name and location")'s POSIX path -- retrieve audio lane attribute values from the 1st /clip/video and/or /clip/audio element(s) in input file do shell script "perl -w <<'EOF' - " & infile's quoted form & " use strict; use XML::LibXML; local $, = qq(\\t); local $\\ = qq(\\n); my $parser = XML::LibXML->new(); my $doc = $parser->parse_file(shift); my ($rr, @rr); # clip/video/audio # extract lane attribute from //clip[video][1]/video/audio elements $rr = $doc->find( qq(//clip[video][1]/video/audio/\\@lane) ); @rr = sort { $b <=> $a } map { $_->value } @{$rr}; print 'video', @rr if @rr; # clip/audio and clip/audio/audio # add attribute lane='0' to //clip/audio elements $rr = $doc->find( qq(//clip/audio) ); for my $r (@{$rr}) { my $lane = $r->find( qq(\\@lane) ); if ($lane) { $lane->[0]->setValue('0'); } else { $r->addChild(XML::LibXML::Attr->new('lane', '0')); } } # etract lane attribute from //clip[audio][1]//* elemeents $rr = $doc->find( qq(//clip[audio][1]//\\@lane) ); @rr = sort { $b <=> $a } map { $_->value } @{$rr}; print 'audio', @rr if @rr; EOF" (* result is audio lane attributes for video and/or autio element(s) denoted by kind \t lane \t lane ... \n e.g., video -1 -2 -3 -4 audio 0 -1 -2 -3 *) set rr to result's paragraphs try set {astid0, AppleScript's text item delimiters} to {AppleScript's text item delimiters, tab} repeat with r in rr set r's contents to r's text items end repeat set AppleScript's text item delimiters to astid0 on error errs number errn set AppleScript's text item delimiters to astid0 error errs number errn end try (* rr is 2d array representation of audio lane attributes for video and audio e.g. {{"video", "-1", "-2", "-3", "-4"}, {"audio", "0", "-1", "-2", "-3"}} *) -- build lane => role mappings set aa to {} repeat with r in rr set clip_kind to r's item 1 set t to "Mappings for " & clip_kind & " clip" repeat set mappings to "" repeat with i in r's rest display dialog "Enter audio role for audio lane " & i default answer "" with title t set mappings to mappings & i & tab & result's text returned & linefeed end repeat try display dialog "audio lane -> audio role" & return & mappings with title t exit repeat end try end repeat (* mappings holds lane => role mappings of which each mapping is denoted by lane \t role \n e.g. -1 \t ROLE_A \n -2 \t ROLE_B \n -3 \t ROLE_C \n *) set aa's end to clip_kind set aa's end to mappings end repeat (* aa is flat list as {kind, mappings} or {kind, mappings, kind, mappings} *) -- edit the audio role attribute values according to the mappings and yield output file set args to "" repeat with a in {infile} & aa set args to args & a's quoted form & space end repeat do shell script "perl -w <<'EOF' - " & args & " > " & outfile's quoted form & " # # ARGV = infile kind mappings kind mappings ... # use strict; use XML::LibXML; my $parser = XML::LibXML->new(); my $doc = $parser->parse_file(shift); # add attribute lane=\"0\" to //clip/audio elements my $rr = $doc->find( qq(//clip/audio) ); for my $r (@{$rr}) { my $lane = $r->find( qq(\\@lane) ); if ($lane) { $lane->[0]->setValue('0'); } else { $r->addChild(XML::LibXML::Attr->new('lane', '0')); } } while (my $kind = shift) { my %roles = (shift =~ /^(.+?)\\t(.+)$/omg); # set role attribute values according to lane-role mapings for my $k (keys %roles) { my $rr = $doc->find(qq(//clip/$kind/descendant-or-self::audio[\\@lane=\"$k\"]/\\@role)); for my $r (@{$rr}) { $r->setValue($roles{$k}); } } } print $doc->toString(0); EOF"

Jul 23, 2015 5:05 PM in response to Hiroto

Hi,

This is great, thank you very much! As I'll be giving this away to my fellow video editors who need it, may I credit you somewhere? If so, just as Hiroto? Do you have a site or other work you'd like linked?


I agree that Applescript is kind of lame for something like this. :-) This does inspire me to actually delve more into Cocoa, as well as the resources you linked earlier. Again, thanks so much for doing this, this script is a great help to a bunch of people. And if for some reason you need anything edited, let me know. That is something I do know how to do. :-)

Jul 24, 2015 1:32 PM in response to CAIV

My pleasure! Really glad to be of help. And please feel free to modify, reuse and/or redistribute the script without crediting me. My answering question here is like solving puzzle and solution is its own reward. 🙂 Nevertheless, if you're so honourable as to credit me somewhere, please refer to me as Hiroto in Apple Support Communities, and just linked to this thread.


Kindest regards,

Hiroto

Jul 30, 2015 10:39 AM in response to Hiroto

The little app I cobbled together from this has gotten some publicity. :-) Have a look at this page if you'd like to see what people are saying. Just wanted to thank you again, and pass along the thanks from others. :-) Of course, I've gotten a request to add a feature which, as before, conceptually seems like it would be easy, (adding a "name" element to each lane with a parameter that matches the Role the user enters) but it's beyond my skill set to implement. (I'm working on learning though!). So if you're up for one more small challenge, I have one for you. :-) Either way, Thanks!

Jul 30, 2015 1:49 PM in response to CAIV

RE: adding the "name" element.. in general the element is not present unless a user has renamed the component in the lane prior to creating the xml that the script will use as the "infile". If a name element is present, and the script tries to add it, I'm guessing that would cause some type of conflict?

Jul 31, 2015 8:50 AM in response to CAIV

Hello


Glad to hear that tiny script serves good people. 🙂


Regarding new request, I have some questions. I'd guess you're talking about "name" attribute of "audio" elements. And you want something like the following output, where every audio element with attribute lane="L" has attributes role="ROLE_X" and name="ROLE_X" for specified lane-role mapping L => ROLE_X. Am I correct?



<?xml version="1.0" encoding="UTF-8"?> <array> <clip name="TR_R1_V10_SPLIT" duration="2862400/2400s" start="3590s" format="r1" tcFormat="NDF"> <video offset="3590s" ref="r2" duration="2862400/2400s" start="3590s"> <audio lane="-4" offset="3590s" ref="r2" srcID="4" duration="858720000/720000s" start="3590s" role="ROLE_D" srcCh="1, 2" name="ROLE_D"/> <audio lane="-3" offset="3590s" ref="r2" srcID="3" duration="858720000/720000s" start="3590s" role="ROLE_C" srcCh="1, 2" name="ROLE_C"/> <audio lane="-2" offset="3590s" ref="r2" srcID="2" duration="858720000/720000s" start="3590s" role="ROLE_B" srcCh="1, 2" name="ROLE_B"/> <audio lane="-1" offset="3590s" ref="r2" duration="858720000/720000s" start="3590s" role="ROLE_A" srcCh="1, 2" name="ROLE_A" /> </video> </clip> <clip name="COACHVPRET01" duration="169369185/720000s" start="2515120609/48000s" format="r1" tcFormat="NDF"> <audio offset="37726809135/720000s" ref="r2" duration="169369185/720000s" start="37726809135/720000s" role="ROLE_A" srcCh="1" lane="0" name="ROLE_A" > <audio lane="-3" offset="37726809135/720000s" ref="r2" duration="169369185/720000s" start="37726809135/720000s" role="ROLE_D" srcCh="4" name="ROLE_D"/> <audio lane="-2" offset="37726809135/720000s" ref="r2" duration="169369185/720000s" start="37726809135/720000s" role="ROLE_C" srcCh="3" name="ROLE_C"/> <audio lane="-1" offset="37726809135/720000s" ref="r2" duration="169369185/720000s" start="37726809135/720000s" role="ROLE_B" srcCh="2" name="ROLE_B" /> </audio> </clip> </array>




And what do you want to do with the original "name" attribute of "audio" elements if any? Preserve the original name or replace it with the new name matching the new role?


In either case, it is easy to modify the script accordingly. Just need to know I understand the question correctly.


All the best,

H

This thread has been closed by the system or the community team. You may vote for any posts you find helpful, or search the Community for additional answers.

Script to replace XML attribute value in multiple elements?

Welcome to Apple Support Community
A forum where Apple customers help each other with their products. Get started with your Apple Account.