0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】PlaywriteでManabaの課題を自動取得するアプローチ

Last updated at Posted at 2025-07-21

【Python】PlaywriteでManabaの課題を自動取得するアプローチ

経緯

私の通っている大学ではManabaというサイトを利用し、課題提出や授業情報の確認ができます。
しかし、このManabaがまあ使いにくい。特に外部連携が充実していないので、課題のリマインドができない。
そこで、このManabaの課題を自動で取得するスクリプトを作成しました。

概要

弊学の学習管理システム「manaba」から、現在の課題データを自動で取得・分析するPythonスクリプトを開発しました。

PlaywrightとWebスクレイピング技術を活用することで、手動では時間のかかる課題管理作業を完全自動化し、課題情報をjsonファイルとして取得できるようになりました。

この方法に至った経緯

サイトにログインして情報を得たい → HTTP Requestで出来ないか?
→ しかし大学のようなサイトはSAML認証という一度限りの認証鍵でログインする方式であった → Cookieを使う方法もあるが、できれば持続してログインできるような方式にしたい → Playwriteを使ってさも人間がログインしているのと変わらない環境で自動化しよう!

今後の展開

作り方(参考までに)

🛠️ 開発環境・前提条件

# Python環境
Python 3.8以上

# 必要なライブラリ
pip install playwright
playwright install chromium

# その他標準ライブラリ
json, os, time, urllib.parse, datetime, csv

📋 システム構成

取得可能データ

  • 課題基本情報: タイトル、コース、締切日時
  • 課題詳細: 説明文、提出要件、評価基準
  • 提出方法: ファイルアップロード/テキスト入力/選択式
  • 添付ファイル: PDF、Word、Excel等のリンク
  • 統計情報: 課題タイプ別・コース別集計

出力形式

  • 詳細JSON: 全データの構造化保存
  • 統計JSON: 分析用サマリー情報
  • CSV: Excel等での分析用テーブル

🚀 実装コード

メインクラス定義

import json
import os
import time
from playwright.sync_api import sync_playwright
from urllib.parse import urljoin
from datetime import datetime

class TaskScraperFull:
    def __init__(self):
        self.tasks_data = {}
        self.output_dir = "/path/to/output/tasks_data"  # 出力ディレクトリ
        os.makedirs(self.output_dir, exist_ok=True)
        self.success_count = 0
        self.error_count = 0

1. manabaへの自動ログイン

def login_to_manaba(self, page):
    """manabaにログイン"""
    print("🔐 manabaにログインしています...")
    page.goto("https://room.chuo-u.ac.jp/")
    page.wait_for_timeout(3000)
    
    # ユーザーID・パスワード入力
    page.fill("input[name='username']", "YOUR_USER_ID")
    page.fill("input[name='password']", "YOUR_PASSWORD")
    page.click("#login_button")
    page.wait_for_timeout(5000)
    print("✅ ログイン完了")

ポイント:

  • page.wait_for_timeout()でページ読み込み完了を待機
  • セレクタは開発者ツールで事前に調査
  • エラーハンドリングでログイン失敗を検出可能

2. 課題一覧の取得

def extract_task_list(self, page):
    """課題一覧を取得"""
    print("📋 課題一覧ページにアクセスしています...")
    page.goto("https://room.chuo-u.ac.jp/ct/home_library_query")
    page.wait_for_timeout(5000)

    tasks = []
    table_rows = page.query_selector_all("table tr")
    headers = []
    
    for row_index, row in enumerate(table_rows):
        cells = row.query_selector_all("td, th")
        cell_texts = [cell.inner_text().strip() for cell in cells]
        
        # ヘッダー行検出
        header_keywords = ["Type", "タイプ", "Title", "タイトル"]
        if any(keyword in text for text in cell_texts for keyword in header_keywords):
            headers = cell_texts
            continue
            
        # データ行処理(6列の場合)
        task_types = ["レポート", "アンケート", "小テスト", "プロジェクト"]
        if len(cell_texts) == 6 and headers and cell_texts[0] in task_types:
            # 重要: タイトル部分のリンクを取得
            title_links = row.query_selector_all("a")
            task_link = None
            
            for link in title_links:
                href = link.get_attribute("href")
                link_text = link.inner_text().strip()
                
                # タイトルと完全一致するリンクを探す
                if href and link_text == cell_texts[1]:
                    task_link = href
                    break
            
            task_info = {
                "type": cell_texts[0],
                "title": cell_texts[1],
                "course": cell_texts[2],
                "start_time": cell_texts[3],
                "end_time": cell_texts[4],
                "period": cell_texts[5],
                "absolute_url": urljoin(page.url, task_link) if task_link else None
            }
            
            tasks.append(task_info)
    
    return tasks

重要な発見:
manabaでは各行に複数のリンクが存在します:

  • リンク0: タイプ部分 → 一般的な課題ページ
  • リンク1: タイトル部分 → 個別課題詳細ページ ✅

