Author Archives: mix1009

회사 SVN 통계 그래프

얼마전(12월6일)에 TortoiseSVN에서 Merge기능을 사용하다가 새로운 기능을 찾았습니다.
Commit 통계를 그래프로 그려주는 기능이 있더군요! 이제 회사 생긴지도 6년이 넘었고, 연말이고 해서 지난 Commit 통계를 그래프로 그려봤습니다.

회사는 2001년 말에 만들어졌고, APP 개발만 SVN으로 관리하고 있습니다. 원래 CVS로 관리했었는데 작년에 SVN으로 갈아탔습니다. CVS에서 파일단위로 커밋이 관리되는데, SVN으로 가져올때 커밋단위로 변경해서 가져와서 통계내는데도 전혀 문제가 없네요.

먼저 회사의 SVN은 세가지로 관리됩니다. 내부 개발이 Develop, 외부 프로젝트가 Project, 실험 프로젝트가 Test. Test에서 어느정도 쓸만하게 되면 Develop으로 옮겨갑니다. 외부 프로젝트의 경우 외부 저장소에서 관리되는 경우도 있어서 이런 경우는 포함되어 있지 않습니다.

예상외로 그래프가 들쑥날쑥하지는 않네요. 2002년까지는 3명정도, 그 이후로는 4명 정도가 개발인원으로 유지되고 있는거 같네요. 예전엔 외부 프로젝트가 많아서 인당 프로젝트가 할당되서 스트레스 받았었는데, 요즘은 외부 프로젝트를 안해서 그래도 살만합니다.

현재 revision 10948으로 대략 한달에 20일 일한다고 하면 하루 8회 정도 commit이 일어났네요..

아래는 올해 Develop commit 그래프 입니다.

새로 들어온 사람들이 잘 적응하고 있는거 같아서 다행이네요.

아래는 사용자별 그래프입니다. 제가 제일 commit은 많이 하는데… 다른 사람들은 commit하는 주기가 저에 비해서 좀 길어서 그런거 같습니다.


TortoiseCVS에도 이런 기능이 있는지는 모르겠지만 CVS에서 SVN으로 바꾼 이후 후회한적이 거의 없습니다. Trac의 소스보기가 CVSWeb하고 인터페이스가 좀 틀려서 Revision보느거하고 Diff보는게 헷갈렸던거 빼고는 정말 장점이 많네요.

위 그래프를 개발자들에게 보여주니 commit 자주해야겠다고 하네요. commit 횟수가 아닌 commit 라인수 단위로 그래프를 그려볼수 있는 방법이 없나 찾아봐야겠네요.

Visual C++ 컴파일 속도 향상

간만에 글 올리네요…

회사 프로젝트 빌드 시간이 갈수록 길어지고 있었는데, 빌드시간을 아주 많이 줄일수 있는 좋은 글을 찾았습니다. precompiled header를 사용하고 있었지만, 제대로 설정이 안되어있었나봅니다.

제가 VC++ 6.0을 사용하기 때문에 6.0 기준으로 설명합니다.

일단 라이브러리를 제외하고 메인 exe 빌드하는데 시간을 측정했습니다. 측정 방법은 msdev.exe 실행할때 /y3 옵션을 주면 빌드시간이 표시됩니다.

변경하기전 출력입니다.
Build Time 5:21.9

변경후 출력입니다.
Build Time 1:32.8

대략 3.4배 빨라졌습니다.
근데, 팀 개발자들의 반응은 의외였습니다! 빌드시키고 놀시간이 없어졌다고 부정적인 반응을 —

변경 내용은 프로젝트 전체에서 precompiled header 세팅을 새로 했습니다. 원래 Automatic use of precompiled header에서 Use precompiled header file (stdafx.h)로 지정했습니다. 그리고 예외적으로 precompiled header를 안쓰던 몇개의 C(++) 소스들은 파일별로 Not using precompiled header로 설정했습니다.

다음 stdafx.h에 편의상 resource.h와 로그 관련 헤더를 하나 포함했었는데, 프로젝트 관련 헤더는 모두 뺐습니다. 대신 자주 사용하는 STL 헤더들을 stdafx.h에서 include 했습니다.

10분 정도 간단한 변경으로 빌드 속도가 3배 이상 빨라졌습니다. ^^v

lighttpd 모듈

lighttpd 사용하면서 여러 모듈 덕을 많이 보고 있는데, 정리를 좀 해봤습니다.

