Sunday, July 29, 2007

Tracking fast-paced packages on debian based systems (aka debian-volatile project)

debian-volatile
If you run some ISP services (your own mail server with virus and/or spam scanning tools) you will have run into the age old problem that the scanning tools in the stable distribution do not evolve as fast as they should to keep up with their fast-paced projects.

Even continual updates of the software in your distribution are not enough to stay up to date as the release cycle of the stable distribution is out of sync with the speed at which things change in the wild.

According to the debian-volatile project page:

The main goal of volatile is allowing system administrators to update their systems
in a nice, consistent way, without getting the drawbacks of using unstable, even
without getting the drawbacks for the selected packages. So debian-volatile will
only contain changes to stable programs that are necessary to keep them functional.

volatile-sloppy
Great effort goes into ensuring that no functional changes are made to packages in debian-volatile (so that configuration file changes, etc. are not required) for painless upgrades. Unfortunately painful upgrades are not always avoidable so a volatile-sloppy section was created to contain packages that are fast-paced but also require some functional change to how it runs, is installed or configured.

Security
You should note that the debian-volatile project is not supported by the _official_ security team. This responsibility falls to the debian-volatile team who currently has at least one member that is shared with the official debian testing security team.

How do I use it?
Add the relevant repository (volatile and/or volatile-sloppy) to your /etc/apt/sources.list file:

Sarge
deb http://volatile.debian.org/debian-volatile sarge/volatile main contrib non-free
deb http://volatile.debian.org/debian-volatile sarge/volatile-sloppy main contrib non-free

Etch
deb http://volatile.debian.org/debian-volatile etch/volatile main contrib non-free
deb http://volatile.debian.org/debian-volatile etch/volatile-sloppy main contrib non-free


Save sources.list and run _atp-get update_ which should generate something like this (your listing will vary depending on the repositories you have listed in your sources file):

# apt-get update
Get:1 http://archive.ubuntu.com dapper Release.gpg [189B]
Get:2 http://us.archive.ubuntu.com dapper Release.gpg [189B]
Get:3 http://us.archive.ubuntu.com dapper-backports Release.gpg [191B]
Get:4 http://archive.ubuntu.com dapper-updates Release.gpg [191B]
Get:5 http://volatile.debian.org etch/volatile Release.gpg [189B]
Hit http://us.archive.ubuntu.com dapper Release
Hit http://archive.ubuntu.com dapper Release
Get:6 http://volatile.debian.org etch/volatile Release [40.7kB]
Hit http://archive.ubuntu.com dapper-updates Release
Hit http://us.archive.ubuntu.com dapper-backports Release
Hit http://archive.ubuntu.com dapper/main Packages
Hit http://archive.ubuntu.com dapper/restricted Packages
Hit http://us.archive.ubuntu.com dapper/universe Packages
Hit http://us.archive.ubuntu.com dapper/universe Sources
Hit http://archive.ubuntu.com dapper/main Sources
Hit http://archive.ubuntu.com dapper/restricted Sources
Hit http://archive.ubuntu.com dapper-updates/main Packages
Hit http://archive.ubuntu.com dapper-updates/restricted Packages
Hit http://us.archive.ubuntu.com dapper-backports/main Packages
Hit http://us.archive.ubuntu.com dapper-backports/restricted Packages
Hit http://us.archive.ubuntu.com dapper-backports/universe Packages
Hit http://archive.ubuntu.com dapper-updates/main Sources
Hit http://archive.ubuntu.com dapper-updates/restricted Sources
Hit http://us.archive.ubuntu.com dapper-backports/multiverse Packages
Hit http://us.archive.ubuntu.com dapper-backports/main Sources
Hit http://us.archive.ubuntu.com dapper-backports/restricted Sources
Hit http://us.archive.ubuntu.com dapper-backports/universe Sources
Hit http://us.archive.ubuntu.com dapper-backports/multiverse Sources
Ign http://volatile.debian.org etch/volatile Release
Get:7 http://volatile.debian.org etch/volatile/main Packages [3953B]
Hit http://volatile.debian.org etch/volatile/contrib Packages
Hit http://volatile.debian.org etch/volatile/non-free Packages
Get:8 http://security.ubuntu.com dapper-security Release.gpg [191B]
Hit http://security.ubuntu.com dapper-security Release
Hit http://security.ubuntu.com dapper-security/main Packages
Hit http://security.ubuntu.com dapper-security/restricted Packages
Hit http://security.ubuntu.com dapper-security/main Sources
Hit http://security.ubuntu.com dapper-security/restricted Sources
Hit http://security.ubuntu.com dapper-security/universe Packages
Hit http://security.ubuntu.com dapper-security/universe Sources
Fetched 44.8kB in 5s (7554B/s)
Reading package lists... Done
W: GPG error: http://volatile.debian.org etch/volatile Release: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY EC61E0B0BBE55AB3
W: You may want to run apt-get update to correct these problems
#

The inclusion of the debian-volatile release fails because we do not have a key to authenticate the repository. Adding the following will import their key (mentioned as EC61E0B0BBE55AB3 above) into your key ring:

# gpg --keyserver subkeys.pgp.net --recv-keys EC61E0B0BBE55AB3
gpg: directory `/root/.gnupg' created
gpg: new configuration file `/root/.gnupg/gpg.conf' created
gpg: WARNING: options in `/root/.gnupg/gpg.conf' are not yet active during this run
gpg: keyring `/root/.gnupg/secring.gpg' created
gpg: keyring `/root/.gnupg/pubring.gpg' created
gpg: requesting key BBE55AB3 from hkp server subkeys.pgp.net
gpg: /root/.gnupg/trustdb.gpg: trustdb created
gpg: key BBE55AB3: public key "Debian-Volatile Archive Automatic Signing Key (4.0/etch)" imported
gpg: no ultimately trusted keys found
gpg: Total number processed: 1
gpg: imported: 1
# gpg --armor --export EC61E0B0BBE55AB3 | apt-key add -
gpg: no ultimately trusted keys found
OK
#

Another spin of _apt-get update_ (and possibly _apt-get upgrade_ if their are any outdated packages) should then do the trick.

Tuesday, July 24, 2007

Links

IRB
Dr Nic has a great little article describing his favourite additions to his .irbrc. He covers some common productivity wins such as:
  • TABed auto-completion
  • Map by method which allows you to get rid of constructs like articles.columns.map {|p| p.name} or articles.columns.map &:name and simply replace it with a plural: articles.columns.names or articles.columns.name
  • MethodFinder/Object.what?
  • pp
  • Auto-tabbing
GuessMethod
This little gem is the best bad idea ever! Gone are those frustrating typos that waste extra cycles finding and fixing them.

Wirble
Continuing on the irb enhancements theme do yourself a favour and have a look at Wirble. It offers you tab-completion, history, and a built-in ri command as well as colorised results and a couple other goodies.

Tuesday, July 17, 2007

Links

Sequel
Sequel is a light-weight ORM that fills in the gaps where using ActiveRecord without rails doesn't fit your needs.

Gregory Brown has a nice little expose on it as the winner of the June 2007 Ruby Project Spotlight.

Introduction to .NET 3.0 for Architects
Keep your friends close and competition^H^H^H^H^H^H^H^H^H^H alternative platforms closer. InfoQ has a great 50000 foot look at .NET 3.0.

Profiling your ails app with ruby-prof
Charlie Savage wrote a great article on doing some profiling on your rails app using ruby-prof. He discusses using both flat and graph (with associated call tree information) profiles to nuke performance hogs.

ruby-prof is a fast code profiler for Ruby. Its features include:
  • Speed - it is a C extension and therefore many times faster than the standard Ruby profiler.
  • Flat Profiles - similar to the reports generated by the standard Ruby profiler
  • Graph profiles - similar to GProf, these show how long a method runs, which methods call it and which methods it calls
  • Threads - supports profiling multiple threads simultaneously
  • Recursive calls - supports profiling recursive method calls
  • Reports - can generate both text and cross-referenced html reports
  • Output - can output to standard out or to a file

AR-Delegation
This plugin extends ActiveRecord::Base to add useful delegation features. For example: has_columns :from => :source, :only => ["title", "name"] has_column "title", :from => :source, :as => "source_title".

It really improves the conciseness of your code but in so doing hides your implementation adding a layer of indirection that may make your code a little more difficult to understand if the person reading your code does not know that AR-Delegation was used.

Exception Notifier
I use this with most of my projects that reside on remote customer networks where the only way for the application to give me a heads up is if it sends me an email with an attached problem report.

Saturday, May 19, 2007

The Journey from Lambda to Unlambda

While scrounging around the Net looking at functional programming languages I came across Unlambda. The name by itself piqued my interest so I though I'd go have a look-see.

Here follows the troubled tale of the journey from Lambda to Unlambda ...
Nature of the Beast
It was created by David Madore and is a minimal functional programming language that was specifically built to make programming in it obtuse (or as the author's site refers to it as: "... fun and challenging"). It is based on combinatory logic but omits the lambda abstraction forcing you to rely on K and S combinators.

Beside hailing itself as a functional language it also succeeds in being an obfuscated programming language through severely restricting the set of allowed operations in the language and making it generally alien to programmers from more conventional languages.

It strictly only manipulates functions. A function is the only function parameter that you can pass in to a function and a function is the only construct that can be returned from a function. It relies heavily on its built-in k and s functions (K and S combinators) to get anything done.

The source is built to be intentionally incomprehensible for a human making it next to impossible to deduce what the intention of the program is by simply reading the source.

You are welcome to create your own functions, but be warned, you cannot name or save the custom function(s) because Unlambda does not have support for variables. Besides dispelling variables from its cloth you will also notice the lack of built-in support for data structures or code constructs (e.g. loops, conditionals, etc.). You are however welcome to build your own code constructs.

As an illustration, here is a loop that prints “Hello, world!” repeatedly, followed by an incrementing number of asterisks (an explanation of this severely limited subset of alphabet soup can be found here):

```s``sii`ki
``s``s`ks
``s``s`ks``s`k`s`kr
``s`k`si``s`k`s`k
`d````````````.H.e.l.l.o.,. .w.o.r.l.d.!
k
k
`k``s``s`ksk`k.*

Unlambda is the little functional language that could however and in the face of all its consciously implemented obscurity it still manages to be Turing-complete.

Sure, but can it speak?
Here's the Unlambda equivalent for the trusty old "Hello world" we're used to:

`r```````````.H.e.l.l.o. .w.o.r.l.di

More examples, a tutorial and some HOWTOs can be found at David's site for Unlambda.

Secret Murmurings on Unlambda

  • “It's disgusting — it's revolting — we love it.” CyberTabloid

  • “Unlambda, the language in which every program is an IOUCC.” Encyclopædia Internetica

  • “The worst thing to befall us since Intercal.” Computer Languages Today

  • “The effect of reading an Unlambda program is like having your brains smashed out by a Lisp sexp wrapped around an ENIAC. You won't find anything like it west of Alpha Centauri.” The Hitch-Hacker's Guide to Programming



Epitaph
Unlambda is inevitably compared to Intercal, but unlike Intercal it has a kind of weird elegance; this is because Intercal gets in your way, but Unlambda simply fails to help you."

Tuesday, May 15, 2007

OS X File Name Subterfuge

While saving a file via the well known OS X file Save As dialog I noticed something really queer. The filename I had pasted in was magically altered to adhere to OS X's ideas on which characters are kosher for file/directory names!

Silent Substitution
To see the magic simply open TextEdit, create a new document (if you are not already presented with one) and add the following text to the document:

watch: this: space:


Now, select the text, copy it and save the file with the selected text as the file name.



Et voilà!

Subterfuge Uncorked
The dialog magically converts the colons to hyphens (-) to ensure that you don't have to go through he whole 'This filename is not allowed' error cycle needlessly. Brill.

The reason for the substitution is that OS X prohibits the use of colon characters in file/directory names because this character is used to represent a directory in the HFS+ file system.

According to the HFS+ spec you can use any Unicode or ASCII (including NUL) characters. OS APIs may limit some of these characters for legacy reasons.

I love subtlety.

Dynamic Arbitrary Depth Hashes In Ruby

UPDATE: Charles Duan has an interesting article in a similar vein.

Arbitrary array and hash depth constructs cannot be created in Ruby in the way you would in Perl or PHP. The following will simply fail with an error:

irb(main):001:0> a = []
=> []
irb(main):002:0> a[1][2][3][4] = 1
NoMethodError: undefined method `[]' for nil:NilClass
from (irb):2
irb(main):003:0> h = {}
=> {}
irb(main):004:0> h[1][2][3][4] = 5
NoMethodError: undefined method `[]' for nil:NilClass
from (irb):4

When dynamically constructing your array or hash (aka Autovivification) this really gets in the way.

Autovivification
This is a dynamic data structure creation feature that can be found in Perl and PHP (those are the ones I know of). It allows you to create dynamic, complex, nested data structures based on the types implied in the syntax of the statement of code accessed through the data structure.

IOW, the act of fetching or storing a value at a leaf through a branch dynamically creates the branch(es) to the leaf.

VivifiedHash
One approach is to do the following:

irb(main):011:0* VivifiedHash = Hash.new(&(p=lambda{|h,k| h[k] = Hash.new(&p)}))
=> {}
irb(main):012:0> VivifiedHash[1][2][3][4] = 5
=> 5
irb(main):013:0> VivifiedHash[1][2][3][4]
=> 5
irb(main):014:0> VivifiedHash[1][2][3]
=> {4=>5}
irb(main):015:0> VivifiedHash[1][2]
=> {3=>{4=>5}}
irb(main):016:0> VivifiedHash[1]
=> {2=>{3=>{4=>5}}}
irb(main):017:0> VivifiedHash
=> {1=>{2=>{3=>{4=>5}}}}

All this does is recursively assign the default key of the hash a new hash object as value. Each branch you specify in your assignment will recursively trigger the creation of a new hash.

Limitations
The limitation on this are of course that your data structure cannot contain anything but hashes as branches. Leaf nodes can be any data type though.

Sources

  1. Ruby Hashes of Arbitrary Depth

  2. Multidimensional arrays and hashes discussion on the RubyTalk mailing

  3. Auto Vivification

Monday, May 7, 2007

Mechanized Scraping

Ever needed to interface with a web application without any real APIs? Take one step back from looking for a traditional API and use WWW::Mechanize to bend the application to your will.

WWW:Mechanize (inspired by "Andy Lester's":mailto://andy@petdance.com perl Mechanize module and written by Aaron Patterson) allows you to moonlight as a web User Agent (browser) from the comfort of your ruby scripting environment. It is great for building automated tests of your web applications, creating your favourite mashups and also to treat another web application's UI as the API to the application.

I've been working on some code that needs to gather reporting information from our billing system but I have no real access to the Oracle db in the back to get to the require stored procedures. So, I decided to simply use the UI as my API to the data and dusted my trusty old WWW:Mechanize (which uses Hpricot internally to parse and tokenise pages) off for the challenge.

It provides you with all the required tools to log in to a site (as well as automatic cookie handling), click on URI, submit forms and oh so much more. The only real feature currently lacking is support for JavaScript (they do however provide you with ideas on how to manoeuvre around some of the more mundane corers) which is becoming more and more painful in this Web2.0 world of ours.

WWW:Mechanize is quite easy to use so I am not going to write an exposé on the in's and out's of the lib or share with you its secrets that helped me to sate world hunger and bring peace to all. Instead, I will mention some of the bits that tripped me up while trying to make the web application dance to my flute.

Button Value Attributes
I was getting nowhere while trying to submit a form in the web application with some crafted values. Tinker here, tinker there and still no go. Try a browser and the application itself and things work like swiss cheesewatches.

Right you mangy ASP application, its time for the big guns! Out comes Wireshark and the debugging starts in earnest. First I dump a session from my script and then one from a browser.

From the diff of of the POST request I notice that the browser has the value attribute for the 'Save' button in the form set whereas I didn't. Because the form was posting back to itself I assume they had some code like (pseudocode):

if $submit == 'Submit'
then
do your stuff when the form has been submitted
else
display the normal form
end

Adding something that resembles the following did the trick:

form.buttons.name('some_convoluted_button_name').value = 'Submit'


Out of Buffer Error
A few more form hoops later and I started getting an error like:

hpricot/parse.rb:44:in `scan': ran out of buffer space on element <group>, starting on line 361. (Hpricot::ParseError?)

