AB型技術系 主に備忘録

ほぼプログラム関連の備忘録

Nuxt.js 画面を追加してみる

pagesディレクトリにファイルを追加


pagesディレクトリにファイルを追加

画面のビューはpagesディレクトリに保存すればいいのでsample.vueをpages直下に追加。

/pages/sample.vue

<b>サンプル</b>

サーバーを起動

サーバーを起動してhttp://localhost:3000/sampleにアクセスします。

http://localhost:3000/sample


エラー画面

エラー画面が表示されました。
単純にファイル追加するだけでは駄目みたい。
メッセージを見ると、pages/sample.vue自体を表示しようとはしてますね。
「templateが定義されてないよ」と言われてます。
sample.vueを変更してみます。

/pages/sample.vue

<template>
    <b>サンプル</b>
</template>

サーバー起動中はファイルを保存したタイミングで自動でコンパイルされて便利ですね。
ちなみにESLintの設定を変えないとエラーが発生します。


ESLintError

このエラーを解消するため.eslintrc.jsにruleを追加しました。


/.eslintrc.js

追加したルール
'vue/multi-word-component-names': 'off'

/.eslintrc.js

module.exports = {
  root: true,
  env: {
    browser: true,
    node: true,
  },
  parserOptions: {
    parser: '@babel/eslint-parser',
    requireConfigFile: false,
  },
  extends: ['@nuxtjs', 'plugin:nuxt/recommended', 'prettier'],
  plugins: [],
  // add your custom rules here
  rules: {
    'vue/multi-word-component-names': 'off'
  },
}

/.nuxt/routes.json

ルーティング情報は下記のファイルにコンパイル時に出力されてました。
.nuxt\routes.json


/.nuxt/routes.json
[
  {
    "name": "sample",
    "path": "/sample",
    "component": "C:\\\\sources\\\\vscode\\\\pages\\\\sample.vue",
    "chunkName": "pages/sample",
    "_name": "_37a5c031"
  },
  {
    "name": "index",
    "path": "/",
    "component": "C:\\\\sources\\\\vscode\\\\pages\\\\index.vue",
    "chunkName": "pages/index",
    "_name": "_5573cb9b"
  }
]

サブディレクトリも同じ要領で追加してみます。

pages/sub/subsanple.vue


/pages/sub/subsanple.vue
<template>
    <b>サブディレクトリサンプル</b>
</template>

/.nuxt/routes.json

[
  {
    "name": "sample",
    "path": "/sample",
    "component": "C:\\\\sources\\\\vscode\\\\pages\\\\sample.vue",
    "chunkName": "pages/sample",
    "_name": "_37a5c031"
  },
  {
    "name": "sub-subsample",
    "path": "/sub/subsample",
    "component": "C:\\\\sources\\\\vscode\\\\pages\\\\sub\\\\subsample.vue",
    "chunkName": "pages/sub/subsample",
    "_name": "_5f76edb1"
  },
  {
    "name": "index",
    "path": "/",
    "component": "C:\\\\sources\\\\vscode\\\\pages\\\\index.vue",
    "chunkName": "pages/index",
    "_name": "_5573cb9b"
  }
]


Nuxt.js 初期画面の構成を確認してみる

初期画面のソース

初期画面のソース

インストール後の初期画面の構成を確認してみました。

ヘッダ部

<!doctype html>
<html data-n-head-ssr lang="en" data-n-head="%7B%22lang%22:%7B%22ssr%22:%22jp%22%7D%7D">
 <head >
    <title>sample</title>
	<meta data-n-head="ssr" charset="utf-8">
	<meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1">
	<meta data-n-head="ssr" data-hid="description" name="description" content="">
	<meta data-n-head="ssr" name="format-detection" content="telephone=no">
	<link data-n-head="ssr" rel="icon" type="image/x-icon" href="/favicon.ico">
	<link rel="preload" href="/_nuxt/runtime.js" as="script">
	<link rel="preload" href="/_nuxt/commons/app.js" as="script">
	<link rel="preload" href="/_nuxt/vendors/app.js" as="script">
	<link rel="preload" href="/_nuxt/app.js" as="script">
	<link rel="preload" href="/_nuxt/pages/index.js" as="script">
	<style data-vue-ssr-id="3191d5ad:0">
	.nuxt-progress {
	  position: fixed;
	  top: 0px;
	  left: 0px;
	  right: 0px;
	  height: 2px;
	  width: 0%;
	  opacity: 1;
	  transition: width 0.1s, opacity 0.4s;
	  background-color: black;
	  z-index: 999999;
	}
	.nuxt-progress.nuxt-progress-notransition {
	  transition: none;
	}
	.nuxt-progress-failed {
	  background-color: red;
	}
	</style>
	<style data-vue-ssr-id="0e36c2db:0">
	.nuxt__build_indicator[data-v-71e9e103] {
	  box-sizing: border-box;
	  position: fixed;
	  font-family: monospace;
	  padding: 5px 10px;
	  border-radius: 5px;
	  box-shadow: 1px 1px 2px 0px rgba(0,0,0,0.2);
	  width: 88px;
	  z-index: 2147483647;
	  font-size: 16px;
	  line-height: 1.2rem;
	}
	.v-enter-active[data-v-71e9e103], .v-leave-active[data-v-71e9e103] {
	  transition-delay: 0.2s;
	  transition-property: all;
	  transition-duration: 0.3s;
	}
	.v-leave-to[data-v-71e9e103] {
	  opacity: 0;
	  transform: translateY(20px);
	}
	svg[data-v-71e9e103] {
	  display: inline-block;
	  vertical-align: baseline;
	  width: 1.1em;
	  height: 0.825em;
	  position: relative;
	  top: 1px;
	}
	</style>
</head>
nuxt.config.js
head: {
    title: 'sample',
    htmlAttrs: {
      lang: 'en',
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
      { name: 'format-detection', content: 'telephone=no' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
  },

タイトル、meta、linkはnuxt.config.jsの設定を変更すれば変更できます。

head: {
    title: 'サンプル',
    htmlAttrs: {
      lang: 'ja',
    },
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: '' },
      { name: 'format-detection', content: 'telephone=no' },
    ],
    link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
  },

変更して初期画面を表示してみる。

タイトルが変更されていることを確認
<!doctype html>
<html data-n-head-ssr lang="ja" data-n-head="%7B%22lang%22:%7B%22ssr%22:%22ja%22%7D%7D">
  <head >
    <title>サンプル</title>

タイトルが変更されていますね。
preloadが設定されているlinkタグは固定で出力されているのか?
styleタグは下記のファイルに記述されているstyleタグが出力されているようです。

.nuxt\components\nuxt-error.vue
.nuxt\components\nuxt-loading.vue
.nuxt\components\nuxt-build-indicator.vue

開発者ツールで見ると下記のstyleタグが追加されてました。

<style type="text/css">
.nuxt-logo {
  height: 180px;
}
</style

こちらは下記ファイルに記述されているstyleタグでした。

components\NuxtLogo.vue

ページの読み込み時に要素が追加されているようですね。

ボディ部

