Many visitors to the UK arrive believing that Fish & Chips are the country's crowning culinary achievement. But ask most natives what they actually consider more essential, and a great number will vote in favour of the humble Sunday Roast.
Given the dish's popularity, any pub worth its salt will offer a Sunday Roast (many with a dizzying array of options), and pubs can live or die by their reputation on this matter.
However, with quality varying considerably between establishments, how do you know who to trust with your dining experience?
We're going to use SerpApi to scrape Google Maps reviews to find the best dish out there - hooray!
Plan of Action
In this blog series, we're going to tackle the complicated task of breaking down a large area into a grid, and iterate over that grid to scrape business information from Google Maps.
We'll break it down into a series of detailed steps, so if you've struggled in the past to understand how to perform a grid search of an area, this series will be perfect for you!
Once we have that list of businesses (in our case, pubs), we will iterate through them to scrape Google Maps Reviews and find out what people are actually saying about the quality of meals being served in each establishment.
With that data, we'll perform some analysis to score each pub (based on the contents of those reviews), and then we'll be able to output a list of the best places to find a roast dinner in London.
By the end of this series, you'll have enough knowledge to follow this pattern for any number of businesses and understand who rates highly for a specific product or service.
So, in summary, we plan to:
- Scrape Google Maps to find all of the pubs in London
- Scrape Google Maps Reviews for each pub
- Analyse each review searching for:
- Keywords relating to Sunday Roasts
- Perform sentiment analysis on relevant scraped Google Maps Reviews
- Return a list of pubs with recommended Sunday Roast offerings
Language & Coding Style
The code used for this article will be written in Ruby, but even if you're not a Rubyist, hopefully you'll find that the code is easy to understand - and you can translate the concepts to whatever language you prefer to work in.
We'll also be using OOP to organise our code into classes, since there will be quite a few moving parts by the end of the series.
The Data Source
SerpApi’s Google Maps API is perfect for the first part of this project. Using this API will allow us to search for businesses (e.g., “Pubs” or "Restaurants") in a given area.
Once we have those businesses, we'll need a second source: the SerpApi Google Maps Reviews API. With this, we can scrape Google Maps reviews for each of the businesses we've collected (along with ratings, opening hours, and metadata).
With the reviews data, we can parse each review to look for keywords like roast, chicken, beef, Yorkshire pudding, etc.
If you want to follow along yourself, it's an excellent time to sign up for a SerpApi account as now the free plan search limit has increased 150% to allow 250 searches per month.
Just head on over to https://serpapi.com and register for access to all of our APIs!
Finding Pubs that Offer Roasts
Now, there are a lot of pubs in London - it's a big city (more on that in part 2 of this series). So we want to concentrate only on pubs that offer a Sunday Roast. For now, it should be sufficient to use the following query:
q: "pub sunday roast"
We can play around with this later if required, but since a Sunday Roast is such a standard offering around the UK, we can be fairly confident that this simple query will pull in a lot of results.
Let's Scrape Some Pubs!
Before we go sweeping across an entire city, let's first see how we can scrape all the pubs from a single set of coordinates. We'll use the following, as they are representative of a fairly central area of London:
- Latitude: 51.51
- Longitude: -0.13
If you're wondering how I got those coordinates, you can easily find them for a given area by searching in Google Maps directly in your browser, and then extracting the following part from the url:

