hana_shinのLinux技術ブログ

Linuxの技術情報を掲載しています。特にネットワークをメインに掲載していきます。

pandasの使い方

1 はじめに

pandasとは、データ分析用ライブラリです。pandasで利用できるデータは、表計算ソフトExcelで扱うような表形式のデータです。pandasを使うと、Excelなどのファイルから表形式のデータを読み込み、集計、データ抽出、グラフの表示などが行えます。

2 検証環境

AlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

pythonの版数は以下のとおりです。

[root@server ~]# python -V
Python 3.9.16

3 事前準備

スシの名前、価格、注文数を記録したcsvファイルを作成します。なお、ikaの値段は意図的に空欄にしています。

[root@server ~]# cat sushi.csv
sushi,price,orders
ikura,500,2
aji,400,4
uni,700,2
toro,800,4
ika,,2
saba,200,2
kohada,200,2

4 csvファイルに対する操作

4.1 csvファイルを読み込む方法(read_csv)

csvファイルを読み込むためには、read_csvメソッドを使用します。read_csvメソッドの引数には、csvファイルのパスを指定します。

読み込んだファイル(sushi.csv)を表示するテストプログラムを作成します。なお、テストプログラム中のdfはDataFrameを表しています。Pandasには2つの主要なデータ構造があって、Series(シリーズ)が1次元のデータ、DataFrame(データフレーム)が2次元のデータに対応しています。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df)

テストプログラムを実行すると、sushi.csvの中身が表示されていることがわかります。

[root@server ~]# ./test.py
    sushi  price  orders
0   ikura  500.0       2
1     aji  400.0       4
2     uni  700.0       2
3    toro  800.0       4
4     ika    NaN       2
5    saba  200.0       2
6  kohada  200.0       2

4.2 csvファイルに書き込む方法(to_csv)

読み込んだファイル(sushi.csv)を/tmp直下に書き込むテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
df.to_csv('/tmp/sushi.csv')

テストプログラムを実行します。

[root@server ~]# ./test.py

/tmpにファイルが書き込まれたことがわかります。

[root@server ~]# cat /tmp/sushi.csv
,sushi,price,orders
0,ikura,500.0,2
1,aji,400.0,4
2,uni,700.0,2
3,toro,800.0,4
4,ika,,2
5,saba,200.0,2
6,kohada,200.0,2

次は、指定した列を/tmp直下のファイル(test.csv)に保存してみます。ここでは、sushi列のみをファイルに保存するテストぽプログラムを作成してみます。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
df.to_csv('/tmp/sushi.csv', columns=['sushi'])

テストプログラムを実行します。

[root@server ~]# ./test.py

テストプログラムを実行すると、sushi列がファイルに保存するされていることがわかります。

[root@server ~]# cat /tmp/sushi.csv
,sushi
0,ikura
1,aji
2,uni
3,toro
4,ika
5,saba
6,kohada

あと始末をします。

[root@server ~]# rm /tmp/sushi.csv

5 DataFrameの各種情報を取得する方法

5.1 DataFrameの要約情報を取得する方法(info)

infoメソッドは、DataFrameのデータ型やメモリ使用量などを表示するメソッドです。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.info())

テストプログラムを実行すると、DataFrameのデータ型やメモリ使用量が表示されていることがわかります。

[root@server ~]# ./test.py
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 7 entries, 0 to 6
Data columns (total 3 columns):
 #   Column  Non-Null Count  Dtype
---  ------  --------------  -----
 0   sushi   7 non-null      object
 1   price   6 non-null      float64
 2   orders  7 non-null      int64
dtypes: float64(1), int64(1), object(1)
memory usage: 296.0+ bytes
None

5.2 DataFrameの行数を求める方法(len)

len はPythonの組み込み関数であり、DataFrameの行数を返します。この関数には、シーケンス(文字列、バイト列、タプル、リスト、rangeなど)またはコレクション(辞書、集合、凍結集合など)を引数として渡します。

DataFrameの行数を求めるテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(len(df))

テストプログラムを実行すると、DataFrameの行数が7行であることがわかります。

[root@server ~]# ./test.py
7

5.3 DataFrameの列名を取得する方法(columns)

columnsはDataFrame の属性です。columns属性は、DataFrame内の列名を含むリストを返します。

CSVファイルからデータを読み込み、DataFrame内の列名を表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.columns)

テストプログラムを実行すると、列名を含むリストが表示されていることがわかります。

[root@server ~]# ./test.py
Index(['sushi', 'price', 'orders'], dtype='object')

次は、DataFrameのカラム数を求めるテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(len(df.columns))

テストプログラムを実行すると、DataFrameのカラム数が3であることがわかります。

[root@server ~]# ./test.py
3

5.4 行数・列数を取得する方法(shape)

shape はpandasのDataFrameの属性です。DataFrameのshape 属性は、行数と列数を含むタプルを返します。このタプルの最初の要素は行数(行の数)であり、2番目の要素は列数(列の数)です。

CSVファイルからデータを読み込み、そのデータの行数と列数を表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.shape)
print(df.shape[0])
print(df.shape[1])

テストプログラムを実行すると、読み込んだファイルの行数と列数が表示されていることがわかります。

[root@server ~]# ./test.py
(7, 3)
7
3

6 DataFrameの行データを取得する方法

6.1 先頭から指定した行数を取り出す方法(head)

headはDataFrameの最初のいくつかの行を取得するたのメソッドです。headに引数を指定しないと、DataFrameの最初の5行を取得します。

DataFrameの先頭2行を表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.head(2))

テストプログラムを実行すると、DataFrameの先頭2行が表示されていることがわかります。

[root@server ~]# ./test.py
   sushi  price  orders
0  ikura  500.0       2
1    aji  400.0       4

6.2 末尾から指定した行数を取り出す方法(tail)

headはDataFrameの最後のいくつかの行を取得するたのメソッドです。

DataFrameの末尾1行を表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.tail(1))

テストプログラムを実行すると、DataFrameの末尾1行が表示されていることがわかります。

[root@server ~]# ./test.py
    sushi  price  orders
6  kohada  200.0       2

6.3 DataFrameの指定した行をまとめて取り出す方法(iloc)

DataFrameのindexが2から4までの行を取り出してみます。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.iloc[2:4])

テストプログラムを実行すると、DataFrameの2行目から4行目までの行が表示されていることがわかります。

[root@server ~]# ./test.py
  sushi  price  orders
2   uni  700.0       2
3  toro  800.0       4

7 DataFrameの列データを取得する方法

DataFrameのsushi列を表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df['sushi'])

テストプログラムを実行すると、DataFrameのsushi列が表示されていることがわかります。

[root@server ~]# ./test.py
0     ikura
1       aji
2       uni
3      toro
4       ika
5      saba
6    kohada
Name: sushi, dtype: object

DataFrameのprice列を表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df['price'])

テストプログラムを実行すると、DataFrameのprice列が表示されていることがわかります。なお、ikaの価格は未設定なので、実行結果はNaNと表示されています。

[root@server ~]# ./test.py
0    500.0
1    400.0
2    700.0
3    800.0
4      NaN
5    200.0
6    200.0
Name: price, dtype: float64

8 DataFrameの基本統計量を取得する方法

8.1 列の最大値の取り出し方(max)

maxは、最大値を取得するSeriesの属性です。Seriesは、1次元のデータを表現するために使用するデータ構造です。

CSVファイルから寿司の価格データを読み込み、その価格(price列)の最大値を表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
series = df['price']
print(series.max())

テストプログラムを実行すると、価格の最大値は800円であることがわかります。

[root@server ~]# ./test.py
800.0

8.2 列の最小値の取り出し方(min)

minは、最小値を取得するSeriesの属性です。Seriesは、1次元のデータを表現するために使用するデータ構造です。

CSVファイルから寿司の価格データを読み込み、その価格(price列)の最小値を表示するテストプログラムを作成します。

200.0
[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
series = df['price']
print(series.min())

テストプログラムを実行すると、価格の最小値が200円であることがわかります。

[root@server ~]# ./test.py
200.0

8.3 平均値求める方法(mean)

meanは、平均値を取得するSeriesの属性です。Seriesは、1次元のデータを表現するために使用するデータ構造です。

CSVファイルから寿司の価格データを読み込み、その価格(preice列)の平均値を表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
series = df['price']
print(series.mean())

テストプログラムを実行すると、価格の平均値が467円であることがわかります。

[root@server ~]# ./test.py
466.6666666666667
[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
mean_price = (df['price']).mean()
mean_orders = (df['orders']).mean()

print("平均 price:", mean_price)
print("平均 orders:", mean_orders)
[root@server ~]# ./test.py
平均 price: 466.6666666666667
平均 orders: 2.5714285714285716

8.4 ユニークな値を調べる方法(unique)

uniqueは、Series内のユニークな値(重複のない値)を抽出するメソッドです。

CSVファイルから寿司の価格データを読み込み、ユニークな値を表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.price.unique())

テストプログラムを実行すると、200円のスシはsabaとkohadaの2つありますが、1つにまとめられていることがわかります。

[root@server ~]# ./test.py
[500. 400. 700. 800.  nan 200.]

9 フィルタリングする方法

9.1 比較演算子で行を抽出する方法(query)

queryは条件を指定してDataFrameからデータを抽出するためのメソッドです。

price列が300円未満のスシを表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.query('price < 300'))

テストプログラムを実行すると、300円未満のスシが表示されていることがわかります。

[root@server ~]# ./test.py
    sushi  price  orders
5    saba  200.0       2
6  kohada  200.0       2

次に、price列が400円以上のスシを表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.query('not price < 400'))

テストプログラムを実行すると、400円以上のスシが表示されていることがわかります。

[root@server ~]# ./test.py
   sushi  price  orders
0  ikura  500.0       2
1    aji  400.0       4
2    uni  700.0       2
3   toro  800.0       4
4    ika    NaN       2

9.2 完全一致によるフィルタリング方法

sushi列がikuraの行を抽出するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
raw = (df.sushi == 'ikura')
print(df[raw])

テストプログラムを実行すると、ikuraの行が表示されていることがわかります。

[root@server ~]# ./test.py
   sushi  price  orders
0  ikura  500.0       2

price列が200円のスシを表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
raw = (df.price == 200)
print(df[raw])

テストプログラムを実行すると、price列が200円のスシが表示されていることがわかります。

[root@server ~]# ./test.py
    sushi  price  orders
5    saba  200.0       2
6  kohada  200.0       2

10 DataFrameをソートする方法

10.1 昇順にソートする方法

DataFrameのprice列を昇順にソートテストプログラムを作成します。デフォルトは昇順でソートします。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.sort_values('price')

テストプログラムを実行すると、price列が昇順に表示されていることがわかります。

[root@server ~]# ./test.py
    sushi  price  orders
5    saba  200.0       2
6  kohada  200.0       2
1     aji  400.0       4
0   ikura  500.0       2
2     uni  700.0       2
3    toro  800.0       4
4     ika    NaN       2

10.2 降順にソートする方法

DataFrameの価格(price)を降順にソートするテストプログラムを作成します。ascending=Falseを指定すると、降順にソートすることができます

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.sort_values('price', ascending=False))

テストプログラムを実行すると、価格が降順になっていることがわかります。

[root@server ~]# ./test.py
    sushi  price  orders
3    toro  800.0       4
2     uni  700.0       2
0   ikura  500.0       2
1     aji  400.0       4
5    saba  200.0       2
6  kohada  200.0       2
4     ika    NaN       2

次に、DataFrameのsushi列を降順に並べるテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.sort_values('sushi', ascending=False))

テストプログラムを実行すると、sushi列がz->aの順で表示されていることがわかります。

[root@server ~]# ./test.py
    sushi  price  orders
2     uni  700.0       2
3    toro  800.0       4
5    saba  200.0       2
6  kohada  200.0       2
0   ikura  500.0       2
4     ika    NaN       2
1     aji  400.0       4

11 DataFrameの値を置換する方法(replace)

replaceはSeriesやDataFrame内の値を指定した値で置換するためのメソッドです。

DataFrameのsushi列のtoroをohtoroに置換するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.replace('toro', 'ohtoro'))

テストプログラムを実行すると、toroがohtoroに置換されていることがわかります。

[root@server ~]# ./test.py
    sushi  price  orders
0   ikura  500.0       2
1     aji  400.0       4
2     uni  700.0       2
3  ohtoro  800.0       4
4     ika    NaN       2
5    saba  200.0       2
6  kohada  200.0       2

12 行列の名前を変更する方法(rename)

renameは、DataFrameの列名や行ラベルを変更するためのメソッドです。

12.1 列の名前を変更する方法

DataFrameの列名をpriceからkakakに変更するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
df_new = df.rename(columns={'price':'kakaku'})
print(df_new)

テストプログラムを実行すると、priceがkakakuに変更されたことがわかります。

[root@server ~]# ./test.py
    sushi  kakaku  orders