Hey?!

A quick look on the bug db for WWW:Mechanize on RubyForge listed this closed bug that has some application to our situation. The error messages are not the same (I assume this is the case due to an earlier version of Hpricot that was used when this was reported).

According to this TT it is a Hrpicot issue and refers to this TT.

According to the problem description:

An 'OUT OF BUFFER SPACE' error shuts down my whole app when I try to parse through an aspx page with an abnormally (or normally?) large viewstate stuffed into an input. Here's what it looks like:

<input type="hidden" name="__VIEWSTATE"
value="dDw3NzQ0ODQ2ODQ ... 11954 characters in total ... DsXdJfP+k" />

If I remove the large value it works fine. Is there a way hpricot could not exit when trying to parse a page like this?

DING! DING DING!

I am also scraping an ASP application and lo and behold I too have a ginormous __VIEWSTATE input tag in the page in question. I knew ASP was evil, but this?!

The limit on the buffer was of course a protection mechanism to ensure that a parsed page does not cause your computer to become the black hole of memory. The workaround for this is quite simple though, just increase the buffer

Okay, kids. [98] now has a buffer_size method.
Hpricot.buffer_size = 262144
doc = Hpricot(open("http://asp.net/big-viewstate-vomit.html"))

Perhaps I will find the wherewithal to fix the parser to read these massive attributes, but on-the-other-hand I don't want to encourage this disastrous behavior by ASP.NET!! You know?

"That's all good and well but we're not really using Hpricot directly, we're using WWW:Mechanize!", you all shout in unison.

True, true. All you do is simply add the buffer_size declaration after instantiating your shiny new WWW:Mechanize object like so:

agent = WWW::Mechanize.new
Hpricot.buffer_size = 204800

The default buffer size is defined in hpricot_scan.rl as:

[...]

#define BUFSIZE 16384

[...]

buffer_size = BUFSIZE;
if (rb_ivar_defined(self, rb_intern("@buffer_size")) == Qtrue) {
bufsize = rb_ivar_get(self, rb_intern("@buffer_size"));
if (!NIL_P(bufsize)) {
buffer_size = NUM2INT(bufsize);
}
}
buf = ALLOC_N(char, buffer_size);

[...]

That's a buffer of about 16KB for an attribute which under normal circumstances would be more than ample space for an attribute but working with ASP seems to be anything but normal.

In Closing
I have not had as much fun in quite some time. WWW:Mechanize had me clapping my little hands in glee while shouting "Wheeeeeeeeeee!" like a little kid that was given his first bunny rabbit just after having his second double espresso for the hour.

Wednesday, May 2, 2007

Ruby (Hpricot) Program Guide - III

As discussed in the previous article our next steps will be to refactor the constructor and provide an example of how we can use objects from the DSTVSchedule class to collect and display channels of our choice.

