其實node.js也可以做爬蟲,相信前端的同學已經(jīng)有所了解困食,但今天我們要講的是如何用python笼蛛,python實現(xiàn)起來其實更簡單趟妥。
import urllib.request
url = "http://www.baidu.com"
response = urllib.request.urlopen(url).read()
data = data.decode('UTF-8')
print(data) //data是html標簽內(nèi)容
urllib是python里面一個處理urls的庫祟敛,可以看教程,這里簡單介紹一下疤坝。
import urllib.request
with urllib.request.urlopen('http://www.python.org/') as f:
print(f.read(300))
This example gets the python.org main page and displays the first 300 bytes of it.
上面的例子展示了讀取該網(wǎng)頁的前300個字節(jié)兆解。
除了可以向urlopen方法里面?zhèn)鬟f網(wǎng)頁地址馆铁,還可以構造一個request對象。
import urllib.request
DATA = b'some data'
// 構造一個request請求對象锅睛,包括請求地址埠巨,參數(shù)和請求方法
req = urllib.request.Request(url='http://localhost:8080', data=DATA,method='PUT')
with urllib.request.urlopen(req) as f:
pass
我們來看看下面這段打印提示
a = urllib.request.urlopen(full_url)
type(a)
<class ‘http.client.HTTPResponse’>
a.geturl()
‘http://www.baidu.com/s?word=Jecvay’
a.info()
<http.client.HTTPMessage object at 0x03272250>
a.getcode()
200
以上可以看出構造出的a都包含哪些內(nèi)容。
如果要抓取百度上面搜索關鍵詞kobe bryant的網(wǎng)頁, 則代碼如下
import urllib
import urllib.request
data={}
data['word']='kobe bryant'
url_values=urllib.parse.urlencode(data)
url="http://www.baidu.com/s?"
full_url=url+url_values
data=urllib.request.urlopen(full_url).read()
data=data.decode('UTF-8')
print(data)
我們看下urllib.parse這個模塊的基本用法
urlencode函數(shù)现拒,可以把key-value這樣的鍵值對轉換成我們想要的格式辣垒,返回的是a=1&b=2這樣的字符串,比如:
from urllib import urlencode
data = {
'a': 'test',
'name': '魔獸'
}
print urlencode(data)
a=test&name=%C4%A7%CA%DE
from urllib.parse import urlparse
o = urlparse('http://www.cwi.nl:80/%7Eguido/Python.html')
o
ParseResult(scheme='http', netloc='www.cwi.nl:80', path='/%7Eguido/Python.html',
params='', query='', fragment='')
o.scheme 'http'
o.port 80
o.geturl() 'http://www.cwi.nl:80/%7Eguido/Python.html'
在寫代碼之前我們需要先了解一下python中隊列的知識印蔬。
from collections import deque
queue = deque(["Eric", "John", "Michael"])
queue.append("Terry") # Terry 入隊
queue.append("Graham") # Graham 入隊
queue.popleft() # 隊首元素出隊
#輸出: 'Eric'
queue.popleft() # 隊首元素出隊
#輸出: 'John'
queue # 隊列中剩下的元素
#輸出: deque(['Michael', 'Terry', 'Graham'])
List用來完成隊列功能其實是低效率的, 因為List在隊首使用 pop(0) 和 insert()
都是效率比較低的, Python官方建議使用collection.deque來高效的完成隊列任務
好了介紹完基本知識勋桶,就開始寫代碼了,畢竟我是一個coder...
import re
import urllib.request
import urllib
from collections import deque
queue = deque()
visited = set()
url = 'http://news.dbanotes.net' # 入口頁面, 可以換成別的
queue.append(url)
cnt = 0
while queue:
url = queue.popleft() # 隊首元素出隊
visited |= {url} # 標記為已訪問
print('已經(jīng)抓取: ' + str(cnt) + ' 正在抓取 <--- ' + url)
cnt += 1
urlop = urllib.request.urlopen(url)
if 'html' not in urlop.getheader('Content-Type'):
continue
# 避免程序異常中止, 用try..catch處理異常
try:
data = urlop.read().decode('utf-8')
except:
continue
# 正則表達式提取頁面中所有隊列, 并判斷是否已經(jīng)訪問過, 然后加入待爬隊列
linkre = re.compile('href="(.+?)"')
for x in linkre.findall(data):
if 'http' in x and x not in visited:
queue.append(x)
print('加入隊列 ---> ' + x)
上面的一個例子侥猬,是先爬取百度首頁例驹,獲取頁面帶有href的a標簽,然后放入隊列退唠,繼續(xù)爬取該地值得內(nèi)容狼荞,直到隊列里面沒有元素為止缩赛。
顯然這還不是我們想要的結果,咱們繼續(xù)來看
// content是爬取到的內(nèi)容
content = response.read().decode('utf-8')
pattern = re.compile('<div.*?class="author.*?>.*?<a.*?</a>.*?<a.*?>(.*?)</a>.*?<div.*?class'+
'="content".*?title="(.*?)">(.*?)</div>(.*?)<div class="stats.*?class="number">(.*?)</i>',re.S)
items = re.findall(pattern,content) 等同于 pattern.findall(content)
for item in items:
print item[0],item[1],item[2],item[3],item[4]
1).? 是一個固定的搭配台汇,.和代表可以匹配任意無限多個字符,加上观蜗?表示使用非貪婪模式進行匹配,也就是我們會盡可能短地做匹配,以后我們還會大量用到 .*? 的搭配圆丹。
2)(.?)代表一個分組,在這個正則表達式中我們匹配了五個分組躯喇,在后面的遍歷item中运褪,item[0]就代表第一個(.?)所指代的內(nèi)容,item[1]就代表第二個(.*?)所指代的內(nèi)容玖瘸,以此類推秸讹。
其中,附加圖片的內(nèi)容我把圖片代碼整體摳了出來雅倒,這個對應item[3]璃诀,我們將忽略帶有圖片的分組。
import urllib
import urllib2
import re
page = 1
url = 'http://www.qiushibaike.com/hot/page/' + str(page)
user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
headers = { 'User-Agent' : user_agent }
try:
request = urllib2.Request(url,headers = headers)
response = urllib2.urlopen(request)
content = response.read().decode('utf-8')
pattern = re.compile('<div.*?class="author.*?>.*?<a.*?</a>.*?<a.*?>(.*?)</a>.*?<div.*?class'+
'="content".*?title="(.*?)">(.*?)</div>(.*?)<div class="stats.*?class="number">(.*?)</i>',re.S)
items = re.findall(pattern,content)
for item in items:
haveImg = re.search("img",item[3])
if not haveImg:
print item[0],item[1],item[2],item[4]
except urllib2.URLError, e:
if hasattr(e,"code"):
print e.code
if hasattr(e,"reason"):
print e.reason
熟悉python的同學都知道蔑匣,它是一們面向對象的語言劣欢,我們下面就用面向對象的方式來完成這個抓取段子的功能。
__author__ = 'CQC'
# -*- coding:utf-8 -*-
import urllib
import urllib2
import re
import thread
import time
#糗事百科爬蟲類
class QSBK:
#初始化方法裁良,定義一些變量
def __init__(self):
self.pageIndex = 1
self.user_agent = 'Mozilla/4.0 (compatible; MSIE 5.5; Windows NT)'
#初始化headers
self.headers = { 'User-Agent' : self.user_agent }
#存放段子的變量凿将,每一個元素是每一頁的段子們
self.stories = []
#存放程序是否繼續(xù)運行的變量
self.enable = False
#傳入某一頁的索引獲得頁面代碼
def getPage(self,pageIndex):
try:
url = 'http://www.qiushibaike.com/hot/page/' + str(pageIndex)
#構建請求的request
request = urllib2.Request(url,headers = self.headers)
#利用urlopen獲取頁面代碼
response = urllib2.urlopen(request)
#將頁面轉化為UTF-8編碼
pageCode = response.read().decode('utf-8')
return pageCode
except urllib2.URLError, e:
if hasattr(e,"reason"):
print u"連接糗事百科失敗,錯誤原因",e.reason
return None
#傳入某一頁代碼,返回本頁不帶圖片的段子列表
def getPageItems(self,pageIndex):
pageCode = self.getPage(pageIndex)
if not pageCode:
print "頁面加載失敗...."
return None
pattern = re.compile('<div.*?author">.*?<a.*?<img.*?>(.*?)</a>.*?<div.*?'+
'content">(.*?)<!--(.*?)-->.*?</div>(.*?)<div class="stats.*?class="number">(.*?)</i>',re.S)
items = re.findall(pattern,pageCode)
#用來存儲每頁的段子們
pageStories = []
#遍歷正則表達式匹配的信息
for item in items:
#是否含有圖片
haveImg = re.search("img",item[3])
#如果不含有圖片价脾,把它加入list中
if not haveImg:
replaceBR = re.compile('<br/>')
text = re.sub(replaceBR,"\n",item[1])
#item[0]是一個段子的發(fā)布者牧抵,item[1]是內(nèi)容,item[2]是發(fā)布時間,item[4]是點贊數(shù)
pageStories.append([item[0].strip(),text.strip(),item[2].strip(),item[4].strip()])
return pageStories
#加載并提取頁面的內(nèi)容侨把,加入到列表中
def loadPage(self):
#如果當前未看的頁數(shù)少于2頁犀变,則加載新一頁
if self.enable == True:
if len(self.stories) < 2:
#獲取新一頁
pageStories = self.getPageItems(self.pageIndex)
#將該頁的段子存放到全局list中
if pageStories:
self.stories.append(pageStories)
#獲取完之后頁碼索引加一,表示下次讀取下一頁
self.pageIndex += 1
#調用該方法秋柄,每次敲回車打印輸出一個段子
def getOneStory(self,pageStories,page):
#遍歷一頁的段子
for story in pageStories:
#等待用戶輸入
input = raw_input()
#每當輸入回車一次获枝,判斷一下是否要加載新頁面
self.loadPage()
#如果輸入Q則程序結束
if input == "Q":
self.enable = False
return
print u"第%d頁\t發(fā)布人:%s\t發(fā)布時間:%s\t贊:%s\n%s" %(page,story[0],story[2],story[3],story[1])
#開始方法
def start(self):
print u"正在讀取糗事百科,按回車查看新段子,Q退出"
#使變量為True骇笔,程序可以正常運行
self.enable = True
#先加載一頁內(nèi)容
self.loadPage()
#局部變量省店,控制當前讀到了第幾頁
nowPage = 0
while self.enable:
if len(self.stories)>0:
#從全局list中獲取一頁的段子
pageStories = self.stories[0]
#當前讀到的頁數(shù)加一
nowPage += 1
#將全局list中第一個元素刪除,因為已經(jīng)取出
del self.stories[0]
#輸出該頁的段子
self.getOneStory(pageStories,nowPage)
spider = QSBK()
spider.start()
代碼我們是寫出來了笨触,但是這種用正則匹配獲取html內(nèi)容的方式實在是很復雜懦傍,那么我們怎么解決這個問題呢?既然我們能夠抓取網(wǎng)頁html元素旭旭,那么有沒有一種庫像jquery一樣能夠解析dom書呢谎脯,答案是有的,下面開始介紹Beautiful Soup持寄。
假設有這樣一段html代碼
html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" id="link1">Elsie</a>,
<a class="sister" id="link2">Lacie</a> and
<a class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)
soup.title
# <title>The Dormouse's story</title>
soup.title.name
# u'title' 相當于是獲得標簽名
soup.title.string
# u'The Dormouse's story' 獲取的是標簽里的文字內(nèi)容
soup.title.parent.name
# u'head' 熟悉jquery的同學一看就懂
soup.p
# <p class="title"><b>The Dormouse's story</b></p> 這種寫法其實獲得是第一個p元素
soup.p['class']
# u'title' 獲取第一個p的class屬性源梭,如果有多個class娱俺,返回list
soup.find_all('a')
[<a class="sister" id="link1">Elsie</a>,
<a class="sister" id="link2">Lacie</a>,
<a class="sister" id="link3">Tillie</a>]
這里是查找html標簽里面所有的a元素,返回的是一個數(shù)組
soup.find(id="link3")
# <a class="sister" id="link3">Tillie</a>
好吧废麻,這不是jquery里面的屬性選擇器嗎
soup.find(id="link3").get('href')
# http://example.com/tillie
相信大家看了上面之后荠卷,覺得還是很簡單的把,咱們接著玩下看烛愧。
html_doc = """
<html>
<head>
<title>The Dormouse's story</title>
<div>The Dormouse's story</div>
</head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">Once upon a time there were three little sisters; and their names were
<a class="sister" id="link1">Elsie</a>,
<a class="sister" id="link2">Lacie</a> and
<a class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>
<p class="story">...</p>
"""
from bs4 import BeautifulSoup
soup = BeautifulSoup(html_doc)
head_tag = soup.head
head_tag
# <head><title>The Dormouse's story</title></head>
head_tag.contents
[<title>The Dormouse's story</title>,<div>The Dormouse's story</div>
]
獲取某個元素的子元素油宜,取某個可以以下標的方式
title_tag = soup.title
title_tag.parent
# <head>
<title>The Dormouse's story</title>
<div>The Dormouse's story</div>
</head>
link = soup.a
link
# <a class="sister" id="link1">Elsie</a>
for parent in link.parents:
if parent is None:
print(parent)
else:
print(parent.name)
# p
# body
# html
# [document]
# None
通過元素的 .parents 屬性可以遞歸得到元素的所有父輩節(jié)點,下面的例子使用了 .parents 方法遍歷了<a>標簽到根節(jié)點的所有節(jié)點.
講到兄弟節(jié)點可能和jquery不大一樣,分的特別細怜姿。
sibling_soup = BeautifulSoup("<a><b>text1</b><c>text2</c></b></a>")
sibling_soup.b.next_sibling
# <c>text2</c>
sibling_soup.c.previous_sibling
# <b>text1</b>
當然我們也可通過 .next_siblings 和 .previous_siblings 屬性可以對當前節(jié)點的兄弟節(jié)點迭代輸出:
for sibling in soup.a.next_siblings:
print(repr(sibling))
# u',\n'
# <a class="sister" id="link2">Lacie</a>
# u' and\n'
# <a class="sister" id="link3">Tillie</a>
# u'; and they lived at the bottom of a well.'
# None
需要注意的是兄弟節(jié)點也包括換行符和字符串等慎冤。
當然也可以使用正則來查找:
import re
for tag in soup.find_all(re.compile("^b")):
print(tag.name)
# body
# b
soup.find_all("p", "title")
# [<p class="title"><b>The Dormouse's story</b></p>]
其實還有什么append和insert方法,簡直和js dom操作如出一轍沧卢,jiang