php实现的网页正文提取算法

 Html2Article-php实现的提取网页正文部分,最近研究百度结果页的资讯采集,其中关键环节就是从采集回的页面中提取出文章。

测试代码

PHP代码
  1. #采集我们新闻网的一个新闻页  
  2. $content = file_get_contents("http://news.qingdaonews.com/qingdao/2016-12/26/content_11882489.htm");  
  3. $r = new Readability($content);  
  4. print_r($r->getContent());  

因为难点在于如何去识别并保留网页中的文章部分,而且删除其它无用的信息,并且要做到通用化,不能像火车头那样根据目标站来制定采集规则,因为搜索引擎结果中有各种的网页。

这个类是从网上找到的一个php实现的提取网页正文部分的算法,在本地也测试了下,准确率非常高。

PHP代码
  1. <?php  
  2.   
  3. class Readability {  
  4.     // 保存判定结果的标记位名称  
  5.     const ATTR_CONTENT_SCORE = "contentScore";  
  6.   
  7.     // DOM 解析类目前只支持 UTF-8 编码  
  8.     const DOM_DEFAULT_CHARSET = "utf-8";  
  9.   
  10.     // 当判定失败时显示的内容  
  11.     const MESSAGE_CAN_NOT_GET = "Readability was unable to parse this page for content.";  
  12.   
  13.     // DOM 解析类(PHP5 已内置)  
  14.     protected $DOM = null;  
  15.   
  16.     // 需要解析的源代码  
  17.     protected $source = "";  
  18.   
  19.     // 章节的父元素列表  
  20.     private $parentNodes = array();  
  21.   
  22.     // 需要删除的标签  
  23.     // Note: added extra tags from https://github.com/ridcully  
  24.     private $junkTags = Array("style""form""iframe""script""button""input""textarea",  
  25.                                 "noscript""select""option""object""applet""basefont",  
  26.                                 "bgsound""blink""canvas""command""menu""nav""datalist",  
  27.                                 "embed""frame""frameset""keygen""label""marquee""link");  
  28.   
  29.     // 需要删除的属性  
  30.     private $junkAttrs = Array("style""class""onclick""onmouseover""align""border""margin");  
  31.   
  32.   
  33.     /** 
  34.      * 构造函数 
  35.      *      @param $input_char 字符串的编码。默认 utf-8,可以省略 
  36.      */  
  37.     function __construct($source$input_char = "utf-8") {  
  38.         $this->source = $source;  
  39.   
  40.         // DOM 解析类只能处理 UTF-8 格式的字符  
  41.         $source = mb_convert_encoding($source'HTML-ENTITIES'$input_char);  
  42.   
  43.         // 预处理 HTML 标签,剔除冗余的标签等  
  44.         $source = $this->preparSource($source);  
  45.   
  46.         // 生成 DOM 解析类  
  47.         $this->DOM = new DOMDocument('1.0'$input_char);  
  48.         try {  
  49.             //libxml_use_internal_errors(true);  
  50.             // 会有些错误信息,不过不要紧 :^)  
  51.             if (!@$this->DOM->loadHTML('<?xml encoding="'.Readability::DOM_DEFAULT_CHARSET.'">'.$source)) {  
  52.                 throw new Exception("Parse HTML Error!");  
  53.             }  
  54.   
  55.             foreach ($this->DOM->childNodes as $item) {  
  56.                 if ($item->nodeType == XML_PI_NODE) {  
  57.                     $this->DOM->removeChild($item); // remove hack  
  58.                 }  
  59.             }  
  60.   
  61.             // insert proper  
  62.             $this->DOM->encoding = Readability::DOM_DEFAULT_CHARSET;  
  63.         } catch (Exception $e) {  
  64.             // ...  
  65.         }  
  66.     }  
  67.   
  68.   
  69.     /** 
  70.      * 预处理 HTML 标签,使其能够准确被 DOM 解析类处理 
  71.      * 
  72.      * @return String 
  73.      */  
  74.     private function preparSource($string) {  
  75.         // 剔除多余的 HTML 编码标记,避免解析出错  
  76.         preg_match("/charset=([\w|\-]+);?/"$string$match);  
  77.         if (isset($match[1])) {  
  78.             $string = preg_replace("/charset=([\w|\-]+);?/"""$string, 1);  
  79.         }  
  80.   
  81.         // Replace all doubled-up <BR> tags with <P> tags, and remove fonts.  
  82.         $string = preg_replace("/<br\/?>[ \r\n\s]*<br\/?>/i""</p><p>"$string);  
  83.         $string = preg_replace("/<\/?font[^>]*>/i"""$string);  
  84.   
  85.         // @see https://github.com/feelinglucky/php-readability/issues/7  
  86.         //   - from http://stackoverflow.com/questions/7130867/remove-script-tag-from-html-content  
  87.         $string = preg_replace("#<script(.*?)>(.*?)</script>#is"""$string);  
  88.   
  89.         return trim($string);  
  90.     }  
  91.   
  92.   
  93.     /** 
  94.      * 删除 DOM 元素中所有的 $TagName 标签 
  95.      * 
  96.      * @return DOMDocument 
  97.      */  
  98.     private function removeJunkTag($RootNode$TagName) {  
  99.          
  100.         $Tags = $RootNode->getElementsByTagName($TagName);  
  101.          
  102.         //Note: always index 0, because removing a tag removes it from the results as well.  
  103.         while($Tag = $Tags->item(0)){  
  104.             $parentNode = $Tag->parentNode;  
  105.             $parentNode->removeChild($Tag);  
  106.         }  
  107.          
  108.         return $RootNode;  
  109.          
  110.     }  
  111.   
  112.     /** 
  113.      * 删除元素中所有不需要的属性 
  114.      */  
  115.     private function removeJunkAttr($RootNode$Attr) {  
  116.         $Tags = $RootNode->getElementsByTagName("*");  
  117.   
  118.         $i = 0;  
  119.         while($Tag = $Tags->item($i++)) {  
  120.             $Tag->removeAttribute($Attr);  
  121.         }  
  122.   
  123.         return $RootNode;  
  124.     }  
  125.   
  126.     /** 
  127.      * 根据评分获取页面主要内容的盒模型 
  128.      *      判定算法来自:http://code.google.com/p/arc90labs-readability/   
  129.      *      这里由郑晓博客转发 
  130.      * @return DOMNode 
  131.      */  
  132.     private function getTopBox() {  
  133.         // 获得页面所有的章节  
  134.         $allParagraphs = $this->DOM->getElementsByTagName("p");  
  135.   
  136.         // Study all the paragraphs and find the chunk that has the best score.  
  137.         // A score is determined by things like: Number of <p>'s, commas, special classes, etc.  
  138.         $i = 0;  
  139.         while($paragraph = $allParagraphs->item($i++)) {  
  140.             $parentNode   = $paragraph->parentNode;  
  141.             $contentScore = intval($parentNode->getAttribute(Readability::ATTR_CONTENT_SCORE));  
  142.             $className    = $parentNode->getAttribute("class");  
  143.             $id           = $parentNode->getAttribute("id");  
  144.   
  145.             // Look for a special classname  
  146.             if (preg_match("/(comment|meta|footer|footnote)/i"$className)) {  
  147.                 $contentScore -= 50;  
  148.             } else if(preg_match(  
  149.                 "/((^|\\s)(post|hentry|entry[-]?(content|text|body)?|article[-]?(content|text|body)?)(\\s|$))/i",  
  150.                 $className)) {  
  151.                 $contentScore += 25;  
  152.             }  
  153.   
  154.             // Look for a special ID  
  155.             if (preg_match("/(comment|meta|footer|footnote)/i"$id)) {  
  156.                 $contentScore -= 50;  
  157.             } else if (preg_match(  
  158.                 "/^(post|hentry|entry[-]?(content|text|body)?|article[-]?(content|text|body)?)$/i",  
  159.                 $id)) {  
  160.                 $contentScore += 25;  
  161.             }  
  162.   
  163.             // Add a point for the paragraph found  
  164.             // Add points for any commas within this paragraph  
  165.             if (strlen($paragraph->nodeValue) > 10) {  
  166.                 $contentScore += strlen($paragraph->nodeValue);  
  167.             }  
  168.   
  169.             // 保存父元素的判定得分  
  170.             $parentNode->setAttribute(Readability::ATTR_CONTENT_SCORE, $contentScore);  
  171.   
  172.             // 保存章节的父元素,以便下次快速获取  
  173.             array_push($this->parentNodes, $parentNode);  
  174.         }  
  175.   
  176.         $topBox = null;  
  177.          
  178.         // Assignment from index for performance.  
  179.         //     See http://www.peachpit.com/articles/article.aspx?p=31567&seqNum=5  
  180.         for ($i = 0, $len = sizeof($this->parentNodes); $i < $len$i++) {  
  181.             $parentNode      = $this->parentNodes[$i];  
  182.             $contentScore    = intval($parentNode->getAttribute(Readability::ATTR_CONTENT_SCORE));  
  183.             $orgContentScore = intval($topBox ? $topBox->getAttribute(Readability::ATTR_CONTENT_SCORE) : 0);  
  184.   
  185.             if ($contentScore && $contentScore > $orgContentScore) {  
  186.                 $topBox = $parentNode;  
  187.             }  
  188.         }  
  189.          
  190.         // 此时,$topBox 应为已经判定后的页面内容主元素  
  191.         return $topBox;  
  192.     }  
  193.   
  194.   
  195.     /** 
  196.      * 获取 HTML 页面标题 
  197.      * 
  198.      * @return String 
  199.      */  
  200.     public function getTitle() {  
  201.         $split_point = ' - '; 
  202.         $titleNodes = $this->DOM->getElementsByTagName("title"); 
  203.  
  204.         if ($titleNodes->length 
  205.             && $titleNode = $titleNodes->item(0)) { 
  206.             // @see http://stackoverflow.com/questions/717328/how-to-explode-string-right-to-left 
  207.             $title  = trim($titleNode->nodeValue); 
  208.             $result = array_map('strrev', explode($split_point, strrev($title))); 
  209.             return sizeof($result) > 1 ? array_pop($result) : $title; 
  210.         } 
  211.  
  212.         return null; 
  213.     } 
  214.  
  215.  
  216.     /** 
  217.      * Get Leading Image Url 
  218.      * 
  219.      * @return String 
  220.      */ 
  221.     public function getLeadImageUrl($node) { 
  222.         $images = $node->getElementsByTagName("img"); 
  223.  
  224.         if ($images->length && $leadImage = $images->item(0)) { 
  225.             return $leadImage->getAttribute("src"); 
  226.         } 
  227.  
  228.         return null; 
  229.     } 
  230.  
  231.  
  232.     /** 
  233.      * 获取页面的主要内容(Readability 以后的内容) 
  234.      * 
  235.      * @return Array 
  236.      */ 
  237.     public function getContent() { 
  238.         if (!$this->DOM) return false; 
  239.  
  240.         // 获取页面标题 
  241.         $ContentTitle = $this->getTitle(); 
  242.  
  243.         // 获取页面主内容 
  244.         $ContentBox = $this->getTopBox(); 
  245.         
  246.         //Check if we found a suitable top-box. 
  247.         if($ContentBox === null) 
  248.             throw new RuntimeException(Readability::MESSAGE_CAN_NOT_GET); 
  249.         
  250.         // 复制内容到新的 DOMDocument 
  251.         $Target = new DOMDocument; 
  252.         $Target->appendChild($Target->importNode($ContentBox, true)); 
  253.  
  254.         // 删除不需要的标签 
  255.         foreach ($this->junkTags as $tag) { 
  256.             $Target = $this->removeJunkTag($Target, $tag); 
  257.         } 
  258.  
  259.         // 删除不需要的属性 
  260.         foreach ($this->junkAttrs as $attr) { 
  261.             $Target = $this->removeJunkAttr($Target, $attr); 
  262.         } 
  263.  
  264.         $content = mb_convert_encoding($Target->saveHTML(), Readability::DOM_DEFAULT_CHARSET, "HTML-ENTITIES"); 
  265.  
  266.         // 多个数据,以数组的形式返回 
  267.         return Array( 
  268.             'lead_image_url' => $this->getLeadImageUrl($Target), 
  269.             'word_count' => mb_strlen(strip_tags($content), Readability::DOM_DEFAULT_CHARSET), 
  270.             'title' => $ContentTitle ? $ContentTitle : null, 
  271.             'content' => $content  
  272.         );  
  273.     }  
  274.   
  275.     function __destruct() { }  
  276. }  

使用起来也非常简单,实例化时传入网页的html源码和相应的编码,然后直接调用其getContent方法即可返回提取到的正文部分,提取出的文章中可能还会含有少部分链接,可以自己后期再修改。



上一篇: 安卓APP承载网页(WebView)
下一篇: CentOS 7 YUM 安装 LNMP 环境
文章来自: 本站原创
引用通告: 查看所有引用 | 我要引用此文章
Tags: php
相关日志:
评论: 0 | 引用: 0 | 查看次数: 294
发表评论
昵 称:
密 码: 游客发言不需要密码.
邮 箱: 邮件地址支持Gravatar头像,邮箱地址不会公开.
网 址: 输入网址便于回访.
内 容:
验证码:
选 项:
虽然发表评论不用注册,但是为了保护您的发言权,建议您注册帐号.
字数限制 1000 字 | UBB代码 开启 | [img]标签 关闭

 广告位

↑返回顶部↑