Let's change the constructor to take the channel ID, time offset (to account for different time zones) and the period ahead in time for which we want to gather schedule information as parameters. This will mean that we get rid of the custom hash class and tidy things up a little bit:

def initialize(channel=219, offset=2, period=30)
start_date, end_date = get_search_dates(period)
url = build_url(build_query_string(channel, start_date ,end_date))

p "Start: #{start_date} End: #{end_date} URL: #{url}"

@hp = Hpricot(open(url))
@ic = Iconv.new('US-ASCII//TRANSLIT', 'UTF-8')
@coder = HTMLEntities.new
@schedule = process_html(@hp, offset)
end

def get_search_dates(period=30)
[DateTime.now().strftime("%d %b %Y"), (DateTime.now()+period).strftime("%d %b %Y")]
end

def build_query_string(channel, start_date, end_date)
urlencode({
'channelid' => channel,
'startDate' => start_date,
'EndDate' => end_date}) +
'&sType=5&searchstring=&submit=Submit'
end

def build_url(query_string)
host = 'www.mnet.co.za'
cgi = '/schedules/default.asp?'
"http://#{host}#{cgi}#{query_string}"
end

def urlencode(hash)
hash.map {|k, v| "#{URI::encode(k.to_s)}=#{URI::encode(v.to_s)}"}.join('&')
end

We no longer statically define the query parameters in the constructor and therefore have no real need for the custom hash. We can still use the urlencode() method though and add it as a helper in the class.

The start and end dates for the query are calculated based on today's date and the period provided to the constructor as an argument.

We also dumped all that horrible looking query string and url variable construction code into separate methods.

The next step is to provide some automation to the channel schedule collection code for our example program. Look at the the HTML data in any of the search pages and you'll see the following (excerpt):

<select name="channelid" class="ScheduleInputSelect">
<option value="" >CHANNEL</option>
<option value=246>actionX </option>
<option value=322>Activate </option>
<option value=496>Africa Magic</option>
<option value=487>Africa Magic Channel (C-Band) </option>
<option value=639>Africa Magic W4</option>
<option value=417>Animal Planet </option>
[...]
<option value=254>TV Globo </option>
<option value=493>TV5 Afrique </option>
<option value=110>TV5 Afrique (Africa) </option>
<option value=65>VH1 </option>
<option value=67>ZEE TV </option>
</select>

These are the channels that we can search for. What we need is to represent this information as an internal data structure that we can use to search for the channels we want. I suggest a hash that has the channel name as a key and the channel ID and offset as a tuple.

