去除c中的註釋

最近老師給了一道題啊...叫我們寫一個掃描器,讓它可以去除空白,然後可以去除註釋,把結果導入另外的文件裏啊....


答:空白就檢查吧。註釋用stack匹配/**/檢查//去行

 

去掉C/C++程序代碼中的註釋

C知識點 2009-12-27 17:36:04 閱讀106 評論0 字號:

程序員面試寶典的題目解析方法基本上沒什麼問題,但是答案中有很多紕漏,甚至有些事錯誤的,這是很多有一定編程經驗的程序員看過那本書得出的共同結論。

有錯誤不怕,我們學習的人一定要懂,畢竟那不是教材嘛,自己要把c基礎打好,上面有很多經典問題供我們思考,而且會對我們面試有很大好處。不過讀者一定要相信,這本書不可能提高你的C基礎水平。

下面是第45頁的一個題目,其中有一處錯誤,我將那句話註釋掉了,寫了一句正確的代碼,且給了原因分析,編譯通過,可以去掉程序代碼中的註釋語句。如果還有不嚴謹的部分,盼指爲幸!

===============================================================================

        下面的代碼比較複雜一些,請分段思考。

        另外要聯繫思考:在""和''中間的雙斜槓中第一個斜槓在"和'後面判斷就跳過了,第二次是讓case語句來處理,然而case的第一個if就是依靠前面是否已經探測到過"或',探測到了的話,要跳過。

        還要考慮到非註釋中的單引號''中間只能有一個/,否則C語句非法。

===============================================================================

