XML을 YAML로 변환
YAML로 변환하고, 위험한 값은 따옴표로 감쌉니다.
모든 처리는 이 탭 안에서 이루어집니다. 붙여넣은 내용은 업로드되거나 기록되거나 전송되지 않습니다. 네트워크 패널을 열어 확인하세요.
위에 XML을 붙여넣으면 옆에 YAML이 나타납니다. 문서는 정형식인지 검사되고, 나무로 옮겨지고, 그다음 어떤 값에 인용부호가 필요한지를 정하는 일이 주업인 출력기가 써 냅니다. 아무것도 업로드되지 않습니다. 스캐너와 나무로 옮기는 일, 출력기가 모두 이 탭에서 돕니다.
이것을 바라게 되는 흔한 이유는, 설정 파일이나 쿠버네티스 매니페스트, CI 파이프라인, 앤서블 인벤토리가 지금 XML에 들어 있는 데이터를 필요로 한다는 것입니다. 출력은 기계가 글자 그대로 읽는 파일로 들어가며, 그래서 보기보다 인용부호가 더 중요합니다.
YAML은 다정한 형식처럼 보이면서, 여러분의 데이터를 조용히 바꿀 가능성이 가장 높은 형식입니다. 인용부호 없는 국가 코드 NO는 이 생태계 대부분에서 논릿값 false가 됩니다. 우편번호 01730은 1730이 됩니다. 버전 1.10은 1.1이 됩니다. 이 출력기는 그대로 두면 잘못 읽힐 값에 인용부호를 붙이고, 이 페이지는 어떤 값에 왜 붙였는지를 정확히 말해 줍니다.
옮기는 방식은 XML에서 JSON으로 옮기는 방식과 같다
YAML 1.2는 JSON의 상위집합으로 설계되었으므로, 여기에 별도의 나무는 없습니다. XML은 「XML에서 JSON으로」 페이지가 만드는 것과 같은 구조로 변환되고, 다른 직렬화기가 그것을 써 냅니다. 그 페이지의 옮기기 결정은 모두 그대로 적용됩니다. 속성은 접두사가 붙은 키가 되고, 속성이나 자식과 한 요소를 나눠 쓰는 텍스트는 텍스트 키 아래로 들어가고, 두 번 나타나는 요소는 수열이 되며, 주석은 버려집니다.
한 가지는 더 나빠집니다. JSON이라면 받는 쪽이 적어도 대괄호를 봅니다. YAML에서는 한 건과 두 건의 차이가 들여쓴 스칼라 대 붙임표 목록의 차이이고, 그것을 diff에서 알아보는 사람은 없습니다. 개념상 목록인 것에는 「항상 배열」 칸을 쓰세요. 한 건짜리 문서와 쉰 건짜리 문서가 같은 모양을 내놓습니다.
<order id="00042">
<total currency="GBP">19.90</total>
<line sku="0071">Widget</line>
<line sku="0072">Gasket</line>
<country>NO</country>
</order>
order:
attr_id: '00042'
total:
attr_currency: GBP
text: '19.90'
line:
- attr_sku: '0071'
text: Widget
- attr_sku: '0072'
text: Gasket
country: 'NO'노르웨이 문제, 그리고 그것이 덮는 정확한 목록
YAML 1.1은 논리형을 열거로 정의하고, 그 열거는 누구의 예상보다도 넓습니다. 공개된 형 문서는 그대로 이렇게 적습니다. y, Y, yes, Yes, YES, n, N, no, No, NO, true, True, TRUE, false, False, FALSE, on, On, ON, off, Off, OFF. 이 가운데 무엇이든 인용부호가 없으면 논릿값으로 읽힙니다.
그 결과에는 이름이 붙어 있습니다. ISO 국가 코드 데이터는 노르웨이에 NO를 주고, 파서는 애플리케이션에 false를 건넵니다. 같은 목록은 스프레드시트에서 내보낸 예/아니오 열도, 텍스트로 쓰려던 on이나 off라는 스위치도 삼킵니다. YAML 1.2는 핵심 스키마를 true와 false만으로 좁혔지만, PyYAML과 루비의 Psych, 앤서블, 쿠버네티스 주변 도구의 상당수는 아직 1.1 집합을 해석하므로, 전부 살아 있다고 여기세요.
출력기는 그 목록과 정확히 맞는 스칼라를 한 글자 형태까지 포함해 홑인용부호로 감싸고, 거기에 null, Null, NULL과 물결표도 함께 감쌉니다. 대소문자 구분에 주의하세요. yES와 nO는 1.1 목록에 없어 인용부호가 붙지 않습니다. 적합한 파서도 그것들을 논릿값으로 읽지 않기 때문입니다.
또 무엇에 인용부호가 붙고, 무엇이 빠져나가는가
논릿값 집합은 유명한 경우이지 흔한 경우는 아닙니다. 망가지는 값은 대부분 애초에 숫자가 아니었던 숫자입니다. YAML은 JSON이 결코 하지 않는 방식으로, 맨 스칼라의 적힌 꼴에서 형을 추론하기 때문입니다. 스칼라에 홑인용부호가 붙는 것은, 논릿값이나 널 집합과 맞을 때, JSON의 숫자 문법과 맞을 때(42, 19.90, 1.10을 포함합니다), 앞에 0이 오고 뒤에 숫자가 더 올 때, 비어 있을 때, 붙임표나 우물 정자 같은 YAML 지시 문자로 시작할 때, 그리고 앞이나 뒤에 공백이 있을 때입니다.
여러 줄 텍스트에는 인용부호가 붙지 않습니다. 수직선과 잘라내기 지시로 시작하는 리터럴 블록 스칼라가 됩니다. 접는 방식이 아니라 리터럴을 고른 것은 일부러입니다. 접는 블록은 홀로 있는 줄바꿈을 공백으로 흘려 넣어 안에 든 코드와 주소를 망가뜨립니다. 잘라내기 지시는 블록이 그대로 두면 붙이는 마지막 줄바꿈을 없앱니다.
그래도 인용부호 없이 출력기를 떠나, 아래쪽에서 형이 바뀔 수 있는 값들이 있습니다. 뭉개지 않고 적어 둡니다. 맨 스칼라를 쓰는 출력기 가운데 YAML의 형 추론을 풀어낸 것은 없기 때문입니다.
- 60진 숫자. YAML 1.1은 22:22을 60진 정수로 읽으므로, 지속 시간이 PyYAML에서 1342가 됩니다. js-yaml 같은 1.2 파서는 문자열을 돌려주므로, 이것은 어느 쪽이 파일을 읽느냐에 달렸습니다.
- 16진 표기. 0x1F는 YAML 1.1에서도 1.2 핵심 스키마에서도 31로 읽히므로, 16진 색 코드에는 인용부호가 필요합니다.
- 날짜. 2024-01-05는 YAML의 타임스탬프 형과 맞으므로, js-yaml과 PyYAML 둘 다 문자열이 아니라 날짜 객체를 건넵니다.
- 첫 줄이 뒤의 줄들보다 더 깊이 들여쓰인 블록 스칼라. CDATA 절이 앞쪽 공백을 보존할 때 일어납니다. YAML의 해법은 수직선 뒤에 명시적인 들여쓰기 지시를 두는 것인데, 이 출력기는 그것을 쓰지 않습니다.
변환하기 전에 속성 접두사와 텍스트 키를 정하세요
해 둘 값어치가 있는 준비는 이 하나뿐입니다. 기본값은 JSON을 위해 고른 것이고 거기서는 안전하지만, YAML은 값보다 키에 더 엄격한 문법을 갖습니다.
기본 속성 접두사는 @_이고 기본 텍스트 키는 #text입니다. YAML에서 @는 예약된 지시 문자라 맨 스칼라가 그것으로 시작할 수 없으므로, @_id라는 키는 문서를 파싱 불가로 만듭니다. js-yaml은 「bad indentation of a mapping entry」라고, PyYAML은 어떤 토큰도 시작할 수 없는 문자라고 보고합니다. 앞에 오는 #은 더 고약합니다. 실패하지 않기 때문입니다. #text: 19.90이라고 적힌 줄은 주석이므로, 파일은 읽히고 값은 그냥 거기에 없습니다.
두 칸 모두 편집기 위의 조절 줄에 있습니다. 접두사를 attr_처럼 수수한 것으로, 텍스트 키를 text로 두면 출력의 모든 키가 평범한 YAML 이름이 됩니다. soap:Body처럼 다른 이유로 인용부호가 필요한 키는 자동으로 감싸집니다. 맨 키에 콜론은 적법하지 않기 때문입니다.
코드로 하기
두 단계입니다. XML을 안전하게 파싱하고, 그다음 인용부호를 제대로 붙이라고 일러 둔 덤퍼로 직렬화합니다. XML 쪽에는 늘 쓰던 엔티티 플래그가 필요합니다. 자바와 .NET의 기본값이 DOCTYPE을 해석하기 때문입니다. YAML 쪽은 주의가 필요합니다. 덤퍼마다 인용부호를 붙이는 적극성이 다르기 때문입니다.
import { XMLParser } from 'fast-xml-parser';
import yaml from 'js-yaml';
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: 'attr_', // not @_: YAML reserves a leading @
textNodeName: 'text', // not #text: a leading # is a comment
parseTagValue: false, // keep values as strings
parseAttributeValue: false,
processEntities: false, // do not expand DOCTYPE-declared entities
isArray: (name) => ['line', 'item', 'entry'].includes(name),
});
const out = yaml.dump(parser.parse(xmlSource), {
lineWidth: -1, // never fold long lines; folding rewrites your data
noRefs: true, // never emit anchors and aliases
quotingType: "'",
sortKeys: false,
});
// js-yaml's dumper is conservative: it quotes NO, 01730, 1.10, 22:22 and
// 0x1F on its own, and quotes keys that begin with @ or #. Add
// forceQuotes: true if you want every string quoted regardless.import xmltodict
import yaml
doc = xmltodict.parse(
xml_source,
disable_entities=True, # blocks the expat entity attacks
attr_prefix='attr_',
cdata_key='text',
force_list=('line', 'item', 'entry'),
)
print(yaml.safe_dump(
doc,
default_flow_style=False,
allow_unicode=True,
sort_keys=False,
width=10 ** 9, # effectively disable line folding
))
# PyYAML implements the YAML 1.1 resolver, so its dumper knows that NO,
# 01730 and 1.10 would load back as a bool, an int and a float, and quotes
# them. Use safe_dump, never dump: the full dumper emits Python-specific
# tags that only yaml.unsafe_load can read back.import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.dataformat.xml.XmlFactory;
import com.fasterxml.jackson.dataformat.xml.XmlMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import javax.xml.stream.XMLInputFactory;
XMLInputFactory input = XMLInputFactory.newFactory();
input.setProperty(XMLInputFactory.SUPPORT_DTD, false);
input.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
JsonNode tree = new XmlMapper(new XmlFactory(input)).readTree(xmlSource);
YAMLMapper yaml = YAMLMapper.builder()
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER)
.disable(YAMLGenerator.Feature.MINIMIZE_QUOTES) // off is the safe state
.enable(YAMLGenerator.Feature.LITERAL_BLOCK_STYLE)
.build();
String out = yaml.writeValueAsString(tree);
// MINIMIZE_QUOTES is the setting to leave alone. It is off by default, and
// turning it on is how a value of NO ends up unquoted in a Jackson-generated
// file that a Python service then reads as false.using System.Xml;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using YamlDotNet.Core;
using YamlDotNet.Serialization;
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Prohibit,
XmlResolver = null,
MaxCharactersFromEntities = 1024 * 1024,
};
using var reader = XmlReader.Create(new StringReader(xmlSource), settings);
var document = new XmlDocument { XmlResolver = null };
document.Load(reader);
string json = JsonConvert.SerializeXmlNode(document);
object? tree = JsonConvert.DeserializeObject<JObject>(json)?.ToObject<object>();
var serialiser = new SerializerBuilder()
.WithDefaultScalarStyle(ScalarStyle.SingleQuoted) // quote everything
.Build();
Console.Write(serialiser.Serialize(tree));
// WithDefaultScalarStyle is blunt: every scalar comes out quoted, including
// the ones that did not need it. That is the right trade for generated data.
// Drop it only if you are hand-checking the output.# yq v4 (Mike Farah) converts directly and quotes ambiguous scalars.
yq -p=xml -o=yaml '.' document.xml
# Match the key convention used on this page:
yq -p=xml -o=yaml \
--xml-attribute-prefix='attr_' \
--xml-content-name='text' \
'.' document.xml > out.yaml
# Then load it back with the parser that will actually consume it. This is
# the only check that proves nothing changed type on the way through:
python -c "import yaml; print(yaml.safe_load(open('out.yaml'))['order']['country'])"
# expect: NO not: False이 페이지가 다루는 실패는 조용합니다. 인용부호 없는 NO가 든 YAML 파일은 깔끔하게 파싱되고, 깔끔하게 검증되고, 깔끔하게 배포됩니다. 다만 그 나라는 그때부터 false입니다. 이를 잡아내는 확인은, 생성된 파일을 소비자가 쓰는 것과 같은 라이브러리로 다시 읽어 들여, 까다롭다고 알려진 값을 견주어 보는 것이며, diff를 읽는 것이 아닙니다.
자주 묻는 질문
YAML로 변환할 때 제 XML이 업로드되나요?
아닙니다. XML 스캐너, 나무로 옮기는 일, YAML 출력기는 모두 이 탭의 자바스크립트이고, 보낼 엔드포인트도 없습니다. 개발자 도구의 네트워크 탭을 열고 문서를 붙여넣은 뒤, 아무 일도 일어나지 않는 것을 보세요.
짐작하지 말고 확인할 값어치가 있습니다. YAML로 변환되는 XML은 아주 자주 설정이기 때문입니다. 연결 문자열, 서비스 계정, API 키, 사내 호스트 이름은 모두 사람들이 변환기로 들고 오는 종류의 문서에 들어 있습니다.
노르웨이 문제란 무엇인가요?
YAML 1.1은 논리형을 정해진 표기 목록으로 정의하고, 그 목록에는 n, N, no, No, NO가 들어 있습니다. 그래서 노르웨이의 ISO 코드를 담은 필드를 인용부호 없이 쓰면 false로 읽힙니다. 같은 목록은 y와 Y, on과 off, 그리고 스프레드시트에서 내보낸 예/아니오 열도 삼킵니다.
YAML 1.2는 핵심 스키마를 true와 false만으로 좁혔지만, 그것으로 생태계가 고쳐진 것은 아닙니다. PyYAML, Psych, 앤서블, 쿠버네티스 주변 도구의 상당수는 아직 1.1 집합을 해석하고, 어느 파서가 여러분의 파일을 읽을지 고를 수 있는 일은 드뭅니다. 출력기는 그 목록의 모든 표기에 인용부호를 붙이므로, NO는 문자열 NO로 남습니다.
어떤 값에는 인용부호가 있고 어떤 값에는 없는 이유는 무엇인가요?
인용부호가 구조를 지탱하기 때문입니다. 맨 YAML 스칼라는 적힌 꼴에서 형이 추론되므로, 01730은 숫자, 1.10은 실수, NO는 논릿값이고, 앞에 붙은 붙임표는 목록 항목을 시작합니다. 인용부호는 「이것은 텍스트다」라고 말하는 방법입니다.
출력기는 그대로 두면 형이나 뜻이 바뀔 값에만 인용부호를 붙이고 나머지는 맨 채로 둡니다. 모든 스칼라에 인용부호를 붙이면 얻는 것 없이 파일이 읽기도, 비교하기도 어려워지기 때문입니다. 고르게 붙이고 싶다면, 대부분의 YAML 라이브러리에 강제 인용 옵션이 있습니다. 위 예제는 js-yaml과 YamlDotNet에 대해 그것을 보여 줍니다.
여러 줄 텍스트 내용은 어떻게 되나요?
리터럴 블록 스칼라가 됩니다. 수직선과 잘라내기 지시로 시작하고, 그 아래에 줄들이 들여쓰입니다. 접는 방식이 아니라 리터럴을 고른 것은 일부러입니다. 접는 블록은 홀로 있는 줄바꿈을 공백으로 흘려 넣어, 안에 든 코드와 주소를 조용히 망가뜨립니다.
살펴볼 경우가 하나 있습니다. 텍스트의 첫 줄이 뒤의 줄들보다 더 깊이 들여쓰여 있으면, CDATA 절이 앞쪽 공백을 보존할 때 일어나는 일인데, 그 블록은 모호해지고 파서가 거부합니다.
반복되는 요소는 YAML 목록이 되나요?
됩니다. 같은 부모 아래에 두 번 이상 나타나는 요소는 붙임표 목록으로 적히는 수열이 되고, 한 번 나타나는 것은 맨 중첩 사상이나 스칼라가 됩니다. 그것은 「XML에서 JSON으로」 페이지가 설명하는 단수의 모호함과 같은 것이고, 여기서는 더 위험합니다. YAML이 그것을 숨기기 때문입니다. 한 건과 두 건의 차이는 붙임표 하나와 공백 두 칸의 들여쓰기입니다.
편집기 위의 「항상 배열」 칸을 쓰세요. 개념상 목록인 요소의 이름을 적어 두면, 문서가 그것을 하나 담고 있든 마흔 개 담고 있든 수열로 출력됩니다.
XML의 주석과 이름공간은 보존되나요?
주석은 보존되지 않습니다. 문서를 나무로 옮길 때, 출력기가 무엇을 보기도 전에 버려집니다. YAML의 주석은 데이터 모형의 일부가 아니므로, 출력에 써 넣었다 해도 누군가 파일을 읽고 다시 저장한 첫 번에 사라집니다.
이름공간 접두사는 그대로 남으므로 soap:Body는 soap:Body라고 적힌 키가 되고, 맨 YAML 키에 콜론은 적법하지 않으므로 자동으로 감싸집니다. 「이름공간 접두사 제거」에 표시하면 맨 Body가 되지만, 두 이름공간이 한 키로 합쳐질 위험이 있습니다.