1. 程式人生 > >python 抓取開源中國上閱讀數大於 1000 的優質文章

python 抓取開源中國上閱讀數大於 1000 的優質文章

Python 現在越來越火,連小學生都在學習 Python了 ^ ^,為了跟上時代,趕個時髦,秉承活到老學到老的精神,慢慢也開始學習 Python;理論是實踐的基礎,把 Python 相關語法看了,就迫不及待,大筆一揮來個 Hello world 壓壓驚,理論終歸是理論,實踐還是要要的嘛,動手了之後,才能更好的掌(chui)握(niu)基(bi)礎;發車了..........

'''
作者:tsmyk0715
源自:https://my.oschina.net/mengyuankan/blog/1934171
人生苦短,我用Python
'''

首先瀏覽器輸入 https://www.oschina.net/ 進入開源中國官網,點選頂部導航欄的 “部落格” 選項進入部落格列表頁面,之後點選左側 “服務端開發與管理” 選項,我們要爬取的是服務端相關的文章,如下圖所示:

接下來分析文章列表的佈局方式,按 F12 開啟除錯頁面,如下所示:

可以看到,一篇文章的相關資訊就是一個 div, class 屬性為 item blog-item,開啟該 div,如下:

我們要抓取的是文章的標題,描述,URL,和閱讀數,標題和URL可以通過 a 標籤來獲取,描述通過 <dev class=description> 來獲取,而閱讀數則要麻煩些,是第三個 <div class=item>,

通過以下程式碼就可以獲取到以上到相關資訊:

        # 獲取每個文章相關的 DIV
        articles_div = beautiful_soup.find_all("div", class_="item blog-item")
        # 處理每個 DIV
        for article_div in articles_div:
            content_div = article_div.find("div", class_="content")
            header_div = content_div.find("a", class_="header")
            # 文章URL
            url = header_div["href"]
            # 文章標題
            title = header_div["title"]
            # 文章描述
            description_str = content_div.find("div", class_="description").find("p", class_="line-clamp")
            if description_str is None:
                continue
            description = description_str.text
            # 文章閱讀數
            read_count_div = content_div.find("div", class_="extra").find("div", class_="ui horizontal list").find("div", class_="item")
            # find_next_sibling 獲取兄弟節點
            read_count_i = read_count_div.find_next_sibling("div", class_="item").find_next_sibling("div", class_="item")
            read_count = read_count_i.getText()

上述程式碼就是主要的獲取相關資訊的邏輯,因為閱讀數沒有唯一id,或者 class ,所有可以通過 find_next_sibling 來獲取兄弟節點;

接下來就對獲取到到文章進行處理,如按照閱讀數大於等於1000過濾文章,並按照閱讀數從高到低低排序,並且寫到檔案中:

首先要定義一個文章類,用來表示文章的相關資訊,如下:

"""
文章實體類
@authon:tsmyk0715
"""
class Article:

    def __init__(self, title, url, content="", read_cnt=0):
        """
        文章類構造方法
        :param title:文章標題
        :param url: 文章 URL
        :param content: 文章內容
        :param read_cnt: 文章閱讀數
        """
        self.title = title
        self.url = url
        self.content = content
        self.read_cnt = read_cnt

    def __str__(self):
        return u"文章:標題《{0}》,閱讀數:{1},連結:{2}".format(self.title, self.read_cnt, self.url)

之後,定義文章的處理類 OschinaArticle ,相關處理邏輯在該類中實現:

import requests
# 使用 BeautifulSoup 庫來解析 HTML 頁面
from bs4 import BeautifulSoup
import logging
import time
# 匯入定義的文章實體類
from Article import Article

class OschinaArticle:

    def __init__(self):
        # 日誌
        self.log = logging
        # 設定日誌級別為 INFO
        self.log.basicConfig(level=logging.INFO)
        # 把文章寫到檔案的行號
        self.file_line_num = 1

