004 レポート 統合開発環境「Theia IDE」正式版リリース
005 レポート サイバーエージェントがVLM、新LLM公開
006 製品レビュー パソコン「ASUS Vivobook S 15 S5507QA」
007 NEWS FLASH
008 特集1 Oracle Cloud Infrastructure入門/福島耕平、中村峻大
020 特集2 ChatGPTで手軽に実現/佐藤秀輔 コード掲載
028 特別企画 イノシシ撃退機を作る ハード製作編/米田聡
036 Pythonあれこれ/飯尾淳 コード掲載
042 Markdownを活用する/藤原由来 コード掲載
052 香川大学SLPからお届け!/佐藤楓真 コード掲載
058 中小企業手作りIT化奮戦記/菅雄一
064 これだけは覚えておきたいLinuxコマンド/大津真 コード掲載
073 ユニケージレポート/田中湧也
076 Techパズル/gori.sh
077 コラム「要求工学を取り入れる」/シェル魔人
シェルスクリプトマガジンvol.91 Web掲載記事まとめ
特集2 ChatGPTで手軽に実現(Vol.91記載)
著者:佐藤 秀輔
対話型AIサービス「ChatGPT」が注目を集めています。特に、2023年3月に登場した「GPT-4」という大規模言語モデル(LLM)を利用可能になってからは自然言語の理解力が飛躍的に向上し、指示に基づいて、さまざまな処理を自動化できるようになっています。本特集では、ChatGPTを個人で活用する例として、家計簿レビューをさせる方法と、API(Application Programming Interface)を利用してWebスクレイピングをさせる方法を紹介します。
シェルスクリプトマガジン Vol.91は以下のリンク先でご購入できます。
図1 ランダムな家計簿データを生成するPythonスクリプト
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import datetime import random itemlist = ['食料品', '外食費', '日用品', '家財道具', '交通費', '電気代', '水道代', 'ガス代', 'ガソリン代', 'その他'] for i in range(366): basedate = datetime.datetime(2024, 1, 1) date = basedate + datetime.timedelta(days=i) item = itemlist[random.randrange(10)] amount = random.randrange(100) * 100 print(f"{date.date()},{date.time()},{item},{amount}") |
図10 Webスクレイピング対象となるWebページの例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>サンプル商品リスト</title> </head> <body> <h1>商品リスト</h1> <div class="product" id="product1"> <h2>商品名:スニーカー</h2> <p>価格:\5000</p> <p>評価:4.5 星</p> </div> <div class="product" id="product2"> <h2>商品名:バックパック</h2> <p>価格:\8000</p> <p>評価:4.0 星</p> </div> <div class="product" id="product3"> <h2>商品名:水筒</h2> <p>価格:\1500</p> <p>評価:4.8 星</p> </div> </body> </html> |
図11 Webスクレイピング用のPythonスクリプトの例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import json from bs4 import BeautifulSoup # HTMLファイルを開く with open('sample.html', 'r', encoding='utf-8') as file: html_content = file.read() # BeautifulSoupオブジェクトを作成 soup = BeautifulSoup(html_content, 'lxml') # 商品情報を抽出 products = soup.find_all('div', class_='product') product_list = [] for product in products: product_name = product.find('h2').text.replace('商品名:', '').strip() price = product.find_all('p')[0].text.replace('価格:', '').strip() rating = product.find_all('p')[1].text.replace('評価:', '').strip() # 各商品情報を辞書として追加 product_list.append({ '商品名': product_name, '価格': price, '評価': rating }) # JSON形式で結果を出力 json_output = json.dumps(product_list, ensure_ascii=False, indent=4) print(json_output) |
Pythonあれこれ(Vol.91掲載)
著者:飯尾 淳
本連載では「Pythonを昔から使っているものの、それほど使いこなしてはいない」という筆者が、いろいろな日常業務をPythonで処理することで、立派な「蛇使い」に育つことを目指します。その過程を温かく見守ってください。皆さんと共に勉強していきましょう。第21回では、「ハノイの塔」の解法「GDHP(Gniibe Distributed Hanoi Protocol)」の手順をアニメーションで表示するPythonアプリを作成します。
シェルスクリプトマガジン Vol.91は以下のリンク先でご購入できます。
図2 GDHPの手順をアニメーションで表示するJavaScriptコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 |
const COLORS = [ 'crimson', 'forestgreen', 'yellow', 'royalblue', 'saddlebrown', 'hotpink', 'darkorange', 'darkmagenta' ]; const NUM_OF_DISKS = COLORS.length; const BASE_LENGTH = 200; const C_WIDTH = 3.732 * BASE_LENGTH; const C_HEIGHT = 3.500 * BASE_LENGTH; const DISK_R = 0.9 * BASE_LENGTH; const POLE_R = 15; const POSITIONS = { 'Source' : [0.268, 0.714], 'Auxiliary' : [0.500, 0.286], 'Destination' : [0.732, 0.714] }; const FLASHING_COUNTER = 20; const STEPS = 30; class Vector { constructor(x,y) { this.x = x; this.y = y; } } class Position extends Vector { constructor(x,y) { super(x,y); } move(vec) { this.x += vec.x; this.y += vec.y; } } class Disk { constructor(level) { this.level = level; this.color = COLORS[level]; this.r = (DISK_R-POLE_R)*(NUM_OF_DISKS-level)/NUM_OF_DISKS + POLE_R; } } class MovingDisk extends Disk { constructor(level,from,to) { super(level); [sx,sy] = [from.pos.x,from.pos.y]; [dx,dy] = [to.pos.x, to.pos.y]; this.pos = new Position(sx,sy); this.mvec = new Vector((dx-sx)/STEPS,(dy-sy)/STEPS); this.move_ctr = 0; this.from = from; this.to = to; } step_forward() { this.pos.move(this.mvec); this.move_ctr++; } finish_p() { var ret_flag = false; if (ret_flag = (this.move_ctr == STEPS)) { this.to.disks.push(new Disk(this.level)); } return ret_flag; } } class Tower { constructor(name, disks, direction=null) { this.name = name; this.disks = []; for (var i = 0; i < disks; i++) { this.disks.push(new Disk(i)); } this.direction = direction; this.moving = false; this.flash_ctr = 0; } get toplevel() { var l = this.disks.length; // '-1' means there is no disk. return (l > 0) ? this.disks[l-1].level : -1; } } var src = new Tower('Source', NUM_OF_DISKS); var aux = new Tower('Auxiliary', 0, src); var dst = new Tower('Destination', 0, src); // In the case of NUM_OF_DISKS is odd, // the src must face the src. // Otherwise, the src faces the aux. src.direction = (COLORS.length % 2 == 1) ? dst : aux; // the reference to moving disk is stored to this variable. var moving_disk = null; function setup() { createCanvas(C_WIDTH, C_HEIGHT); frameRate(30); [src,aux,dst].forEach(function(t) { [rx,ry] = POSITIONS[t.name]; t.pos = new Position(rx * C_WIDTH, ry * C_HEIGHT); }) } function base_drawing() { background('beige'); [src,aux,dst].forEach(function(t) { // draw disks t.disks.forEach(function(d) { stroke('black'); fill(d.color); ellipse(t.pos.x,t.pos.y,2*d.r); }) // draw a pole stroke('brown'); fill(t.moving & (t.flash_ctr < FLASHING_COUNTER/2) ? 'gold' : 'white'); ellipse(t.pos.x,t.pos.y,2*POLE_R); // draw a direction stroke('navy'); [sx, sy] = [t.pos.x, t.pos.y]; [dx, dy] = [t.direction.pos.x, t.direction.pos.y]; r = POLE_R / Math.sqrt((dx-sx)*(dx-sx)+(dy-sy)*(dy-sy)); [dx, dy] = [(dx-sx)*r+sx, (dy-sy)*r+sy]; line(sx,sy,dx,dy); }) } function flash_poles() { [src,aux,dst].forEach(function(t) { t.moving = (t.direction.direction === t); t.flash_ctr += 1; t.flash_ctr %= FLASHING_COUNTER; }) } function pop_disk(src,aux,dst) { var towers = [src,aux,dst].filter(t => t.moving); var idx,from,to; idx = (towers[0].toplevel > towers[1].toplevel) ? 0 : 1; [from, to] = [towers[idx], towers[1-idx]]; return new MovingDisk(from.disks.pop().level,from,to); } function draw_moving_disk() { var d = moving_disk; d.step_forward(); stroke('black'); fill(d.color); ellipse(d.pos.x,d.pos.y,2*d.r); return d.finish_p(); } function turn() { [moving_disk.from,moving_disk.to].forEach(function(t) { t.direction = ([src,aux,dst] .filter(x => (x !== t) && (x !== t.direction)))[0]; t.moving = false; }) } function draw() { // base drawing base_drawing(); // find two exchange-towers out of three flash_poles(); // start moving if (moving_disk == null) { moving_disk = pop_disk(src,aux,dst); } else { if (draw_moving_disk()) { turn(); moving_disk = null; } } } |
図3 Pygameを用いて描画ウィンドウを表示するPythonコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import pygame pygame.init() screen = pygame.display.set_mode((640,480)) pygame.display.set_caption('Pygame Window') while True: for event in pygame.event.get(): if event.type == pygame.TEXTINPUT and event.text == 'q': pygame.quit() exit() screen.fill('lavender') pygame.display.flip() pygame.time.delay(30) |
図5 GDHPの手順をアニメーションで表示するPythonコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
import pygame import math COLORS = [ 'crimson', 'forestgreen', 'yellow', 'royalblue', 'saddlebrown', 'hotpink', 'darkorange', 'darkmagenta' ] NUM_OF_DISKS = len(COLORS) BASE_LENGTH = 200 C_WIDTH = 3.732 * BASE_LENGTH C_HEIGHT = 3.500 * BASE_LENGTH DISK_R = 0.9 * BASE_LENGTH POLE_R = 15 POSITIONS = { 'Source' : [0.268, 0.714], 'Auxiliary' : [0.500, 0.286], 'Destination' : [0.732, 0.714] } FLASHING_COUNTER = 20 STEPS = 30 class Vector: def __init__(self, x, y): self.x = x self.y = y class Position(Vector): def __init__(self, x, y): super().__init__(x, y) def move(self, vec): self.x += vec.x self.y += vec.y class Disk: def __init__(self, level): self.level = level self.color = COLORS[level] self.r = (DISK_R-POLE_R)*(NUM_OF_DISKS-level)/NUM_OF_DISKS + POLE_R class MovingDisk(Disk): def __init__(self, level, frm, to): super().__init__(level) [sx, sy] = [frm.pos.x, frm.pos.y] [dx, dy] = [to.pos.x, to.pos.y] self.pos = Position(sx,sy) self.mvec = Vector((dx-sx)/STEPS,(dy-sy)/STEPS) self.move_ctr = 0 self.frm = frm self.to = to def step_forward(self): self.pos.move(self.mvec) self.move_ctr += 1 def finish_p(self): ret_flag = (self.move_ctr == STEPS) if ret_flag: self.to.disks.append(Disk(self.level)) return ret_flag class Tower: def __init__(self, name, disks, direction=None): self.name = name self.disks = [] for i in range(disks): self.disks.append(Disk(i)) self.direction = direction self.moving = False self.flash_ctr = 0 def toplevel(self): l = len(self.disks) # '-1' means there is no disk. return self.disks[l-1].level if l > 0 else -1 def setup(): pygame.init() screen = pygame.display.set_mode((C_WIDTH, C_HEIGHT)) pygame.display.set_caption('GDHP') for t in [src,aux,dst]: [rx, ry] = POSITIONS[t.name] t.pos = Position(rx * C_WIDTH, ry * C_HEIGHT) return screen def base_drawing(): screen.fill('beige') for t in [src,aux,dst]: # draw disks for d in t.disks: pygame.draw.circle(screen, d.color, (t.pos.x,t.pos.y), d.r) pygame.draw.circle(screen, 'black', (t.pos.x,t.pos.y), d.r, 1) # draw a pole fillcolor = 'gold' \ if t.moving and t.flash_ctr < FLASHING_COUNTER/2 else 'white' pygame.draw.circle(screen, fillcolor, (t.pos.x,t.pos.y), POLE_R) pygame.draw.circle(screen, 'brown', (t.pos.x,t.pos.y), POLE_R, 1) # draw a direction [sx, sy] = [t.pos.x, t.pos.y] [dx, dy] = [t.direction.pos.x, t.direction.pos.y] r = POLE_R / math.sqrt((dx-sx)*(dx-sx)+(dy-sy)*(dy-sy)) [dx, dy] = [(dx-sx)*r+sx, (dy-sy)*r+sy] pygame.draw.line(screen, (0,0,128), (sx,sy), (dx,dy), 3) def flash_poles(): for t in [src,aux,dst]: t.moving = (t.direction.direction == t) t.flash_ctr += 1 t.flash_ctr %= FLASHING_COUNTER def pop_disk(src,aux,dst): towers = list(filter(lambda x: x.moving, [src,aux,dst])) idx = 0 if towers[0].toplevel() > towers[1].toplevel() else 1 [frm, to] = [towers[idx], towers[1-idx]] return MovingDisk(frm.disks.pop().level,frm,to) \ if len(frm.disks) > 0 else None def draw_moving_disk(): d = moving_disk d.step_forward() pygame.draw.circle(screen, d.color, (d.pos.x,d.pos.y), d.r) pygame.draw.circle(screen, 'black', (d.pos.x,d.pos.y), d.r, 1) return d.finish_p() def turn(): for t in [moving_disk.frm,moving_disk.to]: t.direction = list(filter( lambda x: (x != t) and (x != t.direction), [src,aux,dst]))[0] t.moving = False def draw(): # base drawing base_drawing() # find two exchange-towers out of three flash_poles() # start moving finish_p = False mdisk = moving_disk if mdisk == None: mdisk = pop_disk(src,aux,dst) if mdisk == None: finish_p = True else: if draw_moving_disk(): turn() mdisk = None return mdisk, finish_p # main routine if __name__ == '__main__': src = Tower('Source', NUM_OF_DISKS) aux = Tower('Auxiliary', 0, src) dst = Tower('Destination', 0, src) # In the case of NUM_OF_DISKS is odd, # the src must face the src. # Otherwise, the src faces the aux. src.direction = dst if len(COLORS) % 2 == 1 else aux # the reference to moving disk is stored to this variable. moving_disk = None screen = setup() while True: for event in pygame.event.get(): if event.type == pygame.TEXTINPUT and event.text == 'q': pygame.quit() exit() moving_disk, finish_p = draw() if finish_p: pygame.quit() exit() pygame.display.flip() pygame.time.delay(30) |
Markdownを活用する(Vol.91掲載)
著者:藤原 由来
本連載では文書の装飾・構造付けを手軽に行える記法であるMarkdownを用いて、さまざまな文書や成果物を作成する方法を紹介します。今回は、Markdownを用いて書籍を制作できる組版アプリ「Vivliostyle」の導入と、書籍作成の基本について説明します。
シェルスクリプトマガジン Vol.91は以下のリンク先でご購入できます。
図11 chap1.mdファイルの内容
1 2 3 4 5 6 7 8 |
# 後注と脚注の例 これは後注[^1]の例です。 インラインで後注^[これも章の後ろに表示される]もできます。 [^1]: 各章の後部にまとめて出力されるタイプの注釈 こちらは脚注<span class="footnote">ページ直下に表示される</span>です。 |
図12 chap2.mdファイルの内容
1 2 3 |
# {吾輩|わがはい}は猫である。 {吾輩|わがはい}は猫である。名前はまだ無い。 |
中小企業手作りIT化奮戦記(Vol.91掲載)
著者:佐藤 楓真
今回は、「焼きなまし法」と呼ばれる手法で、すべての都市を1回訪問して出発地点に帰るまでの最短経路を求める「巡回セールスマン問題」を近似的に解いてみます。焼きなまし法のベースとなる「山登り法」についても解説します。使用するプログラミング言語は、C++です。
シェルスクリプトマガジン Vol.91は以下のリンク先でご購入できます。
図3 図1の巡回セールスマン問題を山登り法で解くC++プログラムの例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
#include <iostream> #include <algorithm> using namespace std; #define TL 3 //制限時間 #define N 4 int d[N][N] = {{0, 2, 10, 5}, {2, 0, 5, 3}, {10, 5, 0, 4}, {5, 3, 4, 0}}; int ans[N+1] = {0, 1, 3, 2, 0}; int rand_num(void) { return rand() % N + 1; } int calc_dist(void) { int sum = 0; for (int i = 0; i < N; i++) { sum += d[ans[i]][ans[i+1]]; } return sum; } int main(void) { srand((unsigned)time(NULL)); int st_time = clock(); while ((clock() - st_time) / CLOCKS_PER_SEC < TL) { int L = rand_num(), R = rand_num(); if (L > R) swap(L, R); int bf_dist = calc_dist(); reverse(ans + L, ans + R); int af_dist = calc_dist(); if (bf_dist < af_dist) reverse(ans + L, ans + R); } for (int i = 0; i < N+1; i++) { cout << ans[i] + 1 << " "; } cout << endl << calc_dist() << endl; return 0; } |
図4 都市数「5000」のテスト用データを作成するC++プログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
#include <iostream> #include <vector> #include <fstream> #include <string> using namespace std; #define N 5000 int main(void) { srand((unsigned)time(NULL)); for (int num = 1; num <= 100; num++) { vector<vector<int>> d(N, vector<int>(N)); for (int i = 0; i < N; i++) { for (int j = i; j < N; j++) { if (i == j) { d[i][j] = 0; } else { d[i][j] = rand() % 5000 + 1; } } } for (int i = 0; i < N; i++) { for (int j = 0; j < i; j++) { d[i][j] = d[j][i]; } } string name = "case" + to_string(num) + ".txt"; ofstream out(name); out << N << endl; for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { out << d[i][j] << " "; } out << endl; } } return 0; } |
図5 テスト用データに基づく巡回セールスマン問題を山登り法で解くC++プログラムの例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
#include <iostream> #include <algorithm> #include <vector> #include <string> #include <fstream> using namespace std; #define TL 3 //制限時間 int calc_dist(vector<vector<int>>& dist, vector<int>& ans, int N) { int sum = 0; for (int i = 0; i < N; i++) { sum += dist[ans[i]][ans[i+1]]; } return sum; } int rand_num(int N){ return rand() % N + 1; } int main(void) { long long ave = 0; for (int num = 1; num <= 100; num++) { srand((unsigned)time(NULL)); string name = "case" + to_string(num) + ".txt"; ifstream infile(name); int N; infile >> N; vector<vector<int>> d(N, vector<int>(N)); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { infile >> d[i][j]; } } vector<int> ans(N + 1); for (int i = 0; i < N; i++) ans[i] = i; ans[N] = 0; int st_time = clock(); while ((clock() - st_time) / CLOCKS_PER_SEC < TL) { int bf_dist = calc_dist(d, ans, N); int L = rand_num(N), R = rand_num(N); if (L > R) swap(L, R); reverse(ans.begin() + L, ans.begin() + R); int af_dist = calc_dist(d, ans, N); if (af_dist > bf_dist) reverse(ans.begin() + L, ans.begin() + R); } ave += calc_dist(d, ans, N); } cout << ave / 100.0 << endl; return 0; } |
図7 テスト用データに基づく巡回セールスマン問題を焼きなまし法で解くC++プログラムの例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
#include <iostream> #include <algorithm> #include <vector> #include <string> #include <fstream> #include <cmath> using namespace std; #define MAX_TMP 100.0 //初期温度 #define TL 3 //制限時間 int calc_dist(vector<vector<int>>& dist, vector<int>& ans, int N) { int sum = 0; for (int i = 0; i < N; i++) { sum += dist[ans[i]][ans[i+1]]; } return sum; } int rand_num(int N) { return rand() % N + 1; } double rand_p(void) { return (double)rand() / RAND_MAX; } int main(void) { srand((unsigned)time(NULL)); long long ave = 0; for (int num = 1; num <= 100; num++) { string name = "case" + to_string(num) + ".txt"; ifstream infile(name); int N; infile >> N; vector<vector<int>> d(N, vector<int>(N)); for (int i = 0; i < N; i++) { for (int j = 0; j < N; j++) { infile >> d[i][j]; } } vector<int> ans(N + 1); for (int i = 0; i < N; i++) ans[i] = i; ans[N] = 0; int st_time = clock(); //焼きなまし開始時間 while ((clock() - st_time) / CLOCKS_PER_SEC < TL ) { int bf_dist = calc_dist(d, ans, N); int L = rand_num(N), R = rand_num(N); if (L > R) swap(L, R); reverse(ans.begin() + L, ans.begin() + R); int af_dist = calc_dist(d, ans, N); double t = (double)(clock() - st_time) / CLOCKS_PER_SEC; //経過時間 double tmp = MAX_TMP - MAX_TMP * (t / TL); //現在の温度 double p = exp((bf_dist - af_dist) / tmp); //採用確率 if (rand_p() > p) reverse(ans.begin() + L, ans.begin() + R); } ave += calc_dist(d, ans, N); } cout << ave / 100.0 <<endl; return 0; } |
Vol.91 補足情報
特集2 ChatGPTで手軽に実現
クラウド型家計簿アプリ「Dr.Wallet」のサンプルデータがここからダウンロードできます。家計簿を付けていない人は、こちらを利用してください。なお、本サンプルデータ中のショップ名、電話番号、住所の情報はすべて架空のものです。実在の企業や事業者とは関係ありません。
情報は随時更新致します。
Markdownを活用する(Vol.90掲載)
著者:藤原 由来
本連載では文書の装飾や構造付けを手軽に行える記法である「Markdown」を用いて、さまざまな文書や成果物を作成する方法を紹介します。今回も前回に引き続き、Markdownベースの高機能なノートアプリ「Obsidian」を扱います。前回の内容を踏まえて、Obsidianの発展的な使い方を紹介します。
図11 デイリーノートのテンプレート(テンプレート_カレー日記)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
テンプレート_カレー日記 作成日時:{{date}} {{time}} ## 今日のカレー - メニュー: - 感想: - 評価:★★★☆☆ ## お店の情報 - 店名: - 住所: - 最寄り駅: - URL: |
香川大学SLPからお届け!(Vol.90掲載)
著者:小河原 直道
Raspberry Pi 5(以下、ラズパイ5)が日本でも発売され、私も購入しました。センサーとの接続やサーバーの構築をしてみたいと思ったため、湿温度/気圧/CO₂濃度を測定できるセンサーを接続し、その値をWebブラウザから確認できるシステムを作成しました。このシステムの作成ポイントについて解説します。
シェルスクリプトマガジン Vol.90は以下のリンク先でご購入できます。
図8 BME280のチップIDを読み出すPythonコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import spidev # 読み取りは8ビット目を「1」に, 書き込みは「0」にする def get_reg_addr(rw: str, addr: int) -> int: rw_flag = 0b1000_0000 if rw == "w": rw_flag = 0b0000_0000 return rw_flag | (addr & 0b0111_1111) spi = spidev.SpiDev() spi.open(0, 0) # bus0, cs0 spi.mode = 0 # mode0, CPOL=CPHA=0 spi.max_speed_hz = 10 * 10**6 # 10MHz bme_id = spi.xfer2([get_reg_addr("r", 0xD0), 0]) print(f"id: {hex(bme_id[1])}") spi.close() |
図9 BME280の気温データを読み出すPythonコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import spidev # 読み取りは8ビット目を「1」に, 書き込みは「0」にする def get_reg_addr(rw: str, addr: int) -> int: rw_flag = 0b1000_0000 if rw == "w": rw_flag = 0b0000_0000 return rw_flag | (addr & 0b0111_1111) spi = spidev.SpiDev() spi.open(0, 0) # bus0, cs0 spi.mode = 0 # mode0, CPOL=CPHA=0 spi.max_speed_hz = 10 * 10**6 # 10MHz # 湿度の測定を有効化。オーバーサンプリングx1 r = spi.xfer2([get_reg_addr("r", 0xF2), 0])[1] ctrl_hum = r & 0b1111_1000 | 0b001 spi.xfer2([get_reg_addr("w", 0xF2), ctrl_hum]) # 気圧、気温の測定を有効化。オーバーサンプリングx1、ノーマルモードに ctrl_meas = (0b001 << 5) + (0b001 << 3) + 0b11 spi.xfer2([get_reg_addr("w", 0xF4), ctrl_meas]) # 気温データの読み出し temp_raw_bins = spi.xfer2([get_reg_addr("r", 0xFA), 0, 0, 0]) temp_raw = (temp_raw_bins[1] << 8+4) | (temp_raw_bins[2] << 4) |\ ((temp_raw_bins[3]>>4) & 0b00001111) print(f"気温: {temp_raw}") spi.close() |
図10 MH-Z19CのCO2濃度データを読み出すPythonコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
import serial import sys import time serial = serial.Serial(port="/dev/ttyAMA0", timeout=5) # 念のためバッファをリセットする serial.reset_input_buffer() serial.reset_output_buffer() time.sleep(2) # 待ち時間は適宜増減して調整 s = bytearray.fromhex("FF 01 86 00 00 00 00 00 79") serial.write(s) r = serial.read(size=9) serial.close() if r[0] != 0xFF: print("invalid data\n") sys.exit(-1) for_check=0 for i, b in enumerate(r): if i==0 or i==8: continue # チェックサムは1バイトのみ、あふれた分は捨てる for_check = for_check + b & 0b1111_1111 for_check = 0xFF - for_check + 1 if r[8] != for_check: print("checksum error\n") sys.exit(-1) print(f"{r[2]*256 + r[3]} ppm") |
図12 測定データをデータベースに保存するPythonコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import mariadb import sys try: conn = mariadb.connect( user = ユーザー名, password = パスワード, host = "127.0.0.1", port = 3306, database = "sensor1", autocommit = True ) except mariadb.Error as e: print(f"db connect error: {e}") sys.exit(-1) cur = conn.cursor() sql = 'INSERT INTO sensor (datetime, temp, humid, press, co2) VALUES(NOW(), ?, ?, ?, ?)' データ(temp_data、humid_data、press_data、co2_data)取得用コードをここに記述 try: cur.execute(sql, (temp_data, humid_data, press_data, co2_data)) except Exception as e: print(f"insert error: {e}") finally: cur.close() conn.close() |
図13 「db/db_pool.js」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 |
const mariadb = require("mariadb"); const pool = mariadb.createPool({ host: "localhost", user: ユーザー名, password: パスワード, database: "sensor1", connectionLimit: 5, }); module.exports = pool; |
図14 「route/api_v1.js」ファイルに記述するコード(抜粋)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
const router = require("express").Router(); const pool = require("../db/db_pool"); function getColumnName(sensor){ switch(sensor){ case "all":return "*"; case "temperature": return "datetime, temp" case "humidity": return "datetime, humid" case "pressure": return "datetime, press" case "co2": return "datetime, co2" default: throw new Error("sensor name error"); } } router.get("/:sensor/latest", async(req, res) => { let sqlCol; try{ sqlCol = getColumnName(req.params.sensor); } catch (e) { return res.status(404).send(`bad sensor name`); } try{ let dbRes = await pool.query( `select {sqlCol} from sensor order by datetime desc limit 1` ); // クエリー結果の最初のレコードをJSON形式で送る return res.status(200).json(dbRes[0]); } catch(err) { return res.status(500).json({mes:err.toString()}); } }); module.exports = router; |
図15 「route/index.js」ファイルに記述するコード
1 2 3 4 5 6 7 8 |
const express = require('express'); const router = express.Router(); const path = require('path'); router.get("/", (req, res) => { res.sendFile(path.join(__dirname, "../view/index.html")); }); router.use("/api/v1", require("./api_v1")); module.exports = router; |
図16 「app.js」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 11 |
const express = require("express"); const app = express(); const path = require('path'); app.use("/", require("./route/index")); app.use(express.static(path.join(__dirname, "public"))); app.all("*", (req, res) => { return res.sendStatus(404); }); app.listen(3000, () => { console.log("Listening at port 3000"); }); |
Pythonあれこれ
筆者:飯尾 淳
本連載では「Pythonを昔から使っているものの、それほど使いこなしてはいない」という筆者が、いろいろな日常業務をPythonで処理することで、立派な「蛇使い」に育つことを目指します。その過程を温かく見守ってください。皆さんと共に勉強していきましょう。第20回では、データの可視化などにも活用できるWebアプリケーションフレームワーク「Streamlit」の、より進んだ使い方を紹介します。
シェルスクリプトマガジン Vol.90は以下のリンク先でご購入できます。
図4 プルダウンメニューでサブページに移動できるようにしたコード
1 2 3 4 5 6 7 8 9 |
import streamlit as st selected = st.selectbox('なに食べる?', ('-', 'ramen', 'curry', 'katsudon')) pages = {'ramen': 'pages/1_🍜_ramen.py', 'curry': 'pages/2_🍛_curry.py', 'katsudon': 'pages/3_🍚_katsudon.py' } if selected in pages.keys(): st.switch_page(pages[selected]) |
図8 「1_edit.py」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 |
import streamlit as st import pandas as pd df = pd.read_excel('TODO.xlsx') df = st.data_editor(df) clicked = st.button('保存', type='primary', on_click=lambda:\ df.to_excel('TODO.xlsx', index=False)) if (clicked): st.switch_page('index.py') |
図11 「2_new.py」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import streamlit as st import pandas as pd df = pd.read_excel('TODO.xlsx') title = st.text_input(label='タイトル') memo = st.text_area(label='内容') date = st.date_input(label='締切') done = False def append(): global df df_new = pd.DataFrame({'タイトル': [title], '内容': [memo], '締切': [date], '実施': [done]}) df = pd.concat([df, df_new]) df.to_excel('TODO.xlsx', index=False) clicked = st.button('登録', type='primary', on_click=append) if (clicked): st.switch_page('index.py') |
図11 「2_new.py」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import streamlit as st import pandas as pd df = pd.read_excel('TODO.xlsx') title = st.text_input(label='タイトル') memo = st.text_area(label='内容') date = st.date_input(label='締切') done = False def append(): global df df_new = pd.DataFrame({'タイトル': [title], '内容': [memo], '締切': [date], '実施': [done]}) df = pd.concat([df, df_new]) df.to_excel('TODO.xlsx', index=False) clicked = st.button('登録', type='primary', on_click=append) if (clicked): st.switch_page('index.py') |
Vol.90 補足情報
特別企画 AlmaLinux 9のインストール
p.36の「2024年6月31日」は「2024年6月30日」の誤りです。同じページの最初から4行目の「をして」は「として」の誤りです。p.37の右段下から2行目の「Ubuntu Server」は「AlmaLinux」の誤りです。お詫びして訂正します。
p.37で紹介したISOイメージの書き込みツール「balenaEtcher」の最新版(バージョン1.19.×)では、「PORTABLE」版が提供されていません。記事で紹介したバージョン1.18.11のPORTABLE版(balenaEtcher-Portable-1.18.11.exe)はここからダウンロードできます。
情報は随時更新致します。
シェルスクリプトマガジンvol.90 Web掲載記事まとめ
004 レポート 米OpenAI社の新LLM「GPT-4o」登場
005 レポート JPEGライブラリ「jpegli」公開
006 製品レビュー バイク用エアバッグ「TECH-AIR 5」
007 NEWS FLASH
008 特集1 2024年大注目のMicrosoft Copilot/三沢友治
018 特集2 NVIDIA NeMoで独自の生成AIを構築する/澤井理紀、藤田充矩 コード掲載
026 特集3 Raspberry Pi 5を使う/麻生二郎
036 特別企画 AlmaLinux 9のインストール/麻生二郎
042 Raspberry Pi Pico W/WHで始める電子工作/米田聡 コード掲載
046 Markdownを活用する/藤原由来 コード掲載
052 中小企業手作りIT化奮戦記/菅雄一
056 香川大学SLPからお届け!/小河原直道 コード掲載
062 Pythonあれこれ/飯尾淳 コード掲載
068 これだけは覚えておきたいLinuxコマンド/大津真
076 Techパズル/gori.sh
077 コラム「プロジェクトリーダーの役割」/シェル魔人
特集2 NVIDIA NeMoで独自の生成AIを構築する(Vol.90記載)
著者:澤井 理紀、藤田 充矩
大規模言語モデル(LLM)に基づく生成AIが注目を集めています。そうした生成AIを活用するには、LLMを特定のタスクやデータセットに最適化する「ファインチューニング」が欠かせません。本特集では、生成AIを支える技術を解説した上で、NVIDIAが提供する生成AI向けのフレームワーク「NVIDIA NeMo」を使って独自の生成AIをローカル環境に構築し、LLMをファインチューニングする方法を紹介します。
シェルスクリプトマガジン Vol.90は以下のリンク先でご購入できます。
図2 LLMダウンロード用のPythonスクリプト
1 2 3 4 5 6 7 8 9 10 |
import os from huggingface_hub import snapshot_download MODEL_DIR = "./models" os.makedirs(MODEL_DIR, exist_ok=True) snapshot_download( repo_id="elyza/ELYZA-japanese-Llama-2-7b", local_dir=f"{MODEL_DIR}/ELYZA-japanese-Llama-2-7b", local_dir_use_symlinks=False ) |
図4 PEFTに使用するデータセットの形式を変換するPythonスクリプト
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
import json import os import random INPUT_TRAIN = "./JGLUE/datasets/jcommonsenseqa-v1.1/train-v1.1.json" INPUT_VALID = "./JGLUE/datasets/jcommonsenseqa-v1.1/valid-v1.1.json" OUTPUT_DIR = "./data/jcommonsenseqa-v1.1" random.seed(42) os.makedirs(OUTPUT_DIR, exist_ok=True) def write_jsonl(fname, json_objs): with open(fname, 'wt') as f: for o in json_objs: f.write(json.dumps(o, ensure_ascii=False)+"\n") def form_question(obj): st = "" st += "### 指示:\n" st += "与えられた選択肢の中から、最適な答えを選んでください。" st += "出力は以下から選択してください:\n" st += f"- {obj['choice0']}\n" st += f"- {obj['choice1']}\n" st += f"- {obj['choice2']}\n" st += f"- {obj['choice3']}\n" st += f"- {obj['choice4']}\n" st += "### 入力:\n" st += f"{obj['question']}\n" st += "### 応答:" return st def prosess(input_path, train=False): with open(input_path) as f: dataset = [json.loads(line) for line in f.readlines()] processed = [] for data in dataset: prompt = form_question(data) answer = data[f"choice{data['label']}"] processed.append({"input": prompt, "output": f"{answer}"}) if train: random.shuffle(processed) train_ds = processed[:-1000] valid_ds = processed[-1000:] write_jsonl(f"{OUTPUT_DIR}/train-v1.1.jsonl", train_ds) write_jsonl(f"{OUTPUT_DIR}/valid-v1.1.jsonl", valid_ds) else: write_jsonl(f"{OUTPUT_DIR}/test-v1.1.jsonl", processed) return def main(): prosess(INPUT_TRAIN, train=True) prosess(INPUT_VALID) if __name__ == "__main__": main() |
Raspberry Pi Pico W/WHで始める電子工作(Vol.90掲載)
著者:米田 聡
本連載では、人気のマイコンボード「Raspberry Pi Pico W/WH」を活用していきます。同ボードは、無
線LANやBluetoothの通信機能を搭載し、入手しやすく価格も手頃なので、IoT機器を自作するのに最適なハードウエアです。第6回は、DHT-11で測った湿温度をグラフ化します。
シェルスクリプトマガジン Vol.90は以下のリンク先でご購入できます。
図2 10 分おきに湿温度を測定して配列に保存する「DhtDataLogger」クラスのコード(ddl.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
import dht import time import gc import micropython import _thread from machine import Pin from machine import RTC from machine import Timer DHT_PIN = 15 TICK = 10 SIZE = 144 class DhtDataLogger(): def __init__(self, dht_gpio = DHT_PIN, tick=TICK, size=SIZE): self.rtc = RTC() self.dht = dht.DHT11(Pin(dht_gpio)) self.data = [] self.tick = tick self.size = size # 1分おきにタイマー割り込みを呼ぶ self.timer = Timer(period=60*1000, mode=Timer.PERIODIC, callback=self._timer_handler) self.lock = _thread.allocate_lock() def _timer_handler(self, t): now = self.rtc.datetime() if now[5] % self.tick == 0: micropython.schedule(self._logger, now) def _logger(self, now): # ロックをかける self.lock.acquire() try: self.dht.measure() except OSError as e: pass # 測定日時 nowstr = "{0:02d}:{1:02d}:{2:02d}".format(now[4],now[5],now[6]) self.data.append([nowstr, self.dht.temperature(), self.dht.humidity()]) if len(self.data) > self.size: del self.data[0] # debug # print(self.data[-1]) # ロック解除 self.lock.release() # ガベージコレクト gc.collect() def get_data(self): if len(self.data) == 0: return None # 配列のコピーを返す rval = [] self.lock.acquire() for v in self.data: rval.append(v) self.lock.release() return rval |
図3 蓄積した湿温度をグラフとして表示する簡易Webサーバーのプログラム(main.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
import usocket as socket import network import time import ujson as json import gc from machine import Timer from ddl import DhtDataLogger http_header = ''' HTTP/1.1 200 OK\r Content-Type: text/html;charset=UTF-8\r Cache-Control: no-store, no-cache, max-age=0, must-revalidate, proxy-revalidate\r Connection: close\r \r ''' html_base=''' <!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <title>Pico W</title> </head> %s </html> ''' graph_body = ''' <body> <canvas id="DHTChart"></canvas> <script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/2.7.2/Chart.bundle.js"></script> <script> var dhtc = document.getElementById("DHTChart"); var DHTChart = new Chart(dhtc, { type: 'line', data: { labels: %s, datasets: [ { label: '気温(度)', data: %s, borderColor: "rgba(255,0,0,1)", backgroundColor: "rgba(0,0,0,0)" }, { label: '湿度(%)', data: %s, borderColor: "rgba(0,0,255,1)", backgroundColor: "rgba(0,0,0,0)" } ], }, options: { title: { display: true, text: '気温と湿度' }, scales: { yAxes: [{ ticks: { suggestedMax: 100, suggestedMin: -10, stepSize: 10, } }] }, } }); </script> </body> ''' logger = DhtDataLogger() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.bind(('', 80)) sock.listen(4) try: while True: conn, addr = sock.accept() connect_from = str(addr[0]) req = conn.recv(1024).decode() if req.startswith('GET / HTTP/1.1'): tlist = logger.get_data() if tlist is None: body = html_base % ("<body><h1>Data not ready</h1></body>") else: labels = [] temps = [] hums = [] for v in tlist: labels.append(v[0]) temps.append(v[1]) hums.append(v[2]) body = graph_body % (json.dumps(labels), json.dumps(temps), json.dumps(hums)) html = html_base % (body) # 送信 conn.send(http_header) conn.sendall(html) conn.close() else: conn.sendall('HTTP/1.1 404 Not Found\r\n') conn.close() gc.collect() except KeyboardInterrupt: pass sock.close() |
シェルスクリプトマガジンvol.89 Web掲載記事まとめ
004 レポート 進研ゼミに生成AI活用新サービス
005 レポート HHKB Studioのキートップカスタム
006 製品レビュー スマートリング「SOXAI RING 1」
007 NEWS FLASH
008 特集1 Amazon EC2入門/飯村望、清水祥伽
023 Hello Nogyo!
024 特集2 コンテンツクラウドのBox/有賀友三、結城亮史、葵健晴、辰己学
040 Markdownを活用する/藤原由来 コード掲載
050 Raspberry Pi Pico W/WHで始める電子工作/米田聡 コード掲載
054 中小企業手作りIT化奮戦記/菅雄一
058 香川大学SLPからお届け!/宇佐美志久眞
062 Pythonあれこれ/飯尾淳 コード掲載
068 Linux定番エディタ入門/大津真 コード掲載
076 Techパズル/gori.sh
077 コラム「顧客と一心同体の精神を育成」/シェル魔人
Raspberry Piを100%活用しよう(Vol.89掲載)
著者:米田 聡
本連載では、人気のマイコンボード「Raspberry Pi Pico W/WH」を活用していきます。同ボードは、無線LANやBluetoothの通信機能を搭載し、入手しやすく価格も手頃なので、IoT機器を自作するのに最適なハードウエアです。第5回は、DHT-11で測った湿温度をスマートフォンに通知します。
シェルスクリプトマガジン Vol.89は以下のリンク先でご購入できます。
図4 LINE Notifyを使って通知を送る「notify()」関数を定義したPythonプログラム
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import usocket as socket from urequests import post from micropython import const TOKEN = const("your_access_token") def notify(msg): url = 'https://notify-api.line.me/api/notify' rheaders = { 'Content-type':'application/x-www-form-urlencoded', 'Authorization':'Bearer ' + TOKEN } message = "message=" + msg req = post(url,headers=rheaders,data=message) return req.status_code |
図6 1時間おきに湿温度をスマートフォンに通知するサンプルプログラム(sample.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
from machine import Pin from machine import Timer from micropython import const from micropython import schedule import _thread import dht import line import time DHT_PIN = const(15) TICK = const(60) class Climate(): def __init__(self, dht_gpio=DHT_PIN, tick=TICK, callback=None, expire=TICK): self.dht = dht.DHT11(Pin(dht_gpio)) self.tick = tick self.lock = _thread.allocate_lock() self.timer = Timer(period=tick*1000, mode=Timer.PERIODIC, callback=self._timer_handler) self._humidity = 0 self._temperature = 0 self._measurement_dt = 0 self.measure() self._callback_func = callback self._expire = expire self._counter = 0 def set_callback(self, callback, expire): self._callback_func = callback self._expire = expire def measure(self, arg=0): self.dht.measure() self.lock.acquire() self._humidity = self.dht.humidity() self._temperature = self.dht.temperature() self._measurement_dt = time.time() self.lock.release() def _timer_handler(self,t): schedule(self.measure, 0) self._counter -= 1 if self._counter <= 0: self._counter = self._expire if self._callback_func is not None: schedule(self._callback_func, self) @property def temp(self): self.lock.acquire() rval = self._temperature self.lock.release() return rval @property def hum(self): self.lock.acquire() rval = self._humidity self.lock.release() return rval @property def measurement_date_time(self): self.lock.acquire() rval = self._measurement_dt self.lock.release() return rval def mycalback(c): tm = time.localtime(c.measurement_date_time) line.notify("temp=%d,hum=%d@%4d/%02d/%02d_%02d:%02d:%02d" % (c.temp, c.hum, tm[0],tm[1],tm[2],tm[3],tm[4],tm[5])) if __name__ == '__main__': cl = Climate(callback = mycalback) while(True): machine.idle() |
Pythonあれこれ(Vol.89掲載)
著者:飯尾 淳
本連載では「Pythonを昔から使っているものの、それほど使いこなしてはいない」という筆者が、いろいろな日常業務をPythonで処理することで、立派な「蛇使い」に育つことを目指します。その過程を温かく見守ってください。皆さんと共に勉強していきましょう。第19回では、データの可視化などにも活用できるWebアプリケーションフレームワーク「Streamlit」の基本的な使い方を紹介します。
シェルスクリプトマガジン Vol.89は以下のリンク先でご購入できます。
図7 「magic.py」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
import streamlit as st # 素のテキストはマークダウンで表示される(docstring以外) ''' # Magicという仕組み 説明を _markdown_ で書けます。 ''' '### 文字列や変数の取り扱い' a = st.number_input("整数aの値を入れてください:+1:", value=0) b = st.number_input("整数bの値を入れてください:+1:", value=0) x = a + b 'a + b = ', x # 文字列と変数の値を並べて直接描画する '### 表の取り扱い' import pandas as pd df = pd.DataFrame({'名前': ['あや','ゆみ','みなこ','とめ'], '年齢': [24,22,32,95]}) df # データフレームを直接描画する '### チャートの取り扱い' import matplotlib.pyplot as plt import numpy as np arr = np.random.normal(1, 1, size=100) fig, ax = plt.subplots() ax.hist(arr, bins=20) fig # チャートも直接描画できる |
図9 キャッシュを設定するコードの例
1 2 3 4 5 6 7 8 9 10 11 12 13 |
'### チャートの取り扱い' import matplotlib.pyplot as plt import numpy as np @st.cache_data def make_chart(): arr = np.random.normal(1, 1, size=100) fig, ax = plt.subplots() ax.hist(arr, bins=20) return fig fig = make_chart() fig # チャートも直接描画できる |
図10 山手線の駅の位置を地図上にプロットするWebアプリのコード
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import streamlit as st import pandas as pd import json with open('yamate.geojson','r') as f: data = json.load(f) coords = [f['geometry']['coordinates'] \ for f in data ['features'] if f['geometry']['type'] == 'Point'] map_data = pd.DataFrame(coords, columns=['lon', 'lat']) st.map(map_data) |
図12 ウィジェットをサイドバーに配置するコードの例
1 2 3 4 5 6 7 8 |
import streamlit as st selected = st.sidebar.selectbox( 'なに食べる?', ('ramen', 'curry', 'katsudon'), ) st.image(selected + '.jpg') |
Linux定番エディタ入門
筆者:大津 真
文章の作成やプログラミングに欠かせないのがテキストエディタ(以下、エディタ)です。この連載では、Linuxで利用できる定番エディタの特徴と使い方を解説していきます。最終回となる今回も、LinuxだけでなくWindowsやmacOSでも利用可能なGUIエディタの定番「Visual Studio Code(VSCode)」を引き続き紹介します。
シェルスクリプトマガジン Vol.89は以下のリンク先でご購入できます。
図1 「test1.md」ファイルに記述するテキストデータ
1 2 3 4 5 6 7 8 9 10 11 12 |
# VSCode 入門 VSCode はシンプルで扱いやすいエディタです。 ## いろいろなエディタ 1. VSCode 2. Vim 3. nano ## VSCode の特徴 - プラグインによる拡張 - ワークスペースによる管理 - コマンドパレットによるコマンド実行 |
Vol.88 補足情報
特集2 自宅Linuxサーバー構築
p.14の「Windows 10のサポートが終了する2024年6月末ごろ」は「Windows 10(バージョン22H2)のサポートが終了する2025年10月14日」の誤りです(詳しくサポート期間は、こちら)。お詫びして訂正いたします。
Windows 10(バージョン22H2)は、まだ1年半以上のサポートが受けられますので、安心してお使いください。
情報は随時更新致します。
シェルスクリプトマガジンvol.88 Web掲載記事まとめ
004 レポート Open Source Summit Japan開催
005 レポート 国内開発の日本語LLM
006 製品レビュー スマートテレビ「32S2」
007 NEWS FLASH
008 特集1 CentOS Linux 7からの移行先を考える/弦本春樹
013 Hello Nogyo!
014 特集2 自宅Linuxサーバー構築/麻生二郎 コード掲載
028 特別企画 ゆっくりMovieMaker4で映像制作 シナリオ作り編/ふる(FuruyamD)
038 Raspberry Pi Pico W/WHで始める電子工作/米田聡 コード掲載
042 大規模言語モデル/桑原滝弥、イケヤシロウ
044 Pythonあれこれ/飯尾淳 コード掲載
049 中小企業手作りIT化奮戦記/菅雄一
054 法林浩之のFIGHTING TALKS/法林浩之
056 タイ語から分かる現地生活/つじみき
062 香川大学SLPからお届け!/谷知紘
068 Linux定番エディタ入門/大津真
076 Techパズル/gori.sh
077 コラム「SELiunxとユニケージ」/シェル魔人
特集2 自宅Linuxサーバー構築(Vol.88記載)
著者:麻生 二郎
Linuxやサーバーソフト、インターネットの仕組みを知りたいのなら、Linuxサーバーを構築するのが一番です。使わなくなった古いノートパソコンを活用し、自宅内にLinuxサーバーを構築してみましょう。本特集では、その手順を分かりやすく紹介します。
シェルスクリプトマガジン Vol.88は以下のリンク先でご購入できます。
Part3 サーバーソフトを導入する
図1 Sambaの設定(/etc/samba/smb.conf)
1 2 3 4 5 6 7 8 9 10 11 |
[global] workgroup = SHMAG dos charset = CP932 unix charset = UTF8 [share] path = /var/share browsable = yes writable = yes create mask = 0777 directory mask = 0777 |
Pythonあれこれ(Vol.88掲載)
著者:飯尾 淳
本連載では「Pythonを昔から使っているものの、それほど使いこなしてはいない」という筆者が、いろいろな日常業務をPythonで処理することで、立派な「蛇使い」に育つことを目指します。その過程を温かく見守ってください。皆さんと共に勉強していきましょう。第18回では、米Google社が提供する機械学習フレームワーク「MediaPipe」の顔検出機能を用いて、お遊びのプログラム作成に挑戦します。
シェルスクリプトマガジン Vol.88は以下のリンク先でご購入できます。
図1 顔ランドマーク検出をするコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
import cv2 import mediapipe as mp spec = mp.solutions.drawing_utils.DrawingSpec(thickness=1, circle_radius=1) cap = cv2.VideoCapture(0) with mp.solutions.face_mesh.FaceMesh( min_detection_confidence=0.5, min_tracking_confidence=0.5) as face_mesh: while True: success, image = cap.read() if not success: print("Warning: No camera image"); continue image = cv2.cvtColor(cv2.flip(image, 1), cv2.COLOR_BGR2RGB) image.flags.writeable = False results = face_mesh.process(image) # Annotate the face mesh image.flags.writeable = True image = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) if results.multi_face_landmarks: for landmarks in results.multi_face_landmarks: mp.solutions.drawing_utils.draw_landmarks( image=image, landmark_list=landmarks, connections=mp.solutions.face_mesh.FACEMESH_TESSELATION, landmark_drawing_spec=spec, connection_drawing_spec=spec) cv2.imshow('MediaPipe FaceMesh', image) if cv2.waitKey(5) & 0xFF == 27: break cap.release() |
図4 「顔ハメ」アプリのコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
import cv2 import numpy as np from PIL import Image, ImageDraw import mediapipe as mp def scaleToHeight(img, height): h, w = img.shape[:2] width = round(w * (height / h)) dst = cv2.resize(img, dsize=(width, height)) return dst def convertToRGBA(src, type): return Image.fromarray(cv2.cvtColor(src, type))\ .convert('RGBA') def trimOutside(base, over, loc): drw = ImageDraw.Draw(base) drw.rectangle([(0, 0), (loc[0]-1, over.size[1]-1)], fill=(0, 0, 0)) drw.rectangle([(loc[0]+over.size[0], 0), (base.size[0]-1,over.size[1]-1)], fill=(0, 0, 0)) def overlayImage(src, overlay, location): # convert images to PIL format pil_src = convertToRGBA(src, cv2.COLOR_BGR2RGB) pil_overlay = convertToRGBA(overlay, cv2.COLOR_BGRA2RGBA) # conpose two images pil_tmp = Image.new('RGBA', pil_src.size, (0, 0, 0, 0)) pil_tmp.paste(pil_overlay, location, pil_overlay) trimOutside(pil_tmp, pil_overlay, location) result_image = Image.alpha_composite(pil_src, pil_tmp) # convert result to OpenCV format return cv2.cvtColor(np.asarray(result_image), cv2.COLOR_RGBA2BGRA) def decrementTimer(timer, image, p_idx): h, w = image.shape[:2] if timer < 0: p_idx = (p_idx + 1) % len(panels) return TIMER_INITIAL_VALUE, p_idx elif timer == 30: global still still = image cv2.rectangle(image, (0, 0), (w, h), (255,255,255), thickness=-1) return timer - 1, p_idx elif timer < 30: image = still return timer - 1, p_idx d, r = timer // 30, timer % 30 c = 255 / 60 * r + 128 cv2.putText(image, org=(int(w/2-100), int(h/2+100)), text=str(d), fontFace=cv2.FONT_HERSHEY_DUPLEX, fontScale=10.0, color=(c, c, c), thickness=30, lineType=cv2.LINE_AA) return timer - 1, p_idx # prepare the kao_hame_panels panels = [] panels.append(cv2.imread('img1.png', cv2.IMREAD_UNCHANGED)) panels.append(cv2.imread('img2.png', cv2.IMREAD_UNCHANGED)) panels.append(cv2.imread('img3.png', cv2.IMREAD_UNCHANGED)) # capture from a camera cap = cv2.VideoCapture(0) ret, frame = cap.read() # rescale the kao_hame image height, width = frame.shape[:2] for i in range(len(panels)): panels[i] = scaleToHeight(panels[i], height) p_idx = 0 panel = panels[p_idx] p_height, p_width = panel.shape[:2] # timing counter TIMER_INITIAL_VALUE = 119 timer = TIMER_INITIAL_VALUE with mp.solutions.face_mesh\ .FaceMesh(max_num_faces=1, refine_landmarks=True, min_detection_confidence=0.5, min_tracking_confidence=0.5)\ as face_mesh: while cap.isOpened(): success, image = cap.read() if not success: print("Ignoring empty camera frame.") continue image = cv2.flip(image, 1) location = ((width-p_width)//2, 0) image = overlayImage(image, panel, location) image2 = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results = face_mesh.process(image2) if results.multi_face_landmarks != None: timer, p_idx = decrementTimer(timer, image, p_idx) panel = panels[p_idx] p_height, p_width = panel.shape[:2] else: timer = TIMER_INITIAL_VALUE # reset timer # triming the image image = image[0:p_height, location[0]:location[0]+p_width] cv2.imshow('Virtual Face-in-Hole Cutout', image) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows() |
Raspberry Pi Pico W/WHで始める電子工作(Vol.88掲載)
著者:米田 聡
本連載では、人気のマイコンボード「Raspberry Pi Pico W/WH」を活用していきます。同ボードは、無
線LANやBluetoothの通信機能を搭載し、入手しやすく価格も手頃なので、IoT機器を自作するのに最適なハードウエアです。第4回は、モノクロ有機ELディスプレイと湿温度センサー「DHT-11」で日本語表示の湿温度計を作ります。
シェルスクリプトマガジン Vol.88は以下のリンク先でご購入できます。
図9 温度と湿度を日本語で表示するプログラム(temp_hum.py)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
from machine import Pin from machine import I2C from ssd1306 import SSD1306_I2C from display import PinotDisplay from pnfont import Font import dht import time # ELパネル i2c=I2C(0, freq=400000) ssd = SSD1306_I2C(128, 32, i2c) # pinotライブラリ fnt = Font('/fonts/shnmk12u.pfn') disp = PinotDisplay(panel = ssd, font = fnt) # DHT-11センサー sensor = dht.DHT11(Pin(15)) while True: os_error = False try: sensor.measure() except OSError as e: os_error = True line1 = "温度: {0:2d} C".format(sensor.temperature()) line2 = "湿度: {0:2d} %".format(sensor.humidity()) line3 = "DHT-11エラー" if os_error else " " # 温度表示 disp.locate(1, 0) disp.text(line1) # 湿度表示 disp.locate(1,12) disp.text(line2) # エラー表示 disp.locate(1,24) disp.text(line3) time.sleep(2) |
香川大学SLPからお届け!(Vol.87掲載)
著者:大村 空良
プログラミングにおいて重要な要素の一つが、プログラムの実行速度です。プログラムの実行速度は、より良いアルゴリズムを選択することで大きく改善できます。今回は、巡回セールスマン問題を解くC++プログラムを、アルゴリズムを変更して高速化した例を紹介します。
シェルスクリプトマガジン Vol.87は以下のリンク先でご購入できます。
図2 図1の巡回セールスマン問題を解くC++コード「sample1.cpp」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#include <bits/stdc++.h> using namespace std; int main() { int n = 4, ans = 1e9-1, sum; int a[4][4] = { {0, 3, 7, 8}, {3, 0, 4, 4}, {7, 4, 0, 3}, {8, 4, 3, 0} }; vector<int> v_ans, v = {0, 1, 2, 3}; do { //初期化 sum = 0; //距離を足す for (int i = 1; i < n; i++) { sum += a[v[i-1]][v[i]]; } sum += a[v[n-1]][v[0]]; //比較 if (ans > sum) { ans = sum; v_ans = v; } } while (next_permutation(v.begin(), v.end())); //結果表示 for_each(v_ans.begin(), v_ans.end(), [](int& item) { item++; }); copy(v_ans.begin(), v_ans.end(), ostream_iterator<int>(cout, "→")); cout << v_ans[0] << endl; cout << ans << endl; return 0; } |
図3 都市数を変えられるように図2のコードを拡張した「sample2.cpp」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <bits/stdc++.h> #include "sales.h" using namespace std; int main() { int ans = 1e9-1, sum; vector<int> v_ans; do { sum = 0; for (int i = 1; i < n; i++) { sum += a[v[i-1]][v[i]]; } sum += a[v[n-1]][v[0]]; if (ans > sum) { ans = sum; v_ans = v; } } while (next_permutation(v.begin(), v.end())); for_each(v_ans.begin(), v_ans.end(), [](int& item) { item++; }); copy(v_ans.begin(), v_ans.end(), ostream_iterator<int>(cout, "→")); cout << v_ans[0] << endl; cout << ans << endl; return 0; } |
図4 ヘッダーファイル生成用のPythonスクリプト「make_header.py」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
import random import sys text = "dummy" while not text.isdecimal(): print("整数値を入力してください: ", file=sys.stderr, end='') text = input() num = int(text) if num > 15 or num < 1: sys.exit(1) array = [[0] * num for i in range(num)] random.seed() for i in range(num): for j in range(num): if i != j: if array[j][i] != 0: array[i][j] = array[j][i] else: array[i][j] = random.randint(1, 9) print('#include <vector>') print('using namespace std;') print(f'int n = {num};') print(f'int a[{num}][{num}] = {{') for i in range(num): line = str(array[i])[1:-1] print('{' + line + '},') print('};') line = list(range(0, num)) line = str(line)[1:-1] print(f'vector<int> v = {{{line}}};') |
図5 フィボナッチ数を求めるC++コード「sample3.cpp」
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <bits/stdc++.h> using namespace std; unsigned long long fib(char n) { if ((n <= 0) || (n > 93)) return 0; if (n == 1) return 1; return fib(n - 1) + fib(n - 2); } int main() { char n = 5; cout << fib(n) << endl; return 0; } |
図7 動的計画法を用いて改良したコード「sample4.cpp」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <bits/stdc++.h> using namespace std; unsigned long long fib(char n) { if ((n <= 0) || (n > 93)) return 0; if (n == 1) return 1; unsigned long long f[128]; f[0] = 0; f[1] = 1; for (char i = 2; i <= n; i++) { f[i] = f[i - 1] + f[i - 2]; } return f[n]; } int main() { char n = 5; cout << fib(n) << endl; return 0; } |
図10 動的計画法を用いて改良したコード「sample5.cpp」
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include <bits/stdc++.h> #include "sales.h" using namespace std; int main() { int dp[(1<<n)][n]; for (int i = 0; i < (1<<n); i++) { for (int j = 0; j < n; j++) { dp[i][j] = 1e9-1; } } dp[0][0] = 0; for (int i = 0; i < (1<<n); i++) { for (int j = 0; j < n; j++) { for (int k = 0; k < n; k++) { if (i==0 || !(i & (1<<k)) && (i & (1<<j))) { if( j != k ) { dp[i | (1<<k)][k] = min(dp[i | (1<<k)][k], dp[i][j] + a[j][k]); } } } } } cout << dp[(1<<n) - 1][0] << endl; return 0; } |
シェルスクリプトマガジンvol.87 Web掲載記事まとめ
004 レポート Raspberry Pi 5が登場
005 レポート キーボード「HHKB Studio」が発売
006 製品レビュー 小型パソコン「X68000 Z PRODUCT EDITION BLACK MODEL」
007 NEWS FLASH
008 特集1 ゆっくりMovieMaker4で映像制作 編集作業編/ふる(FuruyamD)
014 特集2 電子帳簿保存法を知る/永禮啓大
021 Hello Nogyo!
022 特別企画 アプリバで業務システム開発/前佛雅人
032 Raspberry Pi Pico W/WHで始める電子工作/米田聡
035 行動経済学と心理学で円滑に業務を遂行/請園正敏
038 Pythonあれこれ/飯尾淳 コード掲載
044 法林浩之のFIGHTING TALKS/法林浩之
046 中小企業手作りIT化奮戦記/菅雄一 コード掲載
052 エコシステム/桑原滝弥、イケヤシロウ
054 香川大学SLPからお届け!/大村空良
060 タイ語から分かる現地生活/つじみき
066 ユニケージ通信/ぱぺぽ
070 Linux定番エディタ入門/大津真
076 Techパズル/gori.sh
077 コラム「ユニケージが実現・目指しているところ」/シェル魔人
Pythonあれこれ(Vol.87掲載)
著者:飯尾淳
本連載では「Pythonを昔から使っているものの、それほど使いこなしてはいない」という筆者が、いろいろな日常業務をPythonで処理することで、立派な「蛇使い」に育つことを目指します。その過程を温かく見守ってください。皆さんと共に勉強していきましょう。第17回では、データを記録して残す「永続化」の手法の続編として、O/Rマッパーを使ってデータベースにアクセスする方法について解説します。
シェルスクリプトマガジン Vol.87は以下のリンク先でご購入できます。
図3 宣言的マッピングをするコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
from typing import List from typing import Optional from sqlalchemy import ForeignKey from sqlalchemy import String from sqlalchemy.orm import DeclarativeBase from sqlalchemy.orm import Mapped from sqlalchemy.orm import mapped_column from sqlalchemy.orm import relationship class Base(DeclarativeBase): pass class User(Base): __tablename__ = "user_account" id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(String(30)) fullname: Mapped[Optional[str]] addresses: Mapped[List["Address"]] = relationship( back_populates="user", cascade="all, delete-orphan" ) def __repr__(self) -> str: return f"User(id={self.id!r},\ name={self.name!r}, fullname={self.fullname!r})" class Address(Base): __tablename__ = "address" id: Mapped[int] = mapped_column(primary_key=True) email_address: Mapped[str] user_id: Mapped[int] = mapped_column(ForeignKey("user_account.id")) user: Mapped["User"] = relationship(back_populates="addresses") def __repr__(self) -> str: return f"Address(id={self.id!r},\ email_address={self.email_address!r})" |
図4 データベースエンジンとなるオブジェクトを作成するコード
1 2 |
from sqlalchemy import create_engine engine = create_engine("sqlite:///a_db.sqlite3", echo=True) |
図5 発行されるSQL
1 2 3 4 5 6 7 8 9 10 11 12 13 |
CREATE TABLE user_account ( id INTEGER NOT NULL, name VARCHAR(30) NOT NULL, fullname VARCHAR, PRIMARY KEY (id) ) CREATE TABLE address ( id INTEGER NOT NULL, email_address VARCHAR NOT NULL, user_id INTEGER NOT NULL, PRIMARY KEY (id), FOREIGN KEY(user_id) REFERENCES user_account (id) ) |
図6 データ操作用オブジェクトを作成するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
from sqlalchemy.orm import Session with Session(engine) as session: spongebob = User( name="spongebob", fullname="Spongebob Squarepants", addresses=[Address(email_address="spongebob@sqlalchemy.org")], ) sandy = User( name="sandy", fullname="Sandy Cheeks", addresses=[ Address(email_address="sandy@sqlalchemy.org"), Address(email_address="sandy@squirrelpower.org"), ], ) patrick = User(name="patrick", fullname="Patrick Star") session.add_all([spongebob, sandy, patrick]) session.commit() |
図7 図6のコードを実行した際の画面出力(抜粋)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
BEGIN (implicit) INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id (略)('spongebob', 'Spongebob Squarepants') INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id (略)('sandy', 'Sandy Cheeks') INSERT INTO user_account (name, fullname) VALUES (?, ?) RETURNING id (略)('patrick', 'Patrick Star') INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id (略)('spongebob@sqlalchemy.org', 1) INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id (略)('sandy@sqlalchemy.org', 2) INSERT INTO address (email_address, user_id) VALUES (?, ?) RETURNING id (略)('sandy@squirrelpower.org', 2) COMMIT |
図8 簡単な検索をするコード
1 2 3 4 5 6 7 |
from sqlalchemy.orm import Session from sqlalchemy import select session = Session(engine) stmt = select(User).where(User.name.in_(["spongebob", "sandy"])) for user in session.scalars(stmt): print(user) |
図9 図8のコードを実行した際に発行されるSQL
1 2 3 4 5 |
BEGIN (implicit) SELECT user_account.id, user_account.name, user_account.fullname FROM user_account WHERE user_account.name IN (?, ?) (略) ('spongebob', 'sandy') |
図10 図8のコードを実行した際に表示される検索結果
1 2 |
User(id=1, name='spongebob', fullname='Spongebob Squarepants') User(id=2, name='sandy', fullname='Sandy Cheeks') |
図11 少し複雑な検索をするコード
1 2 3 4 5 6 7 |
stmt = ( select(Address) .join(Address.user) .where(User.name == "sandy") .where(Address.email_address == "sandy@sqlalchemy.org") ) sandy_address = session.scalars(stmt).one() |
図12 図11のコードを実行した際に発行されるSQL
1 2 3 4 |
SELECT address.id, address.email_address, address.user_id FROM address JOIN user_account ON user_account.id = address.user_id WHERE user_account.name = ? AND address.email_address = ? (略) ('sandy', 'sandy@sqlalchemy.org') |
図13 データ変更のシンプルなコード例
1 2 |
sandy_address.email_address = "sandy_cheeks@sqlalchemy.org" session.commit() |
図14 図13のコードを実行した際に発行されるSQL
1 2 3 |
UPDATE address SET email_address=? WHERE address.id = ? (略)('sandy_cheeks@sqlalchemy.org', 2) COMMIT |
図15 データ変更の少し複雑なコード例