博客和其他經常更新的網站通常有一個首頁,其中有最新的帖子,以及一個“前 一篇”按鈕,將你帶到以前的帖子。然后那個帖子也有一個“前一篇”按鈕,以此 類推。這創建了一條線索,從最近的頁面,直到該網站的第一個帖子。如果你希望 拷貝該網站的內容,在離線的時候閱讀,可以手工導航至每個頁面並保存。但這是 很無聊的工作,所以讓我們寫一個程序來做這件事。 XKCD 是一個流行的極客漫畫網站,它符合這個結構(參見圖 11-6)。首頁 http://xkcd.com/有一個“Prev”按鈕,讓用戶導航到前面的漫畫。手工下載每張漫 畫要花較長的時間,但你可以寫一個腳本,在幾分鍾內完成這件事。 下面是程序要做的事:
• 加載主頁;
• 保存該頁的漫畫圖片;
• 轉入前一張漫畫的鏈接;
• 重復直到第一張漫畫。
這意味着代碼需要做下列事情:
• 利用 requests 模塊下載頁面。
• 利用 Beautiful Soup 找到頁面中漫畫圖像的 URL。
• 利用 iter_content()下載漫畫圖像,並保存到硬盤。
• 找到前一張漫畫的鏈接 URL,然后重復。
打開一個新的文件編輯器窗口,將它保存為 downloadXkcd.py。
設計程序
打開一個瀏覽器的開發者工具,檢查該頁面上的元素,你會發現下面的內容:
• 漫畫圖像文件的 URL,由一個<img>元素的 href 屬性給出。
• <img>元素在<div id="comic">元素之內。
• Prev 按鈕有一個 rel HTML 屬性,值是 prev。
• 第一張漫畫的Prev按鈕鏈接到 http://xkcd.com/# URL,表明沒有前一個頁面了。
讓你的代碼看起來像這樣:
#! python3 # downloadXkcd.py - Downloads every single XKCD comic. import requests, os, bs4 url = 'https://xkcd.com' # starting url os.makedirs('xkcd', exist_ok=True) # store comics in ./xkcd while not url.endswith('#'): # TODO: Download the page. # TODO: Find the URL of the comic image. # TODO: Download the image. # TODO: Save the image to ./xkcd # TODO: Get the Prev button's url. print('Done')
你會有一個 url 變量,開始的值是'http://xkcd.com',然后反復更新(在一個 for 循環中),變成當前頁面的 Prev 鏈接的 URL。在循環的每一步,你將下載 URL 上 的漫畫。如果 URL 以'#'結束,你就知道需要結束循環。 將圖像文件下載到當前目錄的一個名為 xkcd 的文件夾中。調用 os.makedirs() 函數。確保這個文件夾存在,並且關鍵字參數 exist_ok=True 在該文件夾已經存在時, 防止該函數拋出異常。剩下的代碼只是注釋,列出了剩下程序的大綱。
下載網頁
我們來實現下載網頁的代碼。讓你的代碼看起來像這樣:
#! python3 # downloadXkcd.py - Downloads every single XKCD comic. import requests, os, bs4 url = 'https://xkcd.com' # starting url os.makedirs('xkcd', exist_ok=True) # store comics in ./xkcd while not url.endswith('#'): # Download the page. print('Downloading page %s...' % url) res = requests.get(url) res.raise_for_status() soup = bs4.BeautifulSoup(res.text) # TODO: Find the URL of the comic image. # TODO: Download the image. # TODO: Save the image to ./xkcd # TODO: Get the Prev button's url. print('Done')
首先,打印 url,這樣用戶就知道程序將要下載哪個 URL。然后利用 requests 模塊的 request.get()函數下載它。像以往一樣,馬上調用 Response對象的 raise_for_status()方法, 如果下載發生問題,就拋出異常,並終止程序。否則,利用下載頁面的文本創建一 個 BeautifulSoup 對象。
尋找和下載漫畫圖像
讓你的代碼看起來像這樣:
#! python3 # downloadXkcd.py - Downloads every single XKCD comic. import requests, os, bs4 --snip-- # Find the URL of the comic image. comicElem = soup.select('#comic img') if comicElem == []: print('Could not find comic image.') else: comicUrl = 'https:' + comicElem[0].get('src') # Download the image. print('Downloading iamge %s...' % (comicUrl)) res = requests.get(comicUrl) res.raise_for_status() # TODO: Save the image to ./xkcd # TODO: Get the Prev button's url. print('Done')
用開發者工具檢查 XKCD 主頁后,你知道漫畫圖像的<img>元素是在一個<div>元 素中,它帶有的 id 屬性設置為 comic。所以選擇器'#comic img'將從 BeautifulSoup 對象中選出正確的<img>元素。 有一些 XKCD 頁面有特殊的內容,不是一個簡單的圖像文件。這沒問題,跳過它們 就好了。如果選擇器沒有找到任何元素,那么 soup.select('#comic img')將返回一個空的列 表。出現這種情況時,程序將打印一條錯誤消息,不下載圖像,繼續執行。 否則,選擇器將返回一個列表,包含一個<img>元素。可以從這個<img>元素中 取得 src 屬性,將它傳遞給 requests.get(),下載這個漫畫的圖像文件。
保存圖像,找到前一張漫畫
讓你的代碼看起來像這樣:
#! python3 # downloadXkcd.py - Downloads every single XKCD comic. import requests, os, bs4 --snip-- # Save the image to ./xkcd imageFile = open(os.path.join('xkcd', os.path.basename(comicUrl)), 'wb') for chunk in res.iter_content(100000): imageFile.write(chunk) # Get the Prev button's url. prevLink = soup.select('a[rel="prev"]')[0] url = 'http://xkcd.com' + prevLink.get('href') print('Done')
這時,漫畫的圖像文件保存在變量 res 中。你需要將圖像數據寫入硬盤的文件。 你需要為本地圖像文件准備一個文件名,傳遞給 open()。comicUrl 的值類似 'http://imgs.xkcd.com/comics/heartbleed_explanation.png'。你可能注意到,它看起來很 像文件路徑。實際上,調用 os.path.basename()時傳入 comicUrl,它只返回 URL 的 最后部分:'heartbleed_explanation.png'。你可以用它作為文件名,將圖像保存到硬 盤。用 os.path.join()連接這個名稱和 xkcd 文件夾的名稱,這樣程序就會在 Windows 下使用倒斜杠(\),在 OS X 和 Linux 下使用斜杠(/)。既然你最后得到了文件名, 就可以調用 open(),用'wb'模式打開一個新文件。 回憶一下本章早些時候,保存利用 Requests 下載的文件時,你需要循環處理 iter_content()方法的返回值。for 循環中的代碼將一段圖像數據寫入文件(每次最多 10 萬字節),然后關閉該文件。圖像現在保存到硬盤中。 然后,選擇器'a[rel="prev"]'識別出rel 屬性設置為 prev 的<a>元素,利用這個<a> 元素的 href 屬性,取得前一張漫畫的 URL,將它保存在 url 中。然后 while 循環針 對這張漫畫,再次開始整個下載過程。 這個程序的輸出看起來像這樣:
Downloading page http://xkcd.com...
Downloading image http://imgs.xkcd.com/comics/phone_alarm.png...
Downloading page http://xkcd.com/1358/...
Downloading image http://imgs.xkcd.com/comics/nro.png...
Downloading page http://xkcd.com/1357/...
Downloading image http://imgs.xkcd.com/comics/free_speech.png...
Downloading page http://xkcd.com/1356/...
Downloading image http://imgs.xkcd.com/comics/orbital_mechanics.png...
Downloading page http://xkcd.com/1355/...
Downloading image http://imgs.xkcd.com/comics/airplane_message.png...
Downloading page http://xkcd.com/1354/...
Downloading image http://imgs.xkcd.com/comics/heartbleed_explanation.png...
--snip--
這個項目是一個很好的例子,說明程序可以自動順着鏈接,從網絡上抓取大量 的數據。你可以從 Beautiful Soup 的文檔了解它的更多功能:https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/#
類似程序的想法
下載頁面並追蹤鏈接,是許多網絡爬蟲程序的基礎。類似的程序也可以做下面的事情:
• 順着網站的所有鏈接,備份整個網站。
• 拷貝一個論壇的所有信息。
• 復制一個在線商店中所有產品的目錄。
requests 和 BeautifulSoup 模塊很了不起,只要你能弄清楚需要傳遞給 requests.get() 的 URL。但是,有時候這並不容易找到。或者,你希望編程瀏覽的網站可能要求你先 登錄。selenium 模塊將讓你的程序具有執行這種復雜任務的能力。
完整代碼
#! python3 # downloadXkcd.py - Downloads every single XKCD comic. import requests, os, bs4 url = 'https://xkcd.com' # starting url os.makedirs('xkcd', exist_ok=True) # store comics in ./xkcd while not url.endswith('#'): # Download the page. print('Downloading page %s...' % url) res = requests.get(url) res.raise_for_status() soup = bs4.BeautifulSoup(res.text) # Find the URL of the comic image. comicElem = soup.select('#comic img') if comicElem == []: print('Could not find comic image.') else: comicUrl = 'https:' + comicElem[0].get('src') # Download the image. print('Downloading iamge %s...' % (comicUrl)) res = requests.get(comicUrl) res.raise_for_status() # Save the image to ./xkcd imageFile = open(os.path.join('xkcd', os.path.basename(comicUrl)), 'wb') for chunk in res.iter_content(100000): imageFile.write(chunk) # Get the Prev button's url. prevLink = soup.select('a[rel="prev"]')[0] url = 'http://xkcd.com' + prevLink.get('href') print('Done')
