OnlineJudge 離線題庫采集


  過段時間要把以前的OJ換掉,我負責VirtualJudge的部分。需要用C與PHP寫一個Linux下的VJudge。

    在此之前,將以前寫給自己學弟學妹用的OJ離線題庫的采集程序改進了一下。支持國內一些知名高校的OJ,為之后VJudge的開發練練手,熟悉下各個OJ的結構,免去以后再在LINUX上進行一些繁瑣的測試。

    題目的采集沒有使用任何OJ的API,直接采取從HTML頁面采集數據並處理的方式。下載HTTP文件使用的是WinINet函數集,用起來比CURL還方便。正則表達式使用的ATL庫里的regex。題目的儲存使用的文件進行儲存。別看一個OJ就有幾千道題,文件結構訪問的速度卻是相當地快。因為抓取出來的題目有大量的HTML標簽,先要全部去掉非常地困難(一些題目有不同的格式與鏈接標簽,甚至有一些題目是關於標記語言的題目)。因為我現在做的是離線的題庫,於是將這些標簽保留寫來,利用HTML的方式顯示我采集的題目。

 

    目前做了六個OJ:

    1、bnu(北京師范大學OJ):這個OJ我覺得是國內用戶體驗做得最好的一個OJ,自己OJ的題目很豐富(含有較多不錯的中文),VOJ的功能也完美地融合在自己的OJ中。

    2、hdu(杭州電子科技大學OJ):擁有國內最強判題機群,國內各大算法競賽都選在杭電OJ舉行。

    3、neu(成都東軟學院OJ):很多人可能都不知道這OJ,也是國內少數登錄需要驗證碼的奇葩OJ,以至於VOJ想支持它都困難,簡單題爆多,歡迎來擼。我母校嘛,必須有,雖然我亞洲區牌都沒拿到個,學校最高也就個銅。補習班性質的比賽嘛,二本學校的學生都不想把耍的時間花在覺得苦逼的事情上,我能拿着塑料堅持那么久也是NB了。該OJ即將在今年更換新的系統,判題內核、數據管理、界面、以及VOJ都由我們的老成員完成。希望以后學校的騷年能取得好成績--wchrt。

    4、poj(北京大學OJ):搞ICPC的騷年都知道,國內最為知名的OJ之一。

    5、vijos(高效信息學在線評測系OJ):里面全是中文題哦,小心有大量的小學生,分分鍾虐爆你的小學生。

    6、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:{.*?}&nbsp";
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和標題都提取出來,放到一個文件中記錄,用於題目的引索。

引索的結構是:題目數量、id1、標題1、id2、標題2、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就需要另外的方式獲取題目內容。

    采集程序源碼

    采集程序

    OJ題庫下載


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM