Zabbix의Graphid를 통해 이미지 다운로드--제2판
지난번 Powershell 버전의 그림에서 스크립트를 다운로드한 후에 피드백을 받았습니다.1. 이전 스크립트는 dll을 통해 데이터베이스에 접근하고 sql 문장을 통해graphid를 가져옵니다. 이것은 스크립트 이외에 dll가 있다는 것을 의미합니다.그리고 데이터베이스는 접근 권한을 개방해야 한다. 이것은 안전성이 떨어진다는 것을 의미한다.그리고 모든 생산 환경이 데이터에 들어가거나 데이터베이스를 수정할 수 있는 기회는 아니다.2.cookie는 브라우저에서 수동으로 패키지를 가져와야 합니다.이는 일부 IT에 어려움이 있습니다.3. itemkey는 죽은 것으로 작성되었습니다. itemkey의 그림을 많이 만들려면 여러 번 스크립트를 실행하거나 스크립트 코드를 수정해야 합니다.
내가 처음 본 블로그의 내용에 대한 깨우침을 얻어 개선을 하려고 한다.참조 문서:https://www.cnblogs.com/dreamer-fish/p/5485556.html생산 환경의 윈도우즈에python이 없기 때문에, 나는 아직 설치할 수 없다.그러나 리눅스에 있는python은 그 블로그의 코드를 사용할 수 없고 모듈이 부족합니다.그래서 이번에는 vbs로 코드를 실현하려고 합니다.파워셸의 쿠키,session 같은 대상은 너무 복잡합니다.
나는zabbix에서graph를 볼 때 http 캡처를 통해graphid가 발생하는 과정을 보지 못했다.나는zabbix에 독립된 웹api가 있는데 jsonrpc 형식의 데이터를 통해 통신을 하지만 웹 페이지가 정상적으로 방문할 때 이api를 사용하지 않는다는 것을 발견했다.공식 문서:https://www.zabbix.com/documentation/3.4/zh/manual/api
api의 검색 방법은 ql와 같습니다.zabbix를 통해agent_host는 hostid를 찾았고 hostid와 itemkey에 따라itemid를 찾았으며 Itemid를 통해graphid를 찾았습니다.그리고 사진을 다운로드하세요.이 jsonrpc의 과정은 쿠키를 사용할 필요가 없습니다.다음 그림의 과정은 쿠키가 필요합니다.유일한 차이점은 jsonrpc 웹api는 authcode를 가져오는 과정이 필요하고 사용자 이름과 비밀번호를 가져와야 하며 다음 과정은 이authcode를 사용해야 한다는 것이다.
쿠키를 얻기 위해서는 로그인하는 과정을 시뮬레이션해서 쿠키를 찾아야 할 수도 있습니다.로그인 인터페이스 index.php에서post 사용자 이름과 비밀번호를 통해 쿠키를 얻을 수 있습니다.이렇게 하면 코드에 쿠키가 아닌 사용자 이름과 비밀번호만 입력하면 더욱 친절해집니다.
다음은 코드입니다.
On Error Resume Next
Class VbsJson
'Author: Demon
'Date: 2012/5/3
'Website: http://demon.tw
Private Whitespace, NumberRegex, StringChunk
Private b, f, r, n, t
Private Sub Class_Initialize
Whitespace = " " & vbTab & vbCr & vbLf
b = ChrW(8)
f = vbFormFeed
r = vbCr
n = vbLf
t = vbTab
Set NumberRegex = New RegExp
NumberRegex.Pattern = "(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?"
NumberRegex.Global = False
NumberRegex.MultiLine = True
NumberRegex.IgnoreCase = True
Set StringChunk = New RegExp
StringChunk.Pattern = "([\s\S]*?)([""\\\x00-\x1f])"
StringChunk.Global = False
StringChunk.MultiLine = True
StringChunk.IgnoreCase = True
End Sub
'Return a JSON string representation of a VBScript data structure
'Supports the following objects and types
'+-------------------+---------------+
'| VBScript | JSON |
'+===================+===============+
'| Dictionary | object |
'+-------------------+---------------+
'| Array | array |
'+-------------------+---------------+
'| String | string |
'+-------------------+---------------+
'| Number | number |
'+-------------------+---------------+
'| True | true |
'+-------------------+---------------+
'| False | false |
'+-------------------+---------------+
'| Null | null |
'+-------------------+---------------+
Public Function Encode(ByRef obj)
Dim buf, i, c, g
Set buf = CreateObject("Scripting.Dictionary")
Select Case VarType(obj)
Case vbNull
buf.Add buf.Count, "null"
Case vbBoolean
If obj Then
buf.Add buf.Count, "true"
Else
buf.Add buf.Count, "false"
End If
Case vbInteger, vbLong, vbSingle, vbDouble
buf.Add buf.Count, obj
Case vbString
buf.Add buf.Count, """"
For i = 1 To Len(obj)
c = Mid(obj, i, 1)
Select Case c
Case """" buf.Add buf.Count, "\"""
Case "\" buf.Add buf.Count, "\\"
Case "/" buf.Add buf.Count, "/"
Case b buf.Add buf.Count, "\b"
Case f buf.Add buf.Count, "\f"
Case r buf.Add buf.Count, "\r"
Case n buf.Add buf.Count, "
"
Case t buf.Add buf.Count, "\t"
Case Else
If AscW(c) >= 0 And AscW(c) <= 31 Then
c = Right("0" & Hex(AscW(c)), 2)
buf.Add buf.Count, "\u00" & c
Else
buf.Add buf.Count, c
End If
End Select
Next
buf.Add buf.Count, """"
Case vbArray + vbVariant
g = True
buf.Add buf.Count, "["
For Each i In obj
If g Then g = False Else buf.Add buf.Count, ","
buf.Add buf.Count, Encode(i)
Next
buf.Add buf.Count, "]"
Case vbObject
If TypeName(obj) = "Dictionary" Then
g = True
buf.Add buf.Count, "{"
For Each i In obj
If g Then g = False Else buf.Add buf.Count, ","
buf.Add buf.Count, """" & i & """" & ":" & Encode(obj(i))
Next
buf.Add buf.Count, "}"
Else
Err.Raise 8732,,"None dictionary object"
End If
Case Else
buf.Add buf.Count, """" & CStr(obj) & """"
End Select
Encode = Join(buf.Items, "")
End Function
'Return the VBScript representation of ``str(``
'Performs the following translations in decoding
'+---------------+-------------------+
'| JSON | VBScript |
'+===============+===================+
'| object | Dictionary |
'+---------------+-------------------+
'| array | Array |
'+---------------+-------------------+
'| string | String |
'+---------------+-------------------+
'| number | Double |
'+---------------+-------------------+
'| true | True |
'+---------------+-------------------+
'| false | False |
'+---------------+-------------------+
'| null | Null |
'+---------------+-------------------+
Public Function Decode(ByRef str)
Dim idx
idx = SkipWhitespace(str, 1)
If Mid(str, idx, 1) = "{" Then
Set Decode = ScanOnce(str, 1)
Else
Decode = ScanOnce(str, 1)
End If
End Function
Private Function ScanOnce(ByRef str, ByRef idx)
Dim c, ms
idx = SkipWhitespace(str, idx)
c = Mid(str, idx, 1)
If c = "{" Then
idx = idx + 1
Set ScanOnce = ParseObject(str, idx)
Exit Function
ElseIf c = "[" Then
idx = idx + 1
ScanOnce = ParseArray(str, idx)
Exit Function
ElseIf c = """" Then
idx = idx + 1
ScanOnce = ParseString(str, idx)
Exit Function
ElseIf c = "n" And StrComp("null", Mid(str, idx, 4)) = 0 Then
idx = idx + 4
ScanOnce = Null
Exit Function
ElseIf c = "t" And StrComp("true", Mid(str, idx, 4)) = 0 Then
idx = idx + 4
ScanOnce = True
Exit Function
ElseIf c = "f" And StrComp("false", Mid(str, idx, 5)) = 0 Then
idx = idx + 5
ScanOnce = False
Exit Function
End If
Set ms = NumberRegex.Execute(Mid(str, idx))
If ms.Count = 1 Then
idx = idx + ms(0).Length
ScanOnce = CDbl(ms(0))
Exit Function
End If
Err.Raise 8732,,"No JSON object could be ScanOnced"
End Function
Private Function ParseObject(ByRef str, ByRef idx)
Dim c, key, value
Set ParseObject = CreateObject("Scripting.Dictionary")
idx = SkipWhitespace(str, idx)
c = Mid(str, idx, 1)
If c = "}" Then
Exit Function
ElseIf c <> """" Then
Err.Raise 8732,,"Expecting property name"
End If
idx = idx + 1
Do
key = ParseString(str, idx)
idx = SkipWhitespace(str, idx)
If Mid(str, idx, 1) <> ":" Then
Err.Raise 8732,,"Expecting : delimiter"
End If
idx = SkipWhitespace(str, idx + 1)
If Mid(str, idx, 1) = "{" Then
Set value = ScanOnce(str, idx)
Else
value = ScanOnce(str, idx)
End If
ParseObject.Add key, value
idx = SkipWhitespace(str, idx)
c = Mid(str, idx, 1)
If c = "}" Then
Exit Do
ElseIf c <> "," Then
Err.Raise 8732,,"Expecting , delimiter"
End If
idx = SkipWhitespace(str, idx + 1)
c = Mid(str, idx, 1)
If c <> """" Then
Err.Raise 8732,,"Expecting property name"
End If
idx = idx + 1
Loop
idx = idx + 1
End Function
Private Function ParseArray(ByRef str, ByRef idx)
Dim c, values, value
Set values = CreateObject("Scripting.Dictionary")
idx = SkipWhitespace(str, idx)
c = Mid(str, idx, 1)
If c = "]" Then
ParseArray = values.Items
Exit Function
End If
Do
idx = SkipWhitespace(str, idx)
If Mid(str, idx, 1) = "{" Then
Set value = ScanOnce(str, idx)
Else
value = ScanOnce(str, idx)
End If
values.Add values.Count, value
idx = SkipWhitespace(str, idx)
c = Mid(str, idx, 1)
If c = "]" Then
Exit Do
ElseIf c <> "," Then
Err.Raise 8732,,"Expecting , delimiter"
End If
idx = idx + 1
Loop
idx = idx + 1
ParseArray = values.Items
End Function
Private Function ParseString(ByRef str, ByRef idx)
Dim chunks, content, terminator, ms, esc, char
Set chunks = CreateObject("Scripting.Dictionary")
Do
Set ms = StringChunk.Execute(Mid(str, idx))
If ms.Count = 0 Then
Err.Raise 8732,,"Unterminated string starting"
End If
content = ms(0).Submatches(0)
terminator = ms(0).Submatches(1)
If Len(content) > 0 Then
chunks.Add chunks.Count, content
End If
idx = idx + ms(0).Length
If terminator = """" Then
Exit Do
ElseIf terminator <> "\" Then
Err.Raise 8732,,"Invalid control character"
End If
esc = Mid(str, idx, 1)
If esc <> "u" Then
Select Case esc
Case """" char = """"
Case "\" char = "\"
Case "/" char = "/"
Case "b" char = b
Case "f" char = f
Case "n" char = n
Case "r" char = r
Case "t" char = t
Case Else Err.Raise 8732,,"Invalid escape"
End Select
idx = idx + 1
Else
char = ChrW("&H" & Mid(str, idx + 1, 4))
idx = idx + 5
End If
chunks.Add chunks.Count, char
Loop
ParseString = Join(chunks.Items, "")
End Function
Private Function SkipWhitespace(ByRef str, ByVal idx)
Do While idx <= Len(str) And _
InStr(Whitespace, Mid(str, idx, 1)) > 0
idx = idx + 1
Loop
SkipWhitespace = idx
End Function
End Class
Set wshnamed=wscript.arguments.named
strGraphID = wshnamed.item("graphid")
strPicSavePath = wshnamed.item("PicSavePath")
strCookies = wshnamed.item("Cookies")
Set fso = CreateObject("Scripting.FileSystemObject")
zabbix_url = "192.1.31.66"
zabbix_index = "http://" & zabbix_url & "/zabbix/index.php"
zabbix_webapi= "http://" & zabbix_url & "/zabbix/api_jsonrpc.php"
zabbix_username = "Admin"
zabbix_password = "zabbix"
Zabbix_cookie = GetZabbixCookie(zabbix_index,zabbix_username,zabbix_password)
If(Zabbix_cookie = "")Then
Wscript.Echo "Could not get Zabbix cookies, make sure your username and password is correct."
End If
Function GetAuthToken(url,username,password)
Set Winhttp = CreateObject("WinHttp.WinHttpRequest.5.1")
Winhttp.Open "POST", url
Winhttp.SetRequestHeader "Content-Type", "application/json-rpc"
If(cookie <> "")then
Winhttp.SetRequestHeader "Cookie",cookie
End If
Winhttp.Send "{""jsonrpc"": ""2.0"", ""method"": ""user.login"", ""params"":{""user"": """&username&""",""password"": """&password&"""}, ""id"": 0}"
Set json = New VbsJson
GetAuthToken = json.Decode(Winhttp.ResponseText)("result")
End Function
Function GetDaySecond(Day)
GetDaySecond = Day * 24 * 3600
End Function
Function GetGraphid(url,AuthCode,itemid)
Set Winhttp = CreateObject("WinHttp.WinHttpRequest.5.1")
Winhttp.Open "POST", url
Winhttp.SetRequestHeader "Content-Type", "application/json-rpc"
Winhttp.Send "{""jsonrpc"": ""2.0"",""method"": ""graphitem.get"",""params"": {""output"": ""extend"",""expandData"": 1,""itemids"": """&itemid&"""},""auth"": """&AuthCode&""",""id"": 1}"
Set json = New VbsJson
GetGraphid = json.Decode(WinHttp.ResponseText)("result")(0)("graphid")
End Function
Function GetHostid(url,AuthCode,zabbix_agent_hostname)
Set Winhttp = CreateObject("WinHttp.WinHttpRequest.5.1")
Winhttp.Open "POST", url
Winhttp.SetRequestHeader "Content-Type", "application/json-rpc"
If(cookie <> "")then
Winhttp.SetRequestHeader "Cookie",cookie
End If
Winhttp.Send "{""jsonrpc"": ""2.0"",""method"": ""host.get"",""params"": {""output"": ""extend"",""filter"": {""host"": """&zabbix_agent_hostname&"""}},""auth"": """&AuthCode&""",""id"": 1}"
Set json = New VbsJson
GetHostid = json.Decode(Winhttp.ResponseText)("result")(0)("hostid")
End Function
Function GetItemid(url,AuthCode,hostid,zabbix_item_key)
Set Winhttp = CreateObject("WinHttp.WinHttpRequest.5.1")
Winhttp.Open "POST", url
Winhttp.SetRequestHeader "Content-Type", "application/json-rpc"
If(cookie <> "")then
Winhttp.SetRequestHeader "Cookie",cookie
End If
Winhttp.Send "{""jsonrpc"": ""2.0"",""method"": ""item.get"",""params"": {""output"": ""extend"",""hostids"": """ &hostid & """,""search"":{""key_"": """ & zabbix_item_key & """},""sortfield"": ""name""},""auth"": """&AuthCode&""",""id"": 1}"
GetItemid = GetMid(Winhttp.ResponseText,"{""itemid"":""",""",""",1)
End Function
Function GetMid(strText, strFormer, strLater,intStartLocation)
Dim FormerLocation
Dim LaterLocation
Dim PFormerLocation
Dim PLaterLocation
FormerLocation = InStr(intStartLocation, strText, strFormer)
If (FormerLocation <> 0) Then
FormerLocation = FormerLocation + Len(strFormer)
LaterLocation = InStr(FormerLocation, strText, strLater)
If (LaterLocation <> 0) Then
GetMid = Mid(strText, FormerLocation, LaterLocation - FormerLocation)
Exit Function
End If
End If
GetMid = ""
End Function
Function GetZabbixCookie(zabbix_index,username,password)
Set Winhttp = CreateObject("WinHttp.WinHttpRequest.5.1")
Winhttp.Open "POST", zabbix_index
Winhttp.SetRequestHeader "Content-Type", "application/x-www-form-urlencoded"
Winhttp.Send "name=" & username & "&password=" & password & "&autologin=1&enter=Sign+in"
GetZabbixCookie = "zbx_sessionid=" & GetMid(winhttp.GetAllResponseHeaders,"zbx_sessionid=",";",1) & ";"
End Function
Sub DownloadZabbixPic(url,strPath,cookie)
Set Winhttp = CreateObject("WinHttp.WinHttpRequest.5.1")
Winhttp.Open "GET", url
Winhttp.SetRequestHeader "Content-Type", "application/x-www-form-urlencoded"
If(cookie <> "")then
Winhttp.SetRequestHeader "Cookie",cookie
End If
Winhttp.Send
Set sGet = CreateObject("ADODB.Stream")
sGet.Mode = 3
sGet.Type = 1
sGet.Open()
sGet.Write(Winhttp.ResponseBody)
sGet.SaveToFile strPath
End Sub
AuthCode = GetAuthToken(zabbix_webapi,zabbix_username,zabbix_password)
If AuthCode = "" Then
Wscript.Echo "Could not get AuthCode."
Wscript.Quit
End If
CurrentFolder = fso.getfolder(".")
CSV_Path = CurrentFolder&"\list.csv"
If (fso.fileExists(CSV_Path)=0) Then
Wscript.Echo "Could not find " & CSV_Path & "."
Wscript.Quit
End If
set csv_file = fso.opentextfile(CSV_Path)
csv_text = csv_file.readall
csv_file.close
PicSaveDir = CurrentFolder&"\"&replace(date(),"/","")
If (fso.folderExists(PicSaveDir)=0) Then
fso.createfolder(PicSaveDir)
End If
CSV_ReadLine = split(csv_text,vbCrlf)
for i = 1 to ubound(CSV_ReadLine) step 1
CSV_ReadCol = split(CSV_ReadLine(i),"!")
Zabbix_agent_host = CSV_ReadCol(0)
ItemKey = CSV_ReadCol(1)
PicSaveItemDir = PicSaveDir & "\" & Left(ItemKey,4)
If (fso.folderExists(PicSaveItemDir)=0) Then
fso.createfolder(PicSaveItemDir)
End if
PicSavePath = PicSaveItemDir & "\" & Zabbix_agent_host & ".png"
Hostid = GetHostid(zabbix_webapi,AuthCode,Zabbix_agent_host)
If (Hostid = "") Then
Wscript.echo "Hostid is empty. Current host is: " & Zabbix_agent_host
Else
ItemID = GetItemid(zabbix_webapi,AuthCode,Hostid,ItemKey)
If (Itemid = "") Then
Wscript.echo "Itemid is empty. Current host is: " & Zabbix_agent_host & ". Current item is: "&ItemKey
Else
Graphid = GetGraphid(zabbix_webapi,AuthCode,itemid)
If (graphid = "") Then
Wscript.echo "Graphid is empty. Current host is: " & Zabbix_agent_host
Else
If (fso.fileExists(PicSavePath)) Then
Wscript.echo "PNG alreadly exist. " & "Current host is: " & Zabbix_agent_host & ". Current item is: "&ItemKey
Else
DownloadZabbixPic "http://" & zabbix_url & "/zabbix/chart2.php?graphid=" & Graphid & "&period=" & GetDaySecond(30),PicSavePath,Zabbix_cookie
Wscript.Echo Zabbix_agent_host & " " & ItemKey &" successfully save as " & PicSavePath
End if
End If
End If
End If
Next
제가 이 스크립트의 사용 방법을 소개하겠습니다.첫 번째 단계에서 이 스크립트를 설정하면 오류가 발생할 때 프로그램을 종료하지 않고 계속 실행됩니다.그 다음은 json의 클래스로 vbs에서 json을 분석하는 데 사용되지만 이 json류는 약간의 문제가 있어서 json이 복잡하면 분석에 오류가 발생할 수 있습니다.그래서 만약 json이 길다면 나는 다른 명령을 사용하여 json의 값을 가져올 것이다.
이 스크립트가 정상적으로 실행되려면 검사할 호스트 이름을 수정하기 위해 csv 목록 파일이 필요합니다.그리고 이번에 나는 item 항목을 추가해서host의 어떤 item의 속표를 검사하기로 결정했다.이 중 csv의 기본 구분자는 ","이지만, "!"으로 바꿔야 합니다.Itemkey에 쉼표가 포함될 수 있기 때문에, 이 스크립트는 itemkey의 값을 잘못 분리할 수 있습니다.만약 같은 호스트가 두 가지 다른 item의 도표를 필요로 한다면, 두 줄을 써야 한다.첫 줄은 기본적으로 주석이기 때문에 무시한다.csv 파일은 excel에서 생성하고 수정할 수 있습니다.모든 텍스트 편집기에서 편집할 수도 있습니다.
이 csv는 다음과 같이 씁니다.
zabbixAgentHost!itemkey
Zabbix server!system.cpu.utils[,avg]
Zabbix server!vm.memory.useage
이 스크립트에서 변경되는 부분은 zabbix의 IP 주소(대응 변수: zabbix url), 사용자 이름(zabbix username), 암호(zabbix password)입니다.그리고 도표가 php를 생성하는 매개 변수도 있다.그림은 기본적으로 vbs가 있는 경로에 생성됩니다. 오늘 날짜에 생성된 폴더로 만들고 item 이름의 네 번째 폴더로 하위 폴더를 만들고, 그림은 대응하는 폴더에 저장됩니다.그림의 규칙은zabbixagent_hostname의 값이 추가되었습니다.png 접미사 이름입니다.그림이 이미 존재하면, 실행할 때 파일이 존재하지만, 존재하는 그림을 덮어쓰지 않습니다.
다음은 스크립트의function에 대한 설명입니다. GetAuthToken(url,username,password)은 사용자 이름 비밀번호를 통해 웹api authtoken을 가져옵니다.
GetGraphid(url,AuthCode,itemid)itemid를 통해graphid 가져오기
GetHostid(url, AuthCode, zabbix agent hostname)는 authcode 및 zabbix 를 통해agent_hostname 가져오기hostid
GetItemid(url, AuthCode,hostid,zabbix item key)hostid 및 itemkey를 통해itemid 가져오기
GetMid(strText,strFormer,strLater,intStartLocation)는 컴파일된 '텍스트 중간 텍스트 가져오기' 는 주로 두 문자열의 중간 결과를 추출하여 json이 정상적으로 해석되지 않을 때 텍스트 형식으로 json의 값을 가져오는 데 사용됩니다.
GetDay Second (Day) 는 N일의 초수를 계산하는데, 이 값은 후기에 chart2.php에서 표를 생성할 때 매개 변수의 값을 제공합니다.
GetZabbix Cookie(zabbix index, username, password)는 zabbix의 사용자 이름과 비밀번호를 사용하여 쿠키를 가져옵니다.쿠키는 정확한 이미지를 다운로드하는 데 성공하는 필수 조건입니다!
DownloadZabbixPic(url,strPath,cookie) 사실상 이 함수는 그림을 가져올 수 있는 것이 아니라 웹 페이지가 되돌아오는 내용을 이진 형식으로 파일로 저장합니다.쿠키도 사용할 수 있습니다.
스크립트가 list를 읽습니다.csv 파일의 정보 (디렉터리에 list. csv 파일이 있어야 함) 를 그림으로 변환합니다.그림이 생성된 파라미터를 주의하십시오. 스크립트의 DownloadZabbixPic를 검색해서zabbix가 생성한 도표의 파라미터를 볼 수 있습니다.주로 네 가지 파라미터가 비교적 중요한데 stime는 시작 시간을 나타내는 용법(yyyyMDDhhhmmss)은 과거에 대응했던 시간에서 현재까지의 데이터로 기본값을 채우지 않는다.period는 시간 단위를 초로 하고 getdaysecond로 초수를 계산할 수 있다. 기본값은 30일의 데이터이다. 바꾸려면 스스로 수정할 수 있고 이 데이터를 채우지 않는 것도 기본 30일 데이터이다.그리고 두 개의 값은 weight와 height인데, 이 두 개의 값은 그림의 길이와 넓이를 각각 제어한다.안 채워도 돼요.
cmd에서 cscript를 사용하여 이 vbs를 실행하십시오. 그렇지 않으면 오류 보고와 알림 정보가 정보 상자 형식으로 나타납니다.너는 끊임없이 탄창에 시달려 죽을 것이다.이 스크립트의 로그 파일로 cscript 명령을 로그로 바꿀 수 있습니다.
이 내용에 흥미가 있습니까?
현재 기사가 여러분의 문제를 해결하지 못하는 경우 AI 엔진은 머신러닝 분석(스마트 모델이 방금 만들어져 부정확한 경우가 있을 수 있음)을 통해 가장 유사한 기사를 추천합니다:
[VBS] 이메일 보내기텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
CC BY-SA 2.5, CC BY-SA 3.0 및 CC BY-SA 4.0에 따라 라이센스가 부여됩니다.