Skip to content

Commit

Permalink
Merge pull request #7 from ozgurg/dev
Browse files Browse the repository at this point in the history
release: v1.2.0
  • Loading branch information
ozgurg authored Sep 4, 2022
2 parents 66d9d5f + 395666f commit ef9abea
Show file tree
Hide file tree
Showing 10 changed files with 673 additions and 764 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

# Google Currency Scraper

`google-currency-scraper` goes Google '1 USD to TRY' (for example) search result and scrape exchange rate and last updated date for you by using [Puppeteer](https://github.com/puppeteer/puppeteer) under the hood.
Scrape extremely up-to-date exchange rates from Google fast and free.

## Install

Expand Down
1,208 changes: 512 additions & 696 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "google-currency-scraper",
"version": "1.1.1",
"description": "'google-currency-scraper' goes Google '1 USD to TRY' (for example) search result and scrape exchange rate and last updated date for you by using Puppeteer under the hood.",
"version": "1.2.0",
"description": "Scrape extremely up-to-date exchange rates from Google fast and free.",
"license": "MIT",
"type": "module",
"exports": "./index.js",
Expand All @@ -23,20 +23,20 @@
"url": "https://github.com/ozgurg"
},
"keywords": [
"puppeteer",
"scraper",
"currency",
"google scraper",
"currency api"
],
"dependencies": {
"cheerio": "^1.0.0-rc.12",
"dayjs": "^1.11.5",
"puppeteer": "^16.2.0"
"node-fetch": "^3.2.10"
},
"devDependencies": {
"@jest/globals": "^28.1.3",
"@types/jest": "^28.1.6",
"eslint": "^8.21.0",
"@types/jest": "^28.1.8",
"eslint": "^8.23.0",
"eslint-config-google": "^0.14.0",
"eslint-plugin-jsdoc": "^39.3.6",
"husky": "^8.0.1",
Expand Down
98 changes: 98 additions & 0 deletions src/google-currency-scraper-puppeteer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// This file should not be used and will be removed in the next major release
import {
closeBrowser,
emulateDevice,
ensurePageLoadOnlyDocument,
launchBrowser,
openNewPage
} from "./utils/browser.js";
import { CurrencyCode, isValidCurrencyCode } from "./utils/currency-code.js";
import { objectToQueryString } from "./utils/object-to-query-string.js";
import { getDate, parseAndNormalizeDateInSearchResult } from "./utils/date.js";

/**
* @param {object} params
* @param {CurrencyCode | string} params.from
* @param {CurrencyCode | string} params.to
* @returns {Promise<{from: CurrencyCode | string, to: CurrencyCode | string, rate: number, dateUpdated: string}>}
* @deprecated
*/
const googleCurrencyScraper = async ({ from, to }) => {
if (!isValidCurrencyCode(from)) {
throw new Error(`Invalid 'from' currency code: ${from}`);
}

if (!isValidCurrencyCode(to)) {
throw new Error(`Invalid 'to' currency code: ${to}`);
}

if (from === to) {
return {
from,
to,
rate: 1,
dateUpdated: getDate()
};
}

const browser = await launchBrowser();

const page = await openNewPage(browser);

// To lighter page and faster load times, I emulate a mobile device.
// So why iPhone 7?
// It doesn't matter which one as long as it's a mobile device.
// That's why I chose iPhone 7 as I use it in real life :)
await emulateDevice(page, "iPhone 7");

// To lighter page and faster load times, make sure load only document.
await ensurePageLoadOnlyDocument(page);

await goToGoogleCurrencySearchResult(page, { from, to });

const result = await parseResult(page);

await closeBrowser(browser);

return {
from,
to,
rate: result.rate,
dateUpdated: parseAndNormalizeDateInSearchResult(result.dateUpdated)
};
};

/**
* @param {*} page
* @param {object} params
* @param {CurrencyCode | string} params.from
* @param {CurrencyCode | string} params.to
* @returns {Promise<* | null>}
* @deprecated
*/
async function goToGoogleCurrencySearchResult(page, { from, to }) {
const qs = objectToQueryString({
q: `1+${from}+to+${to}`,
hl: "en" // Make sure to use the English language to avoid any weirdness
});
return await page.goto(`https://www.google.com/search?${qs}`, {
waitUntil: "networkidle2"
});
}

/**
* @param {*} page
* @returns {Promise<{rate: number, dateUpdated: string}>}
* @deprecated
*/
async function parseResult(page) {
return page.$eval(
"[data-exchange-rate]",
element => ({
rate: parseFloat(element.getAttribute("data-exchange-rate")),
dateUpdated: element.nextSibling.querySelector("span").textContent
})
);
}

export default googleCurrencyScraper;
65 changes: 22 additions & 43 deletions src/google-currency-scraper.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// TODO: Make *'s in JSDoc better where possible
import { closeBrowser, emulateDevice, ensurePageLoadOnlyDocument, launchBrowser, openNewPage } from "./utils/browser.js";
import { CurrencyCode, isValidCurrencyCode } from "./utils/currency-code.js";
import { objectToQueryString } from "./utils/object-to-query-string.js";
import { getDate, parseAndNormalizeDateInSearchResult } from "./utils/date.js";
import { makeGetRequest } from "./utils/http-client.js";
import { load } from "cheerio";

/**
* @param {object} params
Expand All @@ -28,62 +28,41 @@ const googleCurrencyScraper = async ({ from, to }) => {
};
}

const browser = await launchBrowser();

const page = await openNewPage(browser);

// To lighter page and faster load times, I emulate a mobile device.
// So why iPhone 7?
// It doesn't matter which one as long as it's a mobile device.
// That's why I chose iPhone 7 as I use it in real life :)
await emulateDevice(page, "iPhone 7");

// To lighter page and faster load times, make sure load only document.
await ensurePageLoadOnlyDocument(page);

await goToGoogleCurrencySearchResult(page, { from, to });
const url = createGoogleCurrencySearchResultUrl(from, to);
const config = {
headers: {
"Accept-Language": "en-US",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
}
};
const response = await makeGetRequest(url, config);
const $ = load(response);

const result = await parseResult(page);
const exchangeRateNode = $("[data-exchange-rate]:first-child");
const dateUpdatedNode = exchangeRateNode.next().find("span:not([class]):first-child");

await closeBrowser(browser);
const exchangeRate = parseFloat(exchangeRateNode.attr("data-exchange-rate"));
const dateUpdated = dateUpdatedNode.text();

return {
from,
to,
rate: result.rate,
dateUpdated: parseAndNormalizeDateInSearchResult(result.dateUpdated)
rate: exchangeRate,
dateUpdated: parseAndNormalizeDateInSearchResult(dateUpdated)
};
};

/**
* @param {*} page
* @param {object} params
* @param {CurrencyCode | string} params.from
* @param {CurrencyCode | string} params.to
* @returns {Promise<* | null>}
* @param {CurrencyCode | string} from
* @param {CurrencyCode | string} to
* @returns {string}
*/
async function goToGoogleCurrencySearchResult(page, { from, to }) {
function createGoogleCurrencySearchResultUrl(from, to) {
const qs = objectToQueryString({
q: `1+${from}+to+${to}`,
hl: "en" // Make sure to use the English language to avoid any weirdness
});
return await page.goto(`https://www.google.com/search?${qs}`, {
waitUntil: "networkidle2"
});
}

/**
* @param {*} page
* @returns {Promise<{rate: number, dateUpdated: string}>}
*/
async function parseResult(page) {
return page.$eval(
"[data-exchange-rate]",
element => ({
rate: parseFloat(element.getAttribute("data-exchange-rate")),
dateUpdated: element.nextSibling.querySelector("span").textContent
})
);
return `https://www.google.com/search?${qs}`;
}

export default googleCurrencyScraper;
2 changes: 0 additions & 2 deletions src/google-currency-scraper.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import googleCurrencyScraper from "./google-currency-scraper.js";
import { jest } from "@jest/globals";
import { getDate } from "./utils/date.js";

jest.setTimeout(20_000);

describe("google-currency-scraper", () => {
const date = Date;

Expand Down
7 changes: 6 additions & 1 deletion src/utils/browser.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// TODO: Make *'s in JSDoc better where possible
// This file should not be used and will be removed in the next major release
import puppeteer from "puppeteer";

/**
* `args` from https://www.bannerbear.com/blog/ways-to-speed-up-puppeteer-screenshots/.
*
* @returns {Promise<*>}
* @deprecated
*/
const launchBrowser = () => {
const args = [
Expand Down Expand Up @@ -54,24 +55,28 @@ const launchBrowser = () => {
/**
* @param {*} browser
* @returns {Promise<*>}
* @deprecated
*/
const openNewPage = browser => browser.newPage();

/**
* @param {*} browser
* @returns {Promise<void>}
* @deprecated
*/
const closeBrowser = browser => browser.close();

/**
* @param {*} page
* @param {string} device
* @returns {Promise<void>}
* @deprecated
*/
const emulateDevice = (page, device) => page.emulate(puppeteer.devices[device]);

/**
* @param {*} page
* @deprecated
*/
const ensurePageLoadOnlyDocument = async page => {
await page.setRequestInterception(true);
Expand Down
9 changes: 8 additions & 1 deletion src/utils/currency-code.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ describe("utils/currency-code", () => {
expect(key).toBe(value);
}
});

it("should have uppercase key value pair", () => {
for (const [key, value] of Object.entries(CurrencyCode)) {
expect(key).toBe(key.toUpperCase());
expect(value).toBe(value.toUpperCase());
}
});
});

describe("isValidCurrencyCode", () => {
Expand All @@ -19,7 +26,7 @@ describe("utils/currency-code", () => {
});

it("should return 'false' for invalid currency code", () => {
expect(isValidCurrencyCode("ABC")).toBe(false);
expect(isValidCurrencyCode("not-real")).toBe(false);
});
});
});
20 changes: 20 additions & 0 deletions src/utils/http-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// TODO: Replace "node-fetch" with "fetch" when its commonly available
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
import fetch from "node-fetch";

/**
* @param {string} url
* @param {object} config
* @returns {Promise<string>}
*/
const makeGetRequest = async (url, config) => {
const response = await fetch(url, {
method: "GET",
...config
});
return response.text();
};

export {
makeGetRequest
};
14 changes: 0 additions & 14 deletions tsconfig.json

This file was deleted.

0 comments on commit ef9abea

Please sign in to comment.