如果编程是魔术,那么网页抓取就是巫术,很多人更加倾向的把网页抓取称作为网络爬虫。引自《Python网络爬虫权威指南》
我们都知道通过浏览器可以很方面的浏览妇联网上的海量信息,通过浏览器打开一个链接,其实浏览器干了很多事情,首先通过http/https协议根据url统一资源定位符获取到该资源的数据,这些数据其实就是html文本,浏览器将这些数据解析并显示出来。当然我们可以不使用浏览器,网络爬虫就是这样一个模仿了浏览器的程序,并批量获取你想获取的信息。因此网络爬虫概括起来其实也只有两个步骤:网页数据的抓取、网页数据的解析处理。
1 预备知识
2 数据请求
网页数据请求,其实就是模仿浏览器像web服务器发起访问,主要使用的协议有http/https等,访问方式主要有get/post等方式。python能访问网络资源的有内置库urllib,还有三方库requests。
2.1 urllib
urllib是Python内置的HTTP请求的标准库。虽然可以使用urllib标准库就可以应对网页表单,但是它的API非常差劲,它是为了当时的web创建的,即便是为了完成最简单的任务也需要大量的工作。详情了解请点击
2.2 requests
requests就是一个擅长处理复杂的HTTP请求、cookie、header等内容的第三方库。和任何Python第三方库一样,可以使用pip对其进行安装与管理。它提供的API功能比较强大且比较简洁,让初学者能够很快速的对其进行各种请求,但是有一个显著的缺点,即不支持js。详情了解请点击
2.3 requests-html
requests作者为了解决不支持js的弊端,特意推出了equests-html模块,除此之外还可以直接对其网页数据进行解析。详情了解请点击
在使用requests-html的时候可以通过函数render()进行对js的渲染,遇到如下错误,根据错误应该是解析js的时候超时了,可以对render进行参数设置
2.4 Selenium+PhantomJS
2.5 pyppeteer+chromium
网络爬虫之使用pyppeteer替代selenium完美绕过webdriver检测
2.6 反爬虫策略
初次抓取JavLib主页的时候,就遇上了一个很棘手的防爬策略。都一次请求进入等待提示网页,等待5秒后浏览器解析js代码进行第二次请求,然后进入有效网页。如下:
经过F12调试跟踪发现第一次请求返回的body数据中有参数__cf_chl_captcha_tk__,其中有js计算5秒超时后进行第二次请求,并将token参数拼接到url的后面,这次请求成功获取到数据,后分析经过该逻辑进行两次请求并拼接url然而打印出来的链接浏览器就能够访问,但是程序第二次还是无法获取到有效数据,后又分析发现第一次请求服务器返回了cookie,第二次请求的时候传递cookie。如下:
更改代码,在第二次请求之前获取cookie并重新设置。代码如下:
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
#设置无头浏览器
chrome_options = Options()
chrome_options.add_argument('--headless')
driver = webdriver.Chrome(options=chrome_options, executable_path="G:\SDT\Python\chromedriver_win32\chromedriver.exe")
#第一次请求
driver.get("http://www.p42u.com/cn/vl_star.php?s=afjdi")
#获取第一次请求返回的cookie
cookie = driver.get_cookies()[0]
#获取第一次请求返回的token
soup = BeautifulSoup(driver.page_source, "html.parser")
token = soup.find("form",id="challenge-form")["action"]
#拼接第二次请求的url
url = "http://www.p42u.com{}".format(token)
print(cookie)
print(url)
#第二次请求先添加cookie在请求token
if "expiry" in cookie:del cookie["expiry"]
driver.add_cookie(cookie)
driver.get(url)
print(driver.page_source)
driver.close()
上面的结果然并卵,后仔细分析了其中js代码块部分,如下:
看来还是破解防爬的终极手段还是讲一切事物全部交给浏览器来处理,自己只需要等待就行了。后使用selenium+Chrome的方式,代码如下:
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
import time
options = Options()
options.add_argument('--headless')
#driver = webdriver.Chrome(options=options, executable_path="G:\SDT\Python\chromedriver_win32\chromedriver.exe")
driver = webdriver.Chrome(executable_path="G:\SDT\Python\chromedriver_win32\chromedriver.exe")
driver.get("http://www.p42u.com/cn/")
time.sleep(5)
print(driver.page_source)
driver.get("http://www.p42u.com/cn/vl_star.php?s=afjdi")
print(driver.page_source)
driver.close()
惊奇的发现,在使用无头浏览器模式的时候,上面代码是无法访问到,后经过查询发现js中已经做了无头浏览器判断,也给出了一些方法。参考如下:
总结防爬策略如下:
- 对于频繁发送请求的,直接限制IP地址
- 后端服务器js检测如果爬虫发起请求直接拒绝,因为python有些工具能够留下痕迹
- 使用token和cookie来进行双重验证,对于这种情况个人建议使用终极手段全权把控制权交给无头浏览器处理
3 数据解析
解析上面抓取下来的网页数据,从庞大的网页数据里面提取到你需要的信息,离不开对网页源码进行分析,可以使用浏览器的F12查看网页源码,然后总结他们的关系跳转流程,使用BeautifulSoup对网页数据进行解析,配合正则表达式进行过滤,最终提取出你需要的信息。
3.1 BeautifulSoup
3.1.1 BeautifulSoup概要
BeautifulSoup库主要功能对抓取下来的网页或者字符串进行解析。官方文档解释如下:
- Beautiful Soup提供一些简单的、python式的函数用来处理导航、搜索、修改分析树等功能。它是一个工具箱,通过解析文档为用户提供需要抓取的数据,因为简单,所以不需要多少代码就可以写出一个完整的应用程序。
- Beautiful Soup自动将输入文档转换为Unicode编码,输出文档转换为utf-8编码。你不需要考虑编码方式,除非文档没有指定一个编码方式,这时,Beautiful Soup就不能自动识别编码方式了。然后,你仅仅需要说明一下原始编码方式就可以了。
- Beautiful Soup已成为和lxml、html6lib一样出色的python解释器,为用户灵活地提供不同的解析策略或强劲的速度
由于BeautifulSoup库并不是python标准库,因此需要单独安装。安装教程请点击我
BeautifulSoup将复杂HTML文档转换成一个复杂的树形结构,每个节点都是Python对象,所有对象可以归纳如下四种:
- BeautifulSoup对象:表示的是一个文档的全部内容。它支持遍历文档树和搜索文档树中描述的大部分的方法。
- Tag对象:表示一个xml节点或者html的标签。它页支持遍历文档树和搜索文档树中描述的大部分的方法。
- NavigableString对象:表示一个标签的文本内容,可以通过tag.string获取得到
- Comment对象:表示一个特殊类型的NavigableString对象,例如文档的注释部分就是一个Comment对象
3.1.2 Tag对象
Tag对象与XML或HTML原生文档中的标签(节点)相同。一个标签中可以有很多属性和文本内容,也可以有很多子标签。其格式如下:<标签名称 属性1="属性值" 属性2="属性2">内容</标签名称>
<!--单标签-->
<br>
<!--普通标签-->
<title>我是如来佛祖</title>
<!--属性标签-->
<font color="red" size="5">我是观音菩萨</font>
<!--嵌套标签-->
<html>
<head>我是玉皇大帝的头</head>
<body>我是玉皇大帝的身体</body>
</html>
- 成员变量name:表示标签的名称,即可以通过tag.name得到标签名称
- 成员变量attrs:表示标签的属性,即tag.attrs返回的是一个存储了所有属性的字典,也可以直接通过类似字典的方式即tag[属性名]来获取属性值
- 成员变量string:表示标签的内容,即tag.string返回的是标签的内容,也是一个NavigableString对象
- 访问子标签:可以通过tag.子标签名的方式直接获取一个字标签tag对象
from bs4 import BeautifulSoup
html_doc="""
<list>
<car1 brand="大众" type="B级轿车">迈腾</car1>
<car2 brand="奥迪" type="C级轿车">A6</car2>
</list>
"""
soup = BeautifulSoup(html_doc, 'html.parser')
tagList=soup.list
#tag.子标签名:直接获取子标签tag对象
car1=tagList.car1
car2=tagList.car2
print("car1: type({})".format(type(car1)))
print("car2: type({})".format(type(car2)))
# car1: type(<class 'bs4.element.Tag'>)
# car2: type(<class 'bs4.element.Tag'>)
#tag.name:直接获取标签名称
print("car1 name: type({}) [{}]".format(type(car1.name),car1.name))
print("car2 name: type({}) [{}]".format(type(car2.name),car2.name))
# car1 name: type(<class 'str'>) [car1]
# car2 name: type(<class 'str'>) [car2]
#tag.string:直接获取标签内容
print("car1 string: type({}) [{}]".format(type(car1.string),car1.string))
print("car2 string: type({}) [{}]".format(type(car2.string),car2.string))
# car1 string: type(<class 'bs4.element.NavigableString'>) [迈腾]
# car2 string: type(<class 'bs4.element.NavigableString'>) [A6]
#tag.attrs:直接获取所有属性
print("car1 attrs: type({}) [{}]".format(type(car1.attrs),car1.attrs))
print("car2 attrs: type({}) [{}]".format(type(car2.attrs),car2.attrs))
# car1 attrs: type(<class 'dict'>) [{'brand': '大众', 'type': 'B级轿车'}]
# car2 attrs: type(<class 'dict'>) [{'brand': '奥迪', 'type': 'C级轿车'}]
#tag[属性名]:直接通过字典方式获取属性值
print("car1[xxx]: {} {}".format(car1["brand"],car1["type"]))
print("car2[xxx]: {} {}".format(car2["brand"],car2["type"]))
# car1[xxx]: 大众 B级轿车
# car2[xxx]: 奥迪 C级轿车
- 成员变量children:表示标签的所有子节点,即tag.children返回一个列表,列表是该标签的所有成员。注意包括Comment
- 遍历标签:遍历tag其实跟遍历tag.children没有任何区别
- 成员函数find_all():返回该标签所有的子标签,即不包括Comment对象
from bs4 import BeautifulSoup
html_doc="""
<list>
<car brand="大众" type="B级轿车">迈腾</car>
<car brand="奥迪" type="C级轿车">A6</car>
</list>
"""
soup = BeautifulSoup(html_doc, 'html.parser')
tagList=soup.list
print("%%%tagList type({}):{}%%%".format(type(tagList),"..."))
#%%%tagList type(<class 'bs4.element.Tag'>):...%%%
#遍历tagList
for car in tagList:
print("%%%type({}):{}%%%".format(type(car),car))
#%%%type(<class 'bs4.element.NavigableString'>):
#%%%
#%%%type(<class 'bs4.element.Tag'>):<car brand="大众" type="B级轿车">迈腾</car>%%%
#%%%type(<class 'bs4.element.NavigableString'>):
#%%%
#%%%type(<class 'bs4.element.Tag'>):<car brand="奥迪" type="C级轿车">A6</car>%%%
#%%%type(<class 'bs4.element.NavigableString'>):
#%%%
#遍历tagList.children
for car in tagList.children:
print("%%%type({}):{}%%%".format(type(car),car))
#%%%type(<class 'bs4.element.NavigableString'>):
#%%%
#%%%type(<class 'bs4.element.Tag'>):<car brand="大众" type="B级轿车">迈腾</car>%%%
#%%%type(<class 'bs4.element.NavigableString'>):
#%%%
#%%%type(<class 'bs4.element.Tag'>):<car brand="奥迪" type="C级轿车">A6</car>%%%
#%%%type(<class 'bs4.element.NavigableString'>):
#%%%
#遍历tagList.find_all()
for car in tagList.find_all():
print("%%%type({}):{}%%%".format(type(car),car))
#%%%type(<class 'bs4.element.Tag'>):<car brand="大众" type="B级轿车">迈腾</car>%%%
#%%%type(<class 'bs4.element.Tag'>):<car brand="奥迪" type="C级轿车">A6</car>%%%
注意:在遍历Tag对象的时候,往往会把回车换行等特殊符当成一个子对象处理,例如上面示例,直接遍历标签对象tagList,除了有Tag类型的对象之外还有NavigableString对象。
3.1.3 BeautifulSoup对象
BeautifulSoup对象表示一个文档的全部内容,即BeautifulSoup对象往往能够描述本地的一个html/xml文件,或者是一次http请求的返回内容。
1) 构建BeautifulSoup
在抓取到一个网页数据之后,或者是已经下载了一个需要解析的网页文件,我们可以通过这些数据构建一个BeautifulSoup对象。该对象解析了文档的全部内容并以树形结构重新存储,这样就能方便我们后续的数据分析和提取。构建BeautifulSoup对象通过函数BeautifulSoup(markup, type)返回
其中参数markup可以是文件路径,也可以是html/xml字符串,也可以是http请求返回内容。如下:
#解析requests请求返回内容
response = requests.get("http://www.p42u.com/cn/")
soup = BeautifulSoup(response.text,"html.parser")
#解析urlopen请求返回内容
response = urlopen("http://www.p42u.com/cn/")
soup = BeautifulSoup(response .read,"html.parser")
#解析本地文件
soup = BeautifulSoup(open("index.html"),"html.parser")
#解析字符串
html="""
<list>
<car1 brand="大众" type="B级轿车">迈腾</car1>
<car2 brand="奥迪" type="C级轿车">A6</car2>
</list>
"""
soup = BeautifulSoup(html, 'html.parser')
其中参数type表示解析采用的解析器的名称,他们各有优缺点。有如下几种解析器:
- Python标准库解析器:Python的内置标准库,执行速度适中,文档容错能力强。一般不是特殊情况都会选择这种方式。只需要传递"html.parser"作为参数
- lxml HTML解析器:执行速度快,文档容错能力强,但是需要安装C语言库。虽然其执行速度比较快,但是考虑到网络本身的速度将宗师你最大的瓶颈,所以网页抓取速度并不是一个必备的优势。只需要传递"lxml"作为参数
- lxml XML解析器:执行速度快,唯一支持XML的解析器,但是也需要安装C语言库。跟上面的一样,只不过它可以解析xml。传递"xml"作为参数
- html5lib解析器:以浏览器的方式解析文档,生成HTML5格式的文档,容错性最好,但是速度慢不。如果你处理的是一些杂乱的或者手写的html网址可用选择它。传递"html5lib"作为参数。
2) 特殊的Tag对象
因为html或者xml文件格式所致,一个文档往往可以被描述成一个根标签,因此BeautifulSoup对象也可以看成是一个特殊的Tag对象。因此Tag对象具备的使用方式,其实BeautifulSoup对象也具备。例如通过BeautifulSoup.子标签名的方式来得到一个子tag对象,如下:
from bs4 import BeautifulSoup
html_doc = """
<html>
<head>
<title>The Dormouse's story</title>
</head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
<p class="story">
Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>
,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a>
and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>
and they lived at the bottom of a well.
</p>
<p class="story">...</p>
</body>
</html>
"""
soup = BeautifulSoup(html_doc, 'html.parser')
print(type(soup.head.title),soup.head.title) #输出结果:<class 'bs4.element.Tag'> <title>The Dormouse's story</title>
print(type(soup.body.p),soup.body.p) #输出结果:<class 'bs4.element.Tag'> <p class="title"><b>The Dormouse's story</b></p>
#注意 如果某个节点下面有多个相同名字的标签,那么该方式永远获取的是第一个tag对象
print(soup.body.p.b) #因为body下面第一个p标签有子标签b 输出结果:<b>The Dormouse's story</b>
print(soup.body.p.a) #因为body下面第一个p标签没有子标签a 输出结果:None
3) 区别Tag对象
BeautifulSoup是一个特殊的Tag,即他们还是有区别的。tag对象作为一个标签,有很重要的两个属性即name和属性,然而BeautifulSoup对象表示一个文档,因此他没有一个名字,也不可能拥有属性。如下:
from bs4 import BeautifulSoup
html_doc1 = """
<html version="10">
<head>The Dormouse's story</head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
</body>
</html>
"""
soup = BeautifulSoup(html_doc1, 'html.parser')
print(type(soup)) #输出结果:<class 'bs4.BeautifulSoup'>
print(soup.name) #输出结果:[document]
print(soup.attrs) #输出结果:{}
html_doc2 = """
<head>
<p>The Dormouse's story</p>
</head>
<body>
<p class="title"><b>The Dormouse's story</b></p>
</body>
"""
soup = BeautifulSoup(html_doc2, 'html.parser')
print(type(soup)) #输出结果:<class 'bs4.BeautifulSoup'>
print(soup.name) #输出结果:[document]
print(soup.attrs) #输出结果:{}
3.1.4 find_all搜索文档树
当得到一个BeautifulSoup对象后,通常的做法是使用find_all来对其进行搜索,定位到需要抓取信息的标签tag对象,然后在对该tag标签对象进行一系列处理。因此对BeautifulSoup对象或者tag对象进行搜索使用find_all函数尤其重要。find_all定义如下:
find_all( name , attrs , recursive , string , **kwargs )
- 返回值是一个tag对象组成的列表,即字符串NavigableString对象会被自动忽略掉。
- 参数name:返回所有名字为name的标签tag对象组成的列表。即可匹配过滤tag的名称。
- 参数recursive:可以指定一个布尔值,如果为false表示不进行递归查找,即只查找自己的一级子标签。
- 参数string:返回所有含有或者匹配的NavigableString对象的tag标签。即可以匹配过滤tag的内容。
- 参数attrs:可以指定一个字典,来返回所有具有该键值对属性的标签tag对象组。即可通过字典来匹配过滤tag的属性。
- 参数kwargs:直接将属性名作为参数进行指定,来返回所有具有该属性的标签tag对象。即可通过动态参数来匹配tag的属性。
<html>
<head>
<title>星球大战</title>
</head>
<body>
<div class="fixeds">
<h1>恒星</h1>
<p class="fixed" id="taiyang">太阳</p>
<p class="fixed" id="zhinv">织女一</p>
<p class="fixed" id="canxiu">参宿七</p>
</div>
<div class="planets">
<h1>行星</h1>
<p class="planet" id="diqiu" href="https://baike.baidu.com/item/%E5%9C%B0%E7%90%83/6431">地球</p>
<p class="planet" id="shui">水星</p>
<p class="planet" id="huo">火星</p>
<p class="planet" id="mu">木星</p>
<p class="planet" id="tu">土星</p>
</div>
<div class="others">
<h1>其他</h1>
<h3>四大洲<h3>
<h5>亚洲</h5><h5>欧洲</h5><h5>非洲</h5><h5>美洲</h5>
<h3>五大洋</h3>
<h5>大西洋</h5><h5>太平洋</h5><h5>北冰洋</h5><h5>印度洋</h5>
<h3>地貌</h3>
<h5>chengdu plain</h5><h5>huabei plain</h5><h5>yamaxun plain</h5>
</div>
</body>
</html>
1) 过滤name
过滤标签tag对象的名称有如下几种方式:
- 传递字符串
- 传递字符串列表
- 传递正则表达式
- 传递True
- 传递方法名
import re
from bs4 import BeautifulSoup
soup = BeautifulSoup(html,"html.parser")
#传递字符串
listx=soup.find_all("h1")
print(listx) # [<h1>恒星</h1>, <h1>行星</h1>, <h1>其他</h1>]
#传递字符串列表
listx=soup.find_all(["title","h1"])
print(listx) # [<title>星球大战</title>, <h1>恒星</h1>, <h1>行星</h1>, <h1>其他</h1>]
#传递正则表达式
listx=soup.find_all(re.compile("h[1-9]"))
print(listx) # 打印所有h数字的tag
#传递true
listx=soup.find_all(True)
print(listx) # 打印所有的tag对象
2) 过滤string
可以传递string参数,匹配过滤所有的NavigableString对象。其中string参数可接受字符串 , 正则表达式 , 列表, True 。注意的是返回的不再是tag对象了,而是NavigableString对象。如下:
soup = BeautifulSoup(html,"html.parser")
#指定string为字符串 全名匹配
listx = soup.find_all(string="洲")
print(listx) # []
listx = soup.find_all(string="非洲")
print(listx) # ['非洲']
#指定string参数 返回值不再是tag对象而是NavigableString对象
print(type(listx[0])) # <class 'bs4.element.NavigableString'>
#指定string为正则表达式 非全名匹配
listx = soup.find_all(string=re.compile("plain$"))
print(listx) # ['chengdu plain', 'huabei plain', 'yamaxun plain']
3) 过滤attrs
可以传递attrs参数,给其赋值一个字典进行过滤,将返回所有与该字典中的键值对匹配的tag对象,如下:
soup = BeautifulSoup(html,"html.parser")
#传递单个键值对
listx = soup.find_all(attrs={"class":"fixed"})
print(listx) # [<p class="fixed" id="taiyang">太阳</p>, <p class="fixed" id="zhinv">织女一</p>, <p class="fixed" id="canxiu">参宿七</p>]
#传递多个键值对
listx = soup.find_all(attrs={"class":"fixed","id":"zhinv"})
print(listx) # [<p class="fixed" id="zhinv">织女一</p>]
#name与attrs结合使用
listx = soup.find_all("p",attrs={"id":"mu"})
print(listx) # [<p class="planet" id="mu">木星</p>]
4) 过滤kwargs
可以传递kwargs,即传递字典作为关键字参数来进行对属性的过滤,如下:
import re
from bs4 import BeautifulSoup
soup = BeautifulSoup(html,"html.parser")
#关键字参数赋值字符串 全名匹配
listx = soup.find_all(id="mu")
print(listx) # [<p class="planet" id="mu">木星</p>]
#关键字参数赋值正则表达式 非全名匹配
listx = soup.find_all(id=re.compile("^t"))
print(listx) # [<p class="fixed" id="taiyang">太阳</p>, <p class="planet" id="tu">土星</p>]
#关键字参数赋值True 匹配拥有该属性任何值即有该属性就匹配
listx = soup.find_all(href=True)
print(listx) # [<p class="planet" href="https://baike.baidu.com/item/%E5%9C%B0%E7%90%83/6431" id="diqiu">地球</p>]
#多个关键字参数 多个条件满足才匹配
listx = soup.find_all(href=True, id="tu")
print(listx) # []
5) 过滤class
按照CSS类名搜索tag的功能非常实用,但标识CSS类名的关键字 class在Python中是保留字,使用class做参数会导致语法错误。但从Beautiful Soup的4.1.1版本开始,可以通过 class_参数搜索有指定CSS类名的标签。即可以通过class_关键字参数来指定所有具有class属性的标签。如下:
soup = BeautifulSoup(html,"html.parser")
#class关键字导致语法错误 可以通过参数attrs
listx = soup.find_all(attrs={"class":"fixed"})
print(listx) # [<p class="fixed" id="taiyang">太阳</p>, <p class="fixed" id="zhinv">织女一</p>, <p class="fixed" id="canxiu">参宿七</p>]
#class关键字导致语法错误 可以通过class_代替
listx = soup.find_all(class_="fixed")
print(listx) # [<p class="fixed" id="taiyang">太阳</p>, <p class="fixed" id="zhinv">织女一</p>, <p class="fixed" id="canxiu">参宿七</p>]
#与其他方式和正则表达式结合
listx = soup.find_all("div",class_=re.compile("^fixeds"))
print(listx)
# [<div class="fixeds">
# <h1>恒星</h1>
# <p class="fixed" id="taiyang">太阳</p>
# <p class="fixed" id="zhinv">织女一</p>
# <p class="fixed" id="canxiu">参宿七</p>
# </div>]
6) 禁止递归recursive
默认情况find_all函数将查找BeautifulSoup或Tag对象所有的子节点,如果深度比较大,而且不需要那么多信息,可以使用参数recursive禁止递归查找。如下:
soup = BeautifulSoup(html,"html.parser")
#禁止递归查找 即只查找直属子节点
listx = soup.find_all("p",recursive=False)
print(listx) # []
#使能递归查找 即查找整个文档或者整个标签
listx = soup.find_all("p",recursive=True)
print(listx) # 所有p标签
#recursive是布尔类型 即指定1,2,3效果都一样
listx = soup.find_all("p",recursive=2)
print(listx) # 所有P标签
#recursive默认为True
listx = soup.find_all("p")
print(listx) # 所有P标签
7) 限制数量limit
find_all方法返回全部的搜索结构,如果文档树很大那么搜索会很慢。如果我们不需要全部结果,可以使用limit参数限制返回结果的数量。效果与SQL中的limit关键字类似,当搜索到的结果数量达到limit的限制时,就停止搜索返回结果。如下:
soup = BeautifulSoup(html,"html.parser")
listx = soup.find_all("p",limit=2)
print(listx) # [<p class="fixed" id="taiyang">太阳</p>, <p class="fixed" id="zhinv">织女一</p>]
listx = soup.find_all("p",limit=3)
print(listx) # [<p class="fixed" id="taiyang">太阳</p>, <p class="fixed" id="zhinv">织女一</p>, <p class="fixed" id="canxiu">参宿七</p>]
#参数limit如果没有给则无限制
listx = soup.find_all("p")
print(listx) # 所有P标签
find方法直接返回一个结果,而不像find_all方法返回一个列表。其实find方法等价于find_all(limit=1)
3.2 字符串
对数据的处理往往离不开字符串解析,例如字符串替换,切割,正则匹配等操作。
3.2.1 正则表达
正则表达式是可匹配文本片段的模式。最简单的正则表达式为普通字符串,与它自己匹配。换而言之,正则表达式 'python' 与字符串 'python' 匹配。你可使用这种匹配行为来完成如下工作:在文本中查找模式,将特定的模式替换为计算得到的值,以及将文本分割成片段。正则表达式由一些普通字符和一些元字符(metacharacters)组成。普通字符包括大小写的字母和数字,而元字符则具有特殊的含义。
1) 元字符
- 转义符:反斜杠\能够对某些字符进行转义,例如\n,如果要匹配换行符,可能再添加一个反斜杠进行取消转义,如下:
import re
#字符串ZhouSi后面有一个\n换行符
string = """I am ZhouSi
I am God"""
print(re.sub("ZhouSi\\n","AoDing and ",string))
#输出结果:I am AoDing and I am God
- 通配符:句点.能够匹配所有字符(不包括换行符),但是只能匹配一个。例如正则表达式 '.ython' 与字符串 'python' 和和'jython' 都匹配,但是无法与字符串'xpython'匹配。
- 字符集:中括号[]能够匹配中括号中指定范围的一个字符,注意与通配符区别。例如正则表达式'h[0-9]'能与字符串'h1'到'h9'的字符串都匹配,正则表达式'[^py]'表示匹配除了字符p和字符y的其他所有字符。
- 字行首:^能够匹配字符串行首,注意其写法应该位于表达式首位,注意与反字符集区分。例如正则表达式'^http:'能够匹配所有以http:开头的链接。
- 字行尾: $ 能够匹配字符串行尾,注意其写法应该位于表达式尾部。例如正则表达式'</head>$'能够陪陪所有以</head>结尾的字符串。
- 二选一:|能够指定两个正则表达式,只要满足其中之一就表示匹配成功。例如‘xyc|.ng’只要满足|左右两边任一表达式都匹配成功。如下:
import re
# 切割了空格和ing和eng几处地方
string = "I am DingPengCheng, I want to Sun."
temp = re.split(" |.ng",string)
print(temp) #输出结果:['I', 'am', 'D', 'P', 'Ch', ',', 'I', 'want', 'to', 'Sun.']
2) re.compile方法:创建模式对象(用于正则表达式匹配查询等操作)
正则表达式其实也是一些奇奇怪怪的字符串,因此re模块在进行正则表达式相关的查找匹配搜索截取等操作的时候,其实分为了两个过程:
- 将正则表达式字符串编译成一个模式对象
- 模式对象可以与字符串进行匹配过滤等操作
因此通过使用re.compile对正则表达式字符串转换成一个模式对象,后续就无需再对其进行转换,通过这个模式对象你可以进行匹配查询切割等操作。如下:
import re
string = "I am DingPengCheng, I want to Sun."
patter1 = re.compile(".ng")
patter2 = re.compile(" ")
temp = patter1.findall(string)
print(temp) #输出结果:['ing', 'eng', 'eng']
temp = patter2.sub("-+",string)
print(temp) #输出结果:I-+am-+DingPengCheng,-+I-+want-+to-+Sun.
3) re.findall方法:返回能够匹配的所有字符串组成的列表
import re
# def findall(pattern, string, flags=0)
# 查找字符串string中能够匹配pattern正则表达式字符串组成的集合
string = "I am DingPengCheng, I want to Sun."
temp = re.findall(".ng",string)
print(temp) #输出结果:['ing', 'eng', 'eng']
4) re.search方法:查找是否具有与正则表达式匹配的子串
该函数在给定字符串中查找第一个与指定正则表达式匹配的子串。如果找到这样的子串,将返回MatchObject(结果为真),否则返回 None (结果为假)。基于该特性,可以通过下面语句进行判断:
import re
# def search(pattern, string, flags=0)
string = "I am DingPengCheng, I want to Sun."
temp = re.search(".ng",string)
print(temp) #输出结果:<_sre.SRE_Match object; span=(6, 9), match='ing'>
temp = re.search("xng",string)
if re.search("xng",string): #输出结果:没找到xng
print("找到xng")
else:
print("没找到xng")
5) re.match方法:字符串开头查找是否匹配指定正则表达式
函数re.match尝试在给定字符串开头查找与正则表达式匹配的子串,因此 re.match('p','python') 返回真(MatchObject),而 re.match('p', 'www.python.org') 返回假( None )。同search一样,该函数返回的也是MatchObject对象,这种对象包含与模式匹配的子串的信息,还包含模式的哪部分与子串的哪部分匹配的信息。这些子串部分称为编组(group)。
import re
# def match(pattern, string, flags=0)
m = re.match(r'www\.(.*)\..{3}', 'www.python.org')
print(m.group(1)) #输出结果:'python'
print(m.start(1)) #输出结果:4
print(m.end(1)) #输出结果:10
print(m.span(1)) #输出结果:(4, 10)
6) 模式对象
从上面的例子可以发现,我们在使用re模块对正则表达式进行过滤匹配等操作的时候,可以使用两种方式:
- 通过re.compile得到一个模式对象,然后对该模式对象进行对应操作
- 直接re.操作方法指定一个正则表达式
通过他们方法的源码实现发现,其实第二种方式内部其实也调用了compile方法来将正则表达式字符串编译生成一个模式对象,最后通过该模式对象完成的。详情参考《Python中请不要再用re.compile了》。个人觉得这篇文章其实也不是呼吁我们不要使用re.compile函数,而是提醒我们不要乱用。
3.2.2 常用操作
1) 拼接
- 运算符+:通过+将两个字符串相加
- str.format方法:通过字符串自身的fomat进行格式化操作
# def format(self, *args, **kwargs)
temp="YangJian"
print("I am {} and I am {},I have {} eye".format(temp,"God",3))
# 输出结果:I am YangJian and I am God,I have 3 eye
- str.join方法:与split相反,将序列的所有元素以指定的字符串链接起来
listx = "I am DingPengCheng".split(" ")
print(listx) #输出结果:['I', 'am', 'DingPengCheng']
print("".join(listx)) #输出结果:IamDingPengCheng
print("###".join(listx)) #输出结果:I###am###DingPengCheng
print("###".join({"name":"dpc","age":18})) #输出结果:name###age
- 操作符%:该方式与format基本一致
- F-strings:f指定字符串中的{变量名}能够直接替换成变量的值。其速度比format方式快的多
def power(x):
return x*x
x=4
print(f'{x} * {x} = {power(x)}')
s1='Hello'
s2='World'
print(f'{s1} {s2}!')
2) 查找
- str.find/rfind方法:字符串自身提供的find和rfind查找方法
# def find(self, sub, start=None, end=None)
# def rfind(self, sub, start=None, end=None)
# 返回字符串sub第一次出现的索引,其中start和end可以用来指定范围
# 如果没有查询到则返回-1
# find函数从左到右查找,rfind函数从右到左查找
temp = "I am DingPengCheng".find("ng")
print(temp) #输出结果:7
temp = "I am DingPengCheng".rfind("ng")
print(temp) #输出结果:16
temp = "I am DingPengCheng".find(" ",2,10)
print(temp) #输出结果:4
temp = "I am DingPengCheng".rfind("g",5,10)
print(temp) #输出结果:8
temp = "I am DingPengCheng".find("dpc")
print(temp) #输出结果:-1
- str.index/rindex方法:字符串自身提供的index和rindex返回索引方法
# def index(self, sub, start=None, end=None)
# def rindex(self, sub, start=None, end=None)
# 返回字符串sub第一次出现的索引,其中start和end可以用来指定范围
# 与find/rfind不一样的是如果没有查询直接抛出异常
temp = "I am DingPengCheng".index("ng")
print(temp) #输出结果:7
temp = "I am DingPengCheng".rindex("ng")
print(temp) #输出结果:16
temp = "I am DingPengCheng".index("*")
print(temp) #输出结果:ValueError: substring not found
3) 替换
- str.replace方法:字符串自身提供的replace替换方法
# def replace(self, old, new, count=None)
# 将字符串的中的old替换成new,参数count为替换次数,默认无限制
# 注意:结果并不保存而是返回,即如果该函数调用后返回新的字符串并不会改变原来字符串
temp = "I am DingPengCheng".replace(' ', '_')
print(temp) #输出结果:I_am_DingPengCheng
temp = "I am DingPengCheng".replace('eng', '*')
print(temp) #输出结果:I am DingP*Ch*
temp = "I am DingPengCheng".replace('eng', '*', 1)
print(temp) #输出结果:I am DingP*Cheng
- re.sub方法:通过正则表达式方式进行替换
import re
# def pattern.sub(self, repl, string, count=0):
# def re.sub(pattern, repl, string, count=0, flags=0)
# 将编译正则表达式字符串生成模式对象patern,并将string字符串能够匹配的全部替换成repl
# 注意:结果并不保存而是返回,即如果该函数调用后返回新的字符串并不会改变原来字符串
# 通过模式对象调用sub函数
pattern = re.compile('eng')
temp = pattern.sub('*',"I am DingPengCheng")
print(temp) #输出结果:I am DingP*Ch*
#直接调用re.sub函数
temp = re.sub("eng","*","I am DingPengCheng",count=1)
print(temp) #输出结果:I am DingP*Cheng
4) 切割
- str.split方法:字符串自身提供的split切割方法
# def split(self, sep=None, maxsplit=-1)
# 以sep切割字符串,其中maxsplit指定切割次数
# 返回被切割后的所有子集列表,注意被切割的字符串已经被去掉了
string = "I am DingPengCheng"
temp = string.split(" ")
print(temp) #输出结果:['I', 'am', 'DingPengCheng']
temp = string.split("en", 1)
print(temp) #输出结果:['I am DingP', 'gCheng']
- re.split方法:以匹配正则表达式方式进行字符串切割
import re
# def pattern.split(self, string, maxsplit=0):
# def re.split(pattern, string, maxsplit=0, flags=0)
# 以pattern切割字符串,其中maxsplit指定切割次数
# 返回被切割后的所有子集列表,注意被切割的字符串已经被去掉了
patter = re.compile(" ")
temp = patter.split("I am DingPengCheng", 1)
print(temp) #输出结果:['I', 'am DingPengCheng']
temp = re.split(".ng","I am DingPengCheng")
print(temp) #输出结果:['I am D', 'P', 'Ch', '']
4 数据存储
5 并行抓取
网络爬虫程序往往限制于网络的状态,例如在遍历整个网站的时候,需要下载上面所有内链的图片,如果等网站遍历完后再进行图片下载,这个时候可能要等疯,最合理的方法是一边遍历一边进行图片下载。因此这里就需要并行任务处理。
5.1 进程
5.2 线程
参考:python 多线程queue导致的死锁问题【但是源码发现方法内部已经加了锁】
5.3 协程
6 Scrapy框架
Scrapy框架继承了上面所有操作,详情请点击