20210915のNode.jsに関する記事は4件です。

Node.jsで簡単♪スクレイピング

どうも、プログラミングは料理に似ている 白金御行<プラチナ☆みゆき>です。 ということで今回は料理レシピみたいなテンションでNode.jsのスクレイピング手順を記述していきます。 参考に本稿ではCookpadさんのtech blogの記事をスクレイピングしてJSON形式で書き出すものを作っていきましょう。 また、本稿はN番煎じ記事なので新規性はなく、主に備忘用の記事になります。 材料 言語:Node.js(v12.19.0) HTTPクライアントライブラリ:node-fetch HTMLパーサライブラリ:jsdom ファイル操作ライブラリ:fs 作り方 まず結論からということでCode全文を以下に記載して、後に個々の説明に入ります。 import fetch from 'node-fetch'; import jsdom from 'jsdom'; import fs from 'fs'; const { JSDOM } = jsdom; // HTTPリクエストからDOM要素を取得 async function getArchive(url, year) { const res = await fetch(url+String(year)); // resが返るまでwait const html = await res.text(); // htmlが返るまでwait const dom = new JSDOM(html); const document = dom.window.document; const titles = document.querySelectorAll('.entry-title-link'); const dates = document.querySelectorAll('time'); for (let i=0; i < titles.length; i++) { let reDate = dates[i].textContent.replace(/\s/g, ''); nodeArray.push({ url: titles[i].href, title: titles[i].textContent, date: reDate, }) }; }; // write to JSON file const writeJson = function(data, filename) { console.log('write start'); fs.appendFile('data/'+filename+'.json', JSON.stringify(data, null, ' '), (error) => { console.log('write end'); }); }; // wait const sleep = () => new Promise(resolve => { setTimeout(() => { resolve() }, 5000) }); // run let nodeArray = []; let url = 'https://techlife.cookpad.com/archive/'; (async function(filename) { for (let year=2021; year>2007; year--) { await getArchive(url, year); await sleep(); console.log(year); } writeJson(nodeArray, filename); })('cookpad'); moduleのimport import fetch from 'node-fetch'; import jsdom from 'jsdom'; import fs from 'fs'; const { JSDOM } = jsdom; まず必要なmoduleをimport文でimportしていきます。次にJSDOMというHTMLパーサのコンストラクタを生成しておくことがポイントです。 スクレイピング関数を用意 // HTTPリクエストからDOM要素を取得 async function getArchive(url, year) { const res = await fetch(url+String(year)); // resが返るまでwait const html = await res.text(); // htmlが返るまでwait const dom = new JSDOM(html); const document = dom.window.document; // native JSと同じように処理 const titles = document.querySelectorAll('.entry-title-link'); const dates = document.querySelectorAll('time'); for (let i=0; i < titles.length; i++) { let reDate = dates[i].textContent.replace(/\s/g, ''); nodeArray.push({ url: titles[i].href, title: titles[i].textContent, date: reDate, }) }; }; getArchiveというHTML要素をスクレイピングする関数の定義部分ですが、async/await文によって非同期処理とすることがポイントです。JavaScriptでメモリを長時間使う処理は順次実行を無視して先々処理が進んでしまいます。 それを防止するためにWebサーバからデータをfetchする処理とHTML本文を抜き出す処理にはawait文を付けることによって後続の処理を待機させています。 後は先ほど作ったJSDOMコンストラクタでdomインスタンスを生成。 タイトルと記事URL、作成日時が欲しいのでそれらのnodeをquerySelectorAllで取得しています。 次に、for文中でnodeArray(あとで作る)に次々とObject形式のデータを放り込んでいってます。 JSON形式で出力 // write const writeJson = function(data, filename) { console.log('write start'); fs.appendFile('data/'+filename+'.json', JSON.stringify(data, null, ' '), (error) => { console.log('write end'); }); }; 書き込むデータ(data)と書き出すファイル名(filename)を引数に取る関数を作成しています。 fs.appendFileの第3引数にはコールバック関数が必須という点がポイントです。 待機 // wait const sleep = () => new Promise(resolve => { setTimeout(() => { resolve() }, 5000) }); 連続でのリクエストはサーバに負荷をかけるので1リクエストにつき5秒待機する関数を定義しています。 実行 // run let nodeArray = []; let url = 'https://techlife.cookpad.com/archive/'; (async function(filename) { for (let year=2021; year>2007; year--) { await getArchive(url, year); await sleep(); console.log(year); } writeJson(nodeArray, filename); })('cookpad'); 最後に実行部です。非同期処理でスクレイピングと待機を望む年数分実行し、最後にJSON形式でファイルに書き出しています。 結果 { "url": "https://techlife.cookpad.com/entry/2021/09/06/130000", "title": "Cookpad Summer Internship 2021 10 Day Techコースを開催しました!", "date": "2021-09-06" }, { "url": "https://techlife.cookpad.com/entry/2021/08/24/175828", "title": "AWSフル活用!クッキングLiveアプリ「cookpadLive」を支える技術", "date": "2021-08-24" }, { "url": "https://techlife.cookpad.com/entry/2021/08/19/100000", "title": "レガシーとなった TLS 1.0/1.1 廃止までの道のり", "date": "2021-08-19" }, 結果の一部ですが、このようにデータが正常に取得、JSON形式で出力できていることがわかりますね! おわりに 私はいつもサーバサイドにはPythonを使っていますがNode.jsでも問題なくスクレイピングできることがわかりました。非同期処理の形式で書く必要があるのがNode.jsの特殊なところかと思いますので詰まった方は以下の参考をお読みください。 参考 Node.js でお手軽スクレイピング 2020 年夏 async/await 入門(JavaScript) Node.js でファイルを保存する方法
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