기본 모듈:

URL 처리 관련 모듈:

보안 모듈:

연동 모듈:

퍼포먼스 향상 모듈:

  • mod_cache : 요청한 내용 파일로 캐싱
  • mod_mem_cache : 요청한 내용 메모리에 캐싱
  • mod_expire : Expire 값 조정. (이미지등 재요청 오랫동안 안하게)
  • mod_compress : 내용 압축해서 보내기 (캐시 파일로 저장)
  • mod_secdownload : URL 암호화, 타임아웃
  • mod_trigger_b4_dl : 특정 URL 방문후 다운로드 가능하도록
  • mod_cml : cache meta language (mod_magnet이 대체)
  • mod_magnet :mod_cml 대신 새로나온 모듈. request 처리 로직 변경할수 있음. 헤더/페이지 조회/수정. Lua 언어로 프로그래밍 가능.

virtual hosting: 설정 파일만으로도 설정 가능.

기타 :

  • mod_useronline : IP별 온라인 사용자수 계산. $_ENV[‘USERS_ONLINE’]에 저장.
  • mod_usertrack : 사용자별 쿠키 할당

lighttpd 1.5에서 부터 지원할것.

boost::format

이번에 프로그램을 중국어로 포팅하면서 죽는 버그가 생겼는데 이 버그 잡느라 많이 삽질했습니다. XP에서 죽을 때 보면 모두 MFC 쪽 스택만 표시되서 UI 쪽이라는 의심은 했었는데 도저히 어딘지는 못찾겠더군요. 바꾼거도 별로 없는데, 이상하게 로그인만 하면 죽는겁니다 TT

Windows 2000에서 디버깅하면 뭔가 나타날줄 알았는데 스택이 똑같더군요 –;

아주 우연히 버그를 발견했는데…. 버그는

[CODE type=C]CString x;
string val = “haha”;
x.Format(“%s”, val);[/HTML][/CODE]

이런 거 였습니다. 뭐 위에 코드가 실행될때 바로 죽는거도 아니고, 몇번 실행되다 보면 이상해지는거죠. printf나 Format 자체가 뒤에 오는 인자를 컴파일러에서 체크할 방법이 없기 때문에 이런 오류는 컴파일러에서 워닝도 표시안해줍니다. 다만 gcc에서는 printf 등의 ANSI 함수에 대해서는 경고 표시는 해주죠.

하여간 아주 간단한 실수 하나로 정말 어의없게 디버깅해도 거의 찾을 수 없는 수준의 버그가 만들어지네요.

소스에서 한글로 되어 있는 부분을 문자열 리소스로 빼는 작업인데, 이를 위해서 GS() 함수와 GSTR() 함수를 만들었습니다. GS는 STL string을 리턴하고, GSTR은 C string을 리턴하도록 정의했습니다. 문자열 리소스 빼고나니 300개 정도 되는데 그중에 하나 실수한거죠.

[CODE type=C]extern CLanguage *gLanguage;

#define GSTR(x) gLanguage->GetString(x).c_str()
#define GS(x) gLanguage->GetString(x)[/HTML][/CODE]

이런 오류는 언제든지 다시 발생할수 있다는 생각에 구글에서 “type safe format”으로 검색해보니, boost에 format 라이브러리가 있더군요. 예전에 서브버전에 등록한 boost 버전에서는 없었던거 같은데… 하여간 라이브러리를 boost 1.34.1로 업데이트했습니다.

boost는 기본적으로 header 파일로만 구성된 template 라이브러리로 따로 컴파일을 할 필요없고, 몇가지 기능(예: regex, python, thread)이 필요하면 라이브러리를 컴파일해야합니다. 기본적인 기능은 거의 헤더 파일로만 구현되어 있어서 라이브러리 컴파일은 하지 않았습니다.

boost::format은 CString::Format이나 printf와 약간 사용법이 틀립니다. Format 등은 떨거지 인자들을 함수 인자로 쭈루루 전달하는데, 이렇게 전달하면 컴파일러에서 type checking을 할수 없기 때문에 문법이 약간 달라졌습니다. 처음에는 생소하지만, 조금만 익숙해지면 불편하지 않게 사용할수 있습니다.

[CODE type=C]#include <boost/format.hpp>