<body >
    <div data-server-rendered="true" id="__nuxt"><!----><!----><div id="__layout"><div class="relative flex items-top justify-center min-h-screen bg-gray-100 sm:items-center sm:pt-0"><link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.1.2/dist/tailwind.min.css" rel="stylesheet"> <div class="max-w-4xl mx-auto sm:px-6 lg:px-8"><a href="https://nuxtjs.org" target="_blank" class="flex justify-center pt-8 sm:pt-0"><svg width="218" height="45" viewBox="0 0 159 30" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M55.5017 6.81866H60.1727L70.0719 22.9912V6.81866H74.3837V29.7345H69.7446L59.8135 13.5955V29.7345H55.5017V6.81866Z" fill="#003543"></path> <path d="M93.657 29.7344H89.6389V27.1747C88.7241 28.9761 86.8628 29.9904 84.5113 29.9904C80.7869 29.9904 78.3684 27.3059 78.3684 23.4423V13.2339H82.3865V22.5976C82.3865 24.8566 83.7594 26.4276 85.8171 26.4276C88.0712 26.4276 89.6389 24.6598 89.6389 22.2377V13.2339H93.657V29.7344Z" fill="#003543"></path> <path d="M107.64 29.7344L103.784 24.2342L99.9291 29.7344H95.6492L101.596 21.1242L96.1074 13.2339H100.485L103.784 17.9821L107.051 13.2339H111.461L105.94 21.1242L111.886 29.7344H107.64Z" fill="#003543"></path> <path d="M120.053 8.25848V13.2339H124.627V16.6063H120.053V24.7974C120.053 25.0725 120.162 25.3363 120.356 25.531C120.55 25.7257 120.813 25.8353 121.087 25.8357H124.627V29.728H121.98C118.386 29.728 116.035 27.6323 116.035 23.9687V16.6095H112.801V13.2339H114.83C115.776 13.2339 116.327 12.6692 116.327 11.7349V8.25848H120.053Z" fill="#003543"></path> <path d="M134.756 24.5446V6.81866H139.066V23.1864C139.066 27.6067 136.943 29.7345 133.349 29.7345H128.332V25.8421H133.461C133.804 25.8421 134.134 25.7054 134.377 25.4621C134.619 25.2188 134.756 24.8888 134.756 24.5446Z" fill="#003543"></path> <path d="M141.649 22.0409H145.799C146.029 24.6006 147.728 26.2308 150.472 26.2308C152.923 26.2308 154.623 25.2501 154.623 23.2199C154.623 18.3085 142.331 21.7129 142.331 12.9395C142.334 9.17515 145.568 6.55945 150.215 6.55945C155.05 6.55945 158.317 9.34153 158.516 13.6306H154.388C154.193 11.6341 152.632 10.2918 150.207 10.2918C147.953 10.2918 146.548 11.3397 146.548 12.9427C146.548 18.0173 159 14.2226 159 23.1576C159 27.4131 155.504 30 150.474 30C145.279 30 141.882 26.8563 141.654 22.0441" fill="#003543"></path> <path d="M24.7203 29.704H41.1008C41.6211 29.7041 42.1322 29.5669 42.5828 29.3061C43.0334 29.0454 43.4075 28.6704 43.6675 28.2188C43.9275 27.7672 44.0643 27.2549 44.0641 26.7335C44.0639 26.2121 43.9266 25.6999 43.6662 25.2485L32.6655 6.15312C32.4055 5.70162 32.0315 5.32667 31.581 5.06598C31.1305 4.8053 30.6195 4.66805 30.0994 4.66805C29.5792 4.66805 29.0682 4.8053 28.6177 5.06598C28.1672 5.32667 27.7932 5.70162 27.5332 6.15312L24.7203 11.039L19.2208 1.48485C18.9606 1.03338 18.5864 0.658493 18.1358 0.397853C17.6852 0.137213 17.1741 0 16.6538 0C16.1336 0 15.6225 0.137213 15.1719 0.397853C14.7213 0.658493 14.3471 1.03338 14.0868 1.48485L0.397874 25.2485C0.137452 25.6999 0.000226653 26.2121 2.8053e-07 26.7335C-0.000226092 27.2549 0.136554 27.7672 0.396584 28.2188C0.656614 28.6704 1.03072 29.0454 1.48129 29.3061C1.93185 29.5669 2.44298 29.7041 2.96326 29.704H13.2456C17.3195 29.704 20.3239 27.9106 22.3912 24.4118L27.4102 15.7008L30.0986 11.039L38.1667 25.0422H27.4102L24.7203 29.704ZM13.0779 25.0374L5.9022 25.0358L16.6586 6.36589L22.0257 15.7008L18.4322 21.9401C17.0593 24.2103 15.4996 25.0374 13.0779 25.0374Z" fill="#00DC82"></path></svg></a> <div class="mt-8 bg-white overflow-hidden shadow sm:rounded-lg p-6"><h2 class="text-2xl leading-7 font-semibold">
        Welcome to your Nuxt Application
      </h2> <p class="mt-3 text-gray-600">
        We recommend you take a look at the
        <a href="https://nuxtjs.org" target="_blank" class="button--doc text-green-500 hover:underline">Nuxt documentation</a>, whether you are new or have previous experience with the
        framework.<br></p> <p class="mt-4 pt-4 text-gray-800 border-t border-dashed">
        To get started, remove
        <code class="bg-gray-100 text-sm p-1 rounded border">components/Tutorial.vue</code>
        and start coding in
        <code class="bg-gray-100 text-sm p-1 rounded border">pages/index.vue</code>. Have fun!
      </p></div> <div class="flex justify-center pt-4 space-x-2"><a href="https://github.com/nuxt/nuxt.js" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" class="w-6 h-6 text-gray-600 hover:text-gray-800 button--github"><path d="M12 2.247a10 10 0 0 0-3.162 19.487c.5.088.687-.212.687-.475c0-.237-.012-1.025-.012-1.862c-2.513.462-3.163-.613-3.363-1.175a3.636 3.636 0 0 0-1.025-1.413c-.35-.187-.85-.65-.013-.662a2.001 2.001 0 0 1 1.538 1.025a2.137 2.137 0 0 0 2.912.825a2.104 2.104 0 0 1 .638-1.338c-2.225-.25-4.55-1.112-4.55-4.937a3.892 3.892 0 0 1 1.025-2.688a3.594 3.594 0 0 1 .1-2.65s.837-.262 2.75 1.025a9.427 9.427 0 0 1 5 0c1.912-1.3 2.75-1.025 2.75-1.025a3.593 3.593 0 0 1 .1 2.65a3.869 3.869 0 0 1 1.025 2.688c0 3.837-2.338 4.687-4.563 4.937a2.368 2.368 0 0 1 .675 1.85c0 1.338-.012 2.413-.012 2.75c0 .263.187.575.687.475A10.005 10.005 0 0 0 12 2.247z" fill="currentColor"></path></svg></a> <a href="https://twitter.com/nuxt_js" target="_blank"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="32" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24" class="w-6 h-6 text-gray-600 hover:text-gray-800"><path d="M22.46 6c-.77.35-1.6.58-2.46.69c.88-.53 1.56-1.37 1.88-2.38c-.83.5-1.75.85-2.72 1.05C18.37 4.5 17.26 4 16 4c-2.35 0-4.27 1.92-4.27 4.29c0 .34.04.67.11.98C8.28 9.09 5.11 7.38 3 4.79c-.37.63-.58 1.37-.58 2.15c0 1.49.75 2.81 1.91 3.56c-.71 0-1.37-.2-1.95-.5v.03c0 2.08 1.48 3.82 3.44 4.21a4.22 4.22 0 0 1-1.93.07a4.28 4.28 0 0 0 4 2.98a8.521 8.521 0 0 1-5.33 1.84c-.34 0-.68-.02-1.02-.06C3.44 20.29 5.7 21 8.12 21C16 21 20.33 14.46 20.33 8.79c0-.19 0-.37-.01-.56c.84-.6 1.56-1.36 2.14-2.23z" fill="currentColor"></path></svg></a></div></div></div></div></div><script>window.__NUXT__=(function(a,b){return {layout:"default",data:[{}],fetch:{},error:a,serverRendered:true,routePath:b,config:{_app:{basePath:b,assetsPath:"\u002F_nuxt\u002F",cdnURL:a}},logs:[]}}(null,"\u002F"));</script><script src="/_nuxt/runtime.js" defer></script><script src="/_nuxt/pages/index.js" defer></script><script src="/_nuxt/commons/app.js" defer></script><script src="/_nuxt/vendors/app.js" defer></script><script src="/_nuxt/app.js" defer></script>
  </body>

画面のビューはpagesディレクトリに保存するようです。
"http://localhost:3000/"にアクセスした時に表示されるのは下記になります。

pages\index.vue

このファイルに直接記述されているわけではないようです。

”<Tutorial />”部分が怪しそうですね。
関連しそうなのは下記のファイル

components\Tutorial.vue

”<Tutorial />”とcomponents\Tutorial.vueが関連することが分かりました。
再利用できそうな部品を定義したファイルを置くのがcomponentsディレクトリのようです。

ページの構成がだいたい把握できたので次は新規ページの追加を試してみます。



Nuxt.jsでSSRを体験してみる

Nuxt.jsを試す目的

SSRの動きがいまいち理解できないので動きをを確認したい。
個人的に開発中のシステムがあるので良さそうなら導入したい。

エディタ

Visual Studio Code

インストール

npxコマンドを使うのでNode.jsのインストールが必要。
既にNode.jsはインストール済みだったのでNuxt.jsのインストールから始める。

ターミナル上でnpx create-nuxt-appを実行

npx create-nuxt-app

プロジェクト名の入力に進みます。

プロジェクト名を入力

プロジェクト名を入力

"sample"と入力してEnter
プログラミング言語の選択に進みます。

プログラミング言語を選択

プログラミング言語を選択

JavaScriptを選択してEnter
パッケージマネージャーの選択に進みます。

