偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

coder-pig發表於2019-01-24

簡述

在上一節偷個懶,公號摳腚早報80%自動化——1.批量生成微信封面圖中,我們利用opencv庫 與PIL庫生成半年分量的微信封面圖,每次釋出直接選圖,美滋滋。按照劇本,本節我們的目標是:

編寫爬蟲定時去爬取新聞,儲存到本地資料庫中

具體點:

  • 1.編寫爬取新聞站點的爬蟲;
  • 2.把爬取到的新聞儲存到資料庫中;
  • 3.指令碼定時執行,每天早上8點清空前一天的資料,再去爬取新聞;

說到爬蟲,不得不提「爬蟲五步曲」:明確爬取目標,分析請求,模擬請求,解析資料,儲存資料。 本節以「澎湃新聞和爬取各類日報微博」為例講解,其他的站點也是大同小異,有興趣的讀者自行擴充套件。

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

不多嗶嗶,直接開始本節內容。

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞


1.爬取澎湃新聞


0x1 明確爬取目標

爬取站點:澎湃新聞:www.thepaper.cn/,網頁介面如下:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

確定爬取目標爬取首頁精選裡的新聞,採集新聞標題,概述,連結,來源等到資料庫中。


0x2 分析請求

首頁,滾動到底部,會載入更多,猜測是**Ajax動態載入**,F12開啟開發者工具,過濾XHR選項, 抓一波包,選項卡中出現一個請求:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

切換到Response選項卡,看下請求的響應資料:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

行吧,XML型別的資料,接著開始分析URL的規則,滾動到底部幾次,採集下請求的URL:

https://www.thepaper.cn/load_chosen.jsp?nodeids=25949&topCids=2838382,2840563,2840504,2840260,&pageidx=2&lastTime=1547170820108
https://www.thepaper.cn/load_chosen.jsp?nodeids=25949&topCids=2838382,2840563,2840504,2840260,&pageidx=3&lastTime=1547170216130
https://www.thepaper.cn/load_chosen.jsp?nodeids=25949&topCids=2838382,2840563,2840504,2840260,&pageidx=4&lastTime=1547166699225
複製程式碼

不難看出,www.thepaper.cn/load_chosen…Ajax介面的基地址pageidx頁碼lastTime時間戳;至於中間的中間這段:nodeids=25949&topCids=2838382,2840563,2840504,2840260,&pageidx=2, 可能需要我們猜測一下。nodeids猜測是分類id?首頁瞎點一下其他的分類,發現有兩類連結:

https://www.thepaper.cn/channel_26916
https://www.thepaper.cn/list_26912
複製程式碼

em...也是五位的數字,分別替換25949成試試?果然,channel_25949顯示的內容和精選的內容一致。 而在這個頁面滾動到底部同樣會載入更多,Ajax的請求URL為:

https://www.thepaper.cn/load_index.jsp?nodeids=&topCids=&pageidx=2&lastTime=1547175167792
複製程式碼

So,nodeids=25949就是固定的咯,然後是topCids=2838382,2840563,2840504,2840260,回到精選頁:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

這四個推,引起我的注意了,看下標題結點對應的HTML程式碼:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

嘖嘖嘖,行吧,這個topCids就是頂部的四個推薦對應的id,到此,請求URL的規則就摸清楚了。 另外,試了下把pageidx=2改成1,響應的結果和2一樣。所以還需要分兩步走:

  • 採集首頁未分頁
  • 載入的資料,然後再去請求Ajax的請求解析資料

接著是請求頭:

referer: https://www.thepaper.cn/
user-agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 Safari/537.36
x-requested-with: XMLHttpRequest
複製程式碼

0x3 模擬請求

使用PostMan模擬下請求URL,加上請求頭,可以,能拿到資料。

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

行吧,寫成Python程式碼:

import time
import requests as r

index_url = "https://www.thepaper.cn/"
ajax_base_url = "https://www.thepaper.cn/load_chosen.jsp?"
headers = {
    'referer': 'https://www.thepaper.cn/',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 '
                  'Safari/537.36',
    'x-requested-with': 'XMLHttpRequest'
}

# 獲取Ajax資料
def fetch_ajax_data():
    ajax_params = {
        'nodeids':25949,
        'topCids':'2840959,2840504,2840804,2841177,',
        'pageidx': 2,
        'lastTime': int(round(time.time() * 1000))
    }
    resp = r.get(ajax_base_url, params=ajax_params, headers=headers).text
    print(resp)
複製程式碼

0x4 解析資料

資料拿到了,接著就到解析資料了,資料是XML格式的,擷取其中一部分來分析:

