[완료] Perl의 WWW::Mechanize로 euc-kr 인코딩 쓰는 사이트에 폼 전송할 때 문자열이 UTF-8인코딩되어 버리는 오류

raymundo의 이미지

안녕하세요, 해결되지 않는 문제가 있어서 간만에 글 남깁니다.

캐릭터셋 인코딩을 euc-kr (또는 cp949 암튼 뭐가 됐든 UTF-8이 아닌 한국어) 사용하는 웹페이지가 있고,

제가 WWW::Mechanize 를 써서 폼에 텍스트를 채워넣고 submit하는 펄 코드를 작성했거든요. (스팸봇은 아니에요 ^^;)

# 아래 펄 코드소스는 euc-kr 로 저장되어 있음
# 또는 펄 소스코드를 UTF-8로 저장하고, use utf8; 을 명시한 후 
# $text 를 Encode::encode("euc-kr", $text) 해서 euc-kr 바이트 스트림으로 바꿔줘도 동일한 결과
$text = "가각간";    # euc-kr \xb0 \xa1 \xb0 \xa2 \xb0 \xa3
$mech->get( "사이트주소" );
$mech->submit_form(
            form_name => "form_edit",
            fields => {
                text => $text,
                }
            );

이 코드를 실행하면 해당 사이트에 텍스트 폼이 입력되어 반영은 되는데... 그 글자가
"가각간" 이 아니라
"째징째짖째짙" 이 되어 있습니다. (c2 b0 c2 a1 c2 b0 c2 a2 c2 b0 c2)

이건 "가각간"의 euc-kr 인코딩한 b0a1 b0a2 b0a3 를 다시 UTF-8 로 인코딩해버렸을 때 나오는 결과물인 것까지는 확인했습니다.

요컨데, 나는 euc-kr 바이트를 전송하고 싶은데, Mechanize 가 제 멋대로 그 데이타를 UTF-8 로 인코딩해버린 후 전송하고 있는 건데요.

구글링해보니까 중국어 GBK 인코딩 데이터를 전송하려다가 동일한 문제를 겪은 사람의 글이 있더군요.
http://groups.google.com/group/www-mechanize-users/browse_thread/thread/7a0cf5cff0c1817c?tvc=2&pli=1

이 사람의 분석에 의하면,
- Mechanize 가 웹 페이지 내용을 자동으로 디코드하면서 폼에 기존 존재하는 데이타들에 utf8 플래그가 켜지고, 여기에 내가 전송하려는 문자열이 합쳐져 하나의 문자열이 되는 과정에서 이 오류가 발생하는 거고,
- 해결책은 URI/_query.pm 를 수정해서 문자열 조인 전에 utf8 플래그를 off 로 하도록 하는 거다...
라는데 말이죠.

차마 시스템에 설치된 모듈을 수정하는 건 좀 그렇고....

혹시 이 문제를 다른 방법으로 해결하셨던 분이 안 계시려나 해서 여쭤봅니다. 감사합니다.

keedi의 이미지

raymundo님의 질문은 항상 명확하지만
이미 찾을 것은 다 찾아보시고,
해볼 것은 다 해보시고 질문해주시는 터라
짧은 저로서 드릴 수 있는 답변은 꼼수 밖에 없네요. ㅎㅎㅎ

지금 상황은 일단, 상황을 타결할 만한 답(패치)을 찾으셨는데,
시스템 모듈에 적용하자니 찜찜한 경우에 한한 lifehack 입니다.
좋은 방법인지 스스로도 의문이고,
권장할 만한 방법인지에 대한 확신도 없습니다만...
저 자신은 요로코롬 꼼수(?) 사용하고 있기에 답변드립니다.

일단 첫 번째 방법:
스크립트가 위치하는 디렉터리에 URI::_query.pm 파일을 복사하는 방법입니다.
{current_path}/URI/_query.pm 정도가 되겠지요. :)
그 후에 언급하신 부분만 수정한 후 BEGIN이나 local::lib 등을 이용해서
현재 디렉터리를 가장 먼저 탐색하도록 합니다.

두 번째 방법:
URI::_query.pm 에서 수정해야할 함수(아마도 1~2개 정도겠죠?)를 복사해서
실행할 스크립트 내부에 copy & paste 합니다.
그리고 언급하신 부분만 수정합니다.
마지막으로 함수 이름이 query() 라고한다면 이름을 다음처럼 변경합니다.

sub URI::_query::query {
    ...
}

세 번째 방법:
patch 디렉터리를 만들고 1 또는 2의 방법을 조합
스크립트에서 해당 패치 파일을 use 또는 require

결국 세 가지 모두 이름공간을 덮어쓰는 동일한 방법이고
스타일만 다를 뿐입니다.