0   ikura   500.0       2
1     aji   400.0       4
2     uni   700.0       2
3    toro   800.0       4
4     ika     NaN       2
5    saba   200.0       2
6  kohada   200.0       2

次に、DataFrameの2つの列名、priceをkakaku、ordersをcyumonに変更してみます。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
df_new = df.rename(columns={'price':'kakaku','orders':'cyumon'})
print(df_new)

テストプログラムを実行すると、priceがkakaku、ordersがcyumonに変更されたことがわかります。

[root@server ~]# ./test.py
    sushi  kakaku  cyumon
0   ikura   500.0       2
1     aji   400.0       4
2     uni   700.0       2
3    toro   800.0       4
4     ika     NaN       2
5    saba   200.0       2
6  kohada   200.0       2

13 欠損値の扱い方

欠損値(NaN)の扱うメソッドに、isnull,dropna,fillna,notnullがあります。各メソッドについて動作確認をしてみます。

13.1 欠損値を判定する方法(isnull)

isnullは、各要素に対して判定を行い、欠損値(NaN)であればTrue、欠損値でなければFalseと判定します。

要素が欠損値ならTrue、欠損値でなければFalseを表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.isnull())

テストプログラムを実行すると、ikaの価格は欠損しているため、Trueと表示されていることがわかります。

[root@server ~]# ./test.py
   sushi  price  orders
0  False  False   False
1  False  False   False
2  False  False   False
3  False  False   False
4  False   True   False
5  False  False   False
6  False  False   False

13.2 欠損値を削除する方法(dropna)

dropnaは、指定した条件に合致するデータ列に欠損値(NaN)が存在するかどうかをチェックし、条件を満たす場合にそのデータ列を削除するメソッドです。

欠損値(NaN)が存在するデータ列を削除するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
print(df.dropna())

テストプログラムを実行すると、欠損値が含まれるikaの行が削除されたことがわかります。

[root@server ~]# ./test.py
    sushi  price  orders
0   ikura  500.0       2
1     aji  400.0       4
2     uni  700.0       2
3    toro  800.0       4
5    saba  200.0       2
6  kohada  200.0       2

13.3 列の欠損値を補完する方法(fillna)

fillnaは欠損値を任意の値で穴埋めするメソッドです。ここでは、ikaの価格(欠損値)を平均値で補完してみます。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('sushi.csv')
series = df['price']
mean = series.mean()
filled = series.fillna(mean)
print(filled)

テストプログラムを実行すると、ikaの価格が平均値(466.666667)で補完されていることがわかります。

[root@server ~]# ./test.py
0    500.000000
1    400.000000
2    700.000000
3    800.000000
4    466.666667
5    200.000000
6    200.000000
Name: price, dtype: float64

14 日付に関する操作方法

テストプログラムが読み込みファイルを作成します。

[root@server ~]# cat date.csv
id,date,order
01,2021-08-01,3
02,2021-08-10,2
03,2021-09-10,1
04,2021-10-11,4
04,2021-10-20,5

14.1 object型からdatetime64型に変換する方法(to_datetime)

to_datetimeは日付と時刻のデータをdatetimeオブジェクトに変換するためのメソッドです。

date列の型を確認するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df=pd.read_csv('date.csv')
print(df.dtypes)

date列はobject型であることがわかります。

[root@server ~]# ./test.py
id        int64
date     object
order     int64
dtype: object

次に、object型からdatetime64型に変換してみます。datetime64型に変換することで、例えば年だけを取得したい、月だけを取得したいということができます

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('date.csv')
df["date"] = pd.to_datetime(df["date"])
print(df.dtypes)

date列がobject型からdatetime64型に変換されたことがわかります。

[root@server ~]# ./test.py
id                int64
date     datetime64[ns]
order             int64
dtype: object

14.2 列を任意のフォーマットに一括変換する方法(strftime)

dt.strftimeは、列を任意のフォーマットの文字列に一括変換する関数です。%mで月のフォーマットを指定しています。年月の場合は%Y%mと指定します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df=pd.read_csv('date.csv')
df['date']=pd.to_datetime(df['date'])
df['month']=df['date'].dt.strftime('%m')
print(df)

年月日から月に変換されたmonth列が追加されたことがわかります。

[root@server ~]# ./test.py
   id       date  order month
0   1 2021-08-01      3    08
1   2 2021-08-10      2    08
2   3 2021-09-10      1    09
3   4 2021-10-11      4    10
4   4 2021-10-20      5    10

14.3 グルーピングする方法(groupby)

データをグルーピングして、グループごとにデータを集約して、それぞれの合計/最大/最小等の統計量を算出したりすることができます。

まず、注文数について、月単位の合計数を求めてみます。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df = pd.read_csv('date.csv')
df['date'] = pd.to_datetime(df['date'])
df['month'] = df['date'].dt.strftime('%Y-%m')

monthly_orders = df.groupby('month')['order'].sum()

print(monthly_orders)

注文数が、8月は5、9月は1、10月は9であることがわかります。

[root@server ~]# ./test.py
month
2021-08    5
2021-09    1
2021-10    9
Name: order, dtype: int64

次に、各月の一番多い注文数を求めるテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

df=pd.read_csv('date.csv')
df['date']=pd.to_datetime(df['date'])
df['month']=df['date'].dt.strftime('%m')
print(df.groupby('month').max()['order'])

各月の注文の最大数は、8月は3、9月は1、10月は5であることがわかります。

[root@server ~]# ./test.py
month
08    3
09    1
10    5
Name: order, dtype: int64

15 DataFrameを結合する方法(merge)

mergeはpandasライブラリのデータ結合操作を行うためのメソッドです。mergeメソッドを使用することで、異なるDataFrameオブジェクトのデータを共通のキー(列)に基づいて結合し、新しいDataFrameを生成できます。

名前と国語の点数を記録したkokugo.csvを作成します。

[root@server ~]# cat kokugo.csv
name,kokugo
kato,70
suzuki,80
tanaka,60

名前と算数の点数を記録したsansu.csvを作成します。

[root@server ~]# cat sansu.csv
name,sansu
kato,50
suzuki,70
tanaka,90

kokugo.csvとsansu.csvを結合するテストプログラムを作成します。共通する列をonに指定することで、紐づく値を軸に結合することができます。howでkokugoとsansuのどちらを主軸にするかを指定します。今回は、kokugoにsansuを結合するので、how='left'と指定します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd

kokugo = pd.read_csv('kokugo.csv')
sansu = pd.read_csv('sansu.csv')
print(pd.merge(kokugo,sansu, on='name', how='left'))

kokugo.csvとsansu.csvに共通のキー(nama)で結合されていることがわかります。

[root@server ~]# ./test.py
     name  kokugo  sansu
0    kato      70     50
1  suzuki      80     70
2  tanaka      60     90

16 その他

16.1 複数ファイルを読み込む方法

テストプログラムが読み込むファイルを作成します。

[root@server ~]# cat data1.csv
name,age
suzuki,40
kato,50

テストプログラムが読み込むファイルを作成します。

[root@server ~]# cat data2.csv
name,age
tanaka,30
itakahashi,20

テストプログラムを作成します。
globはPythonの組み込みライブラリです。指定したパターンに一致するファイルやディレクトリのリストを取得するために使用します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import pandas as pd
import glob

all_files=glob.glob('/root/data[0-9].csv')

li = []
for filename in all_files:
  df = pd.read_csv(filename, delimiter=",")
  li.append(df)
frame = pd.concat(li, ignore_index=True)
print(frame)

テストプログラムを実行すると、

[root@server ~]# ./test.py
         name  age
0      tanaka   30
1  itakahashi   20
2      suzuki   40
3        kato   50

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ

記事作成に参考にした書籍です。

サンプルコードが短く、分かりやすいです。

Pythonコードレシピ集(Kindle版)

Pythonコードレシピ集(単行本)

  • スッキリわかるPython入門 (スッキリわかる入門シリーズ)

簡潔な説明で分かりやすいです。

スッキリわかるPython入門 (スッキリわかる入門シリーズ)(単行本)

スッキリわかるPython入門 (スッキリわかるシリーズ)(Kindle版)

shredコマンドの使い方

1 shredコマンドとは?

shredコマンドは、データの復旧を困難にするためにファイルにランダムデータ(特別なパターン)を繰り返し書き込むコマンドです。通常、rmコマンドでファイルを削除すると、ファイルの内容は見かけ上は表示できなくなりますが、実際にはそのデータがディスク上に残ります。しかし、shredコマンドを使用すると、ディスク上のデータを繰り返しランダムデータで上書きすることでデータの回復ができなくなります。そのため、セキュリティを向上させることができます。

2 検証環境

AlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

shredコマンドの版数は以下のとおりです。

[root@server ~]# shred --version
shred (GNU coreutils) 8.32
Copyright (C) 2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

作者 Colin Plumb。

3 オプション一覧

shredコマンドのオプションは以下のとおりです。

[root@server ~]# shred --help
使用法: shred [OPTION]... FILE...
Overwrite the specified FILE(s) repeatedly, in order to make it harder
for even very expensive hardware probing to recover the data.

If FILE is -, shred standard output.

Mandatory arguments to long options are mandatory for short options too.
  -f, --force    書き込みができるように必要に応じてアクセス権限を変更する
  -n, --iterations=N  N 回上書きを繰り返す (デフォルト: 3 回)
      --random-source=FILE  ランダムバイトのソースを FILE にする
  -s, --size=N   N で指定したバイト数 shred を行う (接尾辞として K, M, G など
                 が使用可能)
  -u             deallocate and remove file after overwriting
      --remove[=HOW]  like -u but give control on HOW to delete;  See below
  -v, --verbose  show progress
  -x, --exact    do not round file sizes up to the next full block;
                   this is the default for non-regular files
  -z, --zero     add a final overwrite with zeros to hide shredding
      --help     この使い方を表示して終了する
      --version  バージョン情報を表示して終了する

4 オプションなしで実行する方法

オプションなしでshredコマンドを実行すると、4096バイトのランダムデータを3回ファイルに上書きします。

テスト用ファイルを作成します。

[root@server ~]# echo "12345" > /tmp/test.txt

作成したテスト用ファイルを確認します。

[root@server ~]# cat /tmp/test.txt
12345

オプションを付けずにshredコマンドを実行します。

[root@server ~]# shred /tmp/test.txt

shredコマンドを実行すると。ファイルは残ったままですが、ファイルサイズが4096byteになっていることがわかります。ファイルの中身を確認すると、ランダムデータで書き換えられていることがわかります。

[root@server ~]# ls -l /tmp/test.txt
-rw-r--r--. 1 root root 4096  9月 28 19:42 /tmp/test.txt

shredコマンド実行時の様子をstraceコマンドで確認すると、/tmp/test.txtをオープンした後、4096バイトのランダムデータを/tmp/test.txtに3回書き込んでいることが確認できます。straceコマンドの使い方は、straceコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# strace -ttT -f -e trace=openat,write shred /tmp/test.txt
-snip
19:57:58.315029 openat(AT_FDCWD, "/tmp/test.txt", O_WRONLY|O_NOCTTY) = 3 <0.000460>
19:57:58.317684 write(3, "\313r\234\10\330\2669\r]\36/\252\25\254V:w\235\3245\3712\270u\26\343\216\276Y\2623P"..., 4096) = 4096 <0.000195>
19:57:58.319256 write(3, "\214W\337H\253,J\22A\227\260\7L\317\301O`;\21Q[\30\5{\301t\32\226\270&\1\332"..., 4096) = 4096 <0.000191>
19:57:58.321464 write(3, "\35\204*O\372\315Z\366\302\26\300L \337\200\247\275\274U\222Rl\343A\320}!\217\301\361S\272"..., 4096) = 4096 <0.000867>
19:57:58.325854 +++ exited with 0 +++

5 ランダムデータのサイズを指定する方法(-s)

-sオプションは、ファイルに書き込むランダムデータのサイズを指定するオプションです。デフォルトでは、ランダムデータのサイズは4096バイトです。

テスト用ファイルを作成します。

[root@server ~]# echo "12345" > /tmp/test.txt

作成したテスト用ファイルを確認します。

[root@server ~]# cat /tmp/test.txt
12345

8192バイトのランダムデータをファイルに書き込んでみました。この操作をstraceコマンドで確認すると、writeシステムコールの戻り値が8192であることから、8192バイトのランダムデータがファイルに書き込まれていることが分かります。

