作者:高玉涵
时间:2022.5.20 16:47
博客:blog.csdn.net/cg_i
助力每一个不知死活的梦想。
网络数据采集并不是一门语言的独门秘籍,Python、Java、PHP、C#、Go 等语言都可以讲出精彩的故事。有人说编程语言就是宗教,不同语言的设计哲学不同,行为方式各异,“非我族类,其心必异”,但本着美好生活、快乐修行的初衷,我对所有语言都时刻保持敬畏之心,尊重信仰自由,努力做好自己的功课。对爱好 Python 的人来说,人生苦短,Python 当歌!简洁轻松的语法,开箱即用的模块,强大快乐的社区,总可以快速构建出简单高效的解决方案。使用 Python 的日子总是充满快乐的,本人使用 Python 网络数据采集的故事也不例外。本着独乐乐,不如众乐乐,遂想不如将这次学习经历分享。鉴于网络数据采集涉及多个领域、内容包罗万象,就此一篇远不足以覆盖所有主题,与其如此,倒不如,做一个专栏,以一个个例子程序开始,通过思考和解决问题,一层层将主题展开。考虑到这是我的一次全新尝试,也只是利用工作之余闲暇时间,边学习边整理,能持否?就用一句话来自勉”但行好事,莫问前程“。
在大数据深入人心的时代,网络数据采集作为网络、数据库与机器学习等领域的交汇点,已经成为满足个性化网络数据需求的最佳实践。搜索引擎可以满足人们对数据的共性需求,即“我来了,我看见”,而网络数据采集技术可以进一步精练数据,把网络中杂乱无意的数据聚合成合理规范的形式,方便分析与挖掘,真正实现“我征服”。工作中,你可能经常为找数据而烦恼,或者眼睁睁看着眼前的几百页数据却只能长恨咫尺天涯,又或者数据杂乱无章的网站中满是带着陷阱的表单和坑爹的验证码,甚至需要的数据都在网页版的 PDF 和网络图片中。而作为一名网站管理员,你也需要了解常用的网络数据采集手段,以及常用的网络表单安全措施,以提高网站访问的安全性,所谓道高一尺,魔高一丈······一念清净,烈焰成池,一念觉醒,方登彼岸。
一旦你开始网络数据采集,就会感受到浏览器为我们做的所有细节。网络上如果没有 HTML 文本格式层、CSS 样式层、JavaScript 执行层和图像渲染层,乍看起来会有点儿吓人,但是在这里,我将介绍如何不通过浏览器的帮助来格式化和理解数据。我的任务是爬取 https://free.kuaidaili.com/free/inha/ 这个页面里所有代理服务器的信息,思考之下通常的想法:
我将围绕上述思考来学习并编写爬虫程序。
“美味的汤,绿色的浓汤,
在热气腾腾的盖碗里装!
谁不愿意尝一尝,这样的好汤?
晚餐用的汤,美味的汤!”
BeautifulSoup 库的名字取自刘易斯·卡罗尔在《爱丽丝梦游仙境》里的同名诗歌。
以下内容引用自《Beautiful Soup 4.4.0 文档》
Beautiful Soup 是一个可以从 HTML 或 XML 文件中提取数据的 Python 库.它能够通过你喜欢的转换器实现惯用的文档导航,查找,修改文档的方式. Beautiful Soup 会帮你节省数小时甚至数天的工作时间.就像它在仙境中的说法一样,BeautifulSoup 尝试化平淡为神奇。它通过定位 HTML 标签来格式化和组织复杂的网络信息,用简单易用的 Python 对象为我们展现 XML 结构信息。
由于 BeautifulSoup 库不是 Python 标准库,因此需要单独安装。我使用最新的 BeautifulSoup 4 版本(也叫 BS4)。BeautifulSoup 4 的所有安装方法都可在官方文档里面找到,这里就不再过多赘述。另外,注意所有例子只在 Python 3.x 下运行。
BeautifulSoup 库最常用的对象恰好就是 BeautifulSoup 对象。
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("http://www.baidu.com")
bsObj = BeautifulSoup(html.read())
print(bsObj.title)
输出结果是:
百度一下,你就知道
代码第一行的含义:它查找 Python 的 request 模块(在 urllib 库里面),只导入一个 urlopen 函数。urllib 是 python 的标准库(就是说你不用额外安装就可以运行这个例子),包含了从网络请求数据,处理 cookie,甚至改变请求头和用户代理这些元数据的函数。网络采集中将广泛使用 urllib,所以建议你读读这个库的 Python 文档(https://docs.python.org/3/library/urllib.html)。
urlopen 用来打开并读取一个从网络获取的远程对象。因为它是一个非常通用的库(它可以轻松读取 HTML 文件、图像文件,或其他任何文件流),所以我将频繁地使用它。然后调用 html.read() 获取网页中的 HTML 内容。这样就可以把 HTML 内容传到 BeautifulSoup 对象,转换成下面的结构:
html -> ……
---- head -> 百度一下,你就知道
-------- title -> 百度一下,你就知道
可以看出,我们从网页中提取的
标签被嵌在 BeautifulSoup 对象 bsObj 结构的第二层(html -> head -> title)。但是,当我从对象里提取 title 标签的时候,可以直接调用它:
bsObj.title
其实,下面的所有函数调用都可以产生同样的结果:
bsObj.html.head.title
bsObj.head.title
bsObj.html.title
希望这个例子可以向你展示 BeautifulSoup 库的强大与简单。其实,任何 HTML(或 XML)文件的任意节点信息都可以被提取出来,只要目标信息的旁边或附近有标记就行。
当我们从复杂的网页中寻觅信息时,有很多技巧可以帮我们“剔去”网页上那些不需要的信息。从中抽取出我们需要的信息。下面我创建一个网络采集程序来抓取 https://free.kuaidaili.com/free/inha/ 这个网页。
在这个页面里,所有免费代理信息内容都放在一个表格里,你可以看到网页源代码里的 table 标签,如下所示:
IP
PORT
匿名度
类型
位置
响应速度
最后验证时间
103.37.141.69
80
高匿名
HTTP
中国 北京
6秒
2022-05-20 14:31:01
103.37.141.69
80
高匿名
HTTP
中国 北京
6秒
2022-05-20 13:31:01
103.37.141.69
80
高匿名
HTTP
中国 北京
6秒
2022-05-20 12:31:01
122.9.101.6
8888
高匿名
HTTP
中国 香港 666666999999999.com
0.4秒
2022-05-20 11:31:01
106.15.197.250
8001
高匿名
HTTP
中国 上海 联通
1秒
2022-05-20 10:31:02
14.215.212.37
9168
高匿名
HTTP
中国 广东 东莞 电信
4秒
2022-05-20 09:31:01
103.37.141.69
80
高匿名
HTTP
中国 北京
6秒
2022-05-20 08:31:01
39.106.71.115
7890
高匿名
HTTP
中国 北京 联通
0.3秒
2022-05-20 07:31:01
183.236.232.160
8080
高匿名
HTTP
中国 广东 江门 移动
6秒
2022-05-20 06:31:01
202.55.5.209
8090
高匿名
HTTP
中国 香港 电信
0.3秒
2022-05-20 05:31:01
118.180.166.195
8060
高匿名
HTTP
甘肃省庆阳市 电信
0.4秒
2022-05-20 04:31:01
121.13.252.61
41564
高匿名
HTTP
广东省东莞市 电信
2秒
2022-05-20 03:31:01
111.3.118.247
30001
高匿名
HTTP
中国 浙江 台州 移动
0.2秒
2022-05-20 02:31:02
61.216.185.88
60808
高匿名
HTTP
中国 台湾 屏东县
0.7秒
2022-05-20 01:31:01
202.55.5.209
8090
高匿名
HTTP
中国 香港 电信
0.3秒
2022-05-20 00:31:01
我们可以抓出整个页面,然后创建一个 BeautifulSoup 对象,和一开始使用的程序类似:
from urllib.request import urlopen
from bs4 import BeautifulSoup
html = urlopen("https://free.kuaidaili.com/free/inha/")
bsObj = BeautifulSoup(html)
通过 BeautifulSoup 对象,我们可以用 findAll 函数抽取只包含在
标签里的文字,这样就会得到 IP (findAll 是一个非常灵活的函数,后面会经常用到它):
nameList = bsObj.findAll("td", {"data-title":"IP"})
for name in nameList:
print(name.get_text())
输出结果:
103.37.141.69
103.37.141.69
103.37.141.69
122.9.101.6
106.15.197.250
14.215.212.37
103.37.141.69
39.106.71.115
183.236.232.160
202.55.5.209
118.180.166.195
121.13.252.61
111.3.118.247
61.216.185.88
202.55.5.209
代码执行以后就会按照表格中的顺序显示所有的 IP。这是怎么实现的呢?之前,我调用 bsObj.tagName 只能获取页面中的第一个指定的标签。现在,我调用 bsObj.findAll(tagName, tgaAttributes) 可以获取页面中所有指定的标签,不再只是第一个了。
获取 IP 列表之后,程序遍历列表中所有的名字,然后打印 name.get_text(),就可以把标签中的内容分开显示了。
BeautifulSoup 的 find() 和 findAll() 可能是最常用的两个函数。借助它们,你可以通过标签的不同属性轻松地过滤 HTML 页面,查找需要的标签组或单个标签。
这两个函数非常相似,BeautifulSoup 文档里两者的定义是这样的:
findAll(tag, attributes, recursive, text, limit, keywords)
find(tag, attributes, recursive, text, keywords)
很可能你会发现,自己在 95% 的时间里都只需要使用前两个参数:tag 和 attributes。但是,我们还是应该仔细地观察所有的参数。
标签参数 tag 前面已经介绍过——你可以传一个标签的名称或多个标签名称组成的 Python 列表做标签参数。例如,下面的代码将返回一个包含 HTML 文档中所有标题标签的列表:
findAll({"h1","h2","h3","h4","h5","h6"})
属性参数 attributes 是用一个 Python 字典封装一个标签的若干属性对应的属性值。例如,下面这个函数会返回 HTML 文档里红色与绿色两种颜色的 span 标签:
findAll("span", {"class":{"green", "red"}})
递归参数 recursive 是一个布尔变量。你想抓取 HTML 文档标签结构里多少层的信息?如果 recursive 设置为 True,findAll 就会根据你的要求去查找标签参数的所有子标签,以及子标签的子标签。如果 recursive 设置为 False,findAll 就只查找文档的一级标签。findAll 默认是支持递归查找的(recursive 默认值是 True);一般情况下这个参数不需要设置,除非你真正了解自己需要哪些信息,而且抓取速度非常重要,那时你可以设置递归参数。
文本参数 text 有点不同,它是用标签的文本内容去匹配,而不是用标签的属性。假如我们想查找前面网页中类型为“HTTP”的标签数量,我们可以把之前的 findAll 方法换成下面的代码:
nameList = bsObj.findAll(text="HTTP")
print(len(nameList))
输出结果为“15”。
范围限制参数 limit,显然只用于 findAll 方法。find 其实等价于 findAll 的 limit 等于 1 时的情形。如果你只对网页中获取的前 X 项结果感兴趣,就可以设置它。但是要注意,这个参数设置之后,获得的前几项结果是按照网页上的顺序排序的,未必是你想要的那前几项。
还有一个关键词参数 keyword,可以让你选择那些具有指定属性的标签。例如:
allText = bsObj.findAll(id="text")
print(allText[0].get_text())
虽然关键词参数 keyword 在一些场景中很有用,但是,它是 BeautifulSoup 在技术上做的一个冗余功能。任何用关键词参数能完成的任务,同样可以用下面将介绍的技术解决。例如,下面两行代码是完全一样的:
bsObj.findAll(id="text")
bsObj.findAll("", {"id":"text"})
另外,用 keyword 偶乐会出现问题,尤其是在用 class 属性查找标签的时候,因为 class 是 Python 中受保护的关键字。也就是说,class 是 Python 语言的保留字,在 Python 程序里是不能当作变量或参数名使用的。假如你运行下面的代码,Python 就会因为你误用 class 保留字而产生一个语法错误:
bsObj.findAll(class="green")
不过,你可以用 BeautifulSoup 提供的有点臃肿的方案,在 class 后面增加一个下划线:
bsObj.findAll(class_="green")
另外,你也可以用属性参数把 class 用引号包起来:
bsObj.findAll("", {"class":"green"})
最后回忆一下前面的内容,通过标签参数 tag 把标签列表传到 findAll() 里获取一列标签,其实就是一个“或”关系的过滤器(即,先择所有带标签1或标签2或标签3······的一列标签)。如果你的标签列表很长,就需要花很长时间才能写完。而关键词参数 keyword 可以让你增加一个“与”关系的过滤器来简化工作。
看到这里,你已经见过 BeautifulSoup 库里的两种对象了。
前面代码示例中的 bsObj
BeautifulSoup 对象通过 find 和 findAll,或者直接调用子标签获取的一列对象或单个对象,就像:
bsObj.title
但是,这个库还有另外两种对象,虽然不常用,却应该了解一下。
用来表示标签里的文字,不是标签(有些函数可以操作和生成 NavigableString 对象,而不是标签对象)。
用来查找 HTML 文档的注释标签。
这四个对象是目前你用 BeautifulSoup 库时会遇到的所有对象。
findAll 函数通过标签的名称和属性来查找标签。但是如果你需要通过标签在文档中的位置来查找标签,该怎么办?这就是导航树(Navigating Trees)的作用。在前面,我们看过用单一方向进行 BeautifulSoup 标签树的导航:
bsObj.tag.subTag.anotherSubTag
现在我们还用免费代理页面,演示 HTML 导航树的纵向和横向导航(为了简洁,只对表格进行操作)。
这个 表格 —— thead —— tr —— th —— tbody —— tr —— th —— td ······ 其它表格行省略 和许多其他库一样,在 BeautifulSoup 库里,孩子(child)和后代(descendant)有显著的不同:和人类的家谱一样,子标签就是一个父标签的下一级,而后代标签是指一个父标签下面所有级别的标签。例如,tr 标签是 table 标签的子标签,而 thead、tr、th 和 tbody 标签都是 table 标签的后代标签(示例页面中就是如此)。所有的子标签都是后代标签,但不是所有的后代标签都是子标签。 一般情况下,BeautifulSoup 函数总是处理当前标签的后代标签。例如,bsObj.body.h1 选择了 body 标签后代里的第一个 h1 标签,不会去找 body 外面的标签。 类似地,bsObj.div.findAll(“img”) 会找出文档中第一个 div 标签,然后获取这个 div 后代里所有的 img 标签列表。 如果你只想找出子标签,可以用 .children 标签: 输出结果: 这段代码会打印表格中找到的第一个符合指定属性的值。因为 td 标签后面没有子标签和后代标签,下面代码输出的结果完全一样: BeautifulSoup 的 next_siblings() 函数可以让收集表格数据成为简单的事情,尤其是处理带标题行的表格: 输出结果: 这段代码会打印表里的所有列值,第一列除外。为什么第一列被跳过了呢?有两个理由。首先,对象不能把自己作为兄弟标签。任何时候你获取一个标签的兄弟标签,都不会包含这个标签本身。其次,这个函数只调用后面的兄弟标签。例如,如果我们选择一组标签中位于中间位置的一个标签,然后用 next_siblings() 函数,那么它就只会返回在它后面兄弟标签。因此,选择标签行然后调用 next_siblings,可以选择表格中除了标题行以外的所有行。 和 next_siblings 一样,如果你很容易找到一组兄弟标签中的最后一个标签,那么 previous_siblings 函数也会很有用。当然,还有 next_sibling 和 previous_sibling 函数,与前面的作用类似,只是它们返回的是单个标签,而不是一组标签。 在抓取网页的时候,查找父标签的需求比查找子标签和兄弟标签要少很多。通常情况下,如果以抓取网页内容为目的来观察 HTML 页面,我们都是从最上层标签开始的,然后思考如何定位我们想要的数据块所在的位置。但是,偶尔在特殊情况下你也会用到父标签查找函数,parent 和 parents。 输出结果: 我基本完成了开篇设定的任务目标,从结果看,程序只爬取了首页上的数据,而网站所提供的众多代理服务器数据,都分散在的其它页面里。任务列表里的“如果有必要,移动到另一个网页重复这个过程”并未实现。这个任务将留在下一节中给出。可以映射成一棵树,如下所示:
values = bsObj.find("td",{"data-title":"IP"}).children
for v in values:
print(v)
60.170.204.30
value = bsObj.find("td",{"data-title":"IP"})
print(value)
values = bsObj.find("td",{"data-title":"IP"}).descendants
for v in values:
print(v)
values = bsObj.find("td",{"data-title":"IP"}).next_siblings
for v in values:
print(v.get_text())
8060
高匿名
HTTP
中国 安徽 蚌埠 电信
1秒
2022-05-20 16:31:02
让标签的选择更具体 如果我们选择 bsObj.table.tr 或直接就用 bsObj.tr 来获取表格中的第一行,上面的代码也可以获得正确的结果。但是,我还是采用更长的形式写了一行代码,这可以避免各种意外。即使页面上只有一个表格(或其它目标标签),只用标签也很容易丢失细节。另外,页面布局总是不断变化的。一个标签这次是在表格中第一行的位置,没准儿哪天就在第二行或第三行了。如果想让你的爬虫更稳定,最好还是让标签的选择更加具体。如果有属性,就利用标签的属性。
2. 一个完整的例子
'''
作者:高玉涵
时间:2022.5.20 17:50
说明:爬取代理信息第一版
'''
import socket
from urllib.request import urlopen
from urllib.error import HTTPError
from bs4 import BeautifulSoup
def getTable(url):
try:
html = urlopen(url)
except HTTPError as e:
return None
except socket.error as e:
return None
try:
bsObj = BeautifulSoup(html.read(), 'html.parser')
table = bsObj.table
except AttributeError as e:
return None
return table
def getAgentData(table):
# 抓取到代理数据
agent_data = []
# 表头
theads = []
# 获取表头
theads = getThead(table)
try:
# 获取所有行
rows = table.findAll('tr')
except AttributeError as e:
print("TR 标签未找到!")
return None
else:
for row in rows:
# 存放代理信息
agent = {}
for t in theads:
# 逐行查找与列对应的数据
text = row.find('td', {'data-title':t})
if text is not None:
agent.setdefault(t, text.get_text())
if len(agent) != 0:
agent_data.append(agent)
return agent_data
def getThead(table):
# 存放获取的表头值
theads = []
try:
# 遍历表格头子标签
for h in table.thead.tr.children:
# 提取标签内的值去掉前后空格
text = h.get_text().replace(" ","")
# 忽略不可见的换行符
if text != '\n':
theads.append(text)
except AttributeError as e:
print("TR 标签未找到!")
return None
else:
return theads
if __name__ == '__main__':
# 免费代理信息表格
table = getTable('https://free.kuaidaili.com/free/inha/')
if table == None:
print('Table could not be found')
else:
# 获取代理信息
agents = getAgentData(table)
for a in agents:
print(a)
{'IP': '183.247.199.114', 'PORT': '30001', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 浙江 台州 移动', '响应速度': '0.4秒', '最后验证时间': '2022-05-20 17:31:01'}
{'IP': '60.170.204.30', 'PORT': '8060', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 安徽 蚌埠 电信', '响应速度': '1秒', '最后验证时间': '2022-05-20 16:31:02'}
{'IP': '14.215.212.37', 'PORT': '9168', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 广东 东莞 电信', '响应速度': '4秒', '最后验证时间': '2022-05-20 15:31:01'}
{'IP': '103.37.141.69', 'PORT': '80', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 北京 ', '响应速度': '6秒', '最后验证时间': '2022-05-20 14:31:01'}
{'IP': '103.37.141.69', 'PORT': '80', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 北京 ', '响应速度': '6秒', '最后验证时间': '2022-05-20 13:31:01'}
{'IP': '103.37.141.69', 'PORT': '80', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 北京 ', '响应速度': '6秒', '最后验证时间': '2022-05-20 12:31:01'}
{'IP': '122.9.101.6', 'PORT': '8888', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 香港 666666999999999.com ', '响应速度': '0.4秒', '最后验证时间': '2022-05-20 11:31:01'}
{'IP': '106.15.197.250', 'PORT': '8001', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 上海 联通', '响应速度': '1秒', '最后验证时间': '2022-05-20 10:31:02'}
{'IP': '14.215.212.37', 'PORT': '9168', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 广东 东莞 电信', '响应速度': '4秒', '最后验证时间': '2022-05-20 09:31:01'}
{'IP': '103.37.141.69', 'PORT': '80', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 北京 ', '响应速度': '6秒', '最后验证时间': '2022-05-20 08:31:01'}
{'IP': '39.106.71.115', 'PORT': '7890', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 北京 联通', '响应速度': '0.3秒', '最后验证时间': '2022-05-20 07:31:01'}
{'IP': '183.236.232.160', 'PORT': '8080', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 广东 江门 移动', '响应速度': '6秒', '最后验证时间': '2022-05-20 06:31:01'}
{'IP': '202.55.5.209', 'PORT': '8090', '匿名度': '高匿名', '类型': 'HTTP', '位置': '中国 香港 电信 ', '响应速度': '0.3秒', '最后验证时间': '2022-05-20 05:31:01'}
{'IP': '118.180.166.195', 'PORT': '8060', '匿名度': '高匿名', '类型': 'HTTP', '位置': '甘肃省庆阳市 电信', '响应速度': '0.4秒', '最后验证时间': '2022-05-20 04:31:01'}
{'IP': '121.13.252.61', 'PORT': '41564', '匿名度': '高匿名', '类型': 'HTTP', '位置': '广东省东莞市 电信', '响应速度': '2秒', '最后验证时间': '2022-05-20 03:31:01'}
3. 下一节,抓取所有网页
你可能感兴趣的:(Python,网络,python,爬虫,BeautifulSoup,免费代理)