过段时间要把之前的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就须要另外的方式获取题目内容。