您的位置:首页 > 编程语言 > Python开发

用python编写第一个简易爬虫(Requests库、BeatifulSoup、正则表达式入门)

2018-02-20 16:08 796 查看
写在前面:

用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_codehttp请求的返回状态,200表示成功,其他表示失败
r.texturl对应的页面内容
r.encoding从HTTP header中获取的内容编码方式
r.apparent从内容中分析得出的编码方式
r.contenturl对应内容的二进制形式
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}

表达式:PYON

‘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.csdn.net/' + 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.csdn.net/' + 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()
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息