XML 뷰어
접을 수 있는 트리와 소스를 나란히 보여 줍니다.
무언가를 붙여넣으면 여기에 문서 구조가 표시됩니다.
모든 처리는 이 탭 안에서 이루어집니다. 붙여넣은 내용은 업로드되거나 기록되거나 전송되지 않습니다. 네트워크 패널을 열어 확인하세요.
위에 문서를 붙여넣으면 두 가지 모습으로 나타납니다. 왼쪽에는 구문 강조와 줄 번호, 접기가 있는 원본이, 오른쪽에는 같은 파싱 결과로 만든 접을 수 있는 트리가 있습니다. 모든 노드는 펼친 상태로 시작하고, 요소를 클릭하면 그 아래 전부와 함께 접힙니다. 두 창은 한 번의 검사에서 나오므로, 트리와 오류 목록은 항상 같은 문서를 설명합니다.
트리 보기가 값을 하는 때는 그 문서를 내가 쓰지 않았을 때입니다. 문서라고는 PDF뿐인 공급사의 API 응답, 이전 팀이 남긴 4,000줄짜리 설정, 레코드 하나만 있어야 하는데 이백 개가 들었을지도 모르는 내보내기 파일. 궁금한 것은 대개 구문 오류의 위치가 아니라, 이게 어떤 모양이고 필요한 값이 어디에 있느냐입니다.
읽는 동안에도 검증이 돌아가며, 적격 형식 문제를 첫 건에서 멈추지 않고 줄, 열, 제안된 해결 방법과 함께 모두 보고합니다. 그리고 아무것도 업로드하지 않습니다. 파서도, 트리도, 편집기도 전부 이 탭에서 돕니다.
트리가 보여 주는 것
요소 하나가 한 줄입니다. 접두사를 포함해 쓰인 그대로의 태그 이름 뒤에 속성이 이름="값" 쌍으로 따라옵니다. 자식이 없는 요소에는 "비어 있음" 표시가 붙어서 <status/>와 내가 접어 둔 노드를 구분할 수 있습니다. 텍스트는 요소 아래 잎으로 나타나고 200자에서 잘립니다. 트리는 구조를 보기 위한 것이고, 40 KB짜리 base64 덩어리를 전부 그리면 구조가 파묻히기 때문입니다. 주석은 자리 표시 줄로 보입니다.
일부러 보여 주지 않는 것이 둘 있습니다. CDATA 섹션과 처리 명령입니다. 그것은 원본 창에서 읽으세요. 원본 창은 붙여넣은 그대로의 문서를 보여 주며 언제나 그쪽이 기준입니다. 트리는 진짜 DOM이고 요소마다 접을 수 있는 노드가 하나씩 있습니다. 그래서 접기가 즉각적이고 텍스트도 선택됩니다. 그것이 대가이기도 합니다. 요소가 수만 개인 문서에서는 XML을 파싱하는 것보다 트리를 만드는 데 시간이 더 걸립니다.
트리가 잘하는 일, 못하는 일
트리는 "모양"에 관한 질문에 스크롤보다 훨씬 빨리 답합니다. 한 단계 아래를 전부 접으면 최상위 섹션들이 한 화면에 들어옵니다. SOAP Header를 접으면 Body가 창 맨 위로 올라옵니다. 컨테이너 아래 반복되는 줄들을 보면 그 응답이 레코드 하나인지 여럿인지 알 수 있고, 이는 테스트에서는 동작하다가 운영에서 데이터를 흘리는 소비자와의 차이입니다. 못하는 일이 세 가지 있는데, 감추기보다 말해 두는 편이 유용합니다.
- 편집. 트리는 입력할 때마다 다시 만들어지므로 타이핑할 대상 자체가 없습니다. 원본 창에서 고치고 트리가 다시 만들어지는 것을 보세요.
- 비교. 트리 둘을 나란히 놓는 것은, 양쪽 서식을 먼저 정규화하는 diff보다 차이를 찾기에 못한 방법입니다.
- 추출. "모든 항목의 모든 가격"은 XPath 식이지 스크롤 노동이 아닙니다.
깊은 중첩, 그리고 접두사를 흐리게 표시하는 이유
네임스페이스가 붙은 XML이 읽기 어려운 이유는 네임스페이스가 복잡해서가 아닙니다. soap:Body나 ns1:PlaceOrder에서 접두사는 받침대입니다. 그 이름이 어느 어휘에 속하는지 알려 주고, 한 번 알고 나면 그 뒤 모든 줄에서는 잡음일 뿐입니다. 들여쓰기가 여덟 단계쯤 되면 화면의 글자 중 상당수가 접두사와 콜론입니다.
그래서 편집기는 접두사와 콜론을 지역 이름보다 옅게 그립니다. 그러면 soap:Body는 "표식이 붙은 Body"로 읽힙니다. 아무것도 감추거나 바꾸지 않으며, 텍스트는 여전히 선택되고 그대로 복사됩니다. 예외는 선언입니다. xmlns:soap에서는 접두사가 받침대가 아니라 본체이므로 원래 진하기를 유지합니다. 바인딩된 적 없는 접두사를 쓰면 추가해야 할 선언과 함께 오류로 보고하는데, 더 큰 문서에서 일부만 복사해 왔을 때 흔히 나오는 결과입니다.
값 찾기
문자열 그대로 찾으려면 원본 창에 커서를 두고 Ctrl-F(맥은 Cmd-F)를 누르세요. 편집기의 검색 패널이 위쪽에 열리며 일치 항목 강조와 바꾸기 입력란을 제공합니다. 검색 대상은 원본이므로, 트리가 잘라 낸 부분까지 포함해 모든 노드의 전체 텍스트가 대상이 됩니다.
구조에 관한 질문이라면 XPath를 쓰세요. //order[status="failed"]/@id는 스크롤 스무 번이 걸릴 답을 한 번에 냅니다. 이 사이트의 XPath 테스터는 문서에서 거둬들인 접두사로 XPath 1.0을 평가합니다. "내 식이 아무것도 안 나온다"는 제보의 대부분은 한 가지로 설명됩니다. XPath 1.0에는 기본 네임스페이스가 없습니다. 루트가 xmlns="urn:example:orders"를 선언했다면 //order는 아무것과도 일치하지 않습니다. //order는 "네임스페이스 없는 order 요소"를 뜻하기 때문입니다. 접두사를 바인딩해 //o:order로 쓰거나, //*[local-name()="order"]로 우회하세요.
코드로 문서 훑기
문서를 손으로 읽는 일은 대개 그 문서를 매일 읽을 코드를 쓰기 위한 한 걸음입니다. 아래 예제들은 문서를 훑어 구조를 출력합니다. 위 트리의 프로그램판이며, 모두 안전하게 파싱합니다. Java와 Python의 기본값이 외부 엔티티를 해석하기 때문입니다.
// Print the shape of a document: element paths with a count, which is
// usually more useful than the tree itself when you are writing a consumer.
function outline(source) {
const doc = new DOMParser().parseFromString(source, 'application/xml');
const error = doc.querySelector('parsererror');
if (error) throw new Error(error.textContent.trim());
const counts = new Map();
const walk = (el, path) => {
const here = path + '/' + el.nodeName;
counts.set(here, (counts.get(here) ?? 0) + 1);
for (const child of el.children) walk(child, here);
};
walk(doc.documentElement, '');
for (const [path, n] of [...counts].sort()) {
console.log(n.toString().padStart(6), path);
}
}
// nodeName keeps the prefix as written. For namespace-aware work use
// el.localName with el.namespaceURI, since the prefix is arbitrary and the
// URI is what identifies the vocabulary.# iterparse reads the document as a stream and lets you drop each element
# once you are done with it, so memory stays flat on files far larger than a
# browser will open.
from defusedxml.ElementTree import iterparse
from collections import Counter
counts = Counter()
stack = []
for event, el in iterparse('document.xml', events=('start', 'end')):
if event == 'start':
stack.append(el.tag)
counts['/'.join(stack)] += 1
else:
stack.pop()
el.clear() # release the subtree; this is the whole point
for path, n in sorted(counts.items()):
print(f'{n:6d} {path}')
# ElementTree reports tags as '{namespace-uri}local' rather than 'prefix:local'
# because the prefix is arbitrary. Split on '}' when you want the local name.import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.*;
public final class Outline {
public static void main(String[] args) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setNamespaceAware(true); // off by default
dbf.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setXIncludeAware(false);
Document doc = dbf.newDocumentBuilder().parse(new java.io.File(args[0]));
print(doc.getDocumentElement(), 0);
}
static void print(Node node, int depth) {
if (node.getNodeType() != Node.ELEMENT_NODE) return;
Element el = (Element) node;
StringBuilder line = new StringBuilder(" ".repeat(depth));
line.append(el.getNodeName());
NamedNodeMap attrs = el.getAttributes();
for (int i = 0; i < attrs.getLength(); i++) {
Node a = attrs.item(i);
line.append(' ').append(a.getNodeName())
.append("=\"").append(a.getNodeValue()).append('"');
}
System.out.println(line);
NodeList kids = el.getChildNodes();
for (int i = 0; i < kids.getLength(); i++) print(kids.item(i), depth + 1);
}
}using System.Xml;
using System.Xml.Linq;
// XDocument.Load uses a reader with DtdProcessing.Prohibit by default, so
// external entities are never fetched. Construct the reader yourself when
// you want that guarantee stated in the code rather than in the docs.
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersInDocument = 20L * 1024 * 1024,
};
using var reader = XmlReader.Create("document.xml", settings);
var doc = XDocument.Load(reader);
// One line per distinct element path, with how often it occurs.
var paths = doc.Descendants()
.GroupBy(e => string.Join(
"/",
e.AncestorsAndSelf().Reverse().Select(a => a.Name.LocalName)))
.OrderBy(g => g.Key);
foreach (var group in paths)
{
Console.WriteLine(group.Count().ToString().PadLeft(6) + " " + group.Key);
}# xmllint has an interactive tree browser almost nobody knows about. It gives
# you ls, cd, cat, du and pwd over the document, with XPath as the path
# syntax: the terminal equivalent of the tree on this page.
xmllint --nonet --shell document.xml
# / > du show the subtree shape
# / > cd //order[1] navigate by XPath
# / > ls list children of the current node
# / > cat status print a node
# / > exit
# Non-interactive: tag counts, a rough outline of an unfamiliar document.
xmllint --nonet --format document.xml |
grep -o '<[a-zA-Z_][^ />]*' |
sort | uniq -c | sort -rn | head -30
# One value, straight out:
xmllint --nonet --xpath 'string(//order[1]/status)' document.xml셸 예제의 grep 개요는 파싱이 아니라 세기 요령입니다. 주석이나 CDATA 섹션 안에 나온 태그 이름도 태연히 세어 버립니다. 다른 예제들은 모두 먼저 트리를 만듭니다. 그것이 대략적인 답과 정확한 답의 차이입니다.
자주 묻는 질문
보기 위해 제 XML이 업로드되나요?
아닙니다. 파서와 트리와 편집기는 이 탭에서 도는 JavaScript입니다. 서버 쪽 구성요소도, 편집기에 접근하는 분석 도구도, 서드파티 스크립트도 없습니다. 네트워크 탭을 열고 문서를 붙여넣어 보세요. 페이지 자체 자원이 한 번 로드된 뒤로는 조용합니다.
이 확인이 여기서 중요한 이유는, "들여다보기"야말로 남의 데이터를 가지고 하는 일이기 때문입니다. 뷰어에 붙여넣는 문서는 대개 실제 API 응답이나 운영 내보내기이고, 이름과 계좌번호, 헤더의 토큰이 그대로 들어 있습니다. 이 검색어에서 상위에 나오는 도구 여럿이 그것을 서버로 보내고, 한 곳은 저장한 문서를 기본적으로 공개로 둡니다.
트리 보기는 실제로 무엇에 쓰나요?
구조에 관한 질문에 답하는 데 씁니다. 어떤 요소가 반복되는지, 중첩이 얼마나 깊은지, 값이 어느 가지에 있는지, 이 응답에 선택적 요소가 들어 있는지. 들여쓴 텍스트에서는 알아보기 어렵고 트리에서는 한눈에 보입니다.
본전을 뽑는 방식은 이렇습니다. 루트 한 단계 아래를 전부 접고, 섹션 이름을 읽고, 관심 있는 것만 펼치고, 나머지 3,000줄은 무시합니다. 반대로 질문이 구문에 관한 것이라면, 여러분에게 필요한 절반은 원본 창과 오류 목록입니다.
트리에서 XML을 편집할 수 있나요?
없습니다. 트리는 입력에 맞춰 다시 만들어지므로 편집해 둘 대상이 남지 않습니다. 변경은 왼쪽 원본 창에서 하세요.
이는 의도한 제약입니다. 트리 편집기는 변경이 있을 때마다 공백, 혼합 콘텐츠, 주석 위치, 속성 순서를 어떻게 할지 정해야 합니다. 안전한 답을 고르면 쓰기 불편해지고, 편한 답을 고르면 건드리지도 않은 부분을 조용히 다시 써 버립니다.
특정 값을 어떻게 찾나요?
문자열 그대로라면 원본 창에 커서를 두고 Ctrl-F(맥은 Cmd-F)를 누르세요. 검색 패널이 위쪽에 열리며 일치 항목을 강조하고, 트리가 보여 주는 잘린 텍스트가 아니라 전체 텍스트를 검색합니다.
구조에 관한 것이라면 XPath를 쓰세요. //order[status="failed"]/@id면 한 번에 답이 나옵니다. 요소가 분명히 있는데 식이 아무것도 반환하지 않는다면 기본 네임스페이스를 의심하세요. XPath 1.0에는 기본 네임스페이스가 없어서, 접두사를 바인딩해 //o:order라고 쓰기 전까지 //order는 urn:example:orders의 요소와 일치하지 않습니다.
브라우저가 파일을 보여 주지 않고 "이 페이지에 다음 오류가 있습니다"라고 하는 이유는?
크롬이나 파이어폭스에 내장된 XML 뷰어가 그 문서는 적격 형식이 아니라고 알려 주는 것입니다. 둘 다 첫 오류에서 멈추고 거기까지만 그립니다. 메시지는 libxml2나 Expat에서 나오며 파서를 만든 사람의 언어로 쓰여 있습니다. "Opening and ending tag mismatch", "Document is empty" 같은 식이죠.
여기에 문서를 붙여넣으면 모든 문제가 줄, 열, 제안과 함께 한 번에 나오고, 파싱된 부분에 대해서는 트리도 함께 나옵니다. 흔한 원인은 그대로 쓰인 앰퍼샌드, 중간에 끊긴 다운로드, XML 선언 앞의 바이트 순서 표식, 그리고 XML 콘텐츠 타입으로 전달된 HTML 오류 페이지입니다.
얼마나 큰 파일을 열 수 있나요?
20 MB까지이며, 실제로는 파싱보다 트리가 먼저 무거워집니다. 1 MB 문서를 훑는 데 약 110밀리초가 걸리지만, 요소마다 접을 수 있는 노드를 하나씩 만드는 작업은 수만 개에 이르면 그보다 오래 걸립니다.
이 상한이 있는 이유는 아무것도 업로드하지 않기 때문입니다. 문서와 그 트리 모두 이 탭의 메모리에 올라갑니다. 더 큰 파일은 로컬에서 스트리밍으로 처리하는 도구를 쓰세요. 살펴보려면 xmllint --shell, 뽑아내려면 파이썬의 iterparse가 있습니다.