Category Archives: Coding

Maybe Steve Jobs Was Right About Flash?

I was building a website for Digital Point Ads, and for what I wanted it to do, I figured Flash would be the way to go. So it was built, and worked fine in Flash.

The problem is that I’m an anal perfectionist and the Flash object was eating 80-90% of a computer’s CPU when being viewed… and me being me, I couldn’t just be okay with that.

…so I rebuilt it with CSS3/HTML5/jQuery. It turned out much nicer in my opinion, only uses about 1% of the resources that Flash needed and as an unintentional bonus, it works on things like iPad and phones/computers without Flash support.

I’m not anti-Flash (as I said, I *wanted* to do the site with Flash), but I AM anti-inefficient. There are still some things you can’t do with HTML5 that you can do with Flash, but those things are becoming fewer and fewer it seems.

strace

Yay, more server admin fun! :) Here’s a useful *nix command that will let you determine what system calls a program uses… For example, I wanted to double check that libevent calls within memcached were using epoll() and select() or poll() calls (epoll scales better) on my SuSE Linux machines…

server:~ # strace memcached -u root

...clipped...
epoll_ctl(5, EPOLL_CTL_ADD, 3, {EPOLLIN, {u32=5313904, u64=5313904}}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 4, {EPOLLIN, {u32=5313920, u64=5313920}}) = 0
epoll_ctl(5, EPOLL_CTL_ADD, 7, {EPOLLIN, {u32=5313968, u64=5313968}}) = 0
epoll_wait(5, {}, 1024, 1000) = 0

Sure enough! :)

Use Server-Side Caching When Possible (memcached)

Purely out of necessity, I’ve become a system administrator/architect for digitalpoint.com servers… and a few people have been asking me for general admin tips to make things stable and scalable, so here’s a good one for you…

Use memcached… no really, use it.

memcached is a distributed memory caching system that allows multiple servers to access the same shared memory (you can use it just for single local server too of course). You can cache pretty much anything… SQL query results (especially ones that can’t hit indexes), dynamically generated web pages, dynamic RSS feeds, etc.

If you compile the memcache() functions/class directly into PHP, you have a an easy (yet powerful) way to incorporate it.

For example, let’s say I have some existing code that makes a call to some sort of API (I use memcached for the keyword suggestion tool in this way)… you can add a couple lines of code to cache the results.

[code=php]$data = @file_get_contents ("http://some_api_call_url?keywords=$keywords");[/code]

could become this:

[code=php]$memcache = new Memcache;
$memcache->addServer('192.168.1.1');
$memcache->addServer('192.168.1.2');
if (!$data = $memcache->get($keywords)) {
$data = @file_get_contents ("http://some_api_call_url?keywords=$keywords");
$memcache->set($keywords, $data, false, 86400); // 24 hour cache
}
$memcache->close();[/code]

Now we are only hitting the API for the same keywords once every 24 hours… that setup utilizes 2 memcached servers working as a single entity for pooling and failover.

I use memcache for all sorts of things (this blog’s pages are cached for 60 seconds as another example because WordPress sucks, and protects against a massive influx of traffic [front-page digg for example]).

Here’s some things I personally use it for…

  • Caching this blog’s pages (for 60 seconds)
  • Caching Keyword Suggestion Tool queries (for 7 days)
  • Caching AdSense Sandbox queries (for 24 hours)
  • Temporary counters in Keyword Tracker (using decrement() for displaying remaining keywords on a “Check All”)
  • Recent forum topics that show on all the webmaster tools
  • vBulletin forum datastore

Any high-traffic site that is dynamically generated (especially if it includes SQL queries) could benefit from it… you could manage which memcache items need to be updated when you insert/update to the database, or an easy way to do it is just check the cache for the pages that potentially have high traffic… if they aren’t in the cache, put them in the cache with a certain expiration date. Then the page is dynamically generated no more often than the cache expiration time (in the case of this blog, the pages are generated no more than once every 60 seconds).

P.S. - Make sure you block outside access to your memcached port since there’s no authentication used!

The $10,000 Space

If you are a programmer, have you ever had one of those bizarre situations where you know something is not working, but you aren’t getting an error or anything else that would make debugging easy?

A couple days ago I migrated a cluster of web servers to PHP 5.1.x (they were running PHP 4.4.x before). One of the things I run on them is a geo targeting mechanism, and in the case where my screw-up was, a function to block a subnet of IP addresses.

[code=php]echo sprintf("%u", ip2long(' 80.1.2.3'));[/code]

That little bit of code was my fuck up… PHP 4.4.x returns “1342243331” (as expected), but PHP 5.1.x would return “0“. PHP4 was more forgiving of the stray leading space before the IP address, while PHP5 was not. The end result was that every IP address before 81.1.2.3 was blocked, instead of blocking FROM that IP address to an end IP address (that was not very many IPs away).

Awesome… so what happened here? I ended up blocking about 40% of the potential traffic for awhile, which equated out to probably about $10,000 in lost revenue.

The lesson learned? Don’t fuck around with spaces, because they will cost you many hours of debugging/tracing as well as ~$10,000.

Google Gmail API

I was just thinking… Google needs to build an API for Gmail. Then we could do things like POP our email into an email client, and then run a script from within the email client that is able to go back to Gmail and tag stuff as spam, apply a label, etc.

That would be handy… can someone please make it so? :)

Update

Oh, speaking of Gmail, it’s been awhile since I poked around in it’s settings, and I noticed you can now send email from within Gmail as a different address (before it would only let you set the “Reply To” address). Nice… this was a suggestion I sent to them (probably along with a lot of other people) a long time ago. Now I’ll actually use their web interface for sending mail sometimes.

The other thing I noticed if you can use their SSL SMTP servers for sending email as your non-Gmail address. Double nice! :)

Canon VB-50i PHP Control Class

I tried to find a PHP class to control the new camera, and I couldn’t find one… so a little packet sniffing later, I was able to figure out how it’s little Java applet communicates with the camera. Anyway, I spent a few minutes to make a PHP class to control the camera, so if you need one, here you go (it hasn’t been extensively tested, so it probably has problems… if it does, leave a comment)…

So here’s a basic PHP script that uses the class to take control of the camera, set the pan/tilt/zoom to a certain position, capture the image and save it to a file… handy for uhm… I dunno… a cron job to take time lapse photography. :)

[code=php] include ('class_camera.php');

$cam =& new Camera();

$cam->connect('your.camera.ip.address');
$cam->get_control();
$cam->goto( -7463, -548, 4126);
sleep(5); // Wait for movement to finish and lens to focus

$handle = fopen('images/' . time() . '.jpg', 'w');
fwrite ($handle, $cam->image());
fclose ($handle);

$cam->disconnect();
?>[/code]

class_camera.php:[code=php]

// #############################################################################
// Canon Camera Control Class
// http://www.shawnhogan.com/2006/08/canon-vb-50i-php-control-class.html

/**
*
* Class to interface with a Canon VB-C50i or VB-C50iR
*
*/

class Camera
{
/**
* Connects to the specified camera
*
* @param string Hostname or IP address of the camera
*
* @return connection_id
*/
function connect($host)
{
$this->host = $host;
return $this->connection_id = substr(file_get_contents ('http://' . $host . '/-wvhttp-01-/OpenCameraServer?client_version=LiveApplet_4125&image_size=640x480'), 14, 9);
}

/**
* Disconnects from the camera
*
* @return none
*/
function disconnect()
{
file_get_contents ('http://' . $this->host . '/-wvhttp-01-/CloseCameraServer?connection_id=' . $this->connection_id);
}

/**
* Attempts to get control of camera
*
* @return none
*/
function get_control()
{
file_get_contents ('http://' . $this->host . '/-wvhttp-01-/GetCameraControl?connection_id=' . $this->connection_id);
}

/**
* Get camera info
*
* @return array of camera info variables
*/
function get_info()
{
preg_match_all ('#(.*?)=(.*)#', file_get_contents ('http://' . $this->host . '/-wvhttp-01-/GetCameraInfo?connection_id=' . $this->connection_id), $matches);
foreach ($matches[1] as $key => $value) {
$this->camera_info[$value] = $matches[2][$key];
}
return($this->camera_info);
}

/**
* Move camera
*
* @param string Movement type (pan | tilt | zoom)
* @param number Position to move to
*
* @return none
*/
function move($operation, $position = 0)
{
file_get_contents ('http://' . $this->host . '/-wvhttp-01-/OperateCamera?' . $operation . '=' . $position . '&connection_id=' . $this->connection_id);
}

/**
* Set pan/tilt/zoom on camera with one call
*
* @param number pan position
* @param number tilt position
* @param number zoom position
*
* @return none
*/
function goto($pan, $tilt, $zoom)
{
$this->move('pan', $pan);
$this->move('tilt', $tilt);
$this->move('zoom', $zoom);
}

/**
* Set parameter
*
* @param string paramter to set
* @param string value
*
* @return none
*/
function set_value($parameter, $value)
{
$mapping = array (
'back_light' => 'B',
);

$parameter = $mapping[$parameter];

file_get_contents ('http://' . $this->host . '/-wvhttp-01-/OperateCamera?' . $parameter . '=' . $value . '&connection_id=' . $this->connection_id);
}

/**
* Get current image
*
* @return none
*/
function image()
{
return file_get_contents ('http://' . $this->host . '/-wvhttp-01-/GetOneShot?image_size=640x480');
}

}
?>[/code]

