天天看點

爬蟲入門之爬取政策 XPath與bs4實作(五)

在爬蟲系統中,待抓取URL隊列是很重要的一部分。待抓取URL隊列中的URL以什麼樣的順序排列也是一個很重要的問題,因為這涉及到先抓取那個頁面,後抓取哪個頁面。而決定這些URL排列順序的方法,叫做抓取政策。下面重點介紹幾種常見的抓取政策:

1 深度優先周遊政策:

深度優先周遊政策是指網絡爬蟲會從起始頁開始,一個連結一個連結跟蹤下去,處理完這條線路之後再轉入下一個起始頁,繼續跟蹤連結。我們以下面的圖為例:周遊的路徑:A-F-G E-H-I B C D

#深度抓取url,遞歸的思路
import requests
import re

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"}

def getURL(url):
    html = getHTML(url)
    # <a asd asdf href="www/s?wd=%E5%B2%9B%E5%9B%azi" dsgfa asdf >島國大片 留下郵箱</a>
    urlre = "<a .*href=\"(.*?)\".*?>"
    urlList = re.findall(urlre, html)
    return urlList

def getHTML(url):
    html = requests.get(url, headers=headers).text
    return html

def getEmail():
    #功能塊

def deepSpider(url, depth):
    print("\t\t\t" * depthDict[url], "抓取了第%d:%s頁面" % (depthDict[url], url))
    # 超出深度結束
    if depthDict[url] >= depth:
        return

    # 子url
    sonUrlList = getURL(url)
    for newUrl in sonUrlList:
        # 去重複兵去除非http連結
        if newUrl.find("http") != -1:
            if newUrl not in depthDict: 
                # 層級+1
                depthDict[newUrl] = depthDict[url] + 1
                # 遞歸及
                deepSpider(newUrl, depth)

if __name__ == '__main__':
    # 起始url
    startUrl = "https://www.baidu.com/s?wd=島國郵箱"
    # 層級控制
    depthDict = {}
    depthDict[startUrl] = 1  # {url:層級}
    deepSpider(startUrl, 4)  #調用函數deepSpider
           

深度周遊,棧思路

import requests
import re

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"}

def getURL(url):
    html = getHTML(url)
    # <a asd asdf href="www/s?wd=%E5%B2%9B%E5%9B%azi" dsgfa asdf >島國大片 留下郵箱</a>
    urlre = "<a .*href=\"(.*?)\".*?>"
    urlList = re.findall(urlre, html)
    return urlList

def getHTML(url):
    html = requests.get(url, headers=headers).text
    return html

def vastSpider(depth):
    '''
    深度抓取方式二,棧實作(先進後出)
    :param depth:深度
    :return:
    '''
    # 是否為空
    while len(urlList) > 0:  #while urlList:
        url = urlList.pop()  #關鍵取最後一個(先進後出)
        print('\t\t\t' * depthDict[url], "抓取了第%d層:%s" % (depthDict[url], url))

        # 層級控制
        if depthDict[url] < depth:
            # 生成新url
            sonUrlList = getURL(url)
            for newUrl in sonUrlList:
                # 去重複及去非http連結
                if newUrl.find("http") != -1:
                    if newUrl not in depthDict:
                        depthDict[newUrl] = depthDict[url] + 1
                        # 放入待爬取棧
                        urlList.append(newUrl)

if __name__ == '__main__':
    # 起始url
    startUrl = "https://www.baidu.com/s?wd=島國郵箱"
    # 層級控制
    depthDict = {}
    depthDict[startUrl] = 1
    # 待爬取棧(棧實際就是清單)
    urlList = []
    urlList.append(startUrl)
    vastSpider(4)           

2 廣度優先周遊政策

橫向優先搜尋政策的基本思路是,将新下載下傳網頁中發現的連結直接**待抓取URL隊列的末尾。也就是指網絡爬蟲會先抓取起始網頁中連結的所有網頁,然後再選擇其中的一個連結網頁,繼續抓取在此網頁中連結的所有網頁。還是以上面的圖為例:周遊路徑:A-B-C-D-E-F-G-H-I

#采用隊列思路,先進後出
import requests
import re

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"}

