You can use PhantomJS for automated scraping. You have two options:
(1) Design your scraper is usually via a 1-to-1 correspondence with the routing logic and the client side templates. Create a new scraping module for each of the templates and use the scraping modules according to the data visible at each route.
(2) Another simpler approach is just to design your scraper to hi-jack the app's own XHR or Sockets module and collect the data directly via the API exposed to the web-app.
The latter approach is the really smart way to scrape client-side web-apps since you can get a lot of additional valuable metadata that may not be written to the screen.
Benchmark your personal time developing a Twitter web scraper similar to this one to the time to follow the (2) approach. The code of the article was done in a few hours while the (2) approach would require further analysis and risks (analyzing protocols can be very tricky). Google Chrome and Firefox are very fast and one developer day may cost more than one PC.
Well at the end of the day, the application really matters. In most cases I find just sniffing the XHR requests with Chrome's Developer Tools to be the most productive.
Interesting. Do you have an example of the latter approach? I have been trying to build a web scraper, but many times after the HTML is returned, the javascript then loads the data dynamically. I'd love to learn of a few techniques to fetch the AJAX data.
Get firebug, or use developer tools in chrome and look at the requests the browser is making when you load the page, and when you scroll down and it loads more data. Then you just implement that in whatever language you are scraping with. I usually use pythons requests module.. That's about as easy as it gets for something like this.
Not an example I can share, but you can approach this starting out looking at the global variable which basic bootstrap data is assigned to on initial payload. From this variable you should be able to work backwards to the function that reads this data on page ready and instantiates some models and collections. From there, I'd study the model/collection code to see how they are configured for fetching data. If you tap in at this point, you can just request data directly via new models or collections.
For people who don't have much experience in web scraping, it's important to note that there is no a better/worse approach. Everything depends on the specific site.
I don't see (2) as a simpler approach that scraping from the browser rendered page: the information retrieved between the browser and the server can be obfuscated and will require too much effort to analyze the protocol between them. You mention the Sockets module but if you are using HTTPS request the information at that level will be encrypted.
I wasn't suggested sniffing traffic. With WebSockets, it's very hard to do in an automated way. I was suggesting looking at the Models and Collections in a web app via de-obfuscation and a little reverse engineering. Injecting JavaScript into a page via PhantomJS is a great way to control a web app.
I used headless selenium successfully for executing javascript. it has a nice feature of waiting the browser until the some element appears in the HTML.
I found all of the available JS engines in Selenium to be insufficient for my purposes. None of them handled a wide range of javascript well and would often fail to execute it. I switched to PhantomJS and couldn't be happier.
Here is another little snippet I used to scrape yelp before. It involved polling for a specific element on the page, if you can inject or find jQuery, this is pretty light and simple. edit: Should note that jQuery is less than necessary for this (since I'm sure someone would have said it right after I posted this), just makes me have to type less crap :)
I'm actually working on this now for an app. The data is for the scores from a golf tournament. When I was that it loaded dynamically, I dug through all the requests using Chrome's developer tools. I was able to find the json that the page was using to load the data which turned out to be easier to deal with than scraping from html in the first place. So if you're running into this problem, try to find the url that they snag the data from in the first place.
When all else fails you can resort to using webdriver. I generally try to reverse the request but occasionally the authentication weirdness and data obfuscation gets in a way to the point it's going to take more time than I'd like.
Disclaimer: I don't have as much experience with phantomjs and the many more traditional services mentioned due to sticking mainly to jvm languages for my backends.
I've been using PhantomJS to get upcoming movie listings from Comcast, so I can plan my DVRing. The hardest part (aside from only having dabbled in JavaScript so I have no idea how to write good or idiomatic JS) was keeping straight in my mind what is executing in the page environment, and what is executing in the environment that controls the script. Get the environments mixed up, and you won't be a happy scrapper.
Here's the code if anyone wants to play with it. Note that a couple of things are hard coded to my area. There's a cookie that gives the zip code, and there's a 981 that is the channel number of a channel that is higher than all the channels I'm interested in. This needs to be a channel that exists--you can't just pick a large number.
I was going to put this on Github at one point, but decided not to because I don't know what Comcast thinks of this sort of thing.
var system = require('system');
var page = require('webpage').create();
var fs = require('fs');
var of = fs.open("out", "w");
var timer0 = null;
var min_start = null;
var want_exit = false;
var days = 2; // how many 24 hours worth of movies to get
// if the page generates any console messages, dump them to our console
page.onConsoleMessage = function (msg) {
console.log('page: ' + msg);
};
//{ These functions are meant for use with page.evaluate()
function set5()
{
var e = document.createEvent('MouseEvents');
e.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
var t = document.getElementById('options-advanced-hours-5');
t.dispatchEvent(e);
return 0;
}
function tag_as_stale()
{
var marker = document.getElementById("981");
marker.setAttribute("foo","bar");
}
function fresh()
{
var marker = document.getElementById("981");
if ( marker == null )
return false;
if (marker.getAttribute("foo") == "bar")
return false;
return true;
}
function get_width()
{
// The 'timeline' class is the div that contains the headings
// for each half hour on the listings. It has a child div for
// each half hour. The count of these children lets us tell
// if the listings are in 1 hour mode, 3 hour mode, or 5 hour mode.
var t = document.getElementsByClassName('timeline');
return t[0].children.length;
}
function forward()
{
var e = document.createEvent('MouseEvents');
e.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
// 'option-forward' is the class of the thingies that advance
// the listings when clicked.
var t = document.getElementsByClassName('option-forward')[0];
t.dispatchEvent(e);
}
// Find all the movies on the current listings, and put their
// information into an array attached to th window, from whence
// they can later be extracted (see get_movie() below).
function find_movies()
{
window.tzs_movies = new Array();
window.tzs_next = 0;
var listings = document.getElementsByClassName('listing movies');
for (var i = 0; i < listings.length; i++) {
var el = listings[i];
var name = el.getElementsByClassName('listing-entity')[0].innerText;
var start = el.getAttribute('data-starttime');
var chan = el.parentElement.getElementsByClassName('channel-actions')[0];
var call = chan.getAttribute('data-callsign');
var cnum = chan.getAttribute(...
The hardest part was keeping straight in my mind what is executing in the page environment, and what is executing in the environment that controls the script
YES! that's why we used a browser extension in this and the previous article. Because you know that all the objects live in the same world. You don't have this controller/scraper duality. If someone likes the Phantomjs approach, they can implement the controller as a background web page and the scraper logic in the content.
27 comments
[ 0.21 ms ] story [ 68.0 ms ] thread(1) Design your scraper is usually via a 1-to-1 correspondence with the routing logic and the client side templates. Create a new scraping module for each of the templates and use the scraping modules according to the data visible at each route.
(2) Another simpler approach is just to design your scraper to hi-jack the app's own XHR or Sockets module and collect the data directly via the API exposed to the web-app.
The latter approach is the really smart way to scrape client-side web-apps since you can get a lot of additional valuable metadata that may not be written to the screen.
I don't see (2) as a simpler approach that scraping from the browser rendered page: the information retrieved between the browser and the server can be obfuscated and will require too much effort to analyze the protocol between them. You mention the Sockets module but if you are using HTTPS request the information at that level will be encrypted.
I really encourage people to explore the Data Big Bang site since offers a lot of different approaches for web scraping. Just look at the resources at the end of the http://blog.databigbang.com/web-scraping-ajax-and-javascript... article.
Why use Google Chrome (or Firefox)? One of the reasons is that you are sure that it is updated very often and their performance is great.
https://gist.github.com/91bananas/5644737
In no particular order: Gtk2::WebKit::Mechanize, Win32::IE::Mechanize, WWW::Mechanize::Firefox, WWW::Scripter, WWW::Selenium
Personally, I use WWW::Mechanize::Firefox
>...(like Twitter).
I think that's just an easy example the author used of a popular site that uses a lot of front-end AJAX calls to show content.
That being said:
I've run in to a lot of problems scraping sites with heavy javascript. I personally use selenium with a headless google chrome for it. http://www.alittlemadness.com/2008/03/05/running-selenium-he...
Granted, there's a bit of custom logic involved at times, but it's by far the most reliable compared to the alternatives I've seen like HTMLUnit.
It's allowed me to just focus on worrying about page content rather than worrying about if something will work or not.
Here's the code if anyone wants to play with it. Note that a couple of things are hard coded to my area. There's a cookie that gives the zip code, and there's a 981 that is the channel number of a channel that is higher than all the channels I'm interested in. This needs to be a channel that exists--you can't just pick a large number.
I was going to put this on Github at one point, but decided not to because I don't know what Comcast thinks of this sort of thing.
YES! that's why we used a browser extension in this and the previous article. Because you know that all the objects live in the same world. You don't have this controller/scraper duality. If someone likes the Phantomjs approach, they can implement the controller as a background web page and the scraper logic in the content.