<div class="news_li" id="cont2840440" contType="91">
    <div class="news_tu">
        <a href="newsDetail_forward_2840440" class="tiptitleImg" data-id="2840440" target="_blank">
            <img src="//image.thepaper.cn/image/14/160/210.jpg" alt="雲南副廳級官員因迎接遲到大罵縣委書記,他為何敢這麼狂?">
            <span class="p_time"></span>
        </a>
    </div>
    <h2>
        <a href="newsDetail_forward_2840440" id="clk2840440" target="_blank">雲南副廳級官員因迎接遲到大罵縣委書記,他為何敢這麼狂?</a>
    </h2>
    <p>
        和建,是一個怎樣的人?他為什麼在退休前後仍對權力戀戀不捨?從他的個案中可以汲取什麼啟示和教訓?
    </p>
    <div class="pdtt_trbs">
        <a href="govAffairs_index.jsp?nodeId=" target="_blank" class="govname">澎湃問政</a>
        <span>2小時前</span>
        <span class="trbszan">62</span>
    </div>
    <div id="last2" lastTime="1547174986415" pageIndex="2" style="display:none;"></div>
</div>
複製程式碼

關於XML的解析,Python中可以通過SAX,DOM,以及ElementTree三種方式來進行解析。 但是有個問題,這XML有點不對勁,所有元素都必須有關閉標籤,但是<img>那裡的標籤並沒有 結束標記。直接用ElementTree解析,直接就報錯了。如果想正常解析需要為每個img標籤 都加上'/',處置之外,你還要在外層裹一層標籤比如<start>資料</start>。那麼繁瑣, 直接寫個正則來提取我們想要的資料吧。先明確下我們想提取的資料結果: 新聞標題新聞描述新聞圖片連結新聞連結新聞來源新聞釋出時間。 接著利用正規表示式分組來提取資料:

ajax_pattern = re.compile(
    r'<a href="(.*?)".*?img src="(.*?)" alt="(.*?)".*?<p>(.*?)</p>.*?_blank">(.*?)</a>.*?<span>(.*?)</span>', re.S)


def fetch_ajax_data():
    ajax_params = {
        'nodeids': 25949,
        'topCids': '2840959,2840504,2840804,2841177,',
        'pageidx': 2,
        'lastTime': int(round(time.time() * 1000))
    }
    resp = r.get(ajax_base_url, params=ajax_params, headers=headers).text
    results = ajax_pattern.findall(resp)
    for result in results:
        print("\n新聞地址:", index_url + result[0])
        print("圖片地址:", 'http:' + result[1])
        print("新聞標題:", result[2])
        print("新聞描述:", result[3].replace('\n', '').replace(' ', ''))
        print("新聞來源:", result[4])
        print("新聞時間:", result[5])
複製程式碼

執行後的部分輸出結果如下:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

可以的,資料獲取到了,接著完善下這個方法,迴圈去獲取直到資料中出現1天前。 另外,為了避免訪問過於頻繁導致被封,每次訪問隨緣休眠0-3秒,因為指令碼是早上 8j點定時去爬取的,採集完可能我可能都還沒醒,耗時一點也沒啥,就不上代理了。 修改完後的程式碼:

def fetch_ajax_data():
    ajax_params = {
        'nodeids': 25949,
        'topCids': '2840959,2840504,2840804,2841177,',
    }
    pageidx = 2
    news_list = []  # 新聞列表
    while True:
        ajax_params['pageidx'] = pageidx
        ajax_params['lastTime'] = int(round(time.time() * 1000))
        resp = r.get(ajax_base_url, params=ajax_params, headers=headers)
        resp_content = resp.text
        print(resp.url)
        results = ajax_pattern.findall(resp_content)
        for result in results:
            if result[5] == '1天前':
                return news_list
            else:
                news_list.append([index_url + result[0], 'http:' + result[1], result[2],
                                  result[3].replace('\n', '').replace(' ', ''), result[4], result[5]])
        pageidx += 1
        time.sleep(random.randint(0, 2))
複製程式碼

部分輸出結果如下

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

前面也說了Ajax的介面並不能獲取所有資料,還需要去解析一波首頁,拿兩個東西:

  • 1.首頁進來後載入的資料
  • 2.頂部的幾個topCids引數

新聞的結點結構r

<div class="news_li" id="cont2845046" conttype="0">
        <div class="news_tu">
            <a href="newsDetail_forward_2845046" class="tiptitleImg" data-id="2845046" target="_blank"> <img src="//image1.thepaper.cn/image/14/202/688.jpg" alt="《紅色通緝》第二集:織網">
                <span class="p_time"></span>
                    </a>
        </div>
        <h2>
            <a href="newsDetail_forward_2845046" id="clk2845046" target="_blank">《紅色通緝》第二集:織網</a>
        </h2>
        <p>
                2015年4月25日,“百名紅通人員”名單公佈僅三天,就傳來了首名嫌犯落網的訊息,速度之快出人意料,也讓落網的第一人戴學民備受輿論關注。</p>
        <div class="pdtt_trbs">
            <a href="list_25424" target="_blank">一號專案</a>
                        <span>59分鐘前</span>
            <div class="trbstxt">推薦</div>
	</div>