/********************************************************
 功能:去除C/C++中的註釋
 輸入:指向C/C++程序代碼的指針
 來源:程序員面試寶典第45頁
 注意:①要考慮到""或' '中的//和/*,//和/*的嵌套關係。
    ②單引號、雙引號中的//是兩個字符,第一個字符在單引號的case語句中跳過了,
    第二個字符則在case '/'中處理。
 *********************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>

void remove_comment(char *buf, size_t size)
{
 char *p, *end, c;          //p-動態移動的字符指針,end-指向文件末尾的字符指針,c-存儲沒一個p指向的字符
 char *sq_start, *dq_start; //sq_start-單引號開始位置(single),dq_start-雙引號開始(double)
 char *lc_start, *bc_start; //lc_start-//的開始位置,bc_start-/*的開始位置
 size_t len;                //記錄某符號結束和開始的位置之差(長度,偏移量)
 
 p = buf;
 end = p + size;
 sq_start = dq_start = NULL;
 lc_start = bc_start = NULL;
 
 while (p < end) /*當指針沒有到達文件末尾   /r///***///*/,故意帶這些不規範的符號的,因爲調試就用這個代碼,哈哈。*/
 {
  c = *p;     //用字符變量c存儲指針指向的字符
  
  switch (c) //根據c的值做相應處理
  {
   case '/'': /*處理單引號,其實只是爲了排除'//'的情況,否則不需要有這個情況判斷*/
   {
    if (dq_start || lc_start || bc_start) //當遇到過雙引號、//或/*的時候,則不需要再判斷'//'的情況了。
    {
     p++;
     continue; //繼續下一個,對while而言的
    }
    /*******************************以下是沒有遇到過雙引號或//或/*的時候*******************************/
    if (sq_start == NULL) /****否則:如果未遇到單引號****/
    {
     sq_start = p++; //start指向單引號的開始位置,p指向下一個(分兩句理解)
    }
    else /*如果遇到過單引號,sq_start指向單引號開始位置*/
    {
     len = (p++) -sq_start; //len = p-sq_start; p++;
     if (len == 2 && *(sq_start+1) == '//') //這個是將遇到'//'的情況排除
     {
      continue; //忽略單引號中單獨存在//的時候,即不再往下處理。
     }
     
     sq_start = NULL; //否則將sq_start置位爲NULL,C語言中單引號內最多隻可能有一個字符而已,不要思考複雜了哦len == 0 或 1 或 2
    }
    /*******************************以上是沒有遇到過雙引號或//或/*的時候*******************************/
    break;
   }
   
   case '/"': /*處理雙引號,其實只也是爲了排除"//"的情況,否則不需要有這個情況判斷,注意第二個斜槓不在這裏判斷*/
   {
    if (sq_start || lc_start || bc_start) //當遇到過單引號、//或/*的時候,則不需要處理
    {
     p++;
     continue;
    }
    /*****************以下是沒有遇到過單引號或//或/*的時候*****************/
    if (dq_start == NULL) /*如果沒有遇到過雙引號*/
    {
     dq_start = p++; //標記遇到了雙引號
    }
    else if (*((p++) -1) =='//') //雙引號中的/也不需要處理。
    {
     continue;
    }
    printf("hello // world?? /**/"); //這種情況呢?怎麼辦?——這個情況會在遇到/是的第一個if語句被跳過
    
    
    dq_start = NULL; //如果雙引號中不是//,標記爲NULL
    /*****************以上是沒有遇到過單引號或//或/*的時候*****************/
   }
   
   case '/': //斜槓,注意這個斜槓也可以是'//',"//",//,/*/中的第二個斜槓,但會在下面第二行代碼中被忽略掉
   {
    if (sq_start || dq_start || lc_start || bc_start) //如果是單引號、雙引號、斜槓、/*的後面
    {
     p++;
     continue;
    }
    /***********************下面是遇到註釋//或/*的時候****************************/
    c = *(p + 1); //否則c取p指向字符的下一個字符
    if (c == '/') //遇到了雙斜槓
    {
     lc_start = p; //標記雙斜槓的開始
     p += 2; //p指向雙斜槓後面的字符
    }
    else if (c == '*') //遇到了/*
    {
     bc_start = p; //標記/*的開始
     p += 2; //p指向/*後面的字符
    }
    /*************************上面是遇到註釋//或/*的時候**************************/
    else
    { //其它情況,再去判斷下一個是什麼符號——注意:C程序可以有其他情況嗎?這句話我認爲永遠不可能執行到。
     p++;
    }
   }
   
   case '*': //星號,同斜槓,但少了如果遇到/*的情況,因爲遇到這種情況後,要判斷是不是遇到結束的地方*/了
   {
    if (sq_start || dq_start || lc_start) //如果是單引號、雙引號、斜槓、/*的後面
    {
     p++;
     continue;
    }
    
    if (*(p + 1) != '/') //如果星號後面緊跟的不是斜槓,那麼忽略過。
    {
     p++;
     continue;
    }
    
    p += 2; //否則p指向斜槓後面那個字符。注意下面的清空語句,p指向的那個字符並不會被清除。
    memset(bc_start, ' ', p-bc_start); //清空/* …… */中間的內容包括註釋符號本身。
    bc_start = NULL;
    break;
   }
   
   case '/n': /*換行符,主要處理遇到雙斜槓時,需要清除雙斜槓到/n的前面的字符*/
   {
    if (lc_start == NULL) //如果還沒有遇到雙斜槓,那麼忽略
    {
     p++;
     continue; /*這兩行本程序每次case後面都緊跟,就是忽略過的意思*/
    }
    
    c = *(p - 1);
    //如果遇到過雙斜槓,清空雙斜槓本身和到n前面的那個字符,p指向下一個字符,/r是回車符(光標退回到最前面),要忽略。有這個情況嗎???
    memset(lc_start, ' ', (c == '/r'? ((p++) -1) : p++) - lc_start);
    lc_start = NULL;
    break;
   }
   
   default:
    p++;
    break;
  }
  /****************************************************
  如果遇到雙斜槓,這個if語句存在的意義在於萬一最後
  一行代碼是帶有雙斜槓但沒有給換行符/n的,也要清除掉。
  不帶文件末尾的雙斜槓的行尾一定有/n,這不是代碼中寫的
  /n而是我們的回車鍵換行操作寫入文件的。
  *****************************************************/
  if (lc_start)
  {
   memset(lc_start, ' ', p - lc_start);
  }
 }
}

/**********************************************
   main函數的開始
***********************************************/
int main (int argc, char *argv[])
{
 int fd, n;
 char buf[102400];
 
 if (argc != 2)
 {
  printf("command error: Input as ./command <file>/n");
 }
 
 fd = open(argv[1], O_RDONLY); /*只讀打開*/
 if (fd == -1)
 {
  return -1;
 }
 
 n = read(fd, buf, sizeof(buf));
 if (n == -1 || n == 0)
 {
  close(fd);
  return -1;
 }
 printf("test/n");
 remove_comment(buf, n);
 *(buf + n) = '/0';
 printf("%s", buf);
 close(fd);
 
 return 0;
}

 

