XML 구문 검사기
모든 구문 오류를 한 번에. 줄, 열, 해결 방법까지.
모든 처리는 이 탭 안에서 이루어집니다. 붙여넣은 내용은 업로드되거나 기록되거나 전송되지 않습니다. 네트워크 패널을 열어 확인하세요.
위에 문서를 붙여넣거나 편집기에 파일을 끌어다 놓으면, 입력하는 동안 XML 1.0의 적격 형식 규칙에 비추어 검사합니다. 모든 위반 사항이 줄, 열, 그리고 대신 무엇을 써야 하는지와 함께 나열되고, 결과를 클릭하면 커서가 문제의 문자로 이동합니다.
이것은 다른 어떤 검사보다 먼저 통과해야 하는 검사입니다. 스키마 검사기도, XSLT 프로세서도, SOAP 스택도, 브라우저도 구문이 깨진 문서는 아예 들여다보지 않습니다. 그러니 빌드에서 나온 "premature end of data in tag"는 구문 문제이고, XSD를 아무리 들여다봐도 설명되지 않습니다.
여기서 다른 점은 스캐너가 첫 실패에서 멈추지 않는다는 것입니다. 복구해서 계속 읽으므로, 한 번의 검사로 4행의 따옴표 없는 속성, 12행의 그대로 쓰인 앰퍼샌드, 끝에서 닫히지 않은 채 남은 태그를 모두 보고합니다. 37가지 오류 분류마다 고유한 문구와 고유한 해결 방법이 있고, 모든 것을 하나의 "Syntax Error"로 뭉뚱그리지 않습니다. 업로드도 없습니다. 네트워크 패널을 열어 두고 계속 비어 있는지 보세요.
"적격 형식"이 실제로 뜻하는 것
XML 1.0은 적격 형식 제약을 열두 가지로 명시하고 나머지는 문법에 맡깁니다. 문서가 통과하거나 실패하는 검사로 추리면 다음 목록이 되고, 여기서는 그 한 줄 한 줄을 모두 적용합니다.
- 루트 요소는 정확히 하나. 그 바깥에는 주석, 처리 명령, 공백 외에는 아무것도 올 수 없습니다.
- 모든 여는 태그는 이름이 정확히 일치하는 닫는 태그로 닫혀야 합니다. 이름은 대소문자를 구분하므로 </Note>는 <note>를 닫지 않습니다.
- 요소는 겹치지 않고 중첩되어야 합니다. <b><i>x</b></i>는 XML이 아닙니다. 여러분의 HTML 파서가 아무리 너그러웠더라도 마찬가지입니다.
- 속성 값은 따옴표로 묶고, 한 요소에 같은 속성을 두 번 쓰지 않습니다.
- 텍스트에도 속성 값에도 리터럴 <를 두지 않고, 참조 밖에 맨 &를 두지 않습니다. 텍스트 안의 >는 괜찮지만 ]]> 라는 연속은 안 됩니다.
- 미리 정의된 엔티티는 amp, lt, gt, quot, apos 다섯 개뿐입니다. DTD가 더 선언한 경우는 예외입니다. 는 HTML의 것이고 이 다섯에 들지 않습니다.
- XML이 허용하는 집합 밖의 문자를 쓰지 않고, --를 포함한 주석을 쓰지 않으며, 선언을 둔다면 파일 맨 앞에 version, encoding, standalone 순으로 적습니다.
- 모든 네임스페이스 접두사는 유효 범위 안의 xmlns 선언으로 바인딩되어야 합니다. 이 규칙은 XML 1.0이 아니라 Namespaces in XML에서 오며, 무료 검사기들이 가장 자주 건너뛰는 항목이기도 합니다. 유력한 경쟁 서비스 하나는 파일 어디에도 바인딩이 없는데 <ns:root>를 유효하다고 판정합니다.
모든 오류를 한 번에, 그리고 다른 도구가 하나만 주는 이유
명세는 치명적 오류 이후 정상 처리를 계속해서는 안 된다고 프로세서에 지시합니다. DOMParser도, Expat도, .NET도 첫 문제를 보고하고 멈추는 이유가 그것입니다. 이는 게으름이 아니라 올바른 동작이며, 동시에 큰 문서를 고치는 데 오류 하나마다 한 번씩 왕복해야 하는 이유이기도 합니다.
여기서의 복구는 의도한 것입니다. 닫는 태그가 맞지 않으면, 스캐너는 열린 요소 스택 위쪽에서 그 이름을 찾습니다. 찾았다면 진짜 잘못은 그 사이에 열린 채 남은 요소들이므로, 각각을 자기 여는 줄에서 보고하고 닫는 태그에 책임을 씌우지 않습니다. 이름 안의 엉뚱한 문자는 토큰 전체를 삼키므로, amount$="10"은 등호와 따옴표에 대한 다섯 건이 아니라 달러 기호에 대한 한 건이 됩니다. 한 가지 주의: 앞쪽의 구조적 실수는 그 뒤의 모든 것을 잘못된 것처럼 보이게 합니다. 처음 두세 개를 고치고 다시 검사하세요. 보고는 200건에서 멈추며, 그 사실을 함께 알려 줍니다.
실제로 나타나는 실패들
현실의 구문 오류는 좀처럼 기이하지 않습니다. 시간을 잡아먹는 이유는, 파서가 증상을 지목하는 동안 원인은 다른 곳에 있거나 눈에 보이지 않기 때문입니다.
- 선언 앞의 UTF-8 바이트 순서 표식. 합법이고, 눈에 보이지 않으며, "Content is not allowed in prolog"의 전형적인 원인입니다. 여기서는 정체불명의 내용이 아니라 그 자체로 보고합니다.
- <?xml encoding="UTF-8" version="1.0"?> 처럼 쓰인 선언, 또는 바이트는 UTF-8인데 windows-1252라고 주장하는 선언. 뒤쪽은 바이트 단위 파서가 무엇을 읽게 될지에 대한 경고로 냅니다. 이 페이지에 도착했을 때 파일은 이미 디코딩된 상태이기 때문입니다.
- , — 및 오십 개쯤 되는 다른 HTML 엔티티. 일반적인 "정의되지 않은 엔티티"가 아니라 HTML의 것으로 지목하고, 대신 쓸 숫자 참조도 알려 줍니다.
- 닫는 루트 태그 뒤에 붙은 PHP 알림이나 스택 트레이스. 그 메시지가 원시 HTTP 본문을 보라고 안내하는 이유입니다.
- 데이터베이스 열에서 나온 제어 문자. XML 1.0은 CDATA 안을 포함해 어디서든 이를 금지합니다.
- 바인딩되지 않은 soap:, xsi:, xlink: 접두사. 더 큰 문서에서 일부만 복사하고 xmlns 선언은 원래 루트에 두고 온 결과입니다.
구문 검사가 멈추는 지점
이 페이지가 답하는 질문은 하나뿐입니다. 구문이 적법한가. <invoice>가 <line>을 담아도 되는지, 필수 요소가 빠지지 않았는지는 절대 묻지 않습니다. 그건 스키마가 필요하므로 XSD 검사기와 DTD 검사기의 몫입니다. 어떤 시스템이 여러분의 문서를 "invalid"라고 했다면 그건 스키마 검사를 돌린 것이고, 여기서 초록불이 떠도 그 이유를 설명해 주지 않습니다.
밝혀 둘 제약이 둘 있습니다. 내부 DTD 서브셋은 엔티티 선언만을 위해 읽으므로, 그 안의 요소 선언과 속성 목록 선언은 지나칠 뿐 강제하지 않습니다. 외부 DTD는 보고만 하고 절대 가져오지 않습니다. 크기는 2천만 자, 중첩은 512단계, 엔티티 확장은 1천만으로 제한됩니다. 모든 것이 이 탭에서 돌기 때문입니다.
코드로 XML 구문 검사하기
XML을 가장 많이 다루는 언어들에서의 같은 검사를, 외부 엔티티와 엔티티 확장에 안전한 형태로 보여 줍니다. 오류를 둘 이상 보고하는 것이 얼마나 적은지 보세요. 그것은 라이브러리가 아니라 명세가 그렇게 정한 결과입니다.
// DOMParser never throws. Given broken input it returns a document whose
// root is <parsererror>, which is why so much code silently accepts XML
// that no other parser would take.
function checkXml(source) {
const doc = new DOMParser().parseFromString(source, 'application/xml');
const error = doc.querySelector('parsererror');
if (!error) return { wellFormed: true, message: null };
// The text is browser-specific. Firefox includes a line and column,
// Chrome's wording differs, and neither exposes them as fields.
return { wellFormed: false, message: error.textContent.trim() };
}
// Browsers do not resolve external entities, so XXE is not reachable here.
// Internal entity expansion is, so cap the input length before parsing:
// if (source.length > 20_000_000) throw new Error('too large');# lxml is the only common Python parser that keeps going after a syntax
# error and hands back the whole list rather than the first entry.
from lxml import etree
parser = etree.XMLParser(
recover=True, # keep parsing so error_log fills up
resolve_entities=False, # do not expand entities
no_network=True, # never fetch an external DTD
load_dtd=False,
huge_tree=False, # keep libxml2's depth and expansion limits
)
etree.fromstring(source.encode('utf-8'), parser)
for entry in parser.error_log:
print(f"line {entry.line}, column {entry.column}: {entry.message}")
if not parser.error_log:
print('well-formed')
# Without recover=True, fromstring raises etree.XMLSyntaxError and
# e.position gives a (line, column) tuple for the first failure only.
# For untrusted input with the standard library, use defusedxml.import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Drop the next line only if your documents legitimately carry a DOCTYPE.
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
var reader = factory.newSAXParser().getXMLReader();
reader.setErrorHandler(new DefaultHandler() {
@Override public void warning(SAXParseException e) { print("warning", e); }
@Override public void error(SAXParseException e) { print("error", e); }
@Override public void fatalError(SAXParseException e) throws SAXParseException {
print("fatal", e);
throw e; // the specification requires processing to stop here
}
private void print(String kind, SAXParseException e) {
System.err.printf("%s at line %d, column %d: %s%n",
kind, e.getLineNumber(), e.getColumnNumber(), e.getMessage());
}
});
reader.parse(new InputSource(new java.io.StringReader(source)));using System.Xml;
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit, // no DTD, so no entity expansion
XmlResolver = null, // never fetch anything
MaxCharactersFromEntities = 1024 * 1024,
MaxCharactersInDocument = 20L * 1024 * 1024,
ConformanceLevel = ConformanceLevel.Document,
};
try
{
using var reader = XmlReader.Create(new StringReader(source), settings);
while (reader.Read()) { } // pull every node; syntax errors surface here
Console.WriteLine("well-formed");
}
catch (XmlException e)
{
// LinePosition is the column, 1-based. Reading stops at this point.
Console.Error.WriteLine(
$"line {e.LineNumber}, column {e.LinePosition}: {e.Message}");
}<?php
// libxml recovers internally, so libxml_get_errors() is the closest a PHP
// script gets to a full list rather than a first failure.
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadXML($source, LIBXML_NONET); // NONET blocks external fetches
$errors = libxml_get_errors();
libxml_clear_errors();
foreach ($errors as $e) {
$kind = $e->level === LIBXML_ERR_FATAL ? 'fatal' : 'error';
fprintf(
STDERR,
"%s at line %d, column %d: %s\n",
$kind,
$e->line,
$e->column,
trim($e->message)
);
}
exit($errors ? 1 : 0);# xmllint ships with libxml2 and is already installed on most machines.
# --nonet stops it fetching a DTD the document points at.
xmllint --noout --nonet document.xml
# --recover keeps parsing after a failure, so you see more than the first.
xmllint --noout --nonet --recover document.xml
# Check a whole tree, one file at a time:
find . -name '*.xml' -print0 | xargs -0 -n1 xmllint --noout --nonet
# Exit status: 0 well-formed, 1 unclassified, 3 well-formedness error,
# 4 validation error against a DTD or schema.모든 예제가 무언가를 끄는데, 그것이 형식이 아니라 본질입니다. 외부 엔티티 해석은 Java에서 기본으로 켜져 있고, Python 표준 라이브러리는 확장 한도를 전혀 두지 않습니다. 이 플래그들이 바로 구문 검사와 "여러분의 서버를 겨눈 파일 읽기 수단"을 가르는 지점입니다.
자주 묻는 질문
아무것도 설치하지 않고 XML 구문을 검사하려면?
이 페이지 위쪽 편집기에 문서를 붙여넣으세요. 입력하는 동안 검사가 돌고, 누를 버튼도 만들 계정도 없습니다. .xml 파일을 끌어다 놓아도 됩니다. 파일은 브라우저의 File API로 읽히며 전송되지 않습니다.
터미널에서는 xmllint가 libxml2의 일부로 macOS와 대부분의 리눅스 배포판에 이미 들어 있습니다. xmllint --noout --nonet yourfile.xml 은 구문이 적법하면 아무것도 출력하지 않습니다. 첫 오류에서 멈춘다는 점이 실질적인 차이입니다.
여기서 검사하면 제 XML이 업로드되나요?
아닙니다. 스캐너는 이 탭의 Web Worker에서 도는 JavaScript입니다. 문서를 보낼 엔드포인트가 없고, 편집기에 접근하는 분석 도구나 오류 보고 스크립트도 없습니다.
이것은 약속이 아니라 확인할 수 있는 사실입니다. 개발자 도구를 열고 네트워크 탭으로 옮긴 뒤 문서를 붙여넣고 보세요. 이 페이지의 자원이 한 번 로드되고 그 뒤로는 아무것도 이어지지 않습니다. 이 점이 중요한 이유는, 사람들이 구문 검사기에 붙여넣는 문서가 베어러 토큰이 든 SOAP 엔벨로프, SAML 어서션, 접속 문자열이 든 설정 파일이기 때문입니다.
구문 검사기와 XML 검사기(validator)의 차이는?
두 말이 섞여 쓰이는 데서 혼란이 시작됩니다. 구문 검사는 문서가 XML 자체의 규칙을 지키는지 묻습니다. 태그가 닫혔는지, 중첩이 올바른지, 루트가 하나인지, 속성이 따옴표로 묶였는지, 앰퍼샌드가 이스케이프되었는지. 문서 말고는 아무것도 필요하지 않습니다.
명세가 말하는 의미의 검증은 문서가 스키마, 즉 XSD나 DTD에 부합하는지 묻습니다. 어떤 요소가 어디에 올 수 있는지를 적은 두 번째 파일이 필요하죠. 구문이 깨끗해지기 전에는 실행할 수 없으므로, 미들웨어가 내놓는 "invalid"는 때로는 구문 오류를, 때로는 스키마 위반을 뜻합니다. 이 페이지는 앞의 검사를 수행하고 그 사실을 명시합니다. 여기 XSD·DTD 검사기가 뒤의 검사를 수행하며, 역시 아무것도 업로드하지 않습니다.
제 편집기는 하나만 알려 주는데 여기서는 왜 다섯 개가 나오나요?
XML 명세가 적합한 프로세서는 치명적 오류 이후 멈추라고 지시하고, 여러분의 편집기 뒤에 있는 파서는 그대로 따르기 때문입니다. 그 규칙에는 근거가 있습니다. 태그가 닫히지 않은 순간부터 파서는 문서의 나머지가 무엇을 뜻하려 했는지 정말로 알 수 없습니다.
이 스캐너는 대신 복구합니다. 태그 경계에서 다시 동기화하고, 망가진 이름 토큰은 통째로 삼키며, 그것을 드러낸 닫는 태그보다 닫히지 않은 여는 태그 쪽을 탓합니다. 목록이 길다면 대개 맨 앞의 구조적 실수 하나가 나머지를 만들어 내므로, 앞쪽 오류를 믿을 만한 것으로 보고 고친 뒤 다시 검사하세요.
XML 태그 이름은 대소문자를 구분하나요?
완전히 구분합니다. <Note>와 <note>는 다른 요소이고, </Note>는 <note>를 닫지 않습니다. HTML에서 넘어온 사람이 자주 걸리는 지점인데, HTML에서는 파서가 태그 이름을 소문자로 접습니다. 손으로 쓴 XML에서 태그 불일치 오류가 나는 가장 흔한 원인이기도 합니다.
속성 이름과 네임스페이스 접두사에도 똑같이 적용됩니다. xmlns:Soap와 xmlns:soap는 다른 접두사입니다. 불일치가 대소문자 차이뿐일 때는 일반적인 불일치가 아니라 그 점을 명시해 알려 줍니다. 널리 베껴지는 "XML 구문 규칙" 요약 몇몇은 XML 태그가 대소문자를 구분하지 않는다고 적고 있는데, 그것은 틀렸고 그대로 따르면 실제 파서가 전부 거부하는 문서가 나옵니다.
검사할 수 있는 파일 크기는 얼마까지인가요?
2천만 자, ASCII 기준 대략 20 MB입니다. 10 MB 문서는 1초 조금 넘게, 1 MB는 약 110밀리초에 검사되며, Web Worker에서 돌기 때문에 편집기는 계속 반응합니다.
그보다 큰 파일은 로컬에서 스트리밍 파서를 쓰세요. xmllint, 파이썬의 iterparse, 또는 어떤 SAX 파서든 트리 전체를 만들지 않고 적격 형식을 검사합니다.