環境
- Spring Framework 6.0.2
- Spring Boot 3.0.0
- Gradle 7.4.2
- Java 17
はじめに
※この記事はネタ記事です。
Springには定期的に処理を実行できる@Scheduled
というものがあるようで、使ったことがないので遊んでみたいです。
「一定時間たったらアプリを停止して休憩を促す」、「定期的に猫ちゃんの画像をネットから取得して表示する」等を思いつきましたが、せっかくですのでもうちょっと実用的なことに使いたいです(猫ちゃんの画像が実用的でないと言っているわけではありません)
作るもの
開発中のSpringプロジェクトに置いておけば、定期的にQiitaの新着記事を取得してブラウザで表示してくれるものを作っていきたいと思います。
というのも、勉強はインプットとアウトプットのバランスが重要です。
そこで、コードを書くというアウトプットをしている間に、適度なインプットを促すためにQiitaの記事を自動的にブラウザで開かせます。
そうすることで、定期的に記事を読むというインプットを割り込ませることができるため、バランス良く学習できると思います。
はい。誰にも喜ばれなそうな記事を書く自分を正当化することができたので、早速作っていきたいと思います。
記事を取得してみる
Qiita APIという便利なものがあるのでこれを使います。
ドキュメントを眺めてみると、
GET /api/v2/items
記事の一覧を作成日時の降順で返します。
とあり、新しい記事を取得するにはこれを叩けばよさそうです。
試しに以下のリクエストをGETしてみると、
https://qiita.com/api/v2/items?page=1&per_page=1
以下のようなJSONを取得できます。page
は最新から何個目の記事から取得するか、per_page
は取得する記事数を表すので、上のリクエストを叩くと最新の記事を1つ取得できます。
↓取得したJSONの形式
[
{
"rendered_body": xxx,
"body": xxx,
...(省略)
"url": "https://qiita.com/xxx",
...(省略)
}
]
次に、このJSONをSpringで取得してみます。
Spring FrameworkでAPIを叩くにはRestTemplate
を使うと簡単にできます。
説明は後でしますが、以下がコードです。
@Component
public class ScheduledTasks {
@Autowired
RestTemplate restTemplate;
@Scheduled(fixedRate = 10000)
public void getQiitaArticles() {
// エンドポイント定義
String endPoint = "https://qiita.com/api/v2/items?page=1&per_page=3";
// 記事情報(JSON)を取得
QiitaResponse[] responses = restTemplate.getForObject(endPoint, QiitaResponse[].class);
// 取得した記事のURLを表示
for(QiitaResponse res : responses){
System.out.println(res.getUrl());
}
}
}
コンソールでの実行結果です。
3記事分のURLが取得できました。(開いてみるとちゃんと最新の3記事でした)
コードの説明です。
@Scheduled(fixedRate = 10000)
で10秒刻みで取得する設定をしています。確認目的で秒刻みにしていますが、もっと長くていいと思います。(Qiita APIは認証していない状態ではIPアドレスごとに1時間に60回の利用制限があります。2022-12月現在)
メソッドの中ではエンドポイントを定義してgetForObject
で取得します。戻り値のQiitaResponse
クラスは私が定義した以下のようなクラスです。
public class QiitaResponse {
String url; // 記事のURL
Date created_at; //記事の作成日時
public String getUrl() {
return url;
}
public Date getCreated_at() {
return created_at;
}
}
JSONの中で取得したいデータはurlと、後で使いたいのでcreated_atなので、それらのフィールド定義しておきました。created_atがスネークケースだとか突っ込みどころはあると思いますが、自動変換に頼りたいのでこれにしておきます。
ブラウザで記事を開く
記事のURLをコンソールに表示できましたが、クリックして開くのは面倒くさいですね。
そこで、Seleniumにブラウザで開いてもらいましょう。
gradle.buildに以下の依存関係を追加します。
implementation 'org.seleniumhq.selenium:selenium-java:4.7.1'
取得したURLを自動で開くコードが以下です。
先ほどの記載した部分は省略しています。
@Scheduled(fixedRate = 10000)
public void getQiitaArticles() throws Exception {
...(省略)
// ① Selenium初期設定
System.setProperty("webdriver.chrome.driver", "./src/main/resource/chromedriver.exe");
this.driver = new ChromeDriver();
// ② SeleniumでWebページを開く
int i = 0;
for(QiitaResponse res : responses){
// 2個目以外はGETの前にタブを開く(ブラウザ自体がたくさん開かれるのを防ぐため)
if(i > 0){
driver.switchTo().newWindow(WindowType.TAB);
Thread.sleep(500);
}
driver.get(res.getUrl());
Thread.sleep(500);
i++;
}
}
①で、chromedriverをSystemのプロパティにセットします。
ChromeDriverはこちらから入手したものをresource直下に配置しています。
②で、取得したURLを順に見ていき、ブラウザでGETリクエストを送っています。
無事、Qiitaの記事を表示してくれました。(※著作権に触れるとあれなので、画像は私の記事を開いたものに差し替えています)
ここまでで、定期的にQiitaの記事を取得して開くものができました。
びっくりするくらい簡単にできました。
一度開いた記事を再取得をしないようにする
記事を開けたのは良いですが、これだと同じ記事を何度も開いてしまいます。
最後に取得した記事の日時を保存しておくとよさそうです。
こんなのにDBを用意するほどではないので、やり方はいけてないですが、テキストファイルに日時を保存しておくことにします。
特に目新しい技術を使っているとかではないので説明はしません。
最終的なコードを載せておきます(汚くてすみません)
@Component
public class ScheduledTasks {
@Autowired
RestTemplate restTemplate;
Date lastGetAt;
private WebDriver driver;
private EditLastGetAtFile editLastGetAtFile;
private ScheduledTasks(){
// テキストファイルから日時を取得
this.editLastGetAtFile = new EditLastGetAtFile();
// Selenium初期設定
System.setProperty("webdriver.chrome.driver", "./src/main/resource/chromedriver.exe");
this.driver = new ChromeDriver();
}
@Scheduled(fixedRate = 100000)
public void getQiitaArticles() throws Exception {
// 最後に取得した記事の作成日時を取得
lastGetAt = editLastGetAtFile.readLastGetAt();
// エンドポイント定義
String endPoint = "https://qiita.com/api/v2/items?page=1&per_page=3";
// 記事情報を取得
QiitaResponse[] responses = restTemplate.getForObject(endPoint, QiitaResponse[].class);
// 表示させるURLをListに格納
// 表示させるURLは、最後に表示させた記事よりも更新日時があたらしいものとする
List<String> displayURL = new ArrayList<>();
for(int i = 0; i < responses.length; i++){
if(lastGetAt != null && !responses[i].getCreated_at().after(lastGetAt)){
break;
}
displayURL.add(responses[i].getUrl());
}
// SeleniumでWebページを開く
int i = 0;
for(String url : displayURL){
// 最初以外はGETの前にタブを開く(Chromeアプリ自体がたくさん開かれるのを防ぐため)
if(i > 0){
driver.switchTo().newWindow(WindowType.TAB);
Thread.sleep(500);
}
driver.get(url);
Thread.sleep(500);
i++;
}
System.out.println(displayURL);
// テキストファイルの日時を更新
editLastGetAtFile.writeLastGetAt(responses[0].getCreated_at());
}
}
public class EditLastGetAtFile {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd hh:mm:ss");
public Date readLastGetAt() throws Exception{
String strDate = "";
// 文字列取得
try {
File f = new File("./src/main/resource/lastGetAt.txt");
BufferedReader br = new BufferedReader(new FileReader(f));
String line = br.readLine();
while (line != null) {
strDate = line;
line = br.readLine();
}
br.close();
} catch (IOException e) {
System.out.println(e);
}
// 文字列→日付
Date date = sdf.parse(strDate);
return date;
}
public void writeLastGetAt(Date date) throws Exception {
// ファイル書き込み
String newStrDate = sdf.format(date);
try{
File file = new File("./src/main/resource/lastGetAt.txt");
FileWriter filewriter = new FileWriter(file);
filewriter.write(newStrDate);
filewriter.close();
}catch(IOException e){
System.out.println(e);
}
}
}
これで、同じ内容のものを2度開くことがなくなりました。
あとはこのファイルを開発中のSpring Bootプロジェクトに置いておけば、適度に記事を開いてくれるので開発しながらのインプットがはかどりそうです。
おわりに
はじめにがあったらおわりを書くべきだと思って生きているのですが、内容が内容なのであまりにも書くことがありませんでした。
読んでいただきありがとうございました。