Ncコメント編【Vol.2】
前回は、「NCワード」を「アドレスと数値・数字」に分解するコードを作成しました
次は、一行の「NCブロック」を「NCワード」に分解させる予定ですが、その前に処理しておきたいのがコメントです。
コメントはファナックでは、( ~ )に挟まれた文字は、制御器は無視するのでメッセージなどに利用されますが、
「Gコード」や「X、Y、Z」などNCワードと同形式の文字列がコメント部に含まれていると
単純に文字列検索だけで処理すると、思わぬ分析ミスになりかねません
この記事では、純粋なコード部分とコメント部分を分離するライブラリを作成します
コントローラによるコメント記号の種類
Fanuc制御器では ( で始まり、 ) で閉じるまでをコメントと認識
このコメントは、複数行を跨いでも ) で閉じるまで、コメントとして扱います
OSP制御器も基本的にFanucと同じですが、( から、行末までをコメントにします
したがって、 ) で閉じる必要はないですが、Fanucのように、行を跨がせないのが一般的です
変換ソフトなどで、ファナックと共有したい場合には、確実に閉じたほうが安全です
Heidenhain の場合は、; 文字から、行末まで、
行頭に * の場合一行すべてをコメントとして扱います
Roedersの場合には、C言語に似ていて、
/* ~ */の間、や // 文字列から行末までがコメントになります
今回は、将来性も考えて、上記4点に対応した、コメント処理ライブラリを作成します
クラスライブラリ
「開始記号〜終了記号」で囲むタイプのコメント1種類分の仕様を定義
行コメント(”;” や “//”)はこのクラスの対象外(line_markers側で扱う)
open_: 開始記号。例 “(” “/*”
close_: 終了記号。例 “)” “*/”
nested: 入れ子 “(…(…)…)” を許容するか。
multiline: 行末までに閉じなかった場合、未閉鎖の状態を次の行に持ち越すか
True = 複数行にまたがるコメントを許容する(例: FANUC)
False = 必ずその行の中で強制的に閉じる(例: OSPは自動で閉じるがデータでの処理が安全)
# test code
bcr = BlockCommentRule("/*", "*/")
open = bcr.open
close = bcr.close
print(f"コメント始まり文字={open} コメント終了文字={close}")
/* */
TypedDict は 実態はただの辞書(dict)であるが、dictに型情報を付与するための仕組み。
dictは値の型が曖昧だが、このクラスで辞書の型を決定し型情報が違う場合、
静的型チェッカーが教えてくれる。
#このクラスでコメントのタイプの型形式を定義
class DialectSpec(TypedDict):
blocks: List[BlockCommentRule] # 囲むタイプ [("(",")") , ("/*","*/")]など
line_markers: List[str] # 行末までのタイプ [ ";" , "//" ]など
line_start_markers: List[str] # 必ず先頭にあるタイプ["*"]など
制御器名とコメント記法(囲みコメントか単独か)の違いをまとめて定義
DIALECTS[“fanuc”][“blocks”][0].open で、
“fanuc”の”blicks”コメントの始まり文字(open)を参照できる
# test code
a = DIALECTS["osp"]["blocks"][0].open
b = DIALECTS["osp"]["blocks"][0].close
c = DIALECTS["osp"]["blocks"][0].nested
d = DIALECTS["osp"]["blocks"][0].multiline
print(f"open={a} close={b} nested={c} multiline={d}")
「実行結果」
open=( close=) nested=True multiline=False
NCブロックを純粋コードとコメントに分解する
引数:dialectに制御器情報を渡すが、複数定義にも対応している
dialect=”fanuc”(文字列1個)と dialect=[“fanuc”,”heidenhain”](リスト)の両方に対応
また、省略定義にも対応
“F”→”fanuc” “O”→”osp” “H”→”heidenhain” “R”→”roeders”
def open_comment(self) -> str:
コメントの始まり記号を返す(@propertyなので、変数のように使える)
コメントの引き継ぎ状態をリセットする
一行のNC文字列を、NCコードとコメントに分解する
別のNCデータを連続で実施する場合、コメントを確実に閉じる
一行NC文字列をNCコードとコメントに分解し、それぞれtupleで返す
# test code
ncblock = [
"G01 X10 Y20 (this is a comment) F100(next",
"next)G02 X30(unterminated comment ;starts here",
]
processor = NcCommentProcessor(dialect="fanuc")
for line in ncblock:
print(f"Original Nc={line!r}")
code, comment = processor.parse_code_comment(line)
print(f"parse_code_comment: code={code!r}, comment={comment!r}")
print(f"-------------------------------")
「実行結果」
Original Nc='G01 X10 Y20 (this is a comment) F100(next'
parse_code_comment: code='G01 X10 Y20 F100', comment='(this is a comment)(next)'
-------------------------------
Original Nc='next)G02 X30(unterminated comment ;starts here'
parse_code_comment: code='G02 X30', comment='(next)(unterminated comment ;starts here)'
-------------------------------
NCコメントプロセッサクラス
実際の、コードです
ただし、コメント行が跨る場合で、意図しない分割になる可能性もあります
NCのコメントは、確実にその行で閉じる処理をすることが安全です
from typing import Dict, List, Optional, Sequence, Tuple, TypedDict, Union
class BlockCommentRule:
"""
「開始記号〜終了記号」で囲むタイプのコメント1種類分の仕様。
例: FANUCの "(" ")" や Roedersの "/*" "*/" など、開始と終了のペアで
構成されるコメント記法を表す。開始記号だけ・終了記号だけの
行コメント(";" や "//")はこのクラスの対象外(line_markers側で扱う)。
"""
__slots__ = ("open", "close", "nested", "multiline")
""" __slots__ を書くと、このクラスのインスタンスは
「ここに列挙した名前の属性しか持てない」という制限がかかる"""
def __init__(
self, open_: str, close_: str, nested: bool = False, multiline: bool = False
):
"""
Args:
open_: 開始記号。例 "(" "/*"
close_: 終了記号。例 ")" "*/"
nested: 入れ子 "(...(...)...)" を許容するか。
True なら開始記号が現れるたびにネスト深さを1つ増やし、
対応する終了記号がすべて出そろうまで閉じたとみなさない。
multiline: 行末までに閉じなかった場合、未閉鎖の状態を
次の行に持ち越すか。
True = 複数行にまたがるコメントを許容する(例: FANUC)
False = 必ずその行の中で強制的に閉じる(例: OSP)
"""
self.open = open_
self.close = close_
self.nested = nested # 入れ子 "(...(...)...)" を許容するか
self.multiline = multiline # 閉じないまま行末を迎えたとき、次行に持ち越すか
class DialectSpec(TypedDict):
"""
1つの制御装置(機種)が持つコメント記法一式を表す型。
DIALECTS 辞書の値の型として使う。全dialectで3つのキーを
必ず揃えること(使わないものは空リスト [] を入れる)。
"""
blocks: List[BlockCommentRule]
"""開始〜終了で囲むタイプのコメント規則の一覧。例: [("(",")"), ("/*","*/")]"""
line_markers: List[str]
"""行の"どこにあっても"、そこから行末まで丸ごとコメントになる記号。例: ";" "//" """
line_start_markers: List[str]
"""行の"先頭にある場合のみ"、そこから行末まで丸ごとコメントになる記号。
例: HEIDENHAINの "*"(行の途中では掛け算として使われるため区別が必要)"""
# 機種(dialect)ごとのコメント記法定義
# ※ キー構成(blocks / line_markers / line_start_markers)は全エントリで必ず揃える
# (使わないものは空リストを明示する)。バラバラだと型チェッカーが辞書の形を
# 一つに決められず、DIALECTS[name]["..."] のようなアクセスで警告が出る。
DIALECTS: Dict[str, DialectSpec] = {
# FANUC: ( ) 入れ子OK、複数行にまたがる未閉鎖を許容
"fanuc": {
"blocks": [BlockCommentRule("(", ")", nested=True, multiline=True)],
"line_markers": [],
"line_start_markers": [],
},
# OSP: ( ) 入れ子OK、ただし必ず1行内で強制的に閉じる
# 通常は複数行にはまたがらないが、fanucとの互換性のため、multiline=Trueにする
"osp": {
"blocks": [BlockCommentRule("(", ")", nested=True, multiline=True)],
"line_markers": [],
"line_start_markers": [],
},
# HEIDENHAIN: ; はどこにあっても行末までコメント。
# * は「掛け算」としても使われるため、行頭にある場合のみ
# アウトライン的な見出しコメントとして扱う(行の途中の * はコード)。
"heidenhain": {
"blocks": [],
"line_markers": [";"],
"line_start_markers": ["*"],
},
# Roeders(独自制御言語): /* */ の囲み(複数行可) または // で行末までコメント
# ※ 囲みコメント内での単文字コメント(; など)の扱いは未確認のため、
# ひとまず「囲み内は閉じタグ(*/)まで不透明」という一般的な仕様を仮定しています。
# 要調査・要確認ポイント。
"roeders": {
"blocks": [BlockCommentRule("/*", "*/", nested=False, multiline=True)],
"line_markers": ["//"],
"line_start_markers": [],
},
}
class NcCommentProcessor:
"""
NCブロックを純粋コードとコメントに分解する(機種ごとのコメント記法に対応)
processor = NcCommentProcessor(dialect="fanuc")
for line in ncs:
code, comment = processor.parse_code_comment(line)
print(f"code={code},comment={comment}")
dialect:
"F" or "fanuc" : ( ) 入れ子OK、複数行にまたがる未閉鎖を許容
"O" or "osp" : ( ) 入れ子OK、必ず1行内で強制的に閉じる(次行に持ち越さない)
"H" or "heidenhain" : ; はどこでも行末までコメント。* は行頭にある場合のみ行末までコメント
"R" or "roeders" : /* */ の囲み(複数行可) または // で行末までコメント
複数dialectをまとめて使いたい場合はリストで渡す(ルールを合成する):
NcCommentProcessor(dialect=["fanuc", "heidenhain", "roeders"])
リスト内で同じ開始記号(例: "(" )を複数dialectが定義していた場合は、
リストで先に書いたdialectのルールが優先される。
"""
def __init__(self, dialect: Union[str, Sequence[str]] = "fanuc"):
# isinstance:指定したデータ型(クラス)であるかどうか
names = [dialect] if isinstance(dialect, str) else list(dialect)
mapping = {"F": "fanuc", "O": "osp", "H": "heidenhain", "R": "roeders"}
for n, i in enumerate(names, 0):
names[n] = mapping.get(i, i)
unknown = [d for d in names if d not in DIALECTS]
if unknown:
raise ValueError(
f"未知のdialectです: {unknown!r} (選べるのは {list(DIALECTS)})"
)
self.dialect = names[0] if len(names) == 1 else names
# 複数dialect分のルールを合成する。開始記号(open)が重複するものは
# 先に書いたdialectのルールを優先し、後発の重複ルールは捨てる。
blocks: List[BlockCommentRule] = []
seen_open = set()
for name in names:
for rule in DIALECTS[name]["blocks"]:
if rule.open in seen_open:
continue
seen_open.add(rule.open)
blocks.append(rule)
line_markers: List[str] = []
for name in names:
for marker in DIALECTS[name]["line_markers"]:
if marker not in line_markers:
line_markers.append(marker)
# 長いマーカーを先に判定できるよう、長さ降順に並べる
# (例: "/*" と "*" が両方あるとき、"*" だけで先に食われないように)
line_markers.sort(key=len, reverse=True)
line_start_markers: List[str] = []
for name in names:
for marker in DIALECTS[name].get("line_start_markers", []):
#for marker in DIALECTS[name]["line_start_markers"]:
if marker not in line_start_markers:
line_start_markers.append(marker)
line_start_markers.sort(key=len, reverse=True)
self._blocks: List[BlockCommentRule] = blocks
self._line_markers: List[str] = line_markers
self._line_start_markers: List[str] = line_start_markers
# 行をまたぐ未閉鎖ブロックの状態: (rule, 現在の未閉鎖の深さ)
self._open_block: Optional[Tuple[BlockCommentRule, int]] = None
@property
def open_comment(self) -> str:
return self._open_block[0].open if self._open_block else ""
def reset(self) -> None:
self._open_block = None
def _scan(self, line: str) -> List[Tuple[str, str]]:
"""1行を (種別, 文字列) のトークン列に分解する。種別は 'code' か 'comment'。"""
text = line.rstrip()
n = len(text)
tokens: List[Tuple[str, str]] = []
i = 0
# 前の行から続く未閉鎖ブロックコメントの続きを最優先で処理する
# (ここを最初に処理することで、囲みの中の ; や // を誤って行コメント開始と
# 判定してしまう不具合を避けている)
if self._open_block is not None:
rule, depth = self._open_block
j = 0
while j < n and depth > 0:
if rule.nested and text.startswith(rule.open, j):
depth += 1
j += len(rule.open)
continue
if text.startswith(rule.close, j):
depth -= 1
j += len(rule.close)
continue
j += 1
if depth > 0:
# まだ閉じない: 開始記号を補って「継続中のコメント」と分かるようにし、
# 残っている深さ(depth)を次行にそのまま引き継ぐ
tokens.append(("comment", rule.open + text + rule.close * depth))
self._open_block = (rule, depth)
return tokens
tokens.append(("comment", rule.open + text[:j]))
self._open_block = None
i = j
while i < n:
matched = False
# 行頭専用のマーカー(例: HEIDENHAINの見出し的な "*")は
# 行の一番最初の位置でのみコメット開始とみなす
if i == 0:
for marker in self._line_start_markers:
if text.startswith(marker, i):
tokens.append(("comment", text[i:n]))
i = n
matched = True
break
if matched:
continue
for rule in self._blocks:
if text.startswith(rule.open, i):
depth = 1
j = i + len(rule.open)
while j < n and depth > 0:
if rule.nested and text.startswith(rule.open, j):
depth += 1
j += len(rule.open)
continue
if text.startswith(rule.close, j):
depth -= 1
j += len(rule.close)
continue
j += 1
if depth > 0:
tokens.append(("comment", text[i:n] + rule.close * depth))
if rule.multiline:
self._open_block = (rule, depth)
i = n
else:
tokens.append(("comment", text[i:j]))
i = j
matched = True
break
if matched:
continue
for marker in self._line_markers:
if text.startswith(marker, i):
tokens.append(("comment", text[i:n]))
i = n
matched = True
break
if matched:
continue
if tokens and tokens[-1][0] == "code":
tokens[-1] = ("code", tokens[-1][1] + text[i])
else:
tokens.append(("code", text[i]))
i += 1
return tokens
def reset_open_block(self):
"""別のNCデータを連続で実施する場合、コメントを確実に閉じる"""
block = self._open_block
if block is not None:
self.reset()
def parse_code_comment(self, line: str) -> Tuple[str, str]:
"""純粋コードとコメントを分割する"""
tokens = self._scan(line)
code = "".join(t for k, t in tokens if k == "code").rstrip()
comment = "".join(t for k, t in tokens if k == "comment")
return code, comment
# test code
if __name__ == "__main__":
ncblock = [
"G01 X10 Y20 (this is a comment) F100(next",
"next)G02 X30(unterminated comment ;starts here",
]
processor = NcCommentProcessor(dialect="fanuc")
for line in ncblock:
print(f"Original Nc={line!r}")
# code = processor.normalize(line)
# print(f"normalize: line={code!r}")
code, comment = processor.parse_code_comment(line)
print(f"parse_code_comment: code={code!r}, comment={comment!r}")
print(f"-------------------------------")
processor.reset_open_block()
for line in ncblock:
print(f"Original Nc={line!r}")
# code = processor.normalize(line)
# print(f"normalize: line={code!r}")
code, comment = processor.parse_code_comment(line)
print(f"parse_code_comment: code={code!r}, comment={comment!r}")
print(f"-------------------------------")

コメント