爬⾍概述
什么是爬⾍?
我们总是希望能够保存互联网上的⼀些重要的数据信息为⼰所⽤,爬⾍就是通过编写程序
来爬取互联⽹上的优秀资源(图⽚, ⾳频, 视频, 数据)
爬虫用于爬取数据, 又称之为数据采集程序
第一个爬虫
使用python自带的urllib模块
from urllib.request import urlopen
resp=urlopen("http://www.baidu.com") #打开百度
#print(resp.read().decode('utf8')) #打印抓取到的内容
with open("baidu.html",mode='w',encoding='utf8') as f:
f.write(resp.read().decode('utf8'))#保存在文件中
urlopen(url, data=None)可以直接发起url的请求, 如果data不为空时,则默认是POST请求,反之为GET请求。
resp是http.client.HTTPResponse类对象
http协议
请求
请求⾏ -> 请求⽅式(get/post) 请求url地址 协议
请求头 -> 放⼀些服务器要使⽤的附加信息
请求体 -> ⼀般放⼀些请求参数
响应
状态⾏ -> 协议 状态码
响应头 -> 放⼀些客户端要使⽤的⼀些附加信息
响应体 -> 服务器返回的真正客户端要⽤的内容(HTML,json)等
请求头中最常⻅的⼀些重要内容(爬⾍需要):
- User-Agent : 请求载体的身份标识(⽤啥发送的请求)
- Referer: 防盗链(这次请求是从哪个⻚⾯来的? 反爬会⽤到)
- cookie: 本地字符串数据信息(⽤户登录信息, 反爬的token)
响应头中⼀些重要的内容:
- cookie: 本地字符串数据信息(⽤户登录信息, 反爬的token)
- 各种神奇的莫名其妙的字符串(这个需要经验了, ⼀般都是token
字样, 防⽌各种攻击和反爬)
request模块
我们使⽤urllib来抓取⻚⾯源代码. 这个是python内置的⼀个模块. 但是, 它并不是我们常⽤的爬⾍⼯具. 常⽤的抓取⻚⾯的模块通常使⽤⼀个第三⽅模块requests. 这个模块的优势就是⽐urllib还要简单, 并且处理各种请求都⽐较⽅便
安装模块
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple requests
核心的函数
-
requests.request() 所有请求方法的基本方法
以下是request()方法的参数说明
-
method: str 指定请求方法, GET, POST, PUT, DELETE
-
url: str 请求的资源接口(API),在RESTful规范中即是URI(统一资源标签识符)
-
params: dict , 用于GET请求的查询参数(Query String params);
-
data: dict , 用于POST/PUT/DELETE 请求的表单参数(Form Data)
-
json: dict 用于上传json数据的参数, 封装到body(请求体)中。请求头的Content-Type默认设置为
application/json
-
files: dict, 结构 {‘name’: file-like-object | tuple}, 如果是tuple, 则有三种情况:
- (‘filename’, file-like-object)
- (‘filename’, file-like-object, content_type)
- (‘filename’, file-like-object, content_type, custom-headers)
指定files用于上传文件, 一般使用post请求,默认请求头的
Content-Type
为multipart/form-data
类型。 -
headers/cookies : dict
-
proxies: dict , 设置代理
-
auth: tuple , 用于授权的用户名和口令, 形式(‘username’, ‘pwd’)
-
-
requests.get() 发起GET请求, 查询数据
可用参数:
- url
- params
- json
- headers/cookies/auth
-
requests.post() 发起POST请求, 上传/添加数据
可用参数:
- url
- data/files
- json
- headers/cookies/auth
-
requests.put() 发起PUT请求, 修改或更新数据
-
requests.patch() HTTP幂等性的问题,可能会出现重复处理, 不建议使用。用于更新数据
-
requests.delete() 发起DELETE请求,删除数据
2.3 requests.Respose
以上的请求方法返回的对象类型是Response, 对象常用的属性如下:
- status_code 响应状态码
- url 请求的url
- headers : dict 响应的头, 相对于urllib的响应对象的getheaders(),但不包含cookie。
- cookies: 可迭代的对象,元素是Cookie类对象(name, value, path)
- text : 响应的文本信息
- content: 响应的字节数据
- encoding: 响应数据的编码字符集, 如utf-8, gbk, gb2312
- json(): 如果响应数据类型为
application/json
,则将响应的数据进行反序化成python的list或dict对象。- 扩展-javascript的序列化和反序列化
- JSON.stringify(obj) 序列化
- JSON.parse(text) 反序列化
- 扩展-javascript的序列化和反序列化
get请求
import requests
keywords=input('请输入你要搜索的内容:')
headers={
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"
} #模拟浏览器的请求,处理反爬
response=requests.get(f"https://www.sogou.com/web?query={keywords}",headers=headers)#发送get请求
print(response.text) #结果文本
response.close()
get请求参数是拼接在?后面的
如果?后面的数据比较多可以通过params
import requests
url = 'https://movie.douban.com/j/chart/top_list'
params = {
'type': '24',
'interval_id': '100:90',
'action':'',
'start': '0',#从库中的第⼏部电影去取
'limit': '20',#⼀次取出的个数
}
headers={
"User-Agent":"Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"
}
response=requests.get(url=url,params=params,headers=headers)
print(response.url)
print(response.json())
response.close()
post请求
import requests
url="https://fanyi.baidu.com/sug"
search=input("请输入你要翻译的英文单词")
data={
"kw":search
}
#发送post请求
response=requests.post(url,data=data)
print(response.json()) #将服务器返回的内容直接处理json格式
response.close()
post是通过data传送参数
数据解析
- re解析
- bs4解析
- xpath解析
这三种⽅式可以混合进⾏使⽤
re解析
正则表达式
这里我只是简单的概述一下详情可以去看我的另一篇博客正则表达式博客链接
Regular Expression, 正则表达式, ⼀种使⽤表达式的⽅式对字符串进⾏匹配的语法规则
我们抓取到的⽹⻚源代码本质上就是⼀个超⻓的字符串, 想从⾥⾯提取内容.⽤正则再合适不过了.
正则的优点: 速度快, 效率⾼, 准确性⾼
元字符: 具有固定含义的特殊符号 常⽤元字符:
. 匹配除换⾏符以外的任意字符
\w 匹配字⺟或数字或下划线
\s 匹配任意的空⽩符
\d 匹配数字
\n 匹配⼀个换⾏符
\t 匹配⼀个制表符
^ 匹配字符串的开始
$ 匹配字符串的结尾
\W 匹配⾮字⺟或数字或下划线
\D 匹配⾮数字
\S 匹配⾮空⽩符
a|b 匹配字符a或字符b
() 匹配括号内的表达式,也表示⼀个组
[...] 匹配字符组中的字符
[^...] 匹配除了字符组中字符的所有字符
量词: 控制前⾯的元字符出现的次数
* 重复零次或更多次
+ 重复⼀次或更多次
? 重复零次或⼀次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次
贪婪匹配和惰性匹配
.* 贪婪匹配
.*? 惰性匹配
re模块
详情也在另一篇博客里正则表达式链接
match方法(只匹配字符串开头)
search方法(扫描整个字符串,找到第一个匹配)
findall方法(扫描整个字符串,找到所有的匹配)
finditer方法(扫描整个字符串,找到所有的匹配,并返回一个可迭代对象)
fullmatch方法: (完整匹配,字符串需要完全满足正则规则才会有结果,否则就是None)
match
只能从字符串的开头进⾏匹配
import re
res1=re.match(r'd','dyk')
print(res1.group(0)) #d 匹配到的元素
print(res1.span()) #(0, 1) 匹配到的元素所在位置 左闭右开
res2=re.match(r'y','dyk')
print(res2) #None 必须是字符串的起始位置
search
会进⾏匹配. 但是如果匹配到了第⼀个结果. 就会返回这个结果. 如果匹配不上search返回的则是None
import re
res1=re.search(r'dy','dyk666')
print(res1.group()) #dy
print(res1.group(0)) #dy
print(res1.span()) #(0, 2)
res2=re.search(r'6','dyk666')
print(res2.group(0)) #6
print(res2.span()) #(3, 4) 第一次出现6的位置
findall
查找所有. 返回list
import re
#\d表示数字 +表示个数一个或一个以上
res1=re.findall(r'\d+','dyk666dyk123')
print(res1) #['666', '123']
finditer
和findall差不多. 只不过这时返回的是迭代器
import re
#\d表示数字 +表示一个或一个以上
res=re.finditer(r'\d+','dyk666dyk123') # 得到的结果是一个可迭代对象
for i in res: # 遍历 res 取出里面的每一项匹配
print(i)
# <re.Match object; span=(3, 6), match='666'>
# <re.Match object; span=(9, 12), match='123'>
compile()
可以将⼀个⻓⻓的正则进⾏预加载. ⽅便后⾯的使⽤
obj = re.compile(r'\d{3}') # 将正则表达式编译成为
⼀个 正则表达式对象, 规则要匹配的是3个数字
ret = obj.search('abc123eeee') # 正则表达式对象调
⽤search, 参数为待匹配的字符串
print(ret.group()) # 结果: 123
案例爬取⾖瓣TOP250电影信息并保存为csv文件
import requests
import re
import csv
url="https://movie.douban.com/top250"
headers={
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36"
}
response=requests.get(url=url,headers=headers)
# 解析数据
obj=re.compile(r'<li>.*?<span class="title">(?P<name>.*?)</span>.*?<div class="bd">.*?<p class="">'
r'.*?<br>(?P<time>.*?) .*?<span class="rating_num" property="v:average">(?P<score>.*?)</span>'
r'.*?<span>(?P<num>.*?)人评价</span>',re.S)
#开始匹配
iters=obj.finditer(response.text)
f=open('data.csv',mode='w',encoding='utf8') #打开写入的文件
csvwriter=csv.writer(f) #创建csv文件写入工具,向指定文件写入
for it in iters:
# print(it.group('name'))
# print(it.group('time').strip())
# print(it.group('score'))
# print(it.group('num'))
dic=it.groupdict() #转化为字典形式
dic['time']=dic['time'].strip() #去除时间的空格
csvwriter.writerow(dic.values()) #写入数据
f.close()
response.close()
多重请求
import re
import requests
url="xxxx"
resp=requests.get(url,verify=False) #verify=False去掉安全验证
resp.encoding='gb2312' #指定字符集
obj=re.compile(r'2020必看热片.*?<ul>(?P<ul>.*?)</ul>',re.S)
obj1=re.compile(r"<a href='(?P<href>.*?)'",re.S)
result=obj.finditer(resp.text)
childlist=[]
for iter in result:
ul=(iter.group('ul'))
#子页面链接
hrefs=obj1.finditer(ul)
for href in hrefs:
#拼接子页面的url地址:域名+子页面地址
print(href.group('href'))
child_url=url+href.group('href').strip('/')
print(child_url)
childlist.append(child_url) #把子页面链接保存起来
for href in childlist:
child_resp=requests.get(href,verify=False)
child_resp.encoding='gb2312'
print(child_resp.text)
resp.close()
bs4解析
需要了解html知识这里也可以去看我的另一篇博客html基础知识链接
因为bs4就是通过标签和属性去定位⻚⾯上的内容的.
安装bs4
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple bs4
创建BeautifulSoup对象
直接把页面源码传过去就可以创建BeautifulSoup对象
#把页面源代码交给BeautifulSoup进行处理生成BeautifulSoup对象
bs=BeautifulSoup(resp.text,"html.parser") #指定html解析器,不指定会出现红色警告,但不影响代码的运行
BeautifulSoup对象获取html中的内容主要通过两个⽅法来完成
find()
查找一个
语法:
find(标签, 属性=值)
意思是在⻚⾯中查找 xxx标签, 并且标签的xxx属性必须是xxx值
find_all()
查找所有
语法:
find_all(标签, 属性=值)
意思是在⻚⾯中查找 xxx标签, 并且标签的xxx属性必须是xxx值
这两个方法经常用来查找class或者id,的属性值,但是注意python中class是关键字,会报错的所以可以在class_加个下划线来代表html里的class
table=bs.find("table",class_="hq_table") #class是python的关键字,所以在后面加一个_来代表html里的class属性
或者也可以使用第二种方法来避免
#和上面那种写法是等价的,可以避免class
table=bs.find("table",attrs={"class":"hq_table"})
.text
表示拿到标签内的内容
name=tds[0].text #.text 表示拿到被标签标记的内容
attrs
获得标签里的属性
bs.find("a").attrs
get
直接通过get就可以拿到属性的值
href=a.get('href') #直接通过get就可以拿到属性的值
案例 爬取农产品价格并保存为csv格式
import requests
from bs4 import BeautifulSoup
import csv
url="http://www.xinfadi.com.cn/marketanalysis/0/list/1.shtml"
resp=requests.get(url)
f=open('price.csv',mode='w',encoding='utf-8')
csvwriter=csv.writer(f)
#解析数据
#把页面源代码交给BeautifulSoup进行处理生成BeautifulSoup对象
bs=BeautifulSoup(resp.text,"html.parser") #指定html解析器,不指定会出现红色警告,但不影响代码的运行
#从bs对象中查找数据
#find(标签,属性=值)
#find_all(标签,属性=值)
table=bs.find("table",class_="hq_table") #class是python的关键字,所以在后面加一个_来代表html里的class属性
#和上面那种写法是等价的,可以避免class
#table=bs.find("table",attrs={"class":"hq_table"})
#拿到数据行,不要表头
trs=table.find_all("tr")[1:]
for tr in trs: #每一行
tds=tr.find_all("td") #拿到每行中的td列
name=tds[0].text #.text 表示拿到被标签标记的内容
price_low=tds[1].text
price_avg=tds[2].text
price_high=tds[3].text
specs=tds[4].text
unit=tds[5].text
date=tds[6].text
csvwriter.writerow([name,price_low,price_avg,price_high,specs,unit,date])
f.close()
resp.close()
案例爬取网页图片下载下来保存本地
import requests
from bs4 import BeautifulSoup
import time
url="https://www.umei.cc/bizhitupian/weimeibizhi/"
resp=requests.get(url)
resp.encoding='utf8' #处理乱码
bs=BeautifulSoup(resp.text) #创建BeautifulSoup对象
div=bs.find("div",class_="TypeList")
alist=div.find_all("a")
for a in alist:
href=a.get('href') #直接通过get就可以拿到属性的值
#拿到子页面的源代码
child_resp=requests.get(href)
child_resp.encoding='utf8'
child_resp_text=child_resp.text
#从子页面拿到图片的下载路径
bs2=BeautifulSoup(child_resp_text,"html.parser")
p=bs2.find("p",align="center")
img=p.find("img")
downlopath=(img.get('src'))
#下载图片
img_resp=requests.get(downlopath)
#img_resp.content #拿到的是字节我们写入文件就成了图片
img_name=downlopath.split("/")[-1] #拿到url中的最后一个/以后的内容作为图片的名字
with open("images/"+img_name,mode='wb') as f: #这里要以二进制写
f.write(img_resp.content) #图片内容写入文件
time.sleep(1) #为了防止被封ip所以延时一下
resp.close()
特别注意如果爬取大量的图片或者视频时,把文件标记一下,不然会使pycharm越来越卡,因为pycharm会建立索引,就会变得慢
xpath解析
XPath是⼀⻔在 XML ⽂档中查找信息的语⾔. XPath可⽤来在 XML⽂档中对元素和属性进⾏遍历. ⽽我们熟知的HTML恰巧属于XML的⼀个⼦集. 所以完全可以⽤xpath去查找html中的内容
安装lxml模块
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple lxml
<book>
<id>1</id>
<name>龙族</name>
<price>99</price>
<author>
<nick>江南</nick>
<nick>路明非</nick>
<span>
<nick>诺诺</nick>
</span>
<div>
<nick>楚子航</nick>
</div>
</author>
</book>
book, id, name, price…都被称为节点.
Id, name, price, author被称为book的⼦节点
book被称为id, name, price, author的⽗节点
id, name, price,author被称为同胞节点
使用步骤
将要解析的html内容构造出etree对象.
from lxml import etree
#构建etree对象
tree=etree.XML("内容")
etree对象的构建方法
etree.XML() #通过xml方式构建etree对象
etree.HTML() #通过html方式构建etree对象
etree.parse() #通过源文件方式构建etree对象
使⽤etree对象的xpath()⽅法配合xpath表达式来完成对数据的提取
/表示层级关系 第一个/表示的是根节点,感觉和linux的根目录很像
text()拿文本,标签里的文字内容
// 可以拿后代的不一定要是亲儿子还可以是孙子重孙子
* 任意的节点,通配符
[]表示索引,xpath的顺序是从1开始数的
[@xx=xx] 中括号里面如果接的是属性则通过属性查找
拿到属性值: @属性
如果要从上一次的基础上再次查找,则可以使用相对路径
li.xpath("./a/text()") #./代表着当前目录
from lxml import etree
xml=''' <book>
<id>1</id>
<name>龙族</name>
<price>99</price>
<author>
<nick>江南</nick>
<nick>路明非</nick>
<span>
<nick>诺诺</nick>
</span>
<div>
<nick>楚子航</nick>
</div>
</author>
</book>'''
#构建etree对象
tree=etree.XML(xml)
res=tree.xpath("/book") # /表示层级关系 第一个/表示的是根节点,感觉和linux的根目录很像
res1=tree.xpath("/book/name/text()") # text()拿文本
res2=tree.xpath("/book/author//nick/text()") # // 可以拿后代的不一定要是亲儿子还可以是孙子重孙子
res3=tree.xpath("/book/author/*/nick/text()") # * 任意的节点,通配符
print(res)
print(res1)
print(res2)
from lxml import etree
tree=etree.parse("index.html")
res=tree.xpath("/html/body/ul/li/a/text()")
res1=tree.xpath("/html/body/ul/li[1]/a/text()") #[]表示索引,xpath的顺序是从1开始数的
res2=tree.xpath("/html/body/ul/li/a[@href='feiji']/text()") #[@xx=xx] 中括号里面如果接的是属性则通过属性查找
ol_li_list=tree.xpath("/html/body/ol/li") #如果有多个,默认的就是查找出多个
for li in ol_li_list:
#从每一个li中提取到文字信息
res3=li.xpath("./a/text()") #在li中继续查找, 相对路径查找
res4=li.xpath("./a/@href") # 拿到属性值: @属性
print(res3)
print(res4)
print(res)
小技巧可以通过谷歌浏览器的f12控制面板选中后,右键点击copy,然后选择里面的copy xpath,然后在根据需求改即可