利用Python爬蟲過濾「掘金」的關鍵詞檢索結果

這幾天剛剛開始學習Python,就像寫個爬蟲小項目練練手,自從間書的「××豚」事件後搬到掘金,感受掘金在各個方面作的都很不錯,尤爲是文章的質量和寫文章的編輯器作的很舒服。css

可是,我每次想要搜索一個本身感興趣的關鍵字時,下面就會出現大量文章,想按照「點贊數」排序連按鈕也找不到,必須得一直向下一行行瀏覽才能找到咱們須要的文章。因此,我在想可否利用剛學習的爬蟲作個功能:只需輸入關鍵字和經過被點贊數,就能自動給出一個列表,它包含了符合(點贊數大於咱們設定的)咱們需求的文章。說幹就開始,首先上結果圖:html

關鍵字爲'Python',點贊數設爲10

下面開始正式的工做:python

1.項目構成

項目構成
程序主要分爲:controller(主控制器)、downloader(下載器)、parser(解析器)、url_manager(url管理器)、outputer(輸出器)。

2. URL管理器(url_manager)

URL管理器主要負責產生、維護須要爬取的網站連接,對於咱們要爬取的網站「掘金」,主要分爲兩類:靜態頁面URL,AJAX動態構建的頁面。web

這兩種請求的URL的構成大相徑庭,而且返回內容也不一樣:靜態頁面URL返回HTML頁面,AJAX請求返回的是JSON字符串。針對這兩種訪問方式,咱們能夠這樣編寫URL管理器:ajax

class UrlManager(object):
    def __init__(self):
        self.new_urls = set()   # 新的url的集合

    # 構建訪問靜態頁面的url
    def build_static_url(self, base_url, keyword):
        static_url = base_url + '?query=' + keyword
        return static_url

    # 根據輸入的base_url(基礎地址)和params(參數字典)來構造一個新的url
    # eg:https://search-merger-ms.juejin.im/v1/search?query=python&page=1&raw_result=false&src=web
    # 參數中的start_page是訪問的起始頁數字,gap是訪問的頁數間隔
    def build_ajax_url(self, base_url, params, start_page=0, end_page=4, gap=1):
        if base_url is None:
            print('Invalid param base_url!')
            return None
        if params is None or len(params)==0:
            print('Invalid param request_params!')
            return None
        if end_page < start_page:
            raise Exception('start_page is bigger than end_page!')
        equal_sign = '='    #鍵值對內部鏈接符
        and_sign = '&'  #鍵值對之間鏈接符
        # 將base_url和參數拼接成url放入集合中
        for page_num in range(start_page, end_page, gap):
            param_list = []
            params['page'] = str(page_num)
            for item in params.items():
                param_list.append(equal_sign.join(item))    # 字典中每一個鍵值對中間用'='鏈接
            param_str = and_sign.join(param_list)       # 不一樣鍵值對之間用'&'鏈接
            new_url = base_url + '?' + param_str
            self.new_urls.add(new_url)
        return None

    # 從url集合中獲取一個新的url
    def get_new_url(self):
        if self.new_urls is None or len(self.new_urls) == 0:
            print('there are no new_url!')
            return None
        return self.new_urls.pop()

    # 判斷集合中是否還有url
    def has_more_url(self):
        if self.new_urls is None or len(self.new_urls) == 0:
            return False
        else:
            return True
複製代碼

如上代碼,在初始化函數__init__中維護了一個集合,建立好的URL將會放入到這個集合中。而後根據網址的結構分爲基礎的網址+訪問參數,二者之間經過'?'連接,參數之間經過'&'連接。經過函數build_ajax_url將二者鏈接起來,構成完整的URL放入到集合中,能夠經過get_new_url獲取集合中的一條URL,has_more_url判斷集合中是否還有未消費的URL。json

2.下載器(html_downloader)

import urllib.request

class HtmlDownloader(object):
    def download(self, url):
        if url is None:
            print('one invalid url is found!')
            return None
        response = urllib.request.urlopen(url)
        if response.getcode() != 200:
            print('response from %s is invalid!' % url)
            return None
        return response.read().decode('utf-8')
複製代碼

這段代碼比較簡單,使用urllib庫訪問URL並返回獲得的返回數據。bootstrap