[root@server ~]# strace -ttT -f -e trace=openat,write shred -s 8192 /tmp/test.txt
-snip-
20:07:06.514961 openat(AT_FDCWD, "/tmp/test.txt", O_WRONLY|O_NOCTTY) = 3 <0.000048>
20:07:06.515322 write(3, "\36\10\\2aA", 6) = 6 <0.000096>
20:07:06.517121 write(3, "W\241\346\225\362\322", 6) = 6 <0.000042>
20:07:06.518447 write(3, ".\3414\201\301h", 6) = 6 <0.000036>
20:07:06.521435 write(3, "n\317\371\303}\217\22\277.z\33\354\244K\373\234\325T=\340\234n\262W\263t\274\301\4?\5\35"..., 8192) = 8192 <0.000078>
20:07:06.522752 write(3, "N%\265\3331:Mkj\345f\34V/\20\20\325\205\340\322U\376o\321\365\270Nt\250\3778\f"..., 8192) = 8192 <0.000051>
20:07:06.523538 write(3, "\301\206\240\313f\254\226aV\216\267h\307/E\215\22\375`.o\363urf\350\360\t\212\216\354S"..., 8192) = 8192 <0.000041>
20:07:06.524505 +++ exited with 0 +++

6 進捗状況を表示する方法(-v)

-vオプションは、shredコマンドの進捗状況を表示するオプションです。

テスト用ファイルを作成します。

[root@server ~]# echo "12345" > /tmp/test.txt

作成したテスト用ファイルを確認します。

[root@server ~]# cat /tmp/test.txt
12345

ファイルにランダムデータを3回書き込んでいることが分かります。

[root@server ~]# shred -v /tmp/test.txt
shred: /tmp/test.txt: 経過 1/3 (random)...
shred: /tmp/test.txt: 経過 2/3 (random)...
shred: /tmp/test.txt: 経過 3/3 (random)...

7 ファイルを削除する方法(-u)

-uオプションは、ファイルにランダムにデータを書き込んだ後、ファイルを削除するオプションです。

テスト用ファイルを作成します。

[root@server ~]# echo "12345" > /tmp/test.txt

作成したテスト用ファイルを確認します。

[root@server ~]# cat /tmp/test.txt
12345

-uオプションを指定してshredコマンドを実行します。

[root@server ~]# shred -v -u /tmp/test.txt
shred: /tmp/test.txt: 経過 1/3 (random)...
shred: /tmp/test.txt: 経過 2/3 (random)...
shred: /tmp/test.txt: 経過 3/3 (random)...
shred: /tmp/test.txt: 削除しています
shred: /tmp/test.txt: /tmp/00000000 に名前が変更されました
shred: /tmp/00000000: /tmp/0000000 に名前が変更されました
shred: /tmp/0000000: /tmp/000000 に名前が変更されました
shred: /tmp/000000: /tmp/00000 に名前が変更されました
shred: /tmp/00000: /tmp/0000 に名前が変更されました
shred: /tmp/0000: /tmp/000 に名前が変更されました
shred: /tmp/000: /tmp/00 に名前が変更されました
shred: /tmp/00: /tmp/0 に名前が変更されました
shred: /tmp/test.txt: 削除しました

テスト用に作成したファイルを確認すると、ファイルが削除されたことがわかります。

[root@server ~]# ls -l /tmp/test.txt
ls: '/tmp/test.txt' にアクセスできません: そのようなファイルやディレクトリはありません

8 ランダムデータの書き込み回数を指定する方法(-n)

-nオプションは、ランダムデータをファイルに書き込む回数を指定するオプションです。オプションを指定しないとランダムデータを3回ファイルに書き込みます。

テスト用ファイルを作成します。

[root@server ~]# echo "12345" > /tmp/test.txt

作成したテスト用ファイルを確認します。

[root@server ~]# cat /tmp/test.txt
12345

straceコマンドを使用して、ランダムデータをファイルに書き込む回数を確認してみました。テスト用ファイルをオープンした後、テスト用ファイルと同じサイズ(6バイト)のランダムデータを5回書き込んだ後、4096バイトのランダムデータを5回書き込んでいることが分かります。

[root@server ~]# strace -ttT -f -e trace=openat,write shred -n 5 /tmp/test.txt
-snip-
10:30:12.169526 openat(AT_FDCWD, "/tmp/test.txt", O_WRONLY|O_NOCTTY) = 3 <0.000160>
10:30:12.170725 write(3, "\317;\236\216\"-", 6) = 6 <0.000230>
10:30:12.172615 write(3, "\0\0\0\0\0\0", 6) = 6 <0.000339>
10:30:12.174522 write(3, "~\263J\277\214\360", 6) = 6 <0.000169>
10:30:12.176208 write(3, "\377\377\377\377\377\377", 6) = 6 <0.000202>
10:30:12.178187 write(3, "\265\300-\330kB", 6) = 6 <0.000238>
10:30:12.180110 write(3, "0\274\344\337\225*\221\225\10\320\360\200\273\10@\341tc\251\"`\26\30\326\3z\233elTB1"..., 4096) = 4096 <0.000167>
10:30:12.182488 write(3, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"..., 4096) = 4096 <0.000234>
10:30:12.184468 write(3, "\226\v\320\26\20\314\232B\371\345\233r:\217~D\37\206\30r\225h\334\241\351\340j\210\252\36\32\310"..., 4096) = 4096 <0.000183>
10:30:12.187589 write(3, "\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377\377"..., 4096) = 4096 <0.000182>
10:30:12.189164 write(3, ".)\260^ U\30[+h!\263\206\3655\377\304\222\335j\4+@)\16Ij\273\210\n8\263"..., 4096) = 4096 <0.000357>
10:30:12.192114 +++ exited with 0 +++

9 ランダムデータで上書きをしたあと0で上書きする方法(-z)

-zオプションは、ランダムデータをファイルに書き込んだ後、最後にゼロをファイルに書き込むオプションです。

テスト用ファイルを作成します。

[root@server ~]# echo "12345" > /tmp/test.txt

作成したテスト用ファイルを確認します。

[root@server ~]# cat /tmp/test.txt
12345

-zオプションを指定してshredコマンドを実行します。このとき、-vオプションも使用してみます。ランダムデータをファイルに3回書き込んだ後、最後にゼロを書き込んでいることが分かります。

[root@server ~]# shred -v -z /tmp/test.txt
shred: /tmp/test.txt: 経過 1/4 (random)...
shred: /tmp/test.txt: 経過 2/4 (random)...
shred: /tmp/test.txt: 経過 3/4 (random)...
shred: /tmp/test.txt: 経過 4/4 (000000)...

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ

BeautifulSoupライブラリの使い方

1 Beautiful Soupとは?

Beautiful Soupは、HTMLファイルを解析および処理するためのライブラリです。このライブラリを使って、まずはローカルマシン上で基本的な使い方を練習し、その後、以下のウェブサイトから情報を抽出するテストプログラムを作成してみます。なお作成するテストプログラムですが、見やすくするため、例外処理等は実装していません。

  1. Anacondaのインストールシェルプログラムの一覧
  2. 歴代首相の名前の一覧
  3. 夏の高校野球全国高等学校野球選手権大会)歴代優勝校の一覧

2 検証環境

AlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

pythonの版数は以下のとおりです。

[root@server ~]# python -V
Python 3.9.16

3 BeautifulSoupのインストール方法

pipコマンドでbeautifulsoup4ライブラリをインストールします。

[root@server ~]# pip install beautifulsoup4

beautifulsoup4の版数を確認します。

[root@server ~]# pip show beautifulsoup4
Name: beautifulsoup4
Version: 4.12.2
Summary: Screen-scraping library
Home-page:
Author:
Author-email: Leonard Richardson <leonardr@segfault.org>
License:
Location: /usr/local/lib/python3.9/site-packages
Requires: soupsieve
Required-by:

4 事前準備

BeautifulSoupの使い方に慣れるため、ローカルマシンでHTMLファイルを作成して、それを読み込むテストプログラムを作成してみます。

[root@server ~]# vi index.html
[root@server ~]# cat index.html
<!DOCTYPE html>
<html>
<head>
    <title>サンプルHTML</title>
</head>
<body>
    <div id="chapter1">
        <h1>これは見出し1です</h1>
        <h2>これは見出し2です</h2>
        <h3>これは見出し3です</h3>
        <p>これは段落です。段落はテキストを表示するために使用されます。</p>
        <ul>
            <li>リスト項目1</li>
            <li>リスト項目2</li>
        </ul>
        <ol>
            <li>項目1</li>
            <li>項目2</li>
        </ol>
        <dl>
            <dt>用語1</dt>
            <dd>用語1の説明</dd>
        </dl>
    </div>
    <div id="chapter2">
      <a href="https://www.example.com" target="_blank">www.example.comへのリンクです。aタグにtarget属性が設定されています。クリックすると新しいタブが開きます</a>
      <br>
      <a href="https://www.kantei.go.jp" title="首相官邸">首相官邸へのリンクです。aタグにtitle属性が設定されています。マウスカーソルをリンクに合わせると、ツールチップが表示されます。</a>
    </div>
</body>
</html>

作成したHTMLファイルを/var/www/htmlにコピーします。

[root@server ~]# cp index.html /var/www/html/

httpdサービスを起動します。

[root@server ~]# systemctl start httpd

httpdプロセスがListenしているTCPの80番ポートを解放します。なお、firewall-cmdコマンドの使い方は、firewall-cmdの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# firewall-cmd --add-port=80/tcp
success

解放しているポート番号を確認します。80番ポートが解放されていることがわかります。

[root@server ~]# firewall-cmd --list-ports
80/tcp

httpdに対してcurlコマンドを実行すると、HTMLファイルを読み出していることがわかります。なお、curlコマンドの使い方は、curlコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# curl http://192.168.1.200
<!DOCTYPE html>
<html>
<head>
    <title>サンプルHTML</title>
-snip-

5 メソッドの使い方

Beautiful Soupオブジェクトは、HTMLファイルから特定の条件を満たすタグを取得するのに、findやfind_allというメソッドを利用することができます。

メソッド 概要
find 引数に指定した条件に合致する最初の要素を取得します。一致する要素が複数ある場合でも、最初に見つかった要素だけが返されます
find_all 指定した条件に合致するすべての要素を取得します。複数の要素が一致する場合、それらの要素がリストとして返されます。

5.1 findメソッドの使い方(タグ名を指定した場合)

指定したタグのタグオブジェクトおよびテキストを返すテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3

import requests
from bs4 import BeautifulSoup

response = requests.get("http://192.168.1.200/index.html")
soup = BeautifulSoup(response.content, "html.parser")
print(soup.find("title"))
print(soup.find("h1"))
print(soup.find("li"))

print(soup.find("title").text)
print(soup.find("h1").text)
print(soup.find("li").text)

テストプログラムを実行すると、1行目から3行目までがHTMLのタグを表しており、4行目から6行目までがそれらのタグに含まれるテキストを表示していることが確認できます。

[root@server ~]# ./test.py
<title>サンプルHTML</title>
<h1>これは見出し1です</h1>
<li>リスト項目1</li>
サンプルHTML
これは見出し1です
リスト項目1

5.2 findメソッドの使い方(属性を指定した場合)

属性がchapter2のコンテンツだけを表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3

import requests
from bs4 import BeautifulSoup

response = requests.get("http://192.168.1.200/index.html")
soup = BeautifulSoup(response.content, "html.parser")
content = soup.find(id="chapter2")
print(content)

テストプログラムを実行すると、chapter2という属性を持つコンテンツだけが表示されます。chapter1属性を持つコンテンツは表示されません。

[root@server ~]# ./test.py
<div id="chapter2">
<a href="https://www.example.com" target="_blank">www.example.comへのリンクです。<a>タグにtarget属性が設定されています。クリックすると新しいタブが開きます</a>
<br/>
<a href="https://www.kantei.go.jp" title="首相官邸">首相官邸へのリンクです。<a>タグにtitle属性が設定されています。マウスカーソルをリンクに合わせると、ツールチップが表示されます。</a>
</div>

5.3 find_allメソッドの使い方

liタグのテキストを全て表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3

import requests
from bs4 import BeautifulSoup

response = requests.get("http://192.168.1.200/index.html")
soup = BeautifulSoup(response.content, "html.parser")
for element in soup.find_all("li"):
    print(element.text)

テストプログラムを実行すると、全てのliタグのテキストが表示されていることがわかります。

[root@server ~]# ./test.py
リスト項目1
リスト項目2
項目1
項目2

6 タグから情報を取得する方法

タグから情報を取得するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3

import requests
from bs4 import BeautifulSoup

response = requests.get("http://192.168.1.200/index.html")
soup = BeautifulSoup(response.content, "html.parser")
content = soup.find("a")
print(content)
print(content.name)
print(content.text)
print(content.attrs)
print(content.get("href"))

テストプログラムを実行します。1行目はタグ、2行目はタグ名、3行目はタグのテキスト、4行目は属性、5行目は指定した属性の値が表示されていることがわかります。

[root@server ~]# ./test.py
<a href="https://www.example.com" target="_blank">www.example.comへのリンクです。aタグにtarget属性が設定されています。クリックすると新しいタブが開きます</a>
a
www.example.comへのリンクです。aタグにtarget属性が設定されています。クリックすると新しいタブが開きます
{'href': 'https://www.example.com', 'target': '_blank'}
https://www.example.com

7 実践練習1(Anacondのソースコードダウンロード)

