Open2
メモ

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>固定長テキスト → CSV変換ツール</title>
<style>
:root{--bg:#0b0c0f;--panel:#151821;--muted:#7c8499;--text:#e6eaf2;--acc:#5aa9ff;--warn:#ffb454;--err:#ff5d5d;--ok:#6ee7b7}
html,body{height:100%}
body{margin:0;background:linear-gradient(180deg,#0b0c0f,#0f1220);color:var(--text);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,"Hiragino Kaku Gothic ProN","Yu Gothic UI",Meiryo,sans-serif}
.wrap{max-width:1200px;margin:40px auto;padding:0 16px}
h1{font-size:22px;margin:0 0 12px}
.sub{color:var(--muted);font-size:13px;margin-bottom:16px}
.grid{display:grid;gap:16px}
@media(min-width:980px){.grid{grid-template-columns:1.2fr .8fr}}
.card{background:var(--panel);border:1px solid #22283a;border-radius:16px;box-shadow:0 10px 30px rgba(0,0,0,.25)}
.card h2{margin:0;padding:14px 16px;border-bottom:1px solid #20263a;font-size:16px}
.card .body{padding:14px 16px}
textarea{width:100%;min-height:200px;resize:vertical;background:#0f1322;color:var(--text);border:1px solid #28314c;border-radius:12px;padding:10px;line-height:1.5}
textarea::placeholder{color:#667099}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
.row> *{margin:6px 0}
select, input[type="text"], input[type="number"]{background:#0f1322;color:var(--text);border:1px solid #28314c;border-radius:10px;padding:8px 10px}
.btn{appearance:none;border:1px solid #2a3558;background:#1b2340;color:#e6eaf2;padding:10px 14px;border-radius:12px;cursor:pointer;transition:all .15s ease;font-weight:600}
.btn:hover{transform:translateY(-1px);box-shadow:0 6px 16px rgba(0,0,0,.25)}
.btn.primary{background:linear-gradient(180deg,#2563eb,#1d4ed8);border-color:#1e40af}
.btn.ghost{background:transparent}
.tag{display:inline-flex;align-items:center;gap:6px;background:#0f1322;border:1px solid #28314c;color:#b9c2df;padding:6px 10px;border-radius:999px;font-size:12px}
.note{font-size:12px;color:var(--muted)}
.error{color:var(--err)}
.ok{color:var(--ok)}
table{width:100%;border-collapse:separate;border-spacing:0;overflow:auto}
thead tr:nth-child(1) th{position:sticky;top:0;background:#11162a;z-index:2}
thead tr:nth-child(2) th{position:sticky;top:36px;background:#11162a;z-index:2}
thead tr:nth-child(3) th{position:sticky;top:72px;background:#11162a;z-index:2}
th,td{border-bottom:1px solid #24304e;padding:8px 10px;font-size:13px;text-align:left}
thead th{font-weight:700}
tbody tr:nth-child(odd){background:#111527}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace}
.toolbar{display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:10px 0}
.switch{display:inline-flex;align-items:center;gap:8px;cursor:pointer}
.switch input{accent-color:#4f7cff}
.footer{margin:18px 0;color:var(--muted);font-size:12px}
.help{font-size:12px;color:#b9c2df}
.kpd{display:inline-block;min-width:2ch;text-align:right}
</style>
</head>
<body>
<div class="wrap">
<h1>固定長テキスト → CSV 変換ツール</h1>
<div class="sub">項目定義(TSV)と固定長テキストを貼り付け、エンコーディング/可視化設定を選んで「変換」してください。</div>
<div class="grid">
<section class="card">
<h2>① 項目定義(TSV)</h2>
<div class="body">
<div class="row note">形式: <span class="tag">name\tbytes</span> を行単位(例: <span class="mono">id\t4</span>)</div>
<textarea id="schema" placeholder="id 4
name 5
area 12"></textarea>
<div class="row">
<label class="switch"><input id="hasHeader" type="checkbox"/> 1行目をヘッダとして扱う(データ側)</label>
</div>
</div>
</section>
<section class="card">
<h2>② 固定長テキスト</h2>
<div class="body">
<textarea id="fixed" class="mono" placeholder="0001Yamada Tokyo──────
0002Suzuki Osaka──────"></textarea>
<div class="row">
<label class="switch"><input id="visualizeSpaces" type="checkbox" checked/> 表示上スペースを可視化(半角=· / 全角=□)</label>
</div>
<div class="row">
<label>エンコーディング
<select id="encoding">
<option value="utf8" selected>UTF-8(厳密)</option>
<option value="sjis">Shift_JIS 近似(ASCII/半角カナ=1B, それ以外=2B)</option>
</select>
</label>
<label>行末
<select id="linebreak">
<option value="auto" selected>自動認識</option>
<option value="\n">LF (\\n)</option>
<option value="\r\n">CRLF (\\r\\n)</option>
</select>
</label>
<button id="run" class="btn primary">変換</button>
<button id="downloadCsv" class="btn">CSVダウンロード</button>
<span id="status" class="note"></span>
</div>
</div>
</section>
</div>
<section class="card" style="margin-top:16px">
<h2>③ 結果</h2>
<div class="body" id="result">
<div class="help">ここにテーブルが表示されます。先頭3行は「項目名」「Byte数」「開始位置(1始まり)」です。</div>
</div>
</section>
<div class="footer">
注意: Shift_JIS はブラウザ標準APIに TextEncoder が無いため、一般的な近似ルールでバイト数を算出しています(ASCII/半角カナ=1バイト、それ以外=2バイト)。厳密なCP932変換が必要な場合は専用ライブラリの組み込みをご検討ください。
</div>
</div>
<script>
// ===== Utility: byte length & safe slice =====
const encoders = {
utf8Length: (s) => new TextEncoder().encode(s).length,
utf8SliceByBytes: (s, maxBytes) => {
const te = new TextEncoder();
let bytes = 0, out = '';
for (const ch of s){
const bl = te.encode(ch).length;
if (bytes + bl > maxBytes) break;
bytes += bl; out += ch;
}
return {text: out, usedBytes: bytes};
},
// SJIS approximation: ASCII (<=0x7F) =1, 半角カナ U+FF61-FF9F =1, その他=2
sjisLenOfChar: (ch) => {
const code = ch.codePointAt(0);
if (code <= 0x7F) return 1; // ASCII
if (code >= 0xFF61 && code <= 0xFF9F) return 1; // 半角カナ
return 2; // おおむね2B
},
sjisLength: (s) => {
let n=0; for (const ch of s) n += encoders.sjisLenOfChar(ch); return n;
},
sjisSliceByBytes: (s, maxBytes) => {
let bytes = 0, out='';
for (const ch of s){
const bl = encoders.sjisLenOfChar(ch);
if (bytes + bl > maxBytes) break;
bytes += bl; out += ch;
}
return {text: out, usedBytes: bytes};
}
};
// Visible marks for spaces (display-only)
const visualize = (s) => s.replaceAll(' ', '·').replaceAll('\u3000','□');
// Parse schema TSV lines → [{name, bytes}]
function parseSchema(tsv){
const lines = tsv.split(/\r?\n/).map(l=>l.trim()).filter(Boolean);
const schema = [];
for (let i=0;i<lines.length;i++){
const [name, bytesStr] = lines[i].split(/\t+/);
if (!name || !bytesStr) throw new Error(`項目定義 ${i+1}行目の形式が不正です(name\tbytes)`);
const bytes = Number(bytesStr);
if (!Number.isInteger(bytes) || bytes<=0) throw new Error(`項目定義 ${i+1}行目のByte数が不正です: ${bytesStr}`);
schema.push({name, bytes});
}
return schema;
}
function splitLines(text, mode){
if (mode === '\\n') return text.split('\n');
if (mode === '\\r\\n') return text.split('\r\n');
// auto
if (text.includes('\r\n')) return text.split('\r\n');
return text.split('\n');
}
// Fixed-width split by bytes
function splitFixedBySchema(line, schema, encoding){
const chunks = [];
let rest = line;
let encLen, encSlice;
if (encoding==='utf8'){
encLen = encoders.utf8Length; encSlice = encoders.utf8SliceByBytes;
} else {
encLen = encoders.sjisLength; encSlice = encoders.sjisSliceByBytes;
}
for (const field of schema){
const {text: part, usedBytes} = encSlice(rest, field.bytes);
chunks.push(part);
// remove consumed chars from rest
rest = rest.slice(part.length);
// If consumed bytes < field.bytes but no more chars, pad empty remainder (display only)
}
// Remaining chars ignored
return chunks;
}
// Build table DOM
function renderTable(container, schema, records, opts){
const enc = opts.encoding;
// Compute start positions (1-based)
const starts = [];
let pos = 1;
for (const f of schema){ starts.push(pos); pos += f.bytes; }
const table = document.createElement('table');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
const tr1 = document.createElement('tr');
for (const f of schema){ const th = document.createElement('th'); th.textContent = f.name; tr1.appendChild(th);}
const tr2 = document.createElement('tr');
for (const f of schema){ const th = document.createElement('th'); th.textContent = `${f.bytes}`; tr2.appendChild(th);}
const tr3 = document.createElement('tr');
for (const s of starts){ const th = document.createElement('th'); th.textContent = `${s}`; tr3.appendChild(th);}
thead.appendChild(tr1); thead.appendChild(tr2); thead.appendChild(tr3);
for (const rec of records){
const tr = document.createElement('tr');
rec.forEach(val => {
const td = document.createElement('td');
td.className = 'mono';
td.textContent = opts.visualize ? visualize(val) : val;
tr.appendChild(td);
});
tbody.appendChild(tr);
}
table.appendChild(thead); table.appendChild(tbody);
container.innerHTML = '';
container.appendChild(table);
}
function toCSV(rows, visualizeSpaces){
// RFC4180-ish, escaping quotes
const esc = (s) => {
const t = visualizeSpaces ? visualize(s) : s;
if (t == null) return '';
const needQuote = /[",\n\r]/.test(t) || t.includes(',');
const x = String(t).replaceAll('"','""');
return needQuote ? `"${x}"` : x;
};
return rows.map(r => r.map(esc).join(',')).join('\r\n');
}
function parse(){
const status = document.getElementById('status');
status.textContent = '';
try {
const schema = parseSchema(document.getElementById('schema').value);
const fixedRaw = document.getElementById('fixed').value.replace(/\u0000/g,'');
const linebreak = document.getElementById('linebreak').value;
const lines = splitLines(fixedRaw, linebreak).filter(l=>l.length>0);
const encoding = document.getElementById('encoding').value;
const visualizeSpaces = document.getElementById('visualizeSpaces').checked;
const records = [];
for (let i=0;i<lines.length;i++){
const row = splitFixedBySchema(lines[i], schema, encoding);
records.push(row);
}
// Header rows for CSV
const headerNames = schema.map(f=>f.name);
const headerBytes = schema.map(f=>String(f.bytes));
const headerStarts = (()=>{let a=[];let p=1;for(const f of schema){a.push(String(p));p+=f.bytes;}return a;})();
renderTable(document.getElementById('result'), schema, records, {visualize: visualizeSpaces, encoding});
// cache for download
window.__csvRows = [headerNames, headerBytes, headerStarts, ...records];
status.innerHTML = ``;
} catch (e){
document.getElementById('result').innerHTML = '';
status.innerHTML = ``;
console.error(e);
}
}
function download(){
const rows = window.__csvRows || [];
if (!rows.length){
document.getElementById('status').innerHTML = ``;
return;
}
const visualizeSpaces = document.getElementById('visualizeSpaces').checked;
const csv = toCSV(rows, visualizeSpaces);
const blob = new Blob([csv], {type:'text/csv;charset=utf-8'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'fixed_to_csv.csv';
document.body.appendChild(a); a.click(); a.remove();
URL.revokeObjectURL(url);
}
document.getElementById('run').addEventListener('click', parse);
document.getElementById('downloadCsv').addEventListener('click', download);
// Demo preload
document.getElementById('schema').value = 'id\t4\nname\t5\narea\t12';
document.getElementById('fixed').value = '0001Yamad\u0020Tokyo\u2500\u2500\u2500\u2500\u2500\u2500\n0002Suzuk\u0020Osaka\u2500\u2500\u2500\u2500\u2500\u2500';
</script>
</body>
</html>

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>固定長テキスト → 表示(SJIS想定)</title>
<style>
body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Hiragino Kaku Gothic ProN","Yu Gothic UI",Meiryo,sans-serif;margin:16px}
h1{font-size:20px;margin:0 0 10px}
h2{font-size:16px;margin:18px 0 8px}
textarea{width:100%;min-height:180px;resize:vertical;padding:6px}
.row{margin:8px 0}
button{padding:6px 12px}
table{border-collapse:collapse;width:100%;margin-top:10px}
th,td{border:1px solid #999;padding:4px 6px;font-size:13px;text-align:left}
th{background:#f5f5f5}
.mono{font-family:ui-monospace,Consolas,Monaco,monospace}
.note{font-size:12px;color:#444}
.error{color:#c00}
.ok{color:#060}
</style>
</head>
<body>
<h1>固定長テキスト → テーブル(Shift_JIS想定)</h1>
<h2>① 項目定義(TSV)</h2>
<div class="note">形式: <code>name\tbytes</code> を行単位(例: <code>id\t4</code>)。</div>
<textarea id="schema" placeholder="id\t4\nname\t5\narea\t12"></textarea>
<h2>② 固定長テキスト(複数行)</h2>
<textarea id="fixed" class="mono" placeholder="0001Yamada Tokyo──────\n0002Suzuki Osaka──────"></textarea>
<div class="row">
<label><input id="visualizeSpaces" type="checkbox" checked> スペース可視化(半角=· / 全角=□)</label>
</div>
<div class="row">
<button id="run">変換</button>
<span id="status" class="note"></span>
</div>
<h2>③ 結果</h2>
<div id="result" class="note">ここにテーブルが表示されます。先頭3行は「項目名」「Byte数」「開始位置(1始まり)」です。</div>
<script>
// --- Shift_JIS(CP932相当)の簡易バイト長ルール ---
// 近似: ASCII(0x00-0x7F)=1B、半角カナ(U+FF61-FF9F)=1B、その他=2B。
// ※厳密変換が必要ならライブラリ置換が必要です。
function sjisLenOfChar(ch){
const code = ch.codePointAt(0);
if (code <= 0x7F) return 1; // ASCII
if (code >= 0xFF61 && code <= 0xFF9F) return 1; // 半角カナ
return 2; // その他(全角類)
}
function sjisSliceByBytes(str, maxBytes){
let out = '', used = 0;
for (const ch of str){
const bl = sjisLenOfChar(ch);
if (used + bl > maxBytes) break;
out += ch; used += bl;
}
return {text: out, usedBytes: used};
}
// 表示用のスペース可視化
const visualize = (s) => s.replaceAll(' ', '·').replaceAll('\u3000','□');
// TSVスキーマ解析
function parseSchema(tsv){
const lines = tsv.split(/\r?\n/).map(l=>l.trim()).filter(Boolean);
const schema = [];
for (let i=0;i<lines.length;i++){
const [name, bytesStr] = lines[i].split(/\t+/);
if (!name || !bytesStr) throw new Error(`${i+1}行目: 形式は name\tbytes です`);
const bytes = Number(bytesStr);
if (!Number.isInteger(bytes) || bytes<=0) throw new Error(`${i+1}行目: Byte数が不正です`);
schema.push({name, bytes});
}
return schema;
}
// 行分割(CRLF/LF自動)
function splitLines(text){
if (text.includes('\r\n')) return text.split('\r\n');
return text.split('\n');
}
// 固定長分割(SJIS前提)
function splitFixedBySchema(line, schema){
let rest = line; const chunks = [];
for (const f of schema){
const {text: part} = sjisSliceByBytes(rest, f.bytes);
chunks.push(part);
rest = rest.slice(part.length);
}
return chunks;
}
// テーブル描画(先頭3行: 項目名 / Byte数 / 開始位置)
function renderTable(container, schema, records, visualizeSpaces){
const starts = []; let pos = 1; for (const f of schema){ starts.push(pos); pos += f.bytes; }
const table = document.createElement('table');
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
const tr1 = document.createElement('tr');
schema.forEach(f=>{const th=document.createElement('th');th.textContent=f.name;tr1.appendChild(th)});
const tr2 = document.createElement('tr');
schema.forEach(f=>{const th=document.createElement('th');th.textContent=f.bytes;tr2.appendChild(th)});
const tr3 = document.createElement('tr');
starts.forEach(s=>{const th=document.createElement('th');th.textContent=s;tr3.appendChild(th)});
thead.appendChild(tr1); thead.appendChild(tr2); thead.appendChild(tr3);
records.forEach(rec=>{
const tr=document.createElement('tr');
rec.forEach(val=>{
const td=document.createElement('td');
td.className='mono';
td.textContent = visualizeSpaces ? visualize(val) : val;
tr.appendChild(td);
});
tbody.appendChild(tr);
});
table.appendChild(thead); table.appendChild(tbody);
container.innerHTML=''; container.appendChild(table);
}
function run(){
const status = document.getElementById('status');
status.textContent = '';
try{
const schema = parseSchema(document.getElementById('schema').value);
const lines = splitLines(document.getElementById('fixed').value).filter(l=>l.length>0);
const visualizeSpaces = document.getElementById('visualizeSpaces').checked;
const records = lines.map(line => splitFixedBySchema(line, schema));
renderTable(document.getElementById('result'), schema, records, visualizeSpaces);
status.innerHTML = ``;
}catch(e){
document.getElementById('result').innerHTML='';
status.innerHTML = ``;
}
}
document.getElementById('run').addEventListener('click', run);
// デモ用初期値
document.getElementById('schema').value = 'id\t4\nname\t5\narea\t12';
document.getElementById('fixed').value = '0001Yamad Tokyo──────\n0002Suzuk Osaka──────';
</script>
</body>
</html>