[첫화면으로]UseModWiki소스수정/문자열일괄치환

마지막으로 [b]

1. 모든 페이지의 문자열 일괄 치환
1.1. 사용법
1.2. 치환 예
1.3. 부작용
1.4. action/adminmenu.pl 수정
1.5. action/replacetext.pl 추가
1.6. translations/korean.pl 수정
1.7. 추가 업데이트 내역
1.8. 사용자 의견

1. 모든 페이지의 문자열 일괄 치환

마눌님 블로그 주소가 바뀌어서 기존에 링크했던 것들을 전부 고쳐주어야 했는데, 한 40개쯤 되길래 작업해 보았음1

1.1. 사용법

관리자만 가능하다.

관리 메뉴에 들어가면 "모든 페이지의 문자열 일괄 치환" 메뉴가 있고 여기에 들어가면 아래와 같은 화면이 나온다.

Upload:replace3.png

쉽게 알 수 있지만 "morning"이라는 문자열을 찾아서 "evening"으로 고치라는 예이다.

"Just test"가 체크되어 있는 경우는, 실제 저장은 하지 않고 테스트만 한다. 실수를 막기 위해서 기본적으로 체크되어 있다.

"replace" 버튼을 누르면 아래와 같이, 문자열이 있는 페이지들을 찾아서 수정해주며, 페이지 이름(첫번째 빨간 네모), 그 페이지에서 해당되는 문자열의 갯수(두번째 빨간 네모), 실제 변경되는 내용(빨간 동그라미)을 보여준다. 아래 스샷의 경우는 테스트이기 때문에 실제로 저장되지는 않는다.

Upload:replace4.png

각각의 페이지는 minor edit 를 한 것으로 취급된다.

1.2. 치환 예

/문자열일괄치환예시

1.3. 부작용

Old string 과 New string 에 담긴 문자열은 그대로 변수에 담겨서 아래 코드에 의해 치환된다.

            $newText =~ s/$oldStr/$newStr/g;

따라서 각각의 문자열은 Perl/정규표현식의 적용을 받기 때문에, 특수 문자(대표적으로 "/", "?", "*", ".", 등등)가 들어가면 예상하지 못한 동작을 하거나, 서버 에러가 날 수 있다. -_-;; 반대로 잘 쓰면 아주 편리하게 치환을 할 수도 있겠다. 그런데 막상 해보니 펄 코드에 직접 문자열을 적은 것과는 또 다르게 동작을 한다. 이유는 주인장도 모르겠음.

예를 들어서, 치환 코드에서 쓰이는 구분자가 "/"이기 때문에, "2/3"을 "5/6"으로 치환하려면 슬래쉬를 그대로 쓸수가 없고 "\/"처럼 넣어야 한다.
$newText =~ s/2\/3/5\/6/g;

그런데, 위키에서 "2\/3" "5\/6"을 넣으면 "2/3"이 "5\/6"으로 치환되어 버린다. -_-; New string에는 "5/6"이라고 그냥 슬래쉬만 적어주어야 한다.

따라서 특수 문자가 없는 일반적인 영문이나 한글 문자열을 고치는 데는 문제가 없지만, URL이나 프로그램 코드 등을 치환할 때는 주의를 해야 한다.

게다가, 한번 바꾸면 "일괄 undo" 같은 건 없다. -_-;; 그러니 사용할 때 매우 주의를 요함. 두 시간 정도를 더 들여서 테스트 기능과 diff보기 기능을 넣은 이유도, 다수의 페이지가 잘못 치환된 경우를 상상만해도 끔찍하기 때문이다2.

1.4. action/adminmenu.pl 수정

       "<p>".&ScriptLink("action=unlock",T('Removing edit lock')).
        "<p>".&ScriptLink("action=replacetext",T('Replace strings in all pages')).  # 이 줄 추가
       "\n";

1.5. action/replacetext.pl 추가

# replacetext action
# 일괄치환
use strict;