接下來獲取 BeautifulSoup 物件:

    def getSoup(self, url):
        """
        根據 url 獲取 BeautifulSoup 物件
        """
        # 請求頭
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
            "Host": "www.oschina.net"}
        # 請求的時候,需要加上頭部資訊,表示是人在操作,而不是機器,不加頭部資訊會報錯
        response = requests.get(url, headers=headers)
        return BeautifulSoup(response.text, "html.parser")

之後,通過 BeautifulSoup 來解析 HTML 頁面,獲取文章相關資訊,之後,根據相關資訊建立文章物件,放到集合中進行返回:

    def get_articles(self, url):
        # 存放文章的集合,進行返回
        article_list = []
        beautiful_soup = self.getSoup(url)
        self.log.info(u"開始解析 HTML 頁面...")
        articles_div = beautiful_soup.find_all("div", class_="item blog-item")
        for article_div in articles_div:
            content_div = article_div.find("div", class_="content")
            header_div = content_div.find("a", class_="header")
            # 文章URL
            url = header_div["href"]
            # 文章標題
            title = header_div["title"]
            # 文章描述
            description_str = content_div.find("div", class_="description").find("p", class_="line-clamp")
            if description_str is None:
                continue
            description = description_str.text
            # 文章閱讀數
            read_count_div = content_div.find("div", class_="extra").find("div", class_="ui horizontal list").find("div", class_="item")
            # find_next_sibling 獲取兄弟節點
            read_count_i = read_count_div.find_next_sibling("div", class_="item").find_next_sibling("div", class_="item")
            read_count = read_count_i.getText()
            # 根據相關資訊建立文章物件,放到集合中,進行返回
            __article = Article(title, url, description, read_count)
            article_list.append(__article)

            self.log.info(u"文章:標題《{0}》,閱讀數:{1},連結:{2}".format(title, read_count, url))

        return article_list

因為文章的閱讀數如果超過 1000 的話,就用 K 來表示,為了在後面篩選指定閱讀數的文章,所以需要進行處理,把 K 轉換為 1000,程式碼如下:

    def handler_read_count(self, article_list):
        """
        處理閱讀數:把 K 轉換為 1000
        :param article_list:文章列表
        """
        if article_list is None or len(article_list) == 0:
            self.log.info(u"文章列表為空...")
            return
        for article in article_list:
            read_count_str = article.read_cnt.strip()
            read_count = 0
            if isinstance(read_count_str, str):
                if read_count_str.endswith("K"):
                    read_count_str = read_count_str[:-1]  # 去掉K
                    read_count = int(float(read_count_str) * 1000)
                else:
                    read_count = int(read_count_str)
            article.read_cnt = read_count

接下來就是文章根據閱讀數進行篩選和排序了,篩選出閱讀數大於等於指定值並且按照閱讀數從高到低排序,程式碼如下:

    def get_article_by_read_count_sort(self, article_list, min_read_cnt):
        """
        獲取大於等於指定閱讀數的文章資訊, 並按照閱讀數從高到低排序
        :param article_list: 文章列表
        :param minx_read_cnt: 最小閱讀數
        :return:
        """
        if article_list is None or len(article_list) == 0:
            self.log.info(u"文章列表為空...")
            return
        article_list_return = []
        for article in article_list:
            if article.read_cnt >= min_read_cnt:
                article_list_return.append(article)
        # 使用 Lambda 對集合中的物件按照 read_cnt 屬性進行排序
        article_list_return.sort(key=lambda Article: Article.read_cnt, reverse=True)
        return article_list_return

以上就可以獲取到我們想要的文章資訊了,此外,我們可以把資訊寫到檔案檔案中,程式碼如下:

    def write_file(self, article_list, file_path):
        # 建立 IO 物件
        file = open(file_path + "/articles.txt", "a")

        for article in article_list:
            _article_str = str(article)
            file.write("(" + str(self.file_line_num) + ")" + _article_str)
            file.write("\n")
            file.write("--------------------------------------------------------------------------------------------------------------------------------------------------------")
            file.write("\n")
            # 檔案行號
            self.file_line_num += 1 
            time.sleep(0.2)  # 休眠 200 毫秒
        file.close()

