Tutorial: Create a HTML scraper with PhantomJS and PHP
This simple tutorial will show you how to create a PhantomJS script that will scrape the state/population html table data from http://www.ipl.org/div/stateknow/popchart.html and output it in a PHP application. For those of you who don’t know about PhantomJS, it’s basically a headless WebKit scriptable with a JavaScript API.
Prerequisites:
- PhantomJS (for this tutorial we’ll be using Linux for this particular example) http://phantomjs.org/download.html
- PHP
1. Create the PhantomJS Script
The first step is to create a script that will be executed by PhantomJS. This script will do the following:
- Take in a JSON “configuration” object with the site URL and a CSS selector of the HTML element that contains the target data
- Load up the page based on the Site URL from the JSON configuration object
- Include jQuery on the page (so we can use it even if the target site doesn’t have it!)
- Use jQuery and CSS selector from configuration object to find and alert the html of the target element. You’ll notice on line 37 that we wrap the target element in a paragraph tag then traverse to it in order to pull the entire table html.
- We can save this file as ‘phantomJsBlogExample.js’
- One thing to note is that on line 24 below we set a timeout inside the evaluate function to allow for the page to fully load before we call the pullHtmlString function. To learn more about the ins and outs of PhantomJS functions read here http://phantomjs.org/documentation/
var page = require('webpage').create();
page.onError = function (msg, trace) {
phantom.exit();
};
page.onAlert = function( msg ) {
console.log( msg );
if( msg == "EXIT" ){
phantom.exit();
}
};
page.open(config.url, function(status) {
page.includeJs('https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js', function() {
page.evaluate(function(config){
window.setTimeout(function(){
setInterval(function(){
pullHtmlString(config);
}, 2000);
}, 1);
}, config);
});
});
function pullHtmlString(config){
alert($(config.selector).wrap('<p/>').parent().html());
alert( "EXIT" );
}
2. Create PHP function to run PhantomJS script and convert output into a SimpleXmlElement Object
Next, we want to create a PHP function that actually executes the above script and converts the html to a SimpleXmlElement object.
- On line 3 below you’ll construct a “configuration” object that we’ll pass into the PhantomJS script above that will contain the site url and CSS selector
- Next on line 10 we’ll actually read in the base PhantomJs Script we created in step 1. Notice that we actually make a copy of the script so that we leave the base script intact. This becomes important if you are executing this multiple times in production using different site urls each time.
- On line 20 we prepend the configuration object onto the copied version of the phantomJS script, make sure you json_encode this so it’s inserted as a proper json object.
- Next on line 29 we execute the phantomJs script using the PHP exec function and save the output into an $output array. Each time the PhantomJS script alerts a string, it’s added as an element in this array. Alerted html strings will split out as one line per element in the array. After we get the output from the script we can go ahead and delete the copied version of the script.
- Starting on line 38, we clean up the $output array a bit, for example when we initially inject jQuery in PhantomJS a line is alerted into the output array which we do not want as it doesn’t represent the actual html data we are scraping. Similarly, want to remove the last element of the $output array where we alert (‘EXIT’) to end the script.
- Now that it’s cleaned up, we have an array of individual html strings representing our target data. We’ll want to remove the whitespace and also join all the elements into one big html string to use for constructing a SimpleXmlElement on line 49.
public function pullXmlObjBlogExample($siteUrl,$cssSelector){
//create configuration object containing jquery selector and target site url to pass to the phantom script
$config = array(
"selector"=>$cssSelector,
"url"=>$siteUrl
);
//read in the base phantom script and create a copy of it so we don't mess with the original base script
$templateScript = "phantomJsBlogExample.js";
$templateFileCopy = "phantomJsBlogExample-copy-".time().".js";
if (!copy($templateScript, $templateFileCopy)) {
echo "failed to copy $templateFileCopy";
return false;
}
//Prepend configuration object onto script
$configObj = file_get_contents($templateFileCopy);
$configObj = 'var config = ' . json_encode($config,JSON_UNESCAPED_SLASHES). ';' . "\n" . $configObj;
file_put_contents($templateFileCopy,$configObj);
//Run the phantom script with php exec function, redirect output of script to an $output array;
echo exec("phantomjs $templateFileCopy 2>&1",$output);
//delete the copied version of the phantom script as we don't need it anymore
if ( !unlink( $templateFileCopy ) ) {
echo "failed to delete $templateFileCopy";
return false;
}
// The first element of the output will be message about adding jquery and the last element will be the 'EXIT' message from the script,
// lets remove those so all we have is the html lines
array_shift($output);
array_pop($output);
//remove any whitespace from the array elements and join all the html lines into one string of all the html
$output= array_map('trim', $output);
$output = join("",$output);
//construct an XML element from the html string
$xmlObj = new \SimpleXMLElement($output);
return $xmlObj;
}
3. Call the function and iterate through the SimpleXmlElement Object to get to the table data
- Call the function from step 2 making sure to pass in the target site url and CSS selector
- Now that we have the SimpleXmlObject on line 7 we’ll want to iterate through the rows of the table body and pull out the state name and population table cells. It may help to var_dump the entire SimpleXmlObject to get a sense for what the structure looks like.
- For purposes of this example we’ll just echo out the state name and population but you could really do anything you wanted with the data at this point (i.e., persist to database etc.)
private function scrapePopulationsByState(){
$cssSelector = "table.sk_popcharttable";
$siteUrl = "http://www.ipl.org/div/stateknow/popchart.html";
$tableXmlObject = pullXmlObjBlogExample($siteUrl,$cssSelector);
$cnt = 0;
foreach($tableXmlObject->tbody->tr as $tableRow){
//the first two rows are the header and "All United States" rows so disregard
if($cnt++ < 2)
continue;
//grab the state and population from the corresponding table cell of the row and output!
$state = (string) $tableRow->td[1]->a;
$population = (string) $tableRow->td[2];
echo $state . " has a population of " . $population . "\n";
}
}
4. Final Output
Finally, running the function from step 3 should result in something like this.
California has a population of 37,253,956
Texas has a population of 25,145,561
New York has a population of 19,378,102
Florida has a population of 18,801,310
Illinois has a population of 12,830,632
Pennsylvania has a population of 12,702,379
Ohio has a population of 11,536,504
Michigan has a population of 9,883,640
Georgia has a population of 9,687,653
North Carolina has a population of 9,535,483
New Jersey has a population of 8,791,894
Virginia has a population of 8,001,024
Washington has a population of 6,724,540
Massachusetts has a population of 6,547,629
Indiana has a population of 6,483,802
Arizona has a population of 6,392,017
Tennessee has a population of 6,346,105
Missouri has a population of 5,988,927
Maryland has a population of 5,773,552
Wisconsin has a population of 5,686,986
Minnesota has a population of 5,303,925
Colorado has a population of 5,029,196
Alabama has a population of 4,779,736
South Carolina has a population of 4,625,364
Louisiana has a population of 4,533,372
Kentucky has a population of 4,339,367
Oregon has a population of 3,831,074
Oklahoma has a population of 3,751,351
Connecticut has a population of 3,574,097
Iowa has a population of 3,046,355
Mississippi has a population of 2,967,297
Arkansas has a population of 2,915,918
Kansas has a population of 2,853,118
Utah has a population of 2,763,885
Nevada has a population of 2,700,551
New Mexico has a population of 2,059,179
West Virginia has a population of 1,852,994
Nebraska has a population of 1,826,341
Idaho has a population of 1,567,582
Hawaii has a population of 1,360,301
Maine has a population of 1,328,361
New Hampshire has a population of 1,316,470
Rhode Island has a population of 1,052,567
Montana has a population of 989,415
Delaware has a population of 897,934
South Dakota has a population of 814,180
Alaska has a population of 710,231
North Dakota has a population of 672,591
Vermont has a population of 625,741
Washington, D. C. has a population of 601,723
Wyoming has a population of 563,626