sub action_replacetext {
    print &GetHeader("", T('Replace strings in all pages'), "");
    return  if (!&UserIsAdminOrError());

    my ($oldStr, $newStr, $ignoreCase, $regular, $evaluate, $test);
    $oldStr = &GetParam("old", "");
    $newStr = &GetParam("new", "");
    $ignoreCase = &GetParam("p_ignore", "0");
    $ignoreCase = "1" if ($ignoreCase eq "on");
    $regular = &GetParam("p_regular", "0");
    $regular = "1" if ($regular eq "on");
    $evaluate = &GetParam("p_evaluate", "0");
    $evaluate = "1" if ($evaluate eq "on");
    $test = &GetParam("p_test", "0");
    $test = "1" if ($test eq "on");

# 폼 출력
    print &GetFormStart();
    print &GetHiddenValue("action", "replacetext"),"\n";
    print "<p><b>Old string:</b><br>\n";
    print $q->textfield(-name=>"old",-size=>"100",-maxlength=>"255",-default=>"$oldStr");
    print "<br>\n";
    print $q->checkbox(-name=>"p_ignore", -override=>1, -checked=>$ignoreCase,
                        -label=>T('Ignore case'));
    print $q->checkbox(-name=>"p_regular", -override=>1, -checked=>$regular,
                        -label=>T('Use regular expression'));

    print "\n<p><b>New string:</b><br>\n";
    print $q->textfield(-name=>"new",-size=>"100",-maxlength=>"255",-default=>"$newStr"). "\n";
    print "<br>\n";
    print $q->checkbox(-name=>"p_evaluate", -override=>1, -checked=>$evaluate,
                        -label=>T('Evaluate'));

    print "<p>";
    print $q->submit(-name=>'Replace'), "\n";
    print $q->checkbox(-name=>"p_test", -override=>1, -checked=>1,
                                -label=>T('Just test'));
    print $q->endform;

# old string 값이 없는 경우. 제일 처음 불렸을 때 등
    if ($oldStr eq '') {
        print &GetCommonFooter();
        return;
    }


    if ($test) {
        print "<p>Just test ...<br>\n";
    } else {
        print "<p>Search & replace ...<br>\n";
    }

    my ($page, $num);
    $num = 0;

    $oldStr = "\Q$oldStr\E" if (not $regular);
    $oldStr = "(?i)$oldStr" if ($ignoreCase);

    foreach $page (&AllPagesList()) {       # 모든 페이지 검사
        &OpenPage($page);
        &OpenDefaultText();
        my $newText = $Text{'text'};
        my $match = 0;

# 치환 시도
        if ($evaluate) {
            $match = ($newText =~ s/$oldStr/$newStr/mgoee);
        } else {
            $match = ($newText =~ s/$oldStr/$newStr/mgo);
        }

# 치환되는 것이 있는 경우
        if ($newText ne $Text{'text'}) {
            $num++;
            print "[$num] Processing $page ... $match string(s) were found.<br>";
            print &DiffToHTML(&GetDiff($Text{'text'}, $newText))."<br>";

            if (!$test) {   # 테스트 모드가 아닐 때는 저장
                DoPostMain($newText, $page, "*", $Section{'ts'}, 0, 1, "!!");
            }
        }
    }

    print "Completed.";
    print &GetCommonFooter();
}

1;

1.6. translations/korean.pl 수정

Replace strings in all pages
모든 페이지의 문자열 일괄 치환

1.7. 추가 업데이트 내역

테스트만 할 수 있는 기능. 실제 바뀌는 것을 diff로 확인할 수 있는 기능 추가함.

ext2.1c - 정규표현식 사용 여부를 선택할 수 있게 함. 치환할 때 Evaluate 가능하게 함

1.8. 사용자 의견

"Evaluate" 기능이 진작에 있었으면 Nyxity님이 모놀로그 아카이브 페이지에 있는 매크로들을 일일이 찾아 바꿔주실 걸 좀 편하게 할 수 있게 할 수 있었을텐데... 담에 또 와르르 바꾸셔야 할 일 있으면 미리 말해주세요. :-)
-- Raymundo 2007-3-7 5:19 pm

아 이걸로 가능하지 않을까 하는 생각을 안한건 아닌데.. 역시.. :)
-- Nyxity 2007-3-7 5:49 pm

Great functionality ... i added replace text for individual page:

...
<< $test = "1" if ($test eq "on");
# the next text is only for to save temporarily the variable $id
>> my ($id) = @_;
>> my $fname = "$DataDir/temp/replacetest";

>> if (!-f $fname || (-f $fname && -f &GetPageFile($id))) {&WriteStringToFile($fname, $id);}
>> ($status, $data) = &ReadFile($fname);
# end of save temporarily

