本篇内容为 python 网络爬虫初级操作,内容主要有以下 3 部分:
- python 关于爬虫的一些基本操作和知识
- 静态网页抓取
- 动态网页抓取
基本操作和知识
通过下面介绍的网络爬虫的流程,我们可以看到包含的知识点有哪些:
- 获取网页——爬取到了整个页面
Rquest, urllib, selenium
多进程多线程抓取、登录抓取、突破IP封禁和服务器抓取 - 解析网页(提取数据)——从页面中找自己需要的数据
Re 正则表达式,BeautifulSoup 和 lxml
解决中文乱码 - 存储数据
存入txt文件和csv文件
存入 MySQL 数据库和 MongoDB 数据库
我们先来看一个简单的爬虫,代码及注释如下:
import requests
from bs4 import BeautifulSoup
link = 'http://www.santostang.com/'
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36'} #用request的headers伪装成浏览器访问
r = requests.get(link, headers=headers) # r是requests的Response回复对象
#print(r.text)
# 获取了博客首页的HTML代码
# 把HTML代码转化为soup对象
soup = BeautifulSoup(r.text, 'lxml') #使用BeautifulSoup解析这段代码
title = soup.find('h1', class_='post-title').a.text.strip()
print(title)
# 从整个网页中提取第一篇文章的标题
with open('title.txt', 'a+') as f:
f.write(title)
f.close()
这样就完成了一次爬虫,下面我们分为静态网页和动态网页两部分来进行具体的学习。
静态网页抓取
响应状态码:
返回200,表示请求成功。
返回4XX,表示客户端错误。
返回5XX,表示服务器错误响应。
一个 HTTPError 对应相应的状态码,HTTP 状态码表示 HTTP 协议所返回的响应的状态。
下面将状态码归结如下:
100:继续 客户端应当继续发送请求。客户端应当继续发送请求的剩余部分,或者如果请求已经完成,忽略这个响应。
101: 转换协议 在发送完这个响应最后的空行后,服务器将会切换到在Upgrade 消息头中定义的那些协议。只有在切换新的协议更有好处的时候才应该采取类似措施。
102:继续处理 由WebDAV(RFC 2518)扩展的状态码,代表处理将被继续执行。
200:请求成功 处理方式:获得响应的内容,进行处理
201:请求完成,结果是创建了新资源。新创建资源的URI可在响应的实体中得到 处理方式:爬虫中不会遇到
202:请求被接受,但处理尚未完成 处理方式:阻塞等待
204:服务器端已经实现了请求,但是没有返回新的信 息。如果客户是用户代理,则无须为此更新自身的文档视图。 处理方式:丢弃
300:该状态码不被HTTP/1.0的应用程序直接使用, 只是作为3XX类型回应的默认解释。存在多个可用的被请求资源。 处理方式:若程序中能够处理,则进行进一步处理,如果程序中不能处理,则丢弃
301:请求到的资源都会分配一个永久的URL,这样就可以在将来通过该URL来访问此资源 处理方式:重定向到分配的URL
302:请求到的资源在一个不同的URL处临时保存 处理方式:重定向到临时的URL
304:请求的资源未更新 处理方式:丢弃
400:非法请求 处理方式:丢弃
401:未授权 处理方式:丢弃
403:禁止 处理方式:丢弃
404:没有找到 处理方式:丢弃
500:服务器内部错误 服务器遇到了一个未曾预料的状况,导致了它无法完成对请求的处理。一般来说,这个问题都会在服务器端的源代码出现错误时出现。
501:服务器无法识别 服务器不支持当前请求所需要的某个功能。当服务器无法识别请求的方法,并且无法支持其对任何资源的请求。
502:错误网关 作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应。
503:服务出错 由于临时的服务器维护或者过载,服务器当前无法处理请求。这个状况是临时的,并且将在一段时间以后恢复。
HTTPError 实例产生后会有一个 code 属性,这就是是服务器发送的相关错误号。
3 开头的代号可以被处理,并且 100-299 范围的号码指示成功,所以你只能看到 400-599 的错误号码。
HTTPError 的父类是 URLError,根据编程经验,父类的异常应当写到子类异常的后面,如果子类捕获不到,那么可以捕获父类的异常。
传递URL参数:
为了请求特定的数据,在URL的查询字符串中加入某些数据。如果是自己构建 URL 数据一般会跟在一个问号后面,以键/值的形式放在URL中。
在Request 中,可以直接把这些参数保存在字典中,用 params 构建至URL中。
举例如下:
key_dict={'key1':'value1','key2':'value2'}
r=requests.get('http://httpbin.org/get',params=key_dict)
运行,得到结果:URL 已正确编码为http://httpbin.org/get?key1=v...
定制请求头:
请求头 Headers 提供了关于请求、响应或其他发送实体的信息。如果没有指定请求头 或请求的请求头和实际网页不一致,就可能无法返回正确的结果。
Requests 并不会基于定制的请求 头Headers 的具体情况改变自己的行为,只是在最后的请求中,所有的请求头信息都会被传递进去。
发送POST请求:
除发送GET请求外,有时发送一些编码为表单形式的数据(如在登录的时候请求就为POST)
——如果用GET请求,密码就会显示在URL中,不安全
——如果要实现POST请求,只需要传递一个字典给 Requests中的 data参数,这个数据字典就会在发出请求时自动编码为表单形式。
超时:
用 Requests 在 timeout 参数设定的秒数结束之后停止等待响应。一般把这个值设置为 20 秒。
相应代码如下:
import requests
#传递URL参数
r = requests.get('http://www.santostang.com/')
#返回一个名为 r 的 response对象
print('文本编码:', r.encoding)
print('响应状态码:', r.status_code)
print('字符串方式的响应体:', r.text)
key_dict = {'key1': 'value1', 'key2': 'value2'}
r = requests.get('http://httpbin.org/get', params=key_dict)
print('URL 已正确编码:', r.url)
print('字符串方式的响应体:\n', r.text)
#定制请求头
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
'Host':'www.santostang.com'
}
r = requests.get('http://www.santostang.com', headers=headers)
print('响应状态码:', r.status_code)
key_dict = {'key1': 'value1', 'key2': 'value2'}
r = requests.post('http://httpbin.org/post', data=key_dict)
print(r.text)
#from变量的值为key_dict输入的值,一个POST请求发送成功
link = 'http://www.santostang.com/'
r = requests.get(link, timeout=0.01)
#异常:时间限制0.01秒内,连接到地址为www.santostang.com的时间已到。
动态网页抓取
在使用 JavaScript 时,很多内容并不会出现在 HTML 源代码中,爬取静态网页的技术可能无法正常使用。用到以下两种技术:
- 通过浏览器审查元素解析真实网页地址
- 使用selenium模拟浏览器的方法
异步更新技术——AJAX(Asynchronous JavaScript And XML):
通过在后台与服务器进行少量的数据交换就可以使网页实现异步更新。即 可以在不重新加载网页的情况下对网页的某部分进行更新。
有时候我们在用requests抓取页面的时候,得到的结果可能和在浏览器中看到的不一样:在浏览器中可以看到正常显示的页面数据,但是使用requests得到的结果并没有。
这是因为requests获取的都是原始的HTML文档,而浏览器中的页面则是经过JavaScript处理数据后生成的结果,这些数据的来源有多种:
- 通过 Ajax 加载的,可能是包含在 HTML 文档中的
数据加载是一种异步加载方式,原始的页面最初不会包含某些数据,原始页面加载完后,会再向服务器请求某个接口获取数据,然后数据才被处理从而呈现到网页上,这其实就是发送了一个 Ajax 请求。如果遇到这样的页面,直接利用 requests 等库来抓取原始页面,是无法获取到有效数据的,这时需要分析网页后台向接口发送的 Ajax 请求,如果可以用 requests 来模拟 Ajax 请求,那么就可以成功抓取了。
- 经过JavaScript和特定算法计算后生成的。
数据传送分为 POST 和 GET 两种方式:
最重要的区别是 GET 方式是直接以链接形式访问,链接中包含了所有的参数,当然如果包含了密码的话是一种不安全的选择,不过你可以直观地看到自己提交了什么内容。
POST则不会在网址上显示所有的参数,不过如果你想直接查看提交了什么就不太方便了。
通过 selenium 模拟浏览器抓取
对于那些很难找到真实地址的 URL 或者很冗长的,使用浏览器渲染引擎:直接用浏览器在显示网页时解析HTML、应用CSS样式、执行JavaScript的语句。
在爬虫过程中,会打开一个浏览器加载该网页,自动操作浏览器浏览各个网页,顺便把数据抓下来。
用 Selenium 控制浏览器加载的内容,从而加快 Selenium 的爬取速度:
- 控制 CSS 的加载
- 控制图片文件的显示
- 控制JavaScript的运行
学习完动态网页加载后,我们来看一个使用 Ajax 加载评论的网页,如何爬取到评论,代码如下:
import json
import requests
def single_page_comment(link):
#link = 'https://api-zero.livere.com/v1/comments/list?callback=jQuery1124030126086598094437_1529672300768&limit=10&repSeq=3871836&requestPath=%2Fv1%2Fcomments%2Flist&consumerSeq=1020&livereSeq=28583&smartloginSeq=5154&_=1529672300770'
headers = {'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36'}
r = requests.get(link, headers=headers)
#它是json数据
#print(r.text)
json_string = r.text
json_string = json_string[json_string.find('{'):-2] #仅仅提取字符串中符合json格式的部分
#使用 json.loads 把字符串格式的响应体数据转化为json数据
json_data = json.loads(json_string)
#利用 json 数据的结构,提取到评论的列表comment_list
comment_list = json_data['results']['parents']
for eachone in comment_list:
message = eachone['content']
print(message)
# 截止测试时共69条评论
for page in range(1,7):
link1 = 'https://api-zero.livere.com/v1/comments/list?callback=jQuery1124030126086598094437_1529672300768&limit=10&offset='
link2 = '&repSeq=3871836&requestPath=%2Fv1%2Fcomments%2Flist&consumerSeq=1020&livereSeq=28583&smartloginSeq=5154&_=1529672300773'
page_str = str(page)
link = link1 + page_str + link2
print(link)
single_page_comment(link)
最后我们看一个豆瓣电影 TOP250 的代码(源代码可至文末下载):
import requests
from bs4 import BeautifulSoup
def get_movies():
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
'Host': 'movie.douban.com'
}
for i in range(0, 10):
link = 'https://movie.douban.com/top250?start=' + str(i * 25)
r = requests.get(link, headers=headers, timeout=10)
print(str(i+1), '页响应状态码:', r.status_code)
print(r.text)
# 得到网页的HTML代码
# get_movies()
movie_list = []
def get_movies():
headers = {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.87 Safari/537.36',
'Host': 'movie.douban.com'
}
for i in range(0, 10):
link = 'https://movie.douban.com/top250?start=' + str(i * 25)
r = requests.get(link, headers=headers, timeout=10)
print(str(i+1), '页响应状态码:', r.status_code)
soup = BeautifulSoup(r.text, 'lxml')
div_list = soup.find_all('div', class_='hd')
for each in div_list:
movie = each.a.span.text.strip()
movie_list.append(movie)
return movie_list
movies = get_movies()
print(movie_list)
得到结果:
['肖申克的救赎', '霸王别姬', '这个杀手不太冷', '阿甘正传', '美丽人生', '千与千寻', '泰坦尼克号', '辛德勒的名单', '盗梦空间', '机器人总动员',
'三傻大闹宝莱坞', '海上钢琴师', '忠犬八公的故事', '放牛班的春天', '大话西游之大圣娶亲', '楚门的世界', '龙猫', '教父', '星际穿越', '熔炉',
'触不可及', '乱世佳人', '无间道', '当幸福来敲门', '天堂电影院', '怦然心动', '十二怒汉', '少年派的奇幻漂流', '蝙蝠侠:黑暗骑士', '鬼子来了',
'搏击俱乐部', '活着', '指环王3:王者无敌', '疯狂动物城', '天空之城', '大话西游之月光宝盒', '罗马假日', '飞屋环游记', '控方证人', '窃听风暴',
'两杆大烟枪', '飞越疯人院', '闻香识女人', '哈尔的移动城堡', '辩护人', '海豚湾', 'V字仇杀队', '死亡诗社', '教父2', '美丽心灵',
'指环王2:双塔奇兵', '指环王1:魔戒再现', '饮食男女', '情书', '摔跤吧!爸爸', '美国往事', '狮子王', '钢琴家', '素媛', '天使爱美丽',
'小鞋子', '七宗罪', '被嫌弃的松子的一生', '致命魔术', '音乐之声', '本杰明·巴顿奇事', '勇敢的心', '西西里的美丽传说', '剪刀手爱德华', '低俗小说',
'看不见的客人', '黑客帝国', '拯救大兵瑞恩', '沉默的羔羊', '让子弹飞', '入殓师', '蝴蝶效应', '春光乍泄', '大闹天宫', '玛丽和马克思',
'末代皇帝', '心灵捕手', '阳光灿烂的日子', '幽灵公主', '第六感', '重庆森林', '布达佩斯大饭店', '禁闭岛', '哈利·波特与魔法石', '狩猎',
'大鱼', '猫鼠游戏', '致命ID', '断背山', '射雕英雄传之东成西就', '甜蜜蜜', '一一', '告白', '阳光姐妹淘', '加勒比海盗',
'穿条纹睡衣的男孩', '上帝之城', '摩登时代', '阿凡达', '爱在黎明破晓前', '消失的爱人', '风之谷', '爱在日落黄昏时', '侧耳倾听', '超脱',
'红辣椒', '恐怖直播', '倩女幽魂', '小森林 夏秋篇', '驯龙高手', '菊次郎的夏天', '喜剧之王', '幸福终点站', '萤火虫之墓', '借东西的小人阿莉埃蒂',
'七武士', '岁月神偷', '杀人回忆', '神偷奶爸', '电锯惊魂', '贫民窟的百万富翁', '喜宴', '真爱至上', '海洋', '谍影重重3',
'东邪西毒', '记忆碎片', '怪兽电力公司', '雨人', '黑天鹅', '疯狂原始人', '卢旺达饭店', '燃情岁月', '英雄本色', '虎口脱险',
'小森林 冬春篇', '7号房的礼物', '哈利·波特与死亡圣器(下)', '傲慢与偏见', '荒蛮故事', '心迷宫', '萤火之森', '恋恋笔记本', '完美的世界', '教父3',
'纵横四海', '花样年华', '海边的曼彻斯特', '玩具总动员3', '猜火车', '魂断蓝桥', '穿越时空的少女', '雨中曲', '唐伯虎点秋香', '时空恋旅人',
'超能陆战队', '请以你的名字呼唤我', '蝙蝠侠:黑暗骑士崛起', '二十二', '我是山姆', '人工智能', '冰川时代', '浪潮', '朗读者', '香水',
'爆裂鼓手', '罗生门', '未麻的部屋', '一次别离', '追随', '血战钢锯岭', '撞车', '可可西里', '恐怖游轮', '战争之王', '被解救的姜戈',
'地球上的星星', '达拉斯买家俱乐部', '梦之安魂曲', '牯岭街少年杀人事件', '房间', '头脑特工队', '魔女宅急便', '谍影重重', '谍影重重2', '惊魂记',
'小萝莉的猴神大叔', '忠犬八公物语', '青蛇', '再次出发之纽约遇见你', '阿飞正传', '模仿游戏', '东京物语', '哪吒闹海', '碧海蓝天', '一个叫欧维的男人决定去死',
'完美陌生人', '你的名字。', '无人知晓', '末路狂花', '秒速5厘米', '源代码', '终结者2:审判日', '黑客帝国3:矩阵革命', '新龙门客栈', '绿里奇迹',
'海盗电台', '这个男人来自地球', '勇闯夺命岛', '城市之光', '卡萨布兰卡', '变脸', '无耻混蛋', '初恋这件小事', '发条橙', 'E.T. 外星人',
'黄金三镖客', '爱在午夜降临前', '荒野生存', '美国丽人', '迁徙的鸟', '英国病人', '无敌破坏王', '步履不停', '疯狂的石头', '燕尾蝶',
'非常嫌疑犯', '勇士', '彗星来的那一夜', '叫我第一名', '国王的演讲', '穆赫兰道', '血钻', '聚焦', '海街日记', '上帝也疯狂',
'枪火', '我爱你', '遗愿清单', '大卫·戈尔的一生', '黑鹰坠落', '荒岛余生', '蓝色大门', '千钧一发', '爱·回家']
本文为崔庆才博客和唐松的《Python网络爬虫从入门到实践》学习记录与总结,具体内容可参考二者。博主在学习过程中的练习代码也已上传至 GitHub。
不足之处,欢迎指正。