Anacondaのウェブページで提供されている以下のソースコード(赤枠内)から、拡張子が.shのファイルをダウンロードするテストプログラムを作成します。
https://repo.anaconda.com/archive/

AnacondaのwebページからHTMLファイルを取得するテストプログラムを作成します。このテストプログラムを実行することで、Anacondaのwebページがどのような構成になっているかを確認することができます。

[root@server ~]# cat test.py
#!/usr/bin/python3

import requests
from bs4 import BeautifulSoup

response = requests.get("https://repo.anaconda.com/archive/")
soup = BeautifulSoup(response.content, "html.parser")
print(soup)

テストプログラムを実行します。このときlessコマンドを併用しながら、AnacondaのwebサイトのHTMLファイルの構成を確認します。拡張子が.shのファイルは、タグ内に存在することがわかります。

<tr>
<td><a href="Anaconda3-2023.07-2-Linux-x86_64.sh">Anaconda3-2023.07-2-Linux-x86_64.sh</a></td>
<td class="s">1015.6M</td>
<td>2023-08-04 10:56:05</td>
<td>589fb34fe73bc303379abbceba50f3131254e85ce4e7cd819ba4276ba29cad16</td>
</tr>
<tr>

テストプログラムを改良して、AnacondaのウェブサイトのHTMLからタグだけを表示するようにします。

[root@server ~]# cat test.py
#!/usr/bin/python3

import requests
from bs4 import BeautifulSoup

response = requests.get("https://repo.anaconda.com/archive/")
soup = BeautifulSoup(response.content, "html.parser")
a_tags = soup.find_all("a")
for a_tag in a_tags:
    print(a_tag)

テストプログラムを実行すると、タグの内容が表示されていることがわかります。

[root@server ~]# ./test.py
<a href=".winzip/">.winzip/</a>
<a href="Anaconda3-2023.07-2-Windows-x86_64.exe">Anaconda3-2023.07-2-Windows-x86_64.exe</a>
<a href="Anaconda3-2023.07-2-MacOSX-x86_64.sh">Anaconda3-2023.07-2-MacOSX-x86_64.sh</a>
<a href="Anaconda3-2023.07-2-MacOSX-x86_64.pkg">Anaconda3-2023.07-2-MacOSX-x86_64.pkg</a>
-snip-

テストプログラムを改良して、タグからhref属性で参照されるリンクだけを表示し、かつ拡張子が "sh" のリンクだけを抽出するようにします。

[root@server ~]# cat test.py
#!/usr/bin/python3

import requests
from bs4 import BeautifulSoup

response = requests.get("https://repo.anaconda.com/archive/")
soup = BeautifulSoup(response.content, "html.parser")

file_links = []
a_tags = soup.find_all("a")
for a_tag in a_tags:
    url = a_tag.get("href")
    if url.endswith(".sh"):
        file_links.append(url)
        print(url)

テストプログラムを実行すると、表示される内容は拡張子が.shであるリンクだけであることがわかります。

[root@server ~]# ./test.py
Anaconda3-2023.07-2-MacOSX-x86_64.sh
Anaconda3-2023.07-2-MacOSX-arm64.sh
Anaconda3-2023.07-2-Linux-x86_64.sh
-snip-

テストプログラムを改良して、ダウンロードするファイルを保存するディレクトリを作成します。そして、作成したディレクトリに拡張子が.shのファイルを2つだけ保存します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os
import requests
from bs4 import BeautifulSoup

# AnacondaのサイトからHTMLファイルを取得する。
archive_url = "https://repo.anaconda.com/archive/"
response = requests.get(archive_url)
soup = BeautifulSoup(response.content, "html.parser")

# 拡張子がshのファイルだけリンクをfile_linksに格納する。
file_links = []
a_tags = soup.find_all("a")
for a_tag in a_tags:
    url = a_tag.get("href")
    if url.endswith(".sh"):
        file_links.append(url)

# ダウンロードしたファイルを格納するディレクトリを作成する
download_dir = "./download/"
if not os.path.exists(download_dir):
    os.mkdir(download_dir)

# Anacondaのサイトから拡張子がshのファイルを2つダウンロードする。
for i, file_link in enumerate(file_links[:2], 1):
    file_name = file_link.split("/")[-1]
    file_url = archive_url + file_link
    file_path = download_dir + file_name
    print(f"ダウンロード {i}: {file_path} : {file_url}")
    response = requests.get(file_url)
    with open(file_path, "wb") as file:
        file.write(response.content)

テストプログラムを実行します。拡張子がshのファイルを2つだけダウンロードします。

[root@server ~]# ./test.py
ダウンロード 1: ./download/Anaconda3-2023.07-2-MacOSX-x86_64.sh : https://repo.anaconda.com/archive/Anaconda3-2023.07-2-MacOSX-x86_64.sh
ダウンロード 2: ./download/Anaconda3-2023.07-2-MacOSX-arm64.sh : https://repo.anaconda.com/archive/Anaconda3-2023.07-2-MacOSX-arm64.sh

ダウンロードしたファイルを確認します。

[root@server ~]# ls -l download/*
-rw-r--r--. 1 root root 676990792  9月 16 23:37 download/Anaconda3-2023.07-2-MacOSX-arm64.sh
-rw-r--r--. 1 root root 641855956  9月 16 23:36 download/Anaconda3-2023.07-2-MacOSX-x86_64.sh

8 実践練習2(歴代首相の一覧)

以下のページに記載されている歴代首相の名前と年を一覧として表示してみます。
https://www.kantei.go.jp/jp/rekidainaikaku/index.html

歴代首相が記載されていwebページからHTMLファイルを取得するテストプログラムを作成します。このテストプログラムを実行することで、webページがどのような構成になっているかを確認することができます。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os
import requests
from bs4 import BeautifulSoup

url = "https://www.kantei.go.jp/jp/rekidainaikaku/index.html"
response = requests.get(url)
soup = BeautifulSoup(response.content, "html.parser")
print(soup)

テストプログラムを実行すると、歴代首相の代と名前が以下の構成で繰り返し出現していることがわかります。

<div class="his-profile">
<h3 class="his-generation">第101代</h3>
<p class="his-name"><a href="/jp/rekidainaikaku/101.html">岸田 文雄</a></p>
</div>

テストプログラムを修正します。h3タグ、pタグのclass属性を指定して歴代首相の名前と代を抽出します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os
import requests
from bs4 import BeautifulSoup

url = "https://www.kantei.go.jp/jp/rekidainaikaku/index.html"
response = requests.get(url)
soup = BeautifulSoup(response.content, "html.parser")

prime_minister_elements = soup.find_all("div", class_="his-profile")

for i, prime_minister_element in enumerate(prime_minister_elements, start=1):
    generation_element = prime_minister_element.find("h3", class_="his-generation")
    name_element = prime_minister_element.find("p", class_="his-name")

    if generation_element and name_element:
        generation = generation_element.text.strip()
        name = name_element.text.strip()
        print(f"{generation}:{name}")

テストプログラムを実行します。

[root@server ~]# ./test.py
第101代:岸田 文雄
第100代:岸田 文雄
第99代:菅 義偉
第98代:安倍 晋三
第97代:安倍 晋三
第96代:安倍 晋三
第95代:野田 佳彦
第94代:菅 直人
第93代:鳩山 由紀夫
第92代:麻生 太郎
-snip-

9 実践練習3(高校野球歴代優勝校の一覧)

以下のページに記載されている歴代の優勝校と優勝年を一覧として表示してみます。
https://www.hanshin.co.jp/koshien/highschool/past/champion/summer.html

8章のようなテストプログラムを作成して実行すると、歴代の優勝高校と優勝年が以下の構成で繰り返し出現していることがわかります。

<tr>
<td>1</td>
<td>大正4年(1915)</td>
<td>京都二中(京都)</td>
</tr>

HTMLファイルの構成を考慮してテストプログラムを作成します。優勝年と優勝校はタグに含まれていることがわかります。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os
import requests
from bs4 import BeautifulSoup

url = "https://www.hanshin.co.jp/koshien/highschool/past/champion/summer.html"

response = requests.get(url)
soup = BeautifulSoup(response.content, "html.parser")

champion_list = soup.find_all("tr")
current_year_group = None

for row in reversed(champion_list):
    columns = row.find_all("td")
    if len(columns) == 3:
        year_text = columns[1].text.strip()
        school_text = columns[2].text.strip()

        year_start = year_text.find("(")
        year_end = year_text.find(")")
        if year_start != -1 and year_end != -1:
            year = year_text[year_start + 1:year_end]
            school = school_text.split("(")[0]
            print(f"{year}: {school}")

テストプログラムを実行すると、歴代の優勝高校と優勝年が表示されていることがわかります。

[root@server ~]# ./test.py
2022: 仙台育英
2021: 智辯和歌山
2020: 新型コロナウイルス感染症の流行により中止
2019: 履正社
2018: 大阪桐蔭
-snip-

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ

記事作成に参考にした書籍です。

  • Pythonコードレシピ集

サンプルコードが短く、分かりやすいです。
Pythonコードレシピ集(単行本)


Pythonコードレシピ集(Kindle版)

  • スッキリわかるPython入門 (スッキリわかる入門シリーズ)

簡潔な説明で分かりやすいです。

スッキリわかるPython入門 (スッキリわかる入門シリーズ)(単行本)

スッキリわかるPython入門 (スッキリわかるシリーズ)(Kindle版)

osモジュール,os.pathモジュールの使い方

1 はじめに

ファイルとディレクトリ操作の機能を提供するosモジュールとos.pathモジュールを使ったプログラムを作成して動作確認をしてみます。

2 検証環境

AlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.2 (Turquoise Kodkod)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-284.11.1.el9_2.x86_64

pythonの版数は以下のとおりです。

[root@server ~]# python -V
Python 3.9.16

3 osモジュールの使い方

osモジュールは、OSに依存する機能を提供するモジュールです。 ファイルやディレクトリの操作を行うことができます。osモジュールが提供する関数(一部)を以下に示します。

関数 概要
getcwd カレントディレクトリ取得する
chdir カレントディレクトリを変更する
listdir ディレクトリの中身をリストで取得する
walk ディレクトリツリーを取得する
mkdir ディレクトリを作成する
rmdir ディレクトリを削除する
makedirs ディレクトリを再帰的に作成する
rename ファイルやディレクトリの名前を変更する
chmod ファイルのパミッションを変更する

3.1 カレントディレクトリを取得、変更する方法(getcwd,chdir)

カレントディレクトリのパスを取得したあと、テストプログラムの引数に指定したパスにカレントディレクトリを変更するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

dir = os.getcwd()
print(dir)

dir = input("ディレクトリ名を入力してください: ")
os.chdir(dir)
dir = os.getcwd()
print(dir)

テストプログラムを実行すると、カレントディレクトリを表示したあと、指定したディレクトリにカレントディレクトリを変更したことがわかります。

[root@server ~]# ./test.py
/root
ディレクトリ名を入力してください: /var/log
/var/log

3.2 ファイル一覧を取得する方法(listdir)

引数に指定したディレクトリのファイル一覧を表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

dir = input("ディレクトリ名を入力してください: ")
files = os.listdir(dir)
print(files)

テストプログラムを実すると、/boot直下のファイルがリスト形式で表示されることがわかります。

[root@server ~]# ./test.py
ディレクトリ名を入力してください: /boot
['efi', 'grub2', 'loader', 'vmlinuz-5.14.0-284.11.1.el9_2.x86_64', 'System.map-5.14.0-284.11.1.el9_2.x86_64', 'config-5.14.0-284.11.1.el9_2.x86_64', '.vmlinuz-5.14.0-284.11.1.el9_2.x86_64.hmac', 'symvers-5.14.0-284.11.1.el9_2.x86_64.gz', 'initramfs-5.14.0-284.11.1.el9_2.x86_64.img', 'vmlinuz-0-rescue-ae58d7b0f9374e8d801cd68b6296a573', 'initramfs-0-rescue-ae58d7b0f9374e8d801cd68b6296a573.img', 'initramfs-5.14.0-284.11.1.el9_2.x86_64kdump.img']

リスト形式ではなく、lsコマンドを実行した時のように表示するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

dir = input("ディレクトリ名を入力してください: ")
files = os.listdir(dir)
for file in files:
    print(file)

テストプログラムを実行すると、/boot直下のファイルがlsコマンドを実行したときのように表示されることがわかります。

[root@server ~]# ./test.py
ディレクトリ名を入力してください: /boot
efi
grub2
loader
-snip-

3.3 ディレクトリのファイルを再帰的に表示する方法(walk)

テスト用のディレクトリ、ファイルを作成します。

[root@server ~]# mkdir -p dir1/dir2
[root@server ~]# touch dir1/file1.txt
[root@server ~]# touch dir1/dir2/file2.txt

テストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

for root, dirs, files in os.walk("dir1"):
    for dir in dirs:
        print("Directory:", os.path.join(root, dir))
    for file in files:
        print("File:", os.path.join(root, file))

テストプログラムを実行します。

