JATS XMLのDTDなどはリモートにあるのですが、XML Catalogという仕掛けを使うとローカルに置いたモノを使えます。
XML Catalogは、外部実体の場所をいわば「リダイレクト」する仕組みで、リダイレクト先をローカルファイルにもできる。そうすると、JATS XML文書が指してるDTDなどが記述上はリモートにあっても、オフラインで作業できます。例えば、Visual Studio CodeのXML拡張(以下、vscode-xml)を使って、フェリーの船上などオフラインでも仕事できます。
そうするために、次の3つが必要です:
- 1度は、JATSのDTDなどをローカルにダウンロードする
- XML Catalogを用意する
- vscode-xmlがXML Catalogを参照するように設定する
先にまとめると:
- 次のページから
JATS-Publishing-1-1-MathML2-DTD.zip
をダウンロードすればよい…ようだ
https://public.nlm.nih.gov/projects/jats/publishing/1.1/ - このZIPにXML Catalogも含まれてる
-
xml:base
付き:catalog-jats-v1-1-with-base.xml
-
xml:base
なし:catalog-jats-v1-1-no-base.xml
-
- そのXML Catalogを使うように、ユーザー設定またはワークスペース設定
settings.json
に、例えば次のように設定する
"xml.catalogs": [
"catalog-jats-v1-1-with-base.xml"
]
「…ようだ」というのは、特に後述のバラエティについて、J-STAGEに説明を見つけられなかったからです。
JATSのDTDなどをローカルに持ってくる
JATSにはいくつかバージョン/リビジョンがあって、また、同じバージョンでも、表(table)や数式(MathMl)の組み合わせでバリエーションがあるとのこと1。
例えばバージョン1.1には次の4つのバリエーションがあるそうです:
- Publishing Tag Set using XHTML tables and MathML 2.0
- Publishing Tag Set using XHTML tables and MathML 3.0
- Publishing Tag Set using both XHTML tables and OASIS Exchange CALS tables with MathML 2.0
- Publishing Tag Set using both XHTML tables and OASIS Exchange CALS tables with MathML 3.0
どのバリエーションも、次のURLのページからZIPでダウンロードできます:
で、どれなんだろう?
これとは別に、後述のスクリプトでダウンロードしたものと比べてみると、J-STAGEのJATSは、Publishing Tag Set using XHTML tables and MathML 2.0が該当しているようです。
そこで、J-STAGEのJATSのDTD他は、そのページからJATS-Publishing-1-1-MathML2-DTD.zip
をダウンロードすればよいようです。
XML Catalogを用意する
前記ダウンロードしたZIPファイルにはXML Catalogも2種類含まれています:
-
xml:base
付き -
xml:base
なし
xml:base
は、HTMLのbase
要素みたいに、相対URL(というかURI)の基点となるURLを指定する属性です。JATSのDTDなどは1つのフォルダ/ディレクトリの下に階層的に配置できるようになってます。このフォルダ/ディレクトリを変えたら、xml:base
の値だけを変えればよい…という仕組みです。
vscode-xmlがXML Catalogを参照するように設定する
vscode-xmlはXML Catalogに対応しています。前記ダウンロードできたJATSのXML CatalogはDTDでバリデート(validate、妥当性検証)する方式なので、次のページのXML catalog with DTDの項に従ってVisual Studio Codeのsettings.json
に設定します:
前記ダウンロードしたxml:base
付きのXML Catalogファイルをそのまま使えばcatalog-jats-v1-1-with-base.xml
というファイル名ですから
"xml.catalogs": [
"catalog-jats-v1-1-with-base.xml"
]
ですね。
setting.json
にはユーザー設定とワークスペース設定があります23。後で変えられますし、お試しであればユーザー設定ワークスペース設定にしておくのがよいかと。
JATSのDTDなどを自分でダウンロードしてXML Catalogも作ってみる
次のPythonのコードは、指定されたJATS XML文書をバリデートしつつ、DTDなどをファイルとして所定のフォルダ構造の下に保存し、それに基づいてXML Catalogファイルを作ります。こうしてダウンロードしたモノと比較して、「Publishing Tag Set using XHTML tables and MathML 2.0が該当している」と言いました。
つまり、確認用の一時的なスクリプトです。スクリプトは、調べてすでに分かってることを前提に、サボったりフォルダ構造を作ったりしてます:
- J-STAGEのJATSのDTDなどのパスは
https://www.jstage.jst.go.jp/dtds/1.1/
までは共通 - 実体宣言は相対パスで書かれている
実体宣言はこんなふうに書かれてました。
<!ENTITY % JATS-ali-namespace.ent
PUBLIC
"-//NLM//DTD JATS ALI Namespace Module v1.1 20151215//EN"
"JATS-ali-namespace1.ent" >
一方、JATS XML文書のDOCTYPE
宣言は次のようにフルパスです。
<!DOCTYPE article
PUBLIC "-//NLM//DTD JATS (Z39.96) Journal Publishing DTD v1.1 20151215//EN"
"https://www.jstage.jst.go.jp/dtds/1.1/JATS-journalpublishing1.dtd"
>
そこまで分かってると、次のようなスクリプトでよいかと:
from lxml import etree
import argparse
import os
import sys
import requests
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument("file", help="XML file to validate.")
arg_parser.add_argument("-e", "--entity-folder", help="Folder to store remote entities.", default='entity')
arg_parser.add_argument("-c", "--catalog-file", help="Catalog file to store entity references.", default='catalog.xml')
args = arg_parser.parse_args()
print(f'start parsing {args.file}')
# sys.exit(0)
count = 0
entity_folder = args.entity_folder
catalog_file = args.catalog_file
path = 'https://www.jstage.jst.go.jp/dtds/1.1/'
path_length = len(path)
with open(catalog_file, 'w') as catalog:
catalog.write(f'<?xml version="1.0" encoding="UTF-8"?>\n')
catalog.write(f'<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">\n')
class DTDResolver(etree.Resolver):
def resolve(self, url, id, context):
global entity_folder
global count
global catalog_file
global path_length
if not os.path.exists(entity_folder):
os.makedirs(entity_folder)
if url.startswith('http'):
count += 1
res = requests.get(url)
# 読み込んだリモートのファイルを保存しておく
file_name = url[path_length:] if url.startswith(path) else os.path.basename(url)
file_name_split = file_name.split('/')
if (len(file_name_split) > 1):
entity_folder_x = os.path.join(entity_folder, *file_name_split[:-1])
if not os.path.exists(entity_folder_x):
os.makedirs(entity_folder_x)
file_name = file_name_split[-1]
else:
entity_folder_x = entity_folder
with open(f"{entity_folder_x}/{file_name}", mode='w') as f:
f.write(res.text)
# print("'%s','%s,'%s'" % (url, id, file_name))
if os.path.exists(catalog_file):
with open(catalog_file, 'a') as catalog:
catalog.write(f'<system systemId="{url}" uri="./{entity_folder_x}/{file_name}" />\n')
else:
with open(catalog_file, 'w') as catalog:
catalog.write(f'<catalog xmlns="urn:oasis:names:tc:entity:xmlns:xml:catalog">\n')
catalog.write(f'<system systemId="{url}" uri="./{entity_folder_x}/{file_name}" />\n')
# リモートのDTDを読み込んで返す
return self.resolve_string(res.text, context)
else:
f = open(url, 'r')
return self.resolve_file(f, context)
parser = etree.XMLParser(dtd_validation=True, no_network=False)
parser.resolvers.add( DTDResolver() )
try:
tree = etree.parse(args.file, parser=parser)
except etree.XMLSyntaxError as e:
print(f'etree.XMLSyntaxError: {e}')
finally:
print(f'Validation completed.')
print(f'Loaded {count} remote entities and stored under the "{entity_folder}" folder.')
with open(catalog_file, 'a') as catalog:
catalog.write(f"</catalog>\n")
print(f'Catalog file "{catalog_file}" has been created with the list of entities.')
前記ZIPに含まれているXML CatalogはPUBLIC IDとファイルを対応付けるものでした。このスクリプトはSYSTEM IDとファイルを対応付けるものです。
-
General Introduction - Journal Publishing Tag Library NISO JATS Version 1.1 (ANSI/NISO Z39.96-2015) https://xml-sch.com/jats/tag-library/ver2/1.1-J/chapter/gen-intro.html ↩
-
User and workspace settings https://code.visualstudio.com/docs/configure/settings ↩
-
VSCodeにおけるsettings.jsonの優先度について / Cursorも対応 https://qiita.com/tatsuyayamakawa/items/df7e5b1b0d7c336af124 ↩