フリーランス 技術調査ブログ

フリーランス/エンジニア Ruby Python Nodejs Vuejs React Dockerなどの調査技術調査の備忘録

Nuxtjs+ExpressでPDF作成及びダウンロード

はじめに

  • Nuxtjs+Expressで構築したシステムでP DFの作成及びダウンロードする機能の実装をする必要があり、技術的に調査した結果を下記にまとめました。

NodejsでPDFを生成できるライブラリ

  • NodejsでPDFを生成できるライブラリをいくつかピックアップしてみて、スター数が多い「pdfmake」を利用してPDFを生成してみることにします。個人的にはレイアウトを簡単に作成できる、node-html-pdfが好きです。※使ったことはありませんが。
- pdfmake [STAR数: 9.8k]
https://github.com/bpampuch/pdfmake

- pdfkit [STAR数: 7.7k]
https://github.com/foliojs/pdfkit

- node-html-pdf [STAR数: 3.4k]
https://github.com/marcbachmann/node-html-pdf/releases

パッケージのインストール

  • 下記のパッケージをインストールする。
yarn add pdfmake --save
yarn add pdfmake-unicode --save
yarn add @types/pdfmake --save-dev

日本語対応するためにフリーフォントをダウンロードする

  • 下記のサイトからフリーフォントをダウンロードして、ダウンロードしたフォントファイルを「assets」フォルダにファイルを格納する jikasei.me

f:id:PX-WING:20211228235836p:plain

Express(バックエンド側の処理)

  • API側でPDFを作成する。
import PdfPrinter from "pdfmake";
import fs from "fs";

export async function postCreatePdf(req: Request, res: Response) {
  const params = req.body

  // PDFファイルの生成
  createPDF(params).then(function(value) {

    // 5秒後にファイルをダウンロードする処理をする。
    // 理由はPDFファイルを生成して直後にダウンロードすると、ファイルが壊れた状態でダウンロードされてしまうため、
 // 少し時間をおいてからダウンロードする様にする
    setTimeout(async function() {

      // ブラウザ表示
      //var data = fs.readFileSync("/<YourAppRoot>/sample.pdf");
      //res.contentType("application/pdf");
      //res.send(data);

      // ファイルダウンロード
      res.download("/<YourAppRoot>/sample.pdf");

    }, 5000);
  });
}

// PDFファイル生成処理
async function createPDF(searchAccoutIdParams: any) {
  return new Promise(async function(resolve, reject) {
    try {

      // アセットフォルダに設置したフォントサイズを読み込む
      const fonts = {
        GenShinGothic: {
          normal: '/<YourAppRoot>/assets/fonts/GenShinGothic/GenShinGothic-Normal.ttf',
          bold: '/<YourAppRoot>/assets/fonts/GenShinGothic/GenShinGothic-Normal.ttf',
        },
      };
      
      const PdfPrinter = require('pdfmake');
      const printer = new PdfPrinter(fonts);
      const fs = require('fs');

      // PDFファイルのレイアウトを指定する
      const docDefinition = {
        content: [
          { text: 'こちらはサンプルです。', style: 'title' },
          {
            table: {
              headerRows: 1,
              body: [
                ['Header 1', 'Header 2', 'Header 3'],
                ['Sample value 1', 'Sample value 2', 'Sample value 3'],
                ['Sample value 1', 'Sample value 2', 'Sample value 3'],
                ['Sample value 1', 'Sample value 2', 'Sample value 3'],
                ['Sample value 1', 'Sample value 2', 'Sample value 3'],
                ['Sample value 1', 'Sample value 2', 'Sample value 3'],
              ]
            },
            layout: {
              hLineStyle: function (i: any, node: any) {
                if (i === 0 || i === node.table.body.length) {
                  return null;
                }
                return {dash: {length: 10, space: 4}};
              },
              vLineStyle: function (i: any, node: any) {
                if (i === 0 || i === node.table.widths.length) {
                  return null;
                }
                return {dash: {length: 4}};
              },
            }
          },      
          {
          layout: 'lightHorizontalLines', // optional
          table: {
            // headers are automatically repeated if the table spans over multiple pages
            // you can declare how many rows should be treated as headers
            headerRows: 1,
            widths: [ '*', 'auto', 100, '*' ],
    
            body: [
              [ 'First', 'Second', 'Third', 'The last one' ],
              [ 'Value 1', 'Value 2', 'Value 3', 'Value 4' ],
              [ { text: 'Bold value', bold: true }, 'Val 2', 'Val 3', 'Val 4' ]
            ]
          }}    
        ],
        styles: {
          h1: {
            font: 'GenShinGothic',
            fontSize: 18,
            bold: true
          },
          style2: {
            alignment: 'right',
            color: 'blue',
          }
        },
        defaultStyle: {
          font: 'GenShinGothic',
          fontSize: 14,
        }
      };

      // PDFファイルの作成
      // <YourAPPRoot>/sample.pdfファイルが作成される
      const pdfDoc = printer.createPdfKitDocument(docDefinition);
      pdfDoc.pipe(fs.createWriteStream('sample.pdf'));
      pdfDoc.end();  
      resolve(true)
    } catch (e) {
      reject(false)
    }
  });
}