複製程式碼

使用lxml庫來解析:

# 提取topCids的正則
cids_pattern = re.compile('&topCids=(.*?)&', re.S)

 # 提取首頁的新聞資料
    index_resp = r.get(index_url).text
    index_html = etree.HTML(index_resp)
    news_urls = index_html.xpath('//div[@class="news_li"]/div[@class="news_tu"]/a')  # 新聞連結列表
    imgs_urls = index_html.xpath('//div[@class="news_li"]/div[@class="news_tu"]/a/img')  # 新聞圖片列表
    overviews = index_html.xpath('//div[@class="news_li"]/p')  # 新聞簡介列表
    times = index_html.xpath('//div[@class="pdtt_trbs"]/span')  # 時間列表
    origins = index_html.xpath('//div[@class="pdtt_trbs"]/a')  # 來源列表
    for i in range(0, int(len(news_urls) / 2)):
        print(index_url + news_urls[i].get('href'))
        print('http:' + imgs_urls[i].get('src'))
        print(imgs_urls[i].get('alt'))
        print(overviews[i].text.replace('\n', '').replace(' ', ''))
        print(times[i].text)
        print(origins[i].text)
    # 正則提取topCids
    topCids = cids_pattern.search(index_resp)
    if topCids is not None:
        print('topCids:', topCids.group(1))
複製程式碼

部分輸出結果如下

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

然後再自行整合下程式碼,把採集到的新聞都放到同一個列表中,返回。

0x5 儲存資料

資料解析完了,接著就到資料儲存了,這裡我們使用MySQL來儲存資料,每次都傳字元有點繁瑣。 直接定義成一個類:

# 新聞類
class News:
    def __init__(self, title, overview, url, image, create_time, origin):
        self.title = title
        self.overview = overview
        self.url = url
        self.image = image
        self.create_time = create_time
        self.origin = origin

    def to_dict(self):
        return {'title': self.title, 'overview': self.overview, 'url': self.url, 'image': self.image,
                'create_time': self.create_time, 'origin': self.origin}
複製程式碼

接著寫一個資料庫操作類,Python中操作MySQL需要安裝一波pymysql庫:

pip install pymysql
複製程式碼

然後定義幾個運算元據庫的函式,顯示建立庫,表,以及刪除表:

# 建立資料庫
    def create_db(self):
        cursor = self.db.cursor()
        cursor.execute("CREATE DATABASE IF NOT EXISTS news CHARACTER SET UTF8MB4")
        cursor.close()

    # 建立表
    def create_table(self):
        self.db = pymysql.connect('localhost', user='root', password='Jay12345', port=3306, db='news')
        cursor = self.db.cursor()
        cursor.execute("CREATE TABLE IF Not Exists news("
                       "id INT AUTO_INCREMENT PRIMARY KEY,"
                       "title TEXT NOT NULL,"
                       "overview TEXT,"
                       "url TEXT NOT NULL,"
                       "image TEXT NOT NULL,"
                       "create_time TEXT NOT NULL,"
                       "origin  TEXT NOT NULL)")
        cursor.close()

    # 刪除表
    def delete_table(self):
        self.db = pymysql.connect('localhost', user='root', password='Jay12345', port=3306, db='news')
        cursor = self.db.cursor()
        cursor.execute("DROP TABLE news")
        cursor.close()
複製程式碼

接著是插入資料,不難寫出如下這樣的插入語句:

sql = "INSERT INTO news(title,overview,url,image,create_time,origin) VALUES 
(" + title + "," + overview + "," + url + "," + image + "," + create_time + "," + create_time + ")"
複製程式碼

這樣拼接除了長不好看,還繁瑣,容易出錯,我們可以使用格式化符%s來替代,然後在呼叫execute() 函式時把value值通過元組的方式傳入,優化下:

sql = "INSERT INTO news(title,overview,url,image,create_time,origin) VALUES (%s, %s, %s, %s, %s, %s)"
cursor.execute(sql, (title,overview,url,image,create_time,origin))
複製程式碼

還有個小的問題是,如果新增或刪除欄位,資料庫語句又要更改,傳入的元組也要改。 其實可以變通下,傳入一個動態變化的字典,然後SQL語句根據字典動態構造。

keys = ','.join(self.news_column_list)
values = ','.join(['%s'] * len(self.news_column_list))
sql = 'INSERT INTO news ({keys}) VALUES ({values})'.format(keys=keys, values=values)
cursor.execute(sql, tuple(news.to_dict().values()))jb 
複製程式碼

