Python3.X 爬蟲實戰(先爬起來嗨)

工匠若水發表於2017-06-13

【工匠若水 http://blog.csdn.net/yanbober 未經允許嚴禁轉載,請尊重作者勞動成果。私信聯絡我

1 背景

爬蟲的價值就不多說了,Python 的便捷與強大也就不 BB 了,在這個資料氾濫、追求效率的時代,使用 Python 可以為我們創造相當多的便捷,Web 開發、桌面小工具開發、粘性指令碼編寫、大資料處理、影像處理、機器學習等等,能做到的事情實在太多。就拿一個再隨便不過的需求來說吧,當我們在做 Android 開發時想將現有 drawable 目錄下 *.png 圖片全部自動轉換為 webp 格式時,我們一般的套路可能都是藉助第三方工具(很多都只能一張一張轉換),而使用 Python 核心就兩行程式碼就可以做到這一點,如果想批量自定義轉換姿勢(路徑、檔名等),使用 Python 也是非常方便的,譬如這裡就有一個我寫的非常簡單的 Python png 批量轉換 webp 工具。具體原始碼如下:

#! /usr/bin/env python3

from PIL import Image
from glob import glob
import os
"""
說明:僅提供核心基礎思想和指令碼,自己可以改善為自動識別 Android 工程全部轉換。
1. 將該指令碼放置在自己 Android png 目錄下;
2. 執行命令 python3 image2webp.py;
3. 在該目錄下的 output 目錄下生成當前資料夾下所有 png 圖片對應的 webp 圖片;
"""

def image2webp(inputFile, outputFile):
    try:
        image = Image.open(inputFile)
        if image.mode != 'RGBA' and image.mode != 'RGB':
            image = image.convert('RGBA')

        image.save(outputFile, 'WEBP')
        print(inputFile + ' has converted to ' + outputFile)
    except Exception as e:
        print('Error: ' + inputFile + ' converte failed to ' + outputFile)

matchFileList = glob('*.png')
if len(matchFileList) <= 0:
    print("There are no *.png file in this directory (you can run this script in your *png directory)!")
    exit(-1)

outputDir = os.getcwd() + "/output"
for pngFile in matchFileList:
    fileName = pngFile[0:pngFile.index('.')]
    if not os.path.exists(outputDir):
        os.makedirs(outputDir)
    image2webp(pngFile, outputDir + "/" + fileName + ".webp")

print("Converted done! all webp file in the output directory!")

震撼吧,人生苦短,我用 Python!真的是這樣咯,不過這一系列我們不探討 Python 的其他奧妙,而是直接探討一個垂直領域 —— Python 爬蟲。其實雙贏的爬蟲(搜尋引擎收錄爬蟲就是共贏的,地下黑作坊在網上肆意洗資料,譬如洗郵箱資料就是被抵制或非法的)對於大多數網站來說是有利的,而惡意的爬蟲就適得其反了。正常來說我們想要獲取某些網站資料應該通過他們的開放 API 進行合法授權訪問,但是企業畢竟是企業,都是有所保留的開放 API 許可權,所以有時候我們不得不使用暴力手段來洗劫有價值的資料,這也就是爬蟲存在的一大價值。

這裡寫圖片描述

【工匠若水 http://blog.csdn.net/yanbober 未經允許嚴禁轉載,請尊重作者勞動成果。私信聯絡我

2 爬蟲基礎

爬蟲其實涉及的東西還是比較雜和多的,比較重要的幾點可能就是得掌握 Python 語法基礎和一些常用的內建或者擴充模組、熟悉 WEB 開發的相關知識、熟悉資料持久化(關係型資料庫、非關係型資料庫、檔案)快取等一些技術、熟悉正則等。

2-1 約定俗成的潛規則