def getURL(url):
    '''
    擷取新url
    :param url:
    :return: urlList
    '''
    html = getHTML(url)
    # <a asd asdf href="www/s?wd=%E5%B2%9B%E5%9B%azi" dsgfa asdf >島國大片 留下郵箱</a>
    urlre = "<a .*href=\"(.*?)\".*?>"
    urlList = re.findall(urlre, html)
    return urlList

def getHTML(url):
    html = requests.get(url, headers=headers).text
    return html

def vastSpider(depth):
    '''
    廣度抓取
    :param depth:深度
    :return:
    '''
    # 是否為空
    while len(urlList) > 0:
        url = urlList.pop(0)
        print('\t\t\t' * depthDict[url], "抓取了第%d層:%s" % (depthDict[url], url))

        # 層級控制
        if depthDict[url] < depth:
            # 生成新url
            sonUrlList = getURL(url)
            for newUrl in sonUrlList:
                # 去重複及非http連結
                if newUrl.find("http") != -1:
                    if newUrl not in depthDict:
                        depthDict[newUrl] = depthDict[url] + 1
                        # 放入待爬取隊列
                        urlList.append(newUrl)

if __name__ == '__main__':
    # 起始url
    startUrl = "https://www.baidu.com/s?wd=島國郵箱"
    # 層級控制
    depthDict = {}
    depthDict[startUrl] = 1
    # 待爬取隊列
    urlList = []
    urlList.append(startUrl)
    vastSpider(4)           

3 頁面解析與資料提取

一般來講對我們而言,需要抓取的是某個網站或者某個應用的内容,提取有用的價值。内容一般分為兩部分,非結構化的資料 和 結構化的資料。

  • 非結構化資料:先有資料,再有結構,
  • 結構化資料:先有結構、再有資料
不同類型的資料,我們需要采用不同的方式來處理。
  • 非結構化的資料處理
正規表達式
HTML 檔案
正規表達式
XPath
CSS選擇器           
  • 結構化的資料處理
JSON Path
轉化成Python類型進行操作(json類)
XML 檔案
轉化成Python類型(xmltodict)
XPath
CSS選擇器
正規表達式           

4 Beautiful Soup

(1) beautifull soup概述

官方文檔位址:

http://www.crummy.com/software/BeautifulSoup/bs4/doc/

Beautiful Soup 相比其他的html解析有個非常重要的優勢,BeautifulSoup将複雜HTML文檔轉換成一個複雜的樹形結構。html會被拆解為對象處理。全篇轉化為字典和數組。

相比正則解析的爬蟲,省略了學習正則的高成本.

相比xpath爬蟲的解析,同樣節約學習時間成本.

安裝

#liunx安裝
apt-get install python-bs4
#python包安裝
pip install beautifulsoup4            

每個節點都是Python對象,我們隻用根據節點進行查詢 , 歸納為4大對象

  • Tag #節點類型
  • NavigableString # 标簽内容
  • BeautifulSoup #根節點類型
  • Comment #注釋

(1) 建立對象:

  • 網上檔案生成對象

    soup = BeautifulSoup(‘網上下載下傳的字元串’, ‘lxml’)

  • 本地檔案生成對象

    soup = BeautifulSoup(open(‘1.html’), ‘lxml’)

(2) tag标簽

  • 格式化輸出
from bs4 import BeautifulSoup  
soup = BeautifulSoup(html_doc)  
print(soup.prettify())  #html格式化           
  • 擷取指定的tag内容
soup.p.b  #擷取p标簽中b标簽
# <b>The Dormouse's story</b>  
soup.a  
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
soup.title  #擷取title标簽
# <title>The Dormouse's story</title>  

soup.title.name  #擷取title标簽名
# u'title'  

soup.title.string   #擷取title标簽内容
# u'The Dormouse's story'  

soup.title.parent.name  #擷取title的父節點tag的名稱
# u'head'  

#- 提取tag屬性   方法是 soup.tag['屬性名稱']
<a href="http://blog.csdn.net/watsy">watsy's blog</a>  
soup.a['href']            

(3)find與find_all

find_all(傳回一個清單)

find_all('a')  查找到所有的a
find_all(['a', 'span'])  傳回所有的a和span
find_all('a', limit=2)  隻找前兩個a
           

find(傳回一個對象)