二叉樹——根據遍歷結果,畫出對應的二叉樹

數據結構 2009-12-26 12:24:49 閱讀79 評論0 字號:

這道題目很經典,具體如下:

已知遍歷結果如下,試畫出對應的二叉樹:

前序:A B C E H F I J D G K

中序:A H E C I F J B D K G

解題要點:

1、前序、中序、後序——都針對中間那個節點而言(根節點也是中間的節點)。

前序,指先遍歷中間節點,然後左,然後右。

中序,指左——中——右。

後序,指右——中——左。

2、根據兩種不同序列的遍歷方法,便可畫出二叉樹。

解題答案如下:(對照着看會好理解這道題目一些的)

二叉樹——根據遍歷結果,畫出對應的二叉樹 - canlynet - DC的嵌入式筆記

解題思路:

1、前序中序都首先找出A,推斷出:A沒有左孩子(否則中序要先遍歷其左孩子)。A爲根節點(前序先遍歷中節點)。

2、A沒有左孩子,一定就只有右孩子了(否則哪來那麼多後面的字符)。右孩子一定是B,根據前序結果(前序先遍歷中節點)。

3、中序的第二個不是B,說明B一定有左孩子(中序先遍歷左孩子)。既然有左孩子,那麼在前序的B後面那位一定是啦——C,C有左孩子嗎?一定有,否則中序的A後面就跟C啦。那麼前序的C後面的E一定是C的左孩子(還是深入講一下:前序是先遍歷左,但左還有左的時候,得先遍歷左的左,一直深入下去,找到最終的那個左節點,這個一定要明白,否則此題無法解開)。

4、同理:E也有左孩子,就是前序的E後面那位:H!好,既然中序的A後面是H,H一定沒有左孩子啦。

5、下面怎麼辦呢?前序:H後面是F,中序:H後面的E、C已經確定了位置了,再後面是I,怎麼辦?

6、注意:中序H後面是E,說明H沒有右孩子啊!(中序遍歷方法!!)同理,E後面是C說明E也沒有右孩子,C後面呢,是B嗎?如果是,那麼C也沒有,驚喜了吧,中序當中C後面是I,說明C有右孩子,是誰呢?

7、既然C後面有右孩子,那麼在前序中H後面的F一定就是我們要找的右孩子啦(因爲前序是找中節點,然後找左孩子,左孩子中又先找中間節點,再找左孩子的左孩子……中——左——右,嵌套思想)。

8、F確定好位置後,那麼考慮F有左孩子嗎?如果沒有,中序的C後面應該跟F,但是中序的C後面跟I,說明F有左節點,那麼前序找完F一定要找到這個左節點了,就是說前序的F後面的I一定是F的左節點。

9、I有左孩子嗎?中序的C後面跟的是I,說明I就是最終的左孩子了。

10、F有右孩子嗎?如果沒有,中序I的下一位應該是B,可是並非,所以它有右孩子,那麼前序的I後面的J一定是這個右孩子。

11、J有左孩子嗎?如果有,中序的F後面不能是J,可是並非如此,所以沒有了。J有右孩子嗎?如果有,中序的已經找過J啦,下一位就是找右孩子,所以看中序的J後面那一位,發現是B,B不是已經確定好位置了嗎,所以啊,沒有啦。回到B節點了。

12、前序的J後面的D一定就是B的右孩子(因爲回到B了嘛,前序要先把B的右孩子當中節點找到,再去找右孩子的左孩子,再把左孩子當中節點找到……嵌套啊)。

13、D有左孩子嗎?如果有,中序的B後面就不可能是D(因爲中序要先找到最最最最最左邊的那個啊)。所以沒有。既然沒有,那麼前序就要找D的右孩子啦,那就是前序的D後面那個G。

14、G有左孩子嗎?如果沒有的話,中序的D後面一定是G啦,可是不是,所以它有左孩子,這個左孩子就是前序在找到G後面該找到的那位了,就是K。

OK,二叉樹的圖畫出來了。

總結二叉樹查找關鍵點:

1、每次都要假設,假設了立即推翻,如果假設有,發現不能成立的話,就推翻了這個假設,即確定了一個節點的存在與否。

2、一定要明白前序、中序、後序的遍歷方法,要透徹的領悟到嵌套的思想,比如找左孩子,結果找到的左孩子還有左孩子,那麼要先找左孩子的左孩子,如果左孩子的左孩子還有左孩子,那麼……以此類推。

3、假設一定就一次,便可深入,否則就帶有一定的猜測,然後證實的成分了,容易暈掉。

4、每次確定好一個位置都是100%確定的,所以畫出來,不要猶豫,然後在前序、中序上做相應標記,知道自己下一個該確定那個字符的位置了。

5、解這個題目,一定不能着急,心要穩,別深入,沒有更好的深入的方法(我沒找到而已,哈哈)。每次找到一個節點就直接問,他有左孩子嗎?如果有,那麼在中序的查找中一定就該找他了,結果發現不是,所以推翻。如果沒有,那麼在前序一定就該找到這個節點了。方法就這一個而已,可以解開題目。

6、這個方法對後序查找也生效。

數據結構經典問題——出棧順序

數據結構 2009-12-26 11:28:59 閱讀109 評論2 字號:

對於數據結構的問題,如果思路稍有不對,就容易陷入邏輯混亂。我希望自己對數據結構的理解,能夠給大家一點幫助。我會將所有我有過心得的問題在我的博客上寫出來,歡迎大家瀏覽,如果有什麼不對的地方,還請大家指正,有問題可以給我留言,我會盡量解決,謝謝。

聲明一下我寫博客的初衷:不是炫耀,而是回報。因爲我在計算機方面的知識好多都從網上找到答案,因此我也

將自己搜尋整理的材料,自己寫的材料,展示到網上,算是盡一份力吧。

一個經典問題如下(不願意看思路的可以直接看紅色字體部分):

一個棧的入棧序列是a,b,c,d,e則棧的不可能的輸出序列是:()

A edcbd         B decba           C dceab            D abcde

棧之根本——先進後出(first in,lastout)初次接觸到這個問題的人,或許會認爲入棧abcde,所以出棧只能是edcba所以BCD都不對。

其實是這個問題描述有歧義,應該是分段入棧的順序,也就是說,可能先入棧a,取出a,入棧b,取出b……,所以D也是可能的。

知道這個意思了以後,就要明確這個問題的矛盾根本所在:第一次出棧d,說明什麼?說明a,b,c一定早已入棧(入棧順序決定的)。那麼在出棧d以後,a,b,c的出棧順序一定是c,b,a,而不用理會中間穿插着出棧了d後面的字符(因爲可以再入棧,再出棧嘛)。所以立即選中C,不用猶豫,理由簡單:d出棧了,abc一定已經入棧,那麼abc只能以cba的順序出棧,C不符合,OK!This problem is so esay, Thanks for my teacher Wang Shanshan.

數據結構中的思路一定要單一,要簡明,抓住問題根本,萬不可考慮過多,試探法只有在不知道解題思路的情況下去嘗試,嘗試多了,你會發現,邏輯容易混亂,俗稱“我暈”!

 

去除C/C++中的註釋

文章分類:C++編程

C代碼 複製代碼
  1. /********************************************************  
  2.     功能:去除C/C++中的註釋  
  3.     輸入:指向C/C++程序代碼的指針  
  4.     來源:程序員面試寶典第45頁  
  5.     注意:①要考慮到""或' '中的//和/*,//和/*的嵌套關係。  
  6.           ②單引號、雙引號中的//是兩個字符,第一個字符在單引號的case語句中跳過了,  
  7.           第二個字符則在case '/'中處理。  
  8.  *********************************************************/  
  9.   
  10. #include <stdio.h>   
  11. #include <stdlib.h>   
  12. #include <fcntl.h>   
  13. #include <string.h>   
  14. #include <unistd.h>   
  15. #include <sys/types.h>   
  16.   
  17. void remove_comment(char *buf, size_t size)   
  18. {   
  19.     char *p, *end, c;          //p-動態移動的字符指針,end-指向文件末尾的字符指針,c-存儲沒一個p指向的字符   
  20.     char *sq_start, *dq_start; //sq_start-單引號開始位置(single),dq_start-雙引號開始(double)   
  21.     char *lc_start, *bc_start; //lc_start-//的開始位置,bc_start-/*的開始位置   
  22.     size_t len;                //記錄某符號結束和開始的位置之差(長度,偏移量)   
  23.        
  24.     p = buf;   
  25.     end = p + size;   
  26.     sq_start = dq_start = NULL;   
  27.     lc_start = bc_start = NULL;   
  28.        
  29.     while (p < end) /*當指針沒有到達文件末尾   /r///***///*/,故意帶這些不規範的符號的,因爲調試就用這個代碼,哈哈。*/   
  30.     {   
  31.         c = *p;     //用字符變量c存儲指針指向的字符   
  32.            
  33.         switch (c) //根據c的值做相應處理   
  34.         {   
  35.             case '/''/*處理單引號,其實只是爲了排除'//'的情況,否則不需要有這個情況判斷*/  
  36.             {   
  37.                 if (dq_start || lc_start || bc_start) //當遇到過雙引號、//或/*的時候,則不需要再判斷'//'的情況了。   
  38.                 {   
  39.                     p++;   
  40.                     continue//繼續下一個,對while而言的   
  41.                 }   
  42.                 /*******************************以下是沒有遇到過雙引號或//或/*的時候*******************************/   
  43.                 if (sq_start == NULL) /****否則:如果未遇到單引號****/  
  44.                 {   
  45.                     sq_start = p++; //start指向單引號的開始位置,p指向下一個(分兩句理解)   
  46.                 }   
  47.                 else /*如果遇到過單引號,sq_start指向單引號開始位置*/  
  48.                 {   
  49.                     len = (p++) -sq_start; //len = p-sq_start; p++;   
  50.                     if (len == 2 && *(sq_start+1) == '//') //這個是將遇到'//'的情況排除   
  51.                     {   
  52.                         continue//忽略單引號中單獨存在//的時候,即不再往下處理。   
  53.                     }   
  54.                        
  55.                     sq_start = NULL; //否則將sq_start置位爲NULL,C語言中單引號內最多隻可能有一個字符而已,不要思考複雜了哦len == 0 或 1 或 2   
  56.                 }   
  57.                 /*******************************以上是沒有遇到過雙引號或//或/*的時候*******************************/  
  58.                 break;   
  59.             }   
  60.                
  61.             case '/"'/*處理雙引號,其實只也是爲了排除"//"的情況,否則不需要有這個情況判斷,注意第二個斜槓不在這裏判斷*/  
  62.             {   
  63.                 if (sq_start || lc_start || bc_start) //當遇到過單引號、//或/*的時候,則不需要處理   
  64.                 {   
  65.                     p++;   
  66.                     continue;   
  67.                 }   
  68.                 /*****************以下是沒有遇到過單引號或//或/*的時候*****************/   
  69.                 if (dq_start == NULL) /*如果沒有遇到過雙引號*/  
  70.                 {   
  71.                     dq_start = p++; //標記遇到了雙引號   
  72.                 }   
  73.                 else if (*((p++) -1) =='//'//雙引號中的/也不需要處理。   
  74.                 {   
  75.                     continue;   
  76.                 }   
  77.                 printf("hello // world?? /**/"); //這種情況呢?怎麼辦?——這個情況會在遇到/是的第一個if語句被跳過   
  78.                    
  79.                    
  80.                 dq_start = NULL; //如果雙引號中不是//,標記爲NULL   
  81.                 /*****************以上是沒有遇到過單引號或//或/*的時候*****************/  
  82.             }   
  83.                
  84.             case '/'//斜槓,注意這個斜槓也可以是'//',"//",//,/*/中的第二個斜槓,但會在下面第二行代碼中被忽略掉   
  85.             {   
  86.                 if (sq_start || dq_start || lc_start || bc_start) //如果是單引號、雙引號、斜槓、/*的後面   
  87.                 {   
  88.                     p++;   
  89.                     continue;   
  90.                 }   
  91.                 /***********************下面是遇到註釋//或/*的時候****************************/   
  92.                 c = *(p + 1); //否則c取p指向字符的下一個字符   
  93.                 if (c == '/'//遇到了雙斜槓   
  94.                 {   
  95.                     lc_start = p; //標記雙斜槓的開始   
  96.                     p += 2; //p指向雙斜槓後面的字符   
  97.                 }   
  98.                 else if (c == '*'//遇到了/*   
  99.                 {   
  100.                     bc_start = p; //標記/*的開始   
  101.                     p += 2; //p指向/*後面的字符   
  102.                 }   
  103.                 /*************************上面是遇到註釋//或/*的時候**************************/   
  104.                 else  
  105.                 { //其它情況,再去判斷下一個是什麼符號——注意:C程序可以有其他情況嗎?這句話我認爲永遠不可能執行到。   
  106.                     p++;    
  107.                 }   
  108.             }   
  109.                
  110.             case '*'//星號,同斜槓,但少了如果遇到/*的情況,因爲遇到這種情況後,要判斷是不是遇到結束的地方*/了   
  111.             {   
  112.                 if (sq_start || dq_start || lc_start) //如果是單引號、雙引號、斜槓、/*的後面   
  113.                 {   
  114.                     p++;   
  115.                     continue;    
  116.                 }   
  117.                    
  118.                 if (*(p + 1) != '/'//如果星號後面緊跟的不是斜槓,那麼忽略過。   
  119.                 {   
  120.                     p++;   
  121.                     continue;   
  122.                 }   
  123.                    
  124.                 p += 2; //否則p指向斜槓後面那個字符。注意下面的清空語句,p指向的那個字符並不會被清除。   
  125.                 memset(bc_start, ' ', p-bc_start); //清空/* …… */中間的內容包括註釋符號本身。   
  126.                 bc_start = NULL;   
  127.                 break;   
  128.             }   
  129.                
  130.             case '/n'/*換行符,主要處理遇到雙斜槓時,需要清除雙斜槓到/n的前面的字符*/  
  131.             {   
  132.                 if (lc_start == NULL) //如果還沒有遇到雙斜槓,那麼忽略   
  133.                 {   
  134.                     p++;   
  135.                     continue/*這兩行本程序每次case後面都緊跟,就是忽略過的意思*/  
  136.                 }   
  137.                    
  138.                 c = *(p - 1);   
  139.                 //如果遇到過雙斜槓,清空雙斜槓本身和到n前面的那個字符,p指向下一個字符,/r是回車符(光標退回到最前面),要忽略。有這個情況嗎???   
  140.                 memset(lc_start, ' ', (c == '/r'? ((p++) -1) : p++) - lc_start);    
  141.                 lc_start = NULL;   
  142.                 break;   
  143.             }   
  144.                
  145.             default:   
  146.                 p++;   
  147.                 break;   
  148.         }   
  149.         /****************************************************  
  150.         如果遇到雙斜槓,這個if語句存在的意義在於萬一最後  
  151.         一行代碼是帶有雙斜槓但沒有給換行符/n的,也要清除掉。  
  152.         不帶文件末尾的雙斜槓的行尾一定有/n,這不是代碼中寫的  
  153.         /n而是我們的回車鍵換行操作寫入文件的。  
  154.         *****************************************************/  
  155.         if (lc_start)    
  156.         {   
  157.             memset(lc_start, ' ', p - lc_start);   
  158.         }   
  159.     }   
  160. }   
  161.   
  162. /**********************************************  
  163.             main函數的開始  
  164. ***********************************************/  
  165. int main (int argc, char *argv[])   
  166. {   
  167.     int fd, n;   
  168.     char buf[102400];   
  169.        
  170.     if (argc != 2)   
  171.     {   
  172.         printf("command error: Input as ./command <file>/n");   
  173.     }   
  174.        
  175.     fd = open(argv[1], O_RDONLY); /*只讀打開*/  
  176.     if (fd == -1)   
  177.     {   
  178.         return -1;   
  179.     }   
  180.        
  181.     n = read(fd, buf, sizeof(buf));   
  182.     if (n == -1 || n == 0)   
  183.     {   
  184.         close(fd);   
  185.         return -1;   
  186.     }   
  187.     printf("test/n");   
  188.     remove_comment(buf, n);   
  189.     *(buf + n) = '/0';   
  190.     printf("%s", buf);   
  191.     close(fd);   
  192.        
  193.     return 0;   
  194. }  
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章