Skip to content
This repository was archived by the owner on Dec 7, 2023. It is now read-only.

Commit f6ae8a0

Browse files
authored
initial checkin of news sample (#174)
* initial checkin of news sample * slight update to readme * A simple update to test new SSH key * another test submit * another tweak to readme. Just testing github * Initial Checkin of News Sentiment * Updates to ReadMe per initial review * final checkin per Readme review
1 parent aaa2082 commit f6ae8a0

File tree

4 files changed

+339
-0
lines changed

4 files changed

+339
-0
lines changed

news-sentiment/README.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
---
2+
title: Analyze the Sentiment of News Headlines
3+
description: Analyze the sentiment of news headlines retrieved for a user-specified topic from a free news API.
4+
labels: Apps Script, Sheets, Cloud Natural Language
5+
material_icon: Feed
6+
create_time: 2021-03-22
7+
update_time: 2021-03-29
8+
---
9+
10+
This solution showcases natural language sentiment analysis on current news headlines. It consists of a Google Sheet that uses Apps Script to fetch the current news headlines from the free news API `newsapi.org` using a user-provided `topic`.
11+
Once the news headlines are fetched and loaded into the sheet, it uses the Google Cloud Natural Language API to run a sentiment analysis for each headline.
12+
As the sentiments are returned from the API, the script updates the sheet with both a numeric score as well as a `happy`, `meh`, or `sad` icon depending on the sentiment.
13+
14+
## Technology highlights
15+
16+
- This solution demonstrates the following:
17+
* Shows how to add custom menus to the Sheets UI.
18+
* Uses `UrlFetch` to load news headlines from an external API.
19+
* Demonstrates the Cloud Natural Language API to analyze headlines.
20+
21+
## Try it
22+
23+
### Before you begin: Obtain your API keys
24+
To run this solution you need to get 2 API keys, one from the [Google Cloud Natural Language API](https://cloud.google.com/natural-language), and the second from the free News API @ [http://newsapi.org/](http://newsapi.org/).
25+
26+
#### Get the Google Cloud Natural Language API key
27+
28+
To get an API key for the Google Cloud Natural Language API, you must configure a Google Cloud Platform (GCP) project.
29+
30+
1. Create a new or use an existing GCP project:
31+
1. Navigate to the main {{console_name_short}} page.
32+
33+
[Cloud Console]({{console_url}}){: class="button button-primary" target="console" track-name="consoleLink" track-type="tutorial" track-metadata-position="body"}
34+
35+
* If you need to create a new project:
36+
* Select your associated billing account.
37+
* Accept the defaults for organization and location.
38+
* Click **Create**, and then select the new project in the console.
39+
40+
1. At the top of the console, click <span class=”material-icons”>menu</span> <span aria-label="and then">></span> **APIs & Services**.
41+
Click **+ Enable APIs and Services**.
42+
1. Search for `Cloud Natural Language API`, and enable it.
43+
1. On the left, click **Credentials** <span aria-label="and then">></span> **+ Create credentials**.
44+
1. In the drop-down menu, select **API key**.
45+
1. Save this key to add to your Apps Script `Code.gs` file.
46+
47+
#### Register for the News API key.
48+
To get an API key for the News API, you need to create a free News API account.
49+
1. To create an account, go to the [News API](https://newsapi.org/) site.
50+
1. Click **Get API Key** and follow the steps.
51+
1. Save the key to add to your Apps Script `Code.gs` file.
52+
53+
### Create your copy of the solution
54+
Copy and customize the solution with your API keys.
55+
56+
1. Click to make a copy of the [News Sentiment Analyzer - External spreadsheet](https://docs.google.com/spreadsheets/d/1Jw-d2ihbjSyO4SyzgXSiC5dzs36GY5aMGxuf_nc7WKU/copy).
57+
58+
1. To open the associated Apps Script Project, at the top, click **Tools <span aria-label="and then">></span> Script editor**.
59+
1. Update the `Code.gs` script file with your API keys:
60+
* const googleAPIKey = `YOUR_GOOGLE_API_KEY`;
61+
* const newsApiKey = `YOUR_NEWS_API_KEY`;
62+
1. Save the script and return to the spreadsheet.
63+
64+
### Run the News Sentiment Analyzer
65+
1. At the top of the spreadsheet, click the custom menu item **News Headlines Sentiments**.
66+
1. Select **Analyze News Headlines**.
67+
* Note: Upon first run, you must go through the Google authorization steps. This leaves the script running, but paused. After you authorize the script, click **Dismiss**, and select **Analyze News Headlines** again.
68+
1. In the popup dialog, enter a news topic or keyword. For example `Global Warming`.
69+
1. Click **Ok** to start the analysis.
70+
71+
## Next steps
72+
73+
* Experiment with other news topics.
74+
75+
* Try adding code to do further steps. For example, you could save all topics and their average sentiments over time in a different tab to see if any trends emerge.
76+
77+
* Add a time-based trigger to run the code on a daily basis with a set of topics selected from a different tab.
78+
79+
* For more help with Apps Script coding, try the [Apps Script Fundamentals codelabs](https://developers.google.com/apps-script/quickstart/fundamentals-codelabs).
80+
81+
* For more information on the Google Cloud Natural Language API, [try a quickstart](https://cloud.google.com/natural-language/docs/quickstarts).

news-sentiment/demo.gif

2.93 MB
Loading

news-sentiment/src/Code.js

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Global variables
16+
const googleAPIKey = 'YOUR_GOOGLE_API_KEY';
17+
const newsApiKey = 'YOUR_NEWS_API_KEY';
18+
const apiEndPointHdr = 'https://newsapi.org/v2/everything?q=';
19+
const happyFace =
20+
'=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/24/smiley-1635449_1280.png\")';
21+
const mehFace =
22+
'=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/24/smiley-1635450_1280.png\")';
23+
const sadFace =
24+
'=IMAGE(\"https://cdn.pixabay.com/photo/2016/09/01/08/25/smiley-1635454_1280.png\")';
25+
const happyColor = '#44f83d';
26+
const mehColor = '#f7f6cc';
27+
const sadColor = '#ff3c3d';
28+
const fullsheet = 'A2:D25';
29+
const sentimentCols = 'B2:D25';
30+
const articleMax = 20;
31+
const threshold = 0.3;
32+
33+
let headlines = [];
34+
let rows = null;
35+
let rowValues = null;
36+
let topic = null;
37+
let bottomRow = 0;
38+
let ds = null;
39+
let ss = null;
40+
let headerRow = null;
41+
let sentimentCol = null;
42+
let headlineCol = null;
43+
let scoreCol = null;
44+
45+
/**
46+
* Create menu in the Google Spreadsheet when Spreadsheet is opened.
47+
*
48+
*/
49+
function onOpen() {
50+
let ui = SpreadsheetApp.getUi();
51+
ui.createMenu('News Headlines Sentiments')
52+
.addItem('Analyze News Headlines...', 'showNewsPrompt')
53+
.addToUi();
54+
}
55+
56+
/**
57+
* Prompt user to enter a new headline topic.
58+
* Calls main function AnalyzeHeadlines with entered topic.
59+
*/
60+
function showNewsPrompt() {
61+
//Initialize global variables
62+
ss = SpreadsheetApp.getActiveSpreadsheet();
63+
ds = ss.getSheetByName('Sheet1');
64+
headerRow = ds.getDataRange().getValues()[0];
65+
sentimentCol = headerRow.indexOf('Sentiment');
66+
headlineCol = headerRow.indexOf('Headlines');
67+
scoreCol = headerRow.indexOf('Score');
68+
69+
// Build Menu
70+
let ui = SpreadsheetApp.getUi();
71+
let result = ui.prompt(
72+
'Enter news topic:',
73+
ui.ButtonSet.OK_CANCEL);
74+
75+
// Process the user's response.
76+
let button = result.getSelectedButton();
77+
topic = result.getResponseText();
78+
if (button == ui.Button.OK) {
79+
analyzeNewsHeadlines();
80+
} else if (button == ui.Button.CANCEL) {
81+
// User clicked "Cancel".
82+
ui.alert('News topic not selected!');
83+
}
84+
}
85+
86+
/**
87+
* For each headline cell, call NL API to get general sentiment and then update
88+
* sentiment response column.
89+
*/
90+
function analyzeNewsHeadlines() {
91+
// Clear and reformat sheet
92+
reformatSheet();
93+
94+
// Get headlines array
95+
headlines = getHeadlinesArray();
96+
97+
// Sync headlines array to sheet using single setValues call
98+
if (headlines.length > 0){
99+
ds.getRange(2, 1, headlines.length, headlineCol+1).setValues(headlines);
100+
// Set global rowValues
101+
rows = ds.getDataRange();
102+
rowValues = rows.getValues();
103+
getSentiments();
104+
} else {
105+
ss.toast("No headlines returned for topic: " + topic + '!');
106+
}
107+
}
108+
109+
/**
110+
* Fetch current headlines from the Free News API
111+
*/
112+
function getHeadlinesArray() {
113+
// Fetch headlines for a given topic
114+
let hdlnsResp = [];
115+
let encodedtopic = encodeURIComponent(topic);
116+
ss.toast("Getting headlines for: " + topic);
117+
let response = UrlFetchApp.fetch(apiEndPointHdr + encodedtopic + '&apiKey=' +
118+
newsApiKey);
119+
let results = JSON.parse(response);
120+
let articles = results["articles"];
121+
122+
for (let i = 0; i < articles.length && i < articleMax; i++) {
123+
let newsStory = articles[i]['title'];
124+
if (articles[i]['description'] !== null) {
125+
newsStory += ': ' + articles[i]['description'];
126+
}
127+
// Scrub newsStory of invalid characters
128+
newsStory = scrub(newsStory);
129+
130+
// Construct hdlnsResp as a 2d array. This simplifies syncing to sheet.
131+
hdlnsResp.push(new Array(newsStory));
132+
}
133+
134+
return hdlnsResp;
135+
}
136+
137+
/**
138+
* For each article cell, call NL API to get general sentiment and then update
139+
* sentiment response columns.
140+
*/
141+
function getSentiments() {
142+
ss.toast('Analyzing the headline sentiments...');
143+
144+
let articleCount = rows.getNumRows() - 1;
145+
let avg = 0;
146+
147+
// Get sentiment for each row
148+
for (let i = 1; i <= articleCount; i++) {
149+
let headlineCell = rowValues[i][headlineCol];
150+
if (headlineCell) {
151+
let sentimentData = retrieveSentiment(headlineCell);
152+
let result = sentimentData['documentSentiment']['score'];
153+
avg += result;
154+
ds.getRange(i + 1, sentimentCol + 1).setBackgroundColor(getColor(result));
155+
ds.getRange(i + 1, sentimentCol + 1).setValue(getFace(result));
156+
ds.getRange(i + 1, scoreCol + 1).setValue(result);
157+
}
158+
}
159+
let avgDecimal = (avg / articleCount).toFixed(2);
160+
161+
// Show news topic and average face, color and sentiment value.
162+
bottomRow = articleCount + 3;
163+
ds.getRange(bottomRow, 1, headlines.length, scoreCol+1).setFontWeight('bold');
164+
ds.getRange(bottomRow, headlineCol + 1).setValue('Topic: \"' + topic + '\"');
165+
ds.getRange(bottomRow, headlineCol + 2).setValue('Avg:');
166+
ds.getRange(bottomRow, sentimentCol + 1).setValue(getFace(avgDecimal));
167+
ds.getRange(bottomRow, sentimentCol + 1).setBackgroundColor(getColor(avgDecimal));
168+
ds.getRange(bottomRow, scoreCol + 1).setValue(avgDecimal);
169+
ss.toast("Done!!");
170+
}
171+
172+
/**
173+
* Call NL API to get sentiment response for headline.
174+
*
175+
* Important note: Not all languages are supported by Google document
176+
* sentiment analysis.
177+
* Unsupported languages generate a "400" response: "INVALID_ARGUMENT".
178+
*/
179+
function retrieveSentiment(text) {
180+
// Set REST call options
181+
let apiEndPoint =
182+
'https://language.googleapis.com/v1/documents:analyzeSentiment?key=' +
183+
googleAPIKey;
184+
let jsonReq = JSON.stringify({
185+
document: {
186+
type: "PLAIN_TEXT",
187+
content: text
188+
},
189+
encodingType: "UTF8"
190+
});
191+
192+
let options = {
193+
'method': 'post',
194+
'contentType': 'application/json',
195+
'payload': jsonReq
196+
}
197+
198+
// Make the REST call
199+
let response = UrlFetchApp.fetch(apiEndPoint, options);
200+
let responseData = JSON.parse(response);
201+
return responseData;
202+
}
203+
204+
// Helper Functions
205+
206+
/**
207+
* Remove old headlines, sentiments and reset formatting
208+
*/
209+
function reformatSheet() {
210+
let range = ds.getRange(fullsheet);
211+
range.clearContent();
212+
range.clearFormat();
213+
range.setWrapStrategy(SpreadsheetApp.WrapStrategy.CLIP);
214+
215+
range = ds.getRange(sentimentCols); // Center the sentiment cols only
216+
range.setHorizontalAlignment("center");
217+
}
218+
219+
/**
220+
* Return a corresponding face based on numeric value.
221+
*/
222+
function getFace(value){
223+
if (value >= threshold) {
224+
return happyFace;
225+
} else if (value < threshold && value > -threshold){
226+
return mehFace;
227+
} else if (value <= -threshold) {
228+
return sadFace;
229+
}
230+
}
231+
232+
/**
233+
* Return a corresponding color based on numeric value.
234+
*/
235+
function getColor(value){
236+
if (value >= threshold) {
237+
return happyColor;
238+
} else if (value < threshold && value > -threshold){
239+
return mehColor;
240+
} else if (value <= -threshold) {
241+
return sadColor;
242+
}
243+
}
244+
245+
/**
246+
* Scrub invalid characters out of headline text.
247+
* Can be expanded if needed.
248+
*/
249+
function scrub(text) {
250+
return text.replace(/[\‘\,\“\”\"\'\’\-\n]/g, ' ');
251+
}

news-sentiment/src/appsscript.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"timeZone": "America/Los_Angeles",
3+
"dependencies": {
4+
},
5+
"exceptionLogging": "STACKDRIVER",
6+
"runtimeVersion": "V8"
7+
}

0 commit comments

Comments
 (0)