Pythonを使ってBIZTEL 2系のユーザー一覧をエクスポートする

# はじめに
弊社で使っているIP電話サービスBIZTEL。バージョン2系と呼ばれる古いバージョンを使っているため、管理画面がかなり使いづらい。特に、ユーザーの一覧をエクスポートする機能がないので、欠番を簡単に確認できない。
そこで、Pythonを使ってユーザー一覧をCSVで吐き出してみようと思った。

ちなみに、これからBIZTELを導入する場合は、BIZTEL 3系になると思うから、管理画面も改善しているはず。

https://biztel.jp/

# 前提条件
- BIZTELのシステム管理権限
- Windows 11
- Python 3.10.3
    - requests 2.27.1
    - beautifulsoup4 4.10.0

# コード
```python
import re
import csv
import time
import urllib3

import requests
from bs4 import BeautifulSoup

# SSL証明書のエラー警告を無視
urllib3.disable_warnings()

class Login:
    def __init__(self, url, id, pw):
        self.session = requests.session()
        # URL末尾にスラッシュがない場合はつける
        self.base_url = url if url[-1] == '/' else url + '/'
        # ログイン情報をセット
        payload = {
            'inpAccount': id,
            'inpPassword': pw
        }
        # verify=FalseとしないとSSL証明書エラーとなる
        self.session.post(self.base_url + 'adminlogin.php', data=payload, verify=False)


class AccountManagement(Login):
    def __init__(self, url, id, pw):
        super().__init__(url, id, pw)
        self.account_management_url = self.base_url + 'account_management.php'
        account_management_response = self.session.get(self.account_management_url, allow_redirects=False)
        self.soup = BeautifulSoup(account_management_response.text, 'html.parser')

    def get_accounts(self):
        # 正規表現でユーザーの合計数を取得
        pattern = r'(?<=合計 )[0-9]+(?= 件)'
        total_text = self.soup.select('td:contains("合計")')
        total = re.search(pattern, total_text[-1].get_text(strip=True)).group(0)
        # ページ数を計算
        pages = int(total) // 10 + 1
        # ユーザー情報を格納するリスト
        accounts = []
        # 1~pagesまでページを取得する
        for i in range(pages):
            page = {
                'Ppage': i
            }

            response = self.session.post(self.account_management_url, data=page, allow_redirects=False)
            soup = BeautifulSoup(response.text, 'html.parser')
            # 名前と内線番号を取得
            names = [e.get_text() for e in soup.select('body > table > tr > td > table > tr > td > table > tr > td > table > TR > td > a[href="#"]')]
            phone_nums = [e.get_text(strip=True) for e in soup.select('body > table > tr > td > table > tr > td > table > tr > td > table > TR > td[width="90"]')]
            # 名前と内線番号をリストに格納
            for name, phone in zip(names, phone_nums):
                accounts.append([name, phone])
            # インターバル
            time.sleep(1)
        
        # ソート
        accounts.sort(key=lambda x: x[1])
        # ヘッダーをセット
        accounts.insert(0, ['ユーザ名', '内線番号'])

        # CSV ファイルへ出力する
        with open('output.csv', mode='w') as f:
            writer = csv.writer(f, lineterminator='\n')
            writer.writerows(accounts)
```


# 使用例
```Python
Biztel = AccountManagement('https://biztel:1234', 'ID', 'Password')
Biztel.get_accounts()
```


## SSLエラー回避
BIZTELはSSLサーバ証明書に自己証明書を利用しているため警告画面が表示されるとのこと。
セキュリティ上は問題ないらしいが、スクレイピングをする上で、エラーが出て止まってしまうので、回避策が必要。
そこで、`urllib3.disable_warnings()`を追記。

>BIZTELではサーバとの通信を暗号化しており、その際に使用しているSSLサーバ証明書に自己証明書を利用しているため警告画面が表示されますが 、セキュリティ上問題になるものではございません。