playwrightでブラウジングの自動化

前回puppeteerの記事を書いて、使っていたらplaywrightを見つけました。コマンドも同じで、こちらの方が複数ブラウザー対応など便利そうだったのでサクッと切り替えました。python用などもあるようです。 Dockerファイル DockerFile FROM mcr.microsoft.com/playwright:focal WORKDIR /app ...ubuntuベースなので少し大きいですが、公式docker対応は楽ですね。日本語も問題ありませんでした。 サンプル yahoo.js const playwright = require('playwright') ;(async () => { const browser = await playwright.chromium.launch() const context = await browser.newContext() const page = await context.newPage() await page.goto('https://www.yahoo.co.jp/') await page.screenshot({ path: `yahoo.png` }) await browser.close() })() 参考
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

LINEのチャットボットでタスクマネージャーを作ってみた!

作成したタスクマネージャーbot 完成したチャットボットです! 使用した技術 1.Node.js 2.firebase 3.Messaging API 4.express 事前準備 firebaseのプロジェクトを作成する ↓↓ここ大事 プロジェクトのプランをBlazeプラン(従量制)に変更 無料のsparkプランでは外部APIを実行できないってさ、、、 Blazeプランは使った分だけ支払うという形で、無料枠があるので 個人で動かすぐらいなら無料枠で十分なのでほぼお金はかからないと思って大丈夫です!! 開発環境構築 こちらを参考に進めました。 ここでは上記サイトを参考に簡単に書いていきます。 1. Firebase CLIのインストール npm i -g firebase-tools 2. Firebase CLIでfirebaseにログイン firebase login 3. Firebaseの初期化(プロジェクトの作成) firebase init functions 4. LINEbotSDKとExpressをインストール cd functions でfunctionsディレクトリに移動して npm i --save @line/bot-sdk express 作成開始! ここからは自分が作成したチャットボットで、重要な部分を切り抜いて書いていきます! 送信された内容をFirestoreに登録してみる index.js 'use strict'; const functions = require('firebase-functions'); const express = require('express'); const line = require('@line/bot-sdk'); const API = require('./api'); // LINE bot const config = { channelSecret: functions.config().line.secret , channelAccessToken: functions.config().line.token, }; const client = new line.Client(config); const app = express(); app.post('/', line.middleware(config), (req, res) => { res.sendStatus(200); Promise.all(req.body.events.map(handleEvent)) .then(() => res.status(200).end()) .catch((err) => { console.error(err); res.status(500).end() }) Promise.all(req.body.events.map(post)) .then(() => res.status(200).end()) .catch((err) => { console.error(err); res.status(500).end() }) }); async function handleEvent(event) { API.addTodo(event.message.text, event.source.userId); if (event.type === "message" && event.message.type === "text") { return client.replyMessage( event.replyToken, { type: "text", text: event.message.text }) } } async function post(event) { return API(event.message.text, event.source.userId); } exports.app = functions.region("asia-northeast1").https.onRequest(app); ※channelSecretとchannelAccessTokenはfirebaseの環境変数で設定しています。方法はこちら 送信されたテキストと送信してきたユーザーのIDを引数として、 下記addTodoメソッドを呼び出しています。 api.js const functions = require('firebase-functions'); const admin = require('firebase-admin'); admin.initializeApp(); const db = admin.firestore(); exports.addTodo = function(content, uid){ db.collection("todos").add({ todoName: content, uid: uid, }) } 新たにapi.jsを作成し、firestoreにデータを登録する処理を記述します。 todoNameとuidの2つのフィールドを登録している処理です。 登録したデータをボタンテンプレートで表示する ボタンテンプレート・・・ MessagingAPIで用意されているメッセージタイプの一種で、 画像、タイトル、テキストに加えてアクションボタンが含まれたメッセージのこと。 こんな感じ↓↓↓ LINE Developers https://developers.line.biz/ja/docs/messaging-api/message-types/#template-messages(参照2021-09-05) index.js/一部抜粋 if(event.message.text == "@show"){ let todos = await API.getTodo(event.source.userId); let list = await API.join(todos); return client.replyMessage(event.replyToken, list); @showと送信されるとgetTodoとjoinメソッドが走ります。 javaScript:api.js/getTodoメソッド,joinメソッド module.exports.getTodo = async (uid) => { const todo = await db.collection("todos") .where("userId", "==", uid); //uidが一致するもの指定 return todo.get().then((snapshot) => { let todos = []; snapshot.forEach((doc)=>{ todos.push({ id: doc.id, todoName: doc.data().todoName, deadLine: doc.data().deadLine, isComplete: doc.data().isComplete, }); }); return todos; }); } //データ整形 module.exports.join = (datas) => { let data = []; for(var i =0; i< datas.length; i++){ data.push( { "type": "template", "altText": "this is a buttons template", "template": { "type": "buttons", "actions": [ {//ポストバックでtodoの情報を返す "type": "postback", "label": "完了", "data" : JSON.stringify(datas[i]), //文字列 }, ], "title" : datas[i].todoName, "text" : "期日 : " + datas[i].deadLine, } }, ) } return data; } getTodoメソッドではuserIdが一致するドキュメントのみを指定してデータを取り出しています。 joinメソッドではボタンテンプレートとして送信するために一つ一つのタスクのデータを整形しています。 それらの整形されたタスクデータを配列に格納し、index.jsで応答メッセージとして送信する感じです! 完了ボタンが押された javaScript:index.js/タスク完了の処理 if(event.type === "postback"){ //タスクが完了した const data = JSON.parse(event.postback.data); //オブジェクトデータに変換 await API.deleteTodo(data.id); //todo削除 return client.replyMessage(event.replyToken , { type :"text", text : "お疲れさまです!\n" + data.todoName + "が完了しました。", },); } api.js/deleteTodoメソッド module.exports.deleteTodo = async (id) => { await db.collection("todos").doc(id).delete(); } ボタンテンプレートのアクションボタンが押されるとポストバックで情報を返すようにしているので(api.jsのjoinメソッドを見てください)、早期リターンでevent.typeがpostbackの時の処理を記述しています。 postbackデータをjavascriptで扱えるようにJSON.parseメソッドでJSONオブジェクトに変換しています。 postbackイベントの中身 そして、api.jsで記述したfirestoreに登録したタスクを消去する処理を呼び出しています! 正規表現 締め切り日を設定する際に mm/dd か mm月dd日 (mm、ddは数字) の形で送信された時のみ登録できるようにしています。 バリデーションチェックに正規表現を用いています。 参考にさせてもらったサイト api.js/checkDateメソッド exports.checkDate = (date) => { if(!date.match(/^\d{1,2}(\/|月)\d{1,2}($|日)/)){ // 〇〇/〇〇か〇〇月〇〇日の形のみ通過 return "error"; } let str = date.split(/\/|月|日/); // "/"か"月"か"日"で区切る let month = parseInt(str[0], 10); let day = parseInt(str[1], 10); if( month < 1 || 12 < month || day < 1 || 31 < day ){ return "error"; } let due = str[0] + "月" + str[1] + "日"; return due; } まとめ ざっと実装した内容について説明しましたが、間違ってたり、こーいう風に書いた方がいいんじゃない?みたいなことあれば是非ともコメントおねがいします!! また、こんな機能あったら便利だね!とかあればドシドシコメントお願いします!!! 実装予定機能 現在インスタを使って友達に使ってもらい、フィードバックをもらっています! 参考文献 https://developers.line.biz/ja/docs/messaging-api/ https://qiita.com/n0bisuke/items/909881c8866e3f2ca642 https://murashun.jp/article/programming/regular-expression.html
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

