Link

文字列操作とソケット通信(紹介編)

この資料は読み物です。 演習をやる上で使う関数とかが入っているため、読んでください。 多分既知の知識も多いと思うので、わかっている範囲は読み飛ばしてしまってかまいません。


1. ロボットを動かすにはどうしたらよいか?

 ふり子さんは大学4年生。春からI研究室に配属され、ロボットの研究とかをすることになります。彼女は春休みの今からやる気充分です。

 休みも折り返しになってきたある日、ふり子さんはふと思いました。

「私は春からロボットとかを研究するわけだけど、ロボットってどうやって動かすんだろう…?」

 情報工学科に通うふり子さんは、既にC言語やJavaを使ってプログラミングをしたことがあります。しかしそれらのプログラミングは計算やファイル操作、パソコン同士の通信などが目的であり、ロボットを動かすやつはやっていませんでした。

「パソコンを使ってロボットを操作するなら、通信は必要かな?あと、データを受け渡しする時に文字列処理を使いそう…」

 というわけでふり子さんは「通信」と「文字列処理」にテーマを絞って特訓することにしました。


 ちなみに、「通信」といっても今はいろいろあります。

 最も基本なものが「ソケット通信(TCP/IP)」。イーサネットケーブルや無線LANを介した通信です。TCPとUDPという方式をどこかしらで習ったと思います。勿論掘り下げていけばもっと下のレイヤから考えることもできますが、今井研のレベルならば大抵はTCPかUDPかというところから考え始めます。ロスの少ないTCP、速さのUDPといった比較が主流です。

 少しレイヤを上って「HTTP通信」。いわゆる「Web」というやつで、TCPの上に乗っかっています。レイヤが上な分当然処理速度は落ちますが、ライブラリが豊富で扱いやすいです。また、ブラウザを使った表示ができるため、ブラウザからロボットを操作することなんかもできます。ただし、HTTP通信は「ステートレス」といい、1回の通信ごとに接続が破棄されるためロボットの操作のように断続的な入力には不向きです。ただ、そうしたニーズに応える技術もまた開発されました。これに関しては後ほどお話します。

 もう一つの選択が「bluetooth」。短距離の無線通信規格であり、マウスやキーボード、イヤホンなどによく使われています。ソケット通信と比べ通信容量が小さく動画の配信などには向きませんが、一方で消費電力は小さく、ロボットの移動命令のような小さなデータの運搬には最適かもしれません。正直使っている人は見たことがないのですが、Raspberry Piをはじめとして対応機器も多く、一つの選択肢であることは間違いありません。

 ここでは「ソケット通信」を中心に見ていこうと思います。



2. C言語の文字列処理

 ふり子さんはとりあえず慣れ親しんだC言語の文字列処理から始めることにしました。

 ご存知のように、C言語には文字列型(String)が存在しません。全て文字型(Char)の固定長配列によって表されます。殆どの言語に可変長の文字列型が存在している今となっては、この仕様はとてもめんどくさいと感じるでしょう。

 文字列の結合、分割、検索などの処理に関しては、1からゴリゴリに実装するかstdio.hやstring.h, stdlib.hなどのライブラリを使用する必要があります。ここではライブラリの仕様を前提として、よく使う関数を見ていきましょう。


標準入出力: printf, scanf

【ライブラリ】stdio.h

【説明】  キーボードからプログラムへの入力、プログラムから黒画面への出力を司ります。GUIを使わない限り必須の関数ですね。  「%~~」と書くことで変数を同時に出力することができますが、この時に書き方を工夫することができます。例えばただ「%d」と書くと

	printf(%d, 1); 	→「1
	printf(%d, 123); 	→「123
	printf(%d, 13579); 	→「13579

 となりますが、これを「%5d」と桁数を指定することで

	printf(%5d, 1); 	→「        1
	printf(%5d, 123); 	→「    123
	printf(%5d, 13579); 	→「13579

 と書くことができ、見栄えが良くなります。数値を一覧で表示する時などに重宝するでしょう。
 「printf()」で使用される書式指定文字列

【例】 名前を入力すると挨拶を返すアプリ

#include <stdio.h>
int main() {
	char name[256]; // 最大255文字(終端を除く)の文字列
	printf(What is your name?: );
	scanf(%s, name);
	printf(Hello %s!\n, name);

	return 0;
}