void test_format()
{
   string str1 = (boost::format(“%02d:%s”) % 30 % “hello”).str();
   CString str2;
   str2.Format(“%02d:%s”, 30, “hello”);
   CString str3 = str1.c_str();
}[/HTML][/CODE]

일단, 인자를 잘못 전달한다던가 해서 테스트해봤는데 죽는 경우는 없었습니다.

인자를 잘못 전달하더라도 컴파일러에서는 경고나 오류가 발생하지 않습니다. (VC 6.0 이라 그런지도 모르겠습니다.)

제일 큰 차이점은 인자들을 인자로 넘기는 것이 아니라 % 로 stream 형태로 넘기는 것입니다.
boost::format(“…”)으로 개체가 만들어지고 개체에 % 함수로 하나씩 인자를 넘기는 거죠. format의 인자(%d %s… 등등등)는 printf나 Format과 차이가 거의 없고, 그대로 사용하면 되고, 몇가지 추가 기능도 있습니다.

앞에 format의 타입과 뒤에 %로 전달되는 인자가 틀려도 알아서 잘(?) 표시 해줍니다. 내부적으로 C++ stream (<<)을 이용해서 변환하기 때문에 type이 안맞아도 그냥 동작하는거 같네요. %03d 등은 C++ stream manipulator 등을 이용할듯 하네요. 그래서 manipulator로 스트림 속성을 바꾸는 거기 때문에, 숫자 전달하는곳에 문자열을 전달해도 문제가 되지 않는듯합니다. stream manipulator는 C++ 책이나 http://www.deitel.com/articles/cplusplus_tutorials/20060218/index.html 를 참고하세요
.

시험 삼아 CString을 직접 전달해봤습니다. 죽지는 않고, 포인터 값을 출력하네요.

CString 넘길때마다 GetBuffer(0) 호출하기는 귀찮으니 operator <<를 만들어주면 CString을 직접 넘겨도 됩니다.

[CODE type=C]std::ostream& operator<<(std::ostream& os, const CString &x);[/HTML][/CODE]

구현은 쉬울거 같았는데, casting 때문에 좀 헤맸습니다.

[CODE type=C]std::ostream& operator<<(std::ostream& os, const CString &x)
{
   os << const_cast<CString&>(x).GetBuffer(0);
   return os;
}[/HTML][/CODE]

이렇게 하면 boost::format에 직접 CString을 넘겨도 문자열이 잘 출력됩니다.

사용하기 편리하도록 stdafx.h에 다음 라인들을 추가했습니다.

[CODE type=C]std::ostream& operator<<(std::ostream& os, const CString &x);
#define FORMAT_STRING(fmt, args) (boost::format(fmt) % args).str()
#define FORMAT_CSTRING(fmt, args) (FORMAT_STRING(fmt, args).c_str())
[/HTML][/CODE]

원래 소스를 boost::format으로 바꾸기 위해서는 일단 boost/format.hpp를 include 하고
아래처럼 바꾸면 됩니다.

       CString x;
       x.Format(“%02d 시”, cnt);
       m_hourCtrl.AddString(x);
    →
       m_hourCtrl.AddString(FORMAT_CSTRING(“%02d 시”, cnt));

       SendRequestFormat(“user %s|%s”, userid.c_str(), version.c_str());
    →
       SendRequest(FORMAT_STRING(“user %s|%s”, m_userid % version));

Resource Hacker: 리소스 편집 툴

외국 프로그램 한글화 할때 사용하는 툴인데, 이번에 회사에서 응용프로그램을 중국어로 바꾸는데 사용하고 있습니다. 원래는 언어별로 리소스 DLL 파일을 만들어서 작업했었는데 이번에 새로운 버그(?)가 들어가서 DLL에 있는 리소스를 잘(!–) 못 읽어오는 문제가 발생했는데, 생각처럼 쉽게 해결이 안되더군요.

시간은 없고 해서 다른 방법을 찾아봤는데, Resource Hacker(reshack.exe)에서 일괄 작업등을 지원하여 불편없이 localization에 사용할수 있더군요. 또한 DLL 처럼 컴파일 할 필요도 없고, 번역하는 사람한테 넘겨서 직접 실행해보면서 미비한 점들을 고칠수 있고, 작업 완료후 exe나 res를 넘겨받은다음 필요한 부분만 res 파일로 CVS나 SVN에 저장해두면 나중에 업그레이드 할때도 큰 문제없이 적용할수 있습니다. 아니면 인스톨러 생성에 필요한 모든 파일을 넘기고 번역후 직접 로컬화된 설치 프로그램을 작성하도록 할수도 있습니다.

