Skip to content

Instantly share code, notes, and snippets.

@cometkim
Last active January 7, 2025 08:05
Show Gist options
  • Save cometkim/f204b035fc9822a60d0d8b421db11567 to your computer and use it in GitHub Desktop.
Save cometkim/f204b035fc9822a60d0d8b421db11567 to your computer and use it in GitHub Desktop.
보건의료빅데이터개방시스템(opendata.hira.or.kr) 공공데이터 상세 페이지 첨부파일 일괄 다운로드 스크립트

보건의료빅데이터개방시스템(opendata.hira.or.kr) 공공데이터 상세 페이지 첨부파일 일괄 다운로드 스크립트

Usage

bun install

PAGE="https://opendata.hira.or.kr/op/opc/selectOpenData.do?sno=11925&publDataTpCd=&searchCnd=&searchWrd=%EC%A0%84%EA%B5%AD&pageIndex=1"
bun download-hira.ts "$PAGE"

Customization

Dext5Upload 라이브러리를 사용하는 여러 사이트에서도 재활용 가능할 것으로 보이나 구체적인 구성 값을 변경해야 할 수 있음.

페이지 실행 컨텍스트에서 Dext5Upload_Config 인스턴스를 참고하여 makeDownloadRequest()의 구성값을 변경할 것

#!/usr/bin/env bun
import * as cheerio from 'cheerio';
const [url] = process.argv.slice(2);
if (!url) {
throw new Error('첨부파일이 있는 상세 페이지 URL을 입력하세요.');
}
const html = await fetch(new URL(url), {
method: 'GET',
headers: {
'Accept': 'text/html',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
},
}).then(res => res.text());
const $ = cheerio.load(html);
const filelistSource = $('#dext5-container script').text();
const filelistPattern = /DEXT5UPLOAD\.AddUploadedFile\(\s*'(?<filenum>\d+)',\s*'(?<webName>[^']+)',\s*'(?<origName>[^']+)'/g;
type FileInfo = {
filenum: string,
webName: string,
origName: string,
};
const filelist = [...filelistSource.matchAll(filelistPattern)]
.map(match => match.groups as FileInfo);
console.debug(filelist);
function makeGuid() {
const c = () => (65536 * (1 + Math.random()) | 0).toString(16).substring(1)
return (c() + c() + "-" + c() + "-" + c() + "-" + c() + "-" + c() + c() + c()).toUpperCase();
}
function base64Encode(value: string) {
return Buffer.from(value, 'utf-8').toString('base64');
}
// base64를 무려 두 번이나 수행하는 매우 강력한 보안 메커니즘 ㄷㄷ
function superStrongEncrypt(value: string): string {
return btoa('R' + base64Encode(value)).replace(/[+]/g, '%2B');
}
// Note:
// Dext5Upload 라이브러리에서 요구하는 파라미터 인코딩 방식을 사용
//
// `Dext5Upload_Config` 객체(예제 사이트에선 `UPLOADTOP.G_CURRUPLOADER._config`)로부터 현재 구성 값(delimiters, handler url, etc) 확인 가능
// `config.security.encryptParams === "1"` 일 때 아래 인코딩이 유효하고, "0"(비활성화)이면 다른 코드를 사용해야 함
function makeDownloadRequest(file: FileInfo): Request {
const unitDelimiter = '\v';
const attributeUnitDelimiter = '\f';
const fileNameRuleEx = '_';
const downloadHandlerUrl = new URL('https://opendata.hira.or.kr/dext5upload/handler/upload.dx?callType=download&url=/op/opc/selectOpenData.do');
type Attr = [key: string, name: string];
function serializeAttr([key, name]: Attr): string {
return key + attributeUnitDelimiter + name;
}
const attrs: Attr[] = [
['d01', 'downloadRequest'],
['d10', fileNameRuleEx],
['d25', file.origName],
['d26', Buffer.from(file.webName).toString('latin1')],
['d07', makeGuid()],
];
const serializedAttrs = attrs.map(serializeAttr).join(unitDelimiter);
const encryptedAttrs = superStrongEncrypt(serializedAttrs);
return new Request(downloadHandlerUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
},
body: new URLSearchParams({
customValue: file.filenum,
d00: encryptedAttrs,
}),
});
}
async function downloadFile(file: FileInfo) {
const downloadRequest = makeDownloadRequest(file);
const response = await fetch(downloadRequest);
if (!response.ok) {
console.error('Failed to download %s', file.webName);
return;
}
await Bun.write(file.webName, response);
}
// Note: 대역폭이 매우 작으니 하나씩 받을 것
// 가끔 소켓 끊어먹는 것 같으니 중요한 곳에선 재시도 추가하면 좋음
for (const file of filelist) {
console.log('Downloading %s', file.webName);
await downloadFile(file);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment