Perl - 589 588 583 579 579576 Bytes
Each rule consists of a 1 char head, a body and an underscore. As long as rules can be chopped off the beginnig, the head of the rule is replaced with its body in the rest of the Text. The head of the first rule is given, the heads of all following rules are generated from the variable $i.
Since the head for the next rule is placed on the beginning of the text by the previous rule, the last rule will create a character that wont be removed anymore. I had to chose a range of names where the last one would be "W", so i could remove the original "W" from the start of the lyrics, and had it replaced by the rule substitution.
The encoding was done by a Python script using a simple hillclimbing algorithm.
$_="Jell_K$_="qou_on_er_Hh_ w_Hayw_
(Ooh_ell_ a_ay it
_F_'reHoC_g0na other_E9onna_makeR _Bmake-_ve_ _Cve_>ing_<'re_=t'soth1 been_:_ing_e a_;
(Ooh_8o___A_t's _9been g_6
You_7_o t_4_D_4, know_5gP)_
N%_2ndYq_G_ _3C-t_I_ up_0know_
NT_nd _ how I'm feel>feel=
_13);_N_O_i;R up_4)5giCKgi;, n%giCnTgi;
(Gi_-GP)_ you_&yq_
I justKanna7J-0Gottajust3annaH5RMGotta Bunderstand
!_%eCrE_#:und1stand
We'C4n eachFV_e;r for9_
We';Jn s8long6reach<for hear=:ch>sCl0gFr but6<7o8shy78sHInsideKehearBach= both4Kha=9o>butF8shyHCs7Insid>w>bothJ3haBgo= on0
We47he9ame:2we<EplH_!5gi35let- down5run:rou2a2desert-5Bcry5say9oodbye5tJ:WeJ2>gam>aLwe'r>9pl7_KgPKletR liedownKrun6rqLaLdes1tRK:2hurt-
_We<cryKsay n8strangers78loC647hegoodbyeKt56 rules:2s8d8Ili>aLhurtR
_e'r>nCstrang1sHClo;FJ2>rules6LsCdCI
A full commitment'sKhatcommitment's3hat I'm7hink>I'm2ink= of6Kouldn't9et7hisofF3qldn't from:nyF9uy&#A2if-:skget2is me0Don't7Jfrom6ny<guySUALifR6sk me-<7o8bli2t8seemeMD0'tH5 meR8bliLtCsee
!!;,9i3);,9i113)VVEEQQ
#&!!";eval"sUSVV";$i=48;eval"s/$1/$2/g"while s/(.)(.*?)_/chr($i++)/s;printse;print
(I find it remarkable that the compressed Text contains "hearBach" :D)
And here the Python code that generates it:
import collections, sys
text = sys.stdin.read().replace('\r\n','\n')
text = text[1:]
names = setlist(["q"] + map(chr, xrange(32,127))ord('0') -, set('"$ord('W')*+./?@[\\]^_|') - set(text))
done = False
name = ""
while not done:
done = True
best = (0, None)
for m in xrange(1, len(text) / 2 + 1):
counter = collections.Counter()
for i in xrange(0, len(text) - m + 1):
snippet=text[i:i+m]
if not '_' in snippet:
counter[snippet] += 1
for snippet in counter:
n = counter[snippet]
gain = n * m - n - (m + 21)
if gain > best[0]:
actual_gain = len(text) - len(text.replace(snippet,"")) - n - (m + 21)
if actual_gain > best[0]:
best=(actual_gain, snippet)
done=False
if not done:
snippet = best[1]
try:
lastname = name
name = names.pop()
while name in 'ADGION':
text = name + '_' + text
name = names.pop()
while name in '?@':
text = '_' + text
name = names.pop()
except:
sys.stderr.write('Warning: out of names.\n')
lastname = "0"
break
text = name + snippet + '_' + text.replace(snippet, name)
sys.stdout.write('$_="')
sys.stdout.write(name + text)
sys.stdout.write('";eval"s'";$i=' + str(ord(lastname)) + ';eval"s/$1/$2/g"while s/(.)(.*?)_/chr($i++)/s;print'se;print')