[root@server ~]# ./test.py
Directory: dir1/dir2
File: dir1/file1.txt
File: dir1/dir2/file2.txt

3.4 ディレクトリを作成・削除する方法(mkdir,rmdir)

引数に指定したディレクトリの作成、削除をするテストプログラムを作成します。指定したディレクトリが存在しなければ作成しますが、存在すれば作成しません。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

new_dir = input("ディレクトリ名を入力してください: ")
if os.path.exists(new_dir):
  print(f"'{new_dir}' が存在するので、作成できません")
else:
  print(f"'{new_dir}' を作成します ")
  os.mkdir(new_dir)

テストプログラムを実行します。初回実行時は/tmp/testが存在しないので、ディレクトリを作成できます。

[root@server ~]# ./test.py
ディレクトリ名を入力してください: /tmp/test
'/tmp/test' を作成します

2回目は/tmp/testが存在するので、ディレクトリが作成できないことがわかります。

[root@server ~]# ./test.py
ディレクトリ名を入力してください: /tmp/test
'/tmp/test' が存在するので、作成できません

3.5 ディレクトリを再帰的に作成する方法(makedirs)

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

new_dir = input("ディレクトリ名を入力してください: ")
if not os.path.exists(new_dir):
    os.makedirs(new_dir)
    print(f"'{new_dir}' を作成します ")
else:
    print(f"'{new_dir}' が存在するので、作成できません")
[root@server ~]# ./test.py
ディレクトリ名を入力してください: /tmp/dir1/dir2/dir3
'/tmp/dir1/dir2/dir3' を作成します

作成したディレクトリを確認します。/tmp直下にdir1/dir2/dir3が作成されていることがわかります。

[root@server ~]# ls -ld /tmp/dir1/dir2/dir3
drwxr-xr-x. 2 root root 6  9月  9 22:25 /tmp/dir1/dir2/dir3

3.6 ファイルやディレクトリの名前を変更する方法(rename)

renameはファイルやディレクトリの名前を変更する関数です。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

old_filename = input("ファイル名を入力してください: ")

with open(old_filename, "w") as file:
    file.write("0123456789\n")

with open(old_filename, "r") as file:
    old_data = file.read()
    print(f"作成したファイルの内容:{old_data}")

new_filename = input("変更後のファイル名を入力してください: ")
os.rename(old_filename, new_filename)

テストプログラムを実行します。

[root@server ~]# ./test.py
ファイル名を入力してください: old.txt
作成したファイルの内容:0123456789

変更後のファイル名を入力してください: new.txt
[root@server ~]# cat new.txt
0123456789

4 os.pathモジュールの使い方

os.path モジュールはファイルパスやディレクトリパスを操作するための関数を提供するモジュールです。

関数 概要
exists ファイルやディレクトリが存在するかどうかを確認します
dirname ファイルパスからディレクトリ名を取得する
basename ファイルパスからファイル名を取得する
splitext ファイル名から拡張子を取り出します
relpath 相対パスを取得します
abspath 相対パス絶対パスに変換します
join パスを結合します
split パス名からディレクトリ名とファイル名を取り出します

4.1 ファイルやディレクトリの存在確認する方法(exitst)

引数に指定したディレクトリ名やファイル名が存在するかどうかを確認するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

path = input("パス名を入力してください: ")

if os.path.exists(path):
    print(f"{path} は存在します.")
else:
    print(f"{path} は存在しません.")

ディレクトリの存在確認をしてみます。

[root@server ~]# ./test.py
パス名を入力してください: /etc
/etc は存在します.

ファイルの存在確認をしてみます。

[root@server ~]# ./test.py
パス名を入力してください: /etc/chrony.conf
/etc/chrony.conf は存在します.

4.2 パス名からディレクトリ名とファイル名を取り出す方法(dirname,basename)

dirnameはパス名からディレクトリ名を取り出す関数、basenameはパス名からファイル名を取り出す関数です。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

path = input("パス名を入力してください: ")

dir = os.path.dirname(path)
file = os.path.basename(path)

print(f"dir={dir}, file={file}")

テストプログラムを実行します。パス名に/etc/chrony.confを指定すると、ディレクトリ名が/efc、ファイル名がchrony.confであることがわかります。

[root@server ~]# ./test.py
パス名を入力してください: /etc/chrony.conf
dir=/etc, file=chrony.conf

4.3 拡張子を取り出す方法(splitext)

拡張子を取りだすテストプログラムを実行します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

path = input("パス名を入力してください: ")
file_name, file_extension = os.path.splitext(path)

print(f"ファイル名:{file_name}, 拡張子:{file_extension}")

chrony.confファイルの拡張子を確認します。拡張子がconfであることがわかります。

[root@server ~]# ./test.py
パス名を入力してください: /etc/chrony.conf
ファイル名:/etc/chrony, 拡張子:.conf

カーネルモジュールの拡張子を確認します。拡張子がxzであることがわかります

[root@server ~]# ./test.py
パス名を入力してください: /usr/lib/modules/5.14.0-284.11.1.el9_2.x86_64/kernel/net/ipv4/ip_gre.ko.xz
ファイル名:/usr/lib/modules/5.14.0-284.11.1.el9_2.x86_64/kernel/net/ipv4/ip_gre.ko, 拡張子:.xz

4.4 パス名からディレクトリ名とファイル名を求めるする方法(split)

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

dir, file = os.path.split("/var/log/messages")
print(f"dir={dir}, file={file}")

テストプログラムを実行すると、/var/log/messagesからディレクトリ名(/var/log)とファイ名(messages)がそれぞれ取得できていることがわかります。

[root@server ~]# ./test.py
dir=/var/log, file=messages

4.5 パスを結合する方法(join)

joinメソッドは、引数で指定したパスを結合するメソッドです。この方法を使うと、ファイルシステムの階層構造に関する違いを気にせずにパスを作成できます。また、プラットフォーム間での互換性が確保されます。

joinメソッドの書式は以下のとおりです。

os.path.join("path1", "path2", "path3", ...)

"/"、"var"、"log”を結合するテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

log = os.path.join("/", "var", "log")
print(log)

テストプログラムを実行すると、引数に指定したパスが結合されて/var/logと表示されることがわかります。

[root@server ~]# ./test.py
/var/log

4.6 相対パス絶対パスを求める方法(relpath,abspath)

テスト用のディレクトリを作成します。

[root@server ~]# mkdir dir1/dir2/dir3

テスト用に作成したディレクトリの相対パス絶対パスを求めるテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3
import os

relative_path = "./dir1/dir2/dir3"
absolute_path = os.path.abspath(relative_path)

print(f"相対パス:{relative_path}, 絶対パス:{absolute_path}")

テストプログラムを実行すると、テスト用に作成したディレクトリの相対パス絶対パスが表示されることがわかります。

[root@server ~]# ./test.py
相対パス:./dir1/dir2/dir3, 絶対パス:/root/dir1/dir2/dir3

5 ファイルオブジェクトの使い方

5.1 read系メソッド

5.1.1 ファイル全体を読み込む方法(read)

readメソッドは、ファイル全体または指定したバイト数を読み込むためのメソッドです。readメソッドを引数なしで呼び出すとファイル全体を読み込みます。readメソッドに引数を指定すると、指定したバイト数を読み出します。

ファイル全体を読み出すテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3

with open("/tmp/test.txt", "r") as file:
    file_contents = file.read()
    print(file_contents)

テストプログラムが読み込むファイルを作成します。

[root@server ~]# vi /tmp/test.txt
[root@server ~]# cat /tmp/test.txt
111 111
222 222 222

テストプログラムを実行すると、ファイル全体読み込んでいることがわかります。

[root@server ~]# ./test.py
111 111
222 222 222

[root@server ~]#

次は、readメソッドの引数に2を指定して、ファイルから2バイトを読み込むテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3

with open("/tmp/test.txt", "r") as file:
    file_contents = file.read(2)
    print(file_contents)

テストプログラムを実行すると、ファイルから2バイト読み込んでいることがわかります。

[root@server ~]# ./test.py
11
5.1.2 ファイルから1行ずつ読み込む方法(readline)

readlineメソッドはファイルから1行ずつ読み込むためのメソッドです。ファイル終端に到達すると空文字を返します。

[root@server ~]# cat test.py
#!/usr/bin/python3

with open("/tmp/test.txt", "r") as f:
    line = f.readline()
    while line:
        print(line, end="")
        line = f.readline()

テストプログラムを実行します。

[root@server ~]# ./test.py
111 111
222 222 222
5.1.3 1行をリスト形式で取得する方法(readlines)

readlinesメソッドはファイルから全ての行を一度に読み取り、各行を文字列のリストとして返すためのメソッドです。

[root@server ~]# cat test.py
#!/usr/bin/python3

with open("/tmp/test.txt", "r") as f:
    lines = f.readlines()
    print(lines)

テストプログラムを実行します。

[root@server ~]# ./test.py
['111 111\n', '222 222 222\n']

5.2 write系メソッド

5.2.1 ファイルにデータを書き込む方法(write)

writeメソッドは、ファイルにデータを書き込むためのメソッドです。

引数に指定したファイルにデータを書き込むテストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3

file = input("ファイル名を入力してください: ")
with open(file, "w") as file:
    file.write("0123456789\n")

テストプログラムを実行して、/tmp/sample.txtを作成してみます。

[root@server ~]# ./test.py
ファイル名を入力してください: /tmp/sample.txt

作成したファイルの中身を確認します。

[root@server ~]# cat /tmp/sample.txt
0123456789

5.3 seekメソッド

seekメソッドは、ファイルオブジェクト内のカーソル位置を移動するためのメソッドです。書式は以下の通りす。

seek(offset, whence)
引数 意味
offset 移動するバイト数を指定します。正の整数値はファイルの先頭から移動し、負の整数値はファイルの末尾から逆方向に移動します
whence デフォルトは0でファイルの先頭からの相対位置を示します。1は現在の位置からの相対位置を示し、2はファイルの末尾からの相対位置を示します

テストプログラムを作成します。

[root@server ~]# cat test.py
#!/usr/bin/python3

seek = int(input("移動するバイト数を入力してください: "))
byte = int(input("読み込むバイト数を入力してください: "))

with open("/tmp/test.txt", "r") as f:
    f.seek(seek)
    data = f.read(byte)
    print(data)

テストプログラムが読み込むファイルを作成します。

[root@server ~]# vi /tmp/test.txt
[root@server ~]# cat /tmp/test.txt
0123456789

ファイル先頭から2バイト読み出してみます。

[root@server ~]# ./test.py
移動するバイト数を入力してください: 0
読み込むバイト数を入力してください: 2
01

ファイル先頭から1バイト移動してから5バイト読み出してみます。

[root@server ~]# ./test.py
移動するバイト数を入力してください: 1
読み込むバイト数を入力してください: 5
12345

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ

記事作成に参考にした書籍です。

サンプルコードが短く、分かりやすいです。

Pythonコードレシピ集(Kindle版)

Pythonコードレシピ集(単行本)

  • スッキリわかるPython入門 (スッキリわかる入門シリーズ)

簡潔な説明で分かりやすいです。

スッキリわかるPython入門 (スッキリわかる入門シリーズ)(単行本)

スッキリわかるPython入門 (スッキリわかるシリーズ)(Kindle版)

ipsetコマンドの使い方

1 ipsetコマンドとは?

ipsetコマンドは、IPアドレスやポート番号などをまとめて管理するためのツールです。ipsetを使用することで、これらの要素をセットとしてまとめることができます。作成したセットは、iptablesのsetモジュールで使用することが可能です。これにより、iptablesコマンドで個々のIPアドレスを1つずつ登録するのではなく、セットを使用して一括登録することができるようになります。

2 検証環境

AlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.1 (Lime Lynx)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-162.6.1.el9_1.x86_64

ipsetコマンドの版数は以下のとおりです。

[root@server ~]# ipset -v
ipset v7.11, protocol version: 7

3 オプション一覧

[root@server ~]# ipset help
ipset v7.11

Usage: ipset [options] COMMAND

Commands:
create SETNAME TYPENAME [type-specific-options]
        Create a new set
add SETNAME ENTRY
        Add entry to the named set
del SETNAME ENTRY
        Delete entry from the named set
test SETNAME ENTRY
        Test entry in the named set
destroy [SETNAME]
        Destroy a named set or all sets
list [SETNAME]
        List the entries of a named set or all sets
save [SETNAME]
        Save the named set or all sets to stdout
restore
        Restore a saved state
flush [SETNAME]
        Flush a named set or all sets
rename FROM-SETNAME TO-SETNAME
        Rename two sets
swap FROM-SETNAME TO-SETNAME
        Swap the contect of two existing sets
help [TYPENAME]
        Print help, and settype specific help
version
        Print version information
quit
        Quit interactive mode

Options:
-o plain|save|xml
       Specify output mode for listing sets.
       Default value for "list" command is mode "plain"
       and for "save" command is mode "save".
-s
        Print elements sorted (if supported by the set type).
