您好,登錄后才能下訂單哦!
這篇文章主要介紹了如何用Python實現網頁正文的提取的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇如何用Python實現網頁正文的提取文章都會有所收獲,下面我們一起來看看吧。
一個典型的新聞網頁包括幾個不同區域:
新聞網頁區域
我們要提取的新聞要素包含在:
標題區域
meta數據區域(發布時間等)
配圖區域(如果想把配圖也提取)
正文區域
而導航欄區域、相關鏈接區域的文字就不屬于該新聞的要素。
新聞的標題、發布時間、正文內容一般都是從我們抓取的html里面提取的。如果僅僅是一個網站的新聞網頁,提取這三個內容很簡單,寫三個正則表達式就可以完美提取了。然而,我們的爬蟲抓來的是成百上千的網站的網頁。對這么多不同格式的網頁寫正則表達式會累死人的,而且網頁一旦稍微改版,表達式可能就失效,維護這群表達式也是會累死人的。
累死人的做法當然想不通,我們就要探索一下好的算法來實現。
標題基本上都會出現在html的<title>
標簽里面,但是又被附加了諸如頻道名稱、網站名稱等信息;
標題還會出現在網頁的“標題區域”。
那么這兩個地方,從哪里提取標題比較容易呢?
網頁的“標題區域”沒有明顯的標識,不同網站的“標題區域”的html代碼部分千差萬別。所以這個區域并不容易提取出來。
那么就只剩下<title>
標簽了,這個標簽很容易提取,無論是正則表達式,還是lxml解析都很容易,不容易的是如何去除頻道名稱、網站名稱等信息。
先來看看,<title>
標簽里面都是設么樣子的附加信息:
上海用“智慧”激活城市交通脈搏,讓道路更安全更有序更通暢_浦江頭條_澎湃新聞-The Paper
“滬港大學聯盟”今天在復旦大學成立_教育_新民網
三亞老人腳踹司機致公交車失控撞墻 被判刑3年_社會
外交部:中美外交安全對話9日在美舉行
進博會:中國行動全球矚目,中國擔當世界點贊_南方觀瀾_南方網
資本市場迎來重大改革 設立科創板有何深意?-新華網
觀察這些title不難發現,新聞標題和頻道名、網站名之間都是有一些連接符號的。那么我就可以通過這些連接符吧title分割,找出最長的部分就是新聞標題了。
這個思路也很容易實現,這里就不再上代碼了,留給小猿們作為思考練習題自己實現一下。
發布時間,指的是這個網頁在該網站上線的時間,一般它會出現在正文標題的下方——meta數據區域。從html代碼看,這個區域沒有什么特殊特征讓我們定位,尤其是在非常多的網站版面面前,定位這個區域幾乎是不可能的。這需要我們另辟蹊徑。
跟標題一樣,我們也先看看一些網站的發布時間都是怎么寫的:
央視網2018年11月06日 22:22
時間:2018-11-07 14:27:00
2018-11-07 11:20:37 來源: 新華網
來源:中國日報網 2018-11-07 08:06:39
2018年11月07日 07:39:19
2018-11-06 09:58 來源:澎湃新聞
這些寫在網頁上的發布時間,都有一個共同的特點,那就是一個表示時間的字符串,年月日時分秒,無外乎這幾個要素。通過正則表達式,我們列舉一些不同時間表達方式(也就那么幾種)的正則表達式,就可以從網頁文本中進行匹配提取發布時間了。
這也是一個很容易實現的思路,但是細節比較多,表達方式要涵蓋的盡可能多,寫好這么一個提取發布時間的函數也不是那么容易的哦。小猿們盡情發揮動手能力,看看自己能寫出怎樣的函數實現。這也是留給小猿們的一道練習題。
正文(包括新聞配圖)是一個新聞網頁的主體部分,它在視覺上占據中間位置,是新聞的內容主要的文字區域。正文的提取有很多種方法,實現上有復雜也有簡單。本文介紹的方法,是結合老猿多年的實踐經驗和思考得出來的一個簡單快速的方法,姑且稱之為“節點文本密度法”。
我們知道,網頁的html代碼是由不同的標簽(tag)組成了一個樹狀結構樹,每個標簽是樹的一個節點。通過遍歷這個樹狀結構的每個節點,找到文本最多的節點,它就是正文所在的節點。根據這個思路,我們來實現一下代碼。
#!/usr/bin/env python3 #File: maincontent.py #Author: veelion import re import time import traceback import cchardet import lxml import lxml.html from lxml.html import HtmlComment REGEXES = { 'okMaybeItsACandidateRe': re.compile( 'and|article|artical|body|column|main|shadow', re.I), 'positiveRe': re.compile( ('article|arti|body|content|entry|hentry|main|page|' 'artical|zoom|arti|context|message|editor|' 'pagination|post|txt|text|blog|story'), re.I), 'negativeRe': re.compile( ('copyright|combx|comment|com-|contact|foot|footer|footnote|decl|copy|' 'notice|' 'masthead|media|meta|outbrain|promo|related|scroll|link|pagebottom|bottom|' 'other|shoutbox|sidebar|sponsor|shopping|tags|tool|widget'), re.I), } class MainContent: def __init__(self,): self.non_content_tag = set([ 'head', 'meta', 'script', 'style', 'object', 'embed', 'iframe', 'marquee', 'select', ]) self.title = '' self.p_space = re.compile(r'\s') self.p_html = re.compile(r'<html|</html>', re.IGNORECASE|re.DOTALL) self.p_content_stop = re.compile(r'正文.*結束|正文下|相關閱讀|聲明') self.p_clean_tree = re.compile(r'author|post-add|copyright') def get_title(self, doc): title = '' title_el = doc.xpath('//title') if title_el: title = title_el[0].text_content().strip() if len(title) < 7: tt = doc.xpath('//meta[@name="title"]') if tt: title = tt[0].get('content', '') if len(title) < 7: tt = doc.xpath('//*[contains(@id, "title") or contains(@class, "title")]') if not tt: tt = doc.xpath('//*[contains(@id, "font01") or contains(@class, "font01")]') for t in tt: ti = t.text_content().strip() if ti in title and len(ti)*2 > len(title): title = ti break if len(ti) > 20: continue if len(ti) > len(title) or len(ti) > 7: title = ti return title def shorten_title(self, title): spliters = [' - ', '–', '—', '-', '|', '::'] for s in spliters: if s not in title: continue tts = title.split(s) if len(tts) < 2: continue title = tts[0] break return title def calc_node_weight(self, node): weight = 1 attr = '%s %s %s' % ( node.get('class', ''), node.get('id', ''), node.get('style', '') ) if attr: mm = REGEXES['negativeRe'].findall(attr) weight -= 2 * len(mm) mm = REGEXES['positiveRe'].findall(attr) weight += 4 * len(mm) if node.tag in ['div', 'p', 'table']: weight += 2 return weight def get_main_block(self, url, html, short_title=True): ''' return (title, etree_of_main_content_block) ''' if isinstance(html, bytes): encoding = cchardet.detect(html)['encoding'] if encoding is None: return None, None html = html.decode(encoding, 'ignore') try: doc = lxml.html.fromstring(html) doc.make_links_absolute(base_url=url) except : traceback.print_exc() return None, None self.title = self.get_title(doc) if short_title: self.title = self.shorten_title(self.title) body = doc.xpath('//body') if not body: return self.title, None candidates = [] nodes = body[0].getchildren() while nodes: node = nodes.pop(0) children = node.getchildren() tlen = 0 for child in children: if isinstance(child, HtmlComment): continue if child.tag in self.non_content_tag: continue if child.tag == 'a': continue if child.tag == 'textarea': # FIXME: this tag is only part of content? continue attr = '%s%s%s' % (child.get('class', ''), child.get('id', ''), child.get('style')) if 'display' in attr and 'none' in attr: continue nodes.append(child) if child.tag == 'p': weight = 3 else: weight = 1 text = '' if not child.text else child.text.strip() tail = '' if not child.tail else child.tail.strip() tlen += (len(text) + len(tail)) * weight if tlen < 10: continue weight = self.calc_node_weight(node) candidates.append((node, tlen*weight)) if not candidates: return self.title, None candidates.sort(key=lambda a: a[1], reverse=True) good = candidates[0][0] if good.tag in ['p', 'pre', 'code', 'blockquote']: for i in range(5): good = good.getparent() if good.tag == 'div': break good = self.clean_etree(good, url) return self.title, good def clean_etree(self, tree, url=''): to_drop = [] drop_left = False for node in tree.iterdescendants(): if drop_left: to_drop.append(node) continue if isinstance(node, HtmlComment): to_drop.append(node) if self.p_content_stop.search(node.text): drop_left = True continue if node.tag in self.non_content_tag: to_drop.append(node) continue attr = '%s %s' % ( node.get('class', ''), node.get('id', '') ) if self.p_clean_tree.search(attr): to_drop.append(node) continue aa = node.xpath('.//a') if aa: text_node = len(self.p_space.sub('', node.text_content())) text_aa = 0 for a in aa: alen = len(self.p_space.sub('', a.text_content())) if alen > 5: text_aa += alen if text_aa > text_node * 0.4: to_drop.append(node) for node in to_drop: try: node.drop_tree() except: pass return tree def get_text(self, doc): lxml.etree.strip_elements(doc, 'script') lxml.etree.strip_elements(doc, 'style') for ch in doc.iterdescendants(): if not isinstance(ch.tag, str): continue if ch.tag in ['div', 'h2', 'h3', 'h4', 'p', 'br', 'table', 'tr', 'dl']: if not ch.tail: ch.tail = '\n' else: ch.tail = '\n' + ch.tail.strip() + '\n' if ch.tag in ['th', 'td']: if not ch.text: ch.text = ' ' else: ch.text += ' ' # if ch.tail: # ch.tail = ch.tail.strip() lines = doc.text_content().split('\n') content = [] for l in lines: l = l.strip() if not l: continue content.append(l) return '\n'.join(content) def extract(self, url, html): '''return (title, content) ''' title, node = self.get_main_block(url, html) if node is None: print('\tno main block got !!!!!', url) return title, '', '' content = self.get_text(node) return title, content
跟新聞爬蟲一樣,我們把整個算法實現為一個類:MainContent。
首先,定義了一個全局變量: REGEXES。它收集了一些經常出現在標簽的class和id中的關鍵詞,這些詞標識著該標簽可能是正文或者不是。我們用這些詞來給標簽節點計算權重,也就是方法calc_node_weight()的作用。
MainContent類的初始化,先定義了一些不會包含正文的標簽 self.non_content_tag,遇到這些標簽節點,直接忽略掉即可。
本算法提取標題實現在get_title()這個函數里面。首先,它先獲得<title>
標簽的內容,然后試著從<meta>
里面找title,再嘗試從<body>
里面找id和class包含title的節點,最后把從不同地方獲得的可能是標題的文本進行對比,最終獲得標題。對比的原則是:
<meta>
, <body>
里面找到的疑似標題如果包含在<title>
標簽里面,則它是一個干凈(沒有頻道名、網站名)的標題;
如果疑似標題太長就忽略
主要把<title>
標簽作為標題
從<title>
標簽里面獲得標題,就要解決標題清洗的問題。這里實現了一個簡單的方法: clean_title()。
在這個實現中,我們使用了lxml.html把網頁的html轉化成一棵樹,從body節點開始遍歷每一個節點,看它直接包含(不含子節點)的文本的長度,從中找出含有最長文本的節點。這個過程實現在方法:get_main_block()中。其中一些細節,小猿們可以仔細體會一下。
其中一個細節就是,clean_node()這個函數。通過get_main_block()得到的節點,有可能包含相關新聞的鏈接,這些鏈接包含大量新聞標題,如果不去除,就會給新聞內容帶來雜質(相關新聞的標題、概述等)。
還有一個細節,get_text()函數。我們從main block中提取文本內容,不是直接使用text_content(),而是做了一些格式方面的處理,比如在一些標簽后面加入換行符合\n
,在table的單元格之間加入空格。這樣處理后,得到的文本格式比較符合原始網頁的效果。
1. cchardet模塊
用于快速判斷文本編碼的模塊
2. lxml.html模塊
結構化html代碼的模塊,通過xpath解析網頁的工具,高效易用,是寫爬蟲的居家必備的模塊。
3. 內容提取的復雜性
我們這里實現的正文提取的算法,基本上可以正確處理90%以上的新聞網頁。
但是,世界上沒有千篇一律的網頁一樣,也沒有一勞永逸的提取算法。大規模使用本文算法的過程中,你會碰到奇葩的網頁,這個時候,你就要針對這些網頁,來完善這個算法類。
關于“如何用Python實現網頁正文的提取”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“如何用Python實現網頁正文的提取”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。