寫在前面
最近的課設是用python寫一個簡單爬蟲,然而不讓用使用起來便捷的第三方模塊和爬蟲框架,要求基於socket來寫,死磕了好幾天,結果還只能爬一丟丟啦,最後才查資料找到了問題所在,其實是遇到了簡單的蜜罐,聽了好久蜜罐,還真讓我遇上一次,不用第三方真的繞不過去啊,還好老師沒太難爲人,不過自己也是挺生氣哦,留個坑吧,以後抽時間再寫一個功能強的說啥也得繞過了!
不多說了,報告整上,做這個爬蟲需要一些web端知識哦,整體思路就是模擬瀏覽器發送request,接受返回的數據,再進行儲存,之後解析抽離出自己需要的數據,註釋都寫得很清楚了,只不過有些亂,沒寫的我會截圖說明,註釋的print都是我bebug用的
爬取目標是 https://movie.douban.com/top250?start=0&filter= 以及後面的頁面
一、需求分析
網絡爬蟲是從 web 中發現 ,下載以及存儲內容,是搜索引擎的核心部分。傳統爬蟲從一個或若干初始網頁的 URL 開始,獲得初始網頁上的 URL ,在抓取網頁的過程中,不斷從當前頁面上抽取新的 URL 放入隊列,直到滿足系統的一定停止條件。
選擇自己熟悉的開發環境和編程語言,實現網絡爬蟲抓取頁面、從而形成結構化數據的基本功能,界面適當美化。
二、系統設計
在本爬蟲程序中共有三個模塊:
1、爬蟲調度端:啓動爬蟲,停止爬蟲,監視爬蟲的運行情況。
2、爬蟲模塊:包含三個小模塊,URL 管理器、網頁下載器、網頁解析器。
(1)URL 管理器:對需要爬取的 URL 和已經爬取過的 URL 進行管理,可以從 URL管理器中取出一個待爬取的 URL,傳遞給網頁下載器。
(2)網頁下載器:網頁下載器將 URL 指定的網頁下載下來,存儲成一個字符串,傳遞給網頁解析器。
(3)網頁解析器:網頁解析器解析傳遞的字符串,解析器不僅可以解析出需要爬取的數據,而且還可以解析出每一個網頁指向其他網頁的 URL,這些 URL 被解析出來會補充進 URL 管理器
3、數據輸出模塊:存儲爬取的數據。
三、系統實現
文件1 爬蟲主體
#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author:XUE
# 目標是 https://movie.douban.com/top250?start=25&filter=
import socket
import ssl
import random
from tkinter import *
#網站的四個組成部分
#協議 主機 端口 路徑
#函數一 模仿瀏覽器訪問功能 構建發送GET請求 接收返回數據
def get (url):
# 目標是 https://movie.douban.com/top250?start=25&filter=
print('url是',url)
u = url.split('://')[1] #切片獲取 協議 域名 路徑
protocol = url.split('://')[0]
i = u.find('/')
host = u[:i]
path = u[i:]
if protocol == 'https': #根據是http還是https 進行傳輸方式判斷
s = ssl.wrap_socket(socket.socket())#創建實例 也可以說是建立連接
port = 443 #默認端口
else:
s = socket.socket()#創建實例
port = 80
# print(protocol)
s.connect((host,port))
request = 'GET {} HTTP/1.1\r\nhost:{}\r\n\r\n'.format(path,host)
# Cookie = {'bid=wPX71ZOfcqg': 'douban-fav-remind=1'}
# print('request是',request)
encoding = 'utf-8'
s.send(request.encode(encoding))
response = b''
buffer_size = 1024
while True :
# print('response是',response)
r = s.recv(buffer_size)
response += r
if len(r) < buffer_size:
break
response = response.decode(encoding)
return response
#函數二 這裏有十個頁面 十個URL 寫一個函數來遍歷十個頁面 得到十個頁面源代碼
def htmls_from_douban():
html = []#創建空列表
url = """https://movie.douban.com/top250?start={}&filter="""
for index in range (0,250,25):#依次是 0 25 50 ...
u = url.format(index) #把index傳入url 得到一個完整的url 就是u
# print('url 是',u) #方便檢測
r = get (u) #調用第一個函數 得到 字符串
# print(' r是',r)
html.append(r) #把字符串添加到列表
return html #得到一個列表 有十個元素 每個元素都是get函數得到的字符串
print('htmls 是',list(html))
# print('htmls',htmls_from_douban())
#函數三 處理得到的頁面源代碼 這裏思路是例如想爬取這些電影的名字 我就去搜尋前後的關鍵字
#可以理解爲一個複雜的字符串操作
#假如調用這個函數的話,先找到一個標題,進入while循環
#把標題放入列表,再繼續查,查到再放入列表,最後跳出
def findall_in_html(html,startpart,endpart):#前後的字符串關鍵字
all_strings = []#定義空列表
#find函數是字符串自帶的函數
#find()方法檢測字符串中是否包含子字符串str ,如果指定beg(開始)和 end(結束)範圍,
#則檢查是否包含在指定範圍內,如果包含子字符串返回開始的索引值,否則返回-1。
start = html.find(startpart) + len(startpart) #得到起始下標 也就是>的位置
end = html.find(endpart,start)#結尾下標 也就是從start開始找 也就是後半部分<的位置
string = html[start:end] #通過下標取到字符串 這裏採用切片
# print('string是',string)
#通過while循環 來滿足25個操作 (一頁25個標題)
# print('進去了嗎',html.find ('</html>')> start > html.find ('<html'))
while html.find ('</html>')> start > html.find ('<html'):#web頁面的頭到尾
all_strings.append(string)
start = html.find(startpart) + len(startpart)
end = html.find(endpart,start)
string = html[start:end]
# print('all_string是',list(all_strings))
return all_strings
#函數四 處理獲取到的html中的中文電影名 因爲名字分了很多繁體字或者英文 所以不可能一步到位
#注意這時候的這些名字是一個列表
def movie_name(html):
# print('html是',html)
name = findall_in_html(html,'<span class="title">','</span>')
print('name是',name)
for i in name:#利用一個for循環來處理得到的名字
if 'nbsp' in i:
name.remove(i)
return name
#函數五 獲取評分
def movie_score(html):
score = findall_in_html(html,'<span class="rating_num" property="v:average">','</span>')
return score
#函數六 獲取引言
def movie_inq(html):
inq = findall_in_html(html,'<span class="inq">','</span>')
return inq
#函數八 調用上面的函數來獲取數據
def movie_data_from_html(htmls):
movie = []
score = []
inq = []
# print('html2是',list(htmls))
for h in htmls: #遍歷十個頁面 htmls是一個列表 十個元素 每個元素是一個html
m = movie_name(h)
s = movie_score(h)
i = movie_inq(h)
# print('m是',list(m))
# print('s是',list(s))
#extend 參數只接受列表 將列表中的元素添加到自己列表的後面
movie.extend(m)
score.extend(s)
inq.extend(i)
data = zip(movie,score,inq)#zip函數用來打包 把每個列表的一二三個元素 依此結合
return data
# print('data是',list(data))
#函數九 保存打印出的東西 相當於日誌
#**args表示任何多個無名參數,它是一個tuple,Python將**args從開始到結束作爲一個tuple傳入函數
#**kwargs表示關鍵字參數,它是一個dict,Python將**kwargs從開始到結束作爲一個dict傳入函數
#函數根據定義對這個tuple或dict進行處理
def log (*args,**kwargs):
with open ('movie.txt','a',encoding='utf-8') as f : #打開movie.txt文件
print (*args,file=f,**kwargs)
#file是print的一個參數 file=f將print的輸出重定向到f 此用法多用於把日誌寫入文件
#函數十 主函數
def main():
htmls = htmls_from_douban() #返回一個列表 十個元素 每個元素是一個html
#print('html1是',list(htmls))
movie_data = movie_data_from_html(htmls) #movie_data格式[('電影名','評分','引言','評分人數'),(),()]
# print('movie_data是',list(movie_data))
counter = 0 # 電影編號
for item in movie_data:
# print('item是',item)
counter = counter + 1
# print('是否log出來' + "NO." + str(counter))
log('NO.'+str(counter))#字符串拼接
log('電影名:',item[0])
log('評分:',item[1])
log('引言:',item[2])
# movie_log('評分人數:',item[3],'\n\n')
#函數唯一入口 當單獨運行這個文件就運行main 當只是當模塊調用就不運行main
# if __name__ == "__main__":
# # print(__name__)
# # print("__main__")
# main()
文件二 運行界面主體
注意要用多線程寫
from tkinter import *
import tkinter.messagebox
import test2
import os
import threading
def judge():
t2.start()
def stop():
tk.destroy()
def show():
os.startfile(r'C:\Users\x\Desktop\py2\movie.txt')
def GUI():
global tk
tk=Tk() #實例化
tk.title('豆瓣爬蟲 made by 薛世豪') #標題
tk.geometry('700x600') #窗口大小
tk.iconbitmap("123456.ico") #左上角小圖標
#標籤控件,顯示文本和位圖,展示在第一行
Label(tk,text="點擊按鈕開始爬取").grid(row=0,sticky=E)# 第一行 靠右
button1=Button(tk,text="啓動",font=('Arial',20),bg="orange", fg="red",command=judge)
button1.grid(row=2,column=1)
button2=Button(tk,text="退出",font=('Arial',20),bg="orange", fg="red",command=stop)
button2.grid(row=4,column=1)
button3=Button(tk,text="點擊查看結果",font=('Arial',20),bg="orange", fg="red",command=show)
button3.grid(row=6,column=1)
#插入圖片
photo=PhotoImage(file="pa.gif")
label=Label(image=photo)
label.grid(row=0,column=2,rowspan=2,columnspan=2,sticky=W+E+N+S, padx=5, pady=5)
#合併兩行,兩列,居中,四周外延5個長度
#主事件循環
mainloop()
t1 = threading.Thread(target=GUI)
t1.start()
t2 = threading.Thread(target=test2.main)
文件三 提示成功窗口
做的一個小彈窗 主函數巡行完後彈出 提示爬取成功
from tkinter import *
import tkinter.messagebox
import threading
def tanchu():
tk=Tk() #實例化
tk.geometry('300x200')
Label(tk,text = '爬取完成',font=('Arial',20)).grid(row = 0,sticky=E)
tk.mainloop()
def tanchu2():
t1 = threading.Thread(target=tanchu) #觸發彈出
t1.start()
除此之外還有兩個圖片文件,用於美化界面
運行結果
四、總結
學會了如何利用socket來模擬瀏覽器發送請求與接收回復的方式,有了編寫爬蟲爬取數據的基本思路,瞭解了現在比較主流的反爬蟲機制及應對手法,在對抗反爬蟲技術的技術方面還需要提升。
五、關於反爬蟲與繞過反爬蟲的小補充
現在主流的反爬蟲主要是:
1.請求頭檢查,比如cookies,user-agent,refer,甚至Accept-Language等等,這也是最基本的反爬機制。2.訪問頻次檢查,如果一個ip在短時間內訪問次服務器次數過於頻繁,且cookies相同,則會被判定爲機器人,你可能會被要求登錄後再訪問服務器或者輸入驗證碼,甚至直接封禁你的ip。3.驗證碼驗證,爬蟲無法輕易繞過這一關。4.有些網頁的元素是動態生成的,只有在js加載完成後纔會顯示。比如很多實用了Ajax的網站,都會有動態生成的元素。直接爬取頁面將無法獲取想要的元素。5.表單安全措施,如服務器生成的隨機變量加入字段檢測,提交表單蜜罐等。所謂蜜罐簡單來說就是騙機器人的一些表單,比如一下三種:
<a href='...' style='display:none;'> #看不見
<input type='hiden' ...> #隱藏
<input style='position:absolute; right:50000px;overflow-x:hidden;' ...> #右移50000像素,且隱藏滾動條,應該出電腦屏幕了
看不到如果你有關於這些元素操作,就表明你是直接通過頁面源碼觀察網頁的,也可以說明你是機器人,至少不是正常人。
這裏引用這篇文章:https://blog.csdn.net/Fourierrr_/article/details/79838128
scrapy實戰之與豆瓣反爬抗爭
這次我遇見的就是第二個,豆瓣管理員還命名個cat
抓個包給大家瞅一眼吧
關於這個繞過呢,上面這篇文章沒有寫,我查了一下,一般都是用selenium進行繞過的,這個模塊可以更加真實的模擬瀏覽器的操作,並且可以識別出這些蜜罐並繞過,關於比較高級的繞過,這篇文章寫的挺好的
https://www.cnblogs.com/changkuk/p/10012547.html 常見表單反爬蟲安全措施解密
就先寫到這裏吧,這次的爬蟲不算成功,但是也算一次不錯的嘗試了,終於忙完學校的事情了,可以全心投入到安全學習上了