0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WSLでJavaやPythonのソースコードからクラス図をDoxygen/Graphivzを使ってAnsible-playbookで生成する

0
Last updated at Posted at 2026-04-12

WSLでJavaやPythonのソースコードからクラス図をDoxygen/Graphivzを使ってAnsible-playbookで生成する

はじめに

  • WSL 上でソースコードからクラス図だけを抽出して一覧化し、ドキュメントとして落とすというニーズは、開発の現場ではときどき出てきますが、毎度手動でクラス図を手書きするのはかなり面倒です
  • 面倒なのはすべて ansible でやりましょう

特徴

  • Ansibleを使います。 Ansibleを使いたくないときは、以下の解説を見ながら手動で試してください
  • Python と Javaの両方に対応しています。実際に両方を試して確認しています
  • Doxygen/Graphivzで出力した HTML からクラス図のみを抽出するときに Python のコードを作っています
  • 試してないですが、RHELでもそのまま使えると思います

本文

構成

  • site_doxygen.yml
    • JavaやPythonのソースコードからクラス図をDoxygen/Graphivzを使ってAnsible-playbookで生成する ansible-playbookです
  • site_dixygen.pl
    • Ansible-playbook で生成した HTML からクラス図のみを抽出して index2.html を生成します
  • VBAを使ってExcelでクラス図の一覧を作ってヘッダやフッタを付けられるようにする

site_doxygen.yml

  • 以下を ansible_doxygen.yml として保存します
  • 環境に合わせて以下を変更します
    • project_source_dir: ソースコードのフォルダ位置を指定してます
    • project_output_dir: 出力先のフォルダ位置を指定します
    • project_name: タイトルを指定します
  • {{ project_output_dir }}/html/index.html にDoxygen/Graphivzを使ったリファレンスマニュアルを自動生成します
- hosts: localhost
  vars:
    # プロジェクトのソースパスとドキュメント出力先を指定
    project_source_dir: "./src/main/java"
    project_output_dir: "./doc_doxygen"
    project_name: "Doxygen Project"

  tasks:
    - name: doxygenの存在をチェック
      shell: command -v doxygen
      register: doxygen_exists
      failed_when: false
      changed_when: false
    - name: graphvizの存在をチェック
      shell: command -v dot
      register: dot_exists
      failed_when: false
      changed_when: false
    - name: Javaの状態を表示
      debug:
        msg: dot_exists

    - name: 存在してなかったらインストール
      apt:
        name:
          - doxygen
          - graphviz
        state: present
      become:
        - "{{doxygen_exists.rc != 0 | bool}}"
        - "{{dot_exists.rc != 0 | bool}}"
      when:
        - "{{doxygen_exists.rc != 0 | bool}}"
        - "{{dot_exists.rc != 0 | bool}}"

    - name: ドキュメント出力ディレクトリを作成
      ansible.builtin.file:
        path: "{{ project_output_dir }}"
        state: directory
        mode: '0755'

    - name: Doxyfileの生成 設定済みのものがない場合
      ansible.builtin.command:
        cmd: doxygen -g
        chdir: "."
        creates: "Doxyfile"

    - name: クラス図を有効にするためのDoxyfile設定変更
      ansible.builtin.lineinfile:
        path: "Doxyfile"
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
      loop:
        - { regexp: '^INPUT\s*=', line: 'INPUT = "{{ project_source_dir }}"' }
        - { regexp: '^PROJECT_NAME\s*=', line: 'PROJECT_NAME = "{{ project_name }}"' }
        - { regexp: '^OUTPUT_DIRECTORY\s*=', line: 'OUTPUT_DIRECTORY = {{ project_output_dir }}' }
        - { regexp: '^HAVE_DOT\s*=', line: 'HAVE_DOT = YES' }
        - { regexp: '^UML_LOOK\s*=', line: 'UML_LOOK = YES' }
        - { regexp: '^EXTRACT_ALL\s*=', line: 'EXTRACT_ALL = YES' }
        - { regexp: '^CLASS_GRAPH\s*=', line: 'CLASS_GRAPH = YES' }
        - { regexp: '^COLLABORATION_GRAPH\s*=', line: 'COLLABORATION_GRAPH = YES' }
        - { regexp: '^RECURSIVE\s*=', line: 'RECURSIVE = YES' }
        - { regexp: '^CALLER_GRAPH\s*=', line: 'CALLER_GRAPH = YES' }
        - { regexp: '^CLASS_DIAGRAMS\s*=', line: 'CLASS_DIAGRAMS = YES' }
        - { regexp: '^EXTRACT_PRIVATE\s*=', line: 'EXTRACT_PRIVATE = YES' }

    - name: Doxygenを実行してドキュメントを生成
      ansible.builtin.command:
        cmd: doxygen Doxyfile
        chdir: "."
      register: doxygen_result

    - name: Doxygenからクラス図を抽出する
      ansible.builtin.command:
        cmd: python3 site_doxygen.py
        chdir: "."

    - name: 実行結果の表示
      ansible.builtin.debug:
        msg: "ドキュメントが {{ project_output_dir }}/html/index.html に生成されました。"
      when: doxygen_result.rc == 0

site_doxygen.py

  • 次に以下を ansible_doxygen.py として保存します
  • ソースコード中にクラス図を抽出するPythonコードです
  • ターゲットとなる site_doxygen.yml を指定しているので、そこだけ注意が必要です
  • {{ project_output_dir }}/html/index2.html に抽出したクラス図のリストを表示します
import re
import sys

base = None
project_name = ""
with open('site_doxygen.yml', 'r', encoding='utf-8') as f:
    for line in f:
        match = re.search('project_output_dir\\s*:\\s*\"([^\"]+)', line)
        if match:
            base = match.group(1)
        match = re.search('project_name\\s*:\\s*\"([^\"]+)', line)
        if match:
            project_name = match.group(1)

if base is None:
    sys.exit(-1)

uml = base + "/html/annotated.html"

urls = []
with open(uml, 'r', encoding='utf-8') as f:
    for line in f:
        match = re.search('<a\\s+class="([^\"]+)\"\\s+href=\"(class[^\"]+)', line)
        if match:
            urls.append(match.group(2))

imgs = []
for url in urls:
    url = base + "/html/" + url
    with open(url, 'r', encoding='utf-8') as f:
        title = ""
        for line in f:
            match = re.search('^(Inheritance|Collaboration)\\s+[^<>]+', line)
            if match:
                title = match.group()
            match = re.search('<img\\s+src=\"([^\"]+)', line)
            if match:
                imgs.append({"title": title, "utl": match.group(1)})
                break

uml = base + "/html/index2.html"
with open(uml, mode='w', encoding="utf-8") as f:
    f.write("<html lang=\"jp\">\n")
    f.write("<head>\n")
    f.write("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n")
    f.write("</head>\n")
    f.write("<body>\n")
    f.write(f"<H1>{project_name}</H1>")
    for key in imgs:
        f.write(f"<H2>{key["title"]}</H2>")
        f.write(f"<img src=\"{key["utl"]}\" />\n")  # height=\"100%\"
        f.write("<mbp:pagebreak/>\n")
    f.write("</body>\n")
    f.write("</html>\n")
#

動かし方

  • 以下を実行します
  • apt install をrootで実行するために -K (大文字のK)オプションを指定しています
ansible-playbook -K ansible_doxygen.yml
  • すでにdoxygenとgraphvizがインストール済であれば rootは不要なので-K (大文字のK)オプションは不要です
ansible-playbook ansible_doxygen.yml

出力例

tmp.png

