写在前面:
用Requests库进行爬取一般是爬取网页,小规模,数据量小的,对爬取速度没有什么要求。
如果要爬取一个网站的所有数据,那么就要用scrapy库,属于中规模。
爬取全网数据(也即搜索引擎的开发)比如说Google、百度等,也是利用爬虫,不过这个需要企业定制开发。
爬虫的功能也即爬取数据,主要编写内容是
1.获取网页信息(用requests或者python自带的urllib2),我们这里讲的是requests库。
2.对网页的数据进行分析,获取需要的数据并存入合适的数据结构,也可以选择继续爬取其中含有的url,使用bs4库中的BeautifulSoup类,其中使用正则表达式(re库)会带来一些方便
3.进行处理,防止重复或循环抓取url
4.用合适的方式存储/打印
库的安装:win+R 进入cmd命令行
Requests库: 在命令行中输入pip install requests
bs4库:pip install beautifulsoup4
当然,如果装的是anaconda,这两个库都已集成在里面了。之前写过关于anaconda的安装,还没有安装python的可以参考:anaconda+pycharm环境搭建
Requests库
requests库是否安装完成可用如下语句检验
import requests
# r = requests.get("www.baidu.com") # www.baidu.com这样写是不对的 会访问到本地的地址
r = requests.get("http://www.baidu.com")
print(r.status_code)
若显示的是200 则表示没有问题
主要方法:(与HTTP的请求方法是对应的)
方法 | 说明 |
---|---|
requests.request(请求方式, url, **kwargs) | 可以用该方法实现下面六个方法中的任意一个的功能 |
requests.get(url,params = None,**kwargs) | 访问并获取网页内容,返回一个response对象 |
requests.head(url,**kwargs) | 类似于requests.get(),但不返回具体内容 |
requests.post(url,data = None,json = None,**kwargs) | 向网页提交post请求,如果内容为键值对,默认会把内容放到表单里,如果是字符串则放到data里 |
requests.patch(url,data = None,**kwargs) | 向网页提交局部修改请求 |
requests.put(url,data = None,**kwargs) | 类似于patch,但修改不是局部的,而是覆盖原来的内容,因此一定要重写所有内容 |
requests.delete(url,**kwargs) | 提交删除请求 |
对一些参数的说明(**kwargs有13个参数):
1.params——对url进行修改
例:百度的关键词接口:http://www.baidu.com/s?wd=keyword
这样写可以得到搜索结果:
kv = {"wd":"yogima的博客"}
r = requests.get(http://www.baidu.com/s, params=kv)
2.data
3.json
4.headers(http定制头)
有些网页会有反爬虫机制,会对headers进行检查,如果发现是爬虫则不允许访问,因此要让爬虫假装是个浏览器。例如:
hd = {'user-agent':'Mozilla/5.0'}
r = requests.request('POST','http://www.baidu.com',headers = hd)
5.cookies
6.auth
7.files——传输文件
fs = {'file':open('data.xls','rb')}
r = requests.request('POST','http://www,baidu.com',files = fs)
8.timeout——设定超时时间,单位为秒 主要和get方法一起使用 超过一定时间没有get的话就会返回一个超时异常
9.proxies——设定访问代理服务器,可以增加登录认证
为了防止对爬虫的逆追踪,这样在访问网址时使用的IP地址就是代理服务器的IP地址:
pxs = {'http':'http://user:pass@10.10.10.1:1234'
'https':'https://10.10.10.1:4321'}
r = requests.request('GET','http://www.baidu.com'.proxies = pxs)
10.allow_redirects——True/False,默认为True,重定向开关,表示是否允许url重定向。
11.stream——True/False,默认为True,指是否对获取的内容立即下载。
12.verify——True/False,默认为True,认证SSL证书。
13.cert——保存本地SSL证书路径
get方法构造的是一个向服务器请求资源的Request对象,执行get方法后返回的是一个包含服务器资源的Response对象。
response对象的属性
属性 | 说明 |
---|---|
r.status_code | http请求的返回状态,200表示成功,其他表示失败 |
r.text | url对应的页面内容 |
r.encoding | 从HTTP header中获取的内容编码方式 |
r.apparent | 从内容中分析得出的编码方式 |
r.content | url对应内容的二进制形式 |
r.encoding会获取charset后的编码,但是当header中不存在charset时,会认为编码为ISO-8859-1(不能解析中文)
因此如果发现r.text很多都是乱码 则可以把r.aparrent_encoding的值赋给encoding
通用代码框架:
import requests
get_html_text(url): # python中的函数和函数中的变量名一般小写,中间用_连接
try:
r = requests.get(url,timeout=30)
r.raise_for_status() # r.status_code不为200,则引发HTTPError异常
r.encoding = r.apparent_encoding
return r.text
except:
print("some error exists!")
return ""
获取一张已知地址的网络图片:
import requests
import os
url = "http://desk.fd.zol-img.com.cn/t_s960x600c5/g5/M00/01/0E/ChMkJ1bKwfKIe14XAAHVVT0GIMgAALGgQNut2kAAdVt06.jpeg" # 任意图片的url
root = "H://pictures//"
path = root + url.split('/')[-1] #root+最后一个反斜杠后面的名称 -多少表示最后多少字节
try:
if not os.path.exists(root):
os.mkdir(root)
if not os.path.exists(path):
r = requests.get(url)
with open(path,'wb') as f:
f.write(r.content)
f.close()
print("文件保存成功")
else:
print("文件已存在")
except:
print("爬取失败")
用类似的方式也可以获取视频 flash等(怎么改?)
requests库的异常:
对于网络爬虫的限制
来源审查:检查来访HTTP协议头的User-Agent域,只响应浏览器或友好爬虫的访问
发布公告:Robots协议 要求爬虫遵守 当然这个并无法强制限制爬虫 只能通过自觉遵守
robots协议:告知爬虫哪些页面是可爬取的 哪些是不可爬取的 形式:在网站根目录下的robot.txt文件
比如说百度的robots协议可以通过www,baidu.com/robots.txt看到
User-agent后面指的是哪个爬虫 *代表所有
disallow后面带的网址就是不允许爬取以XX开头的 /带表根目录
讲道理所有网络爬虫都需要遵守robots协议 但如果访问量很少(类人行为)是可以不遵守的 但注意不能进行商用途哦
总结:人机交互方式(比如说查询啊之类需要输入文本框内容的的),内容在正式向后台提交的时候都是以链接的形式提交的,只要知道向后台提交的链接形式,就可以用代码自动提交(可能需要挖掘一下API),网络上的任何一个内容都有一个url与之对应,内容的获取与查询都是通过构造url实现的。
beautifulsoup4库
用法:
from bs4 import BeautifulSoup #bs4是beautifulsoup4库的简写 BeautifulSoup是一个类
demo = r.text()
soup = BeautifulSoup('demo','html.parser')
html文档 标签树 BeautifulSoup类三者等价 一个BeautifulSoup类对应一个html/xml文档的全部内容
BeautifulSoup类的基本元素:
基本元素 | 说明 |
---|---|
Tag | 标签,用<>和标明开头和结尾 |
Name | 标签的名字,比如 <a href = "xxx"> ,a就是标签名。格式:<tag>.name |
Attributes | 标签的属性,格式:<tag>.attrs |
NavigableString | 标签内非属性字符串,<a><a/> 中间部分的字符串。格式:<tag>.string |
Comment | 标签内字符串的注释部分,为Comment类型 |
在html页面中以<!xxxxx>
的形式来写注释 例如 <a><!--welcome></a>
,则soup.a.string的内容是welcome,和不是注释时打印出来并没有区别。但是在这时 soup.a.string的类型是bs4.element.Comment 如果不是注释 而是welcome则soup.a.string的类型是bs4.element.NavigableString(可以用type(soup.a.string)判断类型来区分)
获取Tag:接上文代码的话就是soup.a
,可以显示出该标签内容(两对尖括号及尖括号内及之间的内容),如果有多个同名标签,则显示第一个。
那么比如说要获取属性,就是soup.a.attrs
若想获得某个特定属性的值,如class属性:则soup.a.attrs['class']
若无此属性,则返回一个空的字典树
html其实可以看成一棵树(标签树),html是树根 左右孩子是head和body (其实就是一棵横着的树)
注意在标签树中 标签之间的NevigableString也构成了树的节点
标签树的上行遍历
html标签的父亲是其本身 soup自身也是一个标签 其父标签为空
.parent->节点的父标签
.parents->节点先辈标签的迭代类型用于循环遍历先辈节点
得到某标签的父标签名(比如a标签的父标签名),则可soup.a.parent.name
上行遍历:
soup = BeautifulSoup(demo,"html.parser") # 注意这边的demo就是前面requests库通用代码框架中return出来的r.text
for parent in soup.a.parents:
if parent is None:
print(parent)
else:
print(parent.name)
下行遍历:
属性 | 说明 |
---|---|
.contents | 子节点的列表,将标签的所有儿子节点存入列表 |
.children | 子节点的迭代类型,用于循环遍历儿子节点 |
.descendants | 子孙节点的迭代类型,包含所有的子孙节点 |
因此
len(soup.body.contents)
可以获得子节点个数
soup.body.contents[1]
之类的 ,可以得到某下标的孩子内容(列表类型)
children和descendants是迭代类型,只能用在for语句中
例如遍历儿子节点:for child in soup.body.children:
平行遍历:
平行遍历发生在同一个父节点下的各节点间
属性 | 说明 |
---|---|
.next_sibling | 返回下一个平行节点 |
.previous_sibling | 返回上一个平行节点 |
.next_siblings | 迭代类型,返回后续所有平行节点 |
.previous_siblings | 迭代类型,返回前续所有平行节点 |
遍历后续节点
for sibling in soup.a.next_siblings:
print(sibling)
遍历前续节点:
for sibling in soup.a.previous_siblings:
print(sibling)
bs4库的find_all函数
find_all函数可以返回一个列表类型,存储查找的结果
soup.find_all(name,attrs,recursive,string,**kwargs)
1.name - 搜索标签树中出现的所有该标签 如果希望同时查找多个标签 则soup.find_all(['a','b'])
2.attrs - 标签属性值的检索字符串 比如说检索a标签属性中含course的 soup.find_all('a','course')
也可以找特定属性值的 即 soup.find_all('a',class = 'course')
3.recursive - 是否对子孙全部检索,默认True,设为False(recursive = False)则只查找儿子节点层面
4.string - <>…</>
中字符串区域的检索
由于find_all()函数过于常用,soup(..)
等价于soup.find_all(..)
find_all的拓展方法(这些拓展方法的参数和find_all()函数的参数均相同):
方法 | 说明 |
---|---|
<>.find() | 搜索且只返回一个结果,字符串类型 |
<>.find_parents() | 在先辈节点中搜索,返回列表类型 |
<>.find_parent() | 在先辈节点中返回一个结果,字符串类型 |
<>.find_next_siblings() | 在后续平行节点中搜索,返回列表类型 |
<>.find_next_sibling() | 在后续平行节点中搜索,返回一个结果,字符串类型 |
<>.find_previous_siblings() | 在前续平行节点中搜索,返回列表类型 |
<>.find_previous_sibling() | 在前续平行节点中搜索,返回一个结果,字符串类型 |
其他:
1.soup.prettify可以为每个标签增加换行符
2.bs4库将任何读入的文件或字符串的编码类型都转换成utf-8编码
3.利用isinstance(tr,bs4.element.Tag) 可以过滤掉tr标签中不属于标签的内容
4.新建一个列表并向里面添加元素:
ulist = []
for tds in soup:
ulist.append([tds[0].string,tds[1].string])
举这个例子是为了说明列表可以是多维的
5.关于国际公认的信息标记种类 :xml json yaml
xml——是html的拓展 通过标签形式构建所有信息
json——js语言中对面向对象信息的一种表达方式 有类型的键值对构建信息表达 key:value
在json中 无论是key还是value 如果是字符串形式 则需要加双引号
多值用[,]
,例如"name":["yogi","ma"]
键值对可以嵌套使用name":{"newname":"yogi","oldname":"grey"}
YAML——无类型的键值对 key:value 无论健还是值 都没双引号 通过缩进的形式表达所属关系
name:
newname:yogi
oldname:grey
-
表达并列关系(多值
name:
-yogi
-grey
|
表示整块数据#
表示注释
例:
introduction:|
xxxx一大段话
XML用于Internet上的信息交互与传递,JSON移动应用云端和节点的信息传递(接口),无注释。 YAML 用于各类系统的配置文件,有注释。
正则表达式
正则表达式用于简洁地表达字符串
正则表达式的常用操作符:
操作符 | 说明 | 实例 |
---|---|---|
[] | 字符集,对单个字符给出取值范围 | [abc]代表a/b/c,[a-z]表示a到z的单个字符 |
[^] | 非字符集,对单个字符给出排除范围 | [^abc]表示非a非b非c的任意单个字符 |
* | 前一个字符0次或无限次扩展 | abc*表示ab、abc、abcc、abccc等 |
+ | 前一个字符1次或无限次扩展 | abc+表示abc、abcc、abccc等 |
? | 前一个字符0次或1次扩展 | abc?表示ab、abc |
| | 左右表达式任意一个 | abc|def表示abc、def |
{m} | 扩展前一个字符m次 | ab{2}c表示abbc |
{m,n} | 扩展前一个字符m至n次 | ab{1,2}c表示abc、abbc |
^ | 匹配字符串开头 | ^abc表示abc且在一个字符串的开头位置 |
$ | 匹配字符串结尾 | abc$表示abc且在字符串的结尾 |
() | 分组标记,内部只能用 | | (abc|def)表示abc、def |
\d | 数字,等价于[0-9] | |
\w | 等价于[A-Za-z0-9] |
示例:
‘PN’
‘PYN’
‘PYTN’
‘PYTHN’
‘PYTHON’
写成正则表达式的话可以这样:
P(Y|YT|YTH|YTHO)?N
PY开头 后续存在不多于十个字符 但字符不能是’P’或者’Y’
表达式:PY[^PY]{0,10}
表达式:PY[TH]ON
‘PYTON’/’PYHON’
表达式:PY{:3}N等价于 PY{0,3}N
^[A-Za-z]+$
表示由26个字母组成的字符串
^-?\d+$
表示整数形式的字符串 -?是因为可能是负数
^[0-9]*[1-9][0-9]*$
正整数形式的字符串 这么麻烦是因为要非0
[1-9]\d{5}
中国境内邮政编码
[\u4e00-\u9fa5]
匹配中文字符(判断一个字符串是否属于中文)
用正则表达式表达0-255
精确写法:0-99:[1-9]?\d 100-199:1\d{2} 200-249:2[0-4]\d 250-255:25[0,5]
再用小括号和|分开即可
匹配IP地址的正则表达式写法(IP地址由四段组成 每段0-255)
((1-9]?\d|1\d{2}|2[0-4]\d|25[0,5]).){3}(1-9]?\d|1\d{2}|2[0-4]\d|25[0,5])
python中的正则表达式库re:
re库是python的标准库,不需要额外安装,直接import re即可
正则表达式的使用需要有一个编译的过程
即将符合正则表达式语法的字符串转换为正则表
regex = 'P(Y|YT|YTH|YTHO)?N'
p = re.compile(regex)
编译前的正则表达式只是一个符合语法的字符串,编译之后才可以表示那一类字符串
re库使用raw string类型(原生字符串,不包含转义字符的字符串)表示正则表达式,表示为r’text’ ,如r'[1-9]\d{5}'
当正则表达式包含转义字符的时候,最好还是用raw string类型
re库的主要功能函数
函数 | 说明 |
---|---|
re.search(pattern,string,flags = 0) | 在一个字符串中搜索匹配正则表达式的第一个位置,返回match对象 |
re.match(pattern,string,flags = 0) | 从一个字符串的开始位置起匹配正则表达式,返回match对象 |
re.findall(pattern,string,flags = 0) | 搜索字符串,以列表类型返回全部能匹配的子串 |
re.finditer(pattern,string,flags = 0) | 搜索字符串,返回一个匹配结果的迭代类型,每个迭代元素是match对象 |
re.sub(pattern,repl,string,count = 0,flags = 0) | 在一个字符串中替换所有匹配正则表达式的子串,返回替换后的字符串 |
re.split() | 将一个字符串按照正则表达式匹配结果进行分割,返回列表类型 |
关于参数的解释:
pattern:正则表达式的字符串或原生字符串
string:待匹配字符串
flags:正则表达式使用时的控制标记
repl :要换进去的字符串
count :最大替换次数
re库的功能函数中search和match的不同在于: match只从头开始匹配, 而search是找第一个匹配位置的对象
如果要对匹配结果进行使用,一定要加if语句确定match不为空,如果match为空则会出现异常
findall函数还可以有另一个参数:
re.findall(pattern,string,maxsplit = 0,flags = 0)
maxsplit指最大分割数,剩余部分作为最后一个元素输出
分割完之后在列表中剩下的元素是不含pattern部分的,会把它去掉
maxsplit默认时该是啥就是啥。指定值时分割出来的部分要么小于最大分割数,要么是值+1 。比如指定最大分割数为1, 那么第一个匹配字符串检索之后前面的字符串是列表的第一个元素, 后面就不再检索 成为列表的第二个元素(包含可能有的匹配)
finditer的用法:
for m in re.finditer(xxx):
rst = re.search(r'[1-9]\d{5}','BIT 100081')
这样的用法是函数式用法 一次性操作
可以使用面向对象的用法,编译后可多次操作
pat = re.compile(r'[1-9]\d{5}')
rst = pat.search('BIT 100081')
六个函数都类似:
regex = re.compile(pattern,flags = 0)
search函数控制标记的常用标记:
1.re.I (re.IGNORECASE)——忽略正则表达式的大小写
2.re.M(re.MULTILINE)——正则表达式中的^操作符能够将给定字符串的每行当作匹配开始
3.re.S(re.DOTALL)——正则表达式中的.操作符能够匹配所有的字符,默认匹配除换行符外的所有字符
re库的match对象
方法 | 说明 |
---|---|
.group(0) | 获得匹配后的字符串 |
.start() | 匹配字符串在原始字符串的开始位置 |
.end() | 匹配字符串在原始字符串的结束位置 |
.span() | 返回(.start(),.end()) |
属性 | 说明 |
---|---|
.string | 待匹配的文本 |
.re | 匹配时使用的pattern对象(正则表达式) |
.pos | 正则表达式搜索文本的开始位置 |
.endpos | 正则表达式搜索文本的结束位置 |
贪婪匹配和最小匹配
match = re.seach(r'PY.*N','PYANBNCNDN')
正则表达式说明要匹配的是以PY开头 N结尾 中间可以有任意字符的字符串 但现在出现了多种匹配结果
re库默认采用贪婪匹配 即输出匹配最长的子串
要最小匹配就要自己在正则表达式中加? PY.*?N 此外 +? ?? (m,n)?都表示最小匹配
最后的附文:
本来看爬虫是想写一个c博一键备份出来的,然而刚刚开了头就要搁置了,希望之后有空的话能回来把它写完。
只大概了解了一下python的语法,也是一年半以前的事情了,那个时候还没开始写博客,所以刚开始看的时候真的什么也不记得了,如果有什么错误的地方欢迎指正~
刚刚开了个头的代码如下:
import requests
from bs4 import BeautifulSoup
import re
def get_html_text(url):
try:
r = requests.get(url, timeout=20)
r.raise_for_status()
r.encoding = r.apparent_encoding
return r.text
except:
print("some error exist")
return ""
def get_list_page(alist, html, url):
start_url = re.compile(url)
# alist = re.findall(start_url, html)
soup = BeautifulSoup(html, "html.parser")
alist = soup.find_all('a', href=start_url)
print(alist)
def parse_list_page(username, article_list, list_page):
url = re.compile(r'http://blog.youkuaiyun.com/' + username + r'/article/list')
alist = re.findall(url, list_page)
for i in range(len(alist)):
article_list.append(alist[i])
# def parse_essay():
# def writeToFile():
def main():
username = 'yogima' # python中字符串用双引号或是单引号都是可以的 但是要注意如果里面和外面符号相同需要转义
blog_url = 'http://blog.youkuaiyun.com/' + username + '/article/list' # python里 函数中的变量应该小写 函数名也应该小写 可以用_分隔 注释前至少需要两个空格 #后也需要一个空格
first_page = get_html_text(blog_url)
article_list_list = [] # 存储每一页目录的url
get_list_page(article_list_list, first_page, blog_url)
list_number = len(article_list_list)
print(list_number)
article_list = []
for i in range(list_number):
list_page = get_html_text(article_list_list[i]) # 每一页目录的html
parse_list_page(username, article_list, list_page)
print(article_list)
# for j in range(len(article_list)):
# essay_html = get_html_text(article_list[j])
main()