回顧requests實現數據爬取的流程
1. 指定url 2. 基於requests模塊發起請求 3. 獲取響應對象中的數據 4. 進行持久化存儲
其實,在上述流程中還需要較為重要的一步,就是在持久化存儲之前需要進行指定數據解析。因為大多數情況下的需求,我們都會指定去使用聚焦爬蟲,也就是爬取頁面中指定部分的數據值,而不是整個頁面的數據。因此,本次課程中會給大家詳細介紹講解三種聚焦爬蟲中的數據解析方式。至此,我們的數據爬取的流程可以修改為:
1. 指定url 2. 基於requests模塊發起請求 3. 獲取響應中的數據 4. 數據解析 5. 進行持久化存儲
今日概要
- 正則解析
- xpath解析
- bs4解析
一. 正則解析
常用正則表達式回顧:

單字符: . : 除換行以外所有字符 [] :[aoe] [a-w] 匹配集合中任意一個字符 \d :數字 [0-9] \D : 非數字 \w :數字、字母、下划線、中文 \W : 非\w \s :所有的空白字符包,括空格、制表符、換頁符等等。等價於 [ \f\n\r\t\v]。 \S : 非空白 數量修飾: * : 任意多次 >=0 + : 至少1次 >=1 ? : 可有可無 0次或者1次 {m} :固定m次 hello{3,} {m,} :至少m次 {m,n} :m-n次 邊界: $ : 以某某結尾 ^ : 以某某開頭 分組: (ab) 貪婪模式: .* 非貪婪(惰性)模式: .*? re.I : 忽略大小寫 re.M :多行匹配 re.S :單行匹配 re.sub(正則表達式, 替換內容, 字符串)
回顧練習:

import re #提取出python key="javapythonc++php" re.findall('python',key)[0] ##################################################################### #提取出hello world key="<html><h1>hello world<h1></html>" re.findall('<h1>(.*)<h1>',key)[0] ##################################################################### #提取170 string = '我喜歡身高為170的女孩' re.findall('\d+',string) ##################################################################### #提取出http://和https:// key='http://www.baidu.com and https://boob.com' re.findall('https?://',key) ##################################################################### #提取出hello key='lalala<hTml>hello</HtMl>hahah' #輸出<hTml>hello</HtMl> re.findall('<[Hh][Tt][mM][lL]>(.*)</[Hh][Tt][mM][lL]>',key) ##################################################################### #提取出hit. key='bobo@hit.edu.com'#想要匹配到hit. re.findall('h.*?\.',key) ##################################################################### #匹配sas和saas key='saas and sas and saaas' re.findall('sa{1,2}s',key) ##################################################################### #匹配出i開頭的行 string = '''fall in love with you i love you very much i love she i love her''' re.findall('^.*',string,re.M) ##################################################################### #匹配全部行 string1 = """<div>靜夜思 窗前明月光 疑是地上霜 舉頭望明月 低頭思故鄉 </div>""" re.findall('.*',string1,re.S)
項目需求:爬取糗事百科指定頁面的糗圖,並將其保存到指定文件夾中
新需求: 爬取妹子圖網站https://www.meizitu.com/a/xiaoqingxin.htm
import re import os import requests url = 'https://www.qiushibaike.com/pic/page/%s/?s=5201892' headers = { 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36', } # 指定起始也結束頁碼 page_start = int(input('enter start page:')) page_end = int(input('enter end page:')) # 創建文件夾 if not os.path.exists('images'): os.mkdir('images') # 循環解析且下載指定頁碼中的圖片數據 for page in range(page_start, page_end + 1): print('正在下載第%d頁圖片' % page) new_url = format(url % page) print(new_url) response = requests.get(url=new_url, headers=headers) # 解析response中的圖片鏈接 ex = '<div class="thumb">.*?<img src="(.*?)".*?>.*?</div>' pa = re.compile(ex, re.S) image_urls = pa.findall(response.text) # 循環下載該頁碼下所有的圖片數據 for image_url in image_urls: image_url = 'https:' + image_url image_name = image_url.split('/')[-1] image_path = 'images/' + image_name image_data = requests.get(url=image_url, headers=headers).content with open(image_path, 'wb') as fp: fp.write(image_data)
二. BeautifulSoup解析
環境安裝
- 需要將pip源設置為國內源,阿里源、豆瓣源、網易源等
- windows (1)打開文件資源管理器(文件夾地址欄中) (2)地址欄上面輸入 %appdata% (3)在這里面新建一個文件夾 pip (4)在pip文件夾里面新建一個文件叫做 pip.ini ,內容寫如下即可 [global] timeout = 6000 index-url = https://mirrors.aliyun.com/pypi/simple/ trusted-host = mirrors.aliyun.com - linux (1)cd ~ (2)mkdir ~/.pip (3)vi ~/.pip/pip.conf (4)編輯內容,和windows一模一樣 - 需要安裝:pip install bs4 把lxml庫也安裝一下, 它是一個解析器,如果使用bs4進行數據解析,需要依靠lxml pip install lxml
測試頁面數據

<html lang="en"> <head> <meta charset="UTF-8" /> <title>測試bs4</title> </head> <body> <div> <p>百里守約</p> </div> <div class="song"> <p>李清照</p> <p>王安石</p> <p>蘇軾</p> <p>柳宗元</p> <a href="http://www.song.com/" title="趙匡胤" target="_self"> <span>this is span</span> 宋朝是最強大的王朝,不是軍隊的強大,而是經濟很強大,國民都很有錢</a> <a href="" class="du">總為浮雲能蔽日,長安不見使人愁</a> <img src="http://www.baidu.com/meinv.jpg" alt="" /> </div> <div class="tang"> <ul> <li><a href="http://www.baidu.com" title="qing">清明時節雨紛紛,路上行人欲斷魂,借問酒家何處有,牧童遙指杏花村</a></li> <li><a href="http://www.163.com" title="qin">秦時明月漢時關,萬里長征人未還,但使龍城飛將在,不教胡馬度陰山</a></li> <li><a href="http://www.126.com" alt="qi">岐王宅里尋常見,崔九堂前幾度聞,正是江南好風景,落花時節又逢君</a></li> <li><a href="http://www.sina.com" class="du">杜甫</a></li> <li><a href="http://www.dudu.com" class="du">杜牧</a></li> <li><b>杜小月</b></li> <li><i>度蜜月</i></li> <li><a href="http://www.haha.com" id="feng">鳳凰台上鳳凰游,鳳去台空江自流,吳宮花草埋幽徑,晉代衣冠成古丘</a></li> </ul> </div> </body> </html>
基礎使用
bs4解析原理 - 實例化一個BeautifulSoup對象,必須把即將被解析的頁面源碼加載到該對象中 - 調用該對象中相關的屬性或方法進行標簽的定位和內容的提取 如何實例化一個BeautifulSoup對象: - 導包:from bs4 import BeautifulSoup - 使用方式:可以將一個html文檔,轉化為BeautifulSoup對象,然后通過對象的方法或者屬性去查找指定的節點內容 (1)轉化本地文件: # 將本地存儲的頁面所對應的源碼加載到實例化好的BeautifulSoup對象中 soup = BeautifulSoup(open('本地文件'), 'lxml') (2)轉化網絡文件: # 直接把響應數據作為構造方法的第一個參數,再通過對象調用解析 soup = BeautifulSoup('字符串類型或者字節類型', 'lxml') (3)打印soup對象顯示內容為html文件中的內容
基礎鞏固:
(1)根據標簽名查找 - soup.a 獲取頁面源碼中第一個符合要求的標簽 (2)獲取屬性, 返回的永遠是一個單數 - soup.a.attrs 獲取a所有的屬性和屬性值,返回一個字典 - soup.a.attrs['href'] 獲取href屬性 - soup.a['href'] 也可簡寫為這種形式 (3)獲取文本內容 - soup.string: 只可以獲取直系標簽中的文本內容 - soup.text: 獲取標簽下的所有文本內容 - soup.get_text(): 獲取標簽下的所有文本內容 【注意】如果標簽還有標簽,那么string獲取到的結果為None,而其它兩個,可以獲取文本內容 (4)find():找到第一個符合要求的標簽, 返回的永遠是一個單數 - soup.find('a') 通過標簽名進行數據解析 通過標簽屬性進行數據解析: - soup.find('a', title="xxx") - soup.find('a', alt="xxx") - soup.find('a', class_="xxx") - soup.find('a', id="xxx") (5)find_all:找到所有符合要求的標簽, 返回的永遠是一個列表 - soup.find_all('a') - soup.find_all(['a','b']) 找到所有的a和b標簽 - soup.find_all('a', limit=2) 限制前兩個 (6)根據選擇器選擇指定的內容 select選擇器, 返回的永遠是一個列表: soup.select('#feng') - 常見的選擇器:標簽選擇器(a)、類選擇器(.)、id選擇器(#)、層級選擇器 - 層級選擇器: - 單層級: div > p > a > .lala 只能是下面一級 - 多層級: div .dudu #lala .meme .xixi 下面好多級 【注意】select選擇器返回永遠是列表,需要通過下標提取指定的對象
需求:使用bs4實現將詩詞名句網站中三國演義小說的每一章的內容爬取到本地磁盤進行存儲
http://www.shicimingju.com/book/sanguoyanyi.html
# !/usr/bin/env python # -*- coding:utf-8 -*- import requests from bs4 import BeautifulSoup headers={ 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36', } def parse_content(url): #獲取標題正文頁數據 page_text = requests.get(url,headers=headers).text soup = BeautifulSoup(page_text,'lxml') #解析獲得標簽 ele = soup.find('div',class_='chapter_content') content = ele.text #獲取標簽中的數據值 return content if __name__ == "__main__": url = 'http://www.shicimingju.com/book/sanguoyanyi.html' reponse = requests.get(url=url,headers=headers) page_text = reponse.text #創建soup對象 soup = BeautifulSoup(page_text,'lxml') #解析數據 a_eles = soup.select('.book-mulu > ul > li > a') print(a_eles) cap = 1 for ele in a_eles: print('開始下載第%d章節'%cap) cap+=1 title = ele.string content_url = 'http://www.shicimingju.com'+ele['href'] content = parse_content(content_url) with open('./sanguo.txt','w') as fp: fp.write(title+":"+content+'\n\n\n\n\n') print('結束下載第%d章節'%cap)
三. Xpath解析
代碼中使用xpath表達式進行數據解析:
1. 特性: 通用性比較強
2. 下載:pip install lxml
3. 導包:from lxml import etree
4. 解析原理:
(1). 實例化一個etree對象,且將解析的頁面源碼加載到該對象中
(2). 使用該對象中的xpath方法結合着xpath表達式進行標簽定位和數據解析提取
5. 將html文檔或者xml文檔轉換成一個etree對象,然后調用對象中的方法查找指定的節點
(1). 本地文件:tree = etree.parse(文件名)
tree.xpath("xpath表達式")
(2). 網絡數據:tree = etree.HTML(網頁內容字符串)
tree.xpath("xpath表達式")
常用xpath表達式
/: 從標簽開始實現層級定位
//: 從任意位置實現標簽的定位
屬性定位:
語法: tag[@attrName="attrValue"]
//div[@class="song"] # 找到class屬性值為song的div標簽
層級&索引定位:
# 注意索引值是從1開始
//div[@class="tang"]/ul/li[2]/a # 找到class屬性值為tang的div的直系子標簽ul下的第二個子標簽li下的直系子標簽a
邏輯運算:
//a[@href="" and @class="du"] # 找到href屬性值為空且class屬性值為du的a標簽
模糊匹配:
//div[contains(@class, "ng")]
//div[starts-with(@class, "ta")]
取文本:
//div[@class="song"]/p[1]/text() # 取直系文本內容
//div[@class="tang"]//text() # 取所有文本內容
取屬性:
//div[@class="tang"]//li[2]/a/@href
安裝xpath插件在瀏覽器中對xpath表達式進行驗證:可以在插件中直接執行xpath表達式
-
將xpath插件拖動到谷歌瀏覽器拓展程序(更多工具)中,安裝成功
-
啟動和關閉插件 ctrl + shift + x
項目需求:解析58二手房的相關數據
#解析出一級頁面的標題和二級頁面的價格和描述 import requests from lxml import etree headers = { 'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' } url = 'https://bj.58.com/changping/ershoufang/?utm_source=sem-baidu-pc&spm=105916147073.26420796294&PGTID=0d30000c-0000-17fc-4658-9bdfb947934d&ClickID=3' page_text = requests.get(url=url,headers=headers).text tree = etree.HTML(page_text) li_list = tree.xpath('//ul[@class="house-list-wrap"]/li') data = [] for li in li_list: #解析標題 title = li.xpath('.//div[@class="list-info"]/h2/a/text()')[0] detail_page_url = li.xpath('.//div[@class="list-info"]/h2/a/@href')[0] if detail_page_url.split('//')[0] != 'https:': detail_page_url = 'https:'+detail_page_url detail_text = requests.get(url=detail_page_url,headers=headers).text tree = etree.HTML(detail_text) #解析二級頁面的描述和價格 desc = ''.join(tree.xpath('//div[@id="generalDesc"]//div[@class="general-item-wrap"]//text()')).strip(' \n \t') price = ''.join(tree.xpath('//div[@id="generalExpense"]//div[@class="general-item-wrap"]//text()')).strip(' \n \t') dic = { 'title':title, 'desc':desc, 'price':price } data.append(dic) #進行持久化存儲 print(data)
解析出所有城市名稱https://www.aqistudy.cn/historydata/
# 需求: 爬取當前頁面全部的城市名稱https://www.aqistudy.cn/historydata/ url = "https://www.aqistudy.cn/historydata/" page_text = requests.get(url=url, headers=headers).text tree = etree.HTML(page_text) # 熱門城市: //div[@class="bottom"]/ul/li/a/text() # 全部城市: //div[@class="bottom"]/ul/div[2]/li/a/text() all_city_names = tree.xpath('//div[@class="bottom"]/ul/li/a/text() | //div[@class="bottom"]/ul/div[2]/li/a/text()') print(all_city_names, len(all_city_names))

import requests from lxml import etree url = 'https://www.aqistudy.cn/historydata/' headers = { 'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' } response = requests.get(url=url,headers=headers) #獲取頁面原始編碼格式 print(response.encoding) page_text = response.text tree = etree.HTML(page_text) li_list = tree.xpath('//div[@class="bottom"]/ul/li | //div[@class="bottom"]/ul//li') for li in li_list: city_name = li.xpath('./a/text()')[0] city_url = 'https://www.aqistudy.cn/historydata/'+li.xpath('./a/@href')[0] print(city_name,city_url)
項目需求:獲取好段子中段子的內容和作者 http://www.haoduanzi.com

from lxml import etree import requests url='http://www.haoduanzi.com/category-10_2.html' headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36', } url_content=requests.get(url,headers=headers).text # 使用xpath對url_conten進行解析 # 使用xpath解析從網絡上獲取的數據 tree=etree.HTML(url_content) # 解析獲取當頁所有段子的標題 title_list=tree.xpath('//div[@class="log cate10 auth1"]/h3/a/text()') ele_div_list=tree.xpath('//div[@class="log cate10 auth1"]') text_list=[] # 最終會存儲12個段子的文本內容 for ele in ele_div_list: # 段子的文本內容(是存放在list列表中) text_list=ele.xpath('./div[@class="cont"]//text()') # list列表中的文本內容全部提取到一個字符串中 text_str=str(text_list) # 字符串形式的文本內容防止到all_text列表中 text_list.append(text_str) print(title_list) print(text_list)
解析圖片數據:http://pic.netbian.com/4kmeinv/
import requests from lxml import etree url = 'http://pic.netbian.com/4kmeinv/' headers = { 'User-Agent':'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36' } response = requests.get(url=url,headers=headers) #獲取頁面原始編碼格式 print(response.encoding) page_text = response.text tree = etree.HTML(page_text) li_list = tree.xpath('//div[@class="slist"]/ul/li') for li in li_list: img_url = 'http://pic.netbian.com'+li.xpath('./a/img/@src')[0] img_name = li.xpath('./a/img/@alt')[0] img_name = img_name.encode('iso-8859-1').decode('gbk') print(img_url,img_name)
【重點】下載煎蛋網中的圖片數據:http://jandan.net/ooxx
import requests from lxml import etree from fake_useragent import UserAgent import base64 import urllib.request url = 'http://jandan.net/ooxx' ua = UserAgent(verify_ssl=False,use_cache_server=False).random headers = { 'User-Agent':ua } page_text = requests.get(url=url,headers=headers).text # 查看頁面源碼:發現所有圖片的src值都是一樣的。 # 簡單觀察會發現每張圖片加載都是通過jandan_load_img(this)這個js函數實現的。 # 在該函數后面還有一個class值為img-hash的標簽,里面存儲的是一組hash值,該值就是加密后的img地址 # 加密就是通過js函數實現的,所以分析js函數,獲知加密方式,然后進行解密。 # 通過抓包工具抓取起始url的數據包,在數據包中全局搜索js函數名(jandan_load_img),然后分析該函數實現加密的方式。 # 在該js函數中發現有一個方法調用,該方法就是加密方式,對該方法進行搜索 # 搜索到的方法中會發現base64和md5等字樣,md5是不可逆的所以優先考慮使用base64解密 # print(page_text) tree = etree.HTML(page_text) # 在抓包工具的數據包響應對象對應的頁面中進行xpath的編寫,而不是在瀏覽器頁面中。 # 獲取了加密的圖片url數據 imgCode_list = tree.xpath('//span[@class="img-hash"]/text()') imgUrl_list = [] for url in imgCode_list: # base64.b64decode(url)為byte類型,需要轉成str img_url = 'http:'+base64.b64decode(url).decode() imgUrl_list.append(img_url) for url in imgUrl_list: filePath = url.split('/')[-1] urllib.request.urlretrieve(url=url,filename=filePath) print(filePath+'下載成功')
面試題:如何爬取攜帶標簽的指定頁面內容
1.我現在想要實現數據解析,但是我解析的內容不是純粹的文本數據,而是攜帶標簽的文本數據,並不是光拿鏈接的文本數據,而是還要拿到這個a標簽 2.為什么我們要去爬取攜帶標簽的文本內容呢?是因為如果這個指定的文本內容攜帶了標簽的話,意味着這個文本內容的樣式就有了,下次想用的時候直接把它放到相關的HTML當中,它就有了對應的樣式 3.那我們如何去實現呢? 因為正則太麻煩,所以我們在bs4和xpath之間去選擇 但是我們使用xpath定位到某一個標簽之后,它返回的是一個Element類型的對象,並不是頁面源碼,所以我們可以暫時不考慮xpath 4.那bs4呢,你會發現當我們實例化了一個bs4對象之后,打印對象的結果是頁面的源碼數據,里面就包含了攜帶標簽的文本內容,比如soup.a返回的是a標簽的源碼,所以我們使用bs4是可以解析到攜帶標簽的源碼數據 5.所以說如果我們想要解析到攜帶標簽的源碼數據的話,使用bs4是最簡單最便捷的
【重點】
- 問題:往往在進行大量請求發送的時候,經常會報出這一樣的一個錯誤:HTTPConnectionPool(host:XX)Max retries exceeded with url。
- 原因: 1.每次數據傳輸前客戶端要和服務器建立TCP連接,為節省傳輸消耗,默認為keep-alive,即連接一次,傳輸多次。然而如果連接遲遲不斷開的話,則連接池滿后則無法產生新的鏈接對象,導致請求無法發送。 2.ip被封 3.請求頻率太頻繁 - 解決:如果下列解決未生效,則可以嘗試再次執行程序 1.設置請求頭中的Connection的值為close,表示每次請求成功后斷開連接 2.更換請求ip 3.每次請求之間使用sleep進行等待間隔
import requests from fake_useragent import UserAgent from lxml import etree import random import time url = 'http://sc.chinaz.com/jianli/free_%d.html' ua = UserAgent(verify_ssl=False,use_cache_server=False).random headers = { 'User-Agent':ua, #保證http請求成功后,立即斷開連接,以解決HTTPConnectionPool(host:XX)Max retries exceeded with url的問題 'Connection': 'close', #該行不寫,則會報錯 } #proxy_list = ['101.255.56.201:36501','39.137.69.10:8080','195.29.106.178:58292','120.76.77.152:9999','178.75.1.111:50411','78.156.225.170:41258','193.192.177.196:56480'] for pageNum in range(1,3): get_url = format(url%pageNum) if pageNum==1: get_url = 'http://sc.chinaz.com/jianli/free.html' response = requests.get(url=get_url,headers=headers) response.encoding = 'utf-8'#處理中文亂碼問題 page_text = response.text tree = etree.HTML(page_text) div_list = tree.xpath('//div[@id="container"]/div') for div in div_list: second_url = div.xpath('./a/@href')[0] name = div.xpath('./p/a/text()')[0]+'.rar' second_page_text = requests.get(url=second_url,headers=headers).text second_tree = etree.HTML(second_page_text) download_url_list = second_tree.xpath('//div[@class="clearfix mt20 downlist"]/ul/li/a/@href') download_url = random.choice(download_url_list) data = requests.get(url=download_url,headers=headers).content with open(name,'wb') as fp: fp.write(data) print('下載完畢===>'+name) #解決HTTPConnectionPool(host:XX)Max retries exceeded with url的問題: #time.sleep(3) #延長請求時間,模擬瀏覽器,否則請求頻率太快會請求失敗 #使用代理池 #設置請求頭信息中的Connection為close
今日作業
1. 爬取梨視頻的視頻數據https://www.pearvideo.com/category_3
2. 爬取站長素材中免費的簡歷模板http://sc.chinaz.com/jianli/free.html