解説

  • apt で doxygen と graphvizを生成します
  • こう書くとインストール済のときroot不要となりより安全です
    - name: doxygenの存在をチェック
      shell: command -v doxygen
      register: doxygen_exists
      failed_when: false
      changed_when: false
    - name: graphvizの存在をチェック
      shell: command -v dot
      register: dot_exists
      failed_when: false
      changed_when: false
    - name: Javaの状態を表示
      debug:
        msg: dot_exists

    - name: 存在してなかったらインストール
      apt:
        name:
          - doxygen
          - graphviz
        state: present
      become:
        - "{{doxygen_exists.rc != 0 | bool}}"
        - "{{dot_exists.rc != 0 | bool}}"
      when:
        - "{{doxygen_exists.rc != 0 | bool}}"
        - "{{dot_exists.rc != 0 | bool}}"
  • ドキュメント用のフォルダを作成します
  • mkdirと同等です
    - name: ドキュメント出力ディレクトリを作成
      ansible.builtin.file:
        path: "{{ project_output_dir }}"
        state: directory
        mode: '0755'
  • ドキュメント用のフォルダを作成します
  • これによりカレントフォルダに DoxyFileが生成されます
  • doxygen -g と同等です
    - name: Doxyfileの生成 設定済みのものがない場合
      ansible.builtin.command:
        cmd: doxygen -g
        chdir: "."
        creates: "Doxyfile"
  • 生成された Doxyfile を Java や Python 用にクラス図を出力するために書き換えます
  • vi Doxyfile による手動修正と同等です
  • Ansibleを使わないときは手動でちまちま探して直さないといけないですが、Ansibleはこのような書き方でまとめて直せます
    - name: クラス図を有効にするためのDoxyfile設定変更
      ansible.builtin.lineinfile:
        path: "Doxyfile"
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
      loop:
        - { regexp: '^INPUT\s*=', line: 'INPUT = "{{ project_source_dir }}"' }
        - { regexp: '^PROJECT_NAME\s*=', line: 'PROJECT_NAME = "{{ project_name }}"' }
        - { regexp: '^OUTPUT_DIRECTORY\s*=', line: 'OUTPUT_DIRECTORY = {{ project_output_dir }}' }
        - { regexp: '^HAVE_DOT\s*=', line: 'HAVE_DOT = YES' }
        - { regexp: '^UML_LOOK\s*=', line: 'UML_LOOK = YES' }
        - { regexp: '^EXTRACT_ALL\s*=', line: 'EXTRACT_ALL = YES' }
        - { regexp: '^CLASS_GRAPH\s*=', line: 'CLASS_GRAPH = YES' }
        - { regexp: '^COLLABORATION_GRAPH\s*=', line: 'COLLABORATION_GRAPH = YES' }
        - { regexp: '^RECURSIVE\s*=', line: 'RECURSIVE = YES' }
        - { regexp: '^CALLER_GRAPH\s*=', line: 'CALLER_GRAPH = YES' }
        - { regexp: '^CLASS_DIAGRAMS\s*=', line: 'CLASS_DIAGRAMS = YES' }
        - { regexp: '^EXTRACT_PRIVATE\s*=', line: 'EXTRACT_PRIVATE = YES' }
  • Doxygen/Graphivz をつかってDoxygileからクラス図を生成します
  • doxygen Doxyfile と同等です
    - name: Doxygenを実行してドキュメントを生成
      ansible.builtin.command:
        cmd: doxygen Doxyfile
        chdir: "."
      register: doxygen_result
  • Doxygen/Graphivz の出力は余分なものも含まれているので、以下によりクラス図を抽出します
  • python3 site_doxygen.py と同等です
    - name: Doxygenからクラス図を抽出する
      ansible.builtin.command:
        cmd: python3 site_doxygen.py
        chdir: "."
  • 結果を表示します
    - name: 実行結果の表示
      ansible.builtin.debug:
        msg: "ドキュメントが {{ project_output_dir }}/html/index.html に生成されました。"
      when: doxygen_result.rc == 0