As is convention, the latitude value comes first, followed by longitude (and in this case I rounded the values down for simplicity).
To keep results at a manageable amount, we'll avoid paginating beyond the first page of results for each set of coordinates. Google Maps returns around 20 results by default per query, so we'll have a good amount of pubs to look at by the end of the process (especially after we cover the whole city in later parts of this series).
The Pub
Class
The first thing you'll notice is our Pub
class. Since we're going to be interested in pubs and their associated reviews, it makes sense to be able to create a representative pub object that can store data about each individual pub.
Sidenote: Ruby's attr_
Methods
Ruby has a nice way of making quick getter and setter methods for class instance variables (which look like this: @variable_name
). There are three options:
attr_reader :variable_name
- this creates a getter forvariable_name
attr_writer :variable_name
- this creates a setter forvariable_name
attr_accessor :variable_name
- this creates a getter & setter forvariable_name
On each Pub
object we will only need to be able to read attribute values, as they are set during object initialisation (creation of the object).
Our Pub
object literally has two jobs:
- Store the
name
,address
,rating
, andplace_id
for each pub returned from Google Maps. - Output a hash (key/value pair object) containing the stored data, when requested.
And that's it! Super simple. If you want to try this on your machine, I recommend putting the following code in a file called pub.rb
:
# Represents a pub and its associated data
class Pub
attr_reader :name, :address, :rating, :place_id
# Assigns attribute values during creation of each pub object
def initialize(name:, address:, rating:, place_id:)
@name = name
@address = address
@rating = rating
@place_id = place_id
end
# Returns a hash (key/value pair object) based on the object properties
def to_h
{
name: name,
address: address,
rating: rating,
place_id: place_id,
}
end
end
The RoastFinder
Class
The RoastFinder
class is going to be the main orchestrator of everything that happens in this series. But for now, its main duties will be to send requests to SerpApi to scrape business data from Google Maps - and then translate that information into Pub
objects (to be stored in an array within the RoastFinder
class).
SerpApi has a Ruby library available which makes it nice and easy to make requests to all of our APIs using a single gem!
To install it, simply run:
gem install serpapi
Then be sure to require the library at the top of the code file (you'll see an example in the following code for the RoastFinder
class).
We'll need to access the Pub
objects later when we scrape Google Maps Reviews data to find out what people are saying about each pub's Sunday Roast!
I've commented the code to explain what each method does, but essentially here are the main execution steps:
- Call the program from the terminal using
ruby roast_finder
(from within the same directory as the files) run
triggers, which in turn callsfetch_pubs_at
using the latitude/longitude values assigned in theinitialize
method (which is executed automatically whenRoastFinder.new
is called in the 'main entry point' at the bottom of theRoastFinder
file)fetch_pubs_at
uses SerpApi to scrape pub data from Google Maps, and stores the results in the local variablepub_data
- Next in the
run
method,build_pubs
is called, which iterates through thepub_data
to createPub
objects, each representing a real pub pulled in, thanks to SerpApi. Pub
objects are stored in the@pubs
instance variable- The final method called inside
run
isoutput_pubs
, which will display the properties of all thePub
objects insidepubs
as you can see a bit later on in this article.
Here's the code for RoastFinder
. Again, if you would like to follow along, I recommend putting this code in a file called roast_finder.rb
.
You will need to assign your own API key to the constant API_KEY
otherwise you will receive an error.
# Require the SerpApi Ruby library
require 'serpapi'
# Require the Ruby JSON tools
require 'json'
# Require our pub.rb file (to have access to the Pub class)
require_relative 'pub'
# Represents a 'roast finder', responsible for finding roast-serving pubs
class RoastFinder
attr_reader :pubs
API_KEY = "your api key"
# Assigns latitude/longitude values (hard-coded for Central London)
def initialize
@lat = 51.51
@long = -0.13
end
# Main workflow: search, build, analyze, score, and output pubs
def run
pub_data = fetch_pubs_at(@lat, @long)
@pubs = build_pubs(pub_data)
output_pubs
end
# Fetch pubs at a specific lat/lng using SerpApi
def fetch_pubs_at(lat, lng)
ll = format('@%.4f,%.4f,13z', lat, lng)
p "Fetching pub at #{ll}"
maps_params = {
api_key: API_KEY,
engine: 'google_maps',
q: 'pub sunday roast',
google_domain: 'google.co.uk',
gl: 'uk',
ll: ll,
type: 'search',
hl: 'en',
}
client = SerpApi::Client.new(maps_params)
client.search[:local_results] || []
end
# Build Pub objects from pub data hashes
def build_pubs(pub_data)
pub_data.map do |pub_hash|
Pub.new(
name: pub_hash[:title],
address: pub_hash[:address],
rating: pub_hash[:rating],
place_id: pub_hash[:data_id]
)
end
end
def output_pubs
puts "#{pubs.count} Pubs Found:"
puts JSON.pretty_generate(pubs.map(&:to_h))
end
end
# Main execution entry point.
if __FILE__ == $PROGRAM_NAME
RoastFinder.new.run
end
Results
Let's take a look at what we get from that search:
"Fetching pub at @51.5100,-0.1300,13z"
20 Pubs Found:
[
{
"name": "The Mayflower Pub",
"address": "117 Rotherhithe St, London, SE16 4NF, United Kingdom",
"rating": 4.7,
"place_id": "0x48760324328c71a3:0xcb470bee32fa422c"
},
{
"name": "The Kings Arms",
"address": "251 Tooley St, London, SE1 2JX, United Kingdom",
"rating": 4.6,
"place_id": "0x48760346814cdae3:0x47a29cd909c8d39f"
},
{
"name": "The Victoria, Paddington",
"address": "10A Strathearn Pl, Tyburnia, London, W2 2NH, United Kingdom",
"rating": 4.6,
"place_id": "0x4876054ca282a945:0x7517499701678b8a"
},
{
"name": "Old Shades",
"address": "37 Whitehall, London, SW1A 2BX, United Kingdom",
"rating": 4.7,
"place_id": "0x487604cfb044871f:0x9a37f113e95d776a"
},
{
"name": "The Marquis Cornwallis",
"address": "31 Marchmont St, Greater, London WC1N 1AP, United Kingdom",
"rating": 4.5,
"place_id": "0x48761b309b064dfd:0x7077a764e0b632a2"
},
{
"name": "Lord Wargrave",
"address": "40-42 Brendon St, London, W1H 5HE, United Kingdom",
"rating": 4.6,
"place_id": "0x48761ab598773367:0xa7aca0158085ae4e"
},
{
"name": "The Pig and Butcher",
"address": "80 Liverpool Rd, London, N1 0QD, United Kingdom",
"rating": 4.4,
"place_id": "0x48761b6812f60359:0x501729b45945c149"
},
{
"name": "The Windmill, Mayfair",
"address": "6-8 Mill St, London, W1S 2AZ, United Kingdom",
"rating": 4.5,
"place_id": "0x4876052a8aed9f7d:0x3865aedecde75c22"
},
{
"name": "The Fox and Pheasant",
"address": "1 Billing Rd, London, SW10 9UJ, United Kingdom",
"rating": 4.6,
"place_id": "0x4876057d60e01771:0xabaef6e4fb8e9248"
},
{
"name": "Fox & Anchor",
"address": "115 Charterhouse St, Barbican, London, EC1M 6AA, United Kingdom",
"rating": 4.5,
"place_id": "0x48761b65333b78d5:0x26ffa85e8f7d336e"
},
{
"name": "The Cadogan Arms",
"address": "298 King's Rd, London, SW3 5UG, United Kingdom",
"rating": 4.6,
"place_id": "0x487605c427caa1eb:0x9711a4c66bec6aa1"
},
{
"name": "The Hoop and Grapes",
"address": "47 Aldgate High St, Greater, London EC3N 1AL, United Kingdom",
"rating": 4.6,
"place_id": "0x4876034b397428a9:0x95b223669cf43c23"
},
{
"name": "The Hereford Arms, South Kensington",
"address": "127 Gloucester Rd, South Kensington, London, SW7 4TE, United Kingdom",
"rating": 4.4,
"place_id": "0x487605674f680181:0x2d34ca0f42aaa23e"
},
{
"name": "Lore of the Land",
"address": "4 Conway St, London, W1T 6BB, United Kingdom",
"rating": 4.6,
"place_id": "0x48761b783c613277:0x9979279b7986df20"
},
{
"name": "The Queens Head",
"address": "15 Denman St, London, W1D 7HN, United Kingdom",
"rating": 4.5,
"place_id": "0x487604d40beb0e95:0x175fabf07e4d14b2"
},
{
"name": "The Duchy Arms",
"address": "63 Sancroft St, London, SE11 5UG, United Kingdom",
"rating": 4.5,
"place_id": "0x487604947d77f0bb:0xe66d85ee32c3ac1a"
},
{
"name": "The Queens Arms",
"address": "30 Queen's Gate Mews, South Kensington, London, SW7 5QL, United Kingdom",
"rating": 4.5,
"place_id": "0x4876055bf5b22663:0x3343c17c9459ff57"
},
{
"name": "The Ladbroke Arms",
"address": "54 Ladbroke Rd, London, W11 3NW, United Kingdom",
"rating": 4.6,
"place_id": "0x48760fe47d0fd707:0x6d0614eb03a8df49"
},
{
"name": "The Scolt Head",
"address": "107A Culford Rd, London, N1 4HT, United Kingdom",
"rating": 4.5,
"place_id": "0x48761c909a936dbd:0x25780105113aaac2"
},
{
"name": "The Queens Arms",
"address": "11 Warwick Wy, Pimlico, London, SW1V 1QT, United Kingdom",
"rating": 4.4,
"place_id": "0x487604e03fe7dbc5:0xbe438016446d92bf"
}
]
Note that in this single area-based sample we already see that there are two pubs with the same name: "The Queens Arms" - but they are different establishments.
In later parts of this series, we may need to deal with duplication as coordinates might return overlapping results when we traverse the city using our grid.
Conclusion
That’s our first step toward uncovering London’s finest Sunday Roasts! We’ve now got a working script that can scrape pub data straight from Google Maps, and we’ve wrapped it up neatly into Ruby classes we can build on.
In the next part of this series, we’ll move beyond a single set of coordinates and scale this up to cover the whole city. Londoners are spoiled for choice, and we want to make sure we don’t miss any contenders.
Stick around: next we’ll start carving up the city into manageable bite-sized chunks so that we can cover every borough on our search for the ultimate Yorkshire Pudding. See you next time!