Python网络爬虫 第二章 数据解析
一、数据解析概述
在上⼀章中, 我们基本上掌握了抓取整个⽹⻚的基本技能. 但是呢, ⼤多数情况下, 我们并不需要整个⽹⻚的内容, 只是需要那么⼀⼩部分.
怎么办呢? 这就涉及到了数据提取的问题.
本课程中, 提供三种解析⽅式:
- 1. re解析
- 2. bs4解析
- 3. xpath解析
这三种⽅式可以混合进⾏使⽤, 完全以结果做导向, 只要能拿到你想要的数据. ⽤什么⽅案并不重要. 当你掌握了这些之后. 再考虑性能的问题
二、正则表达式
Regular Expression, 正则表达式, ⼀种使⽤表达式的⽅式对字符串进⾏匹配的语法规则.
我们抓取到的⽹⻚源代码本质上就是⼀个超⻓的字符串, 想从⾥⾯提取内容.⽤正则再合适不过了.
正则的优点: 速度快, 效率⾼, 准确性⾼ 正则的缺点: 新⼿上⼿难度有点⼉⾼.
不过只要掌握了正则编写的逻辑关系, 写出⼀个提取⻚⾯内容的正则其实并不复杂
https://github.com/cdoco/learn-regex-zh
正则的语法: 使⽤元字符进⾏排列组合⽤来匹配字符串 在线测试正则表达式https://tool.oschina.net/regex/
元字符: 具有固定含义的特殊符号
常⽤元字符:
. 匹配除换⾏符以外的任意字符 \w 匹配字⺟或数字或下划线 \s 匹配任意的空⽩符 \d 匹配数字 \n 匹配⼀个换⾏符 \t 匹配⼀个制表符 ^ 匹配字符串的开始 $ 匹配字符串的结尾 \W 匹配⾮字⺟或数字或下划线 \D 匹配⾮数字 \S 匹配⾮空⽩符 a|b 匹配字符a或字符b () 匹配括号内的表达式,也表示⼀个组 [...] 匹配字符组中的字符 [^...] 匹配除了字符组中字符的所有字符
量词: 控制前⾯的元字符出现的次数
* 重复零次或更多次 + 重复⼀次或更多次 ? 重复零次或⼀次 {n} 重复n次 {n,} 重复n次或更多次 {n,m} 重复n到m次
贪婪匹配和惰性匹配
.* 贪婪匹配
.*? 惰性匹配
这两个要着重的说⼀下. 因为我们写爬⾍⽤的最多的就是这个惰性匹配.
先看案例
str: 玩⼉吃鸡游戏, 晚上⼀起上游戏, ⼲嘛呢? 打游戏啊 reg: 玩⼉.*?游戏 此时匹配的是: 玩⼉吃鸡游戏 reg: 玩⼉.*游戏 此时匹配的是: 玩⼉吃鸡游戏, 晚上⼀起上游戏, ⼲嘛呢? 打游戏 str: <div>胡辣汤</div> reg: <.*> 结果: <div>胡辣汤</div> str: <div>胡辣汤</div> reg: <.*?> 结果: <div> </div> str: <div>胡辣汤</div><span>饭团</span> reg: <div>.*?</div> 结果: <div>胡辣汤</div>
所以我们能发现这样⼀个规律: .? 表示尽可能少的匹配, .表示尽可能多的匹配, 暂时先记住这个规律. 后⾯写爬⾍会⽤到的哦
三、re模块
那么接下来的问题是, 正则我会写了, 怎么在python程序中使⽤正则呢? 答案是re模块
re模块中我们只需要记住这么⼏个功能就⾜够我们使⽤了.
1. findall 查找所有. 返回list
lst = re.findall("m", "mai le fo len, mai ni mei!") print(lst) # ['m', 'm', 'm'] #'r'是防止字符转义的 如果路径中出现'\t'的话 不加r的话\t就会被转义 而加了'r'之后'\t'就能保留原有的样子 lst = re.findall(r"\d+", "5点之前. 你要给我5000万") print(lst) # ['5', '5000']
2. search 会进⾏匹配. 但是如果匹配到了第⼀个结果. 就会返回这个结果. 如果匹配不上search返回的则是None
ret = re.search(r'\d', '5点之前. 你要给我5000万').group() print(ret) # 5
3. match 只能从字符串的开头进⾏匹配
ret = re.match('a', 'abc').group() print(ret) # a
4. finditer, 和findall差不多. 只不过这时返回的是迭代器(重点)
it = re.finditer("m", "mai le fo len, mai ni mei!") for el in it: print(el.group()) # 依然需要分组
5. compile() 可以将⼀个⻓⻓的正则进⾏预加载. ⽅便后⾯的使⽤
obj = re.compile(r'\d{3}') # 将正则表达式编译成为⼀个正则表达式对象, 规则要匹配的是3个数字 ret = obj.search('abc123eeee') # 正则表达式对象调⽤search, 参数为待匹配的字符串 print(ret.group()) # 结果: 123
6. 正则中的内容如何单独提取?
单独获取到正则中的具体内容可以给分组起名字
import re
s = """ <div class='jay'><span id='1'>郭麒麟</span></div> <div class='jj'><span id='2'>宋铁</span></div> <div class='jolin'><span id='3'>大聪明</span></div> <div class='sylar'><span id='4'>范思哲</span></div> <div class='tory'><span id='5'>胡说八道</span></div> """ # (?P<分组名字>正则) 可以单独从正则匹配的内容中进一步提取内容 obj = re.compile(r"<div class='.*?'><span id='(?P<id>\d+)'>(?P<wahaha>.*?)</span></div>", re.S) # re.S: 让.能匹配换行符 result = obj.finditer(s) for it in result: print(it.group("wahaha")) print(it.group("id"))
这⾥可以看到我们可以通过使⽤分组. 来对正则匹配到的内容进⼀步的进⾏筛选.
关于正则, 还有⼀个重要的⼩点, 也⾮常的简单, 在本节中就不继续扩展了. 下⼀⼩节的案例中会把这个⼩点进⾏简单的介绍.
四、⼿刃⾖瓣TOP250电影信息
终于可以放开⼿脚⼲⼀番事业了. 今天我们的⽬标是⾖瓣电影
TOP250排⾏榜. 没别的意思, 练⼿⽽已
先看需求:
⽬标: 抓取"电影名称","上映年份","评分","评分⼈数"四项内容.
怎么做呢? ⾸先, 先看⼀下⻚⾯源代码. 数据是否是直接怼在源代码上的?
很明显, 我们想要的数据全部都在⻚⾯源代码中体现了. 所以, 我们不需要考虑js动态加载数据的情况了. 那么接下来就是编写爬⾍代码的
第⼀步了. 拿到⻚⾯源代码:
import requests headers = { "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36"
} url = "https://movie.douban.com/top250?start=0&filter=" resp = requests.get(url, headers=headers) print(resp.text)
然后呢. 从⻚⾯源代码中提取我们需要的内容. 这时候我们就可以去写正则了
# 解析数据 obj = re.compile(r'<li>.*?<div class="item">.*?<span class="title">(?P<name>.*?)' r'</span>.*?<p class="">.*?<br>(?P<year>.*?) .*?<span ' r'class="rating_num" property="v:average">(?P<score>.*?)</span>.*?' r'<span>(?P<num>.*?)人评价</span>', re.S)
开始匹配, 将最终完整的数据按照⾃⼰喜欢(需要)的⽅式写⼊⽂件.
# 拿到页面源代码. requests # 通过re来提取想要的有效信息 re import requests import re import csv url = "https://movie.douban.com/top250" headers = { "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36" } resp = requests.get(url, headers=headers) page_content = resp.text # 解析数据 obj = re.compile(r'<li>.*?<div class="item">.*?<span class="title">(?P<name>.*?)' r'</span>.*?<p class="">.*?<br>(?P<year>.*?) .*?<span ' r'class="rating_num" property="v:average">(?P<score>.*?)</span>.*?' r'<span>(?P<num>.*?)人评价</span>', re.S) # 开始匹配 result = obj.finditer(page_content) f = open("data.csv", mode="w") #创建csv文件写入工具,也可以直接f.write() csvwriter = csv.writer(f) for it in result: # print(it.group("name")) # print(it.group("score")) # print(it.group("num")) # print(it.group("year").strip()) dic = it.groupdict() # strip() 方法用于移除字符串头尾指定的字符(默认为空格) dic['year'] = dic['year'].strip() csvwriter.writerow(dic.values()) f.close() print("over!")
五、bs4解析-HTML语法
bs4解析⽐较简单, 但是呢, ⾸先你需要了解⼀丢丢的html知识. 然后再去使⽤bs4去提取, 逻辑和编写难度就会⾮常简单和清晰
HTML(Hyper Text Markup Language)超⽂本标记语⾔, 是我们编写⽹⻚的最基本也是最核⼼的⼀种语⾔. 其语法规则就是⽤不同的标签对⽹⻚上的内容进⾏标记, 从⽽使⽹⻚显示出不同的展示效果.
<h1> 我爱你 </h1>
上述代码的含义是在⻚⾯中显示"我爱你"三个字, 但是我爱你三个字被"<h1>"和"</h1>"标记了. ⽩话就是被括起来了. 被H1这个标签括起来了. 这个时候. 浏览器在展示的时候就会让我爱你变粗变⼤. 俗称标题, 所以HTML的语法就是⽤类似这样的标签对⻚⾯内容进⾏标记.不同的标签表现出来的效果也是不⼀样的.
h1: ⼀级标题
h2: ⼆级标题
p: 段落
font: 字体(被废弃了, 但能⽤)
body: 主体
这⾥只是给⼩⽩们简单科普⼀下, 其实HTML标签还有很多很多的.
我们不需要⼀⼀列举(这是爬⾍课, 不是前端课).
OK~ 标签我们明⽩了, 接下来就是属性了.
<h1> 我爱你 </h1> <h1 align='right'> 我爱你妹 </h1>
有意思了. 我们发现在标签中还可以给出xxx=xxx这样的东⻄. 那么它⼜是什么呢? ⼜该如何解读呢?
⾸先, 这两个标签都是h1标签, 都是⼀级标题, 但是下⾯这个会显示在右边. 也就是说, 通过xxx=xxx这种形式对h1标签进⼀步的说明了.
那么这种语法在html中被称为标签的属性. 并且属性可以有很多个.
例如:
<body text="green" bgcolor="#eee"> 你看我的颜⾊. 贼健康 </body>
总结, html语法:
<标签 属性="值" 属性="值"> 被标记的内容 </标签>
有了这些知识, 我们再去看bs4就会得⼼应⼿了. 因为bs4就是通过标签和属性去定位⻚⾯上的内容的。
六、bs4模块安装和使⽤
bs4模块安装
在python中我⼀般只推荐⽤pip进⾏安装. 原因: 简单!!!!
pip install bs4
如果安装的速度慢, 建议更换国内源(推荐阿⾥源或者清华源)
如何使⽤bs4
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple bs4
bs4在使⽤的时候就需要参照⼀些html的基本语法来进⾏使⽤了. 我们直接上案例哈. 案例是最能直观的展现出bs4的便捷效果的.
我们来尝试抓取北京新发地市场的农产品价格. http://www.xinfadi.com.cn/marketanalysis/0/list/1.shtml
⽼规矩, 先获取⻚⾯源代码. 并且确定数据就在⻚⾯源代码中~
import requests from bs4 import BeautifulSoup resp =requests.get("http://www.xinfadi.com.cn/marketanalysis/0/list/1.shtml") print(resp.text)
将⻚⾯源代码丢给BeautifulSoup, 然后我们就可以通过bs对象去检索⻚⾯源代码中的html标签了
page = BeautifulSoup(resp.text)
BeautifulSoup对象获取html中的内容主要通过两个⽅法来完成
find()
find_all()
基本上有这两个⽅法就够⽤了. 其他的可以⾃⾏进⾏英⽂翻译就知道啥意思了.
不论是find还是find_all 参数⼏乎是⼀致的.
语法:
find(标签, 属性=值)
意思是在⻚⾯中查找 xxx标签, 并且标签的xxx属性必须是xxx值 例:
find('div', age=18) 含义: 在⻚⾯中查找div标签, 并且属性age必须是18的这个标签.
find_all()的⽤法和find()⼏乎⼀致.
- find()查找1个.
- find_all()查找⻚⾯中所有的.
但是这种写法会有些问题. ⽐如html标签中的class属性.
<div class="honor"> page.find("div", class="honor") # 注意, python中class是关键字. 会报错的. 怎么办呢? 可以在class后⾯加个下划线 page.find("div", class_="honor") #我们可以使⽤第⼆种写法来避免这类问题出现 page.find("div", attrs={"class": "honor"})
好了, ⽤法说完了. 接下来就回来看怎么抓取新发地的价格吧
table = page.find("table", class_="hq_table") print(table)
接下来就可以进⼀步去提取数据了. 后⾯的直接给出完整代码.
因为逻辑都是⼀样的. 并没有多么的复杂, 过程就省略了.
最后代码
# 安装 # pip install bs4 -i 清华 # 1. 拿到页面源代码 # 2. 使用bs4进行解析. 拿到数据 import requests from bs4 import BeautifulSoup import csv url = "http://www.xinfadi.com.cn/marketanalysis/0/list/1.shtml" resp = requests.get(url) f = open("菜价.csv", mode="w") csvwriter = csv.writer(f) # 解析数据 # 1. 把页面源代码交给BeautifulSoup进行处理, 生成bs对象 # html.parser 告诉解析器这是html文件 page = BeautifulSoup(resp.text, "html.parser") # 指定html解析器 # 2. 从bs对象中查找数据 # find(标签, 属性=值) # find_all(标签, 属性=值) # table = page.find("table", class_="hq_table") # class是python的关键字 table = page.find("table", attrs={"class": "hq_table"}) # 和上一行是一个意思. 此时可以避免class # 拿到所有数据行tr # 行tr 列td print(table) #做切片 从第一个开始切 排除了第0个表头 获得纯数据 trs = table.find_all("tr")[1:] for tr in trs: # 每一行 tds = tr.find_all("td") # 拿到每行中的所有td name = tds[0].text # .text 表示拿到被标签标记的内容 low = tds[1].text # .text 表示拿到被标签标记的内容 avg = tds[2].text # .text 表示拿到被标签标记的内容 high = tds[3].text # .text 表示拿到被标签标记的内容 tp = tds[4].text # .text 表示拿到被标签标记的内容 kind = tds[5].text # .text 表示拿到被标签标记的内容 date = tds[6].text # .text 表示拿到被标签标记的内容 csvwriter.writerow([name, low, avg, high, tp, kind, date]) f.close() print("over1!!!!")
七、bs4抓取图片
为了视频和⽂档能够正常投放在市⾯上, 这里抓取的图⽚都是唯美桌⾯系
https://www.umei.cc/bizhitupian/weimeibizhi/
注意我选中的这个区域, 我们想要的图⽚就在这⾥. 但是, 绝对不是现在你看到的样⼦. 为什么呢? 不够⾼清⼤图~
真正的⾼清⼤图在⼦⻚⾯中, ⽐如, 我点击第⼀个图⽚
这才是我想要的⼤图~
也就是说, 我需要在⽹站的⾸⻚中, 找到⼦⻚⾯的链接, 然后请求到⼦⻚⾯, 才能看到这张⼤图~ 不明⽩的, 把上⾯的内容重新梳理⼀下!!!!!!!
也就是说, 想要下载该⽹站图⽚(⾼清⼤图), 需要三步,
- 第⼀步, 在主⻚⾯中拿到每⼀个图⽚的⼦⻚⾯链接
- 第⼆步, 在⼦⻚⾯中找到真正的图⽚下载地址
- 第三步, 下载图⽚
# 1.拿到主页面的源代码. 然后提取到子页面的链接地址, href # 2.通过href拿到子页面的内容. 从子页面中找到图片的下载地址 img -> src # 3.下载图片 import requests from bs4 import BeautifulSoup import time url = "https://www.umei.cc/bizhitupian/weimeibizhi/" resp = requests.get(url) resp.encoding = 'utf-8' # 处理乱码 # print(resp.text) # 把源代码交给bs main_page = BeautifulSoup(resp.text, "html.parser") alist = main_page.find("div", class_="TypeList").find_all("a") # print(alist) for a in alist: href = a.get('href') # 直接通过get就可以拿到属性的值 # 拿到子页面的源代码 # 从子页面中拿到图片的下载路径 child_page_resp = requests.get(href) child_page_resp.encoding = 'utf-8' child_page_text = child_page_resp.text child_page = BeautifulSoup(child_page_text, "html.parser") p = child_page.find("p", align="center") img = p.find("img") # 图片url src = img.get("src") # 下载图片 img_resp = requests.get(src) # img_resp.content # 这里拿到的是字节 # http://kr.shanghai-jiuxin.com/file/2020/1031/6b72c57a1423c866d2b9dc10d0473f27.jpg # 6b72c57a1423c866d2b9dc10d0473f27.jpg img_name = src.split("/")[-1] # 拿到url中的最后一个/以后的内容 with open("img/"+img_name, mode="wb") as f: f.write(img_resp.content) # 图片内容写入文件 print("over!!!", img_name) time.sleep(1) print("all over!!!")
八、Xpath解析
XPath是⼀⻔在 XML ⽂档中查找信息的语⾔. XPath可⽤来在 XML⽂档中对元素和属性进⾏遍历. ⽽我们熟知的HTML恰巧属于XML的⼀个⼦集. 所以完全可以⽤xpath去查找html中的内容.
详细说明见这篇博客https://www.jianshu.com/p/85a3004b5c06
⾸先, 先了解⼏个概念.在上述html中,
<book> <id>1</id> <name>野花遍地⾹</name> <price>1.23</price> <author> <nick>周⼤强</nick> <nick>周芷若</nick> </author> </book>
- book, id, name, price....都被称为节点.
- Id, name, price, author被称为book的⼦节点
- book被称为id, name, price, author的⽗节点
- id, name, price,author被称为同胞节点
OK~ 有了这些基础知识后, 我们就可以开始了解xpath的基本语法了
在python中想要使⽤xpath, 需要安装lxml模块
pip install lxml
⽤法:
1. 将要解析的html内容构造出etree对象.
2. 使⽤etree对象的xpath()⽅法配合xpath表达式来完成对数据的提取
xpath语法
表达式 | 描述 |
---|---|
nodename | 选取此节点的所有子节点。 |
/ | 从根节点选取。 |
// | 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置。 |
. | 选取当前节点。 |
.. | 选取当前节点的父节点。 |
@ | 选取属性。 |
from lxml import etree xml = """ <book> <id>1</id> <name>野花遍地香</name> <price>1.23</price> <nick>臭豆腐</nick> <author> <nick id="10086">周大强</nick> <nick id="10010">周芷若</nick> <nick class="joy">周杰伦</nick> <nick class="jolin">蔡依林</nick> <div> <nick>热热热热热1</nick> </div> <span> <nick>热热热热热2</nick> </span> </author> <partner> <nick id="ppc">胖胖陈</nick> <nick id="ppbc">胖胖不陈</nick> </partner> </book> """ tree = etree.XML(xml) # result = tree.xpath("/book") # /表示层级关系. 第一个/是根节点 # result = tree.xpath("/book/name") # result = tree.xpath("/book/name/text()") # text() 拿文本 # result = tree.xpath("/book/author//nick/text()") # // 后代 # result = tree.xpath("/book/author/*/nick/text()") # * 任意的节点. 通配符(会儿) result = tree.xpath("/book//nick/text()") print(result)
xpath如何提取属性信息. 我们上⼀段真实的HTML来给各位讲解⼀下
准备HTML:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>Title</title> </head> <body> <ul> <li><a href="http://www.baidu.com">百度</a></li> <li><a href="http://www.google.com">谷歌</a></li> <li><a href="http://www.sogou.com">搜狗</a></li> </ul> <ol> <li><a href="feiji">飞机</a></li> <li><a href="dapao">大炮</a></li> <li><a href="huoche">火车</a></li> </ol> <div class="job">李嘉诚</div> <div class="common">胡辣汤</div> </body> </html>
from lxml import etree tree = etree.parse("b.html") # result = tree.xpath('/html') # result = tree.xpath("/html/body/ul/li/a/text()") # result = tree.xpath("/html/body/ul/li[1]/a/text()") # xpath的顺序是从1开始数的, []表示索引 # result = tree.xpath("/html/body/ol/li/a[@href='dapao']/text()") # [@xxx=xxx] 属性的筛选 # print(result) # ol_li_list = tree.xpath("/html/body/ol/li") # # for li in ol_li_list: # # 从每一个li中提取到文字信息 # result = li.xpath("./a/text()") # 在li中继续去寻找. 相对查找 # print(result) # result2 = li.xpath("./a/@href") # 拿到属性值: @属性 # print(result2) # # print(tree.xpath("/html/body/ul/li/a/@href")) print(tree.xpath('/html/body/div[1]/text()')) print(tree.xpath('/html/body/ol/li/a/text()'))
如果页面过于复杂,但想要要获取xpath,可以借助谷歌浏览器的功能直接获取xpath
右击 检查