VBAを使ってExcelでクラス図の一覧を作ってヘッダやフッタを付けられるようにする

  • 上記では、site_doxygen.py を使ってクラス図の一覧をHTMLで作り、それをpdf化することでクラス図の一覧を印刷できるようにしました
  • しかし、site_doxygen.pyで出力されるindex.htmlはヘッダが付かなかったり、改行がうまく行かなかったり、画像がPDFからはみ出たり、ページ番号が付かないといった事象がおきます
  • そこで、VBAを使ってヘッダやフッタを付けられるようにしたのが、以下となります
  • 簡単にできるかと思ったら結構長く…
Public FSO As Object


Sub ボタン_Click()
    If FSO Is Nothing Then
        Set FSO = CreateObject("Scripting.FileSystemObject")
    End If
    
    Dim filePath As String
    
    ' ファイル選択ダイアログを表示
    filePath = Application.GetOpenFilename(title:="Doxygenのindex.htmを指定してください", FileFilter:="Doxygen File,*.html")
    
    ' キャンセルされた場合は処理を終了
    If filePath = "False" Then
        MsgBox "キャンセルされました"
        Exit Sub
    End If
    
    Dim wb As Workbook
    Set wb = Workbooks.Add
    Dim ws As Worksheet
    Set ws = wb.Worksheets(1)
    ws.Name = "index"


    Dim folderPath As String
    folderPath = FSO.GetParentFolderName(filePath)
    filePath = folderPath & "\annotated.html"

    
    Dim re As New RegExp
    re.Pattern = "\s+<div\s+id=""projectname"">([\s\S]+)"
    re.Global = True
    Dim re2 As New RegExp
    re2.Pattern = "<a\s+class=""[^""]+""\s+href=""(class[^""]+).html"
    re2.Global = True
    Dim re3 As New RegExp
    re3.Pattern = "^\d+"
    re3.Global = True

    Dim projectname As String
    projectname = ""

    Dim rowNum As Long
    rowNum = 2
    
    Set ts = FSO.OpenTextFile(filePath, 1)
    Do Until ts.AtEndOfStream
        Dim mc As MatchCollection
        Dim m As Match
        lineStr = ts.ReadLine ' 1行読み込み
        '
        Set mc = re.Execute(lineStr)
        For Each m In mc
            projectname = m.SubMatches(0)
            ws.Range("A1").Value = projectname
        Next
        '
        Set mc = re2.Execute(lineStr)
        For Each m In mc
            Dim str As String
            str = m.SubMatches(0)
            Cells(rowNum, 2).Value = str & ".html"
            '
            str = Mid(str, InStrRev(str, "_") + 1)
            str = re3.Replace(str, "")
            Cells(rowNum, 1).Value = str
            '
            rowNum = rowNum + 1
        Next
    Loop
    ts.Close

    rowNum = 2
    Do Until ws.Cells(rowNum, 1).Value = ""
        str = ws.Cells(rowNum, 1).Value
        Dim classWs As Worksheet
        Set classWs = SheetGet(wb, str)
        
        str = folderPath & "\" & ws.Cells(rowNum, 2).Value
        ws.Cells(rowNum, 2).Value = CrateClassData(classWs, folderPath, str, projectname)
        
        ' 行を増やす
        rowNum = rowNum + 1
    Loop
        
    ' 全ワークシートを選択
    wb.Worksheets.Select
    ' アクティブウィンドウを改ページプレビューに設定
    ActiveWindow.View = xlPageBreakPreview

    ' 表題を入れる
    Application.DisplayAlerts = False
    ws.Range("A1:B1").Merge
    Application.DisplayAlerts = True
    ws.Range("A1").Value = "Table of Contents"
    ws.Range("A1").Font.Bold = True
    ws.Range("A1").Font.Size = 16

    ' 1行で1ページにする
    ws.PageSetup.Zoom = False
    ws.PageSetup.FitToPagesWide = False
    ws.PageSetup.FitToPagesWide = 1
    
    ' 列幅を文字数に合わせる
    ws.Cells.EntireColumn.AutoFit

    ' 先頭をアクティブページにする
    wb.Worksheets(1).Activate
