質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
Google スプレッドシート

Google スプレッドシートは、フリーで利用できる表計算ソフト。Webアプリのためインターネットに接続することで利用できます。チャートやグラフの作成のほか、シートを他のユーザーと共有したり、同時に作業を進めることも可能です。

Google Apps Script

Google Apps ScriptはGoogleの製品と第三者のサービスでタスクを自動化するためのJavaScriptのクラウドのスクリプト言語です。

Q&A

解決済

1回答

692閲覧

GASでXMLをスプレッドシートに保存するトリガーの設定をしたい

BBA

総合スコア60

Google スプレッドシート

Google スプレッドシートは、フリーで利用できる表計算ソフト。Webアプリのためインターネットに接続することで利用できます。チャートやグラフの作成のほか、シートを他のユーザーと共有したり、同時に作業を進めることも可能です。

Google Apps Script

Google Apps ScriptはGoogleの製品と第三者のサービスでタスクを自動化するためのJavaScriptのクラウドのスクリプト言語です。

0グッド

1クリップ

投稿2022/11/09 15:17

編集2022/11/15 13:01

XMLのスクレイピングをGASでしてます

XmlからXmlServiceを使用して取得してスプレッドシートに保存できました。
昭和三十四年法律第百二十一号←修正しました。
の内容をスプレッドシートに保存しました。
別の条文も取得しました。

スクレイピングの内容が多い時6分以上かかる場合があり、
トリガーを使って6分以上の処理をできるようにコードを書いています。

トリガーを自動で再開できるコードにしたい。

Google Apps Script で6分以上の処理をする
このサイトのコードを参考にしました。
コードの内容は5分で実行を一旦停止し、1分後に途中から再開するという設定です。

発生している問題・エラーメッセージ

スクレイピングが始まらず実行ログで完了になる。

該当のソースコード

function func() { var startTime = new Date() var sheet = SpreadsheetApp.openById("*********").getSheetByName("**"); let url = 'https://elaws.e-gov.go.jp/api/1/lawdata/昭和三十四年法律第百二十一号'; //////↑修正しました let xml = UrlFetchApp.fetch(url).getContentText(); let document = XmlService.parse(xml); let root = document.getRootElement(); const applData = root.getChild("ApplData"); const lawFullText = applData.getChild("LawFullText"); const law = lawFullText.getChild("Law"); const lawBody = law.getChild("LawBody"); const mainProvision = lawBody.getChild("MainProvision"); //////参考サイトのコード var properties = PropertiesService.getScriptProperties(); //途中経過保存用 var startRowKey = "startRow"; //何行目まで処理したかを保存するときに使用するkey var triggerKey = "trigger"; //トリガーIDを保存するときに使用するkey //途中から実行した場合、ここに何行目まで実行したかが入る var startRow = parseInt(properties.getProperty(startRowKey)); //////条文取得コード const chapters = mainProvision.getChildren("Chapter"); chapters.forEach(function(chapter) { const articles = chapter.getChildren("Article"); articles.forEach(function(article){ var lastrow = sheet.getLastRow(); var recordrow = lastrow + 1; //////参考サイトのコード var rows = sheet.getDataRange().getValues(); for(var i = startRow; i < rows.length; i++){ var diff = parseInt((new Date() - startTime) / (1000 * 60)); if(diff >= 5){ //5分経過していたら処理を中断 properties.setProperty(startRowKey, i); //何行まで処理したかを保存 setTrigger(triggerKey, "func"); //トリガーを発行 return; } //////*条文取得コード* const articleCaption = article.getChild("ArticleCaption"); const articleTitle = article.getChild("ArticleTitle"); const paragraphs = article.getChildren("Paragraph"); if (articleCaption != null){ sheet.getRange("A" + recordrow).setValue(articleCaption.getText()); } sheet.getRange("B" + recordrow).setValue(articleTitle.getText());  if (paragraphs != null){ paragraphs.forEach(function(paragraph){ const paragraphNum = paragraph.getChild("ParagraphNum"); const paragraphSentence = paragraph.getChild("ParagraphSentence"); sheet.getRange("C" + recordrow).setValue(paragraphNum.getValue()); sheet.getRange("F" + recordrow).setValue(paragraphSentence.getValue()); recordrow = recordrow + 1; const items = paragraph.getChildren("Item"); if (items !== null) { items.forEach(function (item) { const itemtitle = item.getChild("ItemTitle"); sheet.getRange("D" + recordrow).setValue(itemtitle.getText()); const itemSentence = item.getChild("ItemSentence");     sheet.getRange("F" + recordrow).setValue(itemSentence.getValue());        recordrow = recordrow + 1; const columns = itemSentence.getChildren("Column"); if (columns !== null) { columns.forEach(function (column) {      sheet.getRange("F" + recordrow).setValue(column.getText()); }) } const subitem1s = item.getChildren("Subitem1"); if (subitem1s !== null) { subitem1s.forEach(function (subitem1) { const subitem1Title = subitem1.getChild("Subitem1Title");     sheet.getRange("D" + recordrow).setValue(subitem1Title.getText()); const subitem1Sentences = subitem1.getChildren("Subitem1Sentence"); subitem1Sentences.forEach(function (subitem1Sentence) {      sheet.getRange("F" + recordrow).setValue(subitem1Sentence.getValue()); }); const subitem2s = subitem1.getChildren("Subitem2"); if (subitem2s !== null) { subitem2s.forEach(function (subitem2) { const subitem2Title = subitem2.getChild("Subitem2Title"); sheet.getRange("D" + recordrow).setValue(subitem2Title.getText()); const subitem2Sentences = subitem2.getChildren("Subitem2Sentence"); subitem2Sentences.forEach(function (subitem2Sentence) { sheet.getRange("F" + recordrow).setValue(subitem2Sentence.getValue()); }); const subitem3s = subitem2.getChildren("Subitem3"); if (subitem3s !== null) { subitem3s.forEach(function (subitem3) { const subitem3Title = subitem3.getChild("Subitem3Title");         sheet.getRange("D" + recordrow).setValue(subitem3Title.getText()); const subitem3Sentences = subitem3.getChildren("Subitem3Sentence"); subitem3Sentences.forEach(function (subitem3Sentence) {       sheet.getRange("F" + recordrow).setValue(subitem3Sentence.getValue()); });    }) }    }) } }) } }) } //////↑かっこ追加修正しました。 }) } } //////*参考サイトのコード* //全て実行終えたらトリガーと何行目まで実行したかを削除する deleteTrigger(triggerKey); properties.deleteProperty(startRowKey); }) }) } //指定したkeyに保存されているトリガーIDを使って、トリガーを削除する function deleteTrigger(triggerKey) { var triggerId = PropertiesService.getScriptProperties().getProperty(triggerKey); if(!triggerId) return; ScriptApp.getProjectTriggers().filter(function(trigger){ return trigger.getUniqueId() == triggerId; }) .forEach(function(trigger) { ScriptApp.deleteTrigger(trigger); }); PropertiesService.getScriptProperties().deleteProperty(triggerKey); } //トリガーを発行 function setTrigger(triggerKey, funcName){ deleteTrigger(triggerKey); //保存しているトリガーがあったら削除 var dt = new Date(); dt.setMinutes(dt.getMinutes() + 1); //1分後に再実行 var triggerId = ScriptApp.newTrigger(funcName).timeBased().at(dt).create().getUniqueId(); //あとでトリガーを削除するためにトリガーIDを保存しておく PropertiesService.getScriptProperties().setProperty(triggerKey, triggerId); }

試したこと

初めは参考サイトの/* ここでメイン処理 */の部分にXmlServicしたコードを添付しましたが、
同じエラーで反応せず、
eachの箇所が悪くてループとトリガーに設定にならないのかと思い、
添付したコードにしてみましたがうまくいきませんでした。

補足情報(FW/ツールのバージョンなど)

コードが長く読みにくくてすみません。
ヒントなど教えていただけないでしょうか。

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2022/11/10 21:08 編集

・質問記載のコードをGASエディタに貼り付けて保存しても、構文エラーがあり保存できないため、そもそも実行できません。 ・エラーを直したとしても、トリガーとは無関係に、その法令は読み取れていません。(「明治二十九年法律第八十九号」のXMLデータは Chapter の前に Part があり、また、一部のChapterについてはSubprovisionで分割されているが、質問記載のプログラムは Part を読み取らず、Subprovisionに対応していないので読み取りできません) 今一度ご確認下さい
BBA

2022/11/14 02:44

遅くなりすみません。なかなかできないなと思いながら他の条文を試し、そのまま質問に載せてしまいました。 確認します。すみません。
BBA

2022/11/15 13:01

遅くなり申し訳ありません。修正しました。
guest

回答1

0

ベストアンサー

ヒントです。

① まず、プロパティに何も設定しないで開始した場合、質問のコードは正常に動作しません。
これは下記の理由によります。

js

1 var startRow = parseInt(properties.getProperty(startRowKey));

の部分。
→ プログラムは、プロパティサービスから "startRow"をキーに持つプロパティを読み取ろうとします。
しかし、指定したキー(”startRow")は初期状態では存在しないため、getProperty 関数は undefined を返します。
undefined に対してparseInt 関数を適用すると NaNになります。
つまり

js

1 var startRow = parseInt(properties.getProperty(startRowKey));

の startRow は NaNになるため、その後の

js

1 for (var i = startRow; i < rows.length; i++) {

で、ループが開始しません。

ここは、「プロパティキーが存在せず startRow が NaNの場合、startRow に 1 を設定し直す」というような処理にすべきです。

元記事を見ると

js

1 if(!startRow){ 2 //初めて実行する場合はこっち 3 startRow = 1; 4 }

と書いてあります。

js

1 var rows = sheet.getDataRange().getValues();

ここ自体も想定通りに動作しない原因です。
初期状態では、データがないか、せいぜい見出しの1行なので、この場合 rows.length は 最初 ゼロか1程度にしかなりません。
その後、

for (var i = startRow; i < rows.length; i++) {

としても、 rows.length が 0または1ならば 1>0または1==1ですから、ループは開始しません。

元記事の「Google Apps Script で6分以上の処理をする」は、実際不親切な記事で質問者さんにはあまり参考にならないと思います。(この記事では「/* ここでメイン処理 */」の1行だけしか書かれておらず、「どのような処理について、具体的にどう実装すれば継続できるのか」ということが書かれていません。)
このため、質問者さんも、よく理解できずに適当に追加した挙句「動かない」と困っているのではないでしょうか。

元記事がおそらく元にしているであろうコードは 「あるシート内のデータを1行目から、別のシートに1行ずつコピーする」 という処理を想定したものと考えられます。
つまり記事のコード中の sheet はコピー元のシートを意味しています。
一方質問者さんのコードでは、sheet は抽出した条文の転記先シートなのですから、全く役割が異なりますよね?

js

1 for (var i = startRow; i < rows.length; i++) {

この、ループ開始の forを articles.forEach の後に入れているのも間違いです。
仮に、すでにシートに書き込み済みの条文が100行あり、101行目から書き込みを再開するとした場合、rows.length は100になります。
この状態で動かすと、再開時の同じ条文を100回シートに追記してから、次の条文に移る・・・という動作を繰り返すことになってしまいます。

④ そもそも、XMLのどの部分まで読み込んだか?を記録する処理がない。
100歩譲って、上記①~③を直したとしても、うまく動きません。
なぜなら、質問文のコードには、元記事と同じく「どの行数まで書き込んだか」をプロパティに記録する部分はありますが、「元のXMLデータをどこまで読み込んだか?」を記録し、再開時に読み取り、復元する部分がないため、再開できません。

元記事のコードは、「元シートの内容を別のシートに全く同じ状態で1行ずつコピーする」という至極単純な処理を念頭に置いています。この単純な処理の場合、「書き込み済みの行=読み込み済みの行」といえるので、書き込み済みの行数だけ記録すればOKです。

一方、質問者さんがやろうとしているのは、XMLの複雑な条文を読み込み、加工してスプレッドシートに記録する、という処理です。

このような処理の場合、読み込み元データには「スプレッドシートの行数」という概念がなく、書き込み行数と1対1対応していないため、書き込み済みデータに対応する「読み込み済み」の地点は行数で表現できず、別の切り口で記録する必要があります。

では、どのようなものを使うかですが、ここでは「条文数」が適切でしょう。(1つの条文を書き込むのに数分かかることは通常考えられないため)
つまり、

  • 1条書き込みが完了するごとに、実行開始から5分以上経過したかを確認する。
  • 5分以上経過していたら中断 →「第何条まで読み込んだ(=書き込んだ)か?」というデータをプロパティサービスに記録しておく
  • 再開する時は、プロパティサービスから読み込み済みの条文数を取得し、法律の第一条からその条文まで1条ずつ読み込む処理を再度行う。すでに読み込み済み(シートに転記済み)の条文である間は書き込み処理をスキップし、新しく書き込まなければならない条文に到達した時点で書き込みを開始する
  • 転記先シートの書き込み開始行については getLastRowで取得すればよいので、プロパティに記録する必要はない

というようにすればよいでしょう。
(質問文記載のコードだと、一番時間のかかる処理は「書き込み」であるため)


現状だと、そこそこ大幅な修正が必要と思います。
時間があればコードを考えますが、現状忙しいため、上記ヒントにとどめます。

投稿2022/11/15 15:02

編集2022/11/15 15:30
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

BBA

2022/11/15 15:17

ありがとうございます!理解が追いついておらず、元コードの解釈もままならないので、少しずつ読み込みながら、自分のコードと睨めっこしながら理解してみます。頑張ってみます。
BBA

2022/11/24 16:19

具体的に書いてくださってありがとうございます。読みながら調べてトライしてます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問