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>.*?)&nbsp.*?<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>.*?)&nbsp.*?<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

右击 检查

 

posted @ 2021-05-02 16:56  王陸  阅读(466)  评论(0编辑  收藏  举报