-q
        Suppress any notice or warning message.
-r
        Try to resolve IP addresses in the output (slow!)
-!
        Ignore errors when creating or adding sets or
        elements that do exist or when deleting elements
        that don't exist.
-n
        When listing, just list setnames from the kernel.

-t
        When listing, list setnames and set headers
        from kernel only.
-f
        Read from the given file instead of standard
        input (restore) or write to given file instead
        of standard output (list/save).

Supported set types:
    list:set            3       skbinfo support
    list:set            2       comment support
    list:set            1       counters support
    list:set            0       Initial revision
-snip-

4 セットの作成、削除方法

セットはメソッドとデータタイプを指定して作成します。書式は以下のとおりです。

TYPENAME := method:datatype[,datatype[,datatype]]

・method:list,hash,bitmapを指定できます。
・datatype:ip,net,mac,port,ifaceを指定できます。

メソッドの各意味を以下のとおりです。

メソッド 意味
list セットをリスト形式で格納します
hash ハッシュテーブルを使用してセットを格納します。ハッシュメソッドは、IPアドレスやネットワークを格納するときに使用します
bitmap ビットマップを使用してセットを格納します。ポート番号やポート範囲を格納するときに使用します

4.1 セットを作成する方法(nまたはcreate)

名前がtest1、タイプがhash:ipのセットを作成してみます。

[root@server ~]# ipset n test1 hash:ip

作成したセットを確認します。test1という名前のセットが作成できたことがわかります。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0xcbb84ee8
Size in memory: 216
References: 0
Number of entries: 0
Members:

4.2 セットを削除する方法(xまたはdel)

セットの一覧を確認します。test1という名前のセットが存在することがわかります。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0xcbb84ee8
Size in memory: 216
References: 0
Number of entries: 0
Members:

test1という名前のセットを削除します。

[root@server ~]# ipset x test1

セットの一覧を確認します。test1という名前のセットが削除されたことがわかります。

[root@server ~]# ipset l
[root@server ~]#

4.3 セットを一括削除する方法(xまたはdel)

事前準備として、test1,test2という名前のセットを2つ作成しておきます。

[root@server ~]# ipset n test1 hash:ip
[root@server ~]# ipset n test2 hash:ip

セットの一覧を確認します。test1,test2という名前のセットが作成されたことがわかります。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0xa5a2e8c8
Size in memory: 216
References: 0
Number of entries: 0
Members:

Name: test2
Type: hash:ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0x0dad6e43
Size in memory: 216
References: 0
Number of entries: 0
Members:

2つのセットを一括で削除します。

[root@server ~]# ipset x

セットの一覧を確認します。test1,test2という名前のセットが削除されたことがわかります。

[root@server ~]# ipset l
[root@server ~]#

5 セットの名前を変更する方法(eまたはrename)

事前準備として、test1という名前のセットを作成しておきます。

[root@server ~]# ipset n test1 hash:ip

作成したセットを確認します。test1という名前のセットが作成できたことがわかります。

[root@server ~]# ipset l -n
test1

test1という名前のセットをtest2に変更します。

[root@server ~]# ipset e test1 test2

セットの一覧を確認します。test1という名前のセットがtest2に変更されたことがわかります。

[root@server ~]# ipset l -n
test2

あと始末をします。

[root@server ~]# ipset x

6 セットにエントリを追加/削除する方法

6.1 エントリを追加する方法(aまたはadd)

事前準備として、名前がtest1、タイプがhash:ipのセットを作成してみます。

[root@server ~]# ipset n test1 hash:ip

作成したセットを確認します。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 120
References: 0
Number of entries: 0
Members:
[root@server ~]#

セットにIPアドレスのエントリを2つ追加します。

[root@server ~]# ipset add test1 192.168.10.10
[root@server ~]# ipset add test1 192.168.10.20

セットを確認します。追加したエントリが2つ作成されたことがわかります。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 216
References: 0
Number of entries: 2
Members:
192.168.10.20
192.168.10.10

6.2 エントリを削除する方法(dまたはdel)

セットからエントリを1つ削除します。

[root@server ~]# ipset d test1 192.168.10.10

セットを確認します。エントリが1つ削除されたことがわかります。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 168
References: 0
Number of entries: 1
Members:
192.168.10.20

セットからもう1つエントリを削除します。

[root@server ~]# ipset d test1 192.168.10.20

セットの中身を確認します。エントリが全て削除されたことがわかります。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 120
References: 0
Number of entries: 0
Members:

6.3 エントリを一括削除する方法(fまたはflush)

セットを確認します。エントリが2つ存在することがわかります。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 216
References: 0
Number of entries: 2
Members:
192.168.10.20
192.168.10.10

エントリを一括で削除します。

[root@server ~]# ipset f test1

セットの中身を確認します。エントリが全て削除されたことがわかります。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 4
Header: family inet hashsize 1024 maxelem 65536
Size in memory: 120
References: 0
Number of entries: 0
Members:

7 セーブ、リストア方法

セットの内容をファイルに保存したり、ファイルに保存したセットをリストアすることができます。

7.1 セットの内容をファイルに保存する方法(save)

テスト用のセットを作成します。

[root@server ~]# ipset n test1 hash:ip
[root@server ~]# ipset a test1 192.168.10.1

作成したセットをファイルに保存します。

[root@server ~]# ipset save > ip_set.txt

ファイルに保存したセットの内容を確認します。

[root@server ~]# cat ip_set.txt
create test1 hash:ip family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0x1266b74c
add test1 192.168.10.1

7.2 リストアする方法(restore)

セットの一覧を確認します。セットが登録されていないことを確認します。

[root@server ~]# ipset l
[root@server ~]#

ファイルに保存されているセットを確認します。

[root@server ~]# cat ip_set.txt
create test1 hash:ip family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0x1266b74c
add test1 192.168.10.1

ファイルに保存されているセットをリストアします。

[root@server ~]# ipset restore < ip_set.txt

セットを確認します。ファイルに保存されていたセットがリストアされたことがわかります。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536 bucketsize 12 initval 0x1266b74c
Size in memory: 256
References: 0
Number of entries: 1
Members:
192.168.10.1

8 タイムアウトの設定方法(timeout)

エントリにタイムアウトを設定することができます。指定した時間を経過すると、エントリが削除されます。ここでは、エントリに10秒のタイムアウトを設定してみます。10秒経過すると、エントリが削除されることを確認してみます。

[root@server ~]# ipset n test1 hash:ip timeout 10

セット(test1)にエントリ(192.168.10.10)を追加します。

[root@server ~]# ipset a test1 192.168.10.10

セットに追加されたエントリを確認します。エントリの削除まで4秒であることがわかります。

[root@server ~]# ipset l test1
Name: test1
Type: hash:ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536 timeout 10 bucketsize 12 initval 0x728c1f29
Size in memory: 280
References: 0
Number of entries: 1
Members:
192.168.10.10 timeout 4

セットに追加されたエントリを確認します。エントリが削除されたことがわかります。

[root@server ~]# ipset l test1
Name: test1
Type: hash:ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536 timeout 10 bucketsize 12 initval 0x728c1f29
Size in memory: 280
References: 0
Number of entries: 0
Members:

9 カウンタの設定方法(counters)

カウンタ付のセットを作成します。

[root@server ~]# ipset n test1 hash:ip counters

セットにエントリを追加します。

[root@server ~]# ipset a test1 192.168.122.213

セットの内容を表示します。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536 counters bucketsize 12 initval 0x1707fb58
Size in memory: 296
References: 0
Number of entries: 1
Members:
192.168.122.213 packets 0 bytes 0

セットをINPUTチェインに追加します。

[root@server ~]# iptables -A INPUT -m set --match-set test1 src -p icmp -j LOG

INPUTチェインのルールを確認します。test1セットがINPUTチェインに追加されたことがわかります。

[root@server ~]# iptables -nvL INPUT --line-numbers
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        0     0 LOG        icmp --  *      *       0.0.0.0/0            0.0.0.0/0            match-set test1 src LOG flags 0 level 4

クライアントからサーバに対してpingコマンドを1回実行します。なお、pingコマンドの使い方は、pingコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@client~]# ping -c 1 192.168.122.16

セットの内容を確認します。192.168.122.213から84バイトのパケットを1つ受信したことがわかります。

[root@server ~]# ipset l
Name: test1
Type: hash:ip
Revision: 5
Header: family inet hashsize 1024 maxelem 65536 counters bucketsize 12 initval 0x1707fb58
Size in memory: 296
References: 1
Number of entries: 1
Members:
192.168.122.213 packets 1 bytes 84

INPUTチェインを確認します。ICMPパケットを受信したことがわかります。

[root@server ~]# iptables -nvL INPUT --line-numbers
Chain INPUT (policy ACCEPT 0 packets, 0 bytes)
num   pkts bytes target     prot opt in     out     source               destination
1        1    84 LOG        icmp --  *      *       0.0.0.0/0            0.0.0.0/0            match-set test1 src LOG flags 0 level 4

ipsetコマンドを実行したときのカーネルの動作をsystemtapで確認する。

[root@server ~]# ipset n test1 hash:ip
pp=module("ip_set_hash_ip").function("hash_ip_create@net/netfilter/ipset/ip_set_hash_gen.h:1207")
pp=module("ip_set_hash_ip").function("htable_size@net/netfilter/ipset/ip_set_hash_gen.h:103")

シグナルの受信処理について

1 はじめに

シグナルの受信処理について、以下のことを確認してみます。
・同じシグナルを複数回受信した場合の挙動
・通常シグナル、リアルタイムシグナルの受信処理の順序

シグナルについて以下の記事も書きました。
シグナルについて - hana_shinのLinux技術ブログ

2 検証環境

AlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.1 (Lime Lynx)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-162.6.1.el9_1.x86_64

3 テストプログラム

テストプログラムの内容は次のとおりです。
SIGHUP(1)、SIGINT(2)、SIGRTMAX-1(63)、SIGRTMAX(64)の各シグナルについて、シグナルハンドラを登録し、それらシグナルを受信した際にはシグナル番号を表示します。シグナルハンドラでは、fprintf関数やprintf関数ではなく、シグナルセーフなwriteシステムコールを使ってメッセージを出力します。また、main関数ではfprintfの出力先にstderrを指定しています。これは、メッセージ出力をバッファリングせず、即座に端末に出力できるようにするためです。

[root@server tp]# cat signal.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

void sig_int(int sig) {
    char msg[100];
    snprintf(msg, sizeof(msg), "Signal number:%d\n", sig);
    write(STDERR_FILENO, msg, strlen(msg));
}

int main() {
    struct sigaction sa[4];
    int signals[] = {1, 11, 63, 64};

    for (int i = 0; i < 4; ++i) {
        sa[i].sa_handler = sig_int;
        sigemptyset(&sa[i].sa_mask);
        sa[i].sa_flags = 0;
        sigaction(signals[i], &sa[i], NULL);
    }

    fprintf(stderr, "PID=%d\n", getpid());
    while(1) {
        sleep(600);
    }
}

テストプログラムをコンパイルします。

[root@server tp]# gcc -Wall -o signal signal.c

4 同じシグナルを複数回受信した場合について

4.1 通常シグナルを複数回受信した場合

テストプログラムを実行します。

[root@server tp]# ./signal
PID=1189

Ctrl+zを押下して、signalプロセスを停止状態にします。なお、SIGSTOPシグナルをsignalプロセスに送信しても同じ状態になります。

[root@server tp]# ./signal
PID=1189
^Z
[1]+  停止                  ./signal

psコマンドを使用してsignalプロセスの状態を確認すると、停止状態(STATがT)であることがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1189 0000000000000000 0000000000000000 0000000000000000 c000000000000401 T    pts/1      0:00 ./signal

signalプロセスにSIGHUPシグナルを3つ送信します。

[root@server ~]# kill -SIGHUP 1189
[root@server ~]# kill -SIGHUP 1189
[root@server ~]# kill -SIGHUP 1189

psコマンドを実行して、signalプロセスの状態を確認します。PENDINGが0000000000000001であることがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1189 0000000000000001 0000000000000000 0000000000000000 c000000000000401 T    pts/1      0:00 ./signal

シグナルについて - hana_shinのLinux技術ブログに記載したプログラム(signal.py)を実行すると、SIGHUPシグナルが保留されていることがわかります。

[root@server ~]# ./signal.py
Please enter a hexadecimal number:0000000000000001
Active signals:
SIGHUP(1)

fgコマンドを実行して、signalプロセスを再開します。signalプロセスにSIGHUPシグナルを3つ送信したのですが、シグナルハンドラが1回しか実行されていないので、処理されシグナルは1回だけであることがわかります。

[root@server tp]# fg
./signal
Signal number:1

psコマンドを使用してsignalプロセスの状態を確認すると、PENDINGが0000000000000000であることがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1189 0000000000000000 0000000000000000 0000000000000000 c000000000000401 S+   pts/0      0:00 ./signal

