固定形式の7列目・継続行・コメント行——COBOLの列位置ルールを甘く見ていた最初のバージョンは、思いのほかSQLを取りこぼしていました。そのときの反省を踏まえた実装を載せています。
COBOLの固定形式とその処理方法
図1: COBOL EXEC SQL抽出 — 固定形式正規化からCRUD分類まで
COBOLのソースコードは固定形式(Fixed Format)と自由形式(Free Format)があります。多くのレガシーCOBOLは固定形式であり、各列には決まった役割があります。
| 列 | 名称 | 内容 | 処理方針 |
|---|---|---|---|
| 1〜6 | シーケンス番号 | 行番号(古いシステムでは使用) | 無視 |
| 7 | 指示子 | *=コメント行 / -=継続行 / D=デバッグ行 | コメント・デバッグ行をスキップ |
| 8〜11 | Area A | DIVISION・SECTION・段落名 | PROCEDURE DIVISION を検出 |
| 12〜72 | Area B | 命令文・SQL | 主な解析対象 |
| 73〜80 | プログラムID | プログラム識別子 | 切り捨て |
前処理:コメント行・継続行の正規化
import re
from pathlib import Path
def normalize_cobol_source(source: str) -> str:
"""
COBOLの固定形式ソースを正規化する。
- コメント行(7列目が * または /)を除去
- デバッグ行(7列目が D)を除去
- 継続行(7列目が -)を前の行に連結
- 73〜80列のプログラムID を切り捨て
"""
lines = source.splitlines()
result_lines = []
for raw_line in lines:
# 80文字に満たない行はパディングしない(後で72文字切り捨て)
if len(raw_line) < 7:
result_lines.append(raw_line)
continue
indicator = raw_line[6] # 7列目(0-indexed で 6)
# コメント行・デバッグ行はスキップ
if indicator in ('*', '/', 'D', 'd'):
continue
# 73〜80列を切り捨て(インデックス 72以降)
content = raw_line[:72]
if indicator == '-':
# 継続行:前の行の末尾に Area B(12〜72列)を連結
# 継続先の先頭の引用符を考慮して strip
continuation = content[11:].lstrip()
if result_lines:
# 前の行の末尾が引用符で終わる場合は引用符を除いて連結
prev = result_lines[-1]
if prev.rstrip().endswith("'"):
result_lines[-1] = prev.rstrip()[:-1] + continuation.lstrip("'")
else:
result_lines[-1] = prev.rstrip() + ' ' + continuation
continue
result_lines.append(content)
return '\n'.join(result_lines)
EXEC SQL ブロックの抽出
def extract_exec_sql_blocks(normalized_source: str) -> list[str]:
"""
正規化済みCOBOLソースから EXEC SQL〜END-EXEC ブロックを抽出する。
戻り値: SQL 文字列のリスト(ホスト変数 :VAR は ? に置換済み)
"""
# EXEC SQL から END-EXEC までを抽出(大文字小文字不問、複数行対応)
pattern = re.compile(
r'EXEC\s+SQL\s+(.*?)\s+END-EXEC',
re.IGNORECASE | re.DOTALL
)
blocks = []
for match in pattern.finditer(normalized_source):
sql_raw = match.group(1).strip()
# ホスト変数(:WK-XXX 等)を SQL パーサが処理できる ? に置換
sql_clean = re.sub(r':[A-Z][A-Z0-9-]*', '?', sql_raw, flags=re.IGNORECASE)
# 空白の正規化
sql_clean = re.sub(r'\s+', ' ', sql_clean).strip()
if sql_clean:
blocks.append(sql_clean)
return blocks
💡 ホスト変数の置換について
COBOLの埋め込みSQLでは :WK-TOKUI-CD のようなホスト変数(コロン始まり)を使います。JSqlParser はこれをそのままでは解析できないため、?(プレースホルダ)に置換してから渡します。
プログラム名の取得
def get_program_id(source: str) -> str:
"""
IDENTIFICATION DIVISION の PROGRAM-ID からプログラム名を取得する。
見つからない場合はファイル名を代替として使用する。
"""
pattern = re.compile(
r'PROGRAM-ID\s*\.\s*([A-Z0-9][A-Z0-9-]*)',
re.IGNORECASE
)
match = pattern.search(source)
if match:
return match.group(1).upper()
return 'UNKNOWN'
ファイル横断スキャンの実装
from pathlib import Path
from dataclasses import dataclass, field
@dataclass
class ProgramSqlInfo:
program_id: str
file_path: str
sql_list: list[str] = field(default_factory=list)
def scan_cobol_directory(
source_dir: str,
extensions: tuple[str, ...] = ('.cbl', '.cob', '.CBL', '.COB')
) -> list[ProgramSqlInfo]:
"""
ディレクトリ配下の COBOL ソースファイルをスキャンして
プログラム名と SQL リストを収集する。
"""
results = []
base = Path(source_dir)
for cbl_file in base.rglob('*'):
if cbl_file.suffix not in extensions:
continue
try:
# エンコーディングは環境に応じて変更(CP932 / UTF-8 等)
raw = cbl_file.read_text(encoding='cp932', errors='replace')
except Exception as e:
print(f'[WARN] 読み込み失敗: {cbl_file} — {e}')
continue
program_id = get_program_id(raw) or cbl_file.stem.upper()
normalized = normalize_cobol_source(raw)
sql_list = extract_exec_sql_blocks(normalized)
if sql_list:
results.append(ProgramSqlInfo(
program_id=program_id,
file_path=str(cbl_file),
sql_list=sql_list
))
print(f'スキャン完了: {len(results)} プログラムでSQL検出')
return results
動作確認
if __name__ == '__main__':
infos = scan_cobol_directory('./cobol_src')
for info in infos:
print(f'\n=== {info.program_id} ({info.file_path}) ===')
for i, sql in enumerate(info.sql_list, 1):
print(f' SQL #{i}: {sql[:80]}...' if len(sql) > 80 else f' SQL #{i}: {sql}')
実行結果(例)
スキャン完了: 3 プログラムでSQL検出 === URIAGE-TOROKU (./cobol_src/URIAGE-TOROKU.CBL) === SQL #1: SELECT TOKUI-CD, TOKUI-NM INTO ?, ? FROM TOKUI WHERE TOKUI-CD = ? SQL #2: INSERT INTO URIAGE (URIAGE-NO, TOKUI-CD, URIAGE-DT, KIN-GAK) VALUES (?, ?, ?, ?) SQL #3: INSERT INTO MEISAI (URIAGE-NO, SEQ, SHOHIN-CD, SU, KIN) VALUES (?, ?, ?, ?, ?) === URIAGE-SHUKEI (./cobol_src/URIAGE-SHUKEI.CBL) === SQL #1: SELECT SUM(KIN-GAK) INTO ? FROM URIAGE WHERE URIAGE-DT = ? SQL #2: SELECT URIAGE-NO, TOKUI-CD, KIN-GAK FROM URIAGE WHERE URIAGE-DT BETWEEN ? AND ?
✅ 次の章では…
PART 04 では抽出したSQL文字列を JSqlParser(または正規表現)に渡し、テーブル名と操作種別(C/R/U/D)を判別します。JOIN・サブクエリが絡む複雑なケースも解説します。
COBOLからのSQL抽出でよくある誤り
EXEC SQL〜END-EXECブロックの抽出は、継続行やコメント処理を誤ると取りこぼしが生じます。代表的な誤りを整理します。
| 誤り/失敗パターン | 何が起きるか | 正しい対処/防止策 |
|---|---|---|
| 継続行(7列目が-)の処理を省略する | 複数行に分割されたSQL文が途中で切断され、テーブル名や操作種別を正しく抽出できない | 7列目が-の行は前行の続きとして連結し、EXEC SQL〜END-EXECブロック全体を1文字列に結合してから解析する |
| コメント行の除外を忘れる | 7列目が*のコメント行内のEXEC SQLが実際のSQL文として誤って抽出される | 行処理の最初に7列目が*または/のコメント行をスキップし、有効なソース行のみを解析対象にする |
| EXEC SQLとEND-EXECをcase-sensitiveに比較する | 大文字小文字混在や全角スペース含むケースで抽出漏れが発生する | 比較前に行を大文字変換(upper())し、前後の空白をstrip()してから正規表現に渡す |