返回顶部

收藏

Perl 为 Festival 编写的 Emacspeak speech server

更多

espeakf.pl

#!/usr/bin/perl

# EmacSpeaks Festival
#
# A Emacspeak speech server for Festival written in Perl.
# Written by Mario Lang <mlang@delysid.org>
# Enhancements by Aaron Bingham <abingham@sfu.ca>
# The skeleton was taken from speechd (and modified alot of course).

use strict;

# Configuration

my $USE_ESD = 1; 

    # If nonzero use the Enlightened Sound Daemon for managing the
    # Festival server, otherwise run Festival directly.  Overridden by
    # FESTIVAL_COMMAND

my $LOGFILE = '/tmp/festival.log'; 

    # File to use for logging debugging messages.

my $FESTIVAL_HOST = 'localhost'; 

    # Hostname where Festival server is running.  If FESTIVAL_HOST is
    # 'localhost', a Festival server process will be started
    # automatically if none is currently running.

my $FESTIVAL_PORT = '1314'; 

    # Port Festival server listens to.

my $FESTIVAL_BIN = '/usr/bin/festival'; 

    # Path to Festival executable.

my $FESTIVAL_ARGS = '--server \'(set! server_access_list (list "localhost\.localdomain"))\'';

    # Arguments for the Festival process.  The default starts a server
    # process accessible to any user on the local machine.

my $FESTIVAL_ASYNC = 1; 

    # Use Festival in asynchronous mode if nonzero, otherwise use
    # synchronous mode.  Asynchronous mode allows the client to stop
    # in-progress speech, so the result is generally more responsive
    # behavior.  However, some users have complained that asynchronous
    # mode causes too many utterances to be surpressed (e.g. letter
    # names while typing fast).  If this is a problem for you, set
    # $FESTIVAL_ASYNC = 0.

my $SPEAKER_1 = 'rab_diphone';
my $SPEAKER_2 = 'kal_diphone';

    # Name of the festival speakers to use.  Change this to voices you have
    # installed.

# Internal variables

my $FILE_STUFF_KEY = 'ft_StUfF_key'; # This indicates a new prompt from Festival
my $handle      = undef;                      # this is for the tcp connection...

my $queued_text = '';

my $FESTIVAL_COMMAND;
if ($USE_ESD) {
    $FESTIVAL_COMMAND = "esddsp $FESTIVAL_BIN $FESTIVAL_ARGS &";
} else {
    $FESTIVAL_COMMAND = "$FESTIVAL_BIN $FESTIVAL_ARGS &";
}

#includes libs for TCP socket connection to Festival
use IO::Socket;

my $err = 0;
my $pronounce_punctuation = 0;
my $emacs_in_braces = 0;
my @pq_queues = ([], []);
my %sable_params = ('speaker'=>$SPEAKER_1, 'rate'=>1, 'base'=>150, 
            'range'=>10, 'mid'=>1);
my $festival_busy = 0;
my $emacs_lines = 0;
my $line_number = 0;

sub main {

    my $emacstext = '';
    my $festtext = '';

    &log("INIT: FESTIVAL_COMMAND = $FESTIVAL_COMMAND\n");
    # create a tcp connection to the festival server
    &connect_to_festival();
    &log("INIT: Starting loop.\n");

    # If Festival closes the connection, try to reconnect
    local $SIG{PIPE} = \&connect_to_festival;

    my $info;

    while (1) {
        my $rin;
        vec($rin, fileno(STDIN), 1) = 1;
        vec($rin, fileno($handle), 1) = 1;
        select($rin, undef, undef, undef);
        if (vec($rin, fileno(STDIN), 1)) {
       my $buf;
       sysread STDIN,$buf,1024;
       if (!$buf) {
           &log("Unexpected EOF in STDIN\n");
           exit 1;
       }
       $emacstext .= $buf;
       $emacstext = &handle_emacs_input($emacstext);    
        }
        if (vec($rin, fileno($handle), 1)) {
       my $buf;
       sysread $handle,$buf,1024;
       if (!$buf) {
           &log("Unexpected EOF in Festival socket\n");
           exit 1;
       }
       $festtext .= $buf;
       $festtext = &handle_festival_input($festtext);
        }
        &log("\$festival_busy = $festival_busy\n");
        while (!$festival_busy && !&pq_empty()) {
       &send_command;
        }
    }
}

sub log {
    # Write a message to the logfile
    my $msg = shift;
    open(LOG, ">>$LOGFILE") or die "Cant open logfile\n";
    print LOG $msg;
    close LOG;
}

sub handle_emacs_input {
    my $emacstext = shift;
    while (1) {
    my ($line, $eol, $remainder) = split /(\n)/, $emacstext, 2;
    last if !$eol;
    $emacstext = $remainder;
    &handle_emacs_line($line . $eol);
    }
    return $emacstext;
}

sub handle_emacs_line {
    my $line = shift;
    &log("emacspeak: $line");
    $emacs_lines .= $line;
    if ($line =~ /{[^}]*\n/) {
    $emacs_in_braces = 1;
    } elsif ($line =~ /}/) {
        $emacs_in_braces = 0;
    }
    if (!$emacs_in_braces) {
        handle_emacs_command($emacs_lines);
    $emacs_lines = '';
    }
}

sub handle_festival_input {
    my $festtext = shift;
    while (1) {
    my ($line, $eol, $remainder) = split /(\n)/, $festtext, 2;
    last if !$eol;
    $festtext = $remainder;
    &handle_festival_line($line . $eol);
    }
    return $festtext;
}

sub handle_festival_line {
    my $text = shift;
    &log("festival: $text");

    if ($text =~ /^$FILE_STUFF_KEY/) {
    $festival_busy = 0;
    } elsif ($text =~ /^(OK|ER)/) {
    # the festival session is in a wierd state.  reconnect.
    &log("the festival is in a wierd state.  reconnect.\n");
    &send_to_festival("(quit)");
    &connect_to_festival();
    }
}

sub handle_emacs_command {
    my $text = shift;
    if ($text =~ /^\s*exit\n/) {
        &quit();
    } elsif ($text =~ /^\s*q\s+\{(.*)}/s) {
        &queue_speech($1);
    } elsif ($text =~ /^\s*t\s+(\d+)\s(\d+)/) {
    &tone($1, $2);
    } elsif ($text =~ /^\s*p\s+(.*)\n/) {
    &play_sound($1);
    } elsif ($text =~ /^\s*d/) {
        &flush_speech();
    } elsif ($text =~ /^\s*tts_say \{(.*)\}/) {
    &say($1);
    } elsif ($text =~ /^\s*l \{([^}])\}/) {
    &letter($1);
    } elsif ($text =~ /^\s*s/) {
    &stop();
    } elsif ($text =~ /^\s*tts_set_punctuations (\w+)/) {
    &set_punctuation($1);
    } elsif ($text =~ /^\s*tts_set_speech_rate (\d+)/) {
    &set_speech_rate($1)
    } elsif ($text =~ /^\s*tts_sync_state (\w+) (\d+) (\d+) (\d+) (\d+)/) {
    &set_punctuation($1);
    &set_speech_rate($5);
    } else {
       $err++;
       &log("$line_number: err$err: $text\n");
    }
    return 1;
}

# Actions

sub quit {
    &pq_clear();
    &stop();
    &send_to_festival("(quit)");
    exit 0;
}

sub tone {
    my $pitch = shift; # pitch in Hz
    my $duration = shift; # duration in ms

    # Run asynchronously for better responsiveness
    system("beep -f $pitch -l $duration &");
}

sub play_sound {
    my $filename = shift;
    my $url = "file://" . &url_quote($filename);
    &send_sable("<SABLE><AUDIO SRC=\"$url\"/></SABLE>");
}

sub flush_speech {
    # Flush all queued speech to the speech generator.
    if ($queued_text ne '') {
        speak($queued_text);
    }
    $queued_text = '';
}

sub letter {
    my $char = shift;
    my $content = &sgml_quote($char);
    &send_sable("<SABLE><SAYAS MODE=\"literal\">$content</SAYAS></SABLE>");
}

sub set_punctuation {
    my $mode = shift;
    if ($1 eq "all") {
    $pronounce_punctuation = 1;
    } else {
    $pronounce_punctuation = 0;
    }
}

sub set_speech_rate {
    my $dtk_rate = shift;
    # 225.0 was picked as it gives reasonable behavior.  I do not
    # know if the result is slower or faster than the DECTalk.
    $sable_params{'rate'} = $dtk_rate/225.0;
}

sub stop {
    # The queue must be cleared immediately so that any queued
    # commands recieved before the stop do not get run afterwards.    
    &pq_clear(); 
    $queued_text = '';
    if ($FESTIVAL_ASYNC) {
    &send_to_festival("(audio_mode 'shutup)");
    }
}

sub queue_speech {
    # Save speech to be sent later.
    my $text = shift;
    $queued_text .= $text;
}

sub say {
    my $text = shift;
    &speak($text);
}

sub speak {
    my $text = shift;
    if ($text =~ /\S+/) {
        foreach my $sable (&dtk_to_sable($text)) {
        &send_sable($sable);
        }
    } else { 
        &log("$line_number: Empty queue, nothing sent. \n"); 
    }
}

sub send_sable {
    my $sable = shift;
    $sable =~ s/"/\\"/g;
    &send_to_festival("(tts_text \"$sable\" 'sable)");
}

sub dtk_to_sable {
    my $dtk = shift;
    my @items = &dtk_parse($dtk);

    # '[*]' seems to be used as an alternative (perhaps shorter) to 
    # a space character.

    $dtk =~ s/\[\*\]/ /;

    my @sable_docs = ();
    foreach my $item (@items) {
        my $type = $item->[0];
        my $value = $item->[1];
    &log("($type, $value)\n");
        if ($type eq 'TEXT') {
            push @sable_docs, &text_to_sable($value);
        } elsif ($type eq 'COMMAND') {
        $value =~ s/\s+//; # trim leading whitespace
            &handle_dtk_command(split /\s+/, $value);
        }
    }
    return @sable_docs;
}

sub dtk_parse {
    my $dtk = shift;

    # Return a list of (type, value) tuples, where type is either
    # COMMAND or TEXT.

    my $in_command = 0;
    my @items = ();
    my $value = '';
    for (my $i = 0; $i <= length $dtk; $i++) {
        my $ch = substr $dtk, $i, 1;
    if ($ch eq '[') {
            if (!$in_command) {
            if ($value ne '') {
            my @item = ('TEXT', $value);
            push @items, \@item;
            }
            } else {
                &log("ERROR: [ found while looking for ]\n");
            }
        $in_command = 1;
        $value = '';
        } elsif ($ch eq ']') {
            if ($in_command) {
            if ($value ne '') {
            my @item = ('COMMAND', $value);
            push @items, \@item;
            }
            } else {
                &log("ERROR: ] found while looking for [\n");
            }
        $in_command = 0;
        $value = '';
        } else {
            $value .= $ch;
    }
    }
    if ($value ne '' && !$in_command) {
    my @item = ('TEXT', $value);
        push @items, \@item;
    }
    if ($in_command) {
        &log("ERROR: ] expected\n");
    }

    return @items;
}

sub handle_dtk_command {
    my @cmdlist = @_;
    &log ("cmdlist: @cmdlist\n");
    &log ("cmdlist[0]: $cmdlist[0]\n");
    if ($cmdlist[0] =~ /:np/) {
    $sable_params{'speaker'} = $SPEAKER_1;
    $sable_params{'base'} = 100;
        $sable_params{'range'} = 10;
    } elsif ($cmdlist[0] =~ /:nh/) {
    $sable_params{'speaker'} = $SPEAKER_2;
    $sable_params{'base'} = 100;
        $sable_params{'range'} = 10;
    } elsif ($cmdlist[0] =~ /:nd/) {
    $sable_params{'speaker'} = $SPEAKER_1;
    $sable_params{'base'} = 150;
        $sable_params{'range'} = 10;
    } elsif ($cmdlist[0] =~ /:nf/) {
    $sable_params{'speaker'} = $SPEAKER_2;
    $sable_params{'base'} = 150;
        $sable_params{'range'} = 10;
    } elsif ($cmdlist[0] =~ /:nb/) {
    $sable_params{'speaker'} = $SPEAKER_1;
    $sable_params{'base'} = 200;
        $sable_params{'range'} = 10;
    } elsif ($cmdlist[0] =~ /:nu/) {
    $sable_params{'speaker'} = $SPEAKER_2;
    $sable_params{'base'} = 200;
        $sable_params{'range'} = 10;
    } elsif ($cmdlist[0] =~ /:nr/) {
    $sable_params{'speaker'} = $SPEAKER_1;
    $sable_params{'base'} = 300;
        $sable_params{'range'} = 10;
    } elsif ($cmdlist[0] =~ /:nw/) {
    $sable_params{'speaker'} = $SPEAKER_2;
    $sable_params{'base'} = 300;
        $sable_params{'range'} = 10;
    } elsif ($cmdlist[0] =~ /:nk/) {
    $sable_params{'speaker'} = $SPEAKER_1;
    $sable_params{'base'} = 400;
        $sable_params{'range'} = 10;
    } elsif ($cmdlist[0] =~ /:dv/) {
    &log ("cmdlist (dv): @cmdlist\n");
    for (my $j = 1; $j <= $#cmdlist; $j+=2) {
        print "param: $cmdlist[$j] $cmdlist[$j+1]\n";
        if ($cmdlist[$j] =~ /ap/) {
                # Average pitch (Hz)
        $sable_params{'base'} = $cmdlist[$j+1];
        }
        if ($cmdlist[$j] =~ /pr/) {
                # Pitch range.  The Dectalk parameter ranges between 0
                # -- a flat monnotone -- and 250 -- a highly animated
                # voice.
        $sable_params{'range'} = 2.0*sqrt(5.0*$cmdlist[$j+1]);
        }
    }
    }
}

