【なんでも自動化してみたい🤖③】 ブログやニュースを自動で収集!!! LINE + Heroku + GAS
今まで
【なんでも自動化してみたい🤖1.1】 Twitter ✖️ note ✖️ GAS (Google App Script)
【なんでも自動化してみたい🤖②】 LINE BOT + GAS + Heroku + OpenWeatherMAP API ⛈⛈⛈
ニュース記事やブログを探してほしい!!!💔
ということで実装してみます!
構成は
記事を収集: Heroku x Golang
定期実行: Google App Script
ということで実装していこうと思います。
記事収集を実装していく🔍
とりあえずこの型に変換していくことからです
type Feed struct {
Title, Link, Word, Source string
}
Google Newsを漁る
import (
"fmt"
"net/url"
"github.com/mmcdole/gofeed"
"heroku.dod/domain/interfaces/model"
)
const GOOGLE_NEWS_RSS_URL = "https://news.google.com/rss/search"
func searchGoogleNews(words string) ([]model.Feed, error) {
url := fmt.Sprintf("%s?q=%s&hl=ja&gl=JP&ceid=JP:ja", GOOGLE_NEWS_RSS_URL, url.QueryEscape(words))
feeds := []model.Feed{}
feed, err := gofeed.NewParser().ParseURL(url)
if err != nil {
return nil, err
}
for idx, item := range feed.Items {
if idx > 5 {
break
}
feeds = append(feeds, model.Feed{
Title: item.Title,
Link: item.Link,
Word: words,
Source: "Google News",
})
}
return feeds, nil
}
Noteを漁る
import (
"fmt"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"github.com/PuerkitoBio/goquery"
"github.com/corpix/uarand"
"heroku.dod/domain/interfaces/model"
)
const NOTE_URL = "https://note.com/search?context=note&q=%s&sort=new"
func searchNote(words string) ([]model.Feed, error) {
url := fmt.Sprintf(NOTE_URL, url.QueryEscape(words))
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("User-Agent", uarand.GetRandom())
var mutex sync.Mutex
mutex.Lock()
defer mutex.Unlock()
response, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer response.Body.Close()
doc, err := goquery.NewDocumentFromResponse(response)
if err != nil {
return nil, err
}
feeds := []model.Feed{}
doc.Find("a").Each(func(i int, s *goquery.Selection) {
link, ok := s.Attr("href")
if !ok || !strings.Contains(link, "/n/") {
return
}
text, ok := s.Attr("aria-label")
if !ok || trim(text) == "note" {
return
}
feeds = append(feeds, model.Feed{
Title: trim(text),
Link: trim("https://note.com" + link),
Word: words,
Source: "Note",
})
})
return feeds, nil
}
func trim(text string) string {
return regexp.MustCompile("\n| |\t").ReplaceAllString(text, "")
}
Qiitaを漁る
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"time"
"heroku.dod/domain/interfaces/model"
)
var SearchQiita = searchQiita
func searchQiita(query string) ([]model.Feed, error) {
type Response []struct {
RenderedBody string `json:"rendered_body"`
Body string `json:"body"`
Coediting bool `json:"coediting"`
CommentsCount int `json:"comments_count"`
CreatedAt time.Time `json:"created_at"`
Group interface{} `json:"group"`
ID string `json:"id"`
LikesCount int `json:"likes_count"`
Private bool `json:"private"`
ReactionsCount int `json:"reactions_count"`
Tags []struct {
Name string `json:"name"`
Versions []interface{} `json:"versions"`
} `json:"tags"`
Title string `json:"title"`
UpdatedAt time.Time `json:"updated_at"`
URL string `json:"url"`
User struct {
Description string `json:"description"`
FacebookID string `json:"facebook_id"`
FolloweesCount int `json:"followees_count"`
FollowersCount int `json:"followers_count"`
GithubLoginName interface{} `json:"github_login_name"`
ID string `json:"id"`
ItemsCount int `json:"items_count"`
LinkedinID string `json:"linkedin_id"`
Location string `json:"location"`
Name string `json:"name"`
Organization string `json:"organization"`
PermanentID int `json:"permanent_id"`
ProfileImageURL string `json:"profile_image_url"`
TeamOnly bool `json:"team_only"`
TwitterScreenName interface{} `json:"twitter_screen_name"`
WebsiteURL string `json:"website_url"`
} `json:"user"`
PageViewsCount interface{} `json:"page_views_count"`
TeamMembership interface{} `json:"team_membership"`
}
url := fmt.Sprintf("https://qiita.com/api/v2/items?query=%s", url.QueryEscape(query))
response, err := http.DefaultClient.Get(url)
if err != nil {
return nil, err
}
defer response.Body.Close()
byts, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, err
}
var typedResponse Response
err = json.Unmarshal(byts, &typedResponse)
if err != nil {
return nil, err
}
result := []model.Feed{}
for _, feed := range typedResponse {
result = append(result, model.Feed{
Title: feed.Title,
Link: feed.URL,
Word: query,
Source: "Qiita",
})
}
return result, nil
}
非同期で一気に探しまくる
import (
"context"
"sync"
"heroku.dod/domain/interfaces"
"heroku.dod/domain/interfaces/model"
)
type feed struct{}
func NewFeeder() interfaces.FeedDataSource {
return new(feed)
}
func (*feed) Search(ctx context.Context, words string) ([]model.Feed, error) {
wg := &sync.WaitGroup{}
result := []model.Feed{}
fns := [...](func(string) ([]model.Feed, error)){
searchGoogleNews,
searchNote,
searchQiita,
}
for _, fn := range fns {
fn := fn
wg.Add(1)
go func() {
defer wg.Done()
f1, err := fn(words)
if err != nil {
log.Warn(err.Error())
return
}
result = append(result, f1...)
}()
}
wg.Wait()
return result, nil
}
定期実行📅
Herokuにアクセス
/**
* @param {string} baseUrl
* @param {string} apikey
* @return {HerokuDataSource}
*/
function newHeroku(baseUrl, apikey) {
return new HerokuDataSource(baseUrl, apikey)
}
class HerokuDataSource {
constructor(baseUrl, apikey) {
this._baseUrl = baseUrl;
this.apikey = apikey;
}
searchArticles(words) {
const headers = {
'x-api-key': this.apikey,
};
const body ={
words,
}
return Http.toJson(Http.post(`${this._baseUrl}/search/articles`, {
headers, body: JSON.stringify(body),
}))
}
}
Lineに通知したい
const APIEndpointBase = "https://api.line.me";
const APIEndpointPushMessage = "/v2/bot/message/push";
/**
* @param {string} token
* @param {string} targetGroupId
* @return {LineStreamer}
*/
function newLine(token, targetGroupId) {
return new LineStreamer(token, targetGroupId);
}
class LineStreamer {
constructor(token, targetGroupId) {
this.token = token;
this.groupId = targetGroupId;
}
/**
* @param {string} text
* @param {string} to
*/
publishTextMessage(text, to) {
const headers = {
Authorization : `Bearer ${this.token}`,
"Content-Type" : "application/json; charset=UTF-8",
};
const body = JSON.stringify({
to : to || this.groupId,
messages : [{ type : 'text', text }],
});
return Http.post(`${APIEndpointBase}${APIEndpointPushMessage}`, { body, headers });
}
}
UseCaseをまとめる
notifyArticle(heroku, line, db) {
const linkSet = new Set();
const isDuplicate = (value) => {
const res = linkSet.has(value);
linkSet.add(value);
return res;
};
const words = [
"駆け出しエンジニア",
"ここにワードを追加していく",
"ここにワードを追加していく",
"ここにワードを追加していく",
"ここにワードを追加していく",
]
.sort(() => Math.random() - 0.5)
.slice(0, 5);
const articles = heroku.searchArticles(words)
.filter(article => !isDuplicate(article.link) && !links.includes(article.link))
.sort(() => Math.random() - 0.5)
.slice(0, 15);
const body = articles.map(article => `【${article.word}・${article.source}】${article.title}\n${article.link}\n`).join("\n");
if (body) line.publishTextMessage("今日のお勧め記事\n\n" + body);
},
まとめ✅
これで自動的に毎日朝昼晩記事を探してくれています!
(実際はスプレットシートに記録して同じ記事はお勧めしないようにしていますが、今回は複雑なので紹介しておりません🙇♂️)
とにかく今回の自動化で、情報収集が楽になりました!?
次回は、ラズパイのカメラで監視カメラあたりやりたいかなと思っております🙇♂️