众里寻她千百度,蓦然回首,那人依旧对我不屑一顾。
我们每天都需要获取和过滤大量数据和信息。
比如一个产品运营人员,每天都需要了解:
获取和过滤数据的能力,决定了每个人的效率。
日常工作中,我们会用各类软件来处理数据。
其中使用频率最高、数量最多的,就是浏览器。
历史上曾发生过3次浏览器大战:
目前Chrome浏览器已占有7成PC端市场,1/3移动端市场,成为行业老大。
互联网诞生之初只能显示文本信息,应用非常有限。
随着技术标准化和商业化推进,浏览器慢慢把图片、音频、视频、动画等元素带入互联网,成了如今的模样。
浏览器,最基本的3大核心模块:
当然,浏览器工作远不止这些,它还得管人机交互、多进程资源分配、数据存储以及插件扩展等等。
但只要具备3大核心模块的功能,我们就能处理互联网上的数据。
就像Word软件,可以把一个装着XML和图片等文件的“压缩包”,显示为一篇文档;
浏览器,则是把HTML、CSS、JS、JSON和图像等文件,显示为可浏览的页面。
简单看下浏览器的工作流程:
首先,浏览器根据我们给出的网址,发出网络请求,获取网址背后对应的资源文件。
其次,内核根据文件内容进行渲染,JS模块负责执行脚本,在屏幕上输出可视的页面。
最后,我们通过和页面上元素的交互,“通知”浏览器发出进一步的数据请求。
对于获取数据而言,我们不必太关心浏览器内核渲染页面的过程。
结构化的数据主要包含在HTML和JSON这两类文本文件中。
二进制文件,如图片、音视频等,其访问地址都会包含在以上两类文本文件中。
处理互联网数据的关键,是获取结构化文本,并从中提取所需内容。
通过访问地址从网络端获取HTML或JSON文件
从文件中解析出所需的文本内容,或其他文件访问地址
继续第1步操作。
这也就是“爬虫”的基本工作原理。
和自动办公系列相比,处理文件多了一步,那就是得先从网上下载文件。
网络分很多层,互联网属于应用层,用HTTP协议规定了客户端和服务器间的通信格式。
浏览器就是一种客户端,服务器就是网页和文件存放的地方。
当我们输入某个网址按下回车:
GET /index.html
。最常用HTTP请求动作就2个:
GET
:用来下载数据,比如页面、各类文件等。POST
:用来上传数据,比如表单、文件等。Python用于处理HTTP协议的模块中,requests
门槛最低,简单易用。
比如用它来获取头条的首页:
import requests
r = requests.get('https://www.toutiao.com/')
print(r.content)
只不过,打印出来的内容没有经过排版,看起来比较乱。
当然,我们也可以用它上传数据,比如上传图片:
import time
import pathlib
import requests
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/dataproc/002dataget')
img_path = path.joinpath('tifa.jpg')
url = 'https://zh-cn.imgbb.com/json'
data = {
'type': 'file',
'action': 'upload',
'timestamp': int(time.time()*1000),
}
files = [('source', (img_path.stem, open(img_path, 'rb')))]
r = requests.post(url, data=data, files=files)
j = r.json()
url = j['image']['url']
print(url)
POST
操作中需要指定提交的数据,那么应该提交哪些数据呢?
在用requests
模块自动执行上传操作前,我们可以借助浏览器观察所需要的所有参数。
这里只是用公开的图床服务演示,所以在权限校验等方面比较宽松,正常情况下需要在请求头部中设定使用的账户信息等内容。
拿到HTML文件后,想要提取其中的内容,我们就得解析文件。
HTML是一种标签格式的文本文件,属于一种特殊应用的XML文件。
比如:
它本质上还是个文本文件,所以直接用之前打开文本文件的方式也能处理。
但如果想要提取其中的数据,每次都得用字符串匹配标签,再获取数据。
好在,这些基础工作已经有人更好地完成了,我们只需要使用对应的模块即可。
Python中常用语处理HTML的模块主要有2个:
lxml
:用于处理XML文档结构,解析成树型数据结构。beautifulsoup4
:提供操作HTML文档的简易Python接口。两者定位不同,前者侧重文档解析,速度快;后者是数据访问接口接口易用,底层基于其他模块解析文档,如lxml
、Python标准库里的HTML解析模块html.parser
。
模块安装:
pip install lxml
pip install beautifulsoup4
import pathlib
from lxml import etree
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/dataproc/002dataget')
html_path = path.joinpath('test.html')
# 使用HTML方法从字符串中解析HTML
with open(html_path, 'r') as f:
root = etree.HTML(f.read())
# 使用parse方法从某个文件中解析HTML
# root = etree.parse(str(html_path))
print(root.xpath('//a/text()')) # 所有a元素文本
print(root.xpath('//a/@href')) # 所有a元素链接
# 根据属性查找元素
print(root.xpath('//div[@class="article"]/a/text()'))
# 模糊匹配
print(root.xpath('//div[contains(@class,"article")]/a/@href'))
print(root.xpath('//div[starts-with(@class, "art")]/a/@href'))
lxml
模块支持用xpath语法查找数据:
/
表示根节点//a
表示匹配所有a
元素//a/text()
表示选取所有a
元素的文本数据@
表示选取元素的属性//div[@class="article"]
表示所有class
属性为article
的div
元素xpath也支持更复杂的匹配方式,刚开始只需掌握最基本的使用,就足够应付80%常见情况。
如果不熟悉xpath语法,也没关系,我们可以借助Chrome浏览器获取。
当然,lxml
模块也支持Python语法查询数据,但没有xpath简洁,更适合逐个遍历元素。
import pathlib
from lxml import etree
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/dataproc/002dataget')
html_path = path.joinpath('test.html')
root = etree.parse(str(html_path)).getroot()
print(root.tag) # 根元素标签名
head, body = root.getchildren() # 获取root之下的元素
for div in body:
if div.tag == 'div': # 根据tag判断元素标签
links = div.findall('a') # 获取div元素下所有a子元素
for link in links:
# text获取元素内容,attrib获取属性
print(link.text, link.attrib['href'])
# 纯粹遍历文档所有元素
for ele in root.iter('*'):
print(ele.tag)
相比lxml
,beautifulsoup4
提供的Python接口更丰富,也更易用。
import pathlib
from bs4 import BeautifulSoup
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/dataproc/002dataget')
html_path = path.joinpath('test.html')
with open(html_path, 'r') as f:
# 用Python内置标准库解析文档
soup = BeautifulSoup(f.read(), 'html.parser')
# 用lxml解析文档
# soup = BeautifulSoup(f.read(), 'lxml')
# 支持点操作访问元素
print(soup.html.name)
# 等价于
print(soup.find('html').name)
# 也等价于
print(soup.find_all('html')[0].name)
print(soup.title.text)
# 获取所有链接元素
links = soup.find_all('a')
for link in links:
print(link.text, link.attrs['href'])
# 也支持get方法获取属性
# print(link.get('href'))
# 用属性查找元素
div = soup.find('div', class_='article')
# 支持[]获取属性值
print(div.a.text, div.a['href'])
# 遍历子元素
for div in soup.body.children:
if div.name == 'div':
print(div.get_text())
可以看到,它通过重写__getattr__
方法,提供了点操作符方便元素查找。
同时也提供了支持按属性查找元素的find
方法,比lxml
中的find
方法更易用。
在选择lxml
还是beautifulsoup
时,可以根据个人习惯,但个人比较推荐用lxml
的xpath语法获取元素,这种方式更简洁。
此外,如果是Chrome系浏览器,可以安装xpath_helper
插件,可以方便在单个页面中根据xpath语法批量获取数据。
举个例子,如何可以把自动办公系列中所有文章及其链接都保存到Excel中?
xpath_helper
插件。li
元素后的具体索引值。/@href
,获取文章链接。这个方法适用于所有单个网页上批量获取元素的情况,简单实用。
结合requests
和lxml
模块,我们可以很方便获取页面数据,并保存如csv
等文件。
import csv
import pathlib
import requests
from lxml import etree
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/dataproc/002dataget')
csv_path = path.joinpath('002dataget_csv.csv')
url = 'https://mp.weixin.qq.com/s/JFEASRL17bnr6fRJfezixA'
r = requests.get(url)
root = etree.HTML(r.content)
# 获取所有文章链接
# 根据浏览器中获取xpath修改
link_list = root.xpath('//*[@id="js_content"]/ol[2]//a')
# 获取文章标题和URL
articles = [(link.xpath('.//text()')[0], link.get('href')) for link in link_list]
# 写入csv文件
with open(csv_path, 'w') as f:
csv_f = csv.writer(f)
# 添加表头
csv_f.writerow(('标题', '链接'))
csv_f.writerows(articles)
这样就完成了从获取页面内容,到抽取所需信息,再保存到本地文件的自动流程。
如果需要从多个页面获取数据,需要先识别页面数据间相同的定位特征,比如微信公众号正文内容都在//*[@id="js_content"]
范围之内,然后通过循环方式处理页面即可。
目前互联网上有不少应用都通过JSON格式传送数据,尤其是移动端应用。
之前也介绍过用Python的json
标准库来解析此类数据。
import pathlib
import json
path = list(pathlib.Path.cwd().parents)[1].joinpath('data/dataproc/002dataget')
json_path = path.joinpath('test.json')
with open(json_path, 'r') as f:
data = json.loads(f.read())
print(data['data'])
一般JSON格式的数据请求都是异步的,在浏览器调试模式中属于XHR
类数据,可以打开预览。
requests
模块为返回的数据提供了JSON格式转换的方法。
import requests
HEADERS = {
'User-Agent': 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135'
}
url = 'https://www.toutiao.com/api/pc/feed/'
# 头条会检测浏览器类型
r = requests.get(url, headers=HEADERS)
# 用Response对象的json方法获取JSON数据
j = r.json()
data = j['data']
article_list = [ (d['title'], f'https://www.toutiao.com{d["source_url"]}') for d in data]
print(article_list)
有两个注意点:
json
方法才有效,否则会提示错误。爬虫,可以大幅提高数据获取效率;而数据拥有方,会设法拦截爬虫类操作。
爬虫和反爬虫,是一对围绕数据获取的技术较量。
反爬虫措施一般都以限制访问为主,但首先得准确识别出恶意爬虫。
比如,常见的反爬虫措施:
User-Agent
、是否开启窗口等;比如在上面“获取头条PC推荐数据”案例中,除非指定User-Agent
,否则无法获取数据。
原因是对方服务器会检查User-Agent
这个头部参数,判断是否用了“正常”浏览器。
在用requests
模块发出HTTP请求时,如果不单独指定头部信息,它就会用python-requests/2.24.0
作为默认的User-Agent
。
可以用下面代码来验证:
import requests
HEADERS = {
'User-Agent': 'Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.135'
}
sess = requests.Session()
# 显示requests默认UA
print(sess.headers)
# 不指定UA访问
r = requests.get('https://www.toutiao.com/api/pc/feed/')
print(r.content)
# 指定UA为浏览器所用UA
r = requests.get('https://www.toutiao.com/api/pc/feed/', headers=HEADERS)
print(r.content)
很明显,对方服务器会拦截python-requests/2.24.0
这类UA;但当我们把请求“模拟”成浏览器发出时,就可以正常获取到数据了。
HTTP头部信息参数,是服务方权限校验和反爬虫的常用信息,比如User-Agent
、cookie
、referer
等。
我们可以通过浏览器调试模式查看到每个请求对应的头部信息:
实战时,如果浏览器访问正常,而程序无法访问,可以对照浏览器修改头部信息,用排除法去检查各个参数的影响。能做到这一点,就足够应付大部分普通网站。
对于背后有大型技术团队的平台,反爬虫措施会相对复杂;如果涉及到数据上传,检查会更严格。
本文重点介绍了如何用Python从互联网获取数据和解析数据。
实战中,大部分网站数据的自动获取并不复杂,结合Python基本循环和文件读写,能大幅提高效率。
但对于一些大型平台,则需要研究测试,这部分工作量占整体爬虫的90%以上。
关于爬虫,还必须要说明的是:有2条非常明确的红线不能突破!
扫码加入学习群,前100名免费。