Resource Hacker는 델파이로 만들어졌고, 개발이 중단된 상태지만, 사용하기에는 큰 불편함이 없습니다. 소스가 공개된 XN Resource Editor도 있지만 문서화도 잘 안되어있고, 완성도가 떨어집니다. XN Resource Editor는 오픈소스이기 때문에 발전가능성이 있긴하죠…

사용자 인터페이스는 그리 복잡하지 않습니다. 다이알로그나 문자열 편집은 오른쪽에 편집창을 이용해서 텍스트로 편집할수 있기 때문에 워드 문서 등에서 직접 Copy&Paste를 할수 있어 편합니다.

exe파일이 바뀔대마다 노가다를 할수 없으므로, 자동화는 반드시 필요합니다. reshack에서는 커맨드라인을 통해서 작업을 시킬수도 있고, -script 옵션으로 스크립트로 여러가지 작업을 한번에 돌릴수 있는 기능도 제공합니다.

한가지 사소한 버그를 찾았는데 파일명이 숫자로 시작하면 오류도 없이 작업하지 않고 종료되더군요… –; 파일명만 바꿔주면 되더군요.

배치파일과 script 파일을 만들었습니다:

ResHacker.exe -script apply_cn.txt
// apply_cn.txt
[FILENAMES]
Exe=      appname.exe
SaveAs=   appname_cn.exe
Log=      appname_cn.log

[COMMANDS]
//-delete   MENU,,0
//-delete   DIALOG,,0
//-delete   STRINGTABLE,,1042
-delete   STRINGTABLE,,1033
-addoverwrite      cn.res, MENU,,2052
-addoverwrite      cn.res, DIALOG,,2052
-addoverwrite      cn.res, STRINGTABLE,,2052

리소스 지정은, 타입(MENU,DIALOG,STRINGTABLE), 아이디(100,101..), 언어(1042,1033) 형태로 나오는데 모든걸 지정하려면 해당 항목을 비워주면 됩니다.

언어는 1033은 영어(US), 1042(한국어) 2052(중국어-simplified).

위에서는 appname.exe에 있는 영문 문자열 리소스를 빼고, cn.res에 있는 메뉴,다이알로그,문자열 리소스를 추가하여 appname_cn.exe로 저장합니다.

이메일 보호하기

유튜브에서 이 비디오를 보고, 상식적으로 생각하던것과 틀려서 글 남겨봅니다.

블로그나 게시판 등에서 자신의 이메일을 보호하기 위해서 userid at gmail dot com 등으로 풀어서 쓰는 경우를 종종 보는데, 이게 오히려 스패머들에게는 도움이 된다는 내용입니다. 구글 등의 검색엔진에서 일반 이메일을 찾기보다 저렇게 풀어쓴 이메일을 찾기가 더 쉽다는 거죠. at이나 dot 대신 한글로 써도 틀리지 않을거라 생각이 되네요.

앞으로는 이메일 쓸일이 있으면 이메일 이미지를 써야겠네요.
http://services.nexodyne.com/email/index.php
저도 예전에 위 사이트에서 만들었던거 같은데.. 이 서비스도 많이 사용하면, 이미지에서 이메일을 추출하는 툴들이 나오지 않을까 걱정됩니다. 혹시를 위해서 파일명에 mail 등이 들어가지 않게하는게 좋을거 같네요.

한글 도메인도 되는 저런 이미지 만들어주는 사이트 만들면 사람들이 좋아할거 같네요. 만들기 어렵지 않을테니 시간나면 ㅎㅎ

Google Reader에 검색 기능 추가, 아이팟 새모델

구글 리더 사용하면서 가장 불편했던 점이 검색이 안되는거였는데, 검색기능이 추가되었네요. 검색 전문 회사가 제공하는 서비스에 검색이 없었다니 좀 이상했죠…

그리고 다른 소식 하나, 아이팟 새 모델들이 나온다고 하네요.
아이폰에서 핸드폰 기능을 제거한 아이팟 터치가 꽤 괜찮아 보이네요.
아이팟 터치에서 pdf 볼수 있는 프로그램이 있다면 꽤 쓸만하겠네요…

CAPTCHA와 reCAPTCHA