find('a'):隻找到第一個a标簽
find('a', title='名字')
find('a', class_='名字')
#注意
1. 不能使用name屬性查找
2. class_後面有下劃線
3.可以使用自定義屬性,比如age           
def find_all(self, name=None, attrs={}, recursive=True, text=None,limit=None, **kwargs):
tag的名稱,attrs屬性, 是否遞歸,text是判斷内容 limit是提取數量限制 **kwargs 含字典的關鍵字參數

print(soup.find('p')) # 第一個p
print(soup.find_all('p'))  # 所有的p,清單

print(soup.find_all(['b', 'i'])) #找清單中任意一個
print(soup.find_all('a', attrs={'id': 'link2'}))  #限制屬性為 id: link2
print(soup.find_all('a', id='link2')) #關鍵字形式 id=link2
print(soup.find_all('a', limit=2))

#class_屬性
print(soup.find_all('a', class_="sister"))
print(soup.find_all('a', text=re.compile('^L')))           
tag名稱  
soup.find_all('b')  
# [<b>The Dormouse's story</b>]  

正則參數  
import re  
for tag in soup.find_all(re.compile("^b")): #比對所有以b開頭标簽
    print(tag.name)  
# body  
# b  
for tag in soup.find_all(re.compile("t")):  
    print(tag.name)  
# html  
# title  

函數調用  
def has_class_but_no_id(tag):  
    return tag.has_attr('class') and not tag.has_attr('id')  

soup.find_all(has_class_but_no_id)  
# [<p class="title"><b>The Dormouse's story</b></p>,  
#  <p class="story">Once upon a time there were...</p>,  
#  <p class="story">...</p>]  

tag的名稱和屬性查找  
soup.find_all("p", "title")  
# [<p class="title"><b>The Dormouse's story</b></p>]  

tag過濾  
soup.find_all("a")  
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,  
#  <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,  
#  <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]  

tag屬性過濾  
soup.find_all(id="link2")  
# [<a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>]  

text正則過濾  
import re  
soup.find(text=re.compile("sisters"))  
# u'Once upon a time there were three little sisters; and their names were\n'             

(4) select 根據選擇器–節點對象

element   p
.class   .firstname
#id      #firstname
屬性選擇器
    [attribute]        [target]
    [attribute=value]  [target=blank]
層級選擇器
    element element   div p
    element>element   div>p
    element,element   div,p           

(5) 節點資訊

擷取節點内容
    obj.string
    obj.get_text()【推薦】
節點的屬性
    tag.name 擷取标簽名
    tag.attrs将屬性值作為一個字典傳回
擷取節點屬性
    obj.attrs.get('title')
    obj.get('title')
    obj['title']           

5 XPath文法

XPath 使用路徑表達式來選取 XML 文檔中的節點或節點集

安裝導入

import lxml
from lxml import etree           

添加插件

chrome插件網:

http://www.cnplugins.com/

Ctrl + Shift + X打開或關閉插件

(1) XPath的安裝

#安裝lxml庫
pip install lxml
#導入lxml.etree
from lxml import etree

