GAS
GoogleAppScriptの略で、クラウド上で動作するVBAみたいな感じです。
無料で利用でき、一回の動作に6分(!)という制限が付きますが、
かなり高度なWebベースの処理開発が可能です。
この記事では、Qiitaの記事をOpenAIに読み込ませ、要約とおすすめポイントを生成AIから引き出し、スプレッドシートに保存します。
スクレイピングといいつつ、QiitaAPIで手抜きをしています。ご容赦ください。
*CodeForGifu GASハンズオンの資料としてこの記事は作成されました。
材料
- OpenAIのAPIキー
OpenAIからはAPIキーを取得する必要があり、今回はmodel=gpt-4oを利用します。 - Googleアカウント
アカウントがあれば無料で十分です。
--
APIキーの取得方法はこちらの記事を参考にしてください。
https://qiita.com/IshigiwaKenichiro/items/c84bc5288854abb277a6
*ハンズオンでは一緒に作成します。モデル制限などが難しいので。
つくりかた
GASのプロジェクトを作成する。
下記URLへアクセスします。
https://script.google.com/home/start
あたらしいプロジェクトを選択します。
- プロジェクト名を新しくしておきます
- 最初のコードを書きます
function myFunction() {
console.log('hello');
}
Ctrl+Sで保存できます。
おわったら、▷実行ボタンを押します。
実行ログではconsoleに出力されたログを見ることができます。
安全ではないページに移動します。
許可を促されるのでOKしてください。
QiitaAPIからデータを取得する。
/api/v2/items?page=1&per_page=5
qiitaの検索は、そのままqueryパラメータに使えるようです。
Qiitaからデータを取得する関数と実行結果はこのようになります。
/**
* Qiitaのデータを取得する(JavaScript,TypeScriptをタグにもつ)
*/
function getQiitaLinks() {
const now = new Date();
now.setDate(now.getDate() - 1);
//日付のフォーマット
const dateStr = Utilities.formatDate(now, 'Asia/Tokyo', 'yyyy-MM-dd')
//クエリにURLエンコードが必要な文字があるよ
const result = UrlFetchApp.fetch(encodeURI(
`https://qiita.com/api/v2/items?query=tag:JavaScript,TypeScript created:>=${dateStr}`
));
const json = JSON.parse(result.getContentText('utf-8'));
//title, url, tags, rendered_bodyのみ抽出
return json.map(({ title, url, tags ,rendered_body }) => ({ title, url, tags,rendered_body }))
}
GASでは非同期処理を考えなくてもよいのがいいですね。
但し利用できる関数やクラスはブラウザやNodeで利用するものとは異なってきます。
OpenAIにデータを投げる。
👇こちらのガイドを参考にします。
https://platform.openai.com/docs/guides/text-generation
curlで投げるとこんな感じです。methodがなんでもいいみたいですね。おもしろい。
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $OPENAI_API_KEY" \
-d '{
"model": "gpt-3.5-turbo",
"messages": [
{
"role": "user",
"content": "Where was it played?"
}
]
}'
GASで実装するとこうなります。
- Bodyが必要なので、content-typeをヘッダに指定すること
- Bodyが必要なので、メソッドをGETではなくPOSTにする
- payload部がBodyになるので、特定の形式のJSONを作成する
- APITokenはauthorizationヘッダーにBearerで指定する
以上がポイントになります。
/**
* OpenAIに挨拶する。
*/
function hello(apiToken){
const resp = UrlFetchApp.fetch(
`https://api.openai.com/v1/chat/completions`, {
method : 'post',
headers : {
'content-type' : 'application/json',
'authorization' : `Bearer ${apiToken}`
},
payload : JSON.stringify({
model : 'gpt-4o',
messages : [
{
role : 'user',
content : 'hello'
}
]
})
}
)
return JSON.parse(resp.getContentText('utf-8'))
}
Qiitaの内容をOpenAIに食べさせる
OpenAIにqiitaの内容を食べさせるコードは下記のようになります。
プロンプトをいろいろ凝ってみるのもいいですね。
/**
* 読んでもらう。
*/
function thought(apiToken, content) {
const prompt = [];
prompt.push(`# 指令`)
prompt.push(`あなたはプログラマです。URLを渡すので、要約と感想をお願いします。`);
prompt.push(`#入力`)
prompt.push(content);
const resp = UrlFetchApp.fetch(
`https://api.openai.com/v1/chat/completions`, {
method: 'post',
headers: {
'content-type': 'application/json',
'authorization': `Bearer ${apiToken}`
},
payload: JSON.stringify({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: prompt.join('\n')
}
]
})
}
)
return JSON.parse(resp.getContentText('utf-8'))
}
ここまでで、Qiitaのデータを取得してOpenAIに評価させられるようになりました。
さっそく試してみましょう。
function myFunction() {
// console.log(getQiitaLinks());
// console.log(hello(APITOKEN));
const links = getQiitaLinks().filter((d, idx) => idx < 5);
console.log(`qiita items retrieved.${links.length} items`);
for (let { url, title, rendered_body } of links) {
const result = thought(APITOKEN, url);
console.log({ title, url });
console.log(result.choices[0]);
}
}
GoogleSpreadSheetに結果を書き込む
consoleに置いておいてもしょうがないので、GoogleSpreadSheetに書き込んで保存したいですよね。
/**
* スプレッドシートのURLに配列を書き込みます。
* @param {string} url
* @param {any[][]} aoo
*/
function writeSpreadSheet(url, aoo) {
if (0 == aoo.length) return;
const book = SpreadsheetApp.openByUrl(url);
const sheetName = Utilities.formatDate(new Date, 'Asia/Tokyo', 'yyyy-MM-dd');
//シート名で取得。とれなかったら、新しく作る。
const sheet = book.getSheetByName(sheetName) ?? book.insertSheet();
sheet.setName(sheetName);
sheet.activate();
//全消し
sheet.clear();
const headers = Object.keys(aoo[0]);
//header
sheet.getRange(1, 1, 1, headers.length).setValues([headers])
//data
for (let r = 0; r < aoo.length; r++) {
const item = aoo[r];
for (let c = 0; c < headers.length; c++) {
sheet.getRange(r + 2, c + 1).setValue(item[headers[c]])
}
}
//行列の幅調整
sheet.autoResizeColumns(1, headers.length);
sheet.autoResizeRows(1, aoo.length + 1);
}
URLはスプレッドシートをGoogleDrive内で新しく作って、そのURLを指定すれば大丈夫です。(スプレッドシートを開いたときに、アドレスバーに出ているものでオッケー)
コード全文
さあ、結果をスプレッドシートに保存できるようになりました。
//OpenAIのAPIトークン
const APITOKEN = `sk-***`;
//SpreadSheetのURL
const SSURL = `https://docs.google.com/spreadsheets/d/***`;
function myFunction() {
// console.log(getQiitaLinks());
// console.log(hello(APITOKEN));
const links = getQiitaLinks().filter((d, idx) => idx < 5);
console.log(`qiita items retrieved.${links.length} items`);
const aoo = [];
for (let { url, title, tags, rendered_body } of links) {
const result = thought(APITOKEN, rendered_body);
console.log({ title, url });
console.log(result.choices[0]);
aoo.push({ url, title, tags: tags.map(tag => tag.name).join(','), openai: result.choices[0].message.content })
}
writeSpreadSheet(SSURL, aoo);
}
/**
* Qiitaのデータを取得する(JavaScript,TypeScriptをタグにもつ)
*/
function getQiitaLinks() {
const now = new Date();
now.setDate(now.getDate() - 1);
//日付のフォーマット
const dateStr = Utilities.formatDate(now, 'Asia/Tokyo', 'yyyy-MM-dd')
//クエリにURLエンコードが必要な文字があるよ
const result = UrlFetchApp.fetch(encodeURI(
`https://qiita.com/api/v2/items?query=tag:JavaScript,TypeScript created:>=${dateStr}`
));
const json = JSON.parse(result.getContentText('utf-8'));
//title, url, tags, rendered_bodyのみ抽出
return json.map(({ title, url, tags, rendered_body }) => ({ title, url, tags, rendered_body }))
}
/**
* OpenAIに挨拶する。
*/
function hello(apiToken) {
const resp = UrlFetchApp.fetch(
`https://api.openai.com/v1/chat/completions`, {
method: 'post',
headers: {
'content-type': 'application/json',
'authorization': `Bearer ${apiToken}`
},
payload: JSON.stringify({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: 'hello'
}
]
})
}
)
return JSON.parse(resp.getContentText('utf-8'))
}
/**
* 読んでもらう。
*/
function thought(apiToken, content) {
const prompt = [];
prompt.push(`# 指令`)
prompt.push(`あなたはプログラマです。URLを渡すので、要約と感想をお願いします。`);
prompt.push(`#入力`)
prompt.push(content);
const resp = UrlFetchApp.fetch(
`https://api.openai.com/v1/chat/completions`, {
method: 'post',
headers: {
'content-type': 'application/json',
'authorization': `Bearer ${apiToken}`
},
payload: JSON.stringify({
model: 'gpt-4o',
messages: [
{
role: 'user',
content: prompt.join('\n')
}
]
})
}
)
return JSON.parse(resp.getContentText('utf-8'))
}
/**
* スプレッドシートのURLに配列を書き込みます。
* @param {string} url
* @param {any[][]} aoo
*/
function writeSpreadSheet(url, aoo) {
if (0 == aoo.length) return;
const book = SpreadsheetApp.openByUrl(url);
const sheetName = Utilities.formatDate(new Date, 'Asia/Tokyo', 'yyyy-MM-dd');
//シート名で取得。とれなかったら、新しく作る。
const sheet = book.getSheetByName(sheetName) ?? book.insertSheet();
sheet.setName(sheetName);
//全消し
sheet.clear();
const headers = Object.keys(aoo[0]);
//header
sheet.getRange(1, 1, 1, headers.length).setValues([headers])
//data
for (let r = 0; r < aoo.length; r++) {
const item = aoo[r];
for (let c = 0; c < headers.length; c++) {
sheet.getRange(r + 2, c + 1).setValue(item[headers[c]])
}
}
//行列の幅調整
sheet.autoResizeColumns(1, headers.length);
sheet.autoResizeRows(1, aoo.length + 1);
}
スケジューリング
ここまでで、昨日書かれた記事の要約が取れましたので、このプログラムを毎日動かしたい。
トリガー機能を利用します。左のペインから選択。
時間ベースのトリガーを「日付ベースのタイマー」に
時刻の選択を「0時~1時」に設定します。
すべての実行ログは「実行数」から確認できます。トリガーの下にあります。
まとめ
結果がどうなるか、この記事では載せていません。試してみてね!
エラーハンドリングやロギングなどを省いて、さくっとGASとOpenAIのAPIを試す目的で書きました。OpenAIがURLをいつも読んでくれるわけではなく、500エラーも出ますのでリトライは必要ですね。
このあと時間があれば、claspによるプロジェクト管理とGmailでのメール送付についてもやってみたいと思います。
またTypeScriptにて@types/google-app-scriptを利用しての開発なども時間があれば・・・。