タイトル部分のリンクから遷移することで、正確な課題詳細データを取得できます。

3. 個別課題の詳細取得

def extract_task_details(self, page, task_info):
    """個別の課題詳細を取得"""
    try:
        print(f"📄 課題詳細取得中: {task_info['title']}")
        page.goto(task_info["absolute_url"], timeout=60000)
        page.wait_for_timeout(3000)

        # データ構造を準備
        task_details = {
            "basic_info": task_info,
            "page_title": page.title(),
            "description": "",
            "submission_info": [],
            "files": [],
            "deadline_info": {},
            "full_content": page.inner_text("body")
        }

        # 多段階フォールバック方式で説明文を取得
        description_selectors = [
            ".assignment-description", ".report-description", 
            ".content-body", ".task-content", ".pagebody",
            "main", ".main-content"
        ]
        
        for selector in description_selectors:
            element = page.query_selector(selector)
            if element:
                desc_text = element.inner_text().strip()
                if len(desc_text) > 50:  # 意味のある内容かチェック
                    task_details["description"] = desc_text
                    break

        # 提出方法の詳細分析
        submission_elements = page.query_selector_all(
            "input[type='file'], textarea, select, input[type='text']"
        )
        
        for element in submission_elements:
            tag_name = element.evaluate("el => el.tagName.toLowerCase()")
            element_info = {
                "tag": tag_name,
                "type": element.get_attribute("type") or "",
                "name": element.get_attribute("name") or "",
                "required": element.get_attribute("required") or False
            }
            
            # 提出形式を分類
            if tag_name == "input" and element.get_attribute("type") == "file":
                element_info["submission_type"] = "file_upload"
            elif tag_name == "textarea":
                element_info["submission_type"] = "text_area"
            elif tag_name == "select":
                element_info["submission_type"] = "selection"
            
            task_details["submission_info"].append(element_info)

        # 添付ファイルの取得
        file_selectors = [
            "a[href*='download']", "a[href*='.pdf']", 
            "a[href*='.doc']", "a[href*='.xlsx']"
        ]
        
        files = []
        for selector in file_selectors:
            file_links = page.query_selector_all(selector)
            for link in file_links:
                href = link.get_attribute("href")
                if href:
                    files.append({
                        "name": link.inner_text().strip(),
                        "url": urljoin(page.url, href),
                        "type": self.get_file_type(href)
                    })
        
        task_details["files"] = files
        self.success_count += 1
        return task_details

    except Exception as e:
        print(f"❌ エラー: {str(e)}")
        self.error_count += 1
        return {"error": str(e), "basic_info": task_info}

4. データ保存と統計生成

def save_data(self):
    """多形式でデータを保存"""
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    
    # 1. 詳細JSONデータ
    main_file = os.path.join(self.output_dir, f"all_tasks_{timestamp}.json")
    with open(main_file, "w", encoding="utf-8") as f:
        json.dump(self.tasks_data, f, indent=2, ensure_ascii=False)
    
    # 2. 統計情報JSON
    stats = {
        "total_tasks": len(self.tasks_data),
        "success_count": self.success_count,
        "error_count": self.error_count,
        "success_rate": f"{(self.success_count / len(self.tasks_data) * 100):.1f}%",
        "task_types": {},
        "courses": {},
        "submission_methods": {}
    }
    
    # 統計データの生成
    for task_data in self.tasks_data.values():
        if "basic_info" in task_data:
            task_type = task_data["basic_info"].get("type", "Unknown")
            course = task_data["basic_info"].get("course", "Unknown")
            stats["task_types"][task_type] = stats["task_types"].get(task_type, 0) + 1
            stats["courses"][course] = stats["courses"].get(course, 0) + 1
    
    stats_file = os.path.join(self.output_dir, f"stats_{timestamp}.json")
    with open(stats_file, "w", encoding="utf-8") as f:
        json.dump(stats, f, indent=2, ensure_ascii=False)
    
    # 3. CSVサマリー
    import csv
    csv_file = os.path.join(self.output_dir, f"summary_{timestamp}.csv")
    with open(csv_file, "w", encoding="utf-8", newline="") as f:
        writer = csv.writer(f)
        writer.writerow([
            "タイプ", "タイトル", "コース", "締切日時", 
            "提出形式", "添付ファイル数", "説明文字数"
        ])
        
        for task_data in self.tasks_data.values():
            if "basic_info" in task_data:
                info = task_data["basic_info"]
                files_count = len(task_data.get("files", []))
                desc_length = len(task_data.get("description", ""))
                submission_types = [s.get("submission_type", "") 
                                  for s in task_data.get("submission_info", [])]
                
                writer.writerow([
                    info.get("type", ""),
                    info.get("title", ""),
                    info.get("course", ""),
                    info.get("end_time", ""),
                    ", ".join(submission_types),
                    files_count,
                    desc_length
                ])

5. メイン実行部分