[BIZTEL FAQサイト](https://support.biztel.jp/hc/ja/articles/115001114753-%E7%AE%A1%E7%90%86%E7%94%BB%E9%9D%A2%E3%81%AB%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E3%81%97%E3%81%9F%E9%9A%9B-%E3%83%96%E3%83%A9%E3%82%A6%E3%82%B6%E3%81%A7-%E3%81%93%E3%81%AE-Web-%E3%82%B5%E3%82%A4%E3%83%88%E3%81%AE%E3%82%BB%E3%82%AD%E3%83%A5%E3%83%AA%E3%83%86%E3%82%A3%E8%A8%BC%E6%98%8E%E6%9B%B8%E3%81%AB%E3%81%AF%E5%95%8F%E9%A1%8C%E3%81%8C%E3%81%82%E3%82%8A%E3%81%BE%E3%81%99-%E3%82%84-%E3%81%93%E3%81%AE%E3%82%B5%E3%82%A4%E3%83%88%E3%81%AF%E5%AE%89%E5%85%A8%E3%81%A7%E3%81%AF%E3%81%82%E3%82%8A%E3%81%BE%E3%81%9B%E3%82%93-%E3%81%A8%E3%81%84%E3%81%A3%E3%81%9F%E3%82%A8%E3%83%A9%E3%83%BC%E3%81%8C%E5%87%BA%E3%81%A6%E3%83%AD%E3%82%B0%E3%82%A4%E3%83%B3%E7%94%BB%E9%9D%A2%E3%81%8C%E8%A1%A8%E7%A4%BA%E3%81%95%E3%82%8C%E3%81%AA%E3%81%84)

## ログイン
`inpAccount`と`inpPassword`にIDとパスワードを渡してPOST。

## アカウント管理ページ
```Python
class AccountManagement(Login):
    def __init__(self, url, id, pw):
        super().__init__(url, id, pw)
        self.account_management_url = self.base_url + 'account_management.php'
        account_management_response = self.session.get(self.account_management_url, allow_redirects=False)
        self.soup = BeautifulSoup(account_management_response.text, 'html.parser')
```

- クラス`Login`を継承する。
- アカウント管理用のURLは`https://biztel:1234/account_management.php`となる。
- `allow_redirects=False`これがないとログイン画面に飛ばされる。
- BS4の標準パーサーである`html.parser`を使う。

## アカウント管理ページをループ
```Python
    def get_accounts(self):
        # 合計を取得
        pattern = r'(?<=合計 )[0-9]+(?= 件)'
        total_text = self.soup.select('td:contains("合計")')
        total = re.search(pattern, total_text[-1].get_text(strip=True)).group(0)
        # ページ数を計算
        pages = int(total) // 10 + 1

        accounts = []

        for i in range(pages):
            page = {
                'Ppage': i
            }

            response = self.session.post(self.account_management_url, data=page, allow_redirects=False)
            soup = BeautifulSoup(response.text, 'html.parser')
            # 名前と内線番号を取得
            names = [e.get_text() for e in soup.select('body > table > tr > td > table > tr > td > table > tr > td > table > TR > td > a[href="#"]')]
            phone_nums = [e.get_text(strip=True) for e in soup.select('body > table > tr > td > table > tr > td > table > tr > td > table > TR > td[width="90"]')]
            # 名前と内線番号をリストに格納
            for name, phone in zip(names, phone_nums):
                accounts.append([name, phone])
            # インターバル
            time.sleep(1)
        
        # ソート
        accounts.sort(key=lambda x: x[1])
        accounts.insert(0, ['ユーザ名', '内線番号'])

        # CSV ファイルへ出力する
        with open('output.csv', mode='w') as f:
            writer = csv.writer(f, lineterminator='\n')
            writer.writerows(accounts)
```
- 正規表現を使ってユーザー数の合計を`total`へ格納。
- 1ページあたり10人なので、求めたユーザー数を10で割った整数商に1を足すと総ページ数となる。
- `accounts`にユーザー情報の一覧を格納。
- アカウント管理ページのページ数はパラメーター`Ppage`で管理。
- 1ページごとにユーザー名と内線番号を取得して`accounts`に追加。
- サーバーへ負荷をかけないように`time.sleep(1)`で1秒間のインターバル。
- `accounts.sort(key=lambda x: x[1])`で内線番号を基準にソートして、`accounts.insert(0, ['ユーザ名', '内線番号'])`でヘッダーを追加。
- CSV出力。


# おわりに
バージョン3系へ早く移行したい。。。