フロント側(Nuxtjts)

  • axiosでExpressのAPIを呼び出す
    async createPdf() {
      // responseTypeを指定しないとうまくPDFファイルをダウンロードできません。
      // createメソッドで一度、responseTypeを指定したインスタンスを作成する
      const axios = this.$axios.create({
        'responseType': 'blob',
      });
 
      axios.post('/admin/create-pdf', { userId: 100}).then(response => {
          const blob = new Blob([response.data], { type: "application/pdf" });
          const url = (window.URL || window.webkitURL).createObjectURL(blob);
          const a = document.createElement("a");
          a.href = url;
          a.download = "test.pdf";
          document.body.appendChild(a);
          a.click();
          document.body.removeChild(a);
      })
    },

実行結果

  • 上記の処理を実行した時に生成されたPDFファイルのサンプルは下記のようになります。 f:id:PX-WING:20211229001217p:plain

今回参考にさせて頂いた記事

https://stackoverflow.com/questions/52817280/problem-downloading-a-pdf-blob-in-javascript
https://blog.kozakana.net/2018/04/express-csv-download/
https://nodejs.keicode.com/nodejs/how-to-create-pdf-file.php
https://hapicode.com/javascript/pdfmake.html#npm-%E3%81%8B%E3%82%89%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B%E5%A0%B4%E5%90%88
https://stackoverflow.com/questions/31105846/how-to-send-a-pdf-file-from-node-express-app-to-the-browser
https://qiita.com/akamushi/items/661397c297c2e83acc73
https://github.com/bpampuch/pdfmake/issues/724

問題点

  • PDFファイルがちゃんと生成されるまで5秒で問題ないのか、複雑なPDFファイルを生成したときに5秒以上、待ってからダウンロードしないといけないのか検証が必要である。
  • pdfmakeでどこまで複雑なレイアウトが実現可能なのか調査が必要
  • pdfmakeで作成したPDFがサーバーに残ってしまうので、削除するタイミングをどうするか検証する必要がある

flutterのスプラッシュ画面

インストール

flutter pub add flutter_native_splash
  • pubspec.yamlに次の設定を追加する。画像はpngを指定する必要がある。
flutter_native_splash:
  image: "images/sample.png"
  color: "FAF3F3"

スプラッシュ画面の作成

flutter pub run flutter_native_splash:create

スプラッシュの設定を変更する場合

flutter clean
flutter pub get
flutter pub run flutter_native_splash:create