def run(self):
    """メイン実行メソッド"""
    print("🚀 課題データ取得を開始します")
    
    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)  # 高速化のため非表示
        page = browser.new_page()

        try:
            # ログイン
            self.login_to_manaba(page)

            # 課題一覧取得
            tasks_list = self.extract_task_list(page)
            total_tasks = len(tasks_list)
            print(f"📋 発見された課題数: {total_tasks}")

            # 各課題の詳細取得
            for i, task_info in enumerate(tasks_list, 1):
                print(f"[{i}/{total_tasks}] {task_info['title']}")
                
                task_details = self.extract_task_details(page, task_info)
                task_id = f"{task_info['course']}_{task_info['title']}_{i}"
                self.tasks_data[task_id] = task_details
                
                # 進捗表示
                progress = (i / total_tasks) * 100
                print(f"進捗: {progress:.1f}% (成功:{self.success_count}, エラー:{self.error_count})")
                
                time.sleep(2)  # サーバー負荷軽減

        except Exception as e:
            print(f"エラーが発生しました: {str(e)}")
        finally:
            browser.close()
            if self.tasks_data:
                self.save_data()

def main():
    scraper = TaskScraperFull()
    scraper.run()

if __name__ == "__main__":
    main()

📊 実行結果

成功率100%を達成!

🚀 課題データ取得を開始します
🔐 manabaにログインしています...
✅ ログイン完了
📋 課題一覧ページにアクセスしています...
📊 テーブル行数: 16
✅ ヘッダー発見: ['タイプ', 'タイトル', 'コース', '受付開始日時', '受付終了日時', '受付期間']

📋 発見された課題数: 13

[1/13] 第13回課題
進捗: 7.7% (成功:1, エラー:0)

[2/13] 工学デザイン概論_最終レポート  
進捗: 15.4% (成功:2, エラー:0)

...

[13/13] 第12回課題
進捗: 100.0% (成功:13, エラー:0)

============================================================
📊 最終結果
============================================================
総課題数: 13
成功: 13
エラー: 0
成功率: 100.0%
✅ 全課題データ取得完了!

取得データの統計分析

課題タイプ別分布:

{
  "task_types": {
    "レポート": 9,
    "アンケート": 4
  }
}

コース別分布:

{
  "courses": {
    "数理基礎1": 5,
    "工学デザイン概論": 2,
    "システムプログラム": 2,
    "特別英語5": 2,
    "脳・神経科学": 1,
    "基礎生物学": 1
  }
}

提出方式統計:

  • ファイルアップロード: 9件(全レポート課題)
  • オンライン回答: 4件(アンケート課題)

💡 技術的ポイント

1. 適切なセレクタ選択

# ❌ 間違った方法
link = row.query_selector("a")  # 最初のリンクを取得

# ✅ 正しい方法  
for link in title_links:
    if link.inner_text().strip() == cell_texts[1]:  # タイトル一致確認
        task_link = href

2. 多段階フォールバック

# 複数のセレクタで説明文を試行
description_selectors = [
    ".assignment-description",  # 最優先
    ".content-body",           # 次善
    ".pagebody"               # フォールバック
]

3. エラーハンドリング

try:
    page.goto(url, timeout=60000)  # 60秒タイムアウト
except Exception as e:
    self.error_count += 1
    return {"error": str(e)}

🔒 セキュリティ・注意事項

  1. 認証情報の管理

    # 環境変数での管理推奨
    import os
    username = os.getenv('MANABA_USERNAME')
    password = os.getenv('MANABA_PASSWORD')
    
  2. サーバー負荷への配慮

    time.sleep(2)  # 適切な間隔でアクセス
    
  3. 利用規約の遵守

    • 大学の情報システム利用規約に従う
    • 個人利用目的での使用に留める
    • 過度な負荷をかけない

📈 応用・拡張可能性

データ分析への活用

  • 締切管理: 優先度順の課題リスト生成
  • 学習計画: コース別・期限別の学習スケジュール
  • 統計分析: 課題提出状況・成績分析

他システムへの展開

  • 他大学のLMS(Learning Management System)
  • 企業内研修システム
  • 各種Webアプリケーションの自動データ取得

🏁 まとめ

PlaywrightとPythonを使用することで、manabaから完全自動で課題データを取得するシステムを構築できました。

主な成果:

  • 100%成功率での全課題データ取得
  • 📊 3つの形式での構造化データ出力
  • 高速・安定な処理性能
  • 🛡️ 堅牢なエラーハンドリング

このシステムにより、手動での課題管理作業を大幅に効率化し、データ駆動型の学習管理が可能になりました。

技術的な学びとして、Webスクレイピングにおける適切なセレクタ選択の重要性、多段階フォールバック方式の有効性、そして包括的なエラーハンドリングの必要性を実感できるプロジェクトでした。


この記事が同じような課題を抱える学生や開発者の方の参考になれば幸いです。質問やご意見がございましたら、コメントでお聞かせください!

🏷️ タグ

Python Playwright WebScraping 自動化 データ分析 manaba 中央大学

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?