I am lazy so I'd prefer to avoid typing all that information up or manually trying to transform it in the editor. Perhaps we can use some good old command line ruby to chew up and spit out the code we need which we can then just cut 'n paste or import (depending on the editor you use).

Copy the HTML and drop it in a file somewhere. Let's call the file in.html and run it through this command line script (output is truncated):

$ ruby -n -e '$_=~/value=(\d+)\>(.+)\s+\</;if $1&&$2 then a=$1;b=$2;print "\# \"#{b.sub(/\s+$/,"")}\" => [#{a}, 120],\n" end' < in.html | head
# "actionX" => [246, 120],
# "Activate" => [322, 120],
# "Africa Magic Channel (C-Band)" => [487, 120],
# "Animal Planet" => [417, 120],
# "B4U Movies" => [227, 120],
# "BBC Food" => [284, 120],
# "BBC Prime" => [121, 120],
# "BBC World" => [5, 120],
# "Bloomberg Information TV" => [8, 120],
# "Boomerang" => [314, 120],
[...]

Now take the output and place it in your script as a hash (as described above):

channels = {
# "actionX" => [246, 120],
# "Activate" => [322, 120],
# "Africa Magic Channel (C-Band)" => [487, 120],
# "Animal Planet" => [417, 120],
# "B4U Movies" => [227, 120],
"BBC Food" => [284, 120],
"BBC Prime" => [121, 120],
# "BBC World" => [5, 120],
# "Bloomberg Information TV" => [8, 120],
# "Boomerang" => [314, 120],
# "BVN" => [270, 120],
# "Canal+ Horizons" => [237, 120],
# "Cartoon Network" => [13, 120],
# "Cartoon Network (Africa)" => [219, 120],
# "Cartoon Network (W4)" => [182, 120],
# "Channel O - Sound Television" => [27, 120],
# "China Central Television 4" => [15, 120],
# "China Central Television 9 (Africa)" => [226, 120],
# "CNBC" => [90, 120],
# "CNBC (Africa)" => [194, 120],
# "CNBC (W4)" => [187, 120],
# "CNN International" => [18, 120],
# "Deukom - 3SAT" => [165, 120],
# "Deukom - ARD" => [93, 120],
# "Deukom - DW" => [94, 120],
# "Deukom - PRO 7" => [164, 120],
# "Deukom - RTL" => [91, 120],
# "Deukom - SAT 1" => [92, 120],
# "Deukom - ZDF" => [95, 120],
"Discovery Channel" => [21, 120],
# "E-Entertainment" => [646, 120],
"ESPN" => [24, 120],
# "eTV" => [111, 120],
# "Fashion TV" => [145, 120],
# "Fashion TV (Africa)" => [196, 120],
# "Fashion TV (W4)" => [216, 120],
"GO" => [542, 120],
# "Go (K-World Teen)" => [341, 120],
"Hallmark Entertainment Network" => [32, 120],
"History Channel" => [484, 120],
# "History Channel (Africa)" => [485, 120],
# "K-TV World" => [36, 120],
# "KTV (Indian Bouquet)" => [501, 120],
# "kykNET" => [112, 120],
# "M-Net Domestic" => [39, 120],
"M-Net East (Africa)" => [40, 120],
"M-Net Series" => [75, 120],
# "MK89" => [592, 120],
# "Movie Magic (Africa)" => [57, 120],
"Movie Magic 2 (Africa)" => [234, 120],
# "Movie Magic 2 (W4)" => [233, 120],
# "MTV" => [42, 120],
# "MTV Base" => [69, 120],
"National Geographic" => [102, 120],
# "NDTV" => [499, 120],
# "Parliamentary Service" => [45, 120],
# "Pay Per View" => [109, 120],
"Reality TV" => [248, 120],
# "Rhema Network" => [46, 120],
# "RTPi" => [48, 120],
# "SABC 1" => [84, 120],
# "SABC 2" => [85, 120],
# "SABC 3" => [86, 120],
# "SABC Africa" => [87, 120],
# "SIC" => [255, 120],
# "Sky News" => [120, 120],
"Sony Entertainment" => [228, 90],
# "Summit" => [104, 120],
# "Sun TV" => [500, 120],
# "SuperSport" => [52, 120],
# "SuperSport 2" => [54, 120],
# "SuperSport 3" => [80, 120],
# "SuperSport 3 (W4)" => [172, 120],
# "SuperSport 5" => [208, 120],
# "SuperSport 5 (Africa)" => [252, 120],
# "SuperSport 5 (W4)" => [251, 120],
# "SuperSport 6" => [209, 120],
# "SuperSport 7 (C-Band)" => [580, 120],
# "SuperSport Zone Mosaic" => [235, 120],
# "TellyTrack" => [34, 120],
# "Travel Channel" => [61, 120],
# "Trinity Broadcasting Network" => [276, 120],
# "Turner Classic Movies" => [59, 120],
# "Turner Classic Movies (Africa)" => [60, 120],
# "Turner Classic Movies (W4)" => [181, 120],
# "TV Globo" => [254, 120],
# "TV5 Afrique" => [493, 120],
# "TV5 Afrique (Africa)" => [110, 120],
# "VH1" => [65, 120],
# "ZEE TV" => [67, 120]
}

You'll notice I have removed the comments from any of the channels I want (I recommend you do the same for the channels you may be interested in). I also added a default time offset of 2 hours (120 minutes) for most of the channels to adjust the time for my time zone. You can change this in the command line ruby filter above to suit your needs.

All we need to do now is wrap our object creation and the output from it in a loop and we're off:

channels.keys.each do |channel|
p "Channel: #{channel}"
schedule = DSTVSchedule.new(channels[channel][0], channels[channel][1], 30)
schedule.print_schedule
print "\n\n"
end

All done. Here is the complete script source listing:

#!/usr/bin/ruby

class DSTVSchedule
require 'rubygems'
require 'hpricot'
require 'open-uri'
require 'htmlentities'
require 'iconv'
require 'collections/sequenced_hash'

def initialize(channel=219, offset=2, period=30)
start_date, end_date = get_search_dates(period)
url = build_url(build_query_string(channel, start_date ,end_date))

p "Start: #{start_date} End: #{end_date} URL: #{url}"

@hp = Hpricot(open(url))
@ic = Iconv.new('US-ASCII//TRANSLIT', 'UTF-8')
@coder = HTMLEntities.new
@schedule = process_html(@hp, offset)
end

def process_html(hp, offset)
schedule = SequencedHash.new
date = ""
time = ""
(hp/"td").each do |line|
case line.inner_html
when /ScheduleChannel/
@channel = sanitize((line/"[@class='ScheduleChannel']").inner_html)
when /(ScheduleDate|date)/
date = utf7((line/"[@class='ScheduleDate']|[@class=date]").inner_html)
schedule[date] = SequencedHash.new
when /ScheduleTime/
time = sanitize((line/"[@class='ScheduleTime']").inner_html)
time = (Time.parse("#{date} #{time}") + (60 * offset)).strftime("%H:%M")
schedule[date][time] = []
when /ScheduleTitle/
schedule[date][time] << sanitize((line/"[@class='ScheduleTitle']").inner_html)
when /\<p\>/
schedule[date][time] << sanitize((line/"p").inner_html)
end
end

schedule
end

def to_s
self.print_schedule("\t")
end

alias :to_tdt :to_s

def to_csv
##TODO - Add channel to the output
self.print_schedule(",")
end

def print_schedule(separator="||")
sep = separator
@schedule.keys.each do |date|
@schedule[date].keys.each do |time|
print [date, time, @schedule[date][time][0], @schedule[date][time][1]].join(sep) + "\n"
end
end
end

protected

def sanitize(string)
string.gsub!(/\<\!\-\-.+$/, '') # remove HTML comments to the end of the line
string.gsub!(/^\s+/, '') # remove leading whitespace
string.gsub!(/\s+$/, '') # remove trailing whitespace
string
end

def utf7(string="")
@ic.iconv(@coder.decode(string))
end

def get_search_dates(period=30)
[DateTime.now().strftime("%d %b %Y"), (DateTime.now()+period).strftime("%d %b %Y")]
end

def build_query_string(channel, start_date, end_date)
urlencode({
'channelid' => channel,
'startDate' => start_date,
'EndDate' => end_date}) +
'&sType=5&searchstring=&submit=Submit'
end

def build_url(query_string)
host = 'www.mnet.co.za'
cgi = '/schedules/default.asp?'
"http://#{host}#{cgi}#{query_string}"
end

def urlencode(hash)
hash.map {|k, v| "#{URI::encode(k.to_s)}=#{URI::encode(v.to_s)}"}.join('&')
end
end


#
# Main
#
channels = {
# "actionX" => [246, 120],
# "Activate" => [322, 120],
# "Africa Magic Channel (C-Band)" => [487, 120],
# "Animal Planet" => [417, 120],
# "B4U Movies" => [227, 120],
"BBC Food" => [284, 120],
"BBC Prime" => [121, 120],
# "BBC World" => [5, 120],
# "Bloomberg Information TV" => [8, 120],
# "Boomerang" => [314, 120],
# "BVN" => [270, 120],
# "Canal+ Horizons" => [237, 120],
# "Cartoon Network" => [13, 120],
# "Cartoon Network (Africa)" => [219, 120],
# "Cartoon Network (W4)" => [182, 120],
# "Channel O - Sound Television" => [27, 120],
# "China Central Television 4" => [15, 120],
# "China Central Television 9 (Africa)" => [226, 120],
# "CNBC" => [90, 120],
# "CNBC (Africa)" => [194, 120],
# "CNBC (W4)" => [187, 120],
# "CNN International" => [18, 120],
# "Deukom - 3SAT" => [165, 120],
# "Deukom - ARD" => [93, 120],
# "Deukom - DW" => [94, 120],
# "Deukom - PRO 7" => [164, 120],
# "Deukom - RTL" => [91, 120],
# "Deukom - SAT 1" => [92, 120],
# "Deukom - ZDF" => [95, 120],
"Discovery Channel" => [21, 120],
# "E-Entertainment" => [646, 120],
"ESPN" => [24, 120],
# "eTV" => [111, 120],
# "Fashion TV" => [145, 120],
# "Fashion TV (Africa)" => [196, 120],
# "Fashion TV (W4)" => [216, 120],
"GO" => [542, 120],
# "Go (K-World Teen)" => [341, 120],
"Hallmark Entertainment Network" => [32, 120],
"History Channel" => [484, 120],
# "History Channel (Africa)" => [485, 120],
# "K-TV World" => [36, 120],
# "KTV (Indian Bouquet)" => [501, 120],
# "kykNET" => [112, 120],
# "M-Net Domestic" => [39, 120],
"M-Net East (Africa)" => [40, 120],
"M-Net Series" => [75, 120],
# "MK89" => [592, 120],
# "Movie Magic (Africa)" => [57, 120],
"Movie Magic 2 (Africa)" => [234, 120],
# "Movie Magic 2 (W4)" => [233, 120],
# "MTV" => [42, 120],
# "MTV Base" => [69, 120],
"National Geographic" => [102, 120],
# "NDTV" => [499, 120],
# "Parliamentary Service" => [45, 120],
# "Pay Per View" => [109, 120],
"Reality TV" => [248, 120],
# "Rhema Network" => [46, 120],
# "RTPi" => [48, 120],
# "SABC 1" => [84, 120],
# "SABC 2" => [85, 120],
# "SABC 3" => [86, 120],
# "SABC Africa" => [87, 120],
# "SIC" => [255, 120],
# "Sky News" => [120, 120],
"Sony Entertainment" => [228, 90],
# "Summit" => [104, 120],
# "Sun TV" => [500, 120],
# "SuperSport" => [52, 120],
# "SuperSport 2" => [54, 120],
# "SuperSport 3" => [80, 120],
# "SuperSport 3 (W4)" => [172, 120],
# "SuperSport 5" => [208, 120],
# "SuperSport 5 (Africa)" => [252, 120],
# "SuperSport 5 (W4)" => [251, 120],
# "SuperSport 6" => [209, 120],
# "SuperSport 7 (C-Band)" => [580, 120],
# "SuperSport Zone Mosaic" => [235, 120],
# "TellyTrack" => [34, 120],
# "Travel Channel" => [61, 120],
# "Trinity Broadcasting Network" => [276, 120],
# "Turner Classic Movies" => [59, 120],
# "Turner Classic Movies (Africa)" => [60, 120],
# "Turner Classic Movies (W4)" => [181, 120],
# "TV Globo" => [254, 120],
# "TV5 Afrique" => [493, 120],
# "TV5 Afrique (Africa)" => [110, 120],
# "VH1" => [65, 120],
# "ZEE TV" => [67, 120]
}

channels.keys.each do |channel|
p "Channel: #{channel}"
schedule = DSTVSchedule.new(channels[channel][0], channels[channel][1], 30)
schedule.print_schedule
print "\n\n"
end

I hope these articles have tickled your lobes and gets you to go explore Hpricot and the Wonderful World of Web Scraping.

About Me

My photo
I love solving real-world problems with code and systems (web apps, distributed systems and all the bits and pieces in-between).