過段時間要把之前的OJ換掉,我負責VirtualJudge的部分。須要用C與PHP寫一個Linux下的VJudge。php
在此以前,將之前寫給本身學弟學妹用的OJ離線題庫的採集程序改進了一下。支持國內一些知名高校的OJ,爲以後VJudge的開發練練手,熟悉下各個OJ的結構,免去之後再在LINUX上進行一些繁瑣的測試。html
題目的採集沒有使用任何OJ的API,直接採起從HTML頁面採集數據並處理的方式。下載HTTP文件使用的是WinINet函數集,用起來比CURL還方便。正則表達式使用的ATL庫裏的regex。題目的儲存使用的文件進行儲存。別看一個OJ就有幾千道題,文件結構訪問的速度倒是至關地快。由於抓取出來的題目有大量的HTML標籤,先要所有去掉很是地困難(一些題目有不一樣的格式與連接標籤,甚至有一些題目是關於標記語言的題目)。由於我如今作的是離線的題庫,因而將這些標籤保留寫來,利用HTML的方式顯示我採集的題目。正則表達式
目前作了六個OJ:算法
一、bnu(北京師範大學OJ):這個OJ我以爲是國內用戶體驗作得最好的一個OJ,本身OJ的題目很豐富(含有較多不錯的中文),VOJ的功能也完美地融合在本身的OJ中。瀏覽器
二、hdu(杭州電子科技大學OJ):擁有國內最強判題機羣,國內各大算法競賽都選在杭電OJ舉行。服務器
三、neu(成都東軟學院OJ):不少人可能都不知道這OJ,也是國內少數登陸須要驗證碼的奇葩OJ,以致於VOJ想支持它都困難,簡單題爆多,歡迎來擼。我母校嘛,必須有,雖然我亞洲區牌都沒拿到個,學校最高也就個銅。補習班性質的比賽嘛,二本學校的學生都不想把耍的時間花在以爲苦逼的事情上,我能拿着塑料堅持那麼久也是NB了。該OJ即將在今年更換新的系統,判題內核、數據管理、界面、以及VOJ都由咱們的老成員完成。但願之後學校的騷年能取得好成績--wchrt。網絡
四、poj(北京大學OJ):搞ICPC的騷年都知道,國內最爲知名的OJ之一。ide
五、vijos(高效信息學在線評測系OJ):裏面全是中文題哦,當心有大量的小學生,分分鐘虐爆你的小學生。函數
六、zoj(浙江大學OJ):國內起步最先的OJ之一,但我基本沒怎麼用過這個OJ。測試
下面根據各oj分別列出了獲取題目內容與標題的正則表達式:
1 ojnum=0; 2 cbox->AddString("bnu"); 3 ojurl[ojnum]="http://www.bnuoj.com/bnuoj/problem_show.php?pid="; 4 ojgetstr[ojnum].allcontent="{<div id=\"showproblem\">(.|\n)*?<div id=\"one_content_base\">}"; 5 ojgetstr[ojnum].title="<div id=\"showproblem\.*?<h1.*?>{.*?}</h1>"; 6 ojgetstr[ojnum].iscode=true; 7 ojgetstr[ojnum].getnum=-1; 8 ojgetstr[ojnum].pnostart=1000; 9 ojnum++; 10 11 cbox->AddString("hdu"); 12 ojurl[ojnum]="http://acm.hdu.edu.cn/showproblem.php?pid="; 13 ojgetstr[ojnum].allcontent="{<h1(.|\n)*?Note</a>}"; 14 ojgetstr[ojnum].title="<h1.*?>{.*?}</h1>"; 15 ojgetstr[ojnum].iscode=false; 16 ojgetstr[ojnum].getnum=-1; 17 ojgetstr[ojnum].pnostart=1000; 18 ojnum++; 19 20 cbox->AddString("neu"); 21 ojurl[ojnum]="http://acm.nsu.edu.cn/JudgeOnline/problem.php?id="; 22 ojgetstr[ojnum].allcontent="{<div id=main(.|\n)*?Sample Output(.|\n)*?BBS(.|\n)*?</a>}"; 23 ojgetstr[ojnum].title="<title.*?>{.*?}</title>"; 24 //目前不須要把內容信息分開,就不正則詳細的題目內容 25 /*ojgetstr[ojnum].tim="Time Limit:{.*?} "; 26 ojgetstr[ojnum].mem="Memory Limit:{.*?}<br>"; 27 ojgetstr[ojnum].des=">Description</h2>.*?<div.*?>{.*?}</div>"; 28 ojgetstr[ojnum].input=">Input</h2>.*?<div.*?>{.*?</div>.*?}</div>"; 29 ojgetstr[ojnum].output=">Output</h2>.*?<div.*?>{.*?}</div>"; 30 ojgetstr[ojnum].sinput=">Sample Input</h2>.*?{<pre>(.|\n)*?</pre>}"; 31 ojgetstr[ojnum].soutput=">Sample Output</h2>.*?{<pre>(.|\n)*?</pre>}";*/ 32 ojgetstr[ojnum].iscode=true; 33 ojgetstr[ojnum].getnum=-1; 34 ojgetstr[ojnum].pnostart=1000; 35 ojnum++; 36 37 cbox->AddString("poj"); 38 ojurl[ojnum]="http://poj.org/problem?id="; 39 ojgetstr[ojnum].allcontent="{<div class=\"ptt\"(.|\n)*?Discuss</a>}"; 40 ojgetstr[ojnum].title="<div class=\"ptt\".*?>{.*?}</div>"; 41 ojgetstr[ojnum].iscode=true; 42 ojgetstr[ojnum].getnum=49;//poj最多隻容許短期訪問49道題 43 ojgetstr[ojnum].pnostart=1000; 44 ojnum++; 45 46 cbox->AddString("vijos"); 47 ojurl[ojnum]="https://vijos.org/p/"; 48 ojgetstr[ojnum].allcontent="{<div class=\"pcontent\">(.|\n)*}<h4"; 49 ojgetstr[ojnum].title="<div class=\"content\">.*?</span>{.*?}<div"; 50 ojgetstr[ojnum].iscode=true; 51 ojgetstr[ojnum].getnum=-1; 52 ojgetstr[ojnum].pnostart=1000; 53 ojnum++; 54 55 cbox->AddString("zoj"); 56 ojurl[ojnum]="http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode="; 57 ojgetstr[ojnum].allcontent="{<div id=\"content_body\">(.|\n)*</div>}"; 58 ojgetstr[ojnum].title="<div id=\"content_body\">.*?>.*?>{.*?}</span>"; 59 ojgetstr[ojnum].iscode=false; 60 ojgetstr[ojnum].getnum=-1; 61 ojgetstr[ojnum].pnostart=1000; 62 ojnum++;
由於不一樣的OJ網站所使用的編碼格式不一樣,我使用的是ansi,因此要把一些使用utf-8的網頁轉換爲ansi,下面是轉換的代碼:
static void UTF8toANSI(CString &strUTF8) { UINT nLen = MultiByteToWideChar(CP_UTF8,NULL,strUTF8,-1,NULL,NULL); WCHAR *wszBuffer = new WCHAR[nLen+1]; nLen = MultiByteToWideChar(CP_UTF8,NULL,strUTF8,-1,wszBuffer,nLen); wszBuffer[nLen] = 0; nLen = WideCharToMultiByte(936,NULL,wszBuffer,-1,NULL,NULL,NULL,NULL); CHAR *szBuffer = new CHAR[nLen+1]; nLen = WideCharToMultiByte(936,NULL,wszBuffer,-1,szBuffer,nLen,NULL,NULL); szBuffer[nLen] = 0; strUTF8 = szBuffer; delete []szBuffer; delete []wszBuffer; }
使用WinINet函數集的CInternetSession與CHttpFile下載HTTP文件,那hdu做爲例子
hdu的1001題的地址是:"http://acm.hdu.edu.cn/showproblem.php?pid=1001"
這裏的http://acm.hdu.edu.cn/showproblem.php是hdu的題目PHP文件,咱們須要以GET的方式請求pid=1001的數據。對於hdu的其餘題目,咱們只需把pid的值設置成其餘的值便可。
//獲取GET方式獲取題目的HTML頁面 CInternetSession intsess; CHttpFile *phtfile = NULL; phtfile = (CHttpFile *)intsess.OpenURL(url); UINT nfilelen = (UINT) phtfile->GetLength(); CString strhtml; CString buffer; UINT dw=0; while(dw<nfilelen)//將數據轉入strhtml處理 { dw+=phtfile->ReadString(buffer); strhtml+=buffer; dw++; } //使用正則表達式提取數須要的內容 data->all=strhtml; getcontent(ojgetstr->allcontent,strhtml,data->allcontent);//去除題目頁面的多與信息 getcontent(ojgetstr->title,strhtml,data->title); /*getcontent(ojgetstr->tim,strhtml,data->tim); getcontent(ojgetstr->mem,strhtml,data->mem); getcontent(ojgetstr->des,strhtml,data->des); getcontent(ojgetstr->input,strhtml,data->input); getcontent(ojgetstr->output,strhtml,data->output); getcontent(ojgetstr->sinput,strhtml,data->sinput); getcontent(ojgetstr->soutput,strhtml,data->soutput);*/ if(ojgetstr->iscode) { UTF8toANSI(data->allcontent); UTF8toANSI(data->title); /*UTF8toANSI(data->tim); UTF8toANSI(data->mem); UTF8toANSI(data->des); UTF8toANSI(data->input); UTF8toANSI(data->output); UTF8toANSI(data->sinput); UTF8toANSI(data->soutput);*/ } if(data->allcontent.GetLength()<5||data->title.GetLength()<1) { return 0; } //將處理後的數據寫入到文件中 CFile file; if(!file.Open("c:\\ojdata\\"+data->oj+"\\"+data->id,CFile::modeWrite)) { if(!file.Open("c:\\ojdata\\"+data->oj+"\\"+data->id,CFile::modeCreate|CFile::modeWrite)) { return 0; } } CArchive ar(&file,CArchive::store); ar<<data; ar.Close(); file.Close();
由於每一個OJ的題目都有幾千道,不可能每次打開軟件都去遍歷那麼幾千個文件,效率過低並且容易出錯。所以我將每一個題目的ID和標題都提取出來,放到一個文件中記錄,用於題目的引索。
引索的結構是:題目數量、id一、標題一、id二、標題二、id3...的格式,每一個OJ一個表。
下面是儲存記錄的代碼,使用序列化儲存:
CFile file; if(!file.Open("c:\\ojdata\\"+oj+"\\pinfo",CFile::modeWrite)) { if(!file.Open("c:\\ojdata\\"+oj+"\\pinfo",CFile::modeCreate|CFile::modeWrite)) { AfxMessageBox("寫入出錯"); return 1; } } CArchive ar(&file,CArchive::store); ar<<that->infonum; for(int i=0;i<that->infonum;i++) { ar<<(&that->problemarr[i]); } ar.Close(); file.Close(); //統計題目下載成功與失敗 str.Format("SECC:%d FAIL:%d",that->infonum,totalnum-50-that->infonum);
hdu的:
全是中文題目的VIJOS,不錯不錯:
水水更健康:
基本上題目的採集工做到此結束。之後作VOJ的時候再將各個部分正則出來。
由於網絡環境的因素,OJ題目的訪問速度不必定很快,這裏能夠用開多個線程進行下載,以及優化題目的下載和處理,由於我能夠直接掛在個人服務器上下載,因此就一條線程下載處理完了。我用的電信50M的寬帶(坑爆,說是50M,上行只有不到2M,電信太煎餅了,一個月169還不包含短信費,80端口封完喊還推薦我開3000一個月的商務寬帶)大概下載一個OJ的全部題目10多分鐘,我直接6個OJ一塊兒下載的。不想等待的朋友能夠直接在附件下載我採集好的題目包,把它解壓到到C盤便可。
採集到了題目,怎麼看呢?確定不能直接給人看,一堆標記語言煩死了。WINDOWS自帶了瀏覽器控件,直接使用它便可,我使用了兩種方式,一種是對話框上的HTML控件,多是個人緣由可是這個控件不穩定, 在一些電腦上老加載出錯。
因而我另外用了CDHtmlDialog。這直接是個HTML的對話框,每次把題目的本地地址給他後讓其Navigate便可。因爲我題目是儲存的一個problemdata結構體,含有一些其餘的信息。打開題目前須要把要顯示的題目提取出來,從新生成一個HTML文件,而後再讓HTML對話框打開它。
題目的顯示過程:
bool ProblemList::opensafeproblem(CString oj,CString pid) { CFile file; if(!file.Open("c:\\ojdata\\"+oj+"\\"+pid,CFile::modeRead)) { return false; } CArchive ar(&file,CArchive::load); problemdata *data; ar>>data; ar.Close(); file.Close(); HtmlContent *contentdlg=new HtmlContent; if(!contentdlg->setdata(*data)) { AfxMessageBox("文件打開失敗"); return false; } contentdlg->Create(IDD_DIALOG_HTML); contentdlg->ShowWindow(SW_SHOW); contentdlg->SetWindowTextA(pid); return true; }
BOOL HtmlContent::OnInitDialog() { CDHtmlDialog::OnInitDialog(); this->SetHostFlags(DOCHOSTUIFLAG_NO3DBORDER | DOCHOSTUIFLAG_FLAT_SCROLLBAR); //將題目轉化爲HTML文件並在本地打開顯示 CFile *f=new CFile; if(!f->Open("c:\\ojdata\\"+data->oj+"\\"+data->id+".html",CFile::modeCreate|CFile::modeWrite)) { AfxMessageBox("打開失敗"); this->CloseWindow(); } f->Write(data->allcontent,data->allcontent.GetLength()); f->Close(); this->Navigate("c:\\ojdata\\"+data->oj+"\\"+data->id+".html"); return TRUE; // 除非將焦點設置到控件,不然返回 TRUE }
整個離線題庫就是這樣,須要支持其餘OJ只要改改正則和題號便可,對於一些無法直接獲取到題目的OJ要作另外的處理,uestc的新OJ就須要另外的方式獲取題目內容。