End Sub


' シートの存在を検知する
Function SheetExists(wb As Workbook, sheetName As String) As Boolean
    Dim ws As Worksheet
    SheetExists = False
    For Each ws In wb.Sheets
        ' 大文字小文字を区別せず比較
        If UCase(ws.Name) = UCase(sheetName) Then
            SheetExists = True
            Exit Function
        End If
    Next ws
End Function


' オリジナルなシートを取得する
Function SheetGet(wb As Workbook, sheetName As String) As Worksheet
    Dim i As Long
    Dim ws As Worksheet
    i = 0
    Do
        If i = 0 Then
            If SheetExists(wb, sheetName) = False Then
                Set ws = wb.Worksheets.Add(After:=wb.Worksheets(wb.Worksheets.Count))
                ws.Name = sheetName
                Set SheetGet = ws
                Exit Function
            End If
        Else
            Dim str As String
            str = sheetName & "(" & CStr(i) & ")"
            If SheetExists(wb, str) = False Then
                Set ws = wb.Worksheets.Add(After:=wb.Worksheets(wb.Worksheets.Count))
                ws.Name = str
                Set SheetGet = ws
                Exit Function
            End If
        End If
        i = i + 1
    Loop
End Function

' クラス図を表示する
Function CrateClassData(ws As Worksheet, folderPath As String, filePath As String, projectname As String) As String
    Dim re As New RegExp
    re.Pattern = "^(Inheritance|Collaboration)\s+[^<>]+"
    re.Global = True
    Dim re2 As New RegExp
    re2.Pattern = "<img\s+src=""([^""]+)"
    re2.Global = True
    
    Dim title As String
    title = ""

    Set ts = FSO.OpenTextFile(filePath, 1)
    Do Until ts.AtEndOfStream
        Dim mc As MatchCollection
        Dim m As Match
        lineStr = ts.ReadLine ' 1行読み込み
    
        Set mc = re.Execute(lineStr)
        For Each m In mc
            title = m.Value
        Next
        title = Replace(title, "&lt;", "<")
        title = Replace(title, "&gt;", ">")
    
        Set mc = re2.Execute(lineStr)
        For Each m In mc
            Dim str As String
            str = folderPath & "\" & m.SubMatches(0)
            Call InsertPictureToFit(ws, str, projectname)
            ws.Range("A1").Value = title
            Exit Do
        Next
    Loop
    ts.Close

    CrateClassData = title
End Function


' 画像を挿入して1ページに収める
Sub InsertPictureToFit(ws As Worksheet, imgPath As String, projectname As String)
    Dim pic As Shape
    Dim targetRange As Range
    Set targetRange = ws.Range("A1")
    
    Set pic = ws.Shapes.AddPicture( _
        Filename:=imgPath, _
        LinkToFile:=msoFalse, _
        SaveWithDocument:=msoTrue, _
        Left:=targetRange.Left, _
        Top:=targetRange.Top, _
        Width:=-1, _
        Height:=-1)
    
    pic.LockAspectRatio = msoTrue
    If pic.Width > ws.Cells(1, 1).Width * 10 Then
        pic.Width = 500
    End If
    If pic.Height > 500 Then
        pic.Height = 500
    End If

    ' 一行追加する
    ws.Rows(1).Insert

    ' 必要に応じて印刷設定
    ws.PageSetup.Zoom = False
    ws.PageSetup.FitToPagesWide = 1
    ws.PageSetup.FitToPagesTall = 1
    ws.PageSetup.RightHeader = projectname
    ws.PageSetup.CenterFooter = "&P / &N"
End Sub
0
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?