3.JSON解析器(json_parser)

import json
from crawler.beans import result_bean


class JsonParser(object):
    # 將json字符創解析爲一個對象
    def json_to_object(self, json_content):
        if json_content is None:
            print('parse error!json is None!')
            return None
        print('json', str(json_content))
        return json.loads(str(json_content))

    # 從JSON構成的對象中提取出文章的title、link、collectionCount等數據,並將其封裝成一個Bean對象,最後將這些對象添加到結果列表中
    def build_bean_from_json(self, json_collection, baseline):
        if json_collection is None:
            print('build bean from json error! json_collection is None!')
        list = json_collection['d'] # 文章的列表
        result_list = []    # 結果的列表
        for element in list:
            starCount = element['collectionCount']  # 得到的收藏數,即得到的贊數
            if int(starCount) > baseline:   # 若是收藏數超過baseline,則勾結結果對象並添加到結果列表中
                title = element['title']
                link = element['originalUrl']
                result = result_bean.ResultBean(title, link, starCount)
                result_list.append(result)      # 添加到結果列表中
                print(title, link, starCount)
        return result_list
複製代碼

對於JSON的解析主要分爲兩部:1.將JSON字符串轉換爲一個字典對象;2.將文章題目、連接、贊數等信息從字典對象中提取出來,根據baseline判斷是否將這些數據封裝成結果對象並添加到結果列表中。閉包

3.HTML解析器(html_parser)

咱們能夠經過訪問:'https://juejin.im/search?query=python'獲得一個HTML網頁,可是隻有一頁數據,至關於訪問'https://search-merger-ms.juejin.im/v1/search?query=python&page=0&raw_result=false&src=web'得到的數據量,可是區別是一個返回內容的格式是HTML,第二個返回的是JSON。這裏咱們也將HTML的解析器也放到這裏,項目中能夠不用到這個:app

from bs4 import BeautifulSoup
from crawler.beans import result_bean


class HtmlParser(object):

    # 建立BeautifulSoup對象,將html結構化
    def build_soup(self, html_content):
        self.soup = BeautifulSoup(html_content, 'html.parser')
        return self.soup

    # 根據得到的贊數過濾獲得符合條件的tag
    def get_dom_by_star(self, baseline):
        doms = self.soup.find_all('span', class_='count')
        # 根據最少贊數過濾結果,只保留不小於baseline的節點
        for dom in doms:
            if int(dom.get_text()) < baseline:
                doms.remove(dom)
        return doms

    # 根據節點構建結果對象並添加到列表中
    def build_bean_from_html(self, baseline):
        doms = self.get_dom_by_star(baseline)
        if doms is None or len(doms)==0:
            print('doms is empty!')
            return None
        results = []
        for dom in doms:
            starCount = dom.get_text()      # 得到的贊數
            root = dom.find_parent('div', class_='info-box')    #這篇文章的節點
            a = root.find('a', class_='title', target='_blank') #包含了文章題目和連接的tag
            link = 'https://juejin.im' + a['href'] + '/detail'  #構造link
            title = a.get_text()
            results.append(result_bean.ResultBean(title, link, starCount))
            print(link, title, starCount)
        return results
複製代碼

爲了更加高效地解析HTML文件,這裏須要用到'bs4'模塊。dom

4.結果對象(result_bean)

結果對象是對爬蟲結果的一個封裝,將文章名、對應的連接、得到的贊數封裝成一個對象:

# 將每條文章保存爲一個bean,其中包含:題目、連接、得到的贊數 屬性
class ResultBean(object):
    def __init__(self, title, link, starCount=10):
        self.title = title
        self.link = link
        self.starCount = starCount
複製代碼

5.HTML輸出器(html_outputer)

