I'm trying to pull current price change % from Google Finance with Google Apps Script. With the following coding, I couldn't figure out why it doesn't pull current price change % (0.72%), though it retrieves "After Hours" price change % (0.081%). Can anyone help me out? Thank you!!
function test() {
var url = 'https://www.google.com/finance/quote/AAPL:NASDAQ';
var res = UrlFetchApp.fetch(url, { muteHttpExceptions: true }).getContentText();
var location = '', sub = '', index = [], price = '', change = [];
// From google finance, scrape whole plain string from first <div class="YMlKec fxKbKc"> tag where current price is.
location = '<div class="YMlKec fxKbKc">';
index = res.indexOf(location);
sub = res.substring(index + location.length, index + location.length + 1000);
// Logger.log(sub);
// Pull current price from the sub variable. This is succesful.
price = sub.substring(0, sub.indexOf('<'));
Logger.log(price);
// Pull current price change, which should be 0.72%. But it didn't work.
// This retrieved only "After Hours" price change %, 0.081%, but not 0.72%, current price change % that I'm looking for.
location = '%';
index = [sub.indexOf(location)];
for (var i = 0; i < index.length; ++i) {
change = sub.substring(index[i] - 5, index[i] + 1);
Logger.log(change)
}
}
Is it possible to use Google Apps Script to get an info from the code I see at DevTools?
First thing that you should be aware is that the code shown in the Elements tab of Chrome Developers Tools is the current DOM but UrlFetchApp.fetch is only able to get the code from the source file referred directly by the URL.
To review the source code you might right click on the webpage and select View page source or go open the file from the Chrome Developers Tool's Sources tab. If the value that you want to read is not there, the best is to use a stock market API, otherwise you have to implement a headless browser.
There have being a several questions about getting data from Google Finance. Most of the question that involves Google Apps Script are related to using the GOOGLEFINANCE built-in function in Google Sheets, where most of the OP didn't reviewed the official help article about GOOGLEFINANCE. From this doc:
Historical data cannot be downloaded or accessed via the Sheets API or Apps Script. If you attempt to do so, you'll see a #N/A error in place of the values in the corresponding cells of your spreadsheet.
The relevance of the above is that Google is taking some measures to prevent that certain data be retrieved automatically.
In order to make Google Apps Script able to "see" what is shown in the web browser using only server-side code you should have to implement a headless browser:
A headless browser is a web browser without a graphical user interface.
Headless browsers provide automated control of a web page in an environment similar to popular web browsers, but they are executed via a command-line interface or using network communication. They are particularly useful for testing web pages as they are able to render and understand HTML the same way a browser would, including styling elements such as page layout, colour, font selection and execution of JavaScript and Ajax which are usually not available when using other testing methods.
Another alternative might be to u use client-side code as the HTML Service could be used to create a dialog/sidebar/web-application to serve it including google.script API support. The later could be used to send to the server-side code some data from the client-side with certain limitations, i.e. Date, Function and DOM objects can't be sent to server side but you might pass them as JSON.
Related
How can I get stock quotes using Google Finance API?
Using Apps Script to scrape javascript rendered web page
Can we use Chrome V8 functionalities from GAS?
You can get current price change using axios and cheerio, the code below shows you how to do this. And check the code in the online IDE
const cheerio = require("cheerio");
const axios = require("axios");
const currencyName = "AAPL:NASDAQ"; // currency name from browser URL
const BASE_URL = "https://www.google.com/finance/quote/";
const AXIOS_OPTIONS = {
headers: {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.157 Safari/537.36",
}, // adding the User-Agent header as one way to prevent the request from being blocked
};
function getCurrentPrice() {
return axios(BASE_URL + currencyName, AXIOS_OPTIONS).then(({ data }) => {
const $ = cheerio.load(data);
const pattern = /data:\[{3}(?<data>\[.{100,500}\])\]{3}, side/gm //https://regex101.com/r/VLKbBt/1
const currencyData = JSON.parse([...data.matchAll(pattern)].map(({groups}) => ({data: groups.data})).find(el=> el.data.includes(currencyName)).data);
const priceChange = currencyData[5][2]
return priceChange;
});
}
getCurrentPrice().then(console.log);
Output:
-0.53461844
And as you can see in the screenshot below, the data that we received is exactly as on the page.
If for some reason you want to make a Google Finance page parser in Python, have a look at the dedicated Google Finance blog post at SerpApi.
Disclaimer, I work for SerpApi
Related
I'm currently writing using Google-Apps-Scripts to do change labels on threads and messages. When the script completes, the payloads are moved, but the display on the user frontend does not change until the user interacts with Gmail.
Is there a way to push a refresh command to Gmail? How can I gracefully display "job's done" to the user so they know that messages are now appropriately labeled?
I am working directly against the Gmail API, not GmailApp. I discovered ActionResponseBuilder.setStateChanged(), but the problem is I am currently not working with any sort of frontend interface. This is all in the background.
Here is an abbreviated example of some of the code I'm using to grab messages to modify (as requested):
function changeLabel(ids,oldLabel,newLabel) {
Gmail.Users.Messages.batchModify({
"ids":ids,
"addLabelIds":oldLabel,
"removeLabelIds":newLabel
},"me");
}
function start() {
// Labels to work with
const FOO = "Label_5095729546757874255";
const BAR = "Label_5102306845672214551";
// API call to retrieve list of messages by Label
var msgIdsByLabel = new MessageIndex(FOO);
// API call to retrieve message contents
var payloadMessages = new Messages(msgIdsByLabel);
var manifestMessagesToMove = [];
for (var i=0;i < Object.keys(payloadMessages.response).length; i++) {
// Criteria for selecting messages to move goes here
manifestMessagesToMove[i] = payloadMessages.response[i].id;
}
// Change labels of Message Ids
changeLabel(manifestMessagesToMove,FOO,BAR);
// ??? Refresh Gmail Interface ???
}
Unfortunately this isn't possible.
The Gmail UI can't be refreshed from Apps Script as it is run in the cloud as a separate session to that which is being viewed in the web browser by a user. The two aren't connected - and the same goes for the Gmail API.
If you don't have a front-end interface (aka a Gmail Add-on utilising CardService) then there is not a way of displaying a message to the user, either. The refresh will have to be done manually.
So I'm trying to import Cardano Blockchain data like address balance, amount staked, rewards etc into a Google Sheet. I found this project named Blockfrost.io which is an API for accessing Cardano blockchain info and import it into apps etc.
I think I can use this with Google Sheets. Problem is I don't know how to authenticate. I've searched all around on the documentation and it's not clear to me. It seems it's possible if your're building an app or using the terminal.
But I just want to authenticate in the easiest way possible like in the browser address bar that way it would be simple to get the JSON with the info I need and import the info to Google Sheets.
This is where it mentions the Authentication:
https://docs.blockfrost.io/#section/Authentication
I already have an API key to access. But how do I authenticate?
So if I want to check the blockchain metrics (mainnet1234567890 is a dummy key, I won't use mine here):
https://cardano-mainnet.blockfrost.io/api/v0/metrics/project_id:mainnet1234567890
The JSON will still output this:
status_code 403
error "Forbidden"
message "Missing project token. Please include project_id in your request."
Is there a correct way to authenticate on the browser address bar?
It's not clear which BlockFrost API you are using Go JavaScript etc...
the API key goes in as a header on the request object. I was manually trying to connect to the service and found for a request is what I had to do in C#...
var aWR = System.Net.WebRequest.Create(url);
aWR.Method = "GET";
aWR.Headers.Add("project_id", "mainnetTheRestOfMyKeyIsHidden");
var webResponse = aWR.GetResponse();
var webStream = webResponse.GetResponseStream();
var reader = new StreamReader(webStream);
var data = reader.ReadToEnd();
Later I realized I wanted to use their API cause they implement the rate limiter, something I would rather use than build... I use the following with the BlockFrost API in c#
const string apiKey = "mainnetPutYourKeyHere";
const string network = "mainnet";
// your key is set during the construction of the provider.
ServiceProvider provider = new ServiceCollection().AddBlockfrost(network, apiKey).BuildServiceProvider();
// from there individual services are created
var AddressService = provider.GetRequiredService<IAddressesService>();
// The call to get the data looked like
AddressTransactionsContentResponseCollection TXR = await AddressService.GetTransactionsAsync(sAddress, sHeightFrom, sHeightTo, 100, iAddressPage, ESortOrder.Desc, new System.Threading.CancellationToken());
// etc. your gonna need to set the bounds above in terms of block height
Try using postman and include the "project_id" header with api key as the value like this - it will clear up the concept for you I think:enter image description here
I am trying to build a Gmail addon which includes 2 external API calls. The first one is fast (~200ms) and the second one is slow (~5s). Because of this I would like to first build the card with the results of the first fetch, and then update the card after the second call finishes.
Would it be possible to either:
Call fetchAll and build and render the card each time a request finishes
Trigger a function after the initial rendering is done (after return card.build())
Update the root card without returning it (I tried CardService.newNavigation().popToRoot().updateCard(card.build()) without success)
Any preferred way to render a card and then update it after data is fetched would be appreciated!
Below is an example function if useful.
function onGmailMessage(e) {
// Fetching email
var messageId = e.gmail.messageId;
var accessToken = e.gmail.accessToken;
GmailApp.setCurrentMessageAccessToken(accessToken);
var message = GmailApp.getMessageById(messageId);
// Preparing requests
var data = {
'text': message.getPlainBody(),
};
var options = {
'method' : 'post',
'contentType': 'application/json',
'payload' : JSON.stringify(data)
};
// Fetching responses. Here I would love to first display
// createCard(response_1) and then when the second call finishes
// return createCard(response_1 + '/n' + response_2)
var response_1 = UrlFetchApp.fetch('http://API_1/', options);
var response_2 = UrlFetchApp.fetch('http://API_2/', options);
return createCard(response_1 + '/n' + response_2);
Answer:
Unfortunately, this is not possible to do.
More Information:
This is a bit tricky so I'll split this answer down into your three points:
[Is it possible to] call fetchAll and build and render the card each time a request finishes?
A fetchAll function could be made to get all API responses, but you'll still end up waiting for API 2 to respond before updating what can be seen in the card.
The problem with this is that in order to display the rendered card, you need to make a return of some kind. Once you return the response of the first API your second API won't be made at all as the function will have already executed. Which leads onto point two:
[Is it possible to] trigger a function after the initial rendering is done (after return card.build())
I did a test with this, instead of returning API 1's response directly I stored its value in a Script Property and made a trigger execute 200 ms later with the call to API 2:
function onGmailMessage(e) {
// previous code
var response_1 = UrlFetchApp.fetch('http://API_1/', options);
ScriptApp.newTrigger("getSecondResponse").timeBased().after(200).create();
PropertiesService.getScriptProperties().setProperty('response1', response_1);
return createCard(response_1);
}
function getSecondResponse() {
// options 2 definition here;
var response_1 = PropertiesService.getScriptProperties().getProperty("response1");
var response_2 = UrlFetchApp.fetch('http://API_2/', options);
return createCard(response_1 + '/n' + response_2);
}
and adding the correct scopes in the manifest:
{
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/script.locale",
"https://www.googleapis.com/auth/gmail.addons.current.action.compose",
"https://www.googleapis.com/auth/gmail.addons.execute",
"https://mail.google.com/",
"https://www.googleapis.com/auth/script.scriptapp"
]
}
And which this did call the first API, display the response in the card and make the trigger, the card didn't update. I presume this is because the trigger acts as a cron job being executed from somewhere which isn't the add-on itself, so the second card return is never seen in the UI.
[Is it possible to] update the root card without returning it (I tried CardService.newNavigation().popToRoot().updateCard(card.build()) without success)
updateCard() is a method of the Navigation class. There's a whole page in the documentation which details the uses of Card navigation but the important parts to take away here is that the navigation methods are used in response to user interaction. From the documentation:
If a user interaction or event should result in re-rendering cards in the same context, use Navigation.pushCard(), Navigation.popCard(), and Navigation.updateCard() methods to replace the existing cards.
The following are navigation examples:
If an interaction or event changes the state of the current card (for example, adding a task to a task list), use updateCard().
If an interaction or event provides further detail or prompts the user for further action (for example, clicking an item's title to see more details, or pressing a button to create a new Calendar event), use pushCard() to show the new page while allowing the user to exit the new page using the back-button.
If an interaction or event updates state in a previous card (for example, updating an item's title from with the detail view), use something like popCard(), popCard(), pushCard(previous), and pushCard(current) to update previous card and the current card.
You can create multiple cards which have different content - for example one which contains response_1 and one which contains response_1 + "\n" + response_2, but some kind of interaction from a user is still needed to switch between the two views, and it won't get around the wait time you need to get a response from API 2.
Feature Request:
You can however let Google know that this is a feature that is important and that you would like to request they implement it. Google's Issue Tracker is a place for developers to report issues and make feature requests for their development services. I would suggest using the feature request template for G Suite Add-ons for this, rather than Apps Script directly.
References:
Class Navigation | Apps Script | Google Developers
Card navigation | G Suite Add-ons | Google Developers
With the recent news that ScriptDB is being deprecated, I'm searching for a suitable replacement. My particular use case is that I'm running Google Apps Script under Google Forms to process and store data relevant to the function of the form.
I've been through Google's migration guide (link), and I'm trying to connect a Google Apps Script running under a Google Form to a Parse Database (link). I've tried both methods listed on the migration guide (URL Fetch Service and ParseDB Library), and I can't get either to work correctly. I was able to write to the Parse Database using ParseDB, but the query function isn't working as expected. I also tried using the parseCom library from the Excel Liberation site (sorry, I'm out of links for this post, apparently), but that didn't work very well either.
I'm most interested in using Google's URL Fetch Service to connect to a Parse database, as that seems to be my most flexible option (i.e. to let me share data between forms - something I really couldn't do with scriptDB), but I feel like I'm in over my head just a bit. I'm open to other options as well. Thanks in advance!
I have the same 'query' problem using the MongoLab database as overstack-asked here. I thought Parse might work as an alternative, but then I saw this post having the same 'query' problem.
If you run your query from the browser directly to Parse it will probably work fine, just as it does for me using the MongoLab database.
Consequently, I strongly suspect the problem is in the URLFetchApp.fetch() function itself, not the Parse (nor MongoLab) database functions.
Here's what I have found to be the best way to query results using UrlFetchApp and Parse.
function query(key,value) {
var properties = getKeys();
var appId = properties.appId;
var restApi = properties.restApi;
var class = 'TestObject';
var url = 'https://api.parse.com/1/classes/' + class;
var query = 'where={"' + key + '":"' + value + '"}'
var encoded = encodeURIComponent(query);
var queryUrl = url + '?' + encoded;
var options = {
"method" : "get",
"headers" : {
"X-Parse-Application-Id": appId,
"X-Parse-REST-API-Key": restApi,
}
}
var data = UrlFetchApp.fetch(queryUrl, options);
return data;
}
I am also looking for this.
As a temp store ScriptDb has been very helpful. Sheets simply does not have the elegance of beautiful coding as in ScriptDb
I'm making standalone web app in Google Apps Script. I have somekind of task flow in the app. First search something from spreadsheet, then do some selections what to do with the collected data and after that fill spreadsheet row with some userinputs. So I need to run between few states.
I'm fairly sure I don't know enough about web tech so this might be very stupid question but here it goes.
I know that the e.parameters comes from the url like this www.google.com&value=helloworld
, e.parameters.value returns helloworld.
So now the problem: How do I set parameter e values in doGet(e) and call this same page or even different page/script in same script project? In otherwords how do I call self&value=helloworld ?
Is there some other way to do this? In GWT I just store everything to database and keep session in cookies. But cookies are not allowed so how to keep the state of the webapp in google apps script?
EDIT: This is how I pass the parameters for doGet(e).
function doGet(e) {
var app = UiApp.createApplication();
var newValue = 0;
if(e.parameter == undefined || e.parameter.value == undefined){
newValue = 1;
}else{
newValue = 1+parseInt(e.parameter.value);
}
var link = 'https://script.google.com/a/macros/domain.com/s/<insertidhere>/dev';
var anchor = app.createAnchor('Next',link+'?&value='+newValue);
anchor.setTarget('_self');
app.add(anchor);
var label = app.createLabel(newValue);
app.add(label);
return app;
}
The link format has changed a bit so instead of concatenate &value=... you need to add ? first like this ?&value1=helloworld&value2=....
Failing to use ? led me to think that there is bug and I need to use old format for the link and using that old format forbit any other than the script owner using it.
Hopefully this helps somebody to solve similar problems.
You've almost answered yourself. Append the URL paramenters to the end of your web app's URL and you can access them in your script.
Let's say you have a URL like
http://script.google.com/.../exec
When you hit the URL
http://script.google.com/.../exec?value=helloworld
,
inside doGet, you can read these URL parameters as
e.parameter.value;
IMO - instead of using ? to separate multiple parameters try keeping all the values in a single anchor with some other character separator of your choice such as # since after the first ? its possibly ignoring the other one and everything afterwards. Would be great to hear back on what you find.