本文分享的大體框架包含以下三部分
(1)首先介紹html網頁,用來解析html網頁的工具xpath
(2)介紹python中能夠進行網絡爬蟲的庫(requests,lxml,scrapy等)
(3)從四個案例出發有易到難依次介紹scrapy集成爬蟲框架
下面開始對三部分內容逐一開始介紹。
一、html和xpath說明
1. html
超文本標記語言,是用來描述網頁的一種語言。主要用于控制數據的顯示和外觀。HTML文檔一定意義上可以被稱為網頁。但反過來說網頁不僅僅是HTML,網頁本質有三部分構成:負責內容結構的HTML,負責表現的CSS,以及負責行為的javascript。本文主要分享的是最核心的內容結構部分。
(1)html結構
完整的HTML文件至少包括<HTML>標簽、<HEAD>標簽、<TITLE>標簽和<BODY>標簽,并且這些標簽都是成對出現的,開頭標簽為<>,結束標簽為</>,在這兩個標簽之間添加內容。通過這些標簽中的相關屬性可以設置頁面的背景色、背景圖像等。
例如,我們打開豆瓣首頁,摁下鍵盤上的F12鍵,打開瀏覽器自帶“開發者工具”,可以看到一個完整的html文檔結構,如下圖
從上圖可以看出,一個完整的html文檔主要包含三部分:DTD文檔頭,head頭部信息和body正文信息。其中DTD文檔頭用來告訴瀏覽器執行標準是什么(比如html4或是html5),head頭部信息用來說明瀏覽器的編碼方式和文檔頭名稱,body顧名思義就是瀏覽器的正文部分。
(2)html標簽
作為開始和結束的標記,由尖括號包圍的關鍵詞,比如 <html>,標簽對中的第一個標簽是開始標簽,第二個標簽是結束標簽。html中常見標簽如下:
其中, “< ul >< li ></li ></ul >”是一種嵌套順序,無序列表,成對出現;li的父元素必須是ul或者ol,不同之處在于ol是一種有序列列表,而ul是無序列表;
(3)html屬性
屬性是用來修飾標簽的,放在開始標簽里里面,html中常見四大屬性:
屬性 | 說明 |
---|---|
class | 規定元素的類名,大多數時候用于指定樣式表中的類 |
id | 唯一標識一個元素的屬性,在html里面必須是唯一的 |
href | 指定超鏈接目標的url |
src | 指定圖像的url |
2. xpath
(1)xpath定義
是一種路徑查詢語言,簡單的說就是利用一個路徑表達式從html文檔中找到我們需要的數據位置,進而將其寫入到本地或者數據庫中。(可以將xpath類比為sql結構化查詢語言)
(2)xpath常見使用方法
符號 | 功能 |
---|---|
// | 表示在整個文本中查找,是一種相對路徑 |
/ | 表示則表示從根節點開始查找,是一種絕對路徑 |
text() | 找出文本值 |
@ | 找出標簽對應的屬性值,比如@href就是找出對應的href鏈接 |
. | 表示當前節點 |
.. | 表示當前節點的父節點 |
當然xpath除了上述常見用法外,還存兩種比較特殊的用法:以相同的字符開頭;標簽套標簽。
用法1:以相同的字符開頭:starts-with(@屬性部分,屬性字符相同部分
用法2:標簽套標簽:string(.)
#以相同的字符開頭
#比如我們想同時提取到下列html中三條文本內容的話,就需要使用starts-with方法
html1 = """
<!DOCTYPE html>
<html>
<head lang='en'>
<meta charest='utf-8'>
<title></title>
</head>
<body>
<div id="test-1">需要的內容1</div>
<div id="test-2">需要的內容2</div>
<div id="testfault">需要的內容3</div>
</body>
</html>
#爬取代碼
from lxml import etree
selector = etree.HTML(html1)
content = selector.xpath('//div[starts-with(@id,"test")]/text()')
for each in content:
print each
還有一種是標簽套標簽形式,參考如下例子
html2 = """
<!DOCTYPE html>
<html>
<head lang='en'>
<meta charest='utf-8'>
<title></title>
</head>
<body>
<div id="test3">
我左青龍,
<span id='tiger'>
右白虎
<ul>上朱雀,
<li>下玄武,</li>
</ul>
</span>
龍頭在胸口
</div>
</body>
</html>
"""
#如果我們想爬取的內容是html文檔中的所有文本的話,需要使用string方法進行提取
selector2 = etree.HTML(html2)
content2 = selector2.xpath('//div[@id="test3"]')[0] #列表,只有一個元素
info = content2.xpath('string(.)')
content3 = info.replace('\n','').replace(' ','')
print content3
(3)xpath的謂語結構
該小節參考資料:阮一峰的網絡日志
所謂"謂語條件",就是對路徑表達式的附加條件。所有的條件,都寫在方括號"[]"中,表示對節點進行進一步的篩選。例如:
<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore>
<book>
<title lang="eng">Harry Potter</title>
<price>29.99</price>
</book>
<book>
<title lang="eng">Learning XML</title>
<price>39.95</price>
</book>
</bookstore>
下面從幾個簡單的例子讓大家體會一下
- /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'] :表示選擇所有lang屬性的值等于"eng"的title節點。
二、python中能夠進行網絡爬蟲的庫
1. 安裝方式
python中安裝包或者模塊的方式一般有以下兩種:
(1)pip install xxx(xxx表示模塊名字)
pip install lxml/numpy/pandas/scrapy/requests
(2)進入到python庫網站下載所需模塊,然后使用pip install xxx.whl安裝即可
pip install lxml?3.7.3?cp27?cp27m?win_amd64.whl
2. requests
import requests
#我的簡書主頁
r = requests.get('http://www.lxweimin.com/u/95f3a80fac3e')
# r本身是一個reponse對象,需要通過content來返回其內容
print r.content
#其實上面通過content之后得到就是一個完整的html文檔,之后可以直接使用lxml等工具直接對其進行解析,下一小節會講到lxml
print r.status_code
print r.encoding #html的編碼方式,一般是UTF-8
print r.cookies
3. lxml
lxml中集成了剛才講述的xpath這種路徑查詢語言;例如我們首先創建一個html文檔如下
html= """
<!DOCTYPE html>
<html>
<head lang='en'>
<meta charest='utf-8'>
<title></title>
</head>
<body>
<div id="test-1">需要的內容1</div>
<div id="test-2">需要的內容2</div>
<div id="testfault">需要的內容3</div>
</body>
</html>
"""
然后使用lxml對我們想要的內容進行爬取
from lxml import etree
selector = etree.HTML(html)
content = selector.xptah('path') #此處的path指的就是需要爬蟲的文件路徑
for item in content:
print item
前面講到的requests通常也是配合lxml使用,首先使用requests獲取到網頁內容,即html文檔,然后使用lxml中的xpath爬取我們所需要的內容。例子如下:
#爬取豆瓣電影top250,并將電影名稱和評分打印出來
import requests
from lxml import etree
s = requests.Session() #開啟一個requests會話
for id in range(0, 251, 25):
url = 'https://movie.douban.com/top250/?start-' + str(id)
r = s.get(url) #返回是一個reponse對象
r.encoding = 'utf-8'
root = etree.HTML(r.content)
items = root.xpath('//ol/li/div[@class="item"]')
# print(len(items))
for item in items:
title = item.xpath('./div[@class="info"]//a/span[@class="title"]/text()')
name = title[0].encode('gb2312', 'ignore').decode('gb2312')
# rank = item.xpath('./div[@class="pic"]/em/text()')[0]
rating = item.xpath('.//div[@class="bd"]//span[@class="rating_num"]/text()')[0]
print(name, rating)
4. 大殺器scrapy
scrapy是一個為了爬取網站數據,提取結構性數據而編寫的應用框架。 可以應用在包括數據挖掘,信息處理或存儲歷史數據等一系列的程序中。我們需要知道的是,scrapy是一種集成框架,類似于request和xpath這些方法在scrapy都有集成。
(1)scrapy安裝
安裝scrapy前,需要先安裝lxml模塊,然后按照之前說的方法進行安裝就可以(我的電腦安裝的是anaconda集成環境)
(2)scrapy的結構
其中,不同模塊負責不同的任務分工。首先Scheduler發出請求(Requests),Downloader負責從互聯網上下載內容,將下載好的內容(Responses)交給Spiders進行解析,解析完成后將內容Item返回,當然其中可能會涉及到對于解析后數據的進一步處理,這個任務是在Pipeline中完成的。
(3)scrapy通常有兩種使用方式:
- 直接在python腳本里定義一個爬取數據的類(參見爬蟲案例1,2,3)
- 創建完整的scrapy項目(參見爬蟲案例4);創建命令為
scrapy startproject xxx
創建完scrapy項目后,會在對應的文件下生成如下結構:
其中,
spiders文件夾下面是真正爬蟲的代碼;
items之前提到過,是定義需要爬取的內容;
pipelines是對爬取到的數據進行的進一步處理;
settings中主要是一些環境變量和配置。
另外,scrapy有提供兩個xpath選擇器,HtmlXPathSelector和XmlXPathSelector,一個用于html,一個用于xml,xpath選擇器有三個方法:
- select(xpath): 返回一個相對于當前選中節點的選擇器列表(一個xpath可能選到多個節點)
-
extract(): 返回選擇器(列表)對應的節點的字符串(列表,類型就是python中的list)
-_ re(regex)_: 返回正則表達式匹配的字符串(分組匹配)列表
需要注意的是,如果爬取的頁面相對簡單,爬取內容較少,且不用對爬取到的數據做過多的后期處理,使用第一種,反之,使用第二種,當然建立完整的scrapy項目肯定可以處理簡單任務。
三、爬蟲案例
1. 想要爬取得內容都在同一頁面
本實例中,我們通過scrapy爬取七月在線課程信息。
#scrapy runspider spider.py –o xxx.json(運行時可以在命令窗口進行)
import scrapy
class JulyeduSpider(scrapy.Spider):
name = "julyedu"
start_urls = ['https://www.julyedu.com/category/index'] #開始爬取網址,是一個list列表
# 定義解析頁面函數
def parse(self, response):
for julyedu_class in response.xpath('//div[@class="course_info_box"]'):
print julyedu_class.xpath('a/h4/text()').extract_first()
print julyedu_class.xpath('a/p[@class="course-info-tip"][1]/text()').extract_first()
print julyedu_class.xpath('a/p[@class="course-info-tip"][2]/text()').extract_first()
print response.urljoin(julyedu_class.xpath('a/img[1]/@src').extract_first())
print "\n"
# 返回函數值
yield {
'title':julyedu_class.xpath('a/h4/text()').extract_first(),
'desc': julyedu_class.xpath('a/p[@class="course-info-tip"][1]/text()').extract_first(),
'time': julyedu_class.xpath('a/p[@class="course-info-tip"][2]/text()').extract_first(),
'img_url': response.urljoin(julyedu_class.xpath('a/img[1]/@src').extract_first())
}
2. 想要爬去的內容在多頁,不同頁之間可以進行人為拼接構成(例如博客園 )
本實例中,我們利用scrapy爬取博客園中的信息,爬取內容包括每條博文的標題、鏈接、作者、評論等信息
class CnBlogSpider(scrapy.Spider):
name = "cnblogs"
allowed_domains = ["cnblogs.com"]
start_urls = [ 'http://www.cnblogs.com/pick/#p%s' % p for p in xrange(1, 11) ]
#定義解析函數
def parse(self, response):
for article in response.xpath('//div[@class="post_item"]'):
print article.xpath('div[@class="post_item_body"]/h3/a/text()').extract_first().strip()
print response.urljoin(article.xpath('div[@class="post_item_body"]/h3/a/@href').extract_first()).strip()
print article.xpath('div[@class="post_item_body"]/p/text()').extract_first().strip()
print article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/a/text()').extract_first().strip()
print response.urljoin(article.xpath('div[@class="post_item_body"]/div/a/@href').extract_first()).strip()
print article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/span[@class="article_comment"]/a/text()').extract_first().strip()
print article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/span[@class="article_view"]/a/text()').extract_first().strip()
print ""
yield {
'title': article.xpath('div[@class="post_item_body"]/h3/a/text()').extract_first().strip(),
'link': response.urljoin(article.xpath('div[@class="post_item_body"]/h3/a/@href').extract_first()).strip(),
'summary': article.xpath('div[@class="post_item_body"]/p/text()').extract_first().strip(),
'author': article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/a/text()').extract_first().strip(),
'author_link': response.urljoin(article.xpath('div[@class="post_item_body"]/div/a/@href').extract_first()).strip(),
'comment': article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/span[@class="article_comment"]/a/text()').extract_first().strip(),
'view': article.xpath('div[@class="post_item_body"]/div[@class="post_item_foot"]/span[@class="article_view"]/a/text()').extract_first().strip(),
}
3. 想要爬取的內容存在頁面跳轉(例如騰訊社會新聞)
class QQNewsSpider(scrapy.Spider):
name = 'qqnews'
start_urls = ['http://news.qq.com/society_index.shtml']
def parse(self, response):
for href in response.xpath('//*[@id="news"]/div/div/div/div/em/a/@href'):
full_url = response.urljoin(href.extract())
yield scrapy.Request(full_url, callback=self.parse_question) #調用scrapy中Request方法,對合并后的網址進行分析,調用函數是parse_question
#真正意義上解析頁面的函數
def parse_question(self, response):
print response.xpath('//div[@class="qq_article"]/div/h1/text()').extract_first()
print response.xpath('//span[@class="a_time"]/text()').extract_first()
print response.xpath('//span[@class="a_catalog"]/a/text()').extract_first()
print "\n".join(response.xpath('//div[@id="Cnt-Main-Article-QQ"]/p[@class="text"]/text()').extract())
print ""
yield {
'title': response.xpath('//div[@class="qq_article"]/div/h1/text()').extract_first(),
'content': "\n".join(response.xpath('//div[@id="Cnt-Main-Article-QQ"]/p[@class="text"]/text()').extract()),
'time': response.xpath('//span[@class="a_time"]/text()').extract_first(),
'cate': response.xpath('//span[@class="a_catalog"]/a/text()').extract_first(),
}
4. 通過創建scrapy工程的方式爬取全國34個省、市所屬的2290個地區的歷史天氣預報數據,網址請戳這里,并將其保存為json格式
之前跟大家分享的案例大多是參照網絡視頻資源和相關博客資料,這兩天由于項目需要,爬取全國34個省、市所屬的2290個地區的歷史天氣預報數據,真正動手才發現“紙上得來終覺淺,絕知此事要躬行”的道理所在,整個過程碰到很多坑,也請教了一些牛人,終于將數據成功爬取到本地,在此記錄下整個爬取過程。
1. 多級頁面跳轉問題
涉及到多級頁面跳轉才能爬取到數據的場景,有兩個方面是需要特別注意的,第一是確保整個頁面跳轉過程的邏輯正確,第二是跳轉到某個具體頁面使用xpath進行路經查詢時,要保證xpath的寫的沒有問題。
2. Scrapy終端(Scrapy shell)
Scrapy終端是一個交互終端,供您在未啟動spider的情況下嘗試及調試您的爬取代碼。 其本意是用來測試提取數據的代碼,不過您可以將其作為正常的Python終端,在上面測試任何的Python代碼。
對于檢查xpath路徑查詢語言是否正確非常,舉個例子,我想要爬取某個城市2017年一月的天氣中的日期數據,
根據html中提供的路徑,我寫了如下的xpath表達式
day = sel.xpath('//div[@class="tqtongji2"]/ul[position()>1]/li[1]/a/text()').extract()
具體對不對,能不能爬取到相對應的日期數據,我們就可以利用scrapy shell進行檢查。
(1)首先在命令窗口啟動scrapy shell,啟動語句也很簡單,如下
scrapy shell url
其中url表示想測試的網頁鏈接。
(2)輸入需要檢查的xpath語句
可以看到輸出結果中已經正確提取出我們需要的日期數據,從而也說明我們寫的xpath路徑沒有問題。
3. 爬蟲正式開始
(1)建立scrapy工程
利用前面講到的方法創建相應的scrapy工程,在item.py中創建需要爬去的數據如下:
(2)爬蟲spider腳本
我們打開需要爬取頁面的首頁如下,http://lishi.tianqi.com/,
從頁面中我們可以看到,全國各縣區的城市按照A,B,C...Z順序排列,每個字母下面包含很多城市,比如字母A下面包含阿城等地方。且每一類下面的第一個li標簽是不需要的,因為第一個li標簽表示的是字母A,B,C...Z等,如下圖
分析到這一步,我們可以寫出一級解析函數parse來獲取所有城市—鏈接和城市名的xpath路徑查詢語言,
鏈接如下:
sel = Selector(response)
country_urls = sel.xpath('//ul[@class="bcity"]/li[position()>1]/a/@href').extract()
城市名如下:
sel = Selector(response)
country_urls = sel.xpath('//ul[@class="bcity"]/li[position()>1]/a/text()').extract()
接下來就是每個城市(鏈接)進行for循環遍歷,將每個城市的鏈接url和城市名城保存到item中,保存file_name目的在于為了待會兒寫入數據時方便。將所有獲取到的城市鏈接和城市名保存到items列表中,然后使用for循環對每個城市的進行二級頁面解析,調用的是scrapy的Request方法,Request中的回調函數callback就是二級頁面解析函數second_parse。一級解析函數parse的完整代碼如下:
既然說到二級頁面,我們還是首先觀察下二級頁面的特點,以澳門歷史天氣詳情為例
可以看到,澳門所有歷史數據(按月)都在div class = "tqtongji" 標簽下面,每年的數據又被一個ul標簽包圍,每個ul標簽下面擁有很多個li標簽,分別表示一年的各個月份數據,分析到此,我們同樣可以在二級解析頁面函數中寫出獲取每個城市每個月鏈接和月份名稱的xpath路徑
鏈接如下:
sel = Selector(response)
month_urls = sel.xpath('//div[@class="tqtongji1"]/ul/li/a/@href').extract()
城市名如下:
sel = Selector(response)
month_titles = sel.xpath('//div[@class="tqtongji1"]/ul/li/a/text()').extract()
同樣,獲取到每個城市每個月份的鏈接后,在二級解析函數里面對每個月份進行遍歷,最后仍然使用scrapy.Request對每個獲取的月份鏈接進行解析,回調函數是三級頁面解析函數detail_parse。
以下是二級頁面解析函數的腳本
最后跳轉到我們最終要爬取數據的頁面了,到了這一頁面之后(如下圖),便能夠很方便的爬取到我們需要的數據。
直接貼上最終三級頁面解析函數
到此,爬取數據的spider腳本已經全部開發完成,不過為了將數據按照城市分別保存到本地,還需要在pipeline中進行相應設置如下:
最終爬取的效果圖如下圖:
喜歡的朋友請小小的點個贊,你的肯定會讓我更有動力!!!