ファイル入出力: fopen, fclose, fprintf, fscanf

【ライブラリ】stdio.h

【説明】  標準入出力と同様に、ファイル入出力もよく使用します。  fprintfとfscanfに関してはprintfやscanfとほぼ使い方が変わりませんが、使う前にfopenしてファイルポインタを獲得する必要があります。使った後にfcloseするのを忘れがちなのも注意です。

【例】 同じディレクトリにある「text.txt」から一行読み込んで画面に表示するアプリ

#include <stdio.h>

int main(){
	FILE *fp;	// ファイルポインタ
	char line[256];

	fp = fopen(text.txt, r);	// モードは読み込みの ”r”

	if (fp == NULL){
		printf(File not found.\n);
		return 0;	// 本当はエラーコードを返すのが礼儀正しい
	}

	fscanf(fp, %s, line);
	printf(%s, line);

	fclose(fp);	// 忘れずに
	return 0;
}


文字列の数値への変換: atoi, atof

【ライブラリ】stdlib.h

【説明】  入力で受け取った文字列を数値にしたいという需要は往々にしてあります。  scanfで直接数値に代入しても良いのですが、それができないという時もあり(使い回しなど)、そこで使えるのがatoi, atofです。atoiは整数型、atofは浮動小数型で取得が可能です。  ちなみにaはASCIIを指すそうです(by Wikipedia)。

【例】  入力で受け取った文字列に1足して出力するアプリ

#include <stdio.h>
#include <stdlib.h>
int main() {
	char numstr[256];
int num;
	printf(Input an integer: );
	scanf(%s, numstr);
	num = atoi(numstr);
	printf(%d\n, num + 1);

	return 0;
}

		(※scanf(%d, num)でも別に良い)


文字列の結合: strcat

【ライブラリ】string.h

【説明】  文字列の結合を行います。  strcat(文字列1, 文字列2)の形で書くことで、文字列1に文字列1+文字列2が代入されます。返り値は文字列1のポインタになります。

【例】 苗字と名前を結合するプログラム

#include <stdio.h>
#include <string.h>
int main() {
	char first_name[256];
	char last_name[256];
	printf(What is your first name?: );
	scanf(%s, first_name);
	printf(What is your last name?: );
	scanf(%s, last_name);
	printf(Hello %s!\n, strcat(first_name, last_name));

	return 0;
}

※注: この時点で変数first_nameにはfirst_name+last_nameが結合されたものが入ってしまっています。first_nameを使いまわしたい場合は別の場所にコピーし退避する必要があります。


特定文字での文字列の分割: strtok

【ライブラリ】string.h

【使用方法】 strtok(※, 区切り文字); ※…最初は分割対象の文字列を指定。2回目以降はNULLを指定。 返り値が分割された文字列のポインタとなる。

【説明】  文字列を特定の文字で分割してくれます。  例えばロボットから情報を受取る際、スペース区切りやカンマ区切りの文字列でデータが渡されることがあります。そうなった場合、strtokなどの関数で文字列を細切れにしてやる必要があります。  使い方に少し難があるので気をつけましょう。  また、strtokを使用すると分割対象の文字列が破壊されるので、使い回す場合は退避させる必要があります。

【例】 文章をスペースで区切り、単語ごとに表示するアプリ

#include <stdio.h>
#include <string.h>
int main() {
        char str[] = "This is a test string.";
        char *tp;

        tp = strtok( str, " " );
        while ( 1 ) {
               tp = strtok( NULL," " );
               if(tp == NULL) break;	// 文字列の終端
               printf(tp);
        }
        
        return 0;
}


3. pythonとの出会い

 ふり子さんは絶望しました。文字列を結合したり、分割するだけでもいちいち配列やらバッファオーバーフローやらに気をかけなければいけないなんて…こんなんじゃ、ソケット通信をやるだけで何時間もかかってしまいます。そんなのは嫌なのです。ふり子さんは早く特訓を終わらせてゲームがしたいのです。

「C言語なんてもうやりたくないよ…どこかにもっとシンプルに書けてライブラリが充実していて、すぐにいろんなことができる言語はないの?」

 そこに怪しいニシキヘビが突然現れました。

「pythonをやりなさい…」 「わかった」

 ふり子さんはpythonをやることにしました。


python :

オブジェクト指向:java, Ruby, PHP, C++

