HAYATO

HAYATO / PORTFOLIO

検査・評価の経験を、
次のものづくりへ。

これまでの検査・評価業務やプログラミングの経験を土台に、Pythonの基礎を学習しています。データを整理し、業務の課題を解決できるエンジニアを目指しています。

制作物を見る
01

自己紹介

検査プログラムに関わる業務や、テスト・評価業務を経験してきました。Excel・VBAを使ったデータの整理・分析の経験を、今後のIT業務に生かしたいと考えています。

現在はPythonを基礎から学び直しています。小さな課題を一つずつ解きながら、コードの動作と結果を自分の言葉で説明できることを大切にしています。

02

経験のある技術と、学習中の技術

これまでに使用した技術

  • Excel / VBA
  • C
  • Fortran 77

VBAは業務で、C・Fortran 77は学生時代に使用。過去の経験を含むため、現在の習熟度とは区別しています。

現在の学習テーマ

  • Python 基礎
  • データ集計

合計・平均・最大値・最小値など、小さなデータを使った集計を練習しています。

03

これまでの経験

検査プログラム関連

画像検査プログラムに関わる業務を経験しました。

テスト・評価業務

テストエンジニアとして、Excel・VBAを用いた信頼性評価や検査データの分析に取り組みました。

数値計算の学習

学生時代にC・Fortran 77を使った数値計算を学びました。現在のPython学習でも、当時の考え方と比較しながら理解を深めています。

04

制作物

01 / PYTHON · WEB API · LINE

天気予報をLINEに送るBot

指定した地域の天気予報をOpen-Meteoから取得し、LINEへ通知するPythonプログラムです。外出前に天気・気温・降水確率をまとめて確認できることを目的にしています。

  • 天気・最高/最低気温・日内最大の降水確率を日本語で整形
  • LINE Messaging APIによるプッシュ通知
  • 環境変数による認証情報の管理と、通信エラーへの対応
  • 送信しないプレビューと、通信不要の架空データデモ

Pythonコードを見る · 設定・実行手順を見る

ローカルの自動テストで処理を検証済み。実APIとの接続・LINE実機受信・定期運用は未確認です。

Pythonコード

コード全文です。コピーして weather_bot.py という名前で保存できます。長いコードは枠の中をスクロールして読めます。

"""天気予報を整形し、明示的な --send 指定時だけLINEに送信する。"""
import argparse
import json
import math
import os
import re
import sys
from urllib.error import HTTPError, URLError
from urllib.parse import urlencode
from urllib.request import Request, urlopen

WEATHER = {
    0: '快晴', 1: 'おおむね晴れ', 2: '一部曇り', 3: '曇り',
    45: '霧', 48: '着氷性の霧', 51: '弱い霧雨', 53: '霧雨', 55: '強い霧雨',
    56: '弱い着氷性の霧雨', 57: '強い着氷性の霧雨',
    61: '弱い雨', 63: '雨', 65: '強い雨', 66: '弱い着氷性の雨', 67: '強い着氷性の雨',
    71: '弱い雪', 73: '雪', 75: '強い雪', 77: '霧雪',
    80: '弱いにわか雨', 81: 'にわか雨', 82: '激しいにわか雨',
    85: '弱いにわか雪', 86: '強いにわか雪', 95: '雷雨',
    96: 'ひょうを伴う雷雨', 99: '激しいひょうを伴う雷雨',
}


def request_json(request, service):
    """HTTPエラーの本文や認証情報はログに出さない。自動再送はしない。"""
    try:
        with urlopen(request, timeout=20) as response:
            return json.load(response)
    except HTTPError as exc:
        raise RuntimeError(f'{service}: HTTP {exc.code}。設定や利用上限を確認してください。') from None
    except (URLError, TimeoutError, OSError):
        raise RuntimeError(f'{service}: 通信に失敗しました。送信時は到達状況を確認してから再実行してください。') from None
    except (ValueError, UnicodeError):
        raise RuntimeError(f'{service}: 応答が正しいJSONではありません。') from None


def fetch_weather(latitude, longitude):
    if not (math.isfinite(latitude) and -90 <= latitude <= 90
            and math.isfinite(longitude) and -180 <= longitude <= 180):
        raise ValueError('緯度は-90〜90、経度は-180〜180で指定してください。')
    params = urlencode({
        'latitude': latitude, 'longitude': longitude,
        'daily': 'weather_code,temperature_2m_max,temperature_2m_min,precipitation_probability_max',
        'timezone': 'Asia/Tokyo', 'forecast_days': 1,
        'temperature_unit': 'celsius',
    })
    return request_json(Request('https://api.open-meteo.com/v1/forecast?' + params), '天気API')


def format_message(data, city):
    if not city.strip() or len(city) > 80 or '\n' in city or '\r' in city:
        raise ValueError('地域名は1〜80文字の1行で指定してください。')
    try:
        daily = data['daily']
        date = daily['time'][0]
        code = daily['weather_code'][0]
        high = daily['temperature_2m_max'][0]
        low = daily['temperature_2m_min'][0]
        rain = daily['precipitation_probability_max'][0]
        if not isinstance(date, str) or not re.fullmatch(r'\d{4}-\d{2}-\d{2}', date):
            raise ValueError
        if code is not None and (type(code) is not int):
            raise ValueError
        for value in (high, low, rain):
            if value is not None and (type(value) not in (int, float) or not math.isfinite(value)):
                raise ValueError
        if rain is not None and not 0 <= rain <= 100:
            raise ValueError
    except (KeyError, IndexError, TypeError, ValueError):
        raise ValueError('天気データの形式が不正です。送信を中止しました。') from None
    def display(value, unit):
        return '取得できませんでした' if value is None else f'{value:g}{unit}'
    condition = WEATHER.get(code, '不明')
    return (f'{city}の天気予報({date}・日本時間)\n'
            f'天気:{condition}\n最高:{display(high, "℃")} / 最低:{display(low, "℃")}\n'
            f'降水確率(日内最大):{display(rain, "%")}\n'
            '出典:Open-Meteo(予報は変わる場合があります)\nhttps://open-meteo.com/')


def credentials():
    token = os.environ.get('LINE_CHANNEL_ACCESS_TOKEN', '').strip()
    user_id = os.environ.get('LINE_USER_ID', '').strip()
    if not token or '\n' in token or '\r' in token:
        raise ValueError('LINE_CHANNEL_ACCESS_TOKENを環境変数に設定してください。')
    if not re.fullmatch(r'U[0-9a-fA-F]{32}', user_id):
        raise ValueError('LINE_USER_IDに、Uから始まる33文字のユーザーIDを設定してください。')
    return token, user_id


def send_line(text, token, user_id):
    payload = {'to': user_id, 'messages': [{'type': 'text', 'text': text}]}
    request = Request('https://api.line.me/v2/bot/message/push',
                      data=json.dumps(payload, ensure_ascii=False).encode('utf-8'),
                      headers={'Authorization': 'Bearer ' + token, 'Content-Type': 'application/json'},
                      method='POST')
    request_json(request, 'LINE API')


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument('--city', default='東京', help='表示する地域名。緯度・経度も合わせて変更してください。')
    parser.add_argument('--latitude', type=float, default=35.6812)
    parser.add_argument('--longitude', type=float, default=139.7671)
    mode = parser.add_mutually_exclusive_group()
    mode.add_argument('--demo', action='store_true', help='架空データで確認。通信も送信もしません。')
    mode.add_argument('--send', action='store_true', help='実際の予報を取得してLINEへ1回送信します。')
    args = parser.parse_args()
    try:
        auth = credentials() if args.send else None
        if args.demo:
            data = {'daily': {'time': ['2026-01-01'], 'weather_code': [2],
                    'temperature_2m_max': [12.5], 'temperature_2m_min': [4.0],
                    'precipitation_probability_max': [30]}}
        else:
            data = fetch_weather(args.latitude, args.longitude)
        message = format_message(data, args.city)
        if args.send:
            send_line(message, *auth)
            print('LINE APIが送信要求を受け付けました。実際の受信はLINEで確認してください。')
        else:
            print('[架空データのデモ・未送信]' if args.demo else '[実際の予報・未送信]')
            print(message)
        return 0
    except (ValueError, RuntimeError) as exc:
        print(str(exc), file=sys.stderr)
        return 1


if __name__ == '__main__':
    raise SystemExit(main())

設定・実行手順

Pythonを実行できるパソコンで操作してください。このページを開くだけではLINEへ送信されません。

コードをこのページからコピーした場合は、weather-bot フォルダを作成し、その中に weather_bot.py を保存してください。テスト用ファイルはコード一式のZIPに含まれています。

天気予報を取得してLINEへ通知する、学習用のPython制作物です。

コードの理解・修正・動作検証を進めるための初期版です。

構成

- index.html:制作物を紹介するポートフォリオ

- weather-bot/weather_bot.py:予報取得・日本語整形・LINE送信