実機で検証する方法(Android

  • プロジェクトのrootディレクトリで下記のコマンドを実行する
flutter build apk
  • プロジェクトの下記のフォルダにapp-release.apkファイルがあるので、そちらをメールなどで検証端末に送信しテストする。
 build/app/outputs/apk/release/app-release.apk

flutterで画面遷移とパラメータの受け渡しをする

はじめに

  • 久しぶりにflutterの勉強を再開してみる。

flutter doctorでエラーの解消

  • 久しぶりに動かそうとしたら、flutter doctorでエラーになった
C:\Users\user>flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[√] Flutter (Channel stable, 2.5.3, on Microsoft Windows [Version 10.0.19043.1415], locale ja-JP)
[!] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
    X cmdline-tools component is missing
      Run `path/to/sdkmanager --install "cmdline-tools;latest"`
      See https://developer.android.com/studio/command-line for more details.
    X Android license status unknown.
      Run `flutter doctor --android-licenses` to accept the SDK licenses.
      See https://flutter.dev/docs/get-started/install/windows#android-setup for more details.
[√] Chrome - develop for the web
[√] Android Studio (version 2020.3)
[√] VS Code (version 1.62.3)
[√] Connected device (2 available)

! Doctor found issues in 1 category.

cmdline-tools component is missingエラー

  • 下記の赤枠の箇所にチェックを入れて Applyボタンをクリックすることで解消できました。 f:id:PX-WING:20211223084412p:plain

Android license status unknown.エラー

  • 下記のコマンドを実行するとことでエラーを解消することが出来ました。
 flutter doctor --android-licenses

画面遷移するサンプル

  • トップ画面
import 'dart:ui';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:winlogic/next_page.dart';

// よいサンプル
// https://github.com/kenta-wakasa/riverpod_sample

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'トップページ'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);
  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  String _text ="";

  void _incrementCounter() {
    setState(() {
      _counter++;
    });
  }

  void _changeText(input) {
    print(input);
    setState(() {
      _text = input;
    });
  }

  final ButtonStyle style =
  ElevatedButton.styleFrom(textStyle: const TextStyle(fontSize: 20));

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
        actions: <Widget>[Icon(Icons.add), Icon(Icons.share)],
      ),
      body: Container(
          width: double.infinity,
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            crossAxisAlignment: CrossAxisAlignment.center,
            children: [
              //Image.network('https://flutter.github.io/assets-for-api-docs/assets/widgets/owl.jpg'),
              Image.asset('images/sample.jpg', height: 200),
              Icon(Icons.holiday_village,size:30),
              Text(_text),
              Text("テストテスト",
                style: TextStyle(
                  fontSize: 40,
                  color: Colors.green,
                  fontWeight: FontWeight.w900,
                  fontStyle: FontStyle.italic,
                )
              ),
              Text("サンプルテキスト"),
              ElevatedButton(
                  child: Text("次へ"),
                  onPressed: () async {
                    final result = await Navigator.push(
                        context,
                        MaterialPageRoute(builder: (context) => NextPage('1ページ目から渡すパラメータ'))
                    );
                    _text = result;
                    _changeText(result);
                    print(_text);
                  }
              )
            ],
          )
      ),
      floatingActionButton: FloatingActionButton(
      onPressed: _incrementCounter,
      tooltip: 'Increment',
      child: const Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
  );
  }
}
  • 2画面目
import 'package:flutter/material.dart';

class NextPage extends StatelessWidget {
  NextPage(this.name);
  final String name;
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('2ページ目'),
        actions: <Widget>[Icon(Icons.add), Icon(Icons.share)],
      ),
      body: Container(
        height: double.infinity,
        color: Colors.greenAccent,
        child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
            Text(name),
            Center(
              child: ElevatedButton(
                  child: Text('戻る'),
                  onPressed: () {
                    Navigator.pop(context, "2ページ目から渡すパラメータ");
                  }
              )
            ),
          ]
        )
      )
    );
  }
}
  • pubspec.yamlに下記の記述をしないとassetで画像を表示することが出来ない
flutter:
  assets:
    - images/sample.jpg

WSL上でHerokuのgolangを動かす

はじめに

  • golangを無料で動かせるサーバーを探していたらherokuがあったので、heroku上でgolangを動かしてみる。

  • 下記のページを参考に作業を進める devcenter.heroku.com

前提条件

  • wslのubuntu環境にて作業を行う
  • すでにherokuでアカウント作成している状態

ローカル環境

  • herokuのcliを下記のコマンドインストールする
curl https://cli-assets.heroku.com/install.sh | sh
  • herokuにログインする
heroku login
  • 下記のページを参考にgolangプロジェクトのひな形をクローンする devcenter.heroku.com

  • 下記のコマンドを一通り実行するとgit cloneしてきたgolangプロジェクトがherokuにgolangのソースがデプロイされる

git clone https://github.com/heroku/go-getting-started.git
cd go-getting-started
heroku create
git push heroku main
package main

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"   
    "log"
    "os"
    "github.com/gin-gonic/gin"
    _ "github.com/heroku/x/hmetrics/onload"
)

func main() {
    port := os.Getenv("PORT")

    if port == "" {
        log.Fatal("$PORT must be set")
    }

    router := gin.New()
    router.Use(gin.Logger())
    router.LoadHTMLGlob("templates/*.tmpl.html")
    router.Static("/static", "static")

    router.GET("/", func(c *gin.Context) {
        c.HTML(http.StatusOK, "index.tmpl.html", nil)
    })

    router.Run(":" + port)
}

google colabでサイト監視して条件にあったらBeep音で知らせる

はじめに

  • 一時的に、とあるサイトを監視して、ある条件になったら音で知らせるというサンプルを作成しました。

インストールするパッケージ

!pip install requests
!pip install time
!pip install tkinter 

サンプルコード

import time
import requests
from google.colab import output