Moving HTML With JavaScript

Someone asked me why I hide the sidebar on this blog by default, then show it in a different part of the HTML, so here’s the explanation…

Most blog themes float the main body to the left, and make the sidebar on the right “static”. I did the opposite when creating my custom WordPress theme… My sidebar is a DIV that is floated to the right. The reason for this then the content below the bottom of the sidebar wrap around it, which I just think looks better. So here’s the problem… Google weighs content at the beginning of a page higher than content at the bottom, so Google was seeing 99% of my pages as “similar content” because my sidebar needed to be at the top of the HTML source.

So to solve that problem, I just put the sidebar at the bottom of the page in a hidden (with CSS) DIV, then call a simple script to set the contents of an empty DIV (<div id=”sidebar”></div>) at the top of the page, where the sidebar needs to be, to the contents of whatever is in the hidden DIV at the bottom…

<script language="javascript">
      document.getElementById('sidebar').innerHTML = document.getElementById('sidebar_content').innerHTML
</script>

For web browsers, the sidebar renders at the beginning of the HTML source, but Google sees the sidebar at the end of the HTML and pages no longer are seen as 99% similar.

Forum Spy

I decided I didn’t have enough to do (that’s a joke BTW), so this morning I made a “spy” system for my forum (the general idea of course was borrowed from Digg). It’s “extra” slick in Firefox because of the fades and opacity ability Firefox has, but it still works in IE, Safari, etc…

It more or less lets you see what’s going on in the forum in realtime.

Check it out: http://forums.digitalpoint.com/spy.php

Unfortunately it also means that I’m still up right now and haven’t gone to bed yet. Goodnight! :)

APC Datastore Class For vBulletin

On one of my ultra-high traffic web servers, I switched from eAccelerator to APC today (an opcode/caching system for PHP). So far it seems pretty nice… Especially the ability to disable stat for each PHP request.

I ended up making a datastore class for vBulletin also so I could use it for the forum, so if anyone else is using vBulletin on a server with APC, here you go (if you know what this is for, you will know where it goes :)).

[code=php]// #############################################################################
// APC

/**
* Class for fetching and initializing the vBulletin datastore from APC
*
* @package vBulletin
* @version $Revision: 0.0.0.1 $
* @date $Date: 2006/05/08 16:51:06 $
*/
class vB_Datastore_APC extends vB_Datastore
{
/**
* Fetches the contents of the datastore from APC
*
* @param array Array of items to fetch from the datastore
*
* @return void
*/
function fetch($itemarray)
{
if (!function_exists('apc_fetch'))
{
trigger_error('APC not installed', E_USER_ERROR);
}

foreach ($this->defaultitems AS $item)
{
$this->do_fetch($item);
}

if (is_array($itemarray))
{
foreach ($itemarray AS $item)
{
$this->do_fetch($item);
}
}

$this->check_options();

// set the version number variable
$this->registry->versionnumber =& $this->registry->options['templateversion'];
}

/**
* Fetches the data from shared memory and detects errors
*
* @param string title of the datastore item
*
* @return void
*/
function do_fetch($title)
{
$ptitle = $this->prefix . $title;

if (($data = apc_fetch($ptitle)) === false)
{ // appears its not there, lets grab the data and put it in memory
$data = '';
if ($dataitem = $this->dbobject->query_first("
SELECT title, data FROM " . TABLE_PREFIX . "datastore
WHERE title = '" . $this->dbobject->escape_string($title) ."'
"))
{
$data =& $dataitem['data'];
}
$this->build($title, $data);
}
$this->register($title, $data);
}

/**
* Updates the appropriate cache file
*
* @param string title of the datastore item
*
* @return void
*/
function build($title, $data)
{
$title = $this->prefix . $title;

if (!function_exists('apc_store'))
{
trigger_error('APC not installed', E_USER_ERROR);
}
$check = apc_store($title, $data);
}
}[/code]

Update

I just found out APC datastore support was added to the yet unreleased vBulletin 3.6. Nice!

Update 2

I’ve since switched back to eAccelerator. APC was causing Apache segfaults under ultra-high loads.

I Need To Be More Dorky

I just realized that it’s been weeks since I had something dorky to say (like programming or something to do with MySQL for example).

Just so you know that I am still dorky, here’s some PHP code for you (from my post over here)…

This will put the keywords that someone searched on to reach your site (works with Yahoo, AOL, MSN and Google) into the $keywords variable.

[code=php]$parse = parse_url($_SERVER['HTTP_REFERER']);
$se = $parse["host"];
$raw_var = explode("&", $parse["query"] );
foreach ($raw_var as $one_var) {
$raw = explode("=", $one_var);
$var[$raw[0]] = urldecode ($raw[1]);
}
$se = explode (".", $se);
switch ($se[1]) {
case 'yahoo':
$keywords = $var['p'];
break;
case 'aol':
$keywords = $var['query'];
break;
default:
$keywords = $var['q'];
}
unset($parse, $se, $raw_var, $one_var, $var);[/code]

Weeeeeeeeeee! Can I go to bed now?

Cross-Database JOIN With MySQL

You ever wish you could have done something for years, only to realize one day you could have done it this whole time?

I have a couple tables that I was replicating across 3 or 4 MySQL databases because I didn’t think I could reference a table from database A while working with database B.

Today I accidentally tried to do a SELECT on a table from a database I wasn’t even working with and instead of getting an error, it worked. As long as the user you are connected to MySQL as has the proper privileges (duh), it works. You just prefix your table/column name with the database name.

For example:
[code=sql]SELECT database1.tablename.a_column,database2.another_table.another_column
FROM database1.tablename
LEFT JOIN database2.another_table ON(database1.tablename.some_id = database2.another_table.some_id)[/code]

You can do anything you can do with tables in the same database (INSERT SELECT, JOINS, sub-queries, etc.)

Wow, I’m an idiot… its probably been a feature since version 1.0 too. :) Now if you could just do the same thing across different MySQL servers (something like ipaddress.database.table.field), that would be really handy.

Track AdSense Clicks With Google Analytics

It would be cool if Google automatically tracked AdSense clicks into Google Analytics… But until they do, here’s a little bit of JavaScript code you can use to do it. You can then setup an AdSense click as a “goal” within the Analytics interface.

[code=javascript][/code]
This only works with Internet Explorer because of some limitations/bug with Mozilla. I’m too lazy to code a complicated workaround for other browsers (which would involve running JavaScript code whenever the mouse moves which seems really inefficient to me). Hopefully Mozilla will fix the bug (although it’s been pending for 2 1/2 years now).

Basically it will log an AdSense click as a pageview to a non-existent page (http://www.digitalpoint.com/AdSenseClick in my case).

Update

I made a version of the code that will work with AdSense and/or Yahoo Publisher Network. You can find it over here.

Roadway Speeds In Google Local/Maps

I really don’t get why Google hasn’t added live traffic data for major cities yet. The information is out there and freely available for anyone to use. It only took me a few minutes of digging around to realtime traffic data for all of California, so why can’t Google find/use it? For example, Caltrans breaks down the data by district… San Diego is included in District 11:

Labels (descriptions, freeway, direction, etc.)
Current Highway Speeds (cross referenced by ID to the Labels file)

Would be nice if they gave you the longitude/latitude of each location, but hey… assign some intern to look them all up. :)

So let’s see if I could stick the data on Google Maps myself (via API)… I’m fairly lazy, and would rather programatically do something that would otherwise require manual labor (looking up longitude/latitude for a zillion points). Thankfully, I remembered that I looked at Yahoo Maps Beta a few weeks ago, and saw it had live traffic data that was overlayed on the map graphic (which means they probably are making a separate HTTP request for just the traffic data). Sure enough… a packet sniffer reveals this URL for getting traffic data for San Diego:

XML Feed

Okay, that’s not bad, but doesn’t include longitude and latitude. Instead it appears the speed data is relative to the current view of the Yahoo map you are looking at. Hmmm… rather than decode their little proprietary system, let’s see what sfmt is. With some luck, it’s “speed format”. Sure enough… change it to sfmt=0, and you get longitude/latitude of the points! :) So now we have a nice XML feed with longitude and latitude:

XML Feed

Once you have that data, it takes 2 minutes to make a Google Map with the data…

Traffic In San Diego

Of course it doesn’t really answer my original question… why doesn’t Google already have this data on Google Local/Maps? If I worked at Google, this would be my 2nd “20% time project” (this was the other one).

Google Maps API Key

Google Maps recently updated their API key system so you can use the same key within subfolders. And all I can say is thank f’ing GOD. Was so annoying that if you make a map and put it on your blog that it would need a separate key when viewing the map from an archive/month page.

Geolocation Maps

Based on some of the stuff I did for this, I made a system that can be used by any site (just copy/paste a tiny bit of HTML code and it automagically works).

Anyway, if you want to see where in the world people are from that visit this blog (or want to use it yourself), click here.

Google Maps API Is Pimp

It’s not the first time I used the Google Maps API, but every time I do something with it, I’m reminded about how damn cool it is.

It ended up being relatively trivial to show where in the world all the people currently visiting the forum are at in the world. Added some color coded pins and made the pins clickable (you can also automatically fly to the location if you have Google Earth installed).

In truth, the hardest part really had nothing to do with the API itself… it was making a caching mechanism for geolocation data on the IP address (no point in querying the database for all IPs for every page view), so it just queries for IPs that are new since the last time someone looked at the page.

Check it out: http://forums.digitalpoint.com/usermap.php

Live Email Validation

Here’s a handy bit of PHP code that will take the email variable, lookup the mail server for the domain, log into the mail server and see if it will accept delivery for that address.[code=php] $split = preg_split('/^(.*)<(.*)>(.*)/', $_REQUEST['email'], -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
$email = array_pop ($split);
$split = explode ("@", $email);
$domain = $split[1];

getmxrr ($domain, $a, $b);
if ($a) {
foreach ($a as $key => $val) {
$c[$b[$key]] = $val;
}
ksort ($c);
} else {
$c[0] = $domain;
}

$fp = fsockopen (array_shift($c), 25, $errno, $errstr, 30);
$return = fgets ($fp, 1024);
fputs ($fp, "helo none.com\r\n");
$return = fgets ($fp, 1024);
fputs ($fp, "mail from: <[email protected]>\r\n");
$return = fgets ($fp, 1024);
fputs ($fp, "rcpt to:<" . $email . ">\r\n");
$return = fgets ($fp, 1024);
fputs ($fp, "quit");

fclose($fp);

if (substr($return, 0, 3) > 250) {
echo "Bad email";
} else {
echo "Good email";
}
?>[/code]Okay, okay… the truth is I just got done adapting the GeSHi syntax highlighter into a WordPress plug-in and wanted to make sure it worked, so I needed to post some code of some sort. :)

Update

Seems to work pretty well if I do say so myself…

CSS – overflow: auto;

I finally solved a CSS problem that has been driving me crazy on the theme I created for this blog. The fact that if you use a border on a block item (a DIV or H1 for example), the border will overlap a floating DIV. The text will wrap properly, but the border still extends into the floating DIV.

Anyway, the key to it is the overflow: auto; CSS attribute.

Without overflow: auto;

floating this to the right
This is a header

This is the main content of the page… I want the text to wrap (which it always does), but I also want the header border to not overlap the floating DIV (the problem).

<div style=”background-color: #eee; border: 1px solid #ddd; margin: 5px; padding: 5px; width: 300px;”>
<div style=”border: 1px solid #f00; float:right; width:100px; margin: 10px;”>floating this to the right</div>
<div style=”font-size:16px; background-color: #ddd; border: 1px dashed #0f0; overflow: auto”>This is a header</div>
This is the main content of the page… I want the text to wrap (which it always does), but I also want the header border to not overlap the floating DIV (the problem).
</div>

With overflow: auto;

floating this to the right
This is a header

This is the main content of the page… I want the text to wrap (which it always does), but I also want the header border to not overlap the floating DIV (the problem).

<div style=”background-color: #eee; border: 1px solid #ddd; margin: 5px; padding: 5px; width: 300px;”>
<div style=”border: 1px solid #f00; float:right; width:100px; margin: 10px;”>floating this to the right</div>
<div style=”font-size:16px; background-color: #ddd; border: 1px dashed #0f0; overflow: auto;“>This is a header</div>
This is the main content of the page… I want the text to wrap (which it always does), but I also want the header border to not overlap the floating DIV (the problem).
</div>
Update