之後,把上面的方法整合在一起,程式碼如下:

    def run(self, url, min_read_count):
        # 獲取所有文章
        article_list = self.get_articles(url)
        # 對閱讀數進行處理
        self.handler_read_count(article_list)
        # 篩選閱讀數大於等於指定值,並按閱讀數從高到低排序
        _list = self.get_article_by_read_count_sort(article_list, min_read_count)
        # 寫檔案
        self.write_file(_list, "G:/python")
        # 列印控制檯
        for a in _list:
            self.log.info(a)

main 方法測試一下,地址輸入:https://www.oschina.net/blog?classification=428640, 文章閱讀數要大於等於1000

if __name__ == '__main__':
    article = OschinaArticle()
    main_url = "https://www.oschina.net/blog?classification=428640"
    min_read_count = 1000
    article.run(main_url, min_read_count)

控制檯日誌列印如下:

寫入到檔案中的內容如下:

你以為到這裡就完了嗎,no, no, no.............,通過上述方式只能獲取到首頁的文章,如果想獲取更多的文章怎麼辦?開源中國的部落格文章列表沒有分頁,是通過滑動滑鼠滾輪來獲取更多的頁,可是人家的地址導航欄卻沒有絲毫沒有變動,但是可以通過 F12 來看呀,按 F12 後,通過 NetWork 來檢視相關的請求和響應情況:

 

通過滾動幾下滑鼠滾輪之後,可以發現請求的 URL 還是有規律的:

https://www.oschina.net/blog/widgets/_blog_index_recommend_list?classification=428640&p=2&type=ajax
https://www.oschina.net/blog/widgets/_blog_index_recommend_list?classification=428640&p=2&type=ajax
https://www.oschina.net/blog/widgets/_blog_index_recommend_list?classification=428640&p=3&type=ajax
https://www.oschina.net/blog/widgets/_blog_index_recommend_list?classification=428640&p=4&type=ajax
https://www.oschina.net/blog/widgets/_blog_index_recommend_list?classification=428640&p=5&type=ajax

可以看到除了 p 的引數值不同的話,其他的都相同,p 就是分頁標識,p=2就表示第二頁,p=3就等於第三頁,以此類推,就可以獲取到更多的文章啦:

    def main(self, min_read_count, page_size):
        # 首頁 URL
        self.log.info("首頁##########################")
        main_url = "https://www.oschina.net/blog?classification=428640"
        self.run(main_url, min_read_count)
        # 第2頁到第page_size頁
        for page in range(2, page_size):
            self.log.info("第 {0} 頁##########################".format(str(page)))
            page_url = "https://www.oschina.net/blog/widgets/_blog_index_recommend_list?classification=428640&p=" + str(page) + "&type=ajax"
            self.run(page_url, min_read_count)
            time.sleep(2)

測試:

if __name__ == '__main__':
    article = OschinaArticle()
    # 獲取到20頁的相關文章,並且閱讀數要大於等於1000
    article.main(1000, 21)

日誌控制檯列印如下:

寫到檔案中如下:

可以看到,在 1-20 頁中,閱讀數大於等 1000 的文章有 114 篇,之後就可以 copy URL 到位址列進行閱讀啦....................

完整程式碼如下:

OschinaArticle 處理邏輯類:

# -*- coding:utf-8 -*-

import requests
from bs4 import BeautifulSoup
import logging
import time
from Article import Article

"""
    爬取開源中國上的文章,且閱讀數大於等於1000
"""