etree.parse()  解析本地html檔案  html_tree = etree.parse('XX.html')
etree.HTML()   解析網絡的html字元串   html_tree = etree.HTML(response.read().decode('utf-8')
html_tree.xpath()    使用xpath路徑查詢資訊,傳回一個清單                                             

(2) 選取節點

表達式 說明
/bookstore 選取根元素 bookstore。注釋:假如路徑起始于正斜杠( / ),則此路徑始終代表到某元素的絕對路徑!
bookstore/book 選取屬于 bookstore 的子元素的所有 book 元素。
//book 選取所有 book 子元素,而不管它們在文檔中的位置。
bookstore//book 選擇屬于 bookstore 元素的後代的所有 book 元素,而不管它們位于 bookstore 之下的什麼位置。
//@lang 選取名為 lang 的所有屬性。
bookstore 選取 bookstore 元素的所有子節點。

(3) 選取元素

路徑表達式 結果
/bookstore/book[1] 選取屬于 bookstore 子元素的第一個 book 元素。
/bookstore/book[last()] 選取屬于 bookstore 子元素的最後一個 book 元素。
/bookstore/book[last()-1] 選取屬于 bookstore 子元素的倒數第二個 book 元素。
/bookstore/book[position()<3] 選取最前面的兩個屬于 bookstore 元素的子元素的 book 元素。
//title[@lang] 選取所有擁有名為 lang 的屬性的 title 元素。
//title[@lang=’eng’] 選取所有 title 元素,且這些元素擁有值為 eng 的 lang 屬性。
/bookstore/book[price>35.00] 選取 bookstore 元素的所有 book 元素,且其中的 price 元素的值須大于 35.00。
/bookstore/book[price>35.00]/title 選取 bookstore 元素中的 book 元素的所有 title 元素,且其中的 price 元素的值須大于 35.00。

(4) 選取若幹路徑

通過在路徑表達式中使用”|”運算符,您可以選取若幹個路徑。

//book/title | //book/price 選取 book 元素的所有 title 和 price 元素。
//title | //price 選取文檔中的所有 title 和 price 元素。
/bookstore/book/title | //price 選取屬于 bookstore 元素的 book 元素的所有 title 元素,以及文檔中所有的 price 元素。
print('myetree.xpath(//*[@class="item-0"])')  # *所有
print('myetree.xpath(//li[@class="item-0" | //div[@class="item-0"]])')  #或運算           

(5) XPath文法總結

節點查詢
    element
路徑查詢
    //  查找所有子孫節點,不考慮層級關系
    /  找直接子節點
謂詞查詢
    //div[@id]
    //div[@id="maincontent"]
屬性查詢
    //@class
邏輯運算
    //div[@id="head" and @class="s_down"]
    //title | //price
模糊查詢
    //div[contains(@id, "he")]
    //div[starts-with(@id, "he")]
    //div[ends-with(@id, "he")]
内容查詢
    //div/h1/text()           

(6)XPath示例

htmlFile = '''
    <ul>
        <li class="item-0"><a href="link1.html">first item</a></li>
        <li class="item-1"><a href="link2.html">second item</a></li>
        <li class="item-inactive"><a href="link3.html">third item</a></li>
        <li class="item-1"><a href="link4.html">fourth item</a></li>
        <li class="item-0"><a href="link5.html">fifth item</a></li> 
    </ul>
    '''

html = lxml.etree.parse("filename.html") # 讀取檔案,需要傳檔案路徑
myetree = lxml.etree.HTML(htmltext) # 直接加載,直接加載html文檔

ul = myetree.xpath('/html/body/ul')  #根節點開始
ul = myetree.xpath('//ul')  #所有的ul子孫
ul = myetree.xpath('/html//ul')   #html下所有的ul,傳回清單

print(html.xpath("//li/@class")) # 取出li的所有節點class名稱
print(html.xpath("//li/@text")) # 為空,如果包含這個屬性,
print(html.xpath("//li/a")) # li下面5個節點,每個節點對應一個元素
print(html.xpath("//li/a/@href")) # 取出li的所有節點 a内部href名稱
print(html.xpath("//li/a/@href=\"link3.html\"")) # 判斷是有一個節點==link3.html

print(html.xpath("//li//span")) # 取出li下面所有的span
print(html.xpath("//li//span/@class")) # 取出li下面所有的span内部的calss
print(html.xpath("//li/a//@class")) # 取出li的所有節點内部節點a包含的class
print(html.xpath("//li")) # 取出所有節點li
print(html.xpath("//li[1]")) # 取出第一個,li[下标]從1開始
print(html.xpath("//li[last()]")) # 取出最後一個
print(html.xpath("//li[last()-1]")) # 取出倒數第2個
print(html.xpath("//li[last()-1]/a/@href")) # 取出倒數第2個的a下面的href

print(html.xpath("//*[@text=\"3\"]")) # 選着text=3的元素
print(html.xpath("//*[@text=\"3\"]/@class")) # 選着text=3的元素
print(html.xpath("//*[@class=\"nimei\"]")) # 選着text=3的元素
print(html.xpath("//li/a/text()")) # 取出a标簽的文本
print(html.xpath("//li[3]/a/span/text()")) # 取出内部<>資料
           

執行個體

爬取照片操作

from lxml import etree
import urllib.request
import urllib.parse
import os
url = 'http://sc.chinaz.com/tupian/shuaigetupian.html'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.162 Safari/537.36'
}