## 3時間とあるサイトをチェックする
for i in range(0, 180, 1):
    ## 1分おきにリクエストを投げる
    time.sleep(60)
    response = requests.get('https://<あなたのチェックしたいサイト>')
    if (response.status_code == 200):
      print("問題なし")
    else:
      ## 問題あったときに音を出す
      output.eval_js('new Audio("https://upload.wikimedia.org/wikipedia/commons/0/05/Beep-09.ogg").play()')
    print(i, "分経過")

参考記事

stackoverflow.com

WSL/ubuntuにpython/golangをインストールしてローカル環境でvercelの開発をする

はじめに

  • vercelでserverless functionが利用できるようになっていたので、ローカルで少し動かしてみる。PythonGolangRuby、Nodeも使えるらしい。 vercel.com

  • serverless functionは利用できるがメモリなどの制限があるため、何か簡単な処理か、どうしてもサーバー側で処理しないといけないものにとどめておいた方がよさそう。果たしてそんな処理があるのか・・・。 vercel.com

WSL/ ubunts環境にgolangPythonをインストール

golangのインストール

$ sudo add-apt-repository ppa:longsleep/golang-backports
$ sudo apt update
$ sudo apt install golang
$ go version

pythonをインストール

1.下記のサイトからソースファイルをダウンロードする。
https://www.python.org/downloads/source/

2.下記のようにエクスプローラーから直接ダウンロードしたファイルをwsl環境の好きな場所に配置する
[f:id:PX-WING:20211028083247p:plain]

3.下記のコマンドを実行する

tar xf Python-3.10.0.tgz
 cd Python-3.10.0/
./configure
 make
 make install
  • vercelのAPIフォルダにgoとpythonのファイルを設置する f:id:PX-WING:20211030113452p:plain

設置したファイルの中身

golang

  • vercelでプロジェクト作成したときにdefaultで作成されるファイル
package handler

import (
    "fmt"
    "net/http"
    "time"
)

func Handler(w http.ResponseWriter, r *http.Request) {
    currentTime := time.Now().Format(time.RFC850)
    fmt.Fprintf(w, currentTime)
}

python

  • api/index.pyファイルの中身は下記のように記述する
from http.server import BaseHTTPRequestHandler
from urllib import parse
class handler(BaseHTTPRequestHandler):
    def do_GET(self):
        s = self.path
        dic = dict(parse.parse_qsl(parse.urlsplit(s).query))
        self.send_response(200)
        self.send_header('Content-type','text/plain')
        self.end_headers()
        if "name" in dic:
            message = "Hello, " + dic["name"] + "!"
        else:
            message = "Hello, stranger!"
        self.wfile.write(message.encode())
        return  

React側の呼び出し

import React from 'react';
import { useEffect, useState } from 'react';
import './App.css';

function App() {
  const [date, setDate] = useState(null);
  const [message, setMessage] = useState('');
  useEffect(() => {
    async function getDate() {
      // GoのAPIの呼び出す
      const res = await fetch('/api/date');
      const newDate = await res.text();
      // PythonのAPIを呼び出す
      const res2 = await fetch('/api/index');
      const newMessage = await res2.text();
      setDate(newDate);
      setMessage(newMessage);
    }
    getDate();
  }, []);
  return (
    <main>
      <h1>Create React App + Go And Pytho API</h1>
      <br />
      <h2>The date according to Go is:</h2>
      <p>{date ? date : 'Loading date...'}</p>
      <h2>The Message according to Python is:</h2>
      <p>{message ? message : 'Loading date...'}</p>
    </main>
  );
}

export default App;

実行結果

f:id:PX-WING:20211030113756p:plain

AIに必要な数学の勉強メモ⑥

分散

  • 数値データのばらつき具合を表すための指標
  • ある一つの群の数値データにおいて、平均値と個々のデータの差の2乗の平均を求めることによって計算

 v=\frac{1}{n} \sum_{k=1}^{n} (x_k-u)^{2}

分散の求め方

x= np.array([55,45,60,40])
print(np.var(x))

標準偏差

 \sqrt{v}=\sqrt{\frac{1}{n} \sum_{k=1}^{n} (x_k-u)^{2}}

  • すべてのデータを使った、ばらつきを示す値(散らばりの度合い)
x= np.array([55,45,60,40])
print(np.std(x))

【高校 数学Ⅰ】 データ分析11 標準偏差とは? (11分) - YouTube

正規分布

  • 「ありふれた」「通常の」確率分布です
  • ガウス分布と呼ばれることもある

※確率分布

【高校数学】 数B-102 確率分布と確率変数② - YouTube

正規分布

標準化とは

https://www.youtube.com/watch?v=fAFAn3XgMfE https://www.youtube.com/watch?v=wQFY1fnpYcM&t=360s