パッケージマネージャーを選択

パッケージマネージャーを選択

Npmを選択してEnter
UIフレームワークの選択に進みます。

UIフレームワークを選択

UIフレームワークを選択

Noneを選択してEnter
モジュールの選択に進みます。

モジュールを選択

モジュールを選択

複数選択可能ですが、非同期通信だけ試したいのでAxiosのみを選択してEnter
(選択はスペースキー)
Lintツールの選択に進みます。

Lintツールを選択

Lintツールを選択

全て選択してEnter
テストフレームワークの選択に進みます。

テストフレームワークを選択

テストフレームワークを選択

None選択してEnter
レンダリングモードの選択に進みます。

レンダリングモードを選択

レンダリングモードを選択

Universal(SSR/SSG)を選択してEnter
デプロイメントターゲットの選択に進みます。

デプロイメントターゲットを選択

デプロイメントターゲットを選択

Server(Node.js hosting)を選択してEnter
デプロイメントツールの選択に進みます。

デプロイメントツールを選択

デプロイメントツールを選択

jsoonconfig.jsonを選択してEnter
CIツールの選択に進みます。

CIツールを選択

CIツールを選択

None選択してEnter
バージョン管理システムの選択に進みます。

バージョン管理システムを選択

バージョン管理システムを選択

None選択してEnter
インストールが始まります。

インストール完了

インストール完了

インストールが完了。

動作確認

サーバー起動

サーバー起動

npm run devを入力してEnter

起動完了

ポート番号3000でサーバーが起動しました。

動作確認

動作確認

http://localhost:3000/にアクセスすると画面が表示されました。

導入は意外と簡単ですね。

今後試したいこと

  1. 新規ページの追加
  2. デフォルトテンプレートの変更
  3. ヘッダ、メインコンテンツ、サイドメニュー、フッタなどをコンポーネント

Nuxt.jsで最終的にやりたいこと。

Nuxt.jsは静的ファイル(htmlなど)を出力する機能があるので以下を試してみたいです。

  1. ローカル環境で画面作成
  2. 表示するデータはレンタルサーバーに設置済のAPIを利用して取得
  3. generateコマンドで作成した画面を静的コンテンツとして出力
  4. phpMySQLが使えない無料レンタルサーバー上に静的ファイルをアップロード


bottle + pythonでCSVファイルをアップロードした時のBOM問題

bottle + pythonCSVファイルをアップロードした時にはまった時のメモです。

index.pyを下記の用に実装しました。
CSVをアップロードしてサーバ上には保存せずバイトデータを文字列に変換して画面に表示。
最終的にCSVexcelで保存したファイルをアップロードします。

サンプルプログラム1

index.py

from bottle import post, get, run, request, response,template

@get('/upload/<name>')
def upload_page(name):
    return template("view/upload/upd_" + name + ".html")

@post('/upload/<name>')
def upload(name):
    res = {}
    data = request.files.data
    if data and data.file:

        # ファイル読み込み
        data = data.file.read()

        # 文字列型にデコード
        data = data.decode()
        res["str"] = data
    return template("view/upload/upd_" + name + "_result.html", res=res)

アップロード用html

<!DOCTYPE html>
<html lang="ja">
   <head>
       <meta charset="UTF-8">
       <title>アップロード</title>
   </head>
   <body>
       <h1>アップロード</h1>
       <form action="upload/dividend" method="post" enctype="multipart/form-data">
           ファイル:<input type="file" name="data"/>
           <input type="submit" value="アップロード" />
       </form>
    </body>
</html>

アップロード結果用html

<!DOCTYPE html>
<html lang="ja">
   <head>
       <meta charset="UTF-8">
       <title>アップロード結果</title>
   </head>
   <body>
       <h1>アップロード結果</h1>
       {{res["str"]}}
    </body>
</html>

テスト用CSV

cd,value
001,データ1
002,データ2

アップロード結果

f:id:freelancer13:20220127023142p:plain
アップロード結果

読み込んだCSVファイルの中身が表示されました。

サンプルプログラム2

CSVデータを操作しやすいように標準ライブラリのcsv.DictReaderを使用するように変更しました。

index.py

import io
import csv
from bottle import post, get, run, request, response,template

@get('/upload/<name>')
def upload_page(name):
    return template("view/upload/" + name + ".html")

@post('/upload/<name>')
def upload(name):
    res = {}
    data = request.files.data
    if data and data.file:

        # ファイル読み込み
        data = data.file.read()

        # 文字列型にデコード
        data = data.decode()
       
      # 文字列をファイルとして扱う
        strio = io.StringIO(data)

        # csvをDict型で読み込む
        data = csv.DictReader(strio)

        res["csv"] = data
    return template("view/upload/" + name + "_result.html", res=res)

アップロード結果用html

<!DOCTYPE html>
<html lang="ja">
   <head>
       <meta charset="UTF-8">
       <title>アップロード結果</title>
   </head>
   <body>
       <h1>アップロード結果</h1>
       %for dt in res["csv"]:
          {{dt}}     
       %end
    </body>
</html>

アップロード結果

f:id:freelancer13:20220127030422p:plain
アップロード結果

csv.DictReaderによりcsvデータを1行目の項目をkeyとしたDict型の配列に変換してくれます。
dt["date"]と記述すれば個別に値を出力することができます。

最後にExcelから出力したcsvで試します。
テスト用CSV(Excelで保存)

date,seq,value
20220101,1,20220101_データ1
20220101,2,20220101_データ2
20220102,1,20220102_データ1

アップロード結果

f:id:freelancer13:20220127030856p:plain
アップロード結果

一つ目のkeyに\ufeffが表示されてますね。
excelcsvを出力するとUTF-8のBOM付で出力されるようです。

サンプルプログラム3

BOMに対応するためdecode部分を変更しました。

# 文字列型にデコード
data = data.decode("utf-8-sig")

アップロード結果

f:id:freelancer13:20220127031501p:plain
アップロード結果

無事に\ufeffが表示されなくなりました。


スクレイピングした株価情報をjson形式で出力しFTPでサーバーにアップロードする

前回保有している株の株価情報をスクレイピングしてjson形式のファイルに出力することができました。

freelancer.hatenablog.jp

このファイルを元に最終的にはスターサーバー上のMySQLに登録したいのでまずはFTPを使ってアップロードできるようにします。

サンプルプログラム

import requests
import json
import time
import ftplib
from bs4 import BeautifulSoup
    
def getText(el):
    """指定した要素内の文字列を返す

    Args:
        el (bs4.element.ResultSet): 要素

    Returns:
        str: 要素内の文字列
    """
    if el is not None:
        return el.getText()
    return ""

# 処理件数
cnt = 0

# システム日付
sysDate = ""

# 銘柄情報リスト
stockInfoList = []

