├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── client.go ├── data ├── error_sample.json ├── search_coffee_sample.json └── search_coffee_sample_page20.json ├── demo └── demo.go ├── go.mod ├── model.go ├── search.go └── search_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | 11 | build: 12 | name: Build 13 | runs-on: ubuntu-latest 14 | steps: 15 | 16 | - name: Set up Go 1.x 17 | uses: actions/setup-go@v2 18 | with: 19 | go-version: ^1.17 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v2 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | 33 | - name: Test 34 | run: go test -v . 35 | env: 36 | API_KEY: ${{ secrets.API_KEY }} 37 | 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | tmp/ 2 | *.iml 3 | .idea/ 4 | go.mod 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018-2020 SerpApi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | version=3.0 2 | 3 | all: test 4 | 5 | check: 6 | go vet . 7 | go fmt . 8 | 9 | test: 10 | go test -v . 11 | 12 | # check that everything is pushed 13 | package: 14 | git status | grep "Nothing" 15 | 16 | oobt: 17 | mkdir -p /tmp/oobt 18 | cp demo/demo.go /tmp/oobt 19 | cd /tmp/oobt ; \ 20 | go get -u github.com/serpapi/google_search_results_golang ; \ 21 | go run demo.go 22 | 23 | release: oobt 24 | git tag -a ${version} 25 | git push origin ${version} 26 | @echo "create release: ${version}" 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Google Search Results GoLang API 2 | 3 | ![test](https://github.com/serpapi/google-search-results-golang/workflows/Go/badge.svg) 4 | 5 | This Golang package enables to scrape and parse results from Google, Bing, Baidu, Yahoo, Yandex, Ebay, Google Schoolar and more using [SerpApi](https://serpapi.com). 6 | 7 | This project is an implementation of SerpApi in Golang 1.12 8 | There is no dependency for this project. 9 | 10 | This Go module is meant to scrape and parse Google results using [SerpApi](https://serpapi.com). 11 | The following services are provided: 12 | * [Search API](https://serpapi.com/search-api) 13 | * [Location API](https://serpapi.com/locations-api) 14 | * [Search Archive API](https://serpapi.com/search-archive-api) 15 | * [Account API](https://serpapi.com/account-api) 16 | 17 | SerpApi provides a [script builder](https://serpapi.com/demo) to get you started quickly. 18 | 19 | An implementation example is provided here. 20 | > demo/demo.go 21 | 22 | You take a look to our test. 23 | > test/google_seach_results_test.go 24 | 25 | [The full documentation is available here.](https://serpapi.com/search-api) 26 | 27 | 28 | ## Installation 29 | 30 | Go 1.10+ must be already installed. 31 | ```bash 32 | go get -u github.com/serpapi/google-search-results-golang 33 | ``` 34 | 35 | ## Quick start 36 | 37 | ```go 38 | package main 39 | 40 | import ( 41 | "fmt" 42 | g "github.com/serpapi/google-search-results-golang" 43 | ) 44 | 45 | parameter := map[string]string{ 46 | "q": "Coffee", 47 | "location": "Portland" 48 | } 49 | 50 | query := NewGoogleSearch(parameter, api) 51 | // Many search engine available: bing, yahoo, baidu, googemaps, googleproduct, googlescholar, ebay, walmart, youtube.. 52 | 53 | rsp, err := query.json() 54 | results := rsp["organic_results"].([]interface{}) 55 | first_result := results[0].(map[string]interface{}) 56 | fmt.Println(ref["title"].(string)) 57 | ``` 58 | 59 | This example runs a search about "coffee" using your secret api key. 60 | 61 | The SerpApi service (backend) 62 | - searches on Google using the search: q = "coffee" 63 | - parses the messy HTML responses 64 | - return a standardizes JSON response 65 | - Format the request 66 | - Execute GET http request against SerpApi service 67 | - Parse JSON response into a deep hash 68 | Et voila.. 69 | 70 | Alternatively, you can search: 71 | - Bing using NewBingSearch method 72 | - Baidu using NewBaiduSearch method 73 | 74 | See the [playground to generate your code.](https://serpapi.com/playground) 75 | 76 | ## Example 77 | * [Search API capability](#search-api-capability) 78 | * [Example by specification](#example-by-specification) 79 | * [Location API](#location-api) 80 | * [Search Archive API](#search-archive-api) 81 | * [Account API](#account-api) 82 | 83 | ### Search API capability 84 | ```go 85 | parameter = { 86 | "q": "query", 87 | "google_domain": "Google Domain", 88 | "location": "Location Requested", 89 | "device": device, 90 | "hl": "Google UI Language", 91 | "gl": "Google Country", 92 | "safe": "Safe Search Flag", 93 | "num": "Number of Results", 94 | "start": "Pagination Offset", 95 | "api_key": "Your SERP API Key", 96 | "tbm": "nws|isch|shop", 97 | "tbs": "custom to be search criteria", 98 | "async": true|false, // allow async 99 | "output": "json|html" // output format 100 | } 101 | 102 | // api_key from https://serpapi.com/dashboard 103 | api_key := "your personal API key" 104 | 105 | // set search engine: google|yahoo|bing|ebay|yandex 106 | engine := "yahoo" 107 | 108 | // define the search search 109 | search := NewSearch(engine, parameter, api_key) 110 | 111 | // override an existing parameter 112 | search.parameter["location"] = "Portland,Oregon,United States" 113 | 114 | // search format return as raw html 115 | data, err := search.GetHTML() 116 | 117 | // search format returns a json 118 | data, err := search.GetJSON() 119 | ``` 120 | 121 | (the full documentation)[https://serpapi.com/search-api] 122 | 123 | Full example: [https://github.com/serpapi/google-search-results-golang/blob/master/demo/demo.go] 124 | 125 | see below for more hands on examples. 126 | 127 | ### Location API 128 | 129 | ```go 130 | var locationList SerpResponseArray 131 | var err error 132 | locationList, err = search.GetLocation("Austin", 3) 133 | 134 | if err != nil { 135 | log.Println(err) 136 | } 137 | log.Println(locationList) 138 | ``` 139 | rsp contains the first 3 location matching Austin (Texas, Texas, Rochester) 140 | 141 | ### Search Archive API 142 | 143 | Run a search then get search result from the archive using the search archive API. 144 | ```go 145 | parameter := map[string]string{ 146 | "q": "Coffee", 147 | "location": "Portland" 148 | } 149 | 150 | search := NewGoogleSearch(parameter, "your user key") 151 | rsp, err := search.GetJSON() 152 | 153 | if err != nil { 154 | log.Println("unexpected error", err) 155 | } 156 | 157 | searchID := rsp["search_metadata"].(map[string]interface{})["id"].(string) 158 | searchArchive, err := search.GetSearchArchive(searchID) 159 | if err != nil { 160 | log.Println(err) 161 | return 162 | } 163 | 164 | searchIDArchive := searchArchive["search_metadata"].(map[string]interface{})["id"].(string) 165 | if searchIDArchive != searchID { 166 | log.Println("search_metadata.id do not match", searchIDArchive, searchID) 167 | } 168 | 169 | log.Println(searchIDArchive) 170 | ``` 171 | it prints the search ID from the archive. 172 | 173 | ### Account API 174 | ```go 175 | var data SearchResult 176 | var err error 177 | data, err = search.GetAccount() 178 | 179 | if err != nil { 180 | log.Println(err) 181 | return 182 | } 183 | log.Println(data) 184 | ``` 185 | data contains the account information. 186 | 187 | ### Example by specification 188 | 189 | We love true open source, continuous integration and Test Drive Development (TDD). 190 | We are using "go test" to test our infrastructure around the clock 191 | to achieve the best QoS (Quality Of Service). 192 | 193 | The directory test/ includes specification/examples. 194 | 195 | To run the test from bash using make 196 | ```bash 197 | export API_KEY="your secret key" 198 | make test 199 | ``` 200 | 201 | ### Error management 202 | 203 | This library follows the basic error management solution provided by Go. 204 | A simple error is returned in case something goes wrong. 205 | The error wraps a simple error message. 206 | 207 | ## Change log 208 | * 3.2 209 | - add naver search 210 | - add apple store search 211 | * 3.1 212 | - Add home depot search engine 213 | * 3.0 214 | - Naming convention change. 215 | - Rename Client to Search 216 | - Fix lint issue 217 | - Add walmart and youtube 218 | * 2.1 219 | - Add support for Yandex, Ebay, Yahoo 220 | - create HTTP search only once per SerpApiClient 221 | * 2.0 Rewrite fully the implementation 222 | to be more scalable in order to support multiple engines. 223 | * 1.3 Add support for Bing and Baidu 224 | * 1.2 Export NewGoogleSearch outside of the package. 225 | 226 | ## Conclusion 227 | 228 | SerpApi supports mutiple search engines and subservices all available for this Golang search. 229 | 230 | For example: Using Google search. 231 | To enable a type of search, the field tbm (to be matched) must be set to: 232 | 233 | * isch: Google Images API. 234 | * nws: Google News API. 235 | * shop: Google Shopping API. 236 | * any other Google service should work out of the box. 237 | * (no tbm parameter): regular Google search. 238 | 239 | The field `tbs` allows to customize the search even more. 240 | 241 | [The full documentation is available here.](https://serpapi.com/search-api) 242 | 243 | ## Contributing 244 | 245 | Contributions are welcome, feel to submit a pull request! 246 | 247 | To run the tests: 248 | 249 | ```bash 250 | make test 251 | ``` 252 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "io" 7 | "io/ioutil" 8 | "net/http" 9 | "net/url" 10 | ) 11 | 12 | // decodeJson response 13 | func (search *Search) decodeJSON(body io.ReadCloser) (SearchResult, error) { 14 | // Decode JSON from response body 15 | decoder := json.NewDecoder(body) 16 | 17 | // Response data 18 | var rsp SearchResult 19 | err := decoder.Decode(&rsp) 20 | if err != nil { 21 | return nil, errors.New("fail to decode") 22 | } 23 | 24 | // check error message 25 | errorMessage, derror := rsp["error"].(string) 26 | if derror { 27 | return nil, errors.New(errorMessage) 28 | } 29 | return rsp, nil 30 | } 31 | 32 | // decodeJSONArray decodes response body to SearchResultArray 33 | func (search *Search) decodeJSONArray(body io.ReadCloser) (SearchResultArray, error) { 34 | decoder := json.NewDecoder(body) 35 | var rsp SearchResultArray 36 | err := decoder.Decode(&rsp) 37 | if err != nil { 38 | return nil, errors.New("fail to decode array") 39 | } 40 | return rsp, nil 41 | } 42 | 43 | // decodeHTML decodes response body to html string 44 | func (search *Search) decodeHTML(body io.ReadCloser) (*string, error) { 45 | buffer, err := ioutil.ReadAll(body) 46 | if err != nil { 47 | return nil, err 48 | } 49 | text := string(buffer) 50 | return &text, nil 51 | } 52 | 53 | // execute HTTP get reuqest and returns http response 54 | func (search *Search) execute(path string, output string) (*http.Response, error) { 55 | query := url.Values{} 56 | if search.Parameter != nil { 57 | for k, v := range search.Parameter { 58 | query.Add(k, v) 59 | } 60 | } 61 | 62 | // api_key 63 | if len(search.ApiKey) != 0 { 64 | query.Add("api_key", search.ApiKey) 65 | } 66 | 67 | // engine 68 | if len(query.Get("engine")) == 0 { 69 | query.Set("engine", search.Engine) 70 | } 71 | 72 | // source programming language 73 | query.Add("source", "go") 74 | 75 | // set output 76 | query.Add("output", output) 77 | 78 | endpoint := "https://serpapi.com" + path + "?" + query.Encode() 79 | rsp, err := search.HttpSearch.Get(endpoint) 80 | if err != nil { 81 | return nil, err 82 | } 83 | return rsp, nil 84 | } 85 | -------------------------------------------------------------------------------- /data/error_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "error": "Your account credit is too low, please add more credits." 3 | } -------------------------------------------------------------------------------- /data/search_coffee_sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "search_information": { 3 | "total_results": 1670000000, 4 | "time_taken": 0.77, 5 | "query": "Coffee", 6 | "location": "Portland, Oregon" 7 | }, 8 | "serp_api_data": { 9 | "id": { 10 | "$oid": "5a5c7762defb2f7726dabf9c" 11 | }, 12 | "total_time_taken": 70.556139227, 13 | "google_url": "https://www.google.com/search?q=Coffee&oq=Coffee&uule=w+CAIQICIdUG9ydGxhbmQsT3JlZ29uLFVuaXRlZCBTdGF0ZXM&sourceid=chrome&ie=UTF-8" 14 | }, 15 | "map": { 16 | "link": "https://www.google.com/search?q=Coffee&npsic=0&rflfq=1&rldoc=1&rlha=0&rllag=45520495,-122678661,430&tbm=lcl&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QtgMIKQ", 17 | "image": true 18 | }, 19 | "local_results": [ 20 | { 21 | "position": 1, 22 | "title": "Stumptown Coffee Roasters", 23 | "rating": 4.4, 24 | "reviews": 941, 25 | "price": "$", 26 | "type": "Cafe", 27 | "address": "128 SW 3rd Ave", 28 | "description": "Coffee bar serving direct-trade java", 29 | "hours": "Opens at 6:00 AM", 30 | "thumbnail": true 31 | }, 32 | { 33 | "position": 2, 34 | "title": "Heart Coffee", 35 | "rating": 4.5, 36 | "reviews": 282, 37 | "type": "Coffee Shop", 38 | "address": "537 SW 12th Ave", 39 | "description": "Stylish spot for coffee drinking", 40 | "hours": "Opens at 7:00 AM", 41 | "thumbnail": true 42 | }, 43 | { 44 | "position": 3, 45 | "title": "Barista", 46 | "rating": 4.5, 47 | "reviews": 155, 48 | "type": "Coffee Shop", 49 | "address": "529 SW 3rd Ave #110", 50 | "description": "Petite cafe serving multiple roasters", 51 | "hours": "Opens at 6:00 AM", 52 | "thumbnail": true 53 | } 54 | ], 55 | "knowledge_graph": { 56 | "title": "Coffee", 57 | "type": "Drink", 58 | "image": true, 59 | "description": "Coffee is a brewed drink prepared from roasted coffee beans, which are the seeds of berries from the Coffea plant. The genus Coffea is native to tropical Africa and Madagascar, the Comoros, Mauritius, and Réunion in the Indian Ocean.", 60 | "source": { 61 | "name": "Wikipedia", 62 | "link": "https://en.wikipedia.org/wiki/Coffee" 63 | }, 64 | "related_searches": [ 65 | { 66 | "name": "Tea", 67 | "link": "https://www.google.com/search?q=Tea&stick=H4sIAAAAAAAAAONgFuLUz9U3MCorTMtV4gAxzZNzKrQE_UpLijJLMvPzgjNTUssTK4sByyY5qioAAAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QxA0IkQIwGw", 68 | "image": true 69 | }, 70 | { 71 | "name": "Espresso", 72 | "link": "https://www.google.com/search?q=Espresso&stick=H4sIAAAAAAAAAONgFuLUz9U3MCorTMtV4gAxk0vSsrUE_UpLijJLMvPzgjNTUssTK4sBwYKq7CoAAAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QxA0IkwIwGw", 73 | "image": true 74 | }, 75 | { 76 | "name": "Chocolate", 77 | "link": "https://www.google.com/search?q=Chocolate&stick=H4sIAAAAAAAAAONgFuLUz9U3MCorTMtV4gAzDcpytAT9SkuKMksy8_OCM1NSyxMriwHR8y5PKgAAAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QxA0IlQIwGw", 78 | "image": true 79 | }, 80 | { 81 | "name": "Cappuccino", 82 | "link": "https://www.google.com/search?q=Cappuccino&stick=H4sIAAAAAAAAAONgFuLUz9U3MCorTMtV4gAxLc0NjLQE_UpLijJLMvPzgjNTUssTK4sB6b-xkioAAAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QxA0IlwIwGw", 83 | "image": true 84 | }, 85 | { 86 | "name": "Latte", 87 | "link": "https://www.google.com/search?q=Latte&stick=H4sIAAAAAAAAAONgFuLUz9U3MCorTMtVAjMN84pMzbQE_UpLijJLMvPzgjNTUssTK4sBjSmgBisAAAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QxA0ImQIwGw", 88 | "image": true 89 | } 90 | ], 91 | "more_related_searches_link": "https://www.google.com/search?q=Coffee&stick=H4sIAAAAAAAAAONgFuLUz9U3MCorTMtVQjC1xLKTrfTT8vNTwIRVcWZKanliZTEAKVBbezAAAAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QzToIjgIoATAb" 92 | }, 93 | "answer_box": { 94 | "type": "unknow" 95 | }, 96 | "organic_results": [ 97 | { 98 | "position": 1, 99 | "title": "Portland Roasting Coffee", 100 | "link": "https://portlandroastingcoffee.com/", 101 | "displayed_link": "https://portlandroastingcoffee.com/", 102 | "snippet": "Costa Rica. Light | Single OriginBUY · Tour the roastery click here for information. Locate Portland Roasting Coffee. Locate. Portland Roasting Coffee, established 1996. Learn More. FOLLOW US ON INSTAGRAM @pdxroasting ...", 103 | "related_link": "https://www.google.com/search?q=related:https://portlandroastingcoffee.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QHwhtMAM" 104 | }, 105 | { 106 | "position": 2, 107 | "title": "Sip Coffee at These 20 Portland Roasters and Cafes - Eater Portland", 108 | "link": "https://pdx.eater.com/maps/20-great-portland-coffee-shops", 109 | "displayed_link": "https://pdx.eater.com/maps/20-great-portland-coffee-shops", 110 | "date": "Sep 20, 2017", 111 | "snippet": "Sometimes, when you love something, you just keep talking about it. In this case, that would be coffee, coffee people, and coffeeshops. To get up close and personal with Portland coffee, Emily McIntyre, a former barista turned coffee writer and consultant, shares the 20 coffee shops she thinks serve the best ...", 112 | "sitelinks": { 113 | "inline": [ 114 | { 115 | "title": "Sip Coffee at These 20 ...", 116 | "link": "https://pdx.eater.com/maps/20-great-portland-coffee-shops/either-or" 117 | }, 118 | { 119 | "title": "Good Coffee", 120 | "link": "https://pdx.eater.com/maps/20-great-portland-coffee-shops/good-coffee" 121 | }, 122 | { 123 | "title": "Coava Coffee Brew Bar", 124 | "link": "https://pdx.eater.com/maps/20-great-portland-coffee-shops/coava-coffee-brew-bar" 125 | }, 126 | { 127 | "title": "Courier Coffee", 128 | "link": "https://pdx.eater.com/maps/20-great-portland-coffee-shops/courier-coffee" 129 | } 130 | ] 131 | }, 132 | "related_link": "https://www.google.com/search?q=related:https://pdx.eater.com/maps/20-great-portland-coffee-shops+Coffee&tbo=1&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QHwhzMAQ" 133 | }, 134 | { 135 | "position": 3, 136 | "title": "Coffee - Wikipedia", 137 | "link": "https://en.wikipedia.org/wiki/Coffee", 138 | "displayed_link": "https://en.wikipedia.org/wiki/Coffee", 139 | "snippet": "Coffee is a brewed drink prepared from roasted coffee beans, which are the seeds of berries from the Coffea plant. The genus Coffea is native to tropical Africa and Madagascar, the Comoros, Mauritius, and Réunion in the Indian Ocean. The plant was exported from Africa to Arabia and to countries around the world. Coffee ...", 140 | "sitelinks": { 141 | "inline": [ 142 | { 143 | "title": "History", 144 | "link": "https://en.wikipedia.org/wiki/Coffee#History" 145 | }, 146 | { 147 | "title": "Cultivation", 148 | "link": "https://en.wikipedia.org/wiki/Coffee#Cultivation" 149 | }, 150 | { 151 | "title": "Processing", 152 | "link": "https://en.wikipedia.org/wiki/Coffee#Processing" 153 | }, 154 | { 155 | "title": "Health effects", 156 | "link": "https://en.wikipedia.org/wiki/Coffee#Health_effects" 157 | } 158 | ] 159 | }, 160 | "related_link": "https://www.google.com/search?q=related:https://en.wikipedia.org/wiki/Coffee+Coffee&tbo=1&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QHwh_MAU" 161 | }, 162 | { 163 | "position": 4, 164 | "title": "Public Domain Specialty Coffee Shop - Portland, OR", 165 | "link": "http://www.publicdomaincoffee.com/", 166 | "displayed_link": "www.publicdomaincoffee.com/", 167 | "snippet": "Specialty coffee shop. Artisanal roast masters batch roast coffee weekly. Skilled baristas create customized coffee drinks.", 168 | "cached_link": "http://webcache.googleusercontent.com/search?q=cache:6VLIhl7rVCsJ:www.publicdomaincoffee.com/+&cd=7&hl=en&ct=clnk&gl=us", 169 | "related_link": "https://www.google.com/search?q=related:www.publicdomaincoffee.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QHwiKATAG" 170 | }, 171 | { 172 | "position": 5, 173 | "title": "Case Study Coffee - Portland", 174 | "link": "https://www.casestudycoffee.com/", 175 | "displayed_link": "https://www.casestudycoffee.com/", 176 | "snippet": "Roasting direct trade and seasonal coffees in Portland, Oregon.", 177 | "related_link": "https://www.google.com/search?q=related:https://www.casestudycoffee.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QHwiQATAH" 178 | }, 179 | { 180 | "position": 6, 181 | "title": "Heart Coffee Roasters - Portland", 182 | "link": "https://www.heartroasters.com/", 183 | "displayed_link": "https://www.heartroasters.com/", 184 | "snippet": "Heart is a café, roaster & bustling destination point located in Portland, Oregon.", 185 | "related_link": "https://www.google.com/search?q=related:https://www.heartroasters.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QHwiWATAI" 186 | }, 187 | { 188 | "position": 7, 189 | "title": "16 Places in Portland to Drink Coffee Right Now | Portland Monthly", 190 | "link": "https://www.pdxmonthly.com/articles/2017/2/20/16-places-in-portland-to-drink-coffee-right-now", 191 | "displayed_link": "https://www.pdxmonthly.com/.../2/.../16-places-in-portland-to-drink-coffee-right-now", 192 | "date": "Feb 20, 2017", 193 | "snippet": "Northeast's Roseway hood was a coffee wasteland until BFFs Marten Boyden and Austin Roberts opened this bright, spartan café in 2016. Now neighbors clamor for the chatty pair's exuberant yet balanced roasts—consumed via drip, via pour-over, or prepped on the café's Rocket R9 (plus, heady, ...", 194 | "related_link": "https://www.google.com/search?q=related:https://www.pdxmonthly.com/articles/2017/2/20/16-places-in-portland-to-drink-coffee-right-now+Coffee&tbo=1&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QHwicATAJ" 195 | }, 196 | { 197 | "position": 8, 198 | "title": "Water Avenue Coffee - Portland", 199 | "link": "https://wateravenuecoffee.com/", 200 | "displayed_link": "https://wateravenuecoffee.com/", 201 | "snippet": "Roaster and coffee bars located in Portland, Oregon.", 202 | "related_link": "https://www.google.com/search?q=related:https://wateravenuecoffee.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8QHwijATAK" 203 | }, 204 | { 205 | "position": 9, 206 | "title": "Downtown Portland's 10 best coffee shops | OregonLive.com - Portland", 207 | "link": "http://www.oregonlive.com/dining/index.ssf/2017/09/downtown_portlands_10_best_cof.html", 208 | "displayed_link": "www.oregonlive.com/dining/index.ssf/2017/.../downtown_portlands_10_best_cof.ht...", 209 | "date": "Sep 27, 2017", 210 | "snippet": "When you're looking for locally roasted beans, a treat yourself latte or somewhere relaxing to post up for a couple hours, these 10 downtown coffee shops are for you.", 211 | "cached_link": "http://webcache.googleusercontent.com/search?q=cache:ec8ZBekWxroJ:www.oregonlive.com/dining/index.ssf/2017/09/downtown_portlands_10_best_cof.html+&cd=12&hl=en&ct=clnk&gl=us" 212 | }, 213 | { 214 | "position": 10, 215 | "title": "Bulletproof Coffee: Debunking the Hot Buttered Hype - Gizmodo", 216 | "link": "https://gizmodo.com/bulletproof-coffee-debunking-the-hot-buttered-hype-1681321467", 217 | "displayed_link": "https://gizmodo.com/bulletproof-coffee-debunking-the-hot-buttered-hype-1681321467", 218 | "snippet": "People are putting butter in their coffee. And hey, if you're just craving a new flavor experience, more power to you. The problem is that Bulletproof Coffee, the company behind the trend, is claiming that drinking a mug of fatty joe every morning instead of eating breakfast is a secret shortcut to weight loss and ..." 219 | }, 220 | { 221 | "position": 11, 222 | "title": "The Case for Drinking as Much Coffee as You Like - The Atlantic", 223 | "link": "https://www.theatlantic.com/health/archive/2012/11/the-case-for-drinking-as-much-coffee-as-you-like/265693/", 224 | "displayed_link": "https://www.theatlantic.com/health/archive/2012/11/the-case-for...coffee.../265693/", 225 | "snippet": "\"What I tell patients is, if you like coffee, go ahead and drink as much as you want and can,\" says Dr. Peter Martin, director of the Institute for Coffee Studies at Vanderbilt University. He's even developed a metric for monitoring your dosage: If you are having trouble sleeping, cut back on your last cup of the ..." 226 | }, 227 | { 228 | "position": 12, 229 | "title": "More Consensus on Coffee's Effect on Health Than You Might Think ...", 230 | "link": "https://www.nytimes.com/2015/05/12/upshot/more-consensus-on-coffees-benefits-than-you-might-think.html", 231 | "displayed_link": "https://www.nytimes.com/.../more-consensus-on-coffees-benefits-than-you-might-think....", 232 | "snippet": "A review of studies shows that coffee's reputation as being unhealthy is undeserved, with the potential health benefits surprisingly large." 233 | } 234 | ], 235 | "related_place_searches": [ 236 | { 237 | "query": "Best coffee", 238 | "link": "https://www.google.com/search?tbm=lcl&q=Coffee&rflfq=1&num=20&uule=w+CAIQICIdUG9ydGxhbmQsT3JlZ29uLFVuaXRlZCBTdGF0ZXM&stick=H4sIAAAAAAAAACWQO04EMRBENQlCIpyNHHGE_lV_Uq6CZlMCOBjn4hSUdyXLQbtc_apeX9bphomQyKwsaZWwdU6lp0u1TQy0Bcnh-KRyzCPQlJJ1tmu1aoV1oulTvW5aEBHv5t0IBcbXGZGd7k3fscgyG0odEuoNKKg3uDunUJ_qSAe1afaccolPN0GMJrRNbIcJSjLVkxghQ9ibPja09xiMWUKNtEwZOxJFXmjP3rYuttMEqeAVWhJMi5wZ8GXUOErZu6SLYdUtIU0cVG2uqI0pznZqgAruKjd-ZKPMxU8Mbg8pC9uMAjHaz7BZQ8OAYJFpRRpjXcNqe5pMRRfmTvs9jr_j7eP6_nn__Lrfr-sfANw1CsEBAAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q_KoBCLUBMA8", 239 | "snippet": "Spella Caffe, Courier Coffee, and 18 more" 240 | }, 241 | { 242 | "query": "Coffee and Wi-Fi", 243 | "link": "https://www.google.com/search?tbm=lcl&q=Coffee&rflfq=1&num=20&uule=w+CAIQICIdUG9ydGxhbmQsT3JlZ29uLFVuaXRlZCBTdGF0ZXM&stick=H4sIAAAAAAAAAB2QS0pEMRRE6Yk4FJ-jjHoDwv3V_YwFt-BYsBt6oltzXa7CSkN4CeHm1Hn1-LAOH7MYTGjAAFHR9aIFEfFufhuhwPg62rVatcI60SFRvY4Zn9R04RJoSgnfu9g-xljAK7Qk1hGRne7tUrzPMhuORhAbMhCIDWwmNwAS6g0oaGFw9z1r7dPNTCOEWok9O2FpmepJt43KO7cYk-KUqwGYQVvkDEM0R41WKRuQoYKyUCYJYQXbWOP_1LSTwXp41vUcyb4o5VM9WZnsZUTSFOnsMTEavXupdFZSbcNrkil1mLhNs5eA2p6fYNC9i_Yeg_FNqNHUusScRQ83tUro7-n0d3p6-7leL5fz5_fX-eP2-n77B5_2sdPFAQAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q_KoBCLcBMBA", 244 | "snippet": "Floyd's Coffee Shop Old Town, Public Domain, and 18 more" 245 | }, 246 | { 247 | "query": "Espresso bars", 248 | "link": "https://www.google.com/search?tbm=lcl&q=Coffee&rflfq=1&num=20&uule=w+CAIQICIdUG9ydGxhbmQsT3JlZ29uLFVuaXRlZCBTdGF0ZXM&stick=H4sIAAAAAAAAABWQu01FMRBE9RJEQsIluhElzO7O_gqgEJCIQbzGqIsqmGtZtrSy53MeH84XC7gVmuvMaFqDmpJhTWwi4Zu-W5qmxfawInu93CNC004AMaNzkpa5cR67sWUV0EZeDjiP8FwSrOpqjIF-Hj4zOyh9bqsO40i0aMh22sCx9E6_wrq0eicKsbKyNqkqe8qvc5jg6NRTBKk2MYHhwiIUoM12EkZX3vXmNaTawrO8Sp5qUVcAlROZZCiy9LHqetiQqb8eoHnXQP6ChtqoUlpdsmq12hQX25yWSk0szmfhE9VQb4FdeI9ACSh2OrVaWIT1Si9szlZvl0emEv_ebn-3p7f798_n_f71-vH-c_8HG6XI1MEBAAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q_KoBCLkBMBE", 249 | "snippet": "Water Avenue Coffee, Caffè Umbria, and 18 more" 250 | }, 251 | { 252 | "query": "College hangouts", 253 | "link": "https://www.google.com/search?tbm=lcl&q=Coffee&rflfq=1&num=20&uule=w+CAIQICIdUG9ydGxhbmQsT3JlZ29uLFVuaXRlZCBTdGF0ZXM&stick=H4sIAAAAAAAAAB2RO04dQRRE9RLLIWKIJvISbtX9x6yEAOEAQYC9L6-LVbjmRd3quV3V58zPH-fBnGRmDFBsI-jnExzO3O5iOsmu7PNABmpn19tpUwE_j-3ycuvhxibGss5jHK3ADk7lhEXPeUTUlPtoeBnV5F5NacqZTOSMqc6v_vv38VkmFR-gXbMbLFbBS9Fhq64nWNpYpW00W6k291xaone8TNegPQQ7InS9abXgwsI1KuqyDr0qvQNtcR41iOHkYgiw5EP3XSJYCJMq7WOkxXcbKkeVGUqUOkyIlLUhIbsKV9Ojw8xa_SVLrLlUidAjklLq3pScizQAWQ6Dfo5QJJP_brfv28Pz5_v769vrr98vH2-ff_98_QcqlTeuwwEAAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q_KoBCLsBMBI", 254 | "snippet": "Revolución Coffee House, Rimsky-Korsakoffee House, and 18 more" 255 | }, 256 | { 257 | "query": "Coffee shops", 258 | "link": "https://www.google.com/search?tbm=lcl&q=Coffee&rflfq=1&num=20&uule=w+CAIQICIdUG9ydGxhbmQsT3JlZ29uLFVuaXRlZCBTdGF0ZXM&stick=H4sIAAAAAAAAACVQO25cQQzDNkEKd5vqVTmCRIn61DnLLtxtAF_M5_IpwomBwRQUKZH8-eO674TPDpLtNrFTuO5ZldjctI3mRE1c9w44xHHOtHvE4PrlTTOLGf3DdHJFzawpEcJ6kdXA6lJXlJA5m-ljLOn_jydmQWicDhMaBi_rlJrR6W153eW0x70TU5y07DlUWnoMeXwZGBFCM0WWfRoNS-yeY7GJQpVHacsZH1T7lHt2A7KnCEW5Ze1K6LVK3VYmMMBNHa7qajVhqbJiAQXa9CRIc3PJN7ZccfWMJ8uJRTnoyQqqlwK-vVa6sZGqBCaHzVOsnZLbA0UbWWT35-32dXv783o-H4_fH--vvx__AN8MsxXCAQAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q_KoBCL0BMBM", 259 | "snippet": "Deadstock Coffee, Portland Roasting, and 18 more" 260 | }, 261 | { 262 | "query": "Quiet cafes", 263 | "link": "https://www.google.com/search?tbm=lcl&q=Coffee&rflfq=1&num=20&uule=w+CAIQICIdUG9ydGxhbmQsT3JlZ29uLFVuaXRlZCBTdGF0ZXM&stick=H4sIAAAAAAAAAB2QTUolQRCEeRsZcGfPqlceIf8z4hgeQeQJrme8l-fyFEYLRRUEEVlf5J-H86mGEe2dXHB2Js9j3Yk2r0iAsZV2HggjyyxsPdwRwfOomQqWdOY2cqA4mRyfNB1rH1vFo9HRXXCfWAuPPP96ua91hlWUW3D4qzatQVG4BAxQUjMb0DWJ7cxhrcaOopEewysGoIVKJ6lvWYWELfx8Whthd06wxaveMk6Xyqc8zqwSxXn0VPpSLk6L1jjnkcawrHETlol1LiRFoUWYtkSqnXGlwhtWYF4lPTUO2umVYc6oqB7zvKxq3vpLi7Nkad661K21XN8QLWKiA1-32_ft8eXz4_7_-e31_f7vB9j0MRO8AQAA&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q_KoBCL8BMBQ", 264 | "snippet": "Urban Grind Coffee, Museum Grounds Cafe, and 18 more" 265 | } 266 | ], 267 | "related_searches": [ 268 | { 269 | "query": "top 5 best coffee roasters in portland", 270 | "link": "https://www.google.com/search?q=top+5+best+coffee+roasters+in+portland&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q1QIIwQEoAA" 271 | }, 272 | { 273 | "query": "oregon coffee companies", 274 | "link": "https://www.google.com/search?q=oregon+coffee+companies&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q1QIIwgEoAQ" 275 | }, 276 | { 277 | "query": "portland roasting coffee", 278 | "link": "https://www.google.com/search?q=portland+roasting+coffee&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q1QIIwwEoAg" 279 | }, 280 | { 281 | "query": "coava coffee roasters portland or", 282 | "link": "https://www.google.com/search?q=coava+coffee+roasters+portland+or&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q1QIIxAEoAw" 283 | }, 284 | { 285 | "query": "stumptown coffee roasters portland or", 286 | "link": "https://www.google.com/search?q=stumptown+coffee+roasters+portland+or&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q1QIIxQEoBA" 287 | }, 288 | { 289 | "query": "where to buy portland roasting coffee", 290 | "link": "https://www.google.com/search?q=where+to+buy+portland+roasting+coffee&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q1QIIxgEoBQ" 291 | }, 292 | { 293 | "query": "portland roasting coffee menu", 294 | "link": "https://www.google.com/search?q=portland+roasting+coffee+menu&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q1QIIxwEoBg" 295 | }, 296 | { 297 | "query": "portland roasting coffee k cups", 298 | "link": "https://www.google.com/search?q=portland+roasting+coffee+k+cups&sa=X&ved=0ahUKEwjqmbOG19nYAhXhmeAKHWwnCW8Q1QIIyAEoBw" 299 | } 300 | ], 301 | "pagination": { 302 | "current": 1, 303 | "next": "https://www.google.com/search?q=Coffee&ei=p3dcWqqqM-GzggfszqT4Bg&start=10&sa=N", 304 | "other_pages": { 305 | "2": "https://www.google.com/search?q=Coffee&ei=p3dcWqqqM-GzggfszqT4Bg&start=10&sa=N", 306 | "3": "https://www.google.com/search?q=Coffee&ei=p3dcWqqqM-GzggfszqT4Bg&start=20&sa=N", 307 | "4": "https://www.google.com/search?q=Coffee&ei=p3dcWqqqM-GzggfszqT4Bg&start=30&sa=N", 308 | "5": "https://www.google.com/search?q=Coffee&ei=p3dcWqqqM-GzggfszqT4Bg&start=40&sa=N", 309 | "6": "https://www.google.com/search?q=Coffee&ei=p3dcWqqqM-GzggfszqT4Bg&start=50&sa=N", 310 | "7": "https://www.google.com/search?q=Coffee&ei=p3dcWqqqM-GzggfszqT4Bg&start=60&sa=N", 311 | "8": "https://www.google.com/search?q=Coffee&ei=p3dcWqqqM-GzggfszqT4Bg&start=70&sa=N", 312 | "9": "https://www.google.com/search?q=Coffee&ei=p3dcWqqqM-GzggfszqT4Bg&start=80&sa=N", 313 | "10": "https://www.google.com/search?q=Coffee&ei=p3dcWqqqM-GzggfszqT4Bg&start=90&sa=N" 314 | } 315 | } 316 | } -------------------------------------------------------------------------------- /data/search_coffee_sample_page20.json: -------------------------------------------------------------------------------- 1 | { 2 | "search_information": { 3 | "total_results": 3534000000, 4 | "time_taken": 0.69, 5 | "query": "Coffee", 6 | "location": "Portland, Oregon" 7 | }, 8 | "serp_api_data": { 9 | "internal_id": "5a788f85defb2f69513898dd", 10 | "total_time_taken": 74.313295218, 11 | "google_url": "https://www.google.com/search?q=Coffee&oq=Coffee&uule=w+CAIQICIdUG9ydGxhbmQsT3JlZ29uLFVuaXRlZCBTdGF0ZXM&start=20&sourceid=chrome&ie=UTF-8" 12 | }, 13 | "ads": [ 14 | { 15 | "position": 1, 16 | "block_position": "bottom", 17 | "title": "Gourmet Coffee | We Roast & Ship Daily‎", 18 | "link": "https://www.coffeeam.com/gourmet-coffee.html", 19 | "displayed_link": "www.coffeeam.com/Fresh/Coffee", 20 | "rating": 4.7, 21 | "reviews": 1228, 22 | "description": "Enjoy Fresh Roasted Coffee. Delivered Direct to You!", 23 | "extensions": [ 24 | "$50+, Coffee Ships Free", 25 | "Specialty", 26 | "Grade Coffee", 27 | "Freshest Coffee Online", 28 | "Roasted Fresh Daily" 29 | ], 30 | "sitelinks": [ 31 | { 32 | "title": "Fair Trade Coffee", 33 | "link": "https://www.coffeeam.com/gourmet-coffee/regular/certified-fair-trade-coffee.html/" 34 | }, 35 | { 36 | "title": "Best Single-Origin Coffee", 37 | "link": "https://www.coffeeam.com/what-trending/top-rated-single-origin-coffee.html" 38 | }, 39 | { 40 | "title": "Organic Coffee", 41 | "link": "https://www.coffeeam.com/gourmet-coffee/regular/organic-coffee.html" 42 | }, 43 | { 44 | "title": "Gourmet Coffee Club", 45 | "link": "https://www.coffeeam.com/coffee-tea-gifts-1/coffee-club.html/" 46 | } 47 | ] 48 | }, 49 | { 50 | "position": 2, 51 | "block_position": "bottom", 52 | "title": "Coffee Beans Online | Coffee Bean Direct®‎", 53 | "link": "https://www.coffeebeandirect.com/", 54 | "displayed_link": "www.coffeebeandirect.com/", 55 | "description": "Fresh Roasted Specialty Coffee. Free Shipping Available On Orders $50+", 56 | "extensions": [ 57 | "Organic Certified", 58 | "> 100 Variaties of Coffee", 59 | "Fair Trade Certified", 60 | "Free Shipping Over $50" 61 | ], 62 | "sitelinks": [ 63 | { 64 | "title": "Light Roast Coffee", 65 | "link": "http://www.coffeebeandirect.com/coffee/roast-level/light.html" 66 | }, 67 | { 68 | "title": "Blended Coffee", 69 | "link": "https://www.coffeebeandirect.com/roasted-coffee/signature-blends.html" 70 | }, 71 | { 72 | "title": "Dark Roast Coffee", 73 | "link": "https://www.coffeebeandirect.com/coffee/roast-level/dark.html" 74 | }, 75 | { 76 | "title": "Organic + Fair Trade", 77 | "link": "http://www.coffeebeandirect.com/coffee/type/organic-fair-trade.html" 78 | }, 79 | { 80 | "title": "Coffee Accessories", 81 | "link": "http://www.coffeebeandirect.com/coffee/accessories.html" 82 | } 83 | ] 84 | }, 85 | { 86 | "position": 3, 87 | "block_position": "bottom", 88 | "title": "Angry Sloth Coffee | Veteran Owned and Operated‎", 89 | "link": "https://angryslothcoffee.com/", 90 | "displayed_link": "www.angryslothcoffee.com/", 91 | "description": "Treat yourself to the coffee for the working class, operated by Paratroopers.", 92 | "sitelinks": [ 93 | { 94 | "title": "Address", 95 | "link": "https://angryslothcoffee.com/pages/shipping" 96 | }, 97 | { 98 | "title": "Contact Us", 99 | "link": "https://angryslothcoffee.com/pages/contact-us" 100 | } 101 | ] 102 | } 103 | ], 104 | "organic_results": [ 105 | { 106 | "position": 1, 107 | "title": "Coffee | HuffPost", 108 | "link": "https://www.huffingtonpost.com/topic/coffee", 109 | "displayed_link": "https://www.huffingtonpost.com/topic/coffee", 110 | "snippet": "There's no better way to ring in the New Year than with a festive brunch enjoyed with friends and family. From hair of the. BUSINESS. Long Island Iced Tea Corp's Shares Skyrocket After Renaming Itself 'Long Blockchain Corp' · The surge in the company's stock price lifted its market capitalization to $92 million from $23.8 ..." 111 | }, 112 | { 113 | "position": 2, 114 | "title": "Single Origin, Blends, Espresso Coffee Beans | Shop | Blue Bottle Coffee", 115 | "link": "https://bluebottlecoffee.com/store/coffee", 116 | "displayed_link": "https://bluebottlecoffee.com/store/coffee", 117 | "snippet": "Shop for single origin coffee, espresso coffee, decaf coffee, coffee blends in the Blue Bottle Store.", 118 | "related_link": "https://www.google.com/search?q=related:https://bluebottlecoffee.com/store/coffee+Coffee&tbo=1&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBAfCC8wAQ" 119 | }, 120 | { 121 | "position": 3, 122 | "title": "Heart Coffee Roasters - Portland", 123 | "link": "https://www.heartroasters.com/", 124 | "displayed_link": "https://www.heartroasters.com/", 125 | "snippet": "Heart is a café, roaster & bustling destination point located in Portland, Oregon.", 126 | "related_link": "https://www.google.com/search?q=related:https://www.heartroasters.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBAfCDUwAg" 127 | }, 128 | { 129 | "position": 4, 130 | "title": "Death Wish Coffee Company: World's Strongest Coffee | Best Coffee", 131 | "link": "https://www.deathwishcoffee.com/", 132 | "displayed_link": "https://www.deathwishcoffee.com/", 133 | "snippet": "Death Wish Coffee Company is the top online coffee-seller of fair-trade, organic, high-caffeine blends, and we have the world's strongest coffee!", 134 | "related_link": "https://www.google.com/search?q=related:https://www.deathwishcoffee.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBAfCDswAw" 135 | }, 136 | { 137 | "position": 5, 138 | "title": "#coffee hashtag on Twitter", 139 | "link": "https://twitter.com/hashtag/coffee?lang=en", 140 | "displayed_link": "https://twitter.com/hashtag/coffee?lang=en", 141 | "snippet": "24h ago @RealSaltLife tweeted: \"#Coffee & Vitamin #sea are good for ..\" - read what others are saying and join the conversation.", 142 | "related_link": "https://www.google.com/search?q=related:https://twitter.com/hashtag/coffee%3Flang%3Den+Coffee&tbo=1&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBAfCEEwBA" 143 | }, 144 | { 145 | "position": 6, 146 | "title": "Coffee may come with a cancer warning in California - CNN", 147 | "link": "https://www.cnn.com/2018/01/31/health/cancer-coffee-warning/index.html", 148 | "displayed_link": "https://www.cnn.com/2018/01/31/health/cancer-coffee-warning/index.html", 149 | "date": "4 days ago", 150 | "snippet": "California coffee shops may soon be forced to warn customers about a possible cancer risk linked to their morning jolt of java." 151 | }, 152 | { 153 | "position": 7, 154 | "title": "Barrington Coffee Roasting Company", 155 | "link": "https://barringtoncoffee.com/", 156 | "displayed_link": "https://barringtoncoffee.com/", 157 | "snippet": "Barrington Coffee Roasting Company roasts the finest hand-selected coffees in the world. Premium, extremely fresh, gourmet coffees shipped direct to you.", 158 | "related_link": "https://www.google.com/search?q=related:https://barringtoncoffee.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBAfCE0wBg" 159 | }, 160 | { 161 | "position": 8, 162 | "title": "Public Domain Specialty Coffee Shop - Portland, OR", 163 | "link": "http://www.publicdomaincoffee.com/", 164 | "displayed_link": "www.publicdomaincoffee.com/", 165 | "snippet": "Specialty coffee shop. Artisanal roast masters batch roast coffee weekly. Skilled baristas create customized coffee drinks.", 166 | "cached_link": "http://webcache.googleusercontent.com/search?q=cache:6VLIhl7rVCsJ:www.publicdomaincoffee.com/+&cd=28&hl=en&ct=clnk&gl=us", 167 | "related_link": "https://www.google.com/search?q=related:www.publicdomaincoffee.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBAfCFMwBw" 168 | }, 169 | { 170 | "position": 9, 171 | "title": "Home | The Coffee Bean & Tea Leaf", 172 | "link": "https://www.coffeebean.com/", 173 | "displayed_link": "https://www.coffeebean.com/", 174 | "snippet": "Download The Coffee Bean® Rewards app now for exclusive goodies, plus a free brewed coffee or hot tea to welcome you into the fold. Find us in the App Store Find us in the Play Store. Shop Coffee. Shop Now. Shop Tea. Shop Now. CBTL® Capsules. Perfectly sized for the perfect cup. Each single serve CBTL® capsule ...", 175 | "related_link": "https://www.google.com/search?q=related:https://www.coffeebean.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBAfCFkwCA" 176 | }, 177 | { 178 | "position": 10, 179 | "title": "Delicious Coffees for Home & Wholesale - Olympia Coffee Roasting ...", 180 | "link": "https://www.olympiacoffee.com/", 181 | "displayed_link": "https://www.olympiacoffee.com/", 182 | "snippet": "Olympia Coffee Roasting is a small, quality focused coffee roaster from the Pacific Northwest. We source, roast,and brew delicious coffees.", 183 | "related_link": "https://www.google.com/search?q=related:https://www.olympiacoffee.com/+Coffee&tbo=1&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBAfCF8wCQ" 184 | } 185 | ], 186 | "related_searches": [ 187 | { 188 | "query": "top 5 best coffee roasters in portland", 189 | "link": "https://www.google.com/search?q=top+5+best+coffee+roasters+in+portland&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBDVAgiGASgA" 190 | }, 191 | { 192 | "query": "coffee history", 193 | "link": "https://www.google.com/search?q=coffee+history&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBDVAgiHASgB" 194 | }, 195 | { 196 | "query": "coava coffee roasters portland, or", 197 | "link": "https://www.google.com/search?q=coava+coffee+roasters+portland,+or&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBDVAgiIASgC" 198 | }, 199 | { 200 | "query": "coffee meaning", 201 | "link": "https://www.google.com/search?q=coffee+meaning&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBDVAgiJASgD" 202 | }, 203 | { 204 | "query": "coffee effects", 205 | "link": "https://www.google.com/search?q=coffee+effects&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBDVAgiKASgE" 206 | }, 207 | { 208 | "query": "coffee health", 209 | "link": "https://www.google.com/search?q=coffee+health&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBDVAgiLASgF" 210 | }, 211 | { 212 | "query": "coffee plant", 213 | "link": "https://www.google.com/search?q=coffee+plant&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBDVAgiMASgG" 214 | }, 215 | { 216 | "query": "coffee love", 217 | "link": "https://www.google.com/search?q=coffee+love&sa=X&ved=0ahUKEwjUj6mCoo_ZAhWqgFQKHSPtA4k4FBDVAgiNASgH" 218 | } 219 | ], 220 | "pagination": { 221 | "current": 3, 222 | "next": "https://www.google.com/search?q=Coffee&ei=zo94WtTwBKqB0gKj2o_ICA&start=30&sa=N", 223 | "other_pages": { 224 | "1": "https://www.google.com/search?q=Coffee&ei=zo94WtTwBKqB0gKj2o_ICA&start=0&sa=N", 225 | "2": "https://www.google.com/search?q=Coffee&ei=zo94WtTwBKqB0gKj2o_ICA&start=10&sa=N", 226 | "4": "https://www.google.com/search?q=Coffee&ei=zo94WtTwBKqB0gKj2o_ICA&start=30&sa=N", 227 | "5": "https://www.google.com/search?q=Coffee&ei=zo94WtTwBKqB0gKj2o_ICA&start=40&sa=N", 228 | "6": "https://www.google.com/search?q=Coffee&ei=zo94WtTwBKqB0gKj2o_ICA&start=50&sa=N", 229 | "7": "https://www.google.com/search?q=Coffee&ei=zo94WtTwBKqB0gKj2o_ICA&start=60&sa=N", 230 | "8": "https://www.google.com/search?q=Coffee&ei=zo94WtTwBKqB0gKj2o_ICA&start=70&sa=N", 231 | "9": "https://www.google.com/search?q=Coffee&ei=zo94WtTwBKqB0gKj2o_ICA&start=80&sa=N", 232 | "10": "https://www.google.com/search?q=Coffee&ei=zo94WtTwBKqB0gKj2o_ICA&start=90&sa=N" 233 | } 234 | } 235 | } -------------------------------------------------------------------------------- /demo/demo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | serpapi "github.com/serpapi/google-search-results-golang" 8 | ) 9 | 10 | /*** 11 | * Demonstrate how to search on Google 12 | * 13 | * go get -u github.com/serpapi/google_search_results_golang 14 | */ 15 | func main() { 16 | parameter := map[string]string{ 17 | "q": "Coffee", 18 | "location": "Austin,Texas", 19 | } 20 | 21 | search := serpapi.NewGoogleSearch(parameter, os.Getenv("API_KEY")) 22 | data, err := search.GetJSON() 23 | if err != nil { 24 | panic(err) 25 | } 26 | // decode data and display 27 | results := data["organic_results"].([]interface{}) 28 | firstResult := results[0].(map[string]interface{}) 29 | fmt.Println(firstResult["title"].(string)) 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/serpapi/google-search-results-golang 2 | 3 | go 1.17 4 | -------------------------------------------------------------------------------- /model.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import "net/http" 4 | 5 | // Search holds query 6 | type Search struct { 7 | Engine string 8 | Parameter map[string]string 9 | ApiKey string 10 | HttpSearch *http.Client 11 | } 12 | 13 | // SearchResult holds response 14 | type SearchResult map[string]interface{} 15 | 16 | // SearchResultArray hold response array 17 | type SearchResultArray []interface{} 18 | 19 | const ( 20 | // Current version 21 | VERSION = "3.2.0" 22 | ) 23 | -------------------------------------------------------------------------------- /search.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | /* 4 | * This package enables to interact with SerpApi search API 5 | */ 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "time" 11 | ) 12 | 13 | // NewSearch create generic search which support any search engine 14 | func NewSearch(engine string, parameter map[string]string, apiKey string) Search { 15 | // Create the http search 16 | httpSearch := &http.Client{ 17 | Timeout: time.Second * 60, 18 | } 19 | return Search{Engine: engine, Parameter: parameter, ApiKey: apiKey, HttpSearch: httpSearch} 20 | } 21 | 22 | // NewGoogleSearch creates search for google 23 | func NewGoogleSearch(parameter map[string]string, apiKey string) Search { 24 | return NewSearch("google", parameter, apiKey) 25 | } 26 | 27 | // NewBingSearch creates search for bing 28 | func NewBingSearch(parameter map[string]string, apiKey string) Search { 29 | return NewSearch("bing", parameter, apiKey) 30 | } 31 | 32 | // NewBaiduSearch creates search for baidu 33 | func NewBaiduSearch(parameter map[string]string, apiKey string) Search { 34 | return NewSearch("baidu", parameter, apiKey) 35 | } 36 | 37 | // NewYahooSearch creates search for yahoo 38 | func NewYahooSearch(parameter map[string]string, apiKey string) Search { 39 | return NewSearch("yahoo", parameter, apiKey) 40 | } 41 | 42 | // NewGoogleMapsSearch creates search for google_maps 43 | func NewGoogleMapsSearch(parameter map[string]string, apiKey string) Search { 44 | return NewSearch("google_maps", parameter, apiKey) 45 | } 46 | 47 | // NewGoogleProductSearch creates search for google_product 48 | func NewGoogleProductSearch(parameter map[string]string, apiKey string) Search { 49 | return NewSearch("google_product", parameter, apiKey) 50 | } 51 | 52 | // NewGoogleScholarSearch creates search for google_product 53 | func NewGoogleScholarSearch(parameter map[string]string, apiKey string) Search { 54 | return NewSearch("google_scholar", parameter, apiKey) 55 | } 56 | 57 | // NewYandexSearch creates search for yandex 58 | func NewYandexSearch(parameter map[string]string, apiKey string) Search { 59 | return NewSearch("yandex", parameter, apiKey) 60 | } 61 | 62 | // NewEbaySearch creates search for ebay 63 | func NewEbaySearch(parameter map[string]string, apiKey string) Search { 64 | return NewSearch("ebay", parameter, apiKey) 65 | } 66 | 67 | // NewYoutubeSearch creates search for ebay 68 | func NewYoutubeSearch(parameter map[string]string, apiKey string) Search { 69 | return NewSearch("youtube", parameter, apiKey) 70 | } 71 | 72 | // NewWalmartSearch creates search for ebay 73 | func NewWalmartSearch(parameter map[string]string, apiKey string) Search { 74 | return NewSearch("walmart", parameter, apiKey) 75 | } 76 | 77 | // NewHomeDepotSearch creates search for ebay 78 | func NewHomeDepotSearch(parameter map[string]string, apiKey string) Search { 79 | return NewSearch("home_depot", parameter, apiKey) 80 | } 81 | 82 | // NewNaverSearch creates search for Naver search engine 83 | func NewNaverSearch(parameter map[string]string, apiKey string) Search { 84 | return NewSearch("naver", parameter, apiKey) 85 | } 86 | 87 | // NewAppleStoreSearch creates search for Apple store (itunes.apple.com) 88 | func NewAppleStoreSearch(parameter map[string]string, apiKey string) Search { 89 | return NewSearch("apple_app_store", parameter, apiKey) 90 | } 91 | 92 | // SetApiKey globaly set api_key 93 | func (search *Search) SetApiKey(key string) { 94 | search.ApiKey = key 95 | } 96 | 97 | // GetJSON returns SearchResult containing 98 | func (search *Search) GetJSON() (SearchResult, error) { 99 | rsp, err := search.execute("/search", "json") 100 | if err != nil { 101 | return nil, err 102 | } 103 | return search.decodeJSON(rsp.Body) 104 | } 105 | 106 | // GetHTML returns html as a string 107 | func (search *Search) GetHTML() (*string, error) { 108 | rsp, err := search.execute("/search", "html") 109 | if err != nil { 110 | return nil, err 111 | } 112 | return search.decodeHTML(rsp.Body) 113 | } 114 | 115 | // GetLocation returns the standardize location takes location and limit as input. 116 | func (search *Search) GetLocation(location string, limit int) (SearchResultArray, error) { 117 | search.Parameter = map[string]string{ 118 | "q": location, 119 | "limit": fmt.Sprint(limit), 120 | } 121 | rsp, err := search.execute("/locations.json", "json") 122 | if err != nil { 123 | return nil, err 124 | } 125 | return search.decodeJSONArray(rsp.Body) 126 | } 127 | 128 | // GetAccount return account information 129 | func (search *Search) GetAccount() (SearchResult, error) { 130 | search.Parameter = map[string]string{} 131 | rsp, err := search.execute("/account", "json") 132 | if err != nil { 133 | return nil, err 134 | } 135 | return search.decodeJSON(rsp.Body) 136 | } 137 | 138 | // GetSearchArchive retrieve search from the archive using the Search Archive API 139 | func (search *Search) GetSearchArchive(searchID string) (SearchResult, error) { 140 | rsp, err := search.execute("/searches/"+searchID+".json", "json") 141 | if err != nil { 142 | return nil, err 143 | } 144 | return search.decodeJSON(rsp.Body) 145 | } 146 | -------------------------------------------------------------------------------- /search_test.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "os" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | var apiKey string 11 | 12 | func TestMain(m *testing.M) { 13 | apiKey = os.Getenv("API_KEY") 14 | if len(apiKey) == 0 { 15 | panic("API_KEY must be defined!") 16 | } 17 | // call flag.Parse() here if TestMain uses flags 18 | os.Exit(m.Run()) 19 | } 20 | 21 | func shoulSkip() bool { 22 | return len(apiKey) == 0 23 | } 24 | 25 | func TestGoogleQuickStart(t *testing.T) { 26 | if shoulSkip() { 27 | t.Skip("API_KEY required") 28 | return 29 | } 30 | 31 | parameter := map[string]string{ 32 | "q": "Coffee", 33 | "location": "Portland, Oregon, United States", 34 | "hl": "en", 35 | "gl": "us", 36 | "google_domain": "google.com", 37 | "safe": "active", 38 | "start": "10", 39 | "num": "10", 40 | "device": "desktop", 41 | } 42 | 43 | search := NewGoogleSearch(parameter, apiKey) 44 | rsp, err := search.GetJSON() 45 | 46 | if err != nil { 47 | t.Error(err) 48 | return 49 | } 50 | result := rsp["organic_results"].([]interface{})[0].(map[string]interface{}) 51 | if len(result["title"].(string)) == 0 { 52 | t.Error("empty title in local results") 53 | return 54 | } 55 | } 56 | 57 | // basic use case 58 | func TestGoogleJSON(t *testing.T) { 59 | if shoulSkip() { 60 | t.Skip("API_KEY required") 61 | return 62 | } 63 | 64 | parameter := map[string]string{ 65 | "q": "Coffee", 66 | "location": "Portland"} 67 | 68 | search := NewGoogleSearch(parameter, apiKey) 69 | rsp, err := search.GetJSON() 70 | 71 | if err != nil { 72 | t.Error("unexpected error", err) 73 | return 74 | } 75 | 76 | if rsp["search_metadata"].(map[string]interface{})["status"] != "Success" { 77 | t.Error("bad status") 78 | return 79 | } 80 | 81 | if len(rsp["organic_results"].([]interface{})) < 5 { 82 | t.Error("less than 5 organic result") 83 | return 84 | } 85 | } 86 | 87 | func TestBaiduJSON(t *testing.T) { 88 | if shoulSkip() { 89 | t.Skip("API_KEY required") 90 | return 91 | } 92 | 93 | parameter := map[string]string{ 94 | "q": "Coffee", 95 | } 96 | 97 | search := NewBaiduSearch(parameter, apiKey) 98 | rsp, err := search.GetJSON() 99 | 100 | if err != nil { 101 | t.Error("unexpected error", err) 102 | return 103 | } 104 | 105 | if rsp["search_metadata"].(map[string]interface{})["status"] != "Success" { 106 | t.Error("bad status") 107 | return 108 | } 109 | 110 | if len(rsp["organic_results"].([]interface{})) < 5 { 111 | t.Error("less than 5 organic result") 112 | return 113 | } 114 | } 115 | 116 | func TesNewtSearch(t *testing.T) { 117 | if shoulSkip() { 118 | t.Skip("API_KEY required") 119 | return 120 | } 121 | 122 | parameter := map[string]string{ 123 | "q": "Coffee", 124 | } 125 | 126 | search := NewSearch("baidu", parameter, apiKey) 127 | rsp, err := search.GetJSON() 128 | 129 | if err != nil { 130 | t.Error("unexpected error", err) 131 | return 132 | } 133 | 134 | if rsp["search_metadata"].(map[string]interface{})["status"] != "Success" { 135 | t.Error("bad status") 136 | return 137 | } 138 | 139 | if len(rsp["organic_results"].([]interface{})) < 5 { 140 | t.Error("less than 5 organic result") 141 | return 142 | } 143 | } 144 | 145 | func TestBingJSON(t *testing.T) { 146 | if shoulSkip() { 147 | t.Skip("API_KEY required") 148 | return 149 | } 150 | 151 | parameter := map[string]string{ 152 | "q": "Coffee", 153 | "location": "Portland"} 154 | 155 | search := NewBingSearch(parameter, apiKey) 156 | rsp, err := search.GetJSON() 157 | 158 | if err != nil { 159 | t.Error("unexpected error", err) 160 | return 161 | } 162 | 163 | if rsp["search_metadata"].(map[string]interface{})["status"] != "Success" { 164 | t.Error("bad status") 165 | return 166 | } 167 | 168 | // bing results are mostly advertising. not so great! 169 | if len(rsp["organic_results"].([]interface{})) < 3 { 170 | t.Error("less than 5 organic result") 171 | t.Error("response:") 172 | t.Error(rsp["search_metadata"]) 173 | return 174 | } 175 | } 176 | 177 | func TestYandexJSON(t *testing.T) { 178 | if shoulSkip() { 179 | t.Skip("API_KEY required") 180 | return 181 | } 182 | 183 | parameter := map[string]string{ 184 | "text": "Coffee", 185 | } 186 | 187 | search := NewYandexSearch(parameter, apiKey) 188 | rsp, err := search.GetJSON() 189 | 190 | if err != nil { 191 | t.Error("unexpected error", err) 192 | return 193 | } 194 | 195 | if rsp["search_metadata"].(map[string]interface{})["status"] != "Success" { 196 | t.Error("bad status") 197 | return 198 | } 199 | 200 | if len(rsp["organic_results"].([]interface{})) < 5 { 201 | t.Error("less than 5 organic result") 202 | return 203 | } 204 | } 205 | 206 | func TestYahooJSON(t *testing.T) { 207 | if shoulSkip() { 208 | t.Skip("API_KEY required") 209 | return 210 | } 211 | 212 | parameter := map[string]string{ 213 | "p": "Coffee", 214 | } 215 | 216 | search := NewYahooSearch(parameter, apiKey) 217 | rsp, err := search.GetJSON() 218 | 219 | if err != nil { 220 | t.Error("unexpected error", err) 221 | return 222 | } 223 | 224 | if rsp["search_metadata"].(map[string]interface{})["status"] != "Success" { 225 | t.Error("bad status") 226 | return 227 | } 228 | 229 | if len(rsp["organic_results"].([]interface{})) < 5 { 230 | t.Error("less than 5 organic result") 231 | return 232 | } 233 | } 234 | 235 | func TestHomeDepotJSON(t *testing.T) { 236 | if shoulSkip() { 237 | t.Skip("API_KEY required") 238 | return 239 | } 240 | 241 | parameter := map[string]string{ 242 | "q": "Coffee", 243 | } 244 | 245 | search := NewHomeDepotSearch(parameter, apiKey) 246 | rsp, err := search.GetJSON() 247 | 248 | if err != nil { 249 | t.Error("unexpected error", err) 250 | return 251 | } 252 | 253 | if rsp["search_metadata"].(map[string]interface{})["status"] != "Success" { 254 | t.Error("bad status") 255 | return 256 | } 257 | 258 | if len(rsp["products"].([]interface{})) < 5 { 259 | t.Error("less than 5 organic result") 260 | return 261 | } 262 | } 263 | 264 | func TestEbayJSON(t *testing.T) { 265 | if shoulSkip() { 266 | t.Skip("API_KEY required") 267 | return 268 | } 269 | 270 | parameter := map[string]string{ 271 | "_nkw": "Coffee", 272 | } 273 | 274 | search := NewEbaySearch(parameter, apiKey) 275 | rsp, err := search.GetJSON() 276 | 277 | if err != nil { 278 | t.Error("unexpected error", err) 279 | return 280 | } 281 | 282 | if rsp["search_metadata"].(map[string]interface{})["status"] != "Success" { 283 | t.Error("bad status") 284 | return 285 | } 286 | 287 | if len(rsp["organic_results"].([]interface{})) < 5 { 288 | t.Error("less than 5 organic result") 289 | return 290 | } 291 | } 292 | 293 | func TestAppleStoreSearch(t *testing.T) { 294 | if shoulSkip() { 295 | t.Skip("API_KEY required") 296 | return 297 | } 298 | 299 | parameter := map[string]string{ 300 | "term": "Coffee", 301 | } 302 | 303 | search := NewAppleStoreSearch(parameter, apiKey) 304 | rsp, err := search.GetJSON() 305 | 306 | if err != nil { 307 | t.Error("unexpected error", err) 308 | return 309 | } 310 | 311 | if rsp["search_metadata"].(map[string]interface{})["status"] != "Success" { 312 | t.Error("bad status") 313 | return 314 | } 315 | 316 | if len(rsp["organic_results"].([]interface{})) < 5 { 317 | t.Error("less than 5 organic result") 318 | return 319 | } 320 | } 321 | 322 | func TestNaverSearch(t *testing.T) { 323 | if shoulSkip() { 324 | t.Skip("API_KEY required") 325 | return 326 | } 327 | 328 | parameter := map[string]string{ 329 | "query": "Coffee", 330 | } 331 | 332 | search := NewNaverSearch(parameter, apiKey) 333 | rsp, err := search.GetJSON() 334 | 335 | if err != nil { 336 | t.Error("unexpected error", err) 337 | return 338 | } 339 | 340 | if rsp["search_metadata"].(map[string]interface{})["status"] != "Success" { 341 | t.Error("bad status") 342 | return 343 | } 344 | 345 | if len(rsp["ads_results"].([]interface{})) < 5 { 346 | t.Error("less than 5 ads") 347 | return 348 | } 349 | } 350 | 351 | func TestGoogleJSONwithGlobalKey(t *testing.T) { 352 | if shoulSkip() { 353 | t.Skip("API_KEY required") 354 | return 355 | } 356 | 357 | parameter := map[string]string{ 358 | "q": "Coffee", 359 | "location": "Portland"} 360 | 361 | search := NewGoogleSearch(parameter, apiKey) 362 | rsp, err := search.GetJSON() 363 | if err != nil { 364 | t.Error("unexpected error", err) 365 | return 366 | } 367 | result := rsp["organic_results"].([]interface{})[0].(map[string]interface{}) 368 | if len(result["title"].(string)) == 0 { 369 | t.Error("empty title in local results") 370 | return 371 | } 372 | } 373 | 374 | func TestGoogleGetHTML(t *testing.T) { 375 | if shoulSkip() { 376 | t.Skip("API_KEY required") 377 | return 378 | } 379 | 380 | parameter := map[string]string{ 381 | "q": "Coffee", 382 | "location": "Portland"} 383 | 384 | search := NewGoogleSearch(parameter, apiKey) 385 | data, err := search.GetHTML() 386 | if err != nil { 387 | t.Error("err must be nil") 388 | return 389 | } 390 | if !strings.Contains(*data, "") { 391 | t.Error("data does not contains tag") 392 | } 393 | } 394 | 395 | func TestGoogleDecodeJson(t *testing.T) { 396 | reader, err := os.Open("./data/search_coffee_sample.json") 397 | if err != nil { 398 | panic(err) 399 | } 400 | var search Search 401 | rsp, err := search.decodeJSON(reader) 402 | if err != nil { 403 | t.Error("error should be nil", err) 404 | return 405 | } 406 | 407 | results := rsp["organic_results"].([]interface{}) 408 | ref := results[0].(map[string]interface{}) 409 | if ref["title"] != "Portland Roasting Coffee" { 410 | t.Error("empty title in local results") 411 | return 412 | } 413 | } 414 | 415 | func TestGoogleDecodeJsonPage20(t *testing.T) { 416 | t.Log("run test") 417 | reader, err := os.Open("./data/search_coffee_sample_page20.json") 418 | if err != nil { 419 | panic(err) 420 | } 421 | var search Search 422 | rsp, err := search.decodeJSON(reader) 423 | if err != nil { 424 | t.Error("error should be nil") 425 | t.Error(err) 426 | } 427 | t.Log(reflect.ValueOf(rsp).MapKeys()) 428 | results := rsp["organic_results"].([]interface{}) 429 | ref := results[0].(map[string]interface{}) 430 | t.Log(ref["title"].(string)) 431 | if ref["title"].(string) != "Coffee | HuffPost" { 432 | t.Error("fail decoding the title ") 433 | } 434 | } 435 | 436 | func TestGoogleDecodeJsonError(t *testing.T) { 437 | reader, err := os.Open("./data/error_sample.json") 438 | if err != nil { 439 | panic(err) 440 | } 441 | var search Search 442 | rsp, err := search.decodeJSON(reader) 443 | if rsp != nil { 444 | t.Error("response should not be nil") 445 | return 446 | } 447 | 448 | if err == nil { 449 | t.Error("unexcepted err is nil") 450 | } else if strings.Compare(err.Error(), "Your account credit is too low, plesae add more credits.") == 0 { 451 | t.Error("empty title in local results") 452 | return 453 | } 454 | } 455 | 456 | func TestGoogleGetLocation(t *testing.T) { 457 | 458 | var rsp SearchResultArray 459 | var err error 460 | search := NewSearch("google", map[string]string{}, apiKey) 461 | rsp, err = search.GetLocation("Austin", 3) 462 | 463 | if err != nil { 464 | t.Error(err) 465 | } 466 | 467 | //log.Println(rsp[0]) 468 | first := rsp[0].(map[string]interface{}) 469 | googleID := first["google_id"].(float64) 470 | if googleID != float64(200635) { 471 | t.Error(googleID) 472 | return 473 | } 474 | } 475 | 476 | func TestGoogleGetAccount(t *testing.T) { 477 | // Skip this test 478 | if len(apiKey) == 0 { 479 | t.Skip("API_KEY required") 480 | return 481 | } 482 | 483 | var rsp SearchResult 484 | var err error 485 | search := NewSearch("google", map[string]string{}, apiKey) 486 | rsp, err = search.GetAccount() 487 | 488 | if err != nil { 489 | t.Error("fail to fetch data") 490 | t.Error(err) 491 | return 492 | } 493 | 494 | if rsp["account_id"] == nil { 495 | t.Error("no account_id found") 496 | return 497 | } 498 | } 499 | 500 | // Search archive API 501 | func TestGoogleSearchArchive(t *testing.T) { 502 | if len(apiKey) == 0 { 503 | t.Skip("API_KEY required") 504 | return 505 | } 506 | 507 | parameter := map[string]string{ 508 | "q": "Coffee", 509 | "location": "Portland"} 510 | 511 | search := NewGoogleSearch(parameter, apiKey) 512 | rsp, err := search.GetJSON() 513 | 514 | if err != nil { 515 | t.Error("unexpected error", err) 516 | return 517 | } 518 | 519 | searchID := rsp["search_metadata"].(map[string]interface{})["id"].(string) 520 | if len(searchID) == 0 { 521 | t.Error("search_metadata.id must be defined") 522 | return 523 | } 524 | 525 | searchArchive, err := search.GetSearchArchive(searchID) 526 | if err != nil { 527 | t.Error(err) 528 | return 529 | } 530 | 531 | searchIDArchive := searchArchive["search_metadata"].(map[string]interface{})["id"].(string) 532 | if searchIDArchive != searchID { 533 | t.Error("search_metadata.id do not match", searchIDArchive, searchID) 534 | } 535 | } 536 | --------------------------------------------------------------------------------