はじめに
友人の依頼であるニュースサイトの最新記事データを毎日取得できないか?と相談を受けました。
スクレイピングはあまりした事がなかったので、良い機会だと思い挑戦してみました!!!
コードの例は全てあるニュースサイトからデータを取得する想定で書いています
全体像
以下のステップで分けて考えると良いかと思います。
①データ取得
まずはこちらの記事を見てくださいw
これでデータ取得の全体像は掴めるかと。
ただ、今回の場合はデータを取得するだけではダメで、、、改行コードなど不要な文字列が紛れているので、取得した後に編集する工程が結構大事だったりします。
②今日の日付でシートを作成しアクティブにする
「スプレッドシート」と「シート」は違うのでご注意ください!
③取得したデータを②で作成したシートに保存
こんな感じで保存されます。
データ取得
基本こちらの記事を参考にしているので、当然Parserライブラリも使います!
まずはトップページから最新ニュースを取得するfunctionを作りましょう
function getValues() {
let response = UrlFetchApp.fetch("https://jp.news.gree.net/media/scoopie-news");
let text = response.getContentText("utf-8");
//トップページから必要なブロックを取得
let topic_block = Parser.data(text).from('class="unit"').to('</section>').build();
//ulタグで囲まれている記述(トップニュース)を抽出
let content_block = Parser.data(topic_block).from('<ul>').to('</ul>').build();
// ニュースリスト用の配列変数を宣言
let newsList= new Array();
// content_blockの要素のうち、<li class="cst news-list-item">に囲まれている記述を抽出
topics = Parser.data(content_block).from('<li class="cst news-list-item">').to('</li>').iterate();
// <li class="cst news-list-item">に囲まれたブロックの回数分、URL/タイトル/画像リンク/日付/本文/名前を抽出する
for (news of topics) {
//記事のURL
const newsUrl = Parser.data(news).from('href="').to('#"').build();
//記事のタイトル
const newsTitle = Parser.data(news).from('<p class="newsLead">').to('</p>').build();
//記事の見出し画像
const newsImage = Parser.data(news).from('<img src="').to('"').build();
//記事の公開日
const newsDate = Parser.data(news).from('<span class="minor lineL">').to('</span>').build();
//記事のURLを渡し、個別の記事から本文と名前を取得
const newsTextAndName = getNewsTextAndName(newsUrl);
//配列を作る
const newsInfo = [newsDate, newsUrl, newsTitle, newsImage, newsTextAndName[0], newsTextAndName[1]];
//作った配列newsInfoをあらかじめ用意した配列newsListに入れる
newsList.push(newsInfo);
}
return newsList;
}
//記事のURLを渡し、個別の記事から本文と名前を取得
function getNewsTextAndName(newsUrl) {
let response = UrlFetchApp.fetch(newsUrl);
let text = response.getContentText("utf-8");
let news_block = Parser.data(text).from('<article id="news-entry">').to('</article>').build();
let news_text_block = Parser.data(news_block).from('<div class="newsArticle">').to('<ul class="snb">').build();
let textItems = new Array();
const regex = /^(?! ).*/;
textItems = Parser.data(news_text_block).from('<p>').to('</p>').iterate();
let textItemsRemovedNewLine = textItems.filter(function (value) {
return value.match(regex);
});
const startNum = textItemsRemovedNewLine.length -2;
textItemsRemovedNewLine.splice(startNum, 2);
const joinedTextItem = textItemsRemovedNewLine.join();
const textItem = joinedTextItem.replace('<strong>', '').replace('</strong>', '').replace(',', '');
let name = Parser.data(news_text_block).from('<strong>').to('</strong>').build();
return [textItem, name];
}
データ編集の詳細
取得したデータは色々と編集しないととても見にくいです。。。
なのでgetNewsTextAndName(newsUrl)
では、めっちゃコネコネして編集しています。。。
<p>
で分割して配列にする
let textItems = new Array();
textItems = Parser.data(news_text_block).from('<p>').to('</p>').iterate();
これをしないと改行コードを省きにくいです。。。!
改行コードを省く
let textItems = new Array();
textItems = Parser.data(news_text_block).from('<p>').to('</p>').iterate();
const regex = /^(?! ).*/;
let textItemsRemovedNewLine = textItems.filter(function (value) {
return value.match(regex);
});
filter
とmatch
で「改行コードを省いた文字列の配列」であるtextItemsRemovedNewLine
を作成します。
通常のfilter()関数では、配列から検索してヒットした値を取得できます。逆に「ヒットした値を取り除いた状態」にしたい場合は、正規表現とmatch()関数を使うことで実現できました!
不要な<p>
を省く
「最新のグラビア記事はこちら」と「 ※写真はTwitterから」の情報は不要なので、これらを配列から省きます!
配列化した本文の最後の2つの要素は必ず👆になるので、最後の2つを省きます。
// 最後から2つ目の数値
const startNum = textItemsRemovedNewLine.length -2;
// spliceで配列から省く
textItemsRemovedNewLine.splice(startNum, 2);
配列を合体して1つの文字列にする
const joinedTextItem = textItemsRemovedNewLine.join();
1つにした文字列から不要な<strong>
タグを省く
const textItem = joinedTextItem.replace('<strong>', '').replace('</strong>', '').replace(',', '');
取得したデータをシートに保存
さぁここからが本番です!
いよいよデータをシートに保存していきます
function setValues() {
//アクティブなスプレッドシートを取得
const ss = SpreadsheetApp.getActiveSpreadsheet();
//シートの名前用に今日の日付を取得
const today = getTodayDate();
//もし「今日の日付」のシートが存在しなければ、「今日の日付」のシートを作成
if (ss.getSheetByName(today) == null) {
ss.insertSheet(today).activate();
ss.moveActiveSheet(1);
}
//今日のシートを取得
const todaySheet = ss.getSheetByName(today);
//もしA1が空ならisFirstOfDayはtrueになる。A1が空なら何も書き込まれていない作りたてのシートだという事
const isFirstOfDay = todaySheet.getRange('A1').isBlank();
//「データ取得」の項目で解説したfunctionでニュースの値を取得
const newsList = getValues();
for (news of newsList) {
if (!isFirstOfDay) {
// Rowの値(日付)を取得
const dateRow = todaySheet.getRange(1, 1, todaySheet.getLastRow()).getDisplayValues();
const date = news[0];
//includesがfalseならappendRowを実行
if (!dateRow.flat().includes(date)) {
todaySheet.appendRow(news);
}
} else {
todaySheet.appendRow(news);
}
}
}
function getTodayDate() {
//Dateオブジェクトからインスタンスを生成
let today = new Date();
//formatDateメソッドで日付の表示形式を変換する
today = Utilities.formatDate(today, "JST", "yyyy/MM/dd");
return today;
}
シートに保存する処理の解説
スクレイピングは定期実行なので「今回のデータ取得&スプシに保存」が、今日初めて実行されてものか?を主に判別しなければうまくできませんでした。
作成したシートを一番左に置く
if (ss.getSheetByName(today) == null) {
ss.insertSheet(today).activate();
ss.moveActiveSheet(1);
}
gasで作成したシートはデフォルトのままだと「一番右」に保存されます。
今回は4/2, 4/1, 3/31 のように新しい日付を右に持っていきたいのでmoveActiveSheet(1)
で、シート作成後に移動させてます。
今日の2回目以降の実行で重複した記事を保存しないために比較する
for (news of newsList) {
if (!isFirstOfDay) {
// Rowの値(日付)を取得
const dateRow = todaySheet.getRange(1, 1, todaySheet.getLastRow()).getDisplayValues();
const date = news[0];
//includesがfalseならappendRowを実行
if (!dateRow.flat().includes(date)) {
todaySheet.appendRow(news);
}
} else {
todaySheet.appendRow(news);
}
}
今日初めて実行したかどうか?をisFirstOfDay
で判断します。
もし今日初めての実行ではないならA1列にある日付をnews[0]
で取得して、重複する日付があるかどうかを確認します。
そして重複していない値だけをtodaySheet.appendRow(news);
で保存します。
定期実行に設定する
時間主導型に設定して、自分の好きな時間に定期実行されるようようにする。
画像の場合は毎日6時間ごとに、設定した関数を実行する。
最後に
日毎にシートを作成せずに、1つのシートにずっと追加し続けたい人は👆を参考に!!
一応その2があります