調整後的程式碼

    def __init__(self):
        self.db = pymysql.connect('localhost', user='root', password='Jay12345', port=3306)
        self.news_column_list = ['title', 'overview', 'url', 'image', 'create_time', 'origin']
        self.create_db()
        self.create_table()
        
    # 插入一條新聞
    def insert_news(self, news):
        cursor = self.db.cursor()
        try:
            keys = ','.join(self.news_column_list)
            values = ','.join(['%s'] * len(self.news_column_list))
            sql = 'INSERT INTO news ({keys}) VALUES ({values})'.format(keys=keys, values=values)
            cursor.execute(sql, tuple(news.to_dict().values()))
            self.db.commit()
        except Exception as e:
            print(str(e))
            self.db.rollback()
        finally:
            cursor.close()

    # 插入多條新聞
    def insert_some_news(self, some_news):
        cursor = self.db.cursor()
        try:
            keys = ','.join(self.news_column_list)
            values = ','.join(['%s'] * len(self.news_column_list))
            sql = 'INSERT INTO news ({keys}) VALUES ({values})'.format(keys=keys, values=values)
            for news in some_news:
                cursor.execute(sql, tuple(news.to_dict().values()))
            self.db.commit()
        except Exception as e:
            print(str(e))
            self.db.rollback()
        finally:
            cursor.close()    
複製程式碼

接著把前面直接print的都換成News類,傳參,新增到列表中,返回,然後呼叫批量插入函式,執行後可以在 資料庫中看到我們爬取到的新聞內容:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

em...資料是爬取到了,但是貌似有些問題,除了上面的建立時間不對外,還有:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

把提取的正則改下:

r'<a href="(.*?)".*?img src="(.*?)" alt="(.*?)".*?<p>(.*?)</p>.*?pdtt_trbs".*?<a.*?>(.*?)</a>.*?<span>(.*?)</span>', re.S)
複製程式碼

接著再次審視下看看爬取到的資料,到此,澎湃新聞的爬取完成,除了這個站點外,讀者還可j以如法炮製 爬取類似的新聞站點,比如下面的這些,這裡就不再做重複勞動的事咯~


2.抓取各類日報微博

0x1 明確抓取目標與請求分析

接著是爬取各類新聞日報的微博,爬取網站時,如果PC端的網頁驗證比較複雜,可以看看有沒有 M端(手機網頁端),試試從M端尋找突破口。比如新浪微博的M端:m.weibo.cn/ 可以通過:m.weibo.cn/u/使用者id 來開啟某個使用者的微博,比如:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

接著開啟抓包,過濾XHR,看下Ajax載入的請求URL:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

看下返回的資料:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

行吧,分析下URL的規律,基地址就不用說了:m.weibo.cn/api/contain… 接著是引數,可以確定的引數有如下兩個:

  • type=uid,固定
  • value=2803301701,使用者id

剩下的containerid和since_id,要跟一跟,先確定是不是固定的,滾多幾遍,看看新的URL:

https://m.weibo.cn/api/container/getIndex?type=uid&value=2803301701&containerid=1076032803301701&since_id=4328498967786823
https://m.weibo.cn/api/container/getIndex?type=uid&value=2803301701&containerid=1076032803301701&since_id=4328416646208508
複製程式碼

行吧containerid是不變的,而since_id應該是從上一個介面返回的,倒數第二請求搜下:4328416646208508,

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

果然,那就剩下containerid咯,把所有請求都清了,接著重新整理一波頁面,搜下:1076032803301701:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

但是這個URL中也有containerid啊,我都還沒拿到,怎麼拼URL?試試不加這個引數?訪問:

https://m.weibo.cn/api/container/getIndex?type=uid&value=2803301701
複製程式碼

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

0x2 模擬請求

嘖嘖,拿到一樣的資料,行吧,接著捋下模擬請求的流程:

  • 1.先獲取一波containerid
  • 2.拿著這個containerid去執行ajax請求,同時解析獲取since_id用作下次請求的引數。

先是獲取containerid:

# 獲取containerid
def fetch_container_id(userid):
    resp = r.get(ajax_url, headers=ajax_header, params={'type': 'uid', 'value': userid}).json()
    print(resp.get('data').get('tabsInfo').get('tabs')[1].get('containerid'))
複製程式碼

執行結果:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

行吧,成功獲取到containerid了,接著就是構造ajax請求了。第一個請求是沒有since_id的, 而後續請求則從上一次的ajax結果中獲取since_id作為下次請求的引數。

# 獲取微博
def fetch_weibo(userid, containerid):
    since_id = ''
    if since_id == '':
        resp = r.get(ajax_url, headers=ajax_header, params={'type': 'uid', 'value': userid, 'containerid':
            containerid}).json()
        cards = resp.get('data').get('cards')
        for card in cards:
            mblog = card.get('mblog')
            print("建立時間:", mblog.get('created_at'))
            print("text:", mblog.get('text'))
複製程式碼

部分輸出結果如下

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

