被豆瓣反爬虫制裁实记录——温柔小薛的第一个小爬虫

    技术2025-04-12  13

    写在前面

    最近的课设是用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 常见表单反爬虫安全措施解密

    就先写到这里吧,这次的爬虫不算成功,但是也算一次不错的尝试了,终于忙完学校的事情了,可以全心投入到安全学习上了

    Processed: 0.018, SQL: 9