Looks like it doesn’t work in Internet Explorer 6. Oh well, I’m over IE, so I’m not going to bother to come up with a weird hack. IE users will just have to upgrade to IE7 when it comes out I guess (or better yet, just use Firefox). :)

MSN Requiring Cookies For RSS Readers

It seems as of last night MSN is now requiring end users to pass a cookie (specifically MC1) with all search and RSS feed requests. The cookie has 2 parts (V and GUID). V is always 3 for now (probably stands for VERSION), the second part is GUID which looks like an MD5 hash to me (32 character hexadecimal value). It also appears as if you can just make up the GUID to make it work (doesn’t validate against anything to make sure it’s valid).

Anyway, this seems really stupid, because now all the RSS feeds that were using MSN are going to break. The worst part is it doesn’t reject it, the web server simply holds the HTTP connection open (never returning anything and never closing it)… Nice! Wonder who’s idea that was. heh

Anyway, here’s a snippet of PHP code to form the cookie, which you can use with whatever you need (cookie name should be MC1):

[code=php]$cookie_mc1 = urlencode (‘V=3&GUID=’ . md5($_SERVER[‘SERVER_NAME’])); "test"[/code]

You can test this in your browser by going to any MSN search result page (for example this one) after you delete the MC1 cookie from .msn.com (if you have it).

Update

Of course right after I write this and make some backend changes to some of my stuff to take it into account, Microsoft fixes it. Oh well… they must read my blog (heh j/k).

Dude, I’m Awesome!

Well… I decided to write my own PHP script to rip out the old comments (from when this was a Blogger blog), parse them, format them and insert them into the database for WordPress.

The cool part is it all worked on the first try. That’s pretty amazing if you ask me (and that’s why I’m awesome.. hehe). :)

So anyway, the 400+ comments that were made when this was a Blogger blog now show… And we can once again enjoy people telling me I have a small penis. :)

Killer Coding Ninja Monkeys

Today at ninja school, we got our first uniform. What I want to know is how am I going to strike fear into my enemies wearing something like this? It definitely doesn’t make me look like I can kick ass.

It wasn’t quite what I had expected, but I guess it’s a good first step.

Update… A little Googling and I realized that they probably got them from Think Geek. What the hell?? When do we get to assassinate people? Or at least look like we can?

Write Code To Mimic Evolution In Nature

I had an idea this morning.. Why not write code to mimic evolution where needed?

For example a search engine… their algorithms are constantly being changed and updated to fight spam and whatever else comes along.

It wouldn’t work as well on a small scale, but if you have a large number of end users (like a search engine), I don’t see why it wouldn’t work.

Have the system make hundreds or thousands of small random iterations of the existing algorithms (small enough that it doesn’t destroy the results). Then have it feed end users with a random version of the algorithm and keep track of how well it was received. You could say the deeper the user needed to look in the search results, or if they immediately needed to do a different search, then it wasn’t that good of an algorithm change. Then give the “good” changes more weight than the bad, and repeat the process.

It’s survival of the fittest, and becomes an semi-automated way to evolve their algorithms.

My First Yahoo API Use

Mucked around with the Yahoo API a little bit, and I must admit it’s pretty nice. I wish the Google API was as fast and had as much features.

My first useful thing I did with it was I added to the keyword tracker so it can track historical search engine rankings for Yahoo (I also added MSN while I was at it).

Turned out pretty cool… now you can see ranking changes for all three search engines over time on a single chart. Weeeeeee…. :)

Yahoo API

Today at the SES show, Yahoo announced they are following Google’s lead and allowing people to utilize an API for making automated queries without violating their terms of service.

In some ways it’s better than the Google API because it allows you to do more than just web search (you can search images, local, news, video and web). If nothing else, maybe it will be the incentive that Google needs to come out with a new version of their API (the Google API is “beta”, but has not been updated in over 2 years).

One thing I don’t like about the Yahoo API is it’s limited to 5,000 queries per day, per IP address. Google’s method of 1,000 queries per day, per person lets you build killer web apps (like the keyword tracker). But the IP address limitation does not make that practical with the Yahoo API. Either way, it’s really cool they have one now.

Check it out over here:

http://developer.yahoo.net/