...
<< $oldStr = "(?i)$oldStr" if ($ignoreCase);
>> foreach $page (&AllPagesList()) { # ¸ðµç ÆäÀÌÁö °Ë»ç

>> next if ($page ne $data);

I would like "save temporarily ..." on some way more clean ...

-- JuanmaMP
-- 80.58.205.32 2007-6-8 2:43 am

I'm considering to add a feature that user can choose which pages to be replaced, rather than all pages that $oldStr matches. However, I don't know when I will. :-/ I'll refer to your code when I will update this patch. Thank you.
-- Raymundo 2007-6-8 11:27 am

one line more:
<< print "Completed..";

>> if (!$test) {unlink $fname;}

(when it isn't test, unlink the temp. file).

I wonder if with the original sub is possible to obtain the variable $id after a test without make temp. file.
To be continued ... :)

--JuanmaMP
-- 80.58.205.32 2007-6-8 12:46 pm

minor tweak:

+ if (!$num) {
+ print 'there is not matching';
+ }
print '
' . 'Completed ...';
-- JustSameJuanmaMP 2009-8-15 9:25 pm

What do "evaluate" do?. It says: ""Evaluate"에 체크하면 New string에 적힌 것을 Perl expression으로 해석하여 그 결과를 가지고 치환한다.

("Evaluate" New string when the check is written in Perl expression is interpreted as the substitution with the result.).

Evaluate must be check always that Use RegExp is checked too?

I don't get it ... :(

Thanks. --JuanmaMP
-- JustSameJuanmaMP 2009-8-16 10:19 pm

If checked, the value of "New String" field is interpreted as 'Perl expression' rather than simple text. This option is very similar to "eval" option of s///e operator.

Let me show some examples:
1) to replace "111" with "222"
Old string : 111
New string : 222

2) to replace "any sequence of digits" with "aaa"
Old string : \d+ -- this is regexp.
New string : aaa
check Use Regular Expression

3) to replace "any sequence of digits" with "same sequence in parentheses", that is, to enclose numbers with parentheses
Old string : \d+ -- this is regexp.
New string : "($&)" -- this is Perl expression containing a backreference $&.
check Use Regular Expression
check Evaluate

In case 3, aa11bb22 shall be aa(11)bb(22). If you don't check "Evaluate", it shall be aa"($&)"bb"($&).
-- Raymundo 2009-8-16 11:05 pm

Perfect with these threes examples.

Thanks. --JuanmaMP
-- JustSameJuanmaMP 2009-8-17 12:01 am

suggestion: include "Edit Conflict", when start replace
-- JustSameJuanmaMP 2009-8-21 12:31 am

I considered it, but it seemed to be difficult. And I couldn't imagine anyone can manage it if a conflict occurs while he/she is trying to replace several pages at once. :-) Anyway, your suggestion is right.
-- Raymundo 2009-8-21 12:48 am

i am trying to improve mi newbie snippet about replace for single page. This is some help (INPUT forgets $id and this is the remember)

in gotobar, for example:

$result .= ' | ' . &ScriptLink("action=replace&id=$id", T("ReplaceIn"));

in action_replace:

@id = split ('&id=', $ENV{"HTTP_REFERER"});
foreach $page (&AllPagesList()) {
&OpenPage($page);
&OpenDefaultText();
next if ($id[1] && ($page ne $id[1]));
I wonder if "next if" for entire database is expensive load for perl.
Regards.
-- JustSameJuanmaMP 2009-8-21 1:07 am

Thanks for the reply (I did not understand your response if the difficult is in script or is in the probability of occurrence on the users?).

Regards (again :))
-- JustSameJuanmaMP 2009-8-21 1:10 am

I get it, difficult management for users ...
-- JustSameJuanmaMP 2009-8-21 1:31 am

I meant both. :-)

When I was making this patch, I had to consider several things to make it perfect. They include 'managing edit conflict', 'supporting the undo of replacement', 'preventing server error caused by the misuse of regexp' etc.

However, those things required my code to be more complex. And I couldn't find an interface that is so easy that a user can manage such problem without being confused. Moreover, I guessed this patch would not be used frequently. (In fact, I used this function only once after I'd made it three years ago.) Therefore, I decided to give up such supplements and just implemented the essential part.

Is this a enough answer? :-) English is more difficult than Perl! ㅠ,.ㅠ
-- Raymundo 2009-8-21 1:37 am

Yes, perfect and thanks (a good balance of pros and cons).

Another order of difficulty could be: Korean, English and Perl; of course, it depends of mother tongue ... :)
-- JustSameJuanmaMP 2009-8-21 1:43 am

Hi Raymundo, how are you?
this interesting feature could choose one page or a whole site for the action "replace". Adding:

my $pagename;

$pagename = &GetParam("pagename", "");
print "

Page (blank for whole Site):
\n";
print $q->textfield(-name=>"old",-size=>"30%",-maxlength=>"255",- default=>"$pagename");

...
foreach $page (&AllPagesList()) { # 모든 페이지 검사
&OpenPage($page);
&OpenDefaultText();
if ($pagename) {
next if ($page ne $pagename);
}
...

HTH. 감사. --JuanmaMPKr

-- JuanmaMPKr 2012-4-1 7:15 am

(More polished that the old proposal by me @ 2007-6-8 2:43).
감사.
-- JuanmaMPKr 2012-4-1 7:25 am

Thank you. I'll check as soon as possible.
-- Raymundo 2012-4-1 11:50 pm

it's not still a mature proposal, but if action=replace trough evaluate option offers inputs perl expressions (regexp, too?), maybe, it could implementing on action=search. Isn't it?
-- JuanmaMP 2012-6-21 9:14 pm

Currently, regexp is supported in search. (For example, try to search "Use.*Wiki" here)

However, I don't think supporting evaluation is a good idea. It may be a very dangerous security hole. action=replace is allowed only for admin, but search is allowed for anyone.
-- Raymundo 2012-6-21 10:04 pm

Wow!, I didn't notice about "*" feature.
You're right, that's a hole. That reduces the possibilities; then I wonder about a variant of search only for admins. We will see what we can do.
-- JuanmaMP 2012-6-22 12:02 am

And thanks.
-- 88.24.47.50 2012-6-22 12:13 am

Hi Raymundo, nice to typing you; How do you do?
Replace can be extender with "wantword" utility, something like this.
(Only as simple suggestion).

- my ($oldStr, $newStr, $ignoreCase, $regular, $evaluate, $test);
+ my ($oldStr, $newStr, $ignoreCase, $wantWord, $regular, $evaluate, $test);
...
$ignoreCase = &GetParam("p_ignore", "0");
$ignoreCase = "1" if ($ignoreCase eq "on");
+ $wantWord = &GetParam("p_want", "0");
+ $wantWord = "1" if ($wantWord eq "on");
...
print '
' . $q->checkbox(-name=>"p_ignore", -override=>1, -checked=>$ignoreCase, -label=>' ' . T('Ignore case'));
+ print '
' . $q->checkbox(-name=>"p_want", -override=>1, -checked=>$wantWord, -label=>' ' . T('Want Word'));
...
$oldStr = "(?i)$oldStr" if ($ignoreCase);
+ $oldStr = "\\b$oldStr\\b" if ($wantWord);

Regards.
-- JuanmaMP 2012-11-26 4:50 am

Good to see you. It's a good idea. I'll apply this patch soon.
-- Raymundo 2012-11-26 4:42 pm

Hi Raymundo, one more year ... Best for 2013. Although I think "Seollal" is more important?

At least in my script, it seems required:

- $oldStr = "(?i)$oldStr" if ($ignoreCase);
+ $oldStr = "(?i)$oldStr" if (not $ignoreCase);

am I right?
-- JuanmaMP 2013-1-3 5:52 am

Happy new year, JuanmaMP :-)

The answer is No. (?i) option means "ignore case".
-- Raymundo 2013-1-3 9:58 am
이름:  
Homepage:
내용:
 

위키위키분류
각주:
1. 물론 작업하는 시간이 40개의 페이지를 수정하여 링크를 고치는 시간보다는 훨씬 길었다 OTL
2. 다음에는 "특정 시각의 버전으로 모든 페이지를 되돌리기" 같은 기능을 만들어볼까나..

마지막 편집일: 2013-1-3 9:58 am (변경사항 [d])
1725 hits | Permalink | 변경내역 보기 [h] | 페이지 소스 보기