GraphQL + Sequelize でテーブル定義やクエリの実装などをちょっとだけ簡略化する方法

はじめに 業務でGraphQLを使うことになったんですが、GraphQLのモジュール宣言って面倒くさいですよね。 こういうの。 server/modules.js const typeDefs = gql` type User { id: Int! name: String! email: String! recipes: [Recipe!]! } type Recipe { id: Int! title: String! ingredients: String! direction: String! user: User! } type Query { user(id: Int!): User allRecipes: [Recipe!]! recipe(id: Int!): Recipe } type Mutation { createUser(name: String!, email: String!, password: String!): User! createRecipe(userId: Int!, title: String!, ingredients: String!, direction: String!): Recipe! } ` const resolvers = { Query: { async user(root, { id }, { models }) { return models.User.findByPk(id) }, async allRecipes(root, args, { models }) { return models.Recipe.findAll() }, async recipe(root, { id }, { models }) { return models.Recipe.findByPk(id) } }, Mutation: { async createUser(root, { name, email, password }, { models }) { return models.User.create({ name, email, password: await bcrypt.hash(password, 10), }) }, async createRecipe(root, { userId, title, ingredients, direction }, { models }) { return models.Recipe.create({ userId, title, ingredients, direction }) } }, User: { async recipes(user) { return user.getRecipes() }, }, Recipe: { async user(recipe) { return recipe.getUser() }, }, } DBのテーブルをひとつ追加するのに修正箇所が多すぎると感じたので、簡略化するためのモジュールクラスを作ってみました。 もしかしたら、もっといい方法や似たようなライブラリがあるかもしれませんが、とりあえず、一例として共有します。 環境構築から説明しますが、不要な方は読み飛ばしてください。 リポジトリ この記事に記載のコードは上記リポジトリに格納されています。 環境等はこちらをご参照ください。 環境構築 開発環境はWindowsで、WSL上にリポジトリを置き、さらにNode環境を構築したDockerコンテナ上で作業しています。 ただ、今回の内容にはほぼ関係ないので「Linux上で作業している」くらいにとらえてもらえれば良いかと。 こちらの環境を前提に説明していきます。 ちなみにNode.jsはインストール済みの体でよろしく。 こちらのサイトを参考にセットアップしていきます。 使用するDBは参考サイトと同じくSQLiteを使用します。 Sequelize関数の修正 一部Sequelizeのバージョンアップの影響で関数を変更する必要があります。 resolversのQueryの中で実行しているfindByIdをfindByPkに変更しましょう。 async user(root, { id }, { models }) { return models.User.findById(id) } ↓ async user(root, { id }, { models }) { return models.User.findByPk(id) } DBファイルの生成 DBのファイルはプッシュしていませんので、サンプルコードをクローンした場合は、以下のコマンドを実行してください。 DBファイルを生成。 touch ./database/database.sqlite DBをマイグレートします。 npx sequelize db:migrate Playgroundの設定 dockerコンテナ上で実行する場合、Playgroundが動作しない場合があります。 そんな時はsrc/index.jsを以下のように修正しましょう。 src/index.js const { ApolloServer } = require('apollo-server') const { typeDefs, resolvers } = require('../server/modules') const models = require('../models') // playgroundをロード const { ApolloServerPluginLandingPageGraphQLPlayground } = require('apollo-server-core') const server = new ApolloServer({ typeDefs, resolvers, context: { models }, plugins: [ApolloServerPluginLandingPageGraphQLPlayground({})], // playgroundを設定する }) server.listen({ port: 3000, }).then(({ url }) => console.log('Server is running on localhost:3000')) 動作確認 サーバを起動して、Playgroundが起動したら成功です。 GraphQLモジュール モジュール部分はserver/modules.jsという1ファイルにまとめています。 リポジトリではEnvironment-construction-completedタグを付けてますが、ここにも全文を載せてみます。 server/modules.js const { gql } = require('apollo-server') const bcrypt = require('bcryptjs') const typeDefs = gql` type User { id: Int! name: String! email: String! recipes: [Recipe!]! } type Recipe { id: Int! title: String! ingredients: String! direction: String! user: User! } type Query { user(id: Int!): User allRecipes: [Recipe!]! recipe(id: Int!): Recipe } type Mutation { createUser(name: String!, email: String!, password: String!): User! createRecipe(userId: Int!, title: String!, ingredients: String!, direction: String!): Recipe! } ` const resolvers = { Query: { async user(root, { id }, { models }) { return models.User.findByPk(id) }, async allRecipes(root, args, { models }) { return models.Recipe.findAll() }, async recipe(root, { id }, { models }) { return models.Recipe.findByPk(id) } }, Mutation: { async createUser(root, { name, email, password }, { models }) { return models.User.create({ name, email, password: await bcrypt.hash(password, 10), }) }, async createRecipe(root, { userId, title, ingredients, direction }, { models }) { return models.Recipe.create({ userId, title, ingredients, direction }) } }, User: { async recipes(user) { return user.getRecipes() }, }, Recipe: { async user(recipe) { return recipe.getUser() }, }, } module.exports = { typeDefs, resolvers } このファイル内には、DBのUserテーブルとRecipeテーブルの情報が書いてあります。 DBに新たなテーブルを追加した場合、このファイルに追加することになりますが、複数個所に手を加えなければいけないので少々面倒です。 また、すべてのテーブルの情報が1ファイルに混在しているのも混乱の素になります。 そこで、テーブルごとにクラス化することを考えてみようと思います。 今回は色々試し試し実装していったので、基本クラスとかは作ってないです。 同じことを試そうとする方がいらっしゃれば、基本クラスに切り出したりしてみてください。 Userテーブル まずはUserテーブル関連の処理をモジュールクラス化します。 コード全文載せちゃいます。 server/modules/UserModule.js const bcrypt = require('bcryptjs') class UserModule { get apiType() { const result = []; result.push(` type User { id: Int! name: String! email: String! recipes: [Recipe!]! } `); return result.join('\n'); } get queryType() { const result = []; result.push(`user(id: Int!): User`); return result.join('\n'); } get queryList() { const result = []; result.push(this.user); return Object.fromEntries(result.map((func) => [func.name, func])); } get mutationType() { const result = []; result.push(`createUser(name: String!, email: String!, password: String!): User!`); return result.join('\n'); } get mutationList() { const result = []; result.push(this.createUser); return Object.fromEntries(result.map((func) => [func.name, func])); } get otherResolver() { return { User: { async recipes(user) { return user.getRecipes() }, }, }; } async user(root, { id }, { models }) { return models.User.findByPk(id); } async createUser(root, { name, email, password }, { models }) { return models.User.create({ name, email, password: await bcrypt.hash(password, 10), }); } } module.exports = UserModule; ひとつひとつ説明します。 API宣言 API(型)宣言部です。 get apiType() { const result = []; result.push(` type User { id: Int! name: String! email: String! recipes: [Recipe!]! } `); return result.join('\n'); } 基本的には文字列を返します。 InputTypesなどを追加したい時はresult.push()を追加することで対応可能です。 result.push(` input ReviewInput { stars: Int! commentary: String } `); Query Query部はふたつの関数に分かれています。 Queryの実体はクラスのメンバ関数として実装します。 get queryType() { const result = []; result.push(`user(id: Int!): User`); return result.join('\n'); } get queryList() { const result = []; result.push(this.user); return Object.fromEntries(result.map((func) => [func.name, func])); } : 中略 : async user(root, { id }, { models }) { return models.User.findByPk(id); } 追加したい場合は、メンバ関数としてQueryの実体を実装した後、いずれもresult.push()を追記するだけでよいです。 Mutation Mutation部も基本的にQueryと同様です。 get mutationType() { const result = []; result.push(`createUser(name: String!, email: String!, password: String!): User!`); return result.join('\n'); } get mutationList() { const result = []; result.push(this.createUser); return Object.fromEntries(result.map((func) => [func.name, func])); } : 中略 : async createUser(root, { name, email, password }, { models }) { return models.User.create({ name, email, password: await bcrypt.hash(password, 10), }); } Resolver 追加のResolverを作りたいときは、関数を持ったオブジェクトを返すようにします。 get otherResolver() { return { User: { async recipes(user) { return user.getRecipes() }, }, }; } Recipeテーブル Userテーブルと同様にRecipeテーブルも新しいファイルを作成してモジュールクラスを実装します。 server/modules/RecipeModule.js class RecipeModule { get apiType() { const result = []; result.push(` type Recipe { id: Int! title: String! ingredients: String! direction: String! user: User! } `); return result.join('\n'); } get queryType() { const result = []; result.push('allRecipes: [Recipe!]!'); result.push('recipe(id: Int!): Recipe'); return result.join('\n'); } get queryList() { const result = []; result.push(this.allRecipes); result.push(this.recipe); return Object.fromEntries(result.map((func) => [func.name, func])); } get mutationType() { const result = []; result.push('createRecipe(userId: Int!, title: String!, ingredients: String!, direction: String!): Recipe!'); return result.join('\n'); } get mutationList() { const result = []; result.push(this.createRecipe); return Object.fromEntries(result.map((func) => [func.name, func])); } get otherResolver() { return { Recipe: { async user(recipe) { return recipe.getUser() }, }, }; } async allRecipes(root, args, { models }) { return models.Recipe.findAll() } async recipe(root, { id }, { models }) { return models.Recipe.findByPk(id) } async createRecipe(root, { userId, title, ingredients, direction }, { models }) { return models.Recipe.create({ userId, title, ingredients, direction }) } } module.exports = RecipeModule; モジュールファイル 作成したモジュールクラスをGraphQLに渡します。 多態性を持たせているので、配列にインスタンスを作成してぶん回します。 server/modules.js const { gql } = require('apollo-server') const UserModule = require('./modules/UserModule') const RecipeModule = require('./modules/RecipeModule') const moduleList = [ new UserModule(), new RecipeModule(), ] const typeDefs = gql` ${moduleList.map((m) => m.apiType).join('\n')} type Query { ${moduleList.map((m) => m.queryType).join('\n')} } type Mutation { ${moduleList.map((m) => m.mutationType).join('\n')} } ` const resolvers = { Query: { ...Object.fromEntries( moduleList.map((m) => Object.entries(m.queryList)).flat() ), }, Mutation: { ...Object.fromEntries( moduleList.map((m) => Object.entries(m.mutationList)).flat() ), }, ...Object.fromEntries( moduleList.map((m) => Object.entries(m.otherResolver)).flat() ), } module.exports = { typeDefs, resolvers } おわりに 以上が私が行った簡略化方法になります。 ライブラリを使用したり、デザインパターンを用いたとかではなく、小手先の方法に始終していますが、DBのテーブルを追加したり、修正したりするのがわかりやすくなりました。 もうちょっと頑張ればTypeScript化したり、さらに簡略化してライブラリ化とかも可能だと思いますが需要はあるんでしょうかね。 もっと簡単な方法があるぜとか、便利ライブラリがあるぜみたいな情報があれば教えてください。 参考URL https://github.com/apollographql/apollo-server https://www.digitalocean.com/community/tutorials/how-to-set-up-a-graphql-server-in-node-js-with-apollo-server-and-sequelize
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む