제가 즐겨 쓰는 방법이긴한데 바라며
정확한 방법인지 확신도 없고, 일단은 잘 동작하는데
그렇다고 패치를 보내서 메인스트림에 적용시키자니
귀찮거나, 뭔가 찜찜할때 주로 저렇게 사용하고 있습니다.

장점은 시스템의 모듈이 이미 존재하더라도
시스템의 모듈은 전혀 건드리지 않기 때문에 쓸만은 합니다만,
객체지향적인 관점에서는 학을 떼시는 분들도 있으리라고 생각합니다.

개인적으로는 어짜피 스크립트 언어에서 스크립트 작성하는데
이름공간 덮어쓰기 정도의 lv3. 흑마법 정도는 시전해도 반칙은 아니지 않을까합니다.ㅎㅎ

고수분들의 답변을 기다리며 이만 줄입니다. :)

----
use perl;

Keedi Kim

----
use perl;

Keedi Kim

raymundo의 이미지

사실 그 구글링해서 찾은 글 가지고는, "그래서 해당 모듈을 어떻게 고치란 말인지"를 또 찾아봐야하는 상황이지만...;;; 그렇게라도 해봐야겠네요. ^^;

좋은 하루 되세요!

keedi의 이미지

제가 WWW::Mechanize 를 잘 몰라서 조심스럽습니다만.
답변대로라면 URI/_query.pm 파일을 열어보니
URI::Escape::escape_char() 함수를 정규표현식 내부에서
e 옵션을 주어 평가하는 부분이 4 군데 정도 있네요.
바로 이 앞부분에서 utf8 플래그를 꺼주면 되지 않을까합니다.
utf8을 꺼준다는 것은 no utf8; 이런 것 말하는 거 맞겠죠? ^^;;

...
        if (defined $q) {
            no utf8; # <-- 추가?
            $q =~ s/([^$URI::uric])/ URI::Escape::escape_char($1)/ego;
            $$self .= "?$q";
        }
...

이런 느낌이면 괜찮지 않을까합니다만... ^^;

----
use perl;

Keedi Kim

----
use perl;

Keedi Kim

raymundo의 이미지

CPAN에 URI 모듈 가 보니까 저 사람이 여기에도 이 문제를 거론했더라고요.

https://rt.cpan.org/Public/Bug/Display.html?id=43859

그래서 위에 적힌 대로 저 사람이 수정했다는 부분을 똑같이 따라서 해 봤는데...

저는 효과가 없군요 -_-;;;;;;;

천천히 알아봐야겠습니다. 어차피 아주 다급한 문제가 아닌, 좀 더 정확히 말하면 빨리 해결할 수록 좋긴 하지만 해결 못 한다고 큰일 나는 문제가 아니라서요. keedi님 신경써주셔서 감사합니다. 지금 매우 바쁘신 상황인 걸로 아는데...ㅠ,.ㅠ

좋은 하루 되세요!

aero의 이미지

일단 WWW::Mechanize 모듈의 소스를 보니 form을 submit 할때

WWW::Mechanize -> HTTP::Form -> URI 모듈의 함수들이 차례로 호출 되더군요.

그래서 HTTP::Form 모듈 소스를 보니

    my $charset = $self->accept_charset eq "UNKNOWN" ? $self->{default_charset} : $self->accept_charset;
    if ($Encode_available) {
        foreach my $fi (@form) {
            $fi = Encode::encode($charset, $fi) unless ref($fi);
        }
    }

부분이 보였습니다. 이것은 일단 입력은 무조건 펄 내부유니코드 포멧(utf8인코딩/utf8플래그 온)으로 받고 지정된 인코딩(charset)으로 ( 없으면 utf8(default_charset)으로 ) encode함을 뜻합니다.(raw byte stream을 만든다는의미) 따라서 무조건 쿼리할 문자열은 decode등의 과정을 통해서 펄 내부유니코드 포멧으로 만든 스트링을 넘겨줘야 함을 알 수 있습니다.

하지만 WWW::Mechanize에서 submit_form 함수를 통해서 폼을 정의해서 넘길때는 Form의 charset을 지정할 방법이 없습니다. 따라서 Form의 charset을 지정하려면 일단 WWW::Mechanize로 타겟페이지를 읽어와서 Mechanize가 분석한 form객체(HTTP::Form)들로 부터 사용할 form의 객체에 접근해서 그 내부의 인코딩 값을 강제로 지정하는 방법을 쓰면 됩니다.

다음은 test한 코드들입니다.

form.html

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html lang="ko">
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=euc-kr">
  <title></title>
</head>
<body>
<form name="form1" id="form1" action="form.cgi" method="POST">
  <textarea name="textarea1" id="textarea1" style="height:15em; width:40em"></textarea><br>
  <input type="submit" value="확인" class="submit">
  <input type="reset" value="reset">
  <br>