# 保有している銘柄コードを取得する自前のAPI 
# スターサーバー上のMySQLからデータを取得し下記の形式で返します
# [{"stock_cd": "1942"}, {"stock_cd": "2730"}
res = requests.get("http://xxx.yyy.zzz/codeList")

# JSONに変換
data = json.loads(res.text)

# 銘柄コード毎に株価情報を取得
for cd in data:

    # 上限設定
    if cnt == 50:
        break

    # 株価情報取得
    res = requests.get("https://kabutan.jp/stock/?code=" + cd["stock_cd"])
    
    # レスポンスの解析
    doc = BeautifulSoup(res.text, "html.parser")
    
    # 株価情報の取得
    elPrice = doc.select("#kobetsu_left tr")
    
    # タイトルの取得 [<th scope="row">終値</th>]
    elTitle = doc.select("#kobetsu_left th")
    
    # 銘柄名の取得
    name = doc.select_one(".si_i1_1 h2")

    # 銘柄名が取得できない場合はスキップ
    if name is None:
        continue

    # 銘柄情報
    stockInfo = {}

    # 銘柄名を設定 銘柄コード+銘柄名で文字列を取得するので銘柄コードをブランクにする
    stockInfo["stock_cd"] = cd["stock_cd"]
    stockInfo["stock_na"] = name.getText().replace(cd["stock_cd"], "")

    # システム日付が未指定の場合は日付を取得
    if sysDate == "":

        # システム日付の取得 [<time datetime="2021-10-08">10月08日</time>]
        elDate = doc.select("#kobetsu_left h2>time")
        if elDate is None:
            break;
        
        # システム日付を(yyyymmdd形式)にする
        sysDate = elDate[0].attrs["datetime"].replace("-", "")
        
    # タイトルの解析
    if  elTitle is not None:

        # 始値、高値、安値、終値、出来高が存在しない場合はスキップする
        if len(elTitle) < 5:
            continue

        if elTitle[0].getText() != "始値":
            continue

        if elTitle[1].getText() != "高値":
            continue

        if elTitle[2].getText() != "安値":
            continue

        if elTitle[3].getText() != "終値":
            continue

        if elTitle[4].getText() != "出来高":
            continue

    # 銘柄情報の設定
    if elPrice is not None and len(elPrice) >= 5:
        stockInfo["open_price"] = getText(elPrice[0].select_one("td")).replace(",", "")
        stockInfo["high_price"] = getText(elPrice[1].select_one("td")).replace(",", "")
        stockInfo["low_price"] = getText(elPrice[2].select_one("td")).replace(",", "")
        stockInfo["closing_price"] = getText(elPrice[3].select_one("td")).replace(",", "")
        stockInfo["vol"] = getText(elPrice[4].select_one("td")).replace("\u00a0\u682a", "").replace(",", "")
        stockInfoList.append(stockInfo)
    
    # カウントアップ
    cnt+=1

    # 1秒停止
    time.sleep(1)

if sysDate != "":
    dt = {}
    dt["dt"] = sysDate
    dt["stock_info"] = stockInfoList
    filePath = "C:/data/"
    fileNa = sysDate + "_stock_price.json"
    with open(filePath + fileNa, mode='a') as fo:
        fo.write(json.dumps(dt))

    with ftplib.FTP() as ftp:
        try:
            # FTPサーバーに接続
            ftp.connect(host="aaa.bbb.ccc", port=21, timeout=30)
            # パッシブモードに設定
            ftp.set_pasv(1)
            # FTPサーバー ログイン
            ftp.login("username", "password")
            with open(filePath + fileNa, 'rb') as fp:
                ftp.storbinary("STOR /server/json/" + fileNa, fp)
        except ftplib.all_errors as e:
            print(e)

標準モジュールのftplibを使ってFTPを使ってサーバーにアップロードする処理を追加しました。

実行結果

f:id:freelancer13:20211014013016p:plain
アップロードしたディレクトリのファイル一覧

jsonファイルをアップロードすることができました。

次にやりたいこと

アップロードしたjsonファイルを読み込んで結果を表示するページを作成する