sub text_to_sable {
    my $text = shift;

    # Convert a string of plain text to a SABLE document, using the current
    # parameters.

    if ($pronounce_punctuation) {
    $text =~ s/[`]/ backquote /g;
    $text =~ s/[!]/ bang /g;
    $text =~ s/[(]/ left paren /g;
    $text =~ s/[)]/ right paren /g;
    $text =~ s/[-]/ dash /g;
    $text =~ s/[{]/ left brace /g;
    $text =~ s/[}]/ right brace /g;
    $text =~ s/[:]/ colon /g;
    $text =~ s/[;]/ semi /g;
    $text =~ s/["]/ quotes /g;
    $text =~ s/[']/ apostrophe /g;
    $text =~ s/[,]/ comma /g;
    $text =~ s/[.]/ dot /g;
    $text =~ s/[?]/ question /g;
    }
    # escape SGML-unsafe characters
    $text = sgml_quote($text);

    return <<HERE
<SABLE>
 <SPEAKER NAME="$sable_params{'speaker'}">
  <RATE SPEED="$sable_params{'rate'}">
   <PITCH BASE="$sable_params{'base'}" 
          RANGE="$sable_params{'range'}" 
          MID="$sable_params{'mid'}">
    $text
   </PITCH>
  </RATE>
 </SPEAKER>
</SABLE>
HERE
}

sub sgml_quote {
    my $text = shift;
    $text =~ s/&/&amp;/g;
    $text =~ s/</</g;
    $text =~ s/>/>/g;
    return $text;
}

sub url_quote {
    # XXX: incomplete!
    my $text = shift;
    $text =~ s/ /%20/;
    return $text;
}

sub send_to_festival {
    my $command = shift;
    # queue $command for sending to Festival
    &pq_push($command, 1);
}

sub send_command {
    # send a single queued command
    if ($handle) {   # Sanity checks are always nice...
    &send_direct(&pq_pop());
    } else {
    &connect_to_festival;
    }
}

sub send_direct {
    my $command = shift;
    # send $command to festival immediately, without affecting the queue
    &log("$line_number: festival> " . $command . "\n");
    print($handle $command . "\n") or die "Could not write to Festival ($!)\n";
    $festival_busy = 1;
}

sub connect_to_festival
{
  my $tries = 0;
  my $MAX_RECONNECT_TRIES = 10;
  $handle = '';
  while ($handle eq '' and $tries < $MAX_RECONNECT_TRIES)
  {
    &log("($tries) Attempting to connect to the Festival server.\n");
    if ($handle = IO::Socket::INET->new(Proto     => 'tcp',
                                        PeerAddr  => $FESTIVAL_HOST,
                                        PeerPort  => $FESTIVAL_PORT))
    {
      &log("Successfully opened connection to Festival.\n");
    } else
    {
      if ($tries)
      {
        &log("Waiting for Festival server to load -- Can't connect to port $FESTIVAL_PORT on $FESTIVAL_HOST yet ($!).\n");
      } else
      {
        if ($FESTIVAL_HOST eq 'localhost') {
          &log("Failed to connect to Festival server, attempting to load it myself.\n");
          system ($FESTIVAL_COMMAND);
        }
      }
      sleep 1;
    }
    $tries++;
  }

  if ($handle eq '') {
      die "ERROR: can't connect to Festival server!";
  }

  $handle->autoflush(1);     # so output gets there right away

  $festival_busy = 0;

  if ($FESTIVAL_ASYNC) {
      # Set festival to async mode.  We have to call send_direct here to
      # ensure that no commands from emacspeak hava a chance to get
      # executed before this one.

      &send_direct("(audio_mode 'async)");
  }

}

#
# priority queue implementation
#

sub pq_push {
    my $item = shift;
    my $pri = shift;
    push @{$pq_queues[$pri]}, $item;
}

sub pq_pop {
    for (my $i = 0; $i <= $#pq_queues; $i++) {
    if ($#{$pq_queues[$i]} >= 0) {
        my $item = shift @{$pq_queues[$i]};
        return $item;
        }
    }
}

sub pq_clear {
    for (my $i = 0; $i <= $#pq_queues; $i++) {
    $pq_queues[$i] = [];
    }
}

sub pq_empty {
    for (my $i = 0; $i <= $#pq_queues; $i++) {
    if ($#{$pq_queues[$i]} >= 0) {
        return 0;
        }
    }
    return 1;
}

&main();

__END__

=head1 NAME

festival-server - Emacspeak server for the Festival speech synthesizer

=head1 SYNOPSIS

speechd 

=head1 DESCRIPTION

festival-server is a perl script doing Emacspeak server syntax
to festival conversion.
This method makes use of the SABLE markup mode of festival.

Tones are supported if you have beep installed.

=head1 OPTIONS

=head1 FILES

=over 4

=item
/usr/share/emacs/site-lisp/emacspeak/festival-server

threads.pl

#!/usr/bin/perl-thread -w
# EmacSpeaks Festival
#
# A Emacspeak speech server for Festival written in Perl.
# Written by Mario Lang <mlang@delysid.org>
#
# Licensed under the terms of the GNU general public license.
#
# Threaded version:
#  This version of espeakf.pl is written using the Threads module
#  of perl. Threads are a quite new feature to perl and are
#  only available since perl 5.005. You probably have to compile
#  perl with thread support turned on explicitly.
#  Just start this script with your perl interpreter, and it will
#  report if Thread support is not compiled in.
#-----------------------------------------------------------------------------
use strict;
use Config;
$Config{usethreads} or die "Recompile Perl with threads to run this program.\n";
use Thread;
use IO::Socket;          # includes libs for TCP socket connection to Festival
use POSIX qw(strftime);  # strftime for sub logentry
use Data::Dumper;
use vars qw($child @queue $runchild
        $logfile $host $port $cmd
        $handle $cmdcnt $err $file_stuff_key);
# Configuration section:
#  Change the variables below to affect general behavior
#-----------------------------------------------------------------------------
$logfile = '/tmp/festival.log';    # Were is useful debugging into written to?
$host    = 'localhost';            # Were is the Festival server running?
$port    = '1314';
# This is used to start the Festival server when host=locahost
# and a connection attempt failed.
$cmd     = '/usr/bin/festival --server &';
# The stuff_key is used to recognize command-prompts from Festival.
# Only change it if you also changed this in EST.
$file_stuff_key = 'ft_StUfF_key';
# END OF CONFIG SECTION
# Only change things below this line if you know what you are doing..
#-----------------------------------------------------------------------------

$handle = undef;                   # this is for the tcp connection...
$cmdcnt = 0;
$runchild = 1;                     # Loop the child-thread

# Open logfile for writing
# This is very important for debugging.. 
# ATTENTION: Logfile gets overwritten everytime the server is restarted
# s/^>/>>/ to append to logfile.
open(LOG, ">$logfile") or die "Can't open logfile.. $!\n";
&logentry("(Re)started");
# create a tcp connection to the festival server
$handle = &connect_to_festival();
local $SIG{PIPE} = \&connect_to_festival;

$child = new Thread \&childsub;
my $line;
while (defined ($line = <STDIN>)) {
    $cmdcnt++;
    next if $line eq "\n";
    last if $line =~ /^\s*exit\s*\n/;
    if ($line =~ /q\s+\{.*\}/) {
    &logentry("Single line query: '$line'");
    my ($extract) = $line =~ /q\s+\{(.*?)\}/;
    &add_text_to_queue($extract);
    &logentry("Extracted: '$extract'");
    } elsif ($line =~ /\s*d/) {
    &add_send_to_queue;
    } elsif ($line =~ /\s*s/) {
    &add_stop_to_queue;
    } else {
    $err++;
    &logentry("Error$err: '$line' is not a recognized command.");
    }
}
&logentry("Exited main loop.. Joining Threads..");
$runchild = 0;
print $child->join();
&logentry("Thread joined. Exiting..");
close(LOG);
exit 0;

sub childsub {
    my $text;
    my $remains;
    my $i;
    while (!&empty_queue) {sleep 5}
    while (($text = $remains) || defined ($text = <$handle>)) {
    undef $remains;
    if ($text eq "WV\n") { # we have a waveform coming
        # This is not implemented yet
    } elsif ($text eq "LP\n") {
        while ($text = <$handle>) {
        if ($text =~ s/$file_stuff_key(.*)$//s) {
            $remains = $1;
            &logentry("Festival: $text");
            last;
        }
        &logentry("Festival: $text");
        }
    } else { # if we don't recognize it, echo it
        &logentry("Festival: $text");
    }
    }
}

sub add_text_to_queue {
    my $text = shift;
    {   lock(@queue);
    $queue[$#queue+1] = "string: $text";
    }
}
sub add_send_to_queue {
    {   lock(@queue);
    $queue[$#queue+1] = "send";
    }
}
sub add_stop_to_queue {
    {   lock(@queue);
        $queue[$#queue+1] = "stop";
    }
}
sub empty_queue {
    if ($#queue==-1) { return 0; }
    print Dumper(@queue);
    my %lookup;
    my $i;
    my $sendc = 0;
    my $stopc = 0;
    for ($i=0;$i<=$#queue;$i++) {
    if ($queue[$i] =~ /^send/) { $lookup{send}[$sendc] = $i; $sendc++}
    elsif ($queue[$i] =~ /^stop/) { $lookup{stop}[$stopc] = $i; $stopc++}
    }
    print "$sendc, $stopc\n";
    if ($stopc >0 && $sendc >0) {
    if ($lookup{stop}[0] > 0) {
        # The first stop is irrelevant
    }
    }

    return 0;
}

sub connect_to_festival
{
    my $handle = '';
    my $tries = 0;
    while ($handle eq '') {
    &logentry("($tries) Attempting to connect to the Festival server.");
    if ($handle = IO::Socket::INET->new(Proto     => 'tcp',
                        PeerAddr  => $host,
                        PeerPort  => $port))
    {
        &logentry("Successfully opened connection to Festival.");
    } else {
        if ($tries) {
        &logentry("Waiting for Festival server to load -- Can't connect to port $port on $host yet ($!).");
        } else {
        if ($host eq 'localhost') {
            &logentry("Failed to connect to Festival server, attempting to load it myself.");
            system ($cmd);
        }
        }
        sleep 1;
    }
    $tries++;
    }
    $handle->autoflush(1);     # so output gets there right away
    return $handle;
}

sub logentry {
    my $logline = shift;
    chomp($logline);
    my $logtime = strftime "%b %e %H:%M:%S", localtime;
    print LOG "$logtime: $logline\n";
    return 0;
}

标签:Perl,speech,Emacspeak,Festival

收藏

0人收藏

支持

0

反对

0

相关聚客文章
  1. 王 聪 发表 2012-11-30 15:22:20 发送 IGMP 包的 Perl 脚本
  2. 博主 发表 2013-08-13 16:00:00 BeiJing Perl Workshop 2013 参会总结
  3. 博主 发表 2011-02-28 16:00:00 CU的perl大赛
  4. 博主 发表 2013-10-15 16:00:00 Perl 的 overload 妙用
  5. 博主 发表 2013-03-28 07:28:00 Perl入门实战:JVM监控脚本(下)
  6. 博主 发表 2013-03-26 15:00:00 Perl入门实战:JVM监控脚本(上)
  7. SeisMan 发表 2013-08-04 06:19:00 利用Web Service Fetch scripts申请和下载数据
  8. 博主 发表 2013-09-17 02:09:00 [翻译] Unicode 与 Perl
  9. 扶 凯 发表 2015-04-23 09:52:59 rpmbuild 时自动索引包的依赖性
  10. 博主 发表 2015-07-06 14:37:47 我的第一个perl脚本
  11. 摩摩诘 发表 2015-07-03 15:36:03 扯淡:计算数组元素出现次数
  12. Gavin 发表 2017-04-20 05:36:23 运维领域常用的Perl语言成为五大即将消失的编程语言之一