</form>
 </body>
</html>

form.cgi

#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use Encode qw/encode decode/;
 
my $q = CGI->new;
$q->charset('euc-kr');
print $q->header("text/plain");
print $q->param('textarea1');
open my $fh,'>', '/tmp/out.txt';
print {$fh} $q->param('textarea1');  # 넘어온 raw bytes를 그대로 파일로 쓴다. 나중에 확인을 위해서.

test.pl

#!/usr/bin/env perl
use strict;
use warnings;
use utf8;
use Encode qw/encode decode/;
use WWW::Mechanize;
use Data::Dump qw/dump/;
 
my $text = '가각간';  # use utf8 프래그마에 의해서 펄 내부유니코드포멧 스트링이 됨
 
my $mech = WWW::Mechanize->new();
$mech->get('http://some.url.com/form.html');
$mech->form_name('form1')->accept_charset('euc-kr');      # 인코딩 지정
#dump($mech->form_name('form1'));
$mech->submit_form(
              form_name => 'form1',
              fields => {
                  textarea1 => $text,
              },
          );
 
# 결과 페이지가 euc-kr 이라도 결과는 자동으로 펄 유니코드포멧으로 decoding된 결과를 받음
binmode STDOUT, ':encoding(UTF-8)';
print $mech->content(format => 'text');

결과

$ perl test.pl
가각간
 
$ cat /tmp/out.txt | iconv -f euc-kr -t utf8
가각간

raymundo의 이미지

aero님 감사합니다, 역시 고수의 손길... ^^

바로 감사답글 달아야 했는데 확실히 정리하려고 이것저것 해보다가... 그 다음은 잠들어서 -_-; 이제서야...;

남이 만든 코드를 따라가는 게 어려워서... 봐야할 건 HTML::Form 쪽이었군요.

애초에 폼에 accept-charset 속성이 명시되어 있다면 그게 반영이 되는데, 그렇지 않은 경우는 UTF-8값이 되어버리는군요.

# WWW::Mechanize 쪽에서
sub update_html {
    $self->{forms} = [ HTML::Form->parse($html, $self->base) ];   # 요렇게 폼을 추출하는데
}
 
# HTML::Form 에서
sub parse {
    ...
    unless (defined $charset) {
    if (ref($html) and $html->can("content_charset")) {
        $charset = $html->content_charset;
    }
    unless ($charset) {
        $charset = "UTF-8";
    }
    }
    ...
        $f->{default_charset} = $charset;   # 요렇게 default 값이 UTF-8 이 되고,
}
 
sub make_request {
    ...
    my $charset = $self->accept_charset eq "UNKNOWN" ? $self->{default_charset} : $self->accept_charset;  # 그게 다시 반영되고
    if ($Encode_available) {
        foreach my $fi (@form) {
            $fi = Encode::encode($charset, $fi) unless ref($fi);   # 여기에서 인코드 해버리네요
        }
    }
}

http://www.w3.org/TR/html401/interact/forms.html#adef-accept-charset 여기에 있는 accept-charset 속성 설명을 봐도 그렇고, 명시되어 있지 않다면 폼을 포함하고 있는 html 문서 자체의 캐릭터셋 속성을 가져다 써야 할 것 같은데 말이죠.

암튼 해결책은 적어주신대로,

    $mech->form_name("form_edit")->accept_charset("euc-kr");

이 한 줄을 submit 전에 추가해주고, 전송할 데이타는 내부유니코드포맷으로 넣어주니 잘 되는군요.

한 가지 미스테리가 남아 있긴 한데... 사실은 이 스크립트가 일년 전부터 쓰고 있던 건데, 처음에는 잘 동작했었거든요 -_-? 그런데 올해 초에 갑자기 전송한 문자열이 와자작 깨지기 시작했지요.
- 해당 사이트가 예전에는 form 에 accept-charset 속성이 적혀 있었는데 그 시점에서 사라졌다 --- 설마;
- 이 스크립트가 돌던 서버의 Perl 모듈들이 업데이트되면서 Mechanize 나 HTML::Form 에 관련 부분이 바뀌었다
http://cpansearch.perl.org/src/GAAS/libwww-perl-5.834/Changes 2009년 6월에 HTML::Form 에서 accept-charset 을 지원하게 되었다고 나오고 있는 걸로 봐서 얼추 맞아떨어지는 것 같기도 한데...
암튼 이 미스테리는 그냥 묻어두고 -_-; 넘어가죠 뭐~

다시 한 번 감사드립니다~

P.S. 그 중국어 인코딩 때문에 골치썩이던 그 분은 URI 모듈 쪽에 가서 수정을 제안할 게 아니었군요... ^^; 그 후에라도 이러면 된다는 걸 알아냈으려나 모르겠네요ㅋ

좋은 하루 되세요!

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.