class HtmlOutputer(object):

    def __init__(self):
        self.datas = []     # 輸入結果列表

    # 構建輸入數據(結果列表)
    def build_data(self, datas):
        if datas is None:
            print('Invalid data for output!')
            return None
        # 判斷是應該追加仍是覆蓋
        if self.datas is None or len(self.datas)==0:
            self.datas = datas
        else:
            self.datas.extend(datas)

    # 輸出html文件
    def output(self):
        fout = open('output.html', 'w', encoding='utf-8')
        fout.write('<html>')
        fout.write("<head><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\">")
        fout.write("<link rel=\"stylesheet\" href=\"http://cdn.static.runoob.com/libs/bootstrap/3.3.7/css/bootstrap.min.css\"> ")
        fout.write("<script src=\"http://cdn.static.runoob.com/libs/bootstrap/3.3.7/js/bootstrap.min.js\"></script>")
        fout.write("</head>")
        fout.write("<body>")
        fout.write("<table class=\"table table-striped\" width=\"200\">")

        fout.write("<thead><tr><td><strong>文章</strong></td><td><strong>星數</strong></td></tr></thead>")
        for data in self.datas:
            fout.write("<tr>")
            fout.write("<td width=\"100\"><a href=\"%s\" target=\"_blank\">%s</a></td>" % (data.link, data.title))
            fout.write("<td width=\"100\"> %s</td>" % data.starCount)
            fout.write("</tr>")

        fout.write("</table>")
        fout.write("</body>")
        fout.write("</html>")
        fout.close()
複製代碼

將解析後獲得的結果對象列表中的數據保存在HTML表格中。

6.控制器(main_controller)

from crawler.url import url_manager
from crawler.downloader import html_downloader
from crawler.parser import html_parser, json_parser
from crawler.outputer import html_outputer


class MainController(object):
    def __init__(self):
        self.url_manager = url_manager.UrlManager()
        self.downloader = html_downloader.HtmlDownloader()
        self.html_parser = html_parser.HtmlParser()
        self.html_outputer = html_outputer.HtmlOutputer()
        self.json_paser = json_parser.JsonParser()

    def craw(self, func):
        def in_craw(baseline):
            print('begin to crawler..')
            results = []
            while self.url_manager.has_more_url():
                content = self.downloader.download(self.url_manager.get_new_url())  # 根據URL獲取靜態網頁
                results.extend(func(content, baseline))
            self.html_outputer.build_data(results)
            self.html_outputer.output()
            print('crawler end..')
        print('call craw..')
        return in_craw

    def parse_from_json(self, content, baseline):
        json_collection = self.json_paser.json_to_object(content)
        results = self.json_paser.build_bean_from_json(json_collection, baseline)
        return results

    def parse_from_html(self, content, baseline):
        self.html_parser.build_soup(content)  # 使用BeautifulSoup將html網頁構建成soup樹
        results = self.html_parser.build_bean_from_html(baseline)
        return results
複製代碼

在控制器中,經過__init__函數建立前面的幾個模塊的實例。函數parse_from_json和parse_from_html分別負責從JSON和HTML中解析出結果;函數craw中利用閉包將解析函數抽象出來,使咱們方便選擇須要的解析器,就將解析器做爲參數'func'傳入craw函數中,這點相似於Java中對接口的使用,可是更加靈活,主函數中具體的用法能夠是:

if __name__ == '__main__':
    base_url = 'https://juejin.im/search'    # 要爬取的HTML網站網址(不含參數)
    ajax_base_url = 'https://search-merger-ms.juejin.im/v1/search'      #要經過ajax訪問的網址(不含參數,返回JSON)
    keyword = 'python'  # 搜索的關鍵字
    baseline = 10   # 得到的最少贊數量
    # 建立控制器對象
    crawler_controller = MainController()
    static_url = crawler_controller.url_manager.build_static_url(base_url, keyword)     # 構建靜態URL

    # craw_html = crawler_controller.craw(crawler_controller.parse_from_html) # 選擇HTML解析器
    # craw_html(static_url, baseline) #開始抓取

    # ajax請求的網址例子:'https://search-merger-ms.juejin.im/v1/search?query=python&page=0&raw_result=false&src=web'
    params = {}     # 對應的請求參數
    # 初始化請求參數
    params['query'] = keyword
    params['page'] = '1'
    params['raw_result'] = 'false'
    params['src'] = 'web'
    crawler_controller.url_manager.build_ajax_url(ajax_base_url, params)    # 構建ajax訪問的網址
    craw_json = crawler_controller.craw(crawler_controller.parse_from_json) # 選擇JSON解析器
    craw_json(baseline)     #開始抓取
複製代碼

完成

相關文章
相關標籤/搜索