Slackアイコンつき名札シールを作る
n番煎じですが、この前(すでにnヶ月前...)はこんな感じで作ってみた、という備忘録です。
2025年は巳年なのでPythonで作りました。
GASで無理くり作った時の備忘録はこちらです → https://note.com/0375/n/n3f1def9a6c27
Slackのアイコンと名前が載った名札があると、オンラインでしか会ったことがない人とも「あ、あのアイコンの人だ!」と気軽に話しかけられますよね。
社内イベントや勉強会などで使える便利なツール、あるいは参考になれば幸いです。
作るもの
特定のSlackチャンネルに参加しているメンバーの情報を取得し、以下のような名札シールを生成します:
- Slackプロフィール画像
- 氏名
- 所属部署
これを市販のラベルシールに印刷できるPDFとして出力します。
下図は作成イメージです。氏名や所属は架空のものです。
準備するもの
- Python環境(Python 3.6以上推奨)
- 必要なライブラリ(requests, PIL, reportlab)
- Slackアプリのトークン(users, users.profile, channels, groups のスコープが必要)
- 日本語フォント(IPAexゴシック等)
- 印刷用ラベルシート(例:デビカラベルシール、名札サイズW89×H48mm、A4 1シート10枚)
作成手順(ざっくり概要)
- ラベル用紙を用意、仕様確認
- Slackアプリの準備
- コード作成、実行
- 印刷調整
1.ラベル用紙を用意、仕様確認
このラベルに印刷します。
ミシン目でラベル一枚ごとに切り離せるので便利です。
こういう首から下げるホルダーも用意しておいても良いですね。
デビカ ラベルシール 貼ってはがせる名札 ミシン目入り白無地 063622
名札サイズW89×H48mm
A4 1シート10枚
レイアウト仕様
A4の寸法:
横: 210mm、縦: 297mm
ラベルの配置:
縦に5行(59.4mmずつの間隔で配置)
横に2列(105mmずつの間隔で配置)
ラベルサイズ:
横: 89mm(左右に8mmずつマージン)
縦: 48mm(上下に5.7mmずつマージン)
2.Slackアプリの準備
今回利用しているSlack APIは以下の2つです:
https://api.slack.com/methods/users.profile.get - ユーザープロフィール情報を取得するAPI
https://api.slack.com/methods/conversations.members - チャンネルのメンバー一覧を取得するAPI
これらのAPIを利用するため、
Slackアプリを作成し、必要なスコープを設定します:
- Slack API公式サイトにアクセス
- 「Create New App」から新規アプリを作成
- 「OAuth & Permissions」で以下のスコープを追加:
- users
- users.profile
- channels
- groups
- アプリをワークスペースにインストールし、「Bot User OAuth Token」を取得
3.コード作成、実行
全体の流れは以下の通りです:
- Slackチャンネルのメンバー一覧を取得
- 各メンバーのプロフィール情報とアイコン画像を取得
- 名札レイアウトをPDFに生成
今回のコードでは、Slackのカスタムフィールドから氏名、所属の情報を取得しています。
Slackのカスタムフィールドとは?
Slackのカスタムフィールドは、組織のワークスペース管理者が設定できるプロフィール拡張機能です。標準のプロフィール項目(名前、メールアドレス等)に加えて、組織特有の情報(部署、役職、社員番号など)を追加できます。
APIからはこれらのカスタムフィールドの情報も取得でき、各フィールドは「Xf」で始まる一意のIDで識別されます。今回のスクリプトでは、これらのカスタムフィールドから名前や所属情報を取得しています。
https://api.slack.com/methods/users.profile.get これのレスポンス例で言うと以下のあたりです。
"fields": {
"Xf0111111": {
"value": "Barista",
"alt": ""
},
"Xf0222222": {
"value": "2022-04-11",
"alt": ""
},
"Xf0333333": {
"value": "https://example.com",
"alt": ""
}
},
実行方法
- 必要なライブラリをインストール
pip install requests Pillow reportlab
-
IPAexゴシックフォントをダウンロード
-
SlackトークンとチャンネルIDを設定
- スクリプト内の SLACK_API_TOKEN と CHANNEL_ID を実際の値に書き換え
-
スクリプトを実行
python generate_slack_labels.py
- 生成されたPDFをラベルシートに印刷
4.印刷調整
ここが地味に面倒でした。
何度かテスト印刷して、数値を調整して、最終的に綺麗に収まるようにしました。
PDF生成時の数値調整も必要ですが、印刷時の設定(実際のサイズで印刷するのか、ページに合わせて印刷するのかなど)との組み合わせもありますので、各自の環境で最適な方法でお試しください。
コード 主な機能の実装
1. 基本設定とレイアウト
#!/usr/bin/env python3
import os
import requests
from PIL import Image
from reportlab.pdfgen import canvas
from reportlab.lib.pagesizes import A4
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from io import BytesIO
import tempfile
# 日本語フォントの登録
font_path = os.path.join(os.path.dirname(__file__), "fonts", "ipaexg.ttf")
pdfmetrics.registerFont(TTFont("IPAexGothic", font_path))
# SlackAPI設定
SLACK_API_TOKEN = "xoxb-あなたのトークン"
CHANNEL_ID = "対象チャンネルID"
HEADERS = {"Authorization": f"Bearer {SLACK_API_TOKEN}"}
# ラベルのレイアウト設定
PAGE_WIDTH, PAGE_HEIGHT = A4
LABEL_WIDTH = 89 # ラベルの幅(mm)
LABEL_HEIGHT = 48 # ラベルの高さ(mm)
COLUMN_SPACING = 105 # 列間隔(mm)
ROW_SPACING = 59.5 # 行間隔(mm)
MARGIN_LEFT = 8 # 左右のマージン(mm)
MARGIN_TOP = -4 # 上下のマージン(mm)
2. メンバー情報の取得
def get_channel_members(channel_id: str) -> list:
"""指定されたSlackチャンネルの全メンバーIDを取得します。"""
url = "https://slack.com/api/conversations.members"
members = []
cursor = None
while True:
params = {"channel": channel_id, "limit": 1000}
if cursor:
params["cursor"] = cursor
response = requests.get(url, headers=HEADERS, params=params)
data = response.json()
if not data.get("ok"):
raise Exception(f"Error fetching channel members: {data.get('error')}")
members.extend(data.get("members", []))
cursor = data.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
return members
def get_user_info(user_id: str) -> dict:
"""指定されたSlackユーザーの情報を取得します。"""
url = "https://slack.com/api/users.profile.get"
params = {"user": user_id}
response = requests.get(url, headers=HEADERS, params=params)
data = response.json()
if not data.get("ok"):
print(f"Error fetching user info for user ID: {user_id}")
return None
profile = data.get("profile", {})
fields = profile.get("fields", {})
# フィールドから情報を取得
english_name = fields.get("Xf01ABCDFE", {}).get("value", "")
japanese_name = fields.get("Xf02ABCDFE", {}).get("value", "")
organization_jp_full = fields.get("Xf03ABCDFE", {}).get("alt", "")
organization_jp = organization_jp_full.split(">")[-1].strip() if ">" in organization_jp_full else organization_jp_full
return {
"name_en": english_name,
"name_jp": japanese_name,
"organization_jp": organization_jp,
"image_url": profile.get("image_512")
}
3. テキスト調整
def calculate_text_width(c, text, font_name, font_size):
"""テキストの幅を計算します。"""
c.setFont(font_name, font_size)
return c.stringWidth(text)
def draw_text_with_size_adjustment(c, text, x, y, max_width, initial_font_size, min_font_size, font_name, force_full_text=False):
"""適切なサイズでテキストを描画します。"""
if force_full_text:
# テキストが収まるまでフォントサイズを小さくする
current_size = initial_font_size
while current_size > 1: # 最小フォントサイズを1ptまで下げる
text_width = calculate_text_width(c, text, font_name, current_size)
if text_width <= max_width:
break
current_size -= 0.5
c.setFont(font_name, current_size)
c.drawString(x, y, text)
return current_size
else:
# 通常の処理(省略可能な場合)
adjusted_size = get_adjusted_font_size(c, text, max_width, initial_font_size, min_font_size, font_name)
c.setFont(font_name, adjusted_size)
if calculate_text_width(c, text, font_name, adjusted_size) > max_width:
# テキストが最小フォントサイズでも収まらない場合は省略
while text and calculate_text_width(c, text + "...", font_name, adjusted_size) > max_width:
text = text[:-1]
text = text + "..."
c.drawString(x, y, text)
return adjusted_size
4. PDFファイル生成
def create_labels(data: list, output_file: str = "labels.pdf") -> None:
"""名札データからPDFファイルを生成します。"""
with tempfile.TemporaryDirectory() as temp_dir:
c = canvas.Canvas(output_file, pagesize=A4)
# mmをポイントに変換(1mm = 2.83465pt)
MM_TO_PT = 2.83465
label_width = LABEL_WIDTH * MM_TO_PT
label_height = LABEL_HEIGHT * MM_TO_PT
column_spacing = COLUMN_SPACING * MM_TO_PT
row_spacing = ROW_SPACING * MM_TO_PT
margin_left = MARGIN_LEFT * MM_TO_PT
margin_top = MARGIN_TOP * MM_TO_PT
for i, entry in enumerate(data, 1):
col = (i - 1) % 2
row = ((i - 1) // 2) % 5
x = margin_left + col * column_spacing
y = PAGE_HEIGHT - margin_top - (row + 1) * row_spacing
# プロフィール画像を描画
img_width = label_height - 10
img_x = x + 5
img_y = y + label_height - img_width
if entry["image_url"]:
try:
response = requests.get(entry["image_url"])
response.raise_for_status()
img = Image.open(BytesIO(response.content))
img = img.resize((int(img_width), int(img_width)))
temp_filename = f"temp_image_{i}.png"
temp_path = os.path.join(temp_dir, temp_filename)
img.save(temp_path)
c.drawImage(temp_path, img_x, img_y, width=img_width, height=img_width)
except Exception as e:
print(f"Error processing image for {entry.get('name_en', 'NoName')}: {e}")
continue
# テキスト描画用の設定
text_x = img_x + img_width + 10
text_y = img_y + img_width - 10
max_text_width = label_width - (text_x - x) - 5 # 右端までの利用可能な幅
# 英語名
name_en = entry.get("name_en", "NoName")
en_size = draw_text_with_size_adjustment(
c, name_en, text_x, text_y,
max_text_width, 12, 8, "Helvetica-Bold"
)
# 日本語名
name_jp = entry.get("name_jp", "名前がありません")
jp_size = draw_text_with_size_adjustment(
c, name_jp, text_x, text_y - 15,
max_text_width, 10, 6, "IPAexGothic"
)
# 所属
organization_jp = entry.get("organization_jp", "所属なし")
draw_text_with_size_adjustment(
c, organization_jp, text_x, text_y - 30,
max_text_width, 8, 6, "IPAexGothic"
)
if (i % 10) == 0:
c.showPage()
c.save()
print(f"PDF saved to {output_file}")
5. メイン処理
ファーストネームでソートしています。
def main():
"""メイン処理を実行します。"""
print("Fetching channel members...")
members = get_channel_members(CHANNEL_ID)
print(f"Found {len(members)} members.")
profiles = []
for user_id in members:
info = get_user_info(user_id)
if info:
profiles.append(info)
print(f"Fetched profile: {info['name_en']} ({info['name_jp']}) | {info['organization_jp']}")
else:
print(f"Failed to fetch profile for user ID: {user_id}")
# 名前順にソート
profiles.sort(key=lambda x: x.get("name_en", "").lower())
print("Generating PDF...")
create_labels(profiles)
print("Done!")
if __name__ == "__main__":
main()
苦労したポイントと解決策
- 特殊文字を含むユーザー名の処理
Slackのプロフィールでは、ユーザーが自由に名前を設定できます。そのため、real_nameやdisplay_nameにはドットや特殊文字、絵文字などが含まれていることがあります。
この問題を解決するため、カスタムフィールドから名前を取得するようにしました。
# real_nameやdisplay_nameではなく、カスタムフィールドから名前を取得
english_name = fields.get("Xf01ABCDEF", {}).get("value", "")
japanese_name = fields.get("Xf02ABCDEF", {}).get("value", "")
- レイアウト調整の微調整
PDFの生成では、ラベルシートに合わせた正確なレイアウト調整が必要でした。
特に以下の点に注意しました:
- 実際に印刷して微調整する必要がある(理論値と実測値の差)
- 長いテキストが見切れないようにフォントサイズを自動調整
- 画像とテキストのバランス
私の環境では、なぜかマイナスを指定することで綺麗に収まりました。
# レイアウト調整用の定数
COLUMN_SPACING = 105 # 列間隔(mm)
ROW_SPACING = 59.5 # 行間隔(mm) <- 微調整
MARGIN_LEFT = 8 # 左右のマージン(mm)
MARGIN_TOP = -4 # 上下のマージン(mm) <- 微調整
- 一時ファイルの扱い
画像処理時に一時ファイルを作成する際、ファイル名に特殊文字が含まれていると保存に失敗するケースがありました。そこで、tempfileモジュールを使用して安全に一時ファイルを管理するようにしました。
with tempfile.TemporaryDirectory() as temp_dir:
# 安全な一時ファイル名を生成
temp_filename = f"temp_image_{i}.png"
temp_path = os.path.join(temp_dir, temp_filename)
(補足)Python実行環境の準備
Zennの読者には不要な案内かもしれませんが、念の為。
このスクリプトは Python 3.6 以上で動作します。まだPython環境がない方は、以下の方法でセットアップできます:
Windows
- Python公式サイトからインストーラーをダウンロード
- インストール時に「Add Python to PATH」にチェックを入れる
- コマンドプロンプトで
python --version
と入力して確認
macOS
- Homebrewがインストールされている場合:
brew install python
- またはPython公式サイトからインストーラーを使用
- ターミナルで
python3 --version
と入力して確認
仮想環境の作成(推奨)
仮想環境(Virtual Environment)とは
仮想環境は、Pythonプロジェクトごとに独立した環境を作成できる機能です。これにより、異なるプロジェクト間でのパッケージのバージョン競合を避け、クリーンな開発環境を維持できます。
仮想環境を使うメリット
- プロジェクト間の独立性: 異なるプロジェクトで異なるバージョンのライブラリを使用できる
- 依存関係の管理: 必要なパッケージだけをインストールできる
- 再現性: requirements.txtを使って同じ環境を他の人と共有できる
- システム環境の保護: グローバル環境を汚さない
仮想環境の作成方法
venv を使用する場合
# 仮想環境の作成
python -m venv myenv
# 仮想環境の有効化
# Windows
myenv\Scripts\activate
# macOS/Linux
source myenv/bin/activate
# 有効化後、コマンドプロンプトの先頭に (myenv) と表示されます
(myenv) $
# 仮想環境の無効化
deactivate
名札生成プロジェクトへの適用例
# プロジェクトフォルダの作成
mkdir slack-namecards
cd slack-namecards
# 仮想環境の作成
python -m venv venv
# 仮想環境の有効化
# Windows
venv\Scripts\activate
# macOS/Linux
source venv/bin/activate
# 以降のpipコマンドは、この仮想環境内でのみ有効になります
# 必要なパッケージのインストール
pip install requests Pillow reportlab
# フォント用のフォルダ作成
mkdir fonts
# IPAフォントのダウンロードと配置(手動)
# https://moji.or.jp/ipafont/ipaex00401/ からダウンロード後、
# fonts フォルダに ipaexg.ttf を配置
# スクリプトの作成
# generate_slack_labels.py をエディタで作成
# 実行
python generate_slack_labels.py
# プロジェクトの依存関係を保存(共有用)
pip freeze > requirements.txt
おわりに
Pythonを使ってSlackから情報を取得し、印刷用の名札PDFを作ってみました。
Pythonはあんまりよくわかっていないので、もっとちゃんと勉強しないとなあと反省しています。
そして、こういうことをモリモリ推進したり、社内システムをスクラッチで作ったりできるパーソンをへーしゃは求めています。ご興味ある方、カジュアル面談いかがでしょうか?
CIO は 7032さんです。
Discussion