class OschinaArticle:

    def __init__(self):
        self.log = logging
        self.log.basicConfig(level=logging.INFO)
        self.file_line_num = 1

    def getSoup(self, url):
        """
        根據 url 獲取 BeautifulSoup 物件
        """
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/68.0.3440.75 Safari/537.36",
            "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
            "Host": "www.oschina.net"}
        response = requests.get(url, headers=headers)
        return BeautifulSoup(response.text, "html.parser")

    def get_articles(self, url):
        article_list = []
        beautiful_soup = self.getSoup(url)
        self.log.info(u"開始解析 HTML 頁面...")
        articles_div = beautiful_soup.find_all("div", class_="item blog-item")
        for article_div in articles_div:
            content_div = article_div.find("div", class_="content")
            header_div = content_div.find("a", class_="header")
            # 文章URL
            url = header_div["href"]
            # 文章標題
            title = header_div["title"]
            # 文章描述
            description_str = content_div.find("div", class_="description").find("p", class_="line-clamp")
            if description_str is None:
                continue
            description = description_str.text
            # 文章閱讀數
            read_count_div = content_div.find("div", class_="extra").find("div", class_="ui horizontal list").find("div", class_="item")
            # find_next_sibling 獲取兄弟節點
            read_count_i = read_count_div.find_next_sibling("div", class_="item").find_next_sibling("div", class_="item")
            read_count = read_count_i.getText()
            __article = Article(title, url, description, read_count)
            article_list.append(__article)
            # self.log.info(u"文章:標題《{0}》,閱讀數:{1},連結:{2}".format(title, read_count, url))
        return article_list

    def handler_read_count(self, article_list):
        """
        處理閱讀數:把 K 轉換為 1000
        :param article_list:文章列表
        """
        if article_list is None or len(article_list) == 0:
            self.log.info(u"文章列表為空...")
            return
        for article in article_list:
            read_count_str = article.read_cnt.strip()
            read_count = 0
            if isinstance(read_count_str, str):
                if read_count_str.endswith("K"):
                    read_count_str = read_count_str[:-1]  # 去掉K
                    read_count = int(float(read_count_str) * 1000)
                else:
                    read_count = int(read_count_str)
            article.read_cnt = read_count

    def get_article_by_read_count_sort(self, article_list, min_read_cnt):
        """
        獲取大於等於指定閱讀數的文章資訊, 並按照閱讀數從高到低排序
        :param article_list: 文章列表
        :param minx_read_cnt: 最小閱讀數
        :return:
        """
        if article_list is None or len(article_list) == 0:
            self.log.info(u"文章列表為空...")
            return
        article_list_return = []
        for article in article_list:
            if article.read_cnt >= min_read_cnt:
                article_list_return.append(article)
        article_list_return.sort(key=lambda Article: Article.read_cnt, reverse=True)
        return article_list_return

    def write_file(self, article_list, file_path):
        file = open(file_path + "/articles.txt", "a")

        for article in article_list:
            _article_str = str(article)
            file.write("(" + str(self.file_line_num) + ")" + _article_str)
            file.write("\n")
            file.write("--------------------------------------------------------------------------------------------------------------------------------------------------------")
            file.write("\n")
            self.file_line_num += 1
            time.sleep(0.2)  # 休眠 200 毫秒
        file.close()

    def run(self, url, min_read_count):
        # 獲取所有文章
        article_list = self.get_articles(url)
        # 對閱讀數進行處理
        self.handler_read_count(article_list)
        # 篩選閱讀數大於等於指定值,並按閱讀數從高到低排序
        _list = self.get_article_by_read_count_sort(article_list, min_read_count)
        # 寫檔案
        self.write_file(_list, "G:/python")
        # 列印控制檯
        for a in _list:
            self.log.info(a)

    def main(self, min_read_count, page_size):
        # 首頁 URL
        self.log.info("首    頁##########################")
        main_url = "https://www.oschina.net/blog?classification=428640"
        self.run(main_url, min_read_count)
        # 第2頁到第page_size頁
        for page in range(2, page_size):
            self.log.info("第 {0} 頁##########################".format(str(page)))
            page_url = "https://www.oschina.net/blog/widgets/_blog_index_recommend_list?classification=428640&p=" + str(page) + "&type=ajax"
            self.run(page_url, min_read_count)
            time.sleep(2)


if __name__ == '__main__':
    article = OschinaArticle()
    # 獲取到20頁的相關文章,並且閱讀數要大於等於1000
    article.main(1000, 21)