- weather-bot/test_weather_bot.py:外部通信を使わない自動テスト

- .gitignore:認証情報や一時ファイルの除外

できること

Open-Meteoの当日予報(日本時間)から天気・最高/最低気温・降水確率の日内最大値を取得し、LINE Messaging APIのpush APIで1ユーザーに送ります。

地域名は表示用です。地域を変えるときは緯度・経度も必ず変更します。

欠測は0とせず「取得できませんでした」と表示。不正な応答・通信エラーでは処理を止めます。

Webhookを使う対話Botではなく、実行時に1回通知するプログラムです。

最初に試す(認証不要・外部通信なし)

Python 3.10以上を使用してください。追加ライブラリは不要です。

ターミナルで、このREADMEがあるフォルダへ移動して実行します。

python3 weather-bot/weather_bot.py --demo

2026-01-01の架空データが表示されます。現在の予報ではなく、LINEにも送信されません。

実際の予報を確認(送信なし)

python3 weather-bot/weather_bot.py

初期値は東京駅付近です。例:気仙沼市中心部付近を指定する場合:

python3 weather-bot/weather_bot.py --city 気仙沼 --latitude 38.908 --longitude 141.570

インターネット接続が必要です。Open-Meteoの利用条件・制限に従ってください。

LINEに送信するための設定

1. LINE公式アカウントを作成し、LINE Official Account ManagerでMessaging APIを有効にします。

2. LINE Developersコンソールで、そのチャネルのMessaging APIタブからチャネルアクセストークンを発行します。

3. 自分のLINEで公式アカウントを友だち追加します。

4. チャネル基本設定の「あなたのユーザーID」を確認します。表示名や友だち検索用のLINE IDとは異なる、Uから始まる33文字です。表示されない場合はビジネスIDとLINEアカウントの連携を確認してください。

5. 次の環境変数を、自分の端末のターミナルに設定します。トークンをコード・HTML・README・GitHubへ記載しないでください。

macOS標準のzshでは、以下の入力方法ならトークンを画面に表示せず、コマンド履歴にも値を直接書かずに設定できます。

read -s 'LINE_CHANNEL_ACCESS_TOKEN?チャネルアクセストークン: '
echo
export LINE_CHANNEL_ACCESS_TOKEN
read 'LINE_USER_ID?あなたのユーザーID: '
export LINE_USER_ID

同じターミナルで、送信先が自分であることを確認して実行:

python3 weather-bot/weather_bot.py --city 気仙沼 --latitude 38.908 --longitude 141.570 --send

このコマンドはLINEへ実際に送信します。送信回数は公式アカウントのプランや上限の影響を受けます。

HTTP成功は受信の保証ではありません。自分のLINEで受信を確認してください。

タイムアウト時は送信済みの可能性もあるため、受信状況を見てから再実行してください。重複防止のため自動再送は実装していません。

このプログラムは.envを自動読込しません。通常実行と--demoではトークンは不要です。

検証

python3 -m unittest discover -s weather-bot -v

日本語整形・欠測・不正データ・緯度検証・デモ時の通信禁止・認証未設定時の送信禁止・送信JSON・HTTPエラー表示をテストしています。

実API接続、LINEでの実機受信、定期実行は未検証です。定期実行の仕組みは含めていません。

GitHubへ載せるファイル

このフォルダの構成を保ってリポジトリへ追加します。秘密情報を入れたファイルはアップロードしないでください。

index.htmlにはコードと手順を内蔵しています。Pythonを実行する場合やテストする場合は、weather-botフォルダも使用してください。

HTMLは紹介ページです。GitHub PagesなどでHTMLを公開してもPythonは実行されません。BotはPythonのある端末やサーバーで実行します。

設計を説明するポイント

- fetch_weather:HTTP GETで予報を取得

- format_message:JSONを検証し、人が読めるメッセージに変換

- send_line:認証ヘッダー付きのHTTP POSTで送信

- main:引数でデモ・確認・送信を切り替え

公式ドキュメント・出典

- Open-Meteo Forecast API

- Open-Meteo利用条件

- LINE Messaging APIの開始

- LINEユーザーIDの取得

- LINE Messaging APIリファレンス

天気データの出典はOpen-Meteoです。通知文にも出典リンクを含めています。

05

お仕事について

フルリモートでのIT関連業務を希望しています。詳しい職務経歴や勤務条件は、応募先とのやり取りの中でお伝えします。

採用に関するご連絡は、このページをご案内した応募媒体・メールの連絡先へお願いいたします。