웹페이지 돌아다니다가 글 쓰려고 하면 스팸 방지 등을 위해서 그림으로 문자들을 보여주고 문자를 사용자가 입력하여 확인하는 경우가 있습니다.

최근에 제가 rapidshare.com와 badongo.com에서 자료(?)를 받는데 일반 사용자는 다운로드 받을때 그림을 보고 입력하는 부분이 있습니다. 프로그램으로 기계적으로 다운로드해서 네트워크 부하를 많이 일으키지 못하도록 하는거죠. 이미지는 아래와 같이 생겼고, rapidshare에서 사용하는 이미지는 글자의 왜곡이 거의 없네요.


프로그램적으로 저런 이미지를 읽기가 얼마나 어려운지 모르겠지만, 간단히 이미지에서 글을 읽기 힘들기 때문에 조금더 어렵게 하는 의미는 있겠죠.

위와 같은 기술을 CAPTCHA라고 하는데, 위키피디아에서 많은 정보를 얻을수 있습니다. CAPTCHA는 Completely Automated Public Turing Test to tell Computers and Humans Apart의 약자라고 하는데, 컴퓨터와 사람을 구별하는 완전히 자동화된 테스트 정도 되겠네요. 약자를 그럴듯하게 만들기 위해서 약간 부가적인 단어가 들어간듯 싶습니다. CMU에서 만든 용어고 CMU에서 많이 연구를 했나봅니다.

이미지를 복잡하게 만들면 문자를 읽어내는 프로그램을 만들기가 더 힘들지만, 사람 역시 문자를 판독하기가 어려워진다고 합니다. 일반적인 홈페이지에서는 CAPTCHA를 도입하는 것만으로도 악의적인 사용자의 공격을 어렵게 만들수 있지만, 사이트를 공격할만한 가치가 크다면 문자를 판독하는 알고리즘을 개발할수도 있고, relay 공격으로 CAPTCHA 문제들을 자동화해서 풀수도 있습니다. relay 방법은 알고리즘을 통해서 문자를 읽는것이 아니라, 사용자가 어느정도 있는 사이트를 악의적인 사용자가 운영하고 있다면, 사용자들이 로그인하거나 가입할때 목표로 하는 CAPTCHA 문제를 받아와서 사용자가 풀게하여 목표 사이트에 가입이나 스팸성 글을 올리는 방법입니다. 아무리 CAPTCHA를 어렵게 만들어도 relay 공격 방법은 막을수 없죠.

어떻게 계산했는지 모르겠지만, 전세계적으로 매일 대략 150,000 시간이 CAPTCHA를 푸는데 소비(낭비)된다고 합니다! 이렇게 허비되는 시간을 좀더 유용하게 사용하기 위한 프로젝트가 reCAPTCHA입니다. 역시 CMU 프로젝트이고, CAPTCHA 문제를 만들때 임의로 생성하는 것이 아니라, 오래된 문서에서 OCR로 판독이 안되는 부분을 문제로 내는겁니다. 하나의 문제만 내면 사용자가 정답을 입력했는지 알 방법이 없기 때문에, 두개의 CAPTCHA 문제를 내고, 사용자들의 입력 결과를 통계를 내서 오랜된 문서의 디지털화에 도움을 줍니다. 정말 반짝이는 아이디어네요. 아래는 reCAPTCHA의 예제입니다.


시간이 있으면 Tattertools 플러그인을 제작하면 좋을것 같네요. 찾아보니 없는거 같네요. 그리고 트랙백 받을때도 어떻게 활용할 방법이 없을까하는 생각도 드네요. 가끔 트래백으로 스팸성 댓글이 많이 달리거든요.

VMware 상장, Xen 피인수

굴직한 Virtualization 회사들의 소식입니다.

VMware가 2007년 8월 14일 상장했습니다. 상장한지 하루만에 78% 올라서 시가 총액이 $19 billion (약 19조원) 되었다고 하네요.

http://biz.yahoo.com/ap/070814/vmware_ipo.html?.v=22

창업한지 3년된 Xen-Source를 Citrix 에서 $500 million(약 500억원)에 인수했다고 합니다.

http://venturebeat.com/2007/08/15/citrix-acquires-xensource-for-500m-in-virtualization-frenzy/

VMware가 Virtualization 분야에선 아직 절대강자이긴 하지만, 금액은 많이 비교가 되네요…

XPath & libxml2

