在人生苦短我用Python,本文助你快速入門這篇文章中,學習了Python的語法知識。如今咱們就拿Python作個爬蟲玩玩,若是中途個別API忘了能夠回頭看看,別看我,我沒忘!(逃html
學習網絡爬蟲以前,有必要了解一下如何使用Python進行網絡編程。既然說到網絡編程,對於一些計算機網絡的基礎知識最好也有所瞭解。好比HTTP,在這裏就不講計算機基礎了,貼出我以前的一篇博客。感興趣的能夠看看圖解HTTP常見知識點總結。python
網絡編程是Python比較擅長的領域,內置了相關的庫,第三方庫也很豐富。下面主要介紹一下內置的urllib庫和第三方的request庫。web
urllib是Python內置的HTTP請求庫,其使得HTTP請求變得很是方便。首先經過一個表格列出這個庫的內置模塊:正則表達式
模塊 | 做用 |
---|---|
urllib.request | HTTP請求模塊,模擬瀏覽器發送HTTP請求 |
urllib.error | 異常處理模塊,捕獲因爲HTTP請求產生的異常,並進行處理 |
urllib.parse | URL解析模塊,提供了處理URL的工具函數 |
urllib.robotparser | robots.txt解析模塊,網站經過robots.txt文件設置爬蟲可爬取的網頁 |
下面會演示一些經常使用的函數和功能,開始以前先import上面的幾個模塊。編程
這個函數的做用是向目標URL發送請求,其主要有三個參數:url目標地址、data請求數據、timeout超時時間。該函數會返回一個HTTPResponse對象,能夠經過該對象獲取響應內容,示例以下:json
response = urllib.request.urlopen("https://www.baidu.com/") print(response.read().decode("utf8")) # read()是讀取響應內容。decode()是按指定方式解碼
能夠看到咱們使用這個函數只傳入了一個URL,沒傳入data的話默認是None,表示是GET請求。接着再演示一下POST請求:後端
param_dict = {"key":"hello"} # 先建立請求數據 param_str = urllib.parse.urlencode(param_dict) # 將字典數據轉換爲字符串,如 key=hello param_data=bytes(param_str,encoding="utf8") # 把字符串轉換成字節對象(HTTP請求的data要求是bytes類型) response = urllib.request.urlopen("http://httpbin.org/post",data=param_data) #這個網址專門測試HTTP請求的 print(response.read())
timeout就再也不演示了,這個參數的單位是秒。怎麼請求弄明白了,關鍵是要解析響應數據。好比響應狀態碼能夠這麼獲取:response.status
。獲取整個響應頭:response.getheaders()
,也能夠獲取響應頭裏面某個字段的信息:response.getheader("Date")
,這個是獲取時間。瀏覽器
雖然可使用urlopen函數很是方便的發送簡單的HTTP請求,可是對於一些複雜的請求操做,就無能爲力了。這時候能夠經過Request對象來構建更豐富的請求信息。這個類的構造方法有以下參數:服務器
參數名詞 | 是否必需 | 做用 |
---|---|---|
url | 是 | HTTP請求的目標URL |
data | 否 | 請求數據,數據類型是bytes |
headers | 否 | 頭信息,能夠用字典來構建 |
origin_req_host | 否 | 發起請求的主機名或IP |
unverifiable | 否 | 請求是否爲沒法驗證的,默認爲False。 |
method | 否 | 請求方式,如GET、POST等 |
url = "http://httpbin.org/get" method = "GET" # ...其餘參數也能夠本身構建 request_obj = urllib.request.Request(url=url,method=method) # 把參數傳入Request的構造方法 response = urllib.request.urlopen(request_obj) print(response.read())
該模塊中定義了兩個常見的異常:URLEEror和HTTPError,後者是前者的子類。示例以下:cookie
url = "https://afasdwad.com/" # 訪問一個不存在的網站 try: request_obj = urllib.request.Request(url=url) response = urllib.request.urlopen(request_obj) except urllib.error.URLError as e: print(e.reason) # reason屬性記錄着URLError的緣由
產生URLError的緣由有兩種:1.網絡異常,失去網絡鏈接。2.服務器鏈接失敗。而產生HTTPError的緣由是:返回的Response urlopen函數不能處理。能夠經過HTTPError內置的屬性瞭解異常緣由,屬性有:reason記錄異常信息、code記錄響應碼、headers記錄請求頭信息。
requests庫是基於urllib開發的HTTP相關的操做庫,相比urllib更加簡潔、易用。不過requests庫是第三方庫,須要單獨安裝才能使用,能夠經過這個命令安裝:pip3 install requests
。
使用urllib中的urlopen時,咱們傳入data表明POST請求,不傳入data表明GET請求。而在requests中有專門的函數對應GET仍是POST。這些請求會返回一個requests.models.Response
類型的響應數據,示例以下:
import requests response = requests.get("http://www.baidu.com") print(type(response)) #輸出 <class 'requests.models.Response'> print(response.status_code) # 獲取響應碼 print(response.text) # 打印響應內容
上面的例子調用的是get函數,一般能夠傳入兩個參數,第一個是URL,第二個是請求參數params。GET請求的參數除了直接加在URL後面,還可使用一個字典記錄着,而後傳給params。對於其餘的請求方法,POST請求也有個post函數、PUT請求有put函數等等。
返回的Response對象,除了能夠獲取響應碼,它還有如下這些屬性:
其餘的函數就不一一演示,等須要用到的時候你們能夠查文檔,也能夠直接看源碼。好比post函數源碼的參數列表是這樣的:def post(url, data=None, json=None, **kwargs):
。直接看源碼就知道了它須要哪些參數,參數名是啥,一目瞭然。不過接觸Python後,有個很是很差的體驗:雖然寫起來比其餘傳統面嚮對象語言方便不少,可是看別人的源碼時不知道參數類型是啥。不過通常寫的比較好的源碼都會有註釋,好比post函數開頭就會說明data是字典類型的。
urllib庫中能夠用Request類的headers參數來構建頭信息。那麼最後咱們再來講一下requests庫中怎麼構建headers頭信息,這在爬蟲中尤其重要,由於頭信息能夠把咱們假裝成瀏覽器。
咱們直接使用字典把頭信息裏面對應的字段都填寫完畢,再調用對應的get或post函數時,加上headers=dict就好了。**kwargs
就是接收這些參數的。
網絡編程相關的API暫時就講這些,下面就拿小說網站和京東爲例,爬取上面的信息來練練手。
在正式寫程序以前有必要說說爬蟲相關的基礎知識。不知道有多少人和我同樣,瞭解爬蟲以前以爲它是個高大上、高度智能的程序。實際上,爬蟲能作的咱們人類也能作,只是效率很是低。其爬取信息的邏輯也很樸實無華:經過HTTP請求訪問網站,而後利用正則表達式匹配咱們須要的信息,最後對爬取的信息進行整理。雖然過程千差萬別,可是大致的步驟就是這樣。其中還涉及了各大網站反爬蟲和爬蟲高手們的反反爬蟲。
再者就是,具體網站具體分析,因此除了必要的後端知識,學習爬蟲的基本前提就是起碼看得懂HTML和會用瀏覽器的調試功能。不過這些就多說了,相信各位大手子都懂。
第一個實戰咱們就挑選一個簡單點的小說網站:https://www.kanunu8.com/book3/6879/。 先看一下頁面:
咱們要作的就是把每一個章節的內容都爬取下來,並以每一個章節爲一個文件,保存到本地文件夾中。
咱們首先要獲取每一個章節的連接。按F12打開調式頁面,咱們經過HTML代碼分析一下,如何才能獲取這些章節目錄?固然,如何找到章節目錄沒有嚴格限制,只要你寫的正則表達式能知足這個要求便可。我這裏就從正文這兩個字入手,由於章節表格這個元素最開頭的是這兩字。咱們來看一下源碼:
咱們要作的就是,寫一個正則表達式,從正文二字開頭,以</tbody>
結尾,獲取圖中紅色大括號括起來的這段HTML代碼。獲取到章節目錄所在的代碼後,咱們再經過a
標籤獲取每一個章節的連接。注意:這個連接是相對路徑,咱們須要和網站URL進行拼接。
有了大概的思路後,咱們開始敲代碼吧。代碼並不複雜,我就所有貼出來,主要邏輯我就寫在註釋中,就不在正文中說明了。若是忘了正則表達式就去上一篇文章裏回顧一下吧。
import requests import re import os """ 傳入網站的html字符串 利用正則表達式來解析出章節連接 """ def get_toc(html,start_url): toc_url_list=[] # 獲取目錄(re.S表明把/n也看成一個普通的字符,而不是換行符。否則換行後有的內容會被分割,致使表達式匹配不上) toc_block=re.findall(r"正文(.*?)</tbody>",html,re.S)[0] # 獲取章節連接 # 囉嗦一句,Python中單引號和雙引號均可以表示字符串,可是若是有多重引號時,建議區分開來,方便查看 toc_url = re.findall(r'href="(.*?)"',toc_block,re.S) for url in toc_url: # 由於章節連接是相對路徑,因此得和網址進行拼接 toc_url_list.append(start_url+url) return toc_url_list """ 獲取每一章節的內容 """ def get_article(toc_url_list): html_list=[] for url in toc_url_list: html_str = requests.get(url).content.decode("GBK") html_list.append(html_str) # 先建立個文件夾,文章就保存到這裏面,exist_ok=True表明不存在就建立 os.makedirs("動物莊園",exist_ok=True) for html in html_list: # 獲取章節名稱(只有章節名的size=4,咱們根據這個特色匹配),group(1)表示返回第一個匹配到的子字符串 chapter_name = re.search(r'size="4">(.*?)<',html,re.S).group(1) # 獲取文章內容(全文被p標籤包裹),而且把<br />給替換掉,注意/前有個空格 text_block = re.search(r'<p>(.*?)</p>',html,re.S).group(1).replace("<br />","") save(chapter_name,text_block) """ 保存文章 """ def save(chapter_name,text_block): # 以寫的方式打開指定文件 with open(os.path.join("動物莊園",chapter_name+".txt"),"w",encoding="utf-8") as f: f.write(text_block) # 開始 def main(): try: start_url = "https://www.kanunu8.com/book3/6879/" # 獲取小說主頁的html(decode默認是utf8,可是這個網站的編碼方式是GBK) html = requests.get(start_url).content.decode("GBK") # 獲取每一個章節的連接 toc_url_list = get_toc(html,start_url) # 根據章節連接獲取文章內容並保存 get_article(toc_url_list) except Exception as e: print("發生異常:",e) if __name__ == "__main__": main()
最後看一下效果:
拓展:一個簡單的爬蟲就寫完了,可是還有不少能夠拓展的地方。好比:改爲多線程爬蟲,提高效率,這個小項目很符合多線程爬蟲的使用場景,典型的IO密集型任務。還能夠優化一下入口,咱們經過main方法傳入書名,再去網站查找對應的書籍進行下載。
我以多線程爬取爲例,咱們只須要稍微修改兩個方法:
# 首先導入線程池 from concurrent.futures import ThreadPoolExecutor # 咱們把main方法修改一下 def main(): try: start_url = "https://www.kanunu8.com/book3/6879/" html = requests.get(start_url).content.decode("GBK") toc_url_list = get_toc(html,start_url) os.makedirs("動物莊園",exist_ok=True) # 建立一個有4個線程的線程池 with ThreadPoolExecutor(max_workers=4) as pool: pool.map(get_article,toc_url_list) except Exception as e: print("發生異常:",e)
map()
方法中,第一個參數是待執行的方法名,不用加()。第二個參數是傳入到get_article
這個方法的參數,能夠是列表、元組等。以本代碼爲例,map()
方法的做用就是:會讓線程池中的線程去執行get_article
,並傳入參數,這個參數就從toc_url_list
依次獲取。好比線程A拿了``toc_url_list`的第一個元素並傳入,那麼線程B就拿第二個元素並傳入。
既然咱們知道了map()
方法傳入的是一個元素,而get_article
原來接收的是一個列表,因此這個方法也須要稍微修改一下:
def get_article(url): html_str = requests.get(url).content.decode("GBK") chapter_name = re.search(r'size="4">(.*?)<',html_str,re.S).group(1) text_block = re.search(r'<p>(.*?)</p>',html_str,re.S).group(1).replace("<br />","") save(chapter_name,text_block)
經過測試,在個人機器上,使用一個線程爬取這本小說花了24.9秒,使用4個線程花了4.6秒。固然我只測試了一次,應該有網絡的緣由,時間不是很是準確,但效果仍是很明顯的。
有了第一個項目練手,是否是有點感受呢?其實也沒想象的那麼複雜。下面咱們再拿京東試一試,我想達到的目的是:收集京東上某個商品的信息,並保存到Excel表格中。這個項目中涉及了一些第三方庫,不過你們能夠先看個人註釋,事後再去看它們的文檔。
具體問題具體分析,在貼爬蟲代碼以前咱們先分析一下京東的網頁源碼,看看怎麼設計爬蟲的邏輯比較好。
咱們先在京東商城的搜索框裏輸入你想收集的商品,而後打開瀏覽器的調式功能,進入到Network,最後再點擊搜索按鈕。咱們找一下搜索商品的接口連接是啥。
圖中選中的網絡請求就是搜索按鈕對應的接口連接。拿到這個連接後咱們就能夠拼接URL,請求獲取商品信息了。咱們接着看商品搜索出來後,是怎麼呈現的。
經過源碼發現,每一個商品對應一個li標籤。通常商城網站都是由一些模板動態生成的,因此看上去很規整,這讓咱們的爬取難度也下降了。
咱們點進一個看看每一個商品裏又包含什麼信息:
一樣至關規整,最外層li的class叫gl-item,裏面每一個div對應一個商品信息。知道這些後,作起來就至關簡單了,就用這些class的名稱來爬取信息。我仍是直接貼出所有代碼,該說的都寫在註釋裏。貼以前說說每一個方法的做用。search_by_keyword
:根據傳入的商品關鍵詞搜索商品。get_item_info
:根據網頁源碼獲取商品信息。skip_page
:跳轉到下一頁並獲取商品信息。save_excel
:把獲取的信息保存到Excel。
from selenium import webdriver from selenium.common.exceptions import TimeoutException from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.common.desired_capabilities import DesiredCapabilities from pyquery import PyQuery from urllib.parse import quote import re from openpyxl import Workbook from fake_useragent import UserAgent # 設置請求頭裏的設備信息,否則會被京東攔截 dcap = dict(DesiredCapabilities.PHANTOMJS) # 使用隨機設備信息 dcap["phantomjs.page.settings.userAgent"] = (UserAgent().random) # 構建瀏覽器對象 browser = webdriver.PhantomJS(desired_capabilities=dcap) # 發送搜索商品的請求,並返回總頁數 def search_by_keyword(keyword): print("正在搜索:{}".format(keyword)) try: # 把關鍵詞填入搜索連接 url = "https://search.jd.com/Search?keyword=" + \ quote(keyword)+"&enc=utf-8" # 經過瀏覽器對象發送GET請求 browser.get(url) # 等待請求響應 WebDriverWait(browser, 10).until( EC.presence_of_element_located((By.CSS_SELECTOR, ".gl-item")) ) pages = WebDriverWait(browser, 10).until( EC.presence_of_element_located( (By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > em:nth-child(1) > b")) ) return int(pages.text) except TimeoutException as e: print("請求超時:"+e) # 根據HTML獲取對應的商品信息 def get_item_info(page): # 獲取網頁源代碼 html = browser.page_source # 使用 PyQuery解析網頁源代碼 pq = PyQuery(html) # 獲取商品的li標籤 items = pq(".gl-item").items() datas = [] # Excel中的表頭,若是當前是第一頁信息就是添加表頭 if page==1: head = ["商品名稱", "商品連接", "商品價格", "商品評價", "店鋪名稱", "商品標籤"] datas.append(head) # 遍歷當前頁全部的商品信息 for item in items: # 商品名稱,使用正則表達式將商品名稱中的換行符\n替換掉 p_name = re.sub("\\n", "", item.find(".p-name em").text()) href = item.find(".p-name a").attr("href") # 商品連接 p_price = item.find(".p-price").text() # 商品價錢 p_commit = item.find(".p-commit").text() # 商品評價 p_shop = item.find(".p-shop").text() # 店鋪名稱 p_icons = item.find(".p-icons").text() # info表明某個商品的信息 info = [] info.append(p_name) info.append(href) info.append(p_price) info.append(p_commit) info.append(p_shop) info.append(p_icons) print(info) # datas是當前頁全部商品的信息 datas.append(info) return datas # 跳轉到下一頁並獲取數據 def skip_page(page, ws): print("跳轉到第{}頁".format(page)) try: # 獲取跳轉到第幾頁的輸入框 input_text = WebDriverWait(browser, 10).until( EC.presence_of_element_located( (By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > input")) ) # 獲取跳轉到第幾頁的肯定按鈕 submit = WebDriverWait(browser, 10).until( EC.element_to_be_clickable( (By.CSS_SELECTOR, "#J_bottomPage > span.p-skip > a")) ) input_text.clear() # 清空輸入框 input_text.send_keys(page) # 在輸入框中填入要跳轉的頁碼 submit.click() # 點擊肯定按鈕 # 等待網頁加載完成,直到頁面下方被選中而且高亮顯示的頁碼,與頁碼輸入框中的頁碼相等 WebDriverWait(browser, 10).until( EC.text_to_be_present_in_element( (By.CSS_SELECTOR, "#J_bottomPage > span.p-num > a.curr"), str(page)) ) # 獲取商品信息 datas = get_item_info(page) # 若是有數據就保存到Excel中 if len(datas) > 0: save_excel(datas, ws) except TimeoutException as e: print("請求超時:", e) skip_page(page, ws) # 請求超時,重試 except Exception as e: print("產生異常:", e) print("行數:", e.__traceback__.tb_lineno) # 保存數據到Excel中 def save_excel(datas, ws): for data in datas: ws.append(data) def main(): try: keyword = "手機" # 搜索關鍵詞 file_path = "./data.xlsx" # 文件保存路徑 # 建立一個工做簿 wb = Workbook() ws = wb.create_sheet("京東手機商品信息",0) pages = search_by_keyword(keyword) print("搜索結果共{}頁".format(pages)) # 按照順序循環跳轉到下一頁(就不爬取全部的數據了,否則要等好久,若是須要爬取全部就把5改爲pages+1) for page in range(1, 5): skip_page(page, ws) # 保存Excel表格 wb.save(file_path) except Exception as err: print("產生異常:", err) wb.save(file_path) finally: browser.close() if __name__ == '__main__': main()
從main方法開始,藉助着註釋,即便不知道這些庫應該也能看懂了。下面是使用到的操做庫的說明文檔:
selenium:Selenium庫是第三方Python庫,是一個Web自動化測試工具,它可以驅動瀏覽器模擬輸入、單擊、下拉等瀏覽器操做。中文文檔:https://selenium-python-zh.readthedocs.io/en/latest/index.html。部份內容還沒翻譯完,也能夠看看這個:https://zhuanlan.zhihu.com/p/111859925。selenium建議安裝低一點的版本,好比
pip3 install selenium==2.48.0
,默認安裝的新版本不支持PhantomJS了。PhantomJS:是一個可編程的無界面瀏覽器引擎,也可使用谷歌或者火狐的。這個不屬於Python的庫,因此不能經過pip3直接安裝,去找個網址http://phantomjs.org/download.html下載安裝包,解壓後,把所在路徑添加到環境變量中(添加的路徑要到bin目錄中)。文檔:https://phantomjs.org/quick-start.html
openpyxl:Excel操做庫,可直接安裝,文檔:https://openpyxl.readthedocs.io/en/stable/。
pyquery:網頁解析庫,可直接安裝,文檔:https://pythonhosted.org/pyquery/
拓展:能夠加上商品的選擇條件,好比價格範圍、銷量排行。也能夠進入到詳情頁面,爬取銷量排行前幾的評價等。
今天就說到這裏了,有問題感謝指出。若是有幫助能夠點個贊、點個關注。接下來會學更多爬蟲技巧以及其餘的後端知識,到時候再分享給你們~
參考資料:《Python 3快速入門與實戰》、《Python爬蟲開發》、各類文檔~