對 WEB 瞭解的朋友都知道一般的網站都會有 robots.txt 和 Sitemap 定義,這些定義其實對我們進行合理化的爬蟲編寫是具備指導意義的,譬如我們看下稀土掘金(https://juejin.im)這個網站的 robots.txt 檔案(https://juejin.im/robots.txt),如下:

User-agent:*
Disallow:/timeline
Disallow:/submit-entry
......
Disallow:/subscribe/all?sort=newest
Disallow:/search

Sitemap:https://juejin.im/sitemap/sitemappart1.xml
......
Sitemap:https://juejin.im/sitemap/sitemappart4.xml

robots.txt 中定義的 Sitemap,訪問(https://juejin.im/sitemap/sitemappart1.xml)如下:

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://juejin.im/</loc>
<priority>1.0</priority>
<changefreq>always</changefreq>
</url>
<url>
<loc>https://juejin.im/welcome/android</loc>
<priority>0.8</priority>
<changefreq>hourly</changefreq>
</url>
......
</urlset>

可以看到 robots.txt 檔案內容明確建議(注意:只是建議,只是建議,只是建議,惡意的爬蟲管你這屁建議呢)了爬蟲程式爬取該網站時有哪些限制,一般遵守這些限制規則就能很好的降低自己爬蟲被封的風險;Sitemap 提供了網站幾乎所有的頁面列表,我們可以使用這個列表直接爬取這個站點,也可以自己採用別的方式,因為這玩意不是每個網站都有的。所以說 robots.txt 和 Sitemap 只是約定俗成的潛規則,潛規則,一般我們酌情遵守就行了,譬如可以考慮遵守他們提到的訪問請求間隔、代理禁止型別等,其他的就看你自己的節操了。

2-2 基本工具

俗話說“工欲善其事必先利其器”,爬蟲也需要一些利器。對於 Python 開發工具我選擇 PyCharm 和 Sublime;對於瀏覽器可以選擇 Chrome 等,再安裝一些 WEB 開發外掛,譬如 FireBug、Wappalyzer、Chrome Sniffer 等,方便爬蟲時分析網站,尤其是瀏覽器 F12 大法和清空站點 Cookie 一定要掌握,不然就沒法愉快的玩耍爬蟲了。當然了,爬蟲的核心之一其實在於抓取到資料後如何篩選出對自己有價值的資料,關於做到和做好這一點我們有必要對站點頁面有一個比較準確的把握,想要做到這一點就必須大致知道該網頁使用了那些技術,這樣就可以提高我們分析頁面的效率。分析網頁使用哪些技術有很多方法,也有很多瀏覽器外掛,譬如上面提到的 Chrome Sniffer等;也可以直接訪問 https://builtwith.com/ 網站輸入你要爬取的網頁進行識別;當然也可以用 Python 的 builtwith 模組來獲取,悲哀的是這個模組現在不支援 Python 3.X 版本,需要自己安裝後手動修改。

當然了,還有一個不常用的利器需要知道,那就是度娘和 Google 咯,為啥呢?因為有時候我們大型專案可能需要先大致評估全站點爬蟲的量有多大來進行相關爬蟲的技術選型參考,所以有必要知彼知己。下面就以稀土掘金為例說明,如圖:
這裡寫圖片描述
可以看到,通過 site 指令度娘搜尋告訴我們稀土掘金這個站點大約有 27256 個頁面(這只是參考值,不完全準確),當我們真的需要全站點爬蟲攻擊時就要考慮這麼大量情況下的爬蟲方案選型和策略,以便保證爬蟲的效率。

2-3 基本爬取技術思路

爬蟲涉及的通用技術最核心的可能就是 HTTP 請求了,我們至少至少應該掌握 HTTP 的 POST 和 GET 請求方法;其次就是 HTTP 請求和返回的 Header 含義及如何使用瀏覽器等工具跟蹤請求 Header,因為爬蟲連結請求時出現問題最多的情況一般都是 Header 有問題,譬如通常至少要保證 User-Agent、Referer、Cookie 等的偽裝正確性,返回 Header 裡的重定向連結,Gzip 資料需要解壓等;還有就是 POST 資料的 urlencode 包裝傳送等;所以在進行爬蟲前一定要具備比較紮實的前端與後端基礎知識,同時要具備比較充足的 HTTP 知識。

有了這些知識我們可能就會急於開始爬取,其實這是不對的,我們應該做的第一件事是對要爬取的站點進行分析,至於如何分析,下面給出了一些常規套路:

  1. 首先倒騰下看你要抓取的站點有沒有響應式的移動頁面,如果有那就保持一個原則,儘可能的抓取他們的移動頁面(原因就是一般移動頁面都是內容乾貨啊,相對 PC 頁面沒那麼臃腫,方便分析)。

  2. Cookie 的操蛋之處,分析時建議開啟隱身模式等,不然就面對清空 Cookie 大法了,清空 Cookie 對於爬蟲網站分析至關重要,一定要 get 到。

  3. 分析爬取網頁是靜態頁面還是動態頁面,以便採取不同的爬取策略,使用不同的爬取工具。

  4. 檢視網頁原始碼找出對你有價值的資料的網頁排版規律,譬如特定 CSS 選擇等,從而指定抓取後的資料解析規則。

  5. 清洗資料後選擇如何處理抓取到的有價值資料,譬如是儲存還是直接使用,是如何儲存等。

以上幾個套路摸索清楚以後就可以開始編寫爬蟲程式碼了,不過這時候還是有很多程式碼套路需要注意的,譬如 URL 的重複爬取、無效 URL 的剔除、爬蟲欺騙、爬取異常處理等,如果想要自己的爬蟲十分健壯,上面這些套路似乎都是必須要考慮的。

當然了,上面說的只是爬蟲基礎的核心事項,大型爬蟲專案涉及的知識點就更加瑣碎了,隨著這個系列的漸進,我們會慢慢接觸到的,下面我們先小牛試刀一把。

【工匠若水 http://blog.csdn.net/yanbober 未經允許嚴禁轉載,請尊重作者勞動成果。私信聯絡我

3 實戰一把,先爬起來嗨

BB 了辣麼多基礎,還沒有任何實戰,搞毛線啊!Talk is easy, show me the code!既然是 Python3.X 爬蟲實戰系列,所以我們先讓自己爬起來,故我們先來看看一個爬蟲的常規套路流程結構,如下圖(此圖引用自網路):

這裡寫圖片描述

看到了吧,一個爬蟲的核心流程其實就是拿到一個 URL,下載下來這個 URL 指定的資料(網頁或者結構化資料),解析出有價值的資料供自己使用,所以其實爬蟲的核心機制流程就是不停的重複執行這個流程,日復一日的幫你在那各種爬呀爬呀爬。

依據上面的爬蟲流程圖,下面我們給出一個簡單的爬蟲程式,以便理解和感覺爬蟲的魅力。下面是一個深度爬取百度百科 Android 詞條簡介及其衍生詞條簡介的例項,具體可以點選我在 github 檢視該爬蟲模組原始碼,這個小爬蟲程式不是那麼健壯,但是足以說明上面的流程圖,該小爬蟲包結構如下圖:
這裡寫圖片描述
我們在命令列執行 python3 spider_main.py 或者在 PyCharm 中點選 spider_main.py 檔案右鍵執行就能看到爬蟲開始爬取資料了(注意:該小爬蟲依賴 BeautifulSoup 外部模組,如果沒安裝建議執行前先使用 pip 進行安裝,命令為 pip install beautifulsoup4;其次該小爬蟲預設只深度爬取 30 個連結),最終 30 個連結爬取完成後會在當前目錄下自動輸出了一個名為 out_2017-06-13_21:55:57.html 的 HTML 頁面的表格,我們可以開啟檔案發現爬取的結果如下:

這裡寫圖片描述

怎麼樣?我們爬取了百度百科一些關於 Android 和深度連結的名詞介紹,然後依據自己喜好輸出了一張 WEB 頁面,當然咯,我們可以把這些資料寫入資料庫,再用 PHP 等編寫 RESTFUL 介面通過 JSON 結構化語句返回給 APP 使用,贊不讚,再也不用為了自己做個小 App 到處去尋找免費的 API(譬如去聚合資料尋找),完全可以解放雙手自動抓取和使用,不過一定不要未經授權直接抓取給商業 APP 使用,這可能會被起訴的。

下面是 https://github.com/yanbober/SmallReptileTraining/tree/master/AndroidSpider 這個小爬蟲的原始碼,大家可以對照上面的爬蟲流程圖進行對比。

'''
spider_main.py 上面爬蟲流程圖中的[排程器]
物件導向寫法,排程器負責迴圈從 UrlManager 獲取爬取連結,然後交給 HtmlDownLoader 下載,然後把下載內容交給 HtmlParser 解析,然後把有價值資料輸出給 HtmlOutput 進行應用。
'''
class SpiderMain(object):
    def __init__(self):
        self.urls = url_manager.UrlManager()
        self.downloader = html_downloader.HtmlDownLoader()
        self.parser = html_parser.HtmlParser()
        self.out_put = html_output.HtmlOutput()

    def craw(self, root_url):
        count = 1
        self.urls.add_new_url(root_url)
        while self.urls.has_new_url():
            try:
                new_url = self.urls.get_new_url()
                print("craw %d : %s" % (count, new_url))
                html_content = self.downloader.download(new_url)
                new_urls, new_data = self.parser.parse(new_url, html_content, "utf-8")
                self.urls.add_new_urls(new_urls)
                self.out_put.collect_data(new_data)
                #預設只爬取了深度 30,不然太慢,自己可以修改。
                if count >= 30:
                    break
                count = count + 1
            except Exception as e:
                print("craw failed!\n"+str(e))
        self.out_put.output_html()

if __name__ == "__main__":
    rootUrl = "http://baike.baidu.com/item/Android"
    objSpider = SpiderMain()
    objSpider.craw(rootUrl)
'''
url_manager.py 上面爬蟲流程圖中的[URL 管理器]
負責管理深度 URL 連結和去重等機制。
'''
class UrlManager(object):
    def __init__(self):
        self.new_urls = set()
        self.used_urls = set()

    def add_new_url(self, url):
        if url is None:
            return
        if url not in self.new_urls and url not in self.used_urls:
            self.new_urls.add(url)

    def add_new_urls(self, urls):
        if urls is None or len(urls) == 0:
            return
        for url in urls:
            self.add_new_url(url)

    def has_new_url(self):
        return len(self.new_urls) > 0

    def get_new_url(self):
        temp_url = self.new_urls.pop()
        self.used_urls.add(temp_url)
        return temp_url
'''
html_downloader.py 上面爬蟲流程圖中的[下載器]
負責對指定的 URL 網頁內容進行下載獲取,這裡只是簡單處理了 HTTP CODE 200,實質應該依據 400、500 等分情況進行重試等機制處理。
'''
class HtmlDownLoader(object):
    def download(self, url):
        if url is None:
            return None
        response = urllib.request.urlopen(url)
        if response.getcode() != 200:
            return None
        return response.read()
'''
html_parser.py 上面爬蟲流程圖中的[解析器]
負責對下載器下載下來的網頁內容進行解析,解析規則就是我們自己定義的感興趣的內容,這裡我們只分析網頁後解析出 url、title、content,其他的不關心,解析好的資料通過字典返回。
'''
class HtmlParser(object):
    def parse(self, url, content, html_encode="utf-8"):
        if url is None or content is None:
            return
        soup = BeautifulSoup(content, "html.parser", from_encoding=html_encode)
        new_urls = self._get_new_urls(url, soup)
        new_data = self._get_new_data(url, soup)
        return new_urls, new_data


    def _get_new_urls(self, url, soup):
        new_urls = set()
        links = soup.find_all("a", href=re.compile(r"/item/\w+"))
        for link in links:
            url_path = link["href"]
            new_url = urljoin(url, url_path)
            new_urls.add(new_url)
        return new_urls


    def _get_new_data(self, url, soup):
        data = {"url": url}
        title_node = soup.find("dd", class_="lemmaWgt-lemmaTitle-title").find("h1")
        data["title"] = title_node.get_text()
        summary_node = soup.find("div", class_="lemma-summary")
        data["summary"] = summary_node.get_text()
        return data
'''
html_output.py 上面爬蟲流程圖中的[應用器]
負責對解析後的資料應用,這裡簡單用一個 WEB 頁面把爬取的所有存在在 datas 列表的資料以 Table 輸出。
'''
class HtmlOutput(object):
    def __init__(self):
        self.datas = []

    def collect_data(self, data):
        if data is None:
            return
        self.datas.append(data)

    def output_html(self):
        file_name = time.strftime("%Y-%m-%d_%H:%M:%S")
        with open("out_%s.html" % file_name, "w") as f_out:
            f_out.write("<html>")
            f_out.write(r'<head>'
                        r'<link rel="stylesheet" '
                        r'href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" '
                        r'integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" '
                        r'crossorigin="anonymous"></head>')
            f_out.write("<body>")
            f_out.write(r'<table class="table table-bordered table-hover">')

            item_css = ['active', 'success', 'warning', 'info']
            for data in self.datas:
                index = self.datas.index(data) % len(item_css)
                f_out.write(r'<tr class="'+item_css[index]+r'">')
                f_out.write('<td>%s</td>' % data["url"])
                f_out.write('<td>%s</td>' % data["title"])
                f_out.write('<td>%s</td>' % data["summary"])
                f_out.write("</tr>")

            f_out.write("</table>")
            f_out.write("</body>")
            f_out.write("</html>")

哇嗚!就是這麼贊,怎麼樣,到此有沒有對 Python 小爬蟲產生一個整體的認知呢,如果表示瞭解了,那麼我們下一篇會循序漸進的談談其他 Python 爬蟲技術點(當然了,上面程式碼雖然很少,但是你可能還是覺得有些看不懂,那就的自己去補習下相關知識了,至於細節不在本系列探討範圍)。

^-^當然咯,看到這如果發現對您有幫助的話不妨掃描二維碼賞點買羽毛球的小錢(現在球也挺貴的),既是一種鼓勵也是一種分享,謝謝!
這裡寫圖片描述

【工匠若水 http://blog.csdn.net/yanbober 未經允許嚴禁轉載,請尊重作者勞動成果。私信聯絡我

相關文章