この記事は スタンバイ Advent Calendar2022の2日目の記事です。
動機
業務ではmacでxml形式のファイルを触る機会が多く、xmllint
を使って検査したりしてます。
しかし、xmllint
でエラーになってもエディタでは目視できない謎の存在がいたりします。
本記事ではPythonでこの謎の存在Xを退治したいと思います。
動作環境
- macOS Monterey
- Python 3.10.6
方針
全体を一気に読むと大変なのでとりあえずxmlファイルを1行ずつ読み込んでいく方針で行きます。
テストデータの用意
まずはそれっぽいxmlファイルを適当なディレクトリに用意します。
改行が続いてる部分に謎の存在Xがいます。
<?xml version='1.0' encoding='UTF-8'?>
<source>
<section>
<title><![CDATA[タイトル1]]></title>
<paragraph><![CDATA[段落1
]]></paragraph>
</section>
<section>
<title><![CDATA[タイトル2]]></title>
<paragraph><![CDATA[段落2]]></paragraph>
</section>
<section>
<title><![CDATA[タイトル3]]></title>
<paragraph><![CDATA[段落3]]></paragraph>
</section>
<section>
<title><![CDATA[タイトル4]]></title>
<paragraph><![CDATA[段落4]]></paragraph>
</section>
<section>
<title><![CDATA[タイトル5]]></title>
<paragraph><![CDATA[段落5]]></paragraph>
</section>
<section>
<title><![CDATA[タイトル6]]></title>
<paragraph><![CDATA[段落6]]></paragraph>
</section>
</source>
xmllintにかけてみる
xmllint --noout path/to/sample.xml
sample.xmlをxmllintにかけた結果
path/to/sample.xml:5: parser error : CData section not finished
段
<paragraph><![CDATA[段落1
^
path/to/sample.xml:5: parser error : PCDATA invalid Char value 8
<paragraph><![CDATA[段落1
^
path/to/sample.xml:6: parser error : PCDATA invalid Char value 11
^
path/to/sample.xml:7: parser error : PCDATA invalid Char value 6
^
path/to/sample.xml:8: parser error : PCDATA invalid Char value 4
^
path/to/sample.xml:9: parser error : PCDATA invalid Char value 2
^
path/to/sample.xml:10: parser error : PCDATA invalid Char value 1
^
path/to/sample.xml:11: parser error : PCDATA invalid Char value 16
^
path/to/sample.xml:12: parser error : PCDATA invalid Char value 12
^
path/to/sample.xml:13: parser error : PCDATA invalid Char value 3
]]></paragraph>
^
path/to/sample.xml:13: parser error : Sequence ']]>' not allowed in content
]]></paragraph>
^
path/to/sample.xml:13: parser error : internal error: detected an error in element content
]]></paragraph>
^
コードにしてみる
1行ずつ読み込む
with open(input_file, "r", encoding="utf-8") as f:
for line in f:
# lineの型も併せて確認
print(type(line))
謎の存在Xを探す
文字で探してみる
とりあえず謎の存在Xをコピペして正規表現にしてみる。
# 空白に見えるけど複数あるよ
NG_CHARS = '<(||||||||)>'
with open(input_path, "r", encoding="utf-8") as f:
for line in f:
if re.search(NG_CHARS, line):
print('invalid!')
else:
print(line)
しかし、引っかからない・・・
どうやら文字で探すのは無理なようなので、別のアプローチを考える。
10進数にして探してみる
こちらのサイトで対象の文字を10進数に変換する。
# xml内の文字列を10進数に変換
def string_to_ord(str):
ord_list = []
for char in str:
ord_list.append(ord(char))
return ord_list
謎の存在Xを削除する
NG_LIST = [1,2,3,4,6,8,11,12,15,16,17,18,19,20,29]
def remove_invalid_chars(NG_LIST, ord_list):
clean_chars = []
clean_chars = list(filter(lambda item: item not in NG_LIST,ord_list))
return clean_chars
10進数から元に戻す
謎の存在を削除したリストを文字列に戻します。
def ord_to_string(ord_list):
clean_string = ""
string_list = []
for item in ord_list:
string_list.append(chr(item))
clean_string = "".join(string_list)
return clean_string
1行ずつ別ファイルに保存していく
def create_new_file():
if os.path.exists(new_file_path):
os.remove(new_file_path)
output_file = pathlib.Path(new_file_path)
output_file.touch()
def line_writer(file_name, line):
with codecs.open(file_name, 'a', 'utf-8') as f:
f.write(line)
create_new_file()
with open(input_path, "r", encoding="utf-8") as f:
for line in f:
ord_list = string_to_ord(line)
removed_list = remove_invalid_chars(NG_LIST, ord_list)
clean_string = ord_to_string(removed_list)
line_writer(new_file_path, clean_string)
実際に動かしてみる
$ py path/to/main.py path/to/sample.xml
結果を検証
再度xmllintにかけてみる
xmllint --noout path/to/clean_sample.xml; echo $?
再度xmllintにかけた結果
# 0ならば正常終了
0
差分をチェック
後書き
すでにお気づきだと思いますが、謎の存在Xとは『制御文字』と呼ばれるものです。
xmllint
ではこれが含まれているとエラーとなります。
このエラーを回避する方法が見当たらず、今回のようにスクリプトを用意して頑張ってみるという結果になりました。
NG_LIST
にしたのは改行などの残したい記号があったりするので、ブラックリスト方式になっております。
実はVSCode
は設定を変更すれば『制御文字』を可視化できます。
ファイルサイズが小さければVSCode
で編集してしまえばいいのですが、さすがに 1GB を超えるファイルは開くには重過ぎました・・・。
スクリプトを書いてみた感想は、正規表現でサクッと探して除去するだけの簡単な処理かなと思っていましたがそんなことありませんでした。
参考記事
参考記事の執筆者の方、ありがとうございます。