テストCtrl+cを押下して、テストプログラムを終了します。

[root@server tp]# fg
./signal
Signal number:1
^C

4.2 リアルタイムシグナルを複数回受信した場合

テストプログラムを実行します。

[root@server tp]# ./signal
PID=1230

psコマンドを実行して、プロセスの状態を確認します。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1230 0000000000000000 0000000000000000 0000000000000000 c000000000000401 S+   pts/1      0:00 ./signal

Ctrl+zキーを押下して、signalプロセスを停止状態にします。

[root@server tp]# ./signal
PID=1230
^Z
[1]+  停止                  ./signal

psコマンドを使用してsignalプロセスの状態を確認すると、停止状態(STATがT)であることがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1230 0000000000000000 0000000000000000 0000000000000000 c000000000000401 T    pts/1      0:00 ./signal

signalプロセスにSIGRTMAXシグナルを3つ送信します。

[root@server ~]# kill -SIGRTMAX 1230
[root@server ~]# kill -SIGRTMAX 1230
[root@server ~]# kill -SIGRTMAX 1230

psコマンドを実行して、signalプロセスの状態を確認します。PENDINGが8000000000000000であることがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1230 8000000000000000 0000000000000000 0000000000000000 c000000000000401 T    pts/1      0:00 ./signal

シグナルについて - hana_shinのLinux技術ブログに記載したプログラムを実行すると、SIGRTMAXシグナルが保留されていることがわかります。

[root@server ~]# ./signal.py
Please enter a hexadecimal number:8000000000000000
Active signals:
SIGRTMAX(64)

fgコマンドを実行して、signal プロセスを再開します。シグナルハンドラが3回実行されたため、処理されたシグナルは3回であることがわかります。通常のシグナルとは異なり、リアルタイムシグナルは受信したシグナルの回数を実行することができます。

[root@server tp]# fg
./signal
Signal number:64
Signal number:64
Signal number:64

signalプロセスが再開して、保留中のシグナルを処理したので、PENDINGが0になったことがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1230 0000000000000000 0000000000000000 0000000000000000 c000000000000401 S+   pts/1      0:00 ./signal

5 通常シグナルとリアルタイムシグナルの優先度について

テストプログラムを実行します。

[root@server tp]# ./signal
PID=1321

Ctrl+zを押下して、signalプロセスを停止状態にします。

[root@server tp]# ./signal
PID=1321
^Z
[1]+  停止                  ./signal

signalプロセスにSIGHUP, SIGSEGV, SIGRTMAX-1, SIGRTMAX を送信します。SIGHUP, SIGSEGVは通常シグナルです。SIGRTMAX-1, SIGRTMAXはリアルタイムシグナルです。

[root@server ~]# kill -SIGHUP 1321
[root@server ~]# kill -SIGSEGV 1321
[root@server ~]# kill -SIGRTMAX-1 1321
[root@server ~]# kill -SIGRTMAX 1321

psコマンドを実行して、signalプロセスの状態を確認します。PENDINGがc000000000000401であることがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1321 c000000000000401 0000000000000000 0000000000000000 c000000000000401 T    pts/1      0:00 ./signal

signal.pyを実行すると、SIGHUP, SIGSEGV, SIGRTMAX-1, SIGRTMAXが保留されていることがわかります。

[root@server ~]# ./signal.py
Please enter a hexadecimal number:c000000000000401
Active signals:
SIGHUP(1), SIGSEGV(11), SIGRTMAX-1(63), SIGRTMAX(64)

fgコマンドを実行して、signal プロセスを再開します。リアルタイムシグナル、通常シグナルの順でシグナルが処理されていることがわかります。また、リアルタイムシグナルは番号の大きいものから、通常シグナルは番号の小さいものから処理されていることがわかります。

[root@server tp]# fg
./signal
Signal number:64
Signal number:63
Signal number:1
Signal number:11

6 まとめ

項目 説明
通常シグナルとリアルタイムシグナルの優先度 リアルタイムシグナルが通常シグナルより優先して処理されます
リアルタイムシグナル同士の優先度 番号の大きいリアルタイムシグナルが優先して処理されます
通常シグナル同士の優先度 番号の小さい通常シグナルが優先して処理されます
同じシグナルの実行回数 通常シグナルの場合、同じシグナルをN回受信しても処理するのは1回です。リアルタイムシグナルの場合、同じシグナルをN回受信したら、N回受信処理をします

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ

シグナルについて

1 はじめに

シグナルに関して様々な実験を実施してみます。

他にも、以下の記事を投稿しました。
シグナルの受信処理について - hana_shinのLinux技術ブログ

2 検証環境

AlmaLinux版数は以下のとおりです。

[root@server ~]# cat /etc/redhat-release
AlmaLinux release 9.1 (Lime Lynx)

カーネル版数は以下のとおりです。

[root@server ~]# uname -r
5.14.0-162.6.1.el9_1.x86_64

3 シグナル一覧を表示する方法

killコマンドを実行すると、シグナルの一覧を表示することができます。通常シグナルは1~32までの範囲で、リアルタイムシグナルは33~64までの範囲にあります。

[root@server ~]# kill -l
 1) SIGHUP       2) SIGINT       3) SIGQUIT      4) SIGILL       5) SIGTRAP
 6) SIGABRT      7) SIGBUS       8) SIGFPE       9) SIGKILL     10) SIGUSR1
11) SIGSEGV     12) SIGUSR2     13) SIGPIPE     14) SIGALRM     15) SIGTERM
16) SIGSTKFLT   17) SIGCHLD     18) SIGCONT     19) SIGSTOP     20) SIGTSTP
21) SIGTTIN     22) SIGTTOU     23) SIGURG      24) SIGXCPU     25) SIGXFSZ
26) SIGVTALRM   27) SIGPROF     28) SIGWINCH    29) SIGIO       30) SIGPWR
31) SIGSYS      34) SIGRTMIN    35) SIGRTMIN+1  36) SIGRTMIN+2  37) SIGRTMIN+3
38) SIGRTMIN+4  39) SIGRTMIN+5  40) SIGRTMIN+6  41) SIGRTMIN+7  42) SIGRTMIN+8
43) SIGRTMIN+9  44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9  56) SIGRTMAX-8  57) SIGRTMAX-7
58) SIGRTMAX-6  59) SIGRTMAX-5  60) SIGRTMAX-4  61) SIGRTMAX-3  62) SIGRTMAX-2
63) SIGRTMAX-1  64) SIGRTMAX

4 シグナルを送信する方法

killコマンドを実行すると、プロセスにシグナルを送信することができます。ここでは、sleepプロセスにSIGKILLシグナルを送信してみます。まず、sleepコマンドを実行します。sleepコマンドを実行すると、sleepプロセスが起動され、その後600秒スリープします。

[root@server ~]# sleep 600

psコマンドを実行して、sleepプロセスのPIDを確認します。sleepプロセスのPIDは19876であることがわかります。なお、psコマンドの使い方は、psコマンドの使い方 - hana_shinのLinux技術ブログを参照してください。

[root@server ~]# ps -C sleep
    PID TTY          TIME CMD
  19867 pts/2    00:00:00 sleep

4.1 シグナル名を指定する方法

シグナル名を指定してkillコマンドを実行します。

[root@server ~]# kill -SIGKILL 19867

sleepプロセスがSIGKILLシグナルを受信すると、強制終了することがわかります。

[root@server ~]# sleep 600
強制終了

4.2 シグナル番号を指定する方法

シグナル番号を指定してkillコマンドを実行します。

[root@server ~]# kill -9 1188

sleepプロセスがSIGKILLシグナルを受信すると、強制終了することがわかります。

[root@server ~]# sleep 600
強制終了

5 シグナルの状態を表示する方法

psコマンドにsオプションを指定すると、シグナルの各種状態(PENDING、BLOCKED、IGNORED、CAUGHT)を表示することができます。ここでは、これらの各種状態をシグナル情報と呼ぶことにします。シグナル情報は64ビットのデータです。通常シグナルには32ビットが割り当てられており、リアルタイムシグナルにも32ビットが割り当てられています。

[root@server ~]# ps s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0     672 0000000000000000 0000000000000000 0000000000000006 0000000000000000 Ss+  tty1       0:00 /sbin/agetty -o -p
    0    1124 0000000000000000 0000000000010000 0000000000384004 000000004b813efb Ss   pts/0      0:00 -bash
    0    1195 0000000000000000 0000000000000000 0000000000384004 000000004b813efb Ss+  pts/1      0:00 -bash
    0   19660 0000000000000000 0000000000010000 0000000000384004 000000004b813efb Ss   pts/2      0:00 -bash
    0   19908 0000000000000000 0000000000000000 0000000000000000 0000000000000000 S+   pts/2      0:00 sleep 600
    0   19915 0000000000000000 0000000000000000 0000000000000000 0000000073d1fef9 R+   pts/0      0:00 ps s

シグナル情報の意味は以下のとおりです。

シグナル情報 意味
PENDING プロセスが受信したがまだ処理していないシグナルを示します
BLOCKED プロセスが特定のシグナルを受信しないように一時的にシグナルの処理をブロックするシグナルを示します。ブロックを解除するとシグナルを処理します
IGNORED プロセスが特定のシグナルを受信しても無視するシグナルを示します
CAUGHT シグナルハンドラを登録しているシグナルを示します

PIDを指定すると、特定プロセスのシグナル情報を表示することができます。ここでは、systemd(PID=1)のシグナル情報を表示してみます。

[root@server ~]# ps s -p 1
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0       1 0000000000000000 7fe3c0fe28014a03 0000000000001000 00000001000004ec Ss   ?          0:05 /usr/lib/systemd/s

次のようにフォーマットを明に指定しても、プロセスのシグナル情報を表示することができます。

[root@server ~]# ps -p 1 -o comm,pid,sig,sigmask,sigignore,sigcatch
COMMAND             PID          PENDING          BLOCKED          IGNORED           CAUGHT
systemd               1 0000000000000000 7fe3c0fe28014a03 0000000000001000 00000001000004ec

-Cオプションでプロセス名を指定すると、指定したプロセスのシグナル情報を表示することができます。chronydプロセスのシグナル情報を表示してみます。

[root@server ~]#  ps -C chronyd -o comm,pid,sig,sigmask,sigignore,sigcatch
COMMAND             PID          PENDING          BLOCKED          IGNORED           CAUGHT
chronyd             656 0000000000000000 0000000000000000 0000000000001000 0000000100004007

記事末尾のサンプルプログラム(signal.py)を使用してIGNOREDのシグナル名を確認すると、SIGPIPEが無視されていることがわかります。

[root@server ~]# ./signal.py
Please enter a hexadecimal number:0000000000001000
Active signals:
SIGPIPE(13)

次に、chronydプロセスがSIGPIPEシグナルを無視するかどうかを確認するため、chronydプロセスにSIGPIPEシグナルを送信してみます。通常、SIGPIPEを受信するとプロセスは終了しますが、chronydプロセスはSIGPIPEシグナルを無視する設定になっているため、SIGPIPEシグナルを受信してもchronydプロセスは終了しません。

[root@server ~]# kill -SIGPIPE 656

chronydプロセスの状態を確認します。chronydプロセスのPIDは656のままで、chronydプロセスが終了していないことがわかります。

[root@server ~]# ps -C chronyd -o comm,pid,sig,sigmask,sigignore,sigcatch
COMMAND             PID          PENDING          BLOCKED          IGNORED           CAUGHT
chronyd             656 0000000000000000 0000000000000000 0000000000001000 0000000100004007

6 CAUGHTの確認方法

6.1テストプログラムの作成

CAUGHTを確認するためのテストプログラムを作成します。SIGHUP(1)、SIGINT(2)、SIGRTMAX-1(63)、SIGRTMAX(64)の各シグナルについて、シグナルハンドラを登録し、それらシグナルを受信した際にはシグナル番号を表示します。シグナルハンドラでは、fprintf関数やprintf関数ではなく、シグナルセーフなwriteシステムコールを使ってメッセージを出力します。また、main関数ではfprintfの出力先にstderrを指定しています。これは、メッセージ出力をバッファリングせず、即座に端末に出力できるようにするためです。

[root@server ~]# cat signal.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

void sig_int(int sig) {
    char msg[100];
    snprintf(msg, sizeof(msg), "Signal number:%d\n", sig);
    write(STDERR_FILENO, msg, strlen(msg));
}

int main() {
    struct sigaction sa[4];
    int signals[] = {1, 2, 63, 64};

    for (int i = 0; i < 4; ++i) {
        sa[i].sa_handler = sig_int;
        sigemptyset(&sa[i].sa_mask);
        sa[i].sa_flags = 0;
        sigaction(signals[i], &sa[i], NULL);
    }

    fprintf(stderr, "PID=%d\n", getpid());
    while(1) {
        sleep(600);
    }
}

テストプログラムをコンパイルします。

[root@server ~]# gcc -Wall -o signal signal.c

6.2 確認結果

テストプログラムを実行します。

[root@server ~]# ./signal
PID=1421

psコマンドを実行してシグナル情報を確認すると、CAUGHTがc000000000000003であることがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1421 0000000000000000 0000000000000000 0000000000000000 c000000000000003 S+   pts/1      0:00 ./signal

signal.pyを実行すると、CAUGHTのc000000000000003は、テストプログラムで定義した4つのシグナルを補足することを示していることがわかります。

[root@server ~]# ./signal.py
Please enter a hexadecimal number:c000000000000003
Active signals:
SIGHUP(1), SIGINT(2), SIGRTMAX-1(63), SIGRTMAX(64)

7 PENDINGの確認方法

6章と同じテストプログラムを実行します。signalプロセスのPIDが1207であることがわかります。

[root@server ~]# ./signal
PID=1207

Ctrl+zキーを押下して、signalプロセスを停止状態にします。なお、SIGSTOPシグナルをsignalプロセスに送信しても同じ状態になります。

[root@server ~]# ./signal
PID=1207
^Z
[1]+  停止                  ./signal

psコマンドを使用してsignalプロセスの状態を確認すると、停止状態(STATがT)であることがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1207 0000000000000000 0000000000000000 0000000000000000 c000000000000003 S+   pts/1      0:00 ./signal

signalプロセスにSIGHUP(1)シグナルを送信します。

[root@server ~]# kill -1 1207

psコマンドを実行してシグナル情報を確認すると、PENDINGが0000000000000001になったことがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1207 0000000000000001 0000000000000000 0000000000000000 c000000000000003 T    pts/1      0:00 ./signal

signal.pyを実行すると、SIGHUPシグナルが保留されていることがわかります。これは、シグナルを受信するプロセスが停止状態になっているため、SIGHUPシグナルを受信しても処理できずに保留しているからです。

[root@server ~]# ./signal.py
Please enter a hexadecimal number:0000000000000001
Active signals:
SIGHUP(1)

次に、signalプロセスにSIGINT(2)、SIGRTMAX-1(63)、SIGRTMAX(64)を送信します。

[root@server ~]# kill -2 1207
[root@server ~]# kill -63 1207
[root@server ~]# kill -64 1207

psコマンドを実行してシグナル情報を確認すると、PENDINGがc000000000000003になっていることがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1207 c000000000000003 0000000000000000 0000000000000000 c000000000000003 T    pts/1      0:00 ./signal

signal.pyを使ってPENDINGのシグナルを確認すると、SIGHUP(1)、SIGINT(2)、SIGRTMAX-1(63)、SIGRTMAX(64)が保留中であることがわかります。

[root@server ~]# ./signal.py
Please enter a hexadecimal number:c000000000000003
Active signals:
SIGHUP(1), SIGINT(2), SIGRTMAX-1(63), SIGRTMAX(64)

fgコマンドを実行して、signalプロセスを停止状態からスリープ状態に戻します。

[root@server ~]# fg
./signal
Signal number:64
Signal number:63
Signal number:2
Signal number:1

psコマンドを実行してシグナル情報を確認します。signalプロセスが起床して、保留中のシグナルを処理したので、PENDINGが0になったことがわかります。

[root@server ~]# ps -C signal s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1207 0000000000000000 0000000000000000 0000000000000000 c000000000000003 S+   pts/1      0:00 ./signal

8 IGNOREDの確認方法

8.1テストプログラムの作成

IGNOREDを確認するためのテストプログラムを作成します。実行開始から30秒間は、SIGHUP(1)、SIGINT(2)、SIGRTMAX-1(63)、SIGRTMAX(64)を受信しても無視します。30秒経過すると、無視していたシグナルを実行します。

[root@server ~]# cat signal1.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

void sig_int(int sig) {
    char msg[100];
    snprintf(msg, sizeof(msg), "Signal number:%d\n", sig);
    write(STDERR_FILENO, msg, strlen(msg));
}

int main() {
    struct sigaction sa[4];
    int signals[] = {1, 2, 63, 64};
    int block_time = 30;

    for (int i = 0; i < 4; ++i) {
        sa[i].sa_handler = SIG_IGN;
        sigemptyset(&sa[i].sa_mask);
        sa[i].sa_flags = 0;
        sigaction(signals[i], &sa[i], NULL);
    }

    fprintf(stderr, "PID=%d\n", getpid());
    sleep(block_time);
    for (int i = 0; i < 4; ++i) {
        sa[i].sa_handler = sig_int;
        sigaction(signals[i], &sa[i], NULL);
    }

    fprintf(stderr, "Signals unblocked.\n");
    while(1) {
        sleep(600);
    }
    return 0;
}

テストプログラムをコンパイルします。

[root@server ~]# gcc -Wall -o signal1 signal1.c

8.2 確認結果

テストプログラムを実行します。

[root@server ~]# ./signal1
PID=1318

プロセスの状態を確認すると、IGNOREDがc000000000000003になっていることがわかります。

[root@server ~]# ps -C signal1 s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1318 0000000000000000 0000000000000000 c000000000000003 0000000000000000 S+   pts/1      0:00 ./signal1

signalプロセスにSIGHUP(1), SIGINT(2), SIGRTMAX-1(63), SIGRTMAX(64)を送信します。

[root@server ~]# kill -1 1318
[root@server ~]# kill -2 1318
[root@server ~]# kill -63 1318
[root@server ~]# kill -64 1318

シグナルを受信してもシグナル番号が表示されないことから、シグナルが無視されていることがわかります。

[root@server ~]# ./signal1
PID=1318

30秒経過したあと、プロセスの状態を確認するとIGNOREDが0000000000000000になっていることがわかります。

[root@server ~]# ps -C signal1 s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1318 0000000000000000 0000000000000000 0000000000000000 c000000000000003 S+   pts/1      0:00 ./signal1

signalプロセスにSIGHUP(1), SIGINT(2), SIGRTMAX-1(63), SIGRTMAX(64)を送信します。

[root@server ~]# kill -1 1318
[root@server ~]# kill -2 1318
[root@server ~]# kill -63 1318
[root@server ~]# kill -64 1318

シグナルを受信してシグナルハンドラが実行されたため、シグナル番号が表示されていることがわかります。

[root@server ~]# ./signal1
PID=1318
Signals unblocked.
Signal number:1
Signal number:2
Signal number:63
Signal number:64

9 BLOCKEDの確認方法

9.1テストプログラムの作成

BLOCKEDを確認するためのテストプログラムを作成します。テストプログラムを実行すると30秒間シグナルの受信処理をブロックします。30秒経過するとブロックを解除してシグナルの受信処理を実行します。

[root@server ~]# cat signal2.c
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>

void sig_int(int sig) {
    char msg[100];
    snprintf(msg, sizeof(msg), "Signal number:%d\n", sig);
    write(STDERR_FILENO, msg, strlen(msg));
}

int main() {
    struct sigaction sa[4];
    int signals[] = {1, 2, 63, 64};
    int block_time = 30;
    sigset_t set;

    for (int i = 0; i < 4; ++i) {
        sa[i].sa_handler = sig_int;
        sigemptyset(&sa[i].sa_mask);
        sa[i].sa_flags = 0;
        sigaction(signals[i], &sa[i], NULL);
    }

    fprintf(stderr, "PID=%d\n", getpid());

    sigemptyset(&set);
    for (int i = 0; i < 4; ++i) {
        sigaddset(&set, signals[i]);
    }
    sigprocmask(SIG_BLOCK, &set, NULL);

    fprintf(stderr, "Signals blocked for %d seconds.\n", block_time);
    sleep(block_time);

    sigprocmask(SIG_UNBLOCK, &set, NULL);
    fprintf(stderr, "Signals unblocked.\n");

    while (1) {
        sleep(600);
    }
    return 0;
}

テストプログラムをコンパイルします。

[root@server ~]# gcc -Wall -o signal2 signal2.c

9.2 確認結果

テストプログラムを実行します。

[root@server ~]# ./signal2
PID=1383

プロセスの状態を確認すると、BLOCKEDがc000000000000003になっており、SIGHUP(1)、SIGINT(2)、SIGRTMAX-1(63)、SIGRTMAX(64)がブロックされれいることを示しています

[root@server ~]# ps -C signal2 s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1383 0000000000000000 c000000000000003 0000000000000000 c000000000000003 S+   pts/0      0:00 ./signal2

signalプロセスにSIGHUP(1), SIGINT(2), SIGRTMAX-1(63), SIGRTMAX(64)を送信します。

[root@server ~]# kill -1 1383
[root@server ~]# kill -2 1383
[root@server ~]# kill -63 1383
[root@server ~]# kill -64 1383

プロセスの状態を確認すると、PENDINGがc000000000000003になっており、SIGHUP(1)、SIGINT(2)、SIGRTMAX-1(63)、SIGRTMAX(64)が保留中であることを示しています。これは、シグナルの受信処理がブロックされていてシグナルが保留されているからです。

[root@server ~]# ps -C signal2 s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1383 c000000000000003 c000000000000003 0000000000000000 c000000000000003 S+   pts/0      0:00 ./signal2

テストプログラムを実行して30秒経過するとブロックしているシグナルのシグナルハンドラが実行されます。シグナルの処理はリアルタイムシグナルが通常シグナルの処理より優先されます。また、シグナル番号の大きいシグナルから処理されていることがわかります。

[root@server ~]# ./signal2
PID=1383
Signals blocked for 30 seconds.
Signal number:64
Signal number:63
Signal number:2
Signal number:1
Signals unblocked.

プロセスの状態を確認すると、BLOCKEDが0000000000000000 になっていることがわかります。これは、30秒経過してブロックしていたシグナルを解除したからです。

[root@server ~]# ps -C signal2 s
  UID     PID          PENDING          BLOCKED          IGNORED           CAUGHT STAT TTY        TIME COMMAND
    0    1383 0000000000000000 0000000000000000 0000000000000000 c000000000000003 S+   pts/0      0:00 ./signal2

X シグナル名とシグナル番号を表示するプログラム

以下は、シグナル情報の16進数をシグナル名とシグナル番号で表示するプログラムです。

[root@server ~]# cat signal.py
#!/usr/bin/python3

def binary_to_signals(hex_input):
    signals = [
        "SIGHUP", "SIGINT", "SIGQUIT", "SIGILL",
        "SIGTRAP", "SIGABRT", "SIGBUS", "SIGFPE",
        "SIGKILL", "SIGUSR1", "SIGSEGV", "SIGUSR2",
        "SIGPIPE", "SIGALRM", "SIGTERM", "SIGSTKFLT",
        "SIGCHLD", "SIGCONT", "SIGSTOP", "SIGTSTP",
        "SIGTTIN", "SIGTTOU", "SIGURG", "SIGXCPU",
        "SIGXFSZ", "SIGVTALRM", "SIGPROF", "SIGWINCH",
        "SIGIO", "SIGPWR", "SIGSYS", "SIGUNUSED", "SIGUNUSED",
        "SIGRTMIN", "SIGRTMIN+1", "SIGRTMIN+2", "SIGRTMIN+3",
        "SIGRTMIN+4", "SIGRTMIN+5", "SIGRTMIN+6", "SIGRTMIN+7",
        "SIGRTMIN+8", "SIGRTMIN+9", "SIGRTMIN+10", "SIGRTMIN+11",
        "SIGRTMIN+12", "SIGRTMIN+13", "SIGRTMIN+14", "SIGRTMIN+15",
        "SIGRTMAX-14", "SIGRTMAX-13", "SIGRTMAX-12", "SIGRTMAX-11",
        "SIGRTMAX-10", "SIGRTMAX-9", "SIGRTMAX-8", "SIGRTMAX-7",
        "SIGRTMAX-6", "SIGRTMAX-5", "SIGRTMAX-4", "SIGRTMAX-3",
        "SIGRTMAX-2", "SIGRTMAX-1", "SIGRTMAX",
    ]

    signals_activated = []
    binary_str = bin(int(hex_input, 16))[2:]
    binary_str = binary_str.zfill(64)

    for i, bit in enumerate(reversed(binary_str)):
        if bit == "1":
            if i < len(signals):
                signal_name = signals[i]
                signal_number = i + 1
                signals_activated.append(f"{signal_name}({signal_number})")

    return signals_activated

if __name__ == "__main__":
    hex_input = input("Please enter a hexadecimal number:")

    if not all(ch in "0123456789abcdefABCDEF" for ch in hex_input):
        print("Input error: Please enter a hexadecimal number.")
    else:
        activated_signals = binary_to_signals(hex_input)
        if not activated_signals:
            print("There are no active signals:")
        else:
            print("Active signals:")
            for i in range(0, len(activated_signals), 5):
                signals_line = ", ".join(activated_signals[i:i+5])
                print(signals_line)

実行権を付与します。

[root@server ~]# chmod 744 signal.py

Z 参考情報

私が業務や記事執筆で参考にした書籍を以下のページに記載します。
Linux技術のスキルアップをしよう! - hana_shinのLinux技術ブログ