インタプリタ:ソースコードを書いたテキストを1行ずつ読み込んではコンピュータが実行出来る命令に変換しながら動いている >>> 対話モード

 さて、ご存知のようにC言語においてはエディタでファイルを編集し、GCCを通すことで実行できるようになります。pythonでも同様にファイルをコマンドに通して実行することもできますが、今回はちょっと違う使い方、対話モードというものを使って文法の確認をしていきましょう。

 ターミナルに以下のコマンドを打ち込んでください。

python3

 こう打つと以下のような表示が出てくると思います。

Python 3.6.5 (default, Apr  1 2018, 05:46:30)
[GCC 7.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

 これが対話モードです。その名の通り、コードを1行書くごとに逐次的に実行してくれるモードです。  対話モードで打ったコードは保存されないため、何度も使いまわしたいプログラムには不向きです。一方で今回のように、関数の挙動を知りたいときや一度限りの計算を行いたいときには余計なファイルを作らずに済む便利な機能です。

 それではお決まりの「Hello world」をやってみましょう。  後ほど詳しく説明いたしますが、pythonの標準出力は「print」です。  また、C言語やJavaと違いセミコロンは必要ありません。

>>> print("Hello, world!")
Hello, world!

 続いて変数を扱ってみましょう。  ここで重要なポイントなのですが、pythonではC言語の「int」のような「どういう型なのか」を指定する句が存在しません。代入されるものに沿って自動で型を決めてくれるからです。これを「動的型付け」といいます。すなわち、変数の宣言は次の一言で済みます。

>>> x = 19
>>>

 対話モードは1行ごとに処理されるように見えますが、前に宣言した変数はきちんと残ってくれています。確認してみましょう。

>>> x = 19
>>> print(x)
19

 ちなみに、対話モードを終わらせるときはexit()と打ちます(カッコも必要です)  対話モードでなく、スクリプトをファイルに保存して実行する際はpython3コマンドの引数にファイル名をつけます。

python3 script.py

 あとifと配列、辞書、forを説明してから、本題の文字列処理に移りたいと思います。


 pythonのifはカッコを必要とせず終端に:(コロン)が付きます。  また、if内部の行はインデント(タブ)を忘れずにつけてください。

>>> if x > 10:
... 	print(“xは10より大きい”)
...
xは10より大きい

 elseとelifも使用できます。elifはCやJavaでいうelse ifのことです。

>>> if x>20:
...     print("xは20より大きい")
... elif x>10:
...     print("xは10より大きいが20より小さい")
... else:
...     print("xは10より小さい")
...
xは10より大きいが20より小さい

 配列(リスト)は次のように表されます。

>>> list = [“ねこ”, “いぬ”, “キリン”, “ぞう”, “カバ”]
>>> list[1]
いぬ

 またリストと並んでpythonでよく使われるデータ形式が辞書型です。  辞書型とは「キー」と「値」が1対1で対応付けられているもので、例えば次のようなものです。

>>> eng_dict = {“ねこ”: “cat”, “いぬ”: “dog”, “キリン”: “giraffe”, “ぞう”: “elephant”, “カバ”: “hippopotamus”}

 次のように書くことで、「キー」から「値」を取得することができます。

>>> eng_dict[“ねこ”]
cat

 for文は、C言語と同じようにiを使う書き方の場合、次のようになります。

>>> for i in range(10):
...     print("Hello, world!")
...
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!
Hello, world!

 しかし例えば、「リストに含まれる要素それぞれに1度ずつアクセスしたい」といった使い方の場合、次のような書き方もできます。

>>> for animal in list:
...     print(animal)
...
ねこ
いぬ
キリン
ぞう
カバ

 もちろん、C言語的に書きたければ次のように書くこともできます。

>>> for i in range(len(list)):
...     print(list[i])
...
ねこ
いぬ
キリン
ぞう
カバ

※len(list)はlistに含まれる要素の個数を返してくれる関数です。


 それでは、pythonの文字列処理について解説していきます。



4. pythonの文字列処理


標準入出力: print, input, format

【説明】  pythonの標準出力は先程も使用したprintです。そして入力はinputです。  注意点として、C言語のprintfで出来たような値の埋め込みは出来ません。そのため後述の+演算子で結合するか、formatと呼ばれる関数を使用する必要があります。  formatの使い方は例えばこんな感じです。

print(number is {}.format(num))

 {}で囲まれた部分に数値が入ります。{}は複数個設けたり、C言語と同様に書式を指定することも可能です。

【例】 名前を入力すると挨拶を返すアプリ

print(What is your name?: , end=””) # end=””を第二引数に入れると改行なし
name = input()
print(Nice to meet you {}!”.format(name))


ファイル入出力: open, read, readlines, readline, write

【説明】 pythonのファイル入出力はwith open(ファイル名) as 変数で行います。 読み込みに関する関数は豊富にあり、 改行込みで一つの文字列として読み込む場合はread, 改行で切って全体を文字列のリストとして読み込む場合はreadlines, 1行ずつ読みたい場合はreadlineを複数回叩きます。

【例】  同じディレクトリにある「text.txt」から一行読み込んで画面に表示するアプリ  ついでにtry-exceptで例外処理を行い、ファイルが存在しない場合に対応しています。

try:
with open(text.txt) as file: #モードはデフォルトで読み込みになる
	print(file.readline)
except IOError:
	print(File not found.)

※ withでファイルを開いた場合は自動でcloseしてくれる


文字列の数値への変換: int, float

【説明】  文字列の数値への変換はint()関数やfloat()関数を使うとできます。  これらは文字列に限らず、intとfloatの相互変換などにも使えます。とても直感的でわかりやすいですね。

【例】  割愛


文字列の結合: +

【説明】  文字列の結合は+演算子でできます。strcatみたいな関数をいちいち使う必要がなくてとてもよいですね。というか文字列の結合に関しては大体の言語で+で済むのですが…(C++もそう)

【例】 printのところで書いた名前を入力すると挨拶を返すアプリを+演算子を使って書いてみる

print(What is your name?: , end=””) # end=””を第二引数に入れると改行なし
name = input()
print(Nice to meet you  + name + ”!”)


特定文字での文字列の分割や結合: split, join

【説明】  C言語のstrtokはめちゃくちゃ使い方に癖がありましたがpythonではどうでしょう。  pythonにはsplitという関数があります。

>>> str = “This is a test string”
>>> str.split()
[“This”, “is”, “a”, “test”, “string”]

 なんとシンプルなのでしょう。感動です。  引数に何も指定しない場合はスペースと改行で区切ってくれますが、文字を指定するとその文字で区切ってくれます。  ちなみに、逆に単語ごとのリストをスペースを入れて文にしたいときはjoinを使います。

>>> str_split = [“This”, “is”, “a”, “test”, “string”]
>>> “ ”.join(str_split)
This is a test string

 ちょっと変な呼び方ですが、「『” ”』というstring型に属するjoin関数を呼ぶ」という形を取っているためこのようになります。  splitとjoinはよく使う関数ですので覚えておきましょう。



5. pythonでソケット通信を書いてみよう

 今回ソケット通信を行うにあたって、pythonの「socket」ライブラリを使用します。socketライブラリは標準なので、コードの頭に次のように書くだけで使用が可能です。

import socket

 さて、今回はTCPを使った通信を取り扱います。  ロボットで使用する場合、速度重視や一対多通信を実現するためにUDPを使うこともありますが、UDPはTCPよりも(コネクションを確立しない分)楽なのでTCPさえやればUDPもできます。たぶん  TCP通信の流れは以下のとおりです。

サーバー側(受信を待つ側) ポートを指定してbindする 受信を待つ(listen) 通信が来たら受け入れ(accept)、アドレスとコネクションを保持する データを受信(receive)する あるいはデータを送信(send)する 切断する クライアント側(アドレスを指定する側) ポートとアドレスを指定して接続(connect)する データを受信(receive)する あるいはデータを送信(send)する 切断する  

これらをスクリプトにすると次のとおりです。

サーバー側

import socket

host = "127.0.0.1"
port = 8810

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as mysock:
    mysock.bind((host, port))
    mysock.listen(1)  # 1は待ち受けキュー数です 通信する相手の数より多くする必要はありません
    
    while True:
        # 通信を待つ
        clientsock, clientaddr = mysock.accept()
        
        data = clientsock.recv(1024)
        print(data)

クライアント側

import socket

host = 127.0.0.1
port = 8810

with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as mysock:
	mysock.connect((host, port))
	mysock.send(Hello, server!”)

 今回はクライアントからサーバーに一度だけメッセージを送信していますが、もちろんサーバー側からsendすることもできます。