另外,在測試分頁的時候,發現一個很奇怪的問題,since_id拿不到???同一個連結,Requests拿到的結果 和瀏覽器拿到的結果竟然不一樣,儘管我已經設定了各種Header:

# 請求URL
https://m.weibo.cn/api/container/getIndex?type=uid&value=2803301701&containerid=1076032803301701

# Requests請求的響應結果
{'ok': 1, 'data': {'cardlistInfo': {'containerid': '1076032803301701', 'v_p': 42, 'show_style': 1, 'total': 95084, 'page': 2}

# 瀏覽器訪問結果
{"ok": 1, "data": {"cardlistInfo": {"containerid": "1076032803301701", "v_p": 42, "show_style": 1, "total": 95084, "since_id": 4328880058268227}
複製程式碼

有知道怎麼拿到since_id的童鞋可以在評論區留言。搜了下網上的方案,直接用page引數,而非since_id。

0x3 解析資料

一開始,我是想著去解析微博標題的,然後去解析標題,提取資料,但是處理起來有點繁瑣。 後來想想還是算了,即刻APP也沒做這個,點選新聞是直接跳微博詳情的。 詳情頁的url規則:

https://m.weibo.cn/detail/{微博id}
複製程式碼

所以這裡要做只是解析:新聞標題新聞建立時間,獲取微博id拼接URL作為新聞詳情頁的連結。 先提取一波不同樣式的微博資訊:

【送別林清玄 重溫他筆下的愜意與淡然】23日,臺灣知名作家林清玄過世,終年65歲。他的作品常常出現在《讀者》《青年文摘》上,每個青少年或許都曾讀過。“所有時間裡的事物,都永遠不會回來了。”“和時間賽跑”的人,永遠不會回來了。重溫,送別! 

【教科書式教育!公交車禮讓行人,家長教孩子鞠躬致謝<span class="url-icon"><img alt=[贊] src="//h5.sinaimg.cn/m/emoticon/icon/others/h_zan-6e88e6f51d.png" style="width:1em; height:1em;" /></span>】近日,山東青島,一輛公交車在斑馬線禮讓行人時,一位家長牽著孩子過馬路,一邊跟孩子說著什麼,一遍彎腰教孩子鞠躬致謝。公交公司的工作人員表示,平時禮讓常遇到路人豎大拇指的、微笑的,第一次遇到教育孩子感恩的。<a data-url="http://t.cn/E5lZLD1" href="http://miaopai.com/show/23UZs~CF6PAqyDdQ25HYMatq5jujmg0BQ4yfsQ__.htm?showurl=http%3A%2F%2Fmiaopai.com%2Fshow%2F23UZs%7ECF6PAqyDdQ25HYMatq5jujmg0BQ4yfsQ__.htm&url_open_direct=1&toolbar_hidden=1&url_type=39&object_type=video&pos=1&containerid=230442744560b81311e0be36d04147970e4dcf&luicode=10000011&lfid=1076032803301701" data-hide=""><span class='url-icon'><img style='width: 1rem;height: 1rem' src='https://h5.sinaimg.cn/upload/2015/09/25/3/timeline_card_small_video_default.png'></span><span class="surl-text">微辣Video的秒拍視訊</span></a> 

<a  href="https://m.weibo.cn/search?containerid=231522type%3D1%26t%3D10%26q%3D%23%E5%AE%88%E6%8A%A4%E5%AE%9D%E8%B4%9D%23&isnewpage=1&luicode=10000011&lfid=1076032803301701" data-hide=""><span class="surl-text">#守護寶貝#</span></a>【<span class="url-icon"><img alt=[話筒] src="//h5.sinaimg.cn/m/emoticon/icon/others/o_huatong-9f86617336.png" style="width:1em; height:1em;" /></span>急轉尋人!福建12歲女孩昨日走失】陳燕娟(女,12歲),1月22日12時40分許,從福建莆田荔城區拱辰街道富力小區走失。走失時,她上身穿紫色外套,下身穿牛仔褲,戴粉色眼鏡。如有線索,請迅速與警方聯絡:13799610199。速擴!<a href='/n/公安部兒童失蹤資訊緊急釋出平臺'>@公安部兒童失蹤資訊緊急釋出平臺</a> 


【奶白鯽魚湯<span class="url-icon"><img alt=[饞嘴] src="//h5.sinaimg.cn/m/emoticon/icon/default/d_chanzui-ad3f4f182c.png" style="width:1em; height:1em;" /></span>】鯽魚湯這樣做,湯白如牛奶又沒腥味,喝一口根本停不下來!趕緊馬走試試~(吃貨祕籍) 

【春運首日傳送旅客6754.5萬人次 你坐火車還是飛機回家?】21日,<a  href="https://m.weibo.cn/search?containerid=231522type%3D1%26t%3D10%26q%3D%232019%E6%98%A5%E8%BF%90%23&luicode=10000011&lfid=1076032803301701" data-hide=""><span class="surl-text">#2019春運#</span></a>首日,全國鐵路、道路、水路、民航共傳送旅客6754.5萬人次,比去年同期增長1.7%。其中鐵路傳送旅客953.2萬人次,民航傳送旅客166.2萬人次。<a data-url="http://t.cn/E5TC2Xl" href="https://media.weibo.cn/article?object_id=1022%3A2309351000024331564912780165&extparam=lmid--4331573362959560&luicode=10000011&lfid=1076032803301701&id=2309351000024331564912780165" data-hide=""><span class='url-icon'><img style='width: 1rem;height: 1rem' src='https://h5.sinaimg.cn/upload/2015/09/25/3/timeline_card_small_article_default.png'></span><span class="surl-text">春運首日傳送旅客6754.5萬人次 你坐火車還是飛機回家</span></a> 千山萬水,回家的路最美。今年,你的回家路,是什麼? 

【畫面引起極度舒適!這些<a  href="https://m.weibo.cn/search?containerid=231522type%3D1%26t%3D10%26q%3D%23%E9%AB%98%E6%83%85%E5%95%86%E6%89%A7%E6%B3%95%E7%9E%AC%E9%97%B4%23&extparam=%23%E9%AB%98%E6%83%85%E5%95%86%E6%89%A7%E6%B3%95%E7%9E%AC%E9%97%B4%23&luicode=10000011&lfid=1076032803301701" data-hide=""><span class="surl-text">#高情商執法瞬間#</span></a>,真耐看<span class="url-icon"><img alt=[good] src="//h5.sinaimg.cn/m/emoticon/icon/others/h_good-55854d01bb.png" style="width:1em; height:1em;" /></span>】一則“交警要求司機撿起車窗丟擲垃圾”的新聞引發好評。鐵面無私、執法必嚴是警察的擔當,而這些既講道理又有溫度的“高情商”更加珍貴,讓人感覺極度舒適。人性化執法,有溫度!<a data-url="http://t.cn/Eq8OzkF" href="http://miaopai.com/show/3W~4f56NCb1YfSaGEsG7jmTyiNYgVO3PiN7RDg__.htm?showurl=http%3A%2F%2Fmiaopai.com%2Fshow%2F3W%7E4f56NCb1YfSaGEsG7jmTyiNYgVO3PiN7RDg__.htm&url_open_direct=1&toolbar_hidden=1&url_type=39&object_type=video&pos=1&containerid=230442e07076f4d85282f6d54242841ee01b15&luicode=10000011&lfid=1076032803301701" data-hide=""><span class='url-icon'><img style='width: 1rem;height: 1rem' src='https://h5.sinaimg.cn/upload/2015/09/25/3/timeline_card_small_video_default.png'></span><span class="surl-text">人民日報的秒拍視訊</span></a> 
複製程式碼

觀察一波規律,不難發現標題都放在【】裡,寫個正則提取一波即可:

title_pattern = re.compile(r'【(.*?)】', re.S)
複製程式碼

部分解析結果如下:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

接著到資料清洗了,先過濾一波表情標籤

<span class="url-icon"><img alt=[贊] src="//h5.sinaimg.cn/m/emoticon/icon/others/h_zan-6e88e6f51d.png" style="width:1em; height:1em;" /></span>
複製程式碼

直接編寫正則,直接把這一部分替換為空字串。

# 過濾emoji表情的正則
emoji_filter_pattern = re.compile(r'<span class="url-icon">.*?</span>', re.S)
# 把表情替換為空格
title = emoji_filter_pattern.sub('', title)
複製程式碼

行吧,替換完了,接著是下面這種標題的處理:

中國交通<a  href="https://m.weibo.cn/search?containerid=231522type%3D1%26t%3D10%26q%3D%23%E5%8D%81%E5%B9%B4%E5%AF%B9%E6%AF%94%E6%8C%91%E6%88%98%23&extparam=%23%E5%8D%81%E5%B9%B4%E5%AF%B9%E6%AF%94%E6%8C%91%E6%88%98%23&luicode=10000011&lfid=1076032803301701" data-hide=""><span class="surl-text">#十年對比挑戰#</span></a>
複製程式碼

需要提取一波a標籤裡的,兩個#號夾著的文字,依舊是正則提取一波:

a_surl_text_pattern = re.compile(r'(.*?)<a.*?<span class="surl-text">#(.*?)#</span></a>(.*?)')
複製程式碼

提取後,發現還有下面這樣的資料:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

同樣寫個正則,過濾一波a標籤,提取文字:

a_text_pattern = re.compile(r'(.*?)<a.*?>(.*?)</a>') 
複製程式碼

爬個30頁看看結果標題是否正確,確認無問題後,接著就是獲取日期和微博id咯:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

0x4 儲存資料

行吧,資料都拿到了,接著就是存資料庫裡咯,和上面那個一樣,直接存資料庫中。 比較簡單,我們另外加一個字典,用來儲存日報微博的名字和uid,比如這些:

news_ids = {
    '人民日報': '2803301701',
    '廣州日報': '1887790981',
    '南方日報': '1682207150',
    '中國日報': '1663072851',
    '光明日報': '1402977920',
    '央視新聞': '2656274875',
    '環球時報': '1974576991',
    '澎湃新聞': '5044281310',
    '頭題新聞': '1618051664',
}
複製程式碼

加上迴圈,最後的完整程式碼如下:

"""
抓取新浪微博的爬蟲
"""
import requests as r
import time
import random
import re

from DBHelper import News, DBHelper

index_url = 'https://m.weibo.cn/'  # 首頁基地址
wb_detail_base_url = 'https://m.weibo.cn/detail/'  # 微博詳情頁基地址
ajax_url = index_url + 'api/container/getIndex'  # ajax請求基地址
ajax_header = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.92 '
                  'Safari/537.36',
    'X-Requested-With': 'XMLHttpRequest'
}
news_ids = {
    '人民日報': '2803301701',
    '廣州日報': '1887790981',
    '南方日報': '1682207150',
    '中國日報': '1663072851',
    '光明日報': '1402977920',
    '央視新聞': '2656274875',
    '環球時報': '1974576991',
    '澎湃新聞': '5044281310',
    '頭題新聞': '1618051664',
}
title_pattern = re.compile(r'【(.*?)】', re.S)  # 新聞標題獲取正則
emoji_filter_pattern = re.compile(r'<span class="url-icon">.*?</span>', re.S)  # 新聞標題表情過濾正則
a_surl_text_pattern = re.compile(r'(.*?)<a.*?<span class="surl-text">#(.*?)#</span></a>(.*?)')  # 提取連結中的文字
a_text_pattern = re.compile(r'(.*?)<a.*?>(.*?)</a>')  # 提取連結中的文字

abstract_pattern = re.compile(r'', re.S)  # 新聞概述提取正則
news_url_pattern = re.compile(r'<a\s+href="(.*)?".*?#(.*?)#', re.S)  # 新聞概述提取正則


# 獲取containerid
def fetch_container_id(userid):
    resp = r.get(ajax_url, headers=ajax_header, params={'type': 'uid', 'value': userid}).json()
    return resp.get('data').get('tabsInfo').get('tabs')[1].get('containerid')


# 獲取微博
def fetch_weibo(userid, containerid, username):
    news_list = []
    cur_page = 1
    while True:
        resp = r.get(ajax_url, headers=ajax_header, params={'type': 'uid', 'value': userid, 'containerid':
            containerid, 'page': cur_page})
        print('爬取【%s】:%s' % (username, resp.url))
        resp = resp.json()
        cards = resp.get('data').get('cards')
        for card in cards:
            mblog = card.get('mblog')
            text = mblog.get('text')
            title_result = title_pattern.search(text)
            if title_result is not None:
                title = title_result.group(1)
                # 過濾表情
                title = emoji_filter_pattern.sub('', title)
                # 過濾超連結
                a_text_result = a_surl_text_pattern.search(title)
                if a_text_result is not None:
                    title = a_text_result.group(1) + a_text_result.group(2) + a_text_result.group(3)
                title = a_text_pattern.sub('', title)
                if mblog.get('created_at').find('前') == -1:
                    return news_list
                news_list.append(
                    News(title, '', wb_detail_base_url + mblog.get('id'), '', mblog.get('created_at'), username))
        cur_page += 1
        time.sleep(random.randint(5, 10))


if __name__ == '__main__':
    helper = DBHelper()
    for key, value in news_ids.items():
        result_list = fetch_weibo(value, fetch_container_id(value), key)
        helper.insert_some_news(result_list)

複製程式碼

部分執行結果如下

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

到此,各類日報的微博就爬取完了,另外每個微博號對應的containerid是不變的, 你還可以把這些直接存到資料庫中,這樣,就不用每次執行指令碼都去請求一次了。


3.把指令碼丟伺服器上定時執行

0x1 安裝MySQL

把程式碼丟到遠端伺服器上,筆者使用的是騰訊雲主機,先執行一波下述命令安裝MySQL:

# 安裝MySQL服務,輸入Y後,如圖會讓你輸入密碼,重複輸入確認
sudo apt-get install mysql-server

# 安裝MySQL客戶端
sudo apt-get install mysql-client

# 安裝libmysqlclient,輸入Y
sudo apt-get install libmysqlclient-dev
複製程式碼

安裝完後執行一波指令碼,缺什麼庫裝什麼庫,指令碼跑起來就行。

0x2 DataGrip連線遠端MySQL

MySQL預設是不允許遠端訪問的,下面簡單說下使用DataGrip連線遠端資料庫的流程。

  • 1.雲伺服器開啟安全組裡的3306埠,如圖:

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

  • 2.停止MySQL服務
sudo /etc/init.d/mysql stop
複製程式碼
  • 3.修改my.cnf檔案,註釋掉bind-address = 127.0.0.1,鍵入wq儲存退出;
vim /etc/mysql/my.cnf
複製程式碼
  • 4.啟動mysql服務
/etc/init.d/mysql start
複製程式碼
  • 5.輸入下述命令檢視當前3306埠的狀態
netstat -an|grep 3306
複製程式碼
  • 6.修改使用者訪問許可權
mysql -u root -p    # 使用者登入
use mysql;  # 選中mysql資料庫
GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '密碼' WITH GRANT OPTION;    # 授權
FLUSH PRIVILEGES;   # 更新許可權
EXIT # 退出mysql
複製程式碼

注:上面設定的結果是所有ip都能訪問資料庫,如需指定特定ip才能訪問的話, 可以把'@'%改成特定ip。還有這裡用的是root賬戶,你可以通過下述命令建立 一個新的使用者,然後用這個使用者進行訪問,可以在此做一些許可權控制操作。

CREATE USER 新使用者 IDENTIFIED BY '密碼';
GRANT ALL PRIVILEGES ON *.* TO '新使用者'@'%' IDENTIFIED BY '密碼' WITH GRANT OPTION;    # 授權
FLUSH PRIVILEGES;
複製程式碼
  • 7.開啟DataGrip,以此點選:File -> New -> DataSource -> MySQL,如圖依次配置 General和SSH/SSL選項卡,接著點選Apply和Test Connection。

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

出現如圖所示的Successful,代表連線成功。

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

0x3 使用crontab定時執行指令碼

在linux中可以使用cron來執行一些定時任務,linux自帶,預設開機就自動啟動, 啟動後會讀取所有配置檔案(全域性配置檔案**/etc/crontab** 和 每個使用者的計劃任務配置檔案), cron會根據命令和執行時間來排程工作任務。cron的常用命令如下:

sudo service cron restart   # 重啟cron
sudo service cron stop      # 停止cron
sudo service cron start     # 啟動cron
sudo service cron status    # 檢視cron狀態
複製程式碼

除此之外,還有下述命令可以操作crontab

sudo crontab –l     # 顯示crontab檔案
sudo crontab –e     # 修改crontab檔案
sudo crontab –r     # 刪除crontab檔案
sudo crontab –ir    # 刪除crontab檔案前提醒使用者
複製程式碼

接著執行上面的sudo crontab –e命令,修改一波計劃任務,命令的格式如下:

* * * * * command

引數依次為

  • 分:0 - 59
  • 時:0 - 23
  • 天:1 - 31
  • 月:1 - 12
  • 周:0 - 6
  • 執行的命令

除此之外還可以使用萬用字元:

  • *任意值,比如在天的位置寫*代表每天)
  • ,(允許在某一個位填多個值,表示某幾個時間段執行,逗號分隔)
  • /(斜線,配合*使用,代表每隔多長時間,比如在小時位寫/2代表每隔兩小時)

大概規則就這些,接著在配置檔案中新增執行命令,8點鐘刪一波表,8點1分抓澎湃新聞, 8點15分抓新浪微博的新聞。具體內容如下:

0 8 * * * python3 /home/ubuntu/AutoNews/DBHelper.py > /home/ubuntu/AutoNews/db.txt            
1 8 * * * python3 /home/ubuntu/AutoNews/PenpaiSpider.py > /home/ubuntu/AutoNews/pp.txt        
15 8 * * * python3 /home/ubuntu/AutoNews/WeiboSpider.py > /home/ubuntu/AutoNews/wb.txt  
複製程式碼

配置完後,鍵入:sudo service cron restart,重啟一波,然後就可以坐等第二天早上看看結果咯(日誌檔案的建立日期)。

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

如果你不想動/etc/crontab這個全域性配置檔案,也可以單獨寫一個cron的檔案,比如:news.cron, 接著鍵入下述命令新增定時任務:

crontab news.cron > news.log
複製程式碼

新增後可以鍵入:crontab -l 檢視是否配置成功,或者看下/var/spool/cron目錄下是否生成了對應的指令碼。 (注:此方法會直接替換該使用者下的crontabs,而不是新增)


行吧,本節的內容就這麼多,直接在DataGrip上看新聞還是有些不方便, 下節,利用Flask寫幾個介面,可能會好看一些,有疑問的歡迎在評論區留言,謝謝~

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

Tips:公號目前只是堅持發早報,在慢慢完善,有點心虛,只敢貼個小圖,想看早報的可以關注下~

偷個懶,公號摳腚早報80%自動化——2.手撕爬蟲定時爬新聞

相關文章