XML은 꽤 오래전부터 사용했지만, 여러가지 복잡한 용어들이 나오면서 좀 멀어졌던 느낌이었는데, XPath는 정말 프로그래머에게 유용한 툴인것 같네요.

XPath는 XML 문서에서 쉽게 element를 찾는 API로 쿼리를 문자열로 넘기면 조건에 맞는 element나 element 리스트를 반환하게 됩니다. 1.0 버전이 있고 2.0 버전이 최근에 나왔습니다. 아직까지는 라이브러리들이 1.0 기반이 대부분입니다.

쿼리는 예제로 살펴보는것이 빠른듯하네요.

“A/B/C” : A element 밑에 B element 밑에 C element들은 찾을때
“/A/B/C” : 위와 같지만 A가 최상위 element.
“/A/B/C[1]” : C element중 첫번째
“/A/B/C[2]” : C element중 두번째
“//C” : 모든 C element
“B//C” : B 하위에 있는 C element
“A/B/*” : A element 밑에 B element 바로 밑의 모든 element
“A/B//*” : A element 밑에 B element 밑의 모든 element (하위 element 포함)
“//*” : 문서의 모든 element

libxml2라는 C 라이브러리가 있지만 python wrapper를 이용하여 python에서 위의 쿼리들을 돌려봤습니다. 테스트해본 결과 /로 시작하지 않는 쿼리들은 제대로 동작하지 않더군요. (검색되는 결과가 없음) 이런 쿼리들은 앞에 //를 붙여주면 제대로 동작합니다.

아래는 python 소스입니다. 중간에 예제 XML을 보기 좋게(?) 들여쓰기했놨지만 출력할때 한줄로 볼수 있도록 xml에서 공백과 newline을 제거합니다.

[CODE type=python]
import libxml2

def xpathElements(ctxt, query):
   if query[0] == ‘/’:
       print “\”%s\”” % query
   else:
       print “\”%s\” -> \”//%s\”” % (query, query)
       query = “//” + query
   res = ctxt.xpathEval(query)
   for e in res:
       print ”    %s (%s)” % (e.name, e)

xml = “””
<A>
   <B>
       <C id=’c1’/>
       <C id=’c2′>
           <D/>
       </C>
       <E>
           <F/>
           <A>
               <B>
                   <C/>
               </B>
           </A>
       </E>
   </B>
</A>”””

xml = ”.join([l.strip() for l in xml.splitlines()])

doc = libxml2.parseDoc(xml)

ctxt = doc.xpathNewContext()

xpathElements(ctxt, “A/B/C”)
xpathElements(ctxt, “/A/B/C”)
xpathElements(ctxt, “/A/B/C[1]”)
xpathElements(ctxt, “/A/B/C[2]”)
xpathElements(ctxt, “/A/B/C[3]”)
xpathElements(ctxt, “//C”)
xpathElements(ctxt, “//B//C”)
xpathElements(ctxt, “A/B/*”)
xpathElements(ctxt, “A/B//*”)
xpathElements(ctxt, “//*”)
[/HTML][/CODE]

다음은 실행결과입니다.

“A/B/C” -> “//A/B/C”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   C (<C/>)
“/A/B/C”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
“/A/B/C[1]”
   C (<C id=”c1″/>)
“/A/B/C[2]”
   C (<C id=”c2″><D/></C>)
“/A/B/C[3]”
“//C”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   C (<C/>)
“//B//C”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   C (<C/>)
“A/B/*” -> “//A/B/*”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   E (<E><F/><A><B><C/></B></A></E>)
   C (<C/>)
“A/B//*” -> “//A/B//*”
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   D (<D/>)
   E (<E><F/><A><B><C/></B></A></E>)
   F (<F/>)
   A (<A><B><C/></B></A>)
   B (<B><C/></B>)
   C (<C/>)
“//*”
   A (<A><B><C id=”c1″/><C id=”c2″><D/></C><E><F/><A><B><C/></B></A></E></B></A>)
   B (<B><C id=”c1″/><C id=”c2″><D/></C><E><F/><A><B><C/></B></A></E></B>)
   C (<C id=”c1″/>)
   C (<C id=”c2″><D/></C>)
   D (<D/>)
   E (<E><F/><A><B><C/></B></A></E>)
   F (<F/>)
   A (<A><B><C/></B></A>)
   B (<B><C/></B>)
   C (<C/>)