request = urllib.request.Request(url=url,headers=headers)
response = urllib.request.urlopen(request)

# 擷取請求到的html字元串
html_string = response.read().decode('utf-8')

# 将html字元串轉換成etree結構
html_tree = etree.HTML(html_string)

# 解析名字和圖檔
src_list = html_tree.xpath('//div[@id="container"]//div[starts-with(@class,"box")]/div/a/img/@src2')
src_name = html_tree.xpath('//div[@id="container"]//div[starts-with(@class,"box")]/div/a/img/@alt')

# 下載下傳到本地
for index in range(len(src_list)):
    pic_url = src_list[index]
    suffix = os.path.splitext(pic_url)[-1]
    file_name = 'images/' + src_name[index] + suffix
    urllib.request.urlretrieve(pic_url,file_name)  #儲存本地圖檔
           

bs4 xpath與正則

import requests,re
from bs4 import BeautifulSoup
from lxml import etree

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.62 Safari/537.36"
}
url = 'https://sou.zhaopin.com/jobs/searchresult.ashx?jl=%E6%9D%AD%E5%B7%9E&kw=python&sm=0&p=1'

#正則
response = requests.get(url,headers=headers).text
print(re.findall('<em>(\d+)</em>',response))

#bs4
soup = BeautifulSoup(response,'lxml')
print(soup.find_all('span',class_='search_yx_tj')[0].em.text)
print(soup.select('span.search_yx_tj > em')[0].get_text())

#xpath
myetree = etree.HTML(response)
print(myetree.xpath('//span[@class="search_yx_tj"]/em/text()'))
           

xpath綜合運用

import re
import lxml
from lxml import etree
import requests

headers = {
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.181 Safari/537.36"}

def getCity(url):
    '''
    擷取城市清單
    :param url:
    :return: 城市清單
    '''
    response = requests.get(url, headers=headers).content.decode('gbk')
    mytree = lxml.etree.HTML(response)
    # 城市清單
    cityList = mytree.xpath('//div[@class="maincenter"]/div[2]/div[2]//a')
    for city in cityList:
        # 城市名
        cityName = city.xpath('./text()')[0]  # 目前
        # url
        cityurl = city.xpath('./@href')[0]
        print(cityName, cityurl)
        # 調用擷取頁面數量方法
        getPageNum(cityurl)

def getJobInfo(url):
    '''
    擷取崗位資訊
    :param url:
    :return:
    '''
    response = requests.get(url, headers=headers).content.decode('gbk')
    mytree = lxml.etree.HTML(response)
    jobList = mytree.xpath('//div[@class=\'detlist gbox\']/div')
    # 保證有資料
    if len(jobList) != 0:
        for job in jobList:
            # 崗位名稱
            jobName = job.xpath('.//p[@class="info"]/span[1]/a/@title')[0]
            # url
            joburl = job.xpath('.//p[@class="info"]/span[1]/a/@href')[0]
            # 公司名
            company = job.xpath('.//p[@class="info"]/a/@title')[0]
            # 工作地點
            jobAddr = job.xpath('.//p[@class="info"]/span[2]/text()')[0]
            # 薪資
            jobMoney = job.xpath('.//p[@class="info"]/span[@class="location"]/text()')
            if len(jobMoney) == 0:
                jobMoney = "面議"
            else:
                jobMoney = jobMoney[0]
            print(jobName, joburl, company, jobAddr, jobMoney)
            # 職責
            jobResponsibility = job.xpath('.//p[@class="text"]/@title')[0]
            print(jobResponsibility)

def getPageNum(url):
    '''
    擷取頁面數量
    :param url:城市url
    :return:
    '''
    response = requests.get(url, headers=headers).content.decode('gbk')
    mytree = lxml.etree.HTML(response)
    pageNum = mytree.xpath('//*[@id="cppageno"]/span[1]/text()')[0]
    numre = ".*?(\d+).*"
    pageNum = re.findall(numre, pageNum)[0]
    for i in range(1, int(pageNum) + 1):
        newurl = url + 'p%d' % i
        # 擷取崗位資訊
        getJobInfo(newurl)

if __name__ == '__main__':
    starurl = "https://jobs.51job.com/"
    getCity(starurl)