Python爬虫
碎碎念
- 参考资料:https://blog.csdn.net/c406495762/article/category/9268672
- 关于Python爬虫我学习的流程是:看了上面的几篇博客(Scrapy博客前的所有博客都看了),然后我总觉得自己应用起来不知道该怎么操作,也不知道自己应用起来可以爬取什么(以后再专门找来实战练习把)况且又容易涉及到是否违法,我没能去尝试。紧接着我接触到了《Python网络爬虫权威指南》这本书,确实解决了我不少迷惑,思路上也更清晰了。于是打算将这本书先看完。先看完了那几篇博客,然后又接触到了这本书,这一学习过程还算刚刚好吧。
- 此外,了解到Python爬虫并不是很难(主要是会调用爬虫的库就行了),不需要专门去学,所以打算能够自己应用后去学习算法(刷LeetCode)和机器学习吧。
几个名词解释
网页下载器:将互联网上URL对应的网页以HTML代码的形式下载到本地,并存储到本地文件中或使用字符串进行保存的一种工具。
- Python有哪几种网页下载器?
urllib2
——Python官方基础模块requests
——Python的第三方插件,其功能更为强大。下面讲解使用urllib2下载网页的方法:
- 最简洁的方法
import urllib.request # 直接发送下载请求 response = urllib.request.urlopen('https://www.baidu.com/') # 获取状态,看请求是否成功(如果状态码为200,则表示成功) print(response.getcode()) # 查看下载到的内容的长度及内容本身 content = response.read() print(len(content)) print(content)
- 添加data、http header
- urlopen()的参数不仅可以是URL,还可以是一个Request实例对象。Request实例对象在构造时需要传入URL,并设置data、header属性的值。
import urllib.request # 创建request对象 request = urllib.request.Request('https://www.baidu.com/') # 添加data内容 request.add_data('a', '1') # 第一个参数表示属性,第二个参数表示这个属性对应的值 # 添加http的header内容 request.add_header('User-Agent', 'Mozilla/5.0') # 第一个参数表示请求的身份,第二个参数表示请求的身份被设置成浏览器(把爬虫伪装成一个浏览器) # 发送网页下载请求 response = urllib.request.urlopen(request)
Ps. 程序相关的一些解释,可以看看这篇文章。
- 添加特殊情景的处理器
- 例如,有些网页需要登录后才能访问(使用
HTTPCookieProcessor
);有些网页需要代理才能访问(使用ProxyHandler
);有些网页使用HTTPS加密访问(使用HTTPSHandler
);有些网页的URL之间具有相互跳转关系(使用HTTPRedirectHandler
)。import urllib2, cookielib # 创建cookie容器,用于存储cookie数据 cj = cookielib.CookieJar() # 查看cookie内容 # print(cj) # 创建一个opener对象 opener = urllib.build_opener(urllib.HTTPCookieProcessor(cj)) # 给urllib2安装opener urllib2.install_opener(opener) # 使用带有cookie的urllib2访问网页 response = urllib.urllib2.urlopen('http://www.baidu.com/')
网页解析器:从网页中提取有价值数据的一种工具。以获取到的HTML网页代码内容作为操作的数据,从中获取到有价值的数据以及新的待爬取的URL列表。
- Python有哪几种网页解析器?
- 正则表达式:将HTML网页代码内容作为字符串(这个字符串很长),使用某个匹配表达式来提取出对应的数据。当这个字符串很复杂时,此方法就会很麻烦。
- 使用Python自带的
html.parser
模块来解析网页。- 使用第三方插件
BeautifulSoup
(它使用html.parser
或lxml
作为其自身的解析器来使用)关于解析器:
Beautiful Soup为不同的解析器提供了相同的接口,但解析器本身时有区别的.同一篇文档被不同的解析器解析后可能会生成不同结构的树型文档.区别最大的是HTML解析器和XML解析器。具体请看这里。
- 使用第三方插件
lxml
- 第一种方式是一种模糊匹配,另外三种方式是一种结构化解析。所谓结构化解析是指,将 解析出来的HTML网页以DOM树的结构呈现出来。
- 下面讲解使用
BeautifulSoup
从HTML或XML中提取数据:
(1)根据已下载好的HTML网页代码内容的字符串或文档(这个文档里保存的是被解析下来的HTML代码)来创建一个BeautifulSoup
实例对象,就可以直接得到HTML网页代码的DOM树;
(2)根据这个DOM树就可以进行各种节点的搜索(使用find_all()
方法搜索出所有满足要求的节点、使用find()
方法搜索出第一个满足要求的节点;在搜索节点时,有三个“根据”:根据结点的名称或属性或文本内容进行搜索);
(3)得到节点后就能够访问节点的名称、属性、文本内容了。from bs4 import BeautifulSoup # 1 根据下载好的HTML网页字符串创建BeautifulSoup实例对象 soup = BeautifulSoup (html_doc, 'html.parser', from_encoding = 'utf8' ) # 第一个参数:HTML文档的字符串 # 第二个参数:指定将要解析HTML网页的解析器 # 第三个参数:指定HTML文档的编码(如果HTML网页的编码与代码的编码不一致的话,解析过程中就会出现乱码) # 2 搜索节点 # 2.0 方法:find_all(name, attrs, string) # 2.1 查找所有标签为a的节点 soup.find_all('a') # 2.2 查找所有标签为a、链接符合/view/123.htm形式的节点 soup.find_all('a', href='/view/123.htm') soup.find_all('a', href=re.compile(r'/view/\d+\.htm')) # 正则表达式 # 2.3 查找所有标签为div、class为abc、文本内容为‘Python’的节点 soup.find_all('div', class_='abc', string='Python') # 3 访问节点信息 # 3.0 假设得到节点<a href='1.html'>Python</a> # 3.1 获取节点的标签名称 node.name # 3.2 获取节点的所有属性(返回一个字典) node['href'] # 3.3 获取节点的文本内容 node.get_text()
BeautifulSoup(解析器)
- Beautiful Soup 是一个可以从HTML或XML文件中提取数据的Python库。它能够通过你喜欢的转换器实现惯用的文档导航、查找、修改文档的方式。Beautiful Soup会帮你节省数小时甚至数天的工作时间。
- Beautiful Soup将复杂HTML文档转换成一个树形结构,每个节点都是Python对象,所有对象可以归纳为4种:
Tag
,NavigableString
,BeautifulSoup
,Comment
. - 根据已下载好的HTML网页代码内容的字符串或文档(这个文档里保存的是被解析下来的HTML代码)来创建一个BeautifulSoup实例对象,就可以直接得到HTML网页代码的DOM树。
- 创建Beautiful Soup对象:
soup = BeautifulSoup(html_doc,'lxml')
# 默认情况下,Beautiful Soup会将当前文档作为HTML格式解析;如果要解析LXML文档,要在 BeautifulSoup 构造方法中加入第二个参数 “lxml”
注意,构造时的第一个参数可以是字符串,也可以是文档。
soup = BeautifulSoup(open('html_doc.html'),'lxml')
这里我们是将其保存为字符串html_doc的:
# 假设解析出来的一个HTML网页的代码如下:
html_doc = """
<html>
<head>
<title>The Dormouse's story</title>
</head>
<body>
<p class="title" name="copy"><b>The Dormouse's story</b></p>
<li><!--This is comment!--></li>
<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>
"""
利用创建出来的soup对象,格式化输出html_doc的HTML代码:
print(soup.prettify())
Beautiful Soup中涉及到的四种对象
Tag
- 解释:通俗点讲,Tag表示HTML中的一个个标签
- 使用:利用
BeautifulSoup对象.标签名
可以轻松地获取这些标签的内容
print(soup.title) # 运行结果:<title>The Dormouse's story</title>
print(soup.head) # 运行结果:<head><title>The Dormouse's story</title></head>
print(soup.a) # 运行结果:<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
print(soup.p) # 运行结果:<p class="title" name="copy"><b>The Dormouse's story</b></p>
# 验证一下上面这些对象的类型,发现它们就是Tag类型的~
print(type(soup.title)) # 运行结果:<class 'bs4.element.Tag'>
#
#
- Tag的
name属性
:(输出的值为这个标签本身的名称)
print(soup.title.name) # 运行结果:title
- Tag的
attrs属性
:(输出的值为这个标签具有的所有属性,是一个字典类型)
print(soup.a.attrs) # 运行结果:{'href': 'http://example.com/elsie', 'class': ['sister'], 'id': 'link1'}
# 可以看出,有多个a标签,但只打印出第一个a标签的属性
- Tag的
get()方法
:获取某个标签的指定属性的属性值
print(soup.a.get('class')) # 运行结果:['sister']
# 也可以:
print(soup.a['class']) # 运行结果:['sister']
BeautifulSoup
- 解释:BeautifulSoup 对象表示的是一个文档的全部内容(
‘[document]’
)。大部分时候,可以把它当作Tag
对象——一个特殊的Tag
。可以获取它的类型、名称以及属性,代码如下。
print(soup.name) # [document]
print(type(soup.name)) # <class 'str'>
print(soup.attrs) # {}
NavigableString
- 解释:用于获取标签内部的文本内容
- 使用:
Tag对象.string
print(soup.title.string) # 运行结果:The Dormouse's story
Comment
- 解释: Comment对象是一个特殊类型的
NavigableString
对象,也使用Tag对象.string
,但只能对标签里的文本内容是纯注释的标签使用,从而Tag对象.string
将输出注释内容(不包括注释符号)。
print(soup.li) # <li><!--This is comment!--></li>
print(soup.li.string) # This is comment!
print(type(soup.li.string)) # <class 'bs4.element.Comment'>
由于其不包括注释符号,故将很有可能使程序出现不想要的结果,所以,我们在使用前最好做一下判断,判断代码如下:
if type(soup.li.string) == element.Comment:
print(soup.li.string)
遍历DOM树
获取所有Tag的直接子节点(不包含孙节点)
Tag对象.contents
- 一次性将所有子节点打印出来,返回一个列表
print(soup.body.contents)
# 由于上面的返回结果是列表类型,所以可以使用索引得到指定索引的节点
print(soup.body.contents[1])
2. Tag对象.children
- 返回结果不再是存储了所有子节点的列表,而是:
print(soup.body.children)
事实上,其返回结果是一个 list 生成器对象。如果想要将所有子节点打印出来,应该:
for child in soup.body.children:
print(child)
搜索DOM树
BeautifulSoup对象.find_all(name, attrs, recursive, text, limit, **kwargs)
- 搜索所有名为name的子标签,并判断是否符合过滤器的条件
下面对find_all()方法的参数进行说明:
- name参数:要查找的Tag的字符串类型的标签名或一个正则表达式或列表或True值
- 查找文档中所有的< a> 标签(以列表形式返回)
print(soup.find_all('a'))
- 找出所有标签名以b开头的标签,如< body>、< b>、< br>等标签,以列表形式返回
import re print(soup.find_all(re.compile("^b")))
- 如果传入列表参数,Beautiful Soup会将与列表中任一元素匹配的内容返回
print(soup.find_all(['title', 'b']))
- True参数表示查找页面中的所有节点
print(soup.find_all(True)) # 下面的代码查找到所有的tag标签名称 for tag in soup.find_all(True): print(tag.name)
2. attrs参数:一个字典参数,用来查找具有字典中属性&属性值的tag标签print(soup.find_all(attrs={"class":"title"}))
3. recursive参数
- 调用tag的 find_all() 方法时,Beautiful Soup会检索当前tag的所有子孙节点,如果只想搜索tag的直接子节点,可以使用参数 recursive=False。
print(soup.find_all(name='a', recursive=False))
4. text参数
- 通过 text 参数可以搜搜文档中的字符串内容,与 name 参数的可选值一样, text 参数接受字符串 , 正则表达式 , 列表, True。
print(soup.find_all(text="Elsie"))
5. limit参数
- find_all() 方法返回全部的搜索结构,如果文档树很大那么搜索会很慢,如果我们不需要全部结果,可以使用 limit 参数限制返回结果的数量。当搜索到的结果数量达到 limit 的限制时,就停止搜索,返回结果。
print(soup.find_all('a', limit=1))
- kwargs参数
- 如果传入 class 参数,Beautiful Soup 会搜索每个 class 属性为指定参数的 tag 。kwargs 接收字符串,正则表达式。
print(soup.find_all(class_="title"))
实例:小说内容爬取
爬取单章小说内容
审查第一章小说的页面元素(URL:http://www.biqukan.com/1_1094/5403177.html):
审查出上面两个信息后,就可以进行内容的爬取了。
爬取各章小说的链接
审查《一念永恒》小说目录页元素(URL:http://www.biqukan.com/1_1094/):
由审查结果可知,小说每章的链接放在了class为listmain的div标签中。链接具体位置放在html->body->div->dl->dd->a
的href属性
中。如,第1314章链接为:https://www.biqukan.com/1_1094/17967679.html。
知道上面信息后就可以进行页面的爬取了。
爬取所有章节内容,并保存到文件中
from urllib import request
from bs4 import BeautifulSoup
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030') #改变标准输出的默认编码
if __name__ == "__main__":
file = open('一念永恒.txt', 'w', encoding='utf-8') # 创建txt文件
target_url = 'http://www.biqukan.com/1_1094/'
head = {}
head['User-Agent'] = 'Mozilla/5.0 (Linux; Android 4.1.1; Nexus 7 Build/JRO03D) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.166 Safari/535.19'
target_req = request.Request(url=target_url, headers=head)
target_res = request.urlopen(target_req)
target_html = target_res.read().decode('gbk', 'ignore') # target_html是字符串类型
print(type(target_html))
listmain_soup = BeautifulSoup(target_html, 'lxml')
chapters = listmain_soup.find_all('div',class_ = 'listmain') # chapters是列表类型(所以下面强制转换为str类型)
download_soup = BeautifulSoup(str(chapters), 'lxml')
record_flag = False
for child in download_soup.dl.children:
if child != '\n':
if child.string == re.compile("^第"):
record_flag = True
if record_flag == True and child.a != None:
download_url = "http://www.biqukan.com" + child.a.get('href')
download_req = request.Request(url=download_url, headers=head)
download_res = request.urlopen(download_req)
download_html = download_res.read().decode('gbk', 'ignore')
download_name = child.string
soup_texts = BeautifulSoup(download_html, 'lxml')
texts = soup.texts.find_all(id='content', class_='showtxt')
soup_text = BeautifulSoup(str(texts), 'lxml')
count = 1 # 表示下载的章数
write_flag = True
file.write(download_name + '\n\n')
# 将爬取内容写入文件
for each in soup_text.div.text.replace('\xa0', ''):
if each == 'h':
write_flag = False
if write_flag == True and each != '':
file.write(each)
if write_flag == True and each == '\r':
file.write('\n')
file.write('\n\n')
# 打印爬取进度
sys.stdout.write("已下载: %.3f%%" % float(1314/count) + '\r')
sys.stdout.flush()
count += 1
file.close()
Selenium
一个例子:模拟提交提交搜索的功能
# 我的ChromeDriver路径:r'D:\Downloads\chromedriver_win32\chromedriver.exe'
from selenium import webdriver
from selenium.webdriver.common.keys import Keys
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030') #改变标准输出的默认编码
# 1 构建Chrome浏览器的自动测试化工具(Selenium支持Chrome浏览器驱动)
driver = webdriver.Chrome(r'D:\Downloads\chromedriver_win32\chromedriver.exe')
# 2 driver.get方法打开请求的URL(WebDriver 会等待页面完全加载完成之后才会返回,即程序会等待页面的所有内容加载完成,JS渲染完毕之后才继续往下执行)
driver.get("http://www.python.org")
assert "Python" in driver.title
# 3 下面的语句时通过find_element_by_name方法寻找一个name属性值为'q'的<input>输入框name属性
elem = driver.find_element_by_name("q") # WebDriver提供了许多寻找网页元素的方法,譬如 find_element_by_* 的一类方法(下面将介绍到)
# 4 Keys类用来模拟键盘输入
elem.send_keys("pycon")
elem.send_keys(Keys.RETURN)
# 5 输出page_source属性可以获取网页渲染后的源代码
print(driver.page_source)
# 以上,便实现了动态爬取网页的效果
元素选取
单个元素选取
find_element_by_id
find_element_by_name
find_element_by_xpath
find_element_by_link_text
find_element_by_partial_link_text
find_element_by_tag_name
find_element_by_class_name
find_element_by_css_selector
多个元素选取
find_elements_by_name
find_elements_by_xpath
find_elements_by_link_text
find_elements_by_partial_link_text
find_elements_by_tag_name
find_elements_by_class_name
find_elements_by_css_selector
利用By类
来确定选择方式
from selenium.webdriver.common.by import By
driver.find_element(By.XPATH, '//button[text()="Some text"]')
driver.find_elements(By.XPATH, '//button')
By类
的一些属性如下:
ID = "id"
XPATH = "xpath"
LINK_TEXT = "link text"
PARTIAL_LINK_TEXT = "partial link text"
NAME = "name"
TAG_NAME = "tag name"
CLASS_NAME = "class name"
CSS_SELECTOR = "css selector"
界面交互
选取到想要的元素后,就可以根据这个元素的位置进行相应的事件操作,例如输入文本框内容、鼠标单击、填充表单、元素拖拽等等。对于下面的实例:
elem = driver.find_element_by_xpath("//a[@data-fun='next']")
elem.click()
先使用find_element_by_xpath()
找到元素位置(暂且不用理会这句话什么意思,只需要知道:xpath是非常强大的元素查找方式,使用这种方法几乎可以定位到页面上的任意元素。后面会进行单独讲解);然后使用click()方法
(注意,必须得对能够),就可以触发鼠标左键单击事件。是不是很简单?但是有一点需要注意:在点击的时候,元素不能有遮挡。什么意思?就是说我在点击这个按键之前,窗口最好移动到那里,因为如果这个按键被其他元素遮挡,click()就触发异常。因此稳妥起见,在触发鼠标左键单击事件之前,滑动窗口,移动到按键上方的一个元素位置:
page = driver.find_elements_by_xpath("//div[@class='page']")
driver.execute_script('arguments[0].scrollIntoView();', page[-1]) #拖动到可见的元素去
上面的代码,就是将窗口滑动到page这个位置,在这个位置,我们能够看到我们需要点击的按键。
添加User-Agent
使用webdriver,是可以更改User-Agent的,代码如下:
from selenium import webdriver
options = webdriver.ChromeOptions()
options.add_argument('user-agent="Mozilla/5.0 (Linux; Android 4.0.4; Galaxy Nexus Build/IMM76B) AppleWebKit/535.19 (KHTML, like Gecko) Chrome/18.0.1025.133 Mobile Safari/535.19"')
driver = webdriver.Chrome(r'D:\Downloads\chromedriver_win32\chromedriver.exe', options=options)
driver.get('https://www.baidu.com/')
Xpath
XPath是XML Path的简称,由于HTML文档本身就是一个标准的XML页面,所以我们可以使用XPath的语法来定位页面元素。
以下面的HTML代码为例:
- 引用页面上的form元素(即源码中的第3行):
/html/body/form[1]
注意:
- 元素的xpath绝对路径可通过firebug直接查询
- 一般不推荐使用绝对路径的写法,因为一旦页面结构发生变化,该路径也随之失效,必须重新写。
- 绝对路径以
/
表示,相对路径以//
表示。另外,还需要知道:
① 当xpath的路径以/
开头时,表示让Xpath解析引擎从文档的根节点开始解析;当xpath路径以//
开头时,则表示让xpath引擎从文档的任意符合的元素节点开始进行解析。
② 当/
出现在xpath路径中时,则表示寻找父节点的直接子节点;当//
出现在xpath路径中时,表示寻找父节点下任意符合条件的子节点,不管嵌套了多少层级。弄清这个原则,就可以理解其实xpath的路径可以绝对路径和相对路径混合在一起来进行表示,想怎么玩就怎么玩。
使用相对路径:
- 查找页面根元素:
//
- 查找页面上所有的input元素:
//input
- 查找页面上第一个form元素内的直接子input元素(即只包括form元素的下一级input元素,使用绝对路径表示,单/号):
//form[1]/input
- 查找页面上第一个form元素内的所有子input元素(只要在form元素内的input都算,不管还嵌套了多少个其他标签,使用相对路径表示,双//号):
//form[1]//input
- 查找页面上第一个form元素:
//form[1]
- 查找页面上id为loginForm的form元素:
//form[@id='loginForm']
- 查找页面上具有name属性为username的input元素:
//input[@name='username']
- 查找页面上id为loginForm的form元素下的第一个input元素:
//form[@id='loginForm']/input[1]
- 查找页面具有name属性为contiune并且type属性为button的input元素:
//input[@name='continue'][@type='button']
- 查找页面上id为loginForm的form元素下第4个input元素:
//form[@id='loginForm']/input[4]
实例:
如果我们现在要引用id为“J_password”的input元素(如上图。不用看清此图,只需要知道我们要引用的部分位于HTML代码很深的层次中),则可以像下面这样写:
//*[@id='J_login_form']/dl/dt/input[@id='J_password']
或
//*[@id='J_login_form']/*/*/input[@id='J_password']
# 用*号省略具体的标签名称,但元素的层级关系必须体现出来
解释:
//*[@id=’ J_login_form’]
表示在根元素下查找任意id为J_login_form
的标签,然后经过dl、dt层才能找到指定元素。第二种方式中用*号省略了具体的标签名称(但元素的层级关系必须体现出来)。
前面讲的都是xpath中基于准确元素属性的定位,其实xpath作为定位神器也可以用于模糊匹配。本次实战,可以进行准确元素定位,因此就不讲模糊匹配了。如果有兴趣,可以自行了解。
实战:爬取百度文库Word文章
页面切换
请直接查看讲解网页中的实战部分。
讲解时使用的网页与现在有所不同了…根据原理,现在我们要找到的页面上的元素应该是:
通过xpath准确查找元素位置:
continue_read = driver.find_elements_by_xpath("//div[@class='foldpagewg-text']")
内容爬取
整体代码
requests库
介绍
requests库不是Python3内置的urllib.request库,而是一个强大的基于urllib3的第三方库。
Urllib3(官网:https://urllib3.readthedocs.io/en/latest/index.html)是一个功能强大、条理清晰、用于HTTP客户端的Python库,因此requests还可以说是一个基于HTTP协议来使用网络的第三方库。
requests库的官方网站中有这样的一句介绍它的话:“Requests是唯一的一个非转基因的Python HTTP库,人类可以安全享用。”简单的说,使用requests库可以非常方便的使用HTTP,避免安全缺陷、冗余代码以及“重复发明轮子”(行业黑话;通常用在软件工程领域表示重新创造一个已有的或是早已被优化過的基本方法)。
requests库的基础方法如下:
官方中文教程URL:http://docs.python-requests.org/zh_CN/latest/user/quickstart.html
请自行学习.
对requests.get() 方法进行简单的介绍:
- 参数:网页的URL
- 返回值:一个Response对象(Response是requests库中的一个接口/类【官网:http://cn.python-requests.org/zh_CN/latest/api.html#requests.Response】,它包含对一个HTTP请求者的服务器给予的回应,即response);我们可以从这个对象中(如下面示例中的r对象)获取所有我们想要的关于被请求的网页的信息
- 作用:获取某个网页的HTML代码 / 获取网络资源
- 示例:获取 Github 的公共时间线
import requests
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030') #改变标准输出的默认编码
r = requests.get('https://api.github.com/events')
# print(type(r)) # 结果:<class 'requests.models.Response'>
print(r.text) # 读取服务器响应的内容(HTML代码)
实例1:访问网络API获取国内新闻
下面的例子演示了如何使用requests模块(封装得足够好的第三方网络访问模块)访问网络API获取国内新闻
以及如何通过json模块解析JSON数据并显示新闻标题
。这个例子使用了天行数据提供的国内新闻数据接口,其中的APIKey需要自己先到该网站进行注册,然后就会获得一个APIKey,然后用申请到的内容替换代码中网址的“APIKey”字段(我已经进行了申请,获得APIKey是‘82bc97e2f2e2e560dd8ddcce55b93c0b’;代码中已经做了替换)。
import requests
import json
import urllib3
def main():
resp = requests.get('http://api.tianapi.com/guonei/?key=82bc97e2f2e2e560dd8ddcce55b93c0b&num=10')
# 将字符串的内容反序列化成Python对象
data_model = json.loads(resp.text)
for news in data_model['newslist']: # ?取出data_model['newslist']对应的值后直接打印不就行了,怎么还循环打印呢
print(news['title'])
if __name__ == '__main__':
main()
json模块的核心函数:
- dump - 将Python对象按照JSON格式序列化到文件中
- dumps - 将Python对象处理成JSON格式的字符串
- load - 将文件中的JSON数据反序列化成对象
- loads - 将字符串的内容反序列化成Python对象
(关于序列化与反序列化请查看这里的“读写JSON文件”一节)。
实例2:访问网络数据接口,下载美女图片到本地
下面通过requests
来实现访问网络数据接口并从中获取美女图片下载链接然后下载美女图片到本地
这一功能,程序中使用了天行数据提供的网络API。
什么是网络API?
- 网络API接口就是各种大公司对外提供的一种各种信息和数据获取的接口。像百度、腾讯、阿里巴巴等都提供这种接口,一些小公司和个人就可以通过这些接口获取各种信息,如城市天气信息、生成二维码、地图信息、手机号码归属地、摄像头、卫星定位等。这些小公司就可以通过付费的方式使用这些接口,从而做出像天气预报查询,地图导航等一系列的APP。
- 对于我们学习人员来讲,这些接口大部分都有免费使用次数,足够我们学习使用。
import requests
from time import time
from threading import Thread
# 继承Thread类创建自定义的线程类
class DownloadHanlder(Thread):
def __init__(self, url):
super().__init__()
self.url = url
def run(self):
filename = self.url[self.url.rfind('/') + 1:]
#print(filename)
resp = requests.get(self.url)
with open('E:/sublime_python/pics' + filename, 'wb') as f:
f.write(resp.content)
def main():
resp = requests.get('http://api.tianapi.com/meinv/?key=82bc97e2f2e2e560dd8ddcce55b93c0b&num=10')
# 将服务器返回的JSON格式的数据解析为字典(Requests中有一个内置的 JSON 解码器——json()方法,助你处理 JSON 数据)
data_model = resp.json()
for mm_dict in data_model['newslist']: # ????
url = mm_dict['picUrl']
# 通过多线程的方式实现图片下载
DownloadHanlder(url).start()
print()
print('完成')
if __name__ == '__main__':
main()
实例3:访问原网页,下载帅哥图片
爬取单页目标连接
目标URL:http://www.shuaia.net/e/tags/index.php?page=0&tagname=帅哥&line=25&tempid=3
审查元素:可以看到,下图中有两个图片地址,我们应该选择的是< a>标签中的src,因为< img>标签中的src是这个图的缩略图,即显式在此网页上的图,而是< a>标签中的src图片地址是点击缩略图后显示出来的高清图片的地址。
代码如下:
import requests
from bs4 import BeautifulSoup
import sys, io
sys.stdout = io.TextIOWrapper(sys.stdout.buffer,encoding='gb18030') #改变标准输出的默认编码
if __name__ == "__main__":
url = r'http://www.shuaia.net/e/tags/index.php?page=0&tagname=帅哥&line=25&tempid=3'
headers = {
"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
}
response = requests.get(url=url, headers=headers) # Requests 不会基于定制 header 的具体情况改变自己的行为。只不过在最后的请求中,所有的 header 信息都会被传递进去。
response.encoding = 'utf-8' # 指定解析网页时使用的编码方式
html = response.text # # 读取服务器响应的内容(HTML代码)
bf = BeautifulSoup(html, 'lxml')
targets_url = bf.find_all(class_='item-img')
list_url = []
for each in targets_url:
list_url.append(each.img.get('alt') + ' = ' + each.get('href'))
print(list_url)
爬取多页目标连接
对于第一页的网址http://www.shuaia.net/e/tags/index.php?page=0&tagname=帅哥&line=25&tempid=3,发现每翻到下一页,page的值就加1。由此,代码如下:
from bs4 import BeautifulSoup
import requests
if __name__ == "__main__":
list_url = []
for num in range(0, 6): # 爬取前5页内容
url = 'http://www.shuaia.net/e/tags/index.php?page=%d&tagname=帅哥&line=25&tempid=3' % num
headers = {
"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
}
response = requests.get(url=url, headers=headers)
response.encoding = 'utf-8'
html = response.text
bf = BeautifulSoup(html, 'lxml')
targets_url = bf.find_all(class_='item-img')
for each_target in targets_url:
list_url.append(each_target.img.get('alt') + ' = ' + each_target.get('href'))
print(list_url)
单张图片下载
审查元素:可以看到,我们只能将元素定位到class为‘wr-single-content-list’的< div>标签上,然后再进一步得到图片的URL。
代码如下:
from bs4 import BeautifulSoup
import requests
import os
from urllib.request import urlretrieve
filename = '亚运会泳坛帅哥宁泽涛' + '.jpg'
target_url = 'http://www.shuaia.net/shuaige/2018-08-14/15055.html'
headers = {
"User-Agent":"Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36"
}
img_res = requests.get(url = target_url,headers = headers)
img_res.encoding = 'utf-8'
img_html = img_res.text
img_bf_1 = BeautifulSoup(img_html, 'lxml')
img_url = img_bf_1.find_all('div', class_='wr-single-content-list')
img_bf_2 = BeautifulSoup(str(img_url), 'lxml')
img_url = 'http://www.shuaia.net' + img_bf_2.div.img.get('src')
if 'images' not in os.listdir(): # 创建images文件夹,用来保存下载的图片
os.makedirs('images')
urlretrieve(url = img_url, filename = 'images/' + filename)
print('下载完成!')
完整代码(多张下载)
让爬虫程序像用户行为的几种见解
参考:https://blog.csdn.net/c406495762/article/details/72793480
构造合理的HTTP请求头
HTTP 请求头是每次向网络服务器发送请求时,传递的一组属性和配置信息。
HTTP 定义了十几种古怪的请求头类型,不过大多数都不常用。
HTTP请求头中的Cookie信息是最重要的请求头信息之一。
有学问的设置cookie
虽然 cookie 是一把双刃剑,但正确地处理 cookie 可以避免许多采集问题。网站会用 cookie 跟踪你的访问过程,如果发现了爬虫异常行为就会中断你的访问,比如特别快速地填写表单,或者浏览大量页面。虽然这些行为可以通过关闭并重新连接或者改变 IP 地址来伪装,但是如果 cookie 暴露了你的身份,再多努力也是白费。
在采集一些网站时 cookie 是不可或缺的。如果要在一个网站上持续保持自己的登录状态,那么就需要在这个页面中保存一个 cookie;只要保存一个旧的“已登录”的 cookie 就可以访问一个网站,而不是每次登录时都获得一个新 cookie。
如果你在采集一个或者几个目标网站,建议你检查这些网站生成的 cookie,然后想想哪一个 cookie 是爬虫需要处理的。有一些浏览器插件可以为你显示访问网站和离开网站时 cookie 是如何设置的。例如:EditThisCookie,该插件可以谷歌商店进行下载。URL:http://www.editthiscookie.com/
requests库中实现了定制请求头的功能,也就是说,我们可以根据需要来指定Cookie信息。示例:
import requests
if __name__ == '__main__':
url = 'https://www.sougo.com/'
headers = {
'Upgrade-Insecure-Requests':'1',
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Encoding':'gzip, deflate, sdch, br',
'Accept-Language':'zh-CN,zh;q=0.8',
}
# 建立一个Session会话对象,然后通过get()获取网络资源,进而获取cookie
s = requests.Session()
response = s.get(url=url, headers=headers)
print(s.cookies)
关于 会话对象:
- 会话对象是requests库的一个高级用法(参见:https://2.python-requests.org//zh_CN/latest/user/advanced.html)。
- 会话对象让你能够跨请求保持某些参数。它也会在同一个 Session 实例发出的所有请求之间保持 cookie, 期间使用 urllib3 的 connection pooling 功能。所以如果你向同一主机发送多个请求,底层的 TCP 连接将会被重用,从而带来显著的性能提升。 【上面的例子实现的功能:跨请求保持一些cookie】
- 会话对象具有主要的 Requests API 的所有方法。
requests模块对于对于解决上面问题存在的缺陷:
requests 模块不能执行 JavaScript,这使得它不能处理很多新式的跟踪软件生成的 cookie,比如 Google Analytics——只有当客户端脚本执行后才设置 cookie(或者在用户浏览页面时基于网页事件产生 cookie,比如点击按钮)。要处理这些动作,需要用 Selenium 和 PhantomJS 包。Selenium 已介绍过,下面我们来学习一下 PhantomJS 包。
PhantomJS 是一个“无头”浏览器(A Headless Web Browser)。它会把网站加载到内存并执行页面上的 JavaScript,但不会向用户展示网页的图形界面。将 Selenium 和 PhantomJS 结合在一起,就可以运行一个非常强大的网络爬虫了,可以处理 cookie、JavaScript、headers,以及任何你需要做的事情。
PhantomJS可以依据自己的开发平台选择不同的包进行下载:http://phantomjs.org/download.html 解压即用,很方便。
实例:调用selenium
模块中的webdriver
的get_cookie()
方法来查看cookie.
- 原讲解中的例子是这样的:
# -*- coding:UTF-8 -*-
from selenium import webdriver
if __name__ == '__main__':
url = 'http://pythonscraping.com'
driver = webdriver.PhantomJS(executable_path='D:/python_/phantomjs-2.1.1-windows/bin/phantomjs.exe')
driver.get(url)
driver.implicitly_wait(1)
print(driver.get_cookies())
这样就可以获得一个非常典型的 Google Analytics 的 cookie 列表:
还可以调用 delete_cookie()、add_cookie() 和 delete_all_cookies() 方法来处理 cookie。另外,还可以保存 cookie 以备其他网络爬虫使用。
通过Selenium和PhantomJS,我们可以很好的处理需要事件执行后才能获得的cookie。
正常的访问速度
注意隐含输入字段
在 HTML 表单中,“隐含”字段可以让字段的值对浏览器可见,但是对用户不可见(除非看网页源代码)。随着越来越多的网站开始用 cookie 存储状态变量来管理用户状态,在找到另一个最佳用途之前,隐含字段主要用于阻止爬虫操作能自动提交表单。
下图显示的例子就是 Facebook 登录页面上的隐含字段。虽然表单里只有三个可见字段(username、password 和一个确认按钮),但是在源代码里表单会向服务器传送大量的信息。
隐含字段可以阻止网络数据采集,即组织爬虫行为。具体的阻止方式有两种,下面进行介绍。
第一种方式:由于表单页面上的一个字段可以用服务器生成的随机变量表示,所以如果提交时这个随机量的值不属于表单页面生成的随机变量,那么服务器就有理由认为这个提交不是从原始表单页面上提交的,而是由一个网络机器人直接提交到表单处理页面的。【绕开这个问题的最佳方法就是,首先采集表单所在页面上生成的随机变量,然后再提交到表单处理页面。】
第二种方式是“蜜罐”(honey pot)。如果表单里包含一个具有普通名称的隐含字段(设置蜜罐圈套),比如“用户名”(username)或“邮箱地址”(email address),设计不太好的网络机器人往往不管这个字段是不是对用户可见,直接填写这个字段并向服务器提交,这样就会中服务器的蜜罐圈套。然后服务器会把所有隐含字段的真实值(或者与表单提交页面的默认值不同的值)都忽略;此外,填写隐含字段的访问用户(爬虫机器人)也可能被网站封杀。
总之,有时检查表单所在的页面十分必要,看看有没有遗漏或弄错一些服务器预先设定好的隐含字段(蜜罐圈套)。如果你看到一些隐含字段,通常带有较大的随机字符串变量,那么很可能网络服务器会在表单提交的时候检查它们。另外,还有其他一些检查,用来保证这些当前生成的表单变量只被使用一次或是最近生成的(这样可以避免变量被简单地存储到一个程序中反复使用)。
学会避开蜜罐
类似于上一知识点提到的表单中的隐含字段(事实上,隐含字段不仅可以应用在网站的表单上,还可以应用在链接、图片、文件,以及一些可以被机器人读取,但普通用户在浏览器上却看不到的任何内容上面),访问者如果访问了网站上的一个“隐含”内容,就会触发服务器脚本封杀这个用户的 IP 地址,把这个用户踢出网站,或者采取其他措施禁止这个用户接入网站。实际上,许多商业模式就是在干这些事情。请看下面的例子。
打开网页http://pythonscraping.com/pages/itsatrap.html,审查其元素,我们可以发现该网页上包含了三个隐藏元素:
- 第一个链接通过简单的 CSS 属性设置 display:none 进行隐藏;
- 电话号码字段 name=”phone” 是一个隐含的输入字段;
- 邮箱地址字段 name=”email” 是将元素向右移动 50000 像素(将超出电脑显示器的边界)并隐藏滚动条。
由于隐含字段的存在,我们要想更好的进行爬虫操作,就需要采取一定的措施。而Python中的Selenium模块就可以让我们避开蜜罐,因为它可以获取访问页面的内容,从而区分页面上的可见元素与隐含元素——通过is_displayed()
可以判断元素在页面上是否可见。下面的实例就是获取上面讲解的页面的内容,然后查找隐含链接和隐含输入字段。
from selenium import webdriver
if __name__ == '__main__':
url = 'http://pythonscraping.com/pages/itsatrap.html'
driver = webdriver.PhantomJS(executable_path='D:/python_/phantomjs-2.1.1-windows/bin/phantomjs.exe')
driver.get(url)
links = driver.find_elements_by_tag_name('a')
for link in links:
if not link.is_displayed():
print('链接: ' + link.get_attribute('href') + '是一个蜜罐圈套.')
fields = driver.find_elements_by_tag_name('input')
for field in fields:
if not field.is_displayed():
print('不要改变' + field.get_attribute('name') + '的值!')
Selenium 抓取出了每个隐含的链接和字段,结果如下所示:
虽然你不太可能会去访问你找到的那些隐含链接,但是在提交前,记得确认一下那些已经在表单中、准备提交的隐含字段的值(或者让 Selenium 为你自动提交)。
创建自己的代理IP池
如果一个IP访问速度超过这个阈值,那么网站就会认为,这是一个爬虫程序,而不是用户行为。在这种情况下,想加快爬取速度,但又要避免远程服务器封锁自己的IP,一个可行的方法就是使用代理IP,我们需要做的就是创建一个自己的代理IP池。
实例:使用lxml的xpath和Beutifulsoup结合的方法,爬取国内高匿代理网页中的IP(注意:千万不要爬太快!很容易被服务器Block哦!)。
- 思路:通过免费IP代理网站爬取IP,构建一个容量为100的代理IP池。从代理IP池中随机选取IP,在使用IP之前,检查IP是否可用。如果可用,使用该IP访问目标页面,如果不可用,舍弃该IP。当代理IP池中IP的数量小于20的时候,更新整个代理IP池,即重新从免费IP代理网站爬取IP,构建一个新的容量为100的代理IP池。
- 审查元素:
页面中的IP部分位于HTML中id为‘ip_list’的< div>内的< tr>内的第二个< td>内。 - 代码:
# -*- coding:UTF-8 -*-
import requests
from bs4 import BeautifulSoup
from lxml import etree
if __name__ == '__main__':
# 这里我们只爬取一页的IP就够了,如果想要爬取更多,遍历page即可
page = 1
target_url = 'http://www.xicidaili.com/nn/%d' % page
target_headers = {
'Upgrade-Insecure-Requests':'1',
'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36',
'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Referer':'http://www.xicidaili.com/nn/',
'Accept-Encoding':'gzip, deflate, sdch',
'Accept-Language':'zh-CN,zh;q=0.8',
}
#requests的Session可以自动保持cookie,不需要自己维护cookie内容
S = requests.Session()
target_response = S.get(url=target_url, headers=target_headers)
target_response.encoding = 'utf-8'
target_html = target_response.text
bf1_ip_list = BeautifulSoup(target_html, 'lxml')
bf2_ip_list = BeautifulSoup(str(bf1_ip_list.find_all(id='ip_list')), 'lxml')
ip_list_info = bf2_ip_list.table.contents
proxys_list = []
for index in range(len(ip_list_info)):
if index % 2 == 1 and index != 1: # 按此方式只选择页面上的100个IP
dom = etree.HTML(str(ip_list_info[index])) #??HTML()方法的功能?
ip = dom.xpath('//td[2]')
port = dom.xpath('//td[3]')
protocol = dom.xpath('//td[6]')
proxys_list.append(protocol[0].text.lower() + '#' + ip[0].text + '#' + port[0].text)
print(proxys_list)
#print(len(proxys_list)) # 100
- 部分运行截图:
可以看到,通过这种方法,很容易的就获得了这100个IP,包括他们的协议、IP和端口号。这里我是用”#”符号隔开,使用之前,只需要spilt()方法,就可以提取出相应的信息。 - 验证IP是否可用:从免费代理网站获得的代理IP很不稳定,所以我们获取到IP后需要对其进行验证。一种方案是GET请求一个网页,设置timeout超市时间,如果超时服务器没有反应,说明IP不可用。实现代码可以参见Requests的高级用法:http://docs.python-requests.org/zh_CN/latest/user/advanced.html,也可直接看下图。
设置timeout的验证方法是一种常见的方法,很多人都这样验证。原博主又想了其他方法,下面进行介绍。
- 原理:使用ping命令测试本机和代理IP地址的连通性。此方式使用Python实现,则是
Subprocess.Popen()
函数。Subprocess.Popen()
可以创建一个进程,当shell参数为true时,程序通过shell来执行。
- 实例:ping本机回环地址
import subprocess as sp if __name__ == '__main__': # 保存命令 cmd = "ping -n 3 -w 3 127.0.0.1" # 执行命令 p = sp.Popen(cmd, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE, shell=True) # 获得返回结果并解码 out = p.stdout.read().decode("gbk") # 打印结果 print(out)
- 应用到本节内容中:制定相应的规则(这里指定的规则是:如果丢包数大于2个,则认为IP不能用;ping通的平均时间大于200ms也抛弃),根据返回信息来剔除不满足要求的IP
from bs4 import BeautifulSoup import subprocess as sp from lxml import etree import requests import random import re """ 函数说明:获取IP代理 Parameters: page - 高匿代理页数,默认获取第一页 Returns: proxys_list - 代理列表 Modify: 2017-05-27 """ def get_proxys(page = 1): #requests的Session可以自动保持cookie,不需要自己维护cookie内容 S = requests.Session() #西祠代理高匿IP地址 target_url = 'http://www.xicidaili.com/nn/%d' % page #完善的headers target_headers = { 'Upgrade-Insecure-Requests':'1', 'User-Agent':'Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36', 'Accept':'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8', 'Referer':'http://www.xicidaili.com/nn/', 'Accept-Encoding':'gzip, deflate, sdch', 'Accept-Language':'zh-CN,zh;q=0.8', } #get请求 target_response = S.get(url = target_url, headers = target_headers) #utf-8编码 target_response.encoding = 'utf-8' #获取网页信息 target_html = target_response.text #获取id为ip_list的table bf1_ip_list = BeautifulSoup(target_html, 'lxml') bf2_ip_list = BeautifulSoup(str(bf1_ip_list.find_all(id = 'ip_list')), 'lxml') ip_list_info = bf2_ip_list.table.contents #存储代理的列表 proxys_list = [] #爬取每个代理信息 for index in range(len(ip_list_info)): if index % 2 == 1 and index != 1: dom = etree.HTML(str(ip_list_info[index])) ip = dom.xpath('//td[2]') port = dom.xpath('//td[3]') protocol = dom.xpath('//td[6]') proxys_list.append(protocol[0].text.lower() + '#' + ip[0].text + '#' + port[0].text) #返回代理列表 return proxys_list """ 函数说明:检查代理IP的连通性 Parameters: ip - 代理的ip地址 lose_time - 匹配丢包数 waste_time - 匹配平均时间 Returns: average_time - 代理ip平均耗时 Modify: 2017-05-27 """ def check_ip(ip, lose_time, waste_time): #保存命令【-n 要发送的回显请求数 -w 等待每次回复的超时时间(毫秒)】 cmd = "ping -n 3 -w 3 %s" #执行命令 p = sp.Popen(cmd % ip, stdin=sp.PIPE, stdout=sp.PIPE, stderr=sp.PIPE, shell=True) #获得返回结果并解码 out = p.stdout.read().decode("gbk") #从返回结果中匹配正则表达式,得到丢包数 lose_time = lose_time.findall(out) #当匹配到丢失包信息失败,默认为三次请求全部丢包,丢包数lose赋值为3 if len(lose_time) == 0: lose = 3 else: lose = int(lose_time[0]) if lose > 2: #如果丢包数目大于2个,则认为连接超时,返回平均耗时1000ms return 1000 else: #如果丢包数目小于等于2个,获取平均耗时的时间 #从返回结果中匹配正则表达式,得到平均时间 average = waste_time.findall(out) #当匹配耗时时间信息失败,默认三次请求严重超时,返回平均耗时1000ms if len(average) == 0: return 1000 else: average_time = int(average[0]) #返回匹配到的平均耗时的值 return average_time """ 函数说明:初始化正则表达式 Parameters: 无 Returns: lose_time - 匹配丢包数 waste_time - 匹配平均时间 Modify: 2017-05-27 """ def initpattern(): #匹配丢包数 lose_time = re.compile(r"丢失 = (\d+)", re.IGNORECASE) #匹配平均时间 waste_time = re.compile(r"平均 = (\d+)ms", re.IGNORECASE) return lose_time, waste_time if __name__ == '__main__': #初始化正则表达式 lose_time, waste_time = initpattern() #获取IP代理 proxys_list = get_proxys(1) #如果平均时间超过200ms重新选取ip while True: #从100个IP中随机选取一个IP作为代理进行访问 proxy = random.choice(proxys_list) split_proxy = proxy.split('#') # 通过“#”符进行切割,结果保存在split_proxy中 #获取IP ip = split_proxy[1] #检查ip average_time = check_ip(ip, lose_time, waste_time) if average_time > 200: #去掉不能使用的IP proxys_list.remove(proxy) print("ip连接超时, 重新获取中...") if average_time < 200: break #去掉已经使用的IP proxys_list.remove(proxy) proxy_dict = {split_proxy[0]:split_proxy[1] + ':' + split_proxy[2]} print("使用代理:", proxy_dict)
- 总结:
这里只是实现了构建代理IP池和检查IP是否可用,如果你感兴趣也可以将获取的IP放入到数据库中,不过这里没这样做,因为免费获取的代理IP,失效很快,随用随取就行。
除此之外,我们也可以个创建一个User-Agent的列表,多罗列点。也是跟代理IP一样,每次访问随机选取一个。这样在一定程度上,也能避免被服务器封杀。 当然,也可以自己写代码试试reqeusts的GET请求,通过设置timeout参数来验证代理IP是否可用,因为方法简单,所以在此不再累述。