python学习笔记之爬虫(二) re、bs4、xpath解析 丨【生长吧!Python】

举报
菜鸟级攻城狮 发表于 2021/07/06 20:48:38 2021/07/06
【摘要】 python学习笔记之爬虫(二) re、bs4、xpath解析

## 第二章 ###
import csv

''' 1、数据解析概述 '''
'''


三种解析方式:
    1re解析
    2bs4解析
    3xpath解析
这三种方式可以混合使用,完全以结果做向导,只要能拿到你想要的数据,
用什么方式并不重要,当你掌握了这些之后,再考虑性能问题。

'''


''' 2、正则表达式 '''
'''


工具链接:
    开源中国 https://www.oschina.net/
    实用在线工具 https://tool.oschina.net/
    在线正则表达式测试 https://tool.oschina.net/regex

Regular Expression,正则表达式,一种使用表达式的方式对字符串进行匹配的语法规则。
正则的优点:速度快,效率高,准确性高
正则的缺点:新手上手难度有点高。
正则的语法:使用元字符进行排列组合用来匹配字符串

元字符:具有固定含义的特殊符号
常用元字符:
    .       匹配除换行符以外的任意字符
    \w      匹配字母、数字、下划线
    \s      匹配任意的空白符
    \d      匹配数字
    \n      匹配一个换行符
    \t      匹配一个制表符

    ^       匹配字符串的开始
    $       匹配字符串的结尾

    \W      匹配非字母、数字、下划线
    \D      匹配非数字
    \S      匹配非空白符
    a|b     匹配字符a或字符b
    ()      匹配括号内的表达式,也表示一个组
    [...]   匹配字符组中的字符
    [^...]  匹配除了字符组中字符的所有字符

量词:控制前面的元字符出现的次数
    *       重复0次或更多次
    +       重复1次或更多次
    ?       重复0次或1
    {n}     重复n
    {n,}    重复n到更多次
    {n,m}   重复nm

贪婪匹配和惰性匹配:
    .*      贪婪匹配,尽可能多的匹配
    .*?     惰性匹配,尽可能少的匹配
这两个要着重说一下,因为我们写爬虫用的最多的就是这个惰性匹配
先看案例:
    str: 玩儿吃鸡游戏,晚上一起上游戏,干嘛呢?打游戏啊
    reg: 玩儿.*?游戏
    结果:玩儿吃鸡游戏

    reg: 玩儿.*游戏
    结果:玩儿吃鸡游戏,晚上一起上游戏,干嘛呢?打游戏

    str: <div>胡辣汤</div>
    reg: <.*>
    结果:<div>胡辣汤</div>

    reg: <.*?>
    结果:<div>
         </div>

    reg: <div>.*?</div>
    结果:<div>胡辣汤</div>
所以我们能发现这样一个规律:
    .* 表示尽可能多的匹配。
    .*? 表示尽可能少的匹配,
'''


''' 3re模块 '''
'''


import re

# re模块中我们只需要记住这么几个方法就足够使用了。
# 1findall():匹配字符串中所有符合正则表达式的内容,返回list列表

lst = re.findall(r'\d+', '我的电话号码是:10086,我女朋友的电话号码是:10010')
print(lst)  # ['10086', '10010']


# 2finditer: 匹配字符串中所有的内容,返回iter迭代器,从iter中拿数据需要.group()
it = re.finditer(r'\d+', '我的电话号码是:10086,我女朋友的电话号码是:10010')
for i in it:
    # print(i)    # <re.Match object; span=(8, 13), match='10086'>
    print(i.group())    # 10086  10010


# 3search(): 找到一个结果就返回,返回的结果是match对象,匹配不上就返回None,拿数据需要.group()
s = re.search(r'\d+', '我的电话号码是:10086,我女朋友的电话号码是:10010')
print(s)    # <re.Match object; span=(8, 13), match='10086'>
print(s.group())    # 10086


# 4match(): 只能从头开始匹配,返回的结果是match对象
s1 = re.search(r'\d+', '我的电话号码是:10086,我女朋友的电话号码是:10010')
print(s1)    # <re.Match object; span=(8, 13), match='10086'>
print(s1.group())    # 10086


# 5compile(): 预加载正则表达式,方便后面使用
obj = re.compile(r'\d+')
ret = obj.finditer('我的电话号码是:10086,我女朋友的电话号码是:10010')
for it in ret:
    print(it.group())   # 10086  10010


# 6、正则中的内容如何单独提取?
# 单独获取到正则中的具体内容可以给分组起名字

s = """
<div class='type1'><span id='10000'>中国电信</span></div>
<div class='type2'><span id='10086'>中国移动</span></div>
<div class='type3'><span id='10010'>中国联通</span></div>
"""
pattern = re.compile(r"class='(?P<class>.*?)'><span id='(?P<id>.*?)'>(?P<name>.*?)</span>", re.S)
# re.S 使 . 匹配包括换行在内的所有字符
result = pattern.finditer(s)
for it in result:
    print(it.group('class'))
    print(it.group('id'))
    print(it.group('name'))
''' 执行结果
type1
10000
中国电信
type2
10086
中国移动
type3
10010
中国联通
'''



''' 4re案例:豆瓣top250电影排行 '''

import requests
import re
import csv

headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3756.400 QQBrowser/10.5.4043.400'
}
for i in range(0, 101, 25):
    url = 'https://movie.douban.com/top250?start=%s' % i
    # 使用requests拿到页面源代码
    with requests.get(url, headers=headers) as resp: # 省略resp.close()语句
        content = resp.text
        # 通过re模块提取有效信息
        pattern = re.compile(r'.*?<li>.*?<span class="title">(?P<name>.*?)</span>'
                             r'.*?<br>(?P<year>.*?)&nbsp'
                             r'.*?property="v:average">(?P<score>.*?)</span>'
                             r'.*?<span>(?P<num>.*?)人评价</span>'
                             r'.*?<span class="inq">(?P<quote>.*?)</span>', re.S)   # # re.S 使 . 匹配包括换行在内的所有字符
        result = pattern.finditer(content)
        # print(result)   # <callable_iterator object at 0x000002BEBDA44160>
        # Windows默认编码是gbk,需手动设置编码方式为utf-8,否则文件内容为乱码
        # Python3使用csv模块csv.writer().writerow()保存csv文件,会产生空行,newline=''设置为空

        with open('data.csv', mode='a', encoding='utf-8', newline='') as f:
            # print(f)  # <_io.TextIOWrapper name='data.csv' mode='w' encoding='cp936'>
            csvwriter = csv.writer(f)
            for it in result:
                # print(it.group('name'))
                # print(it.group('year').strip())
                # print(it.group('score'))
                # print(it.group('num'))
                # Match.groupdict(default=None):
                # 返回一个字典,包含了所有的 命名 子组。key就是组名,value就是内容。
                # default 参数用于不参与匹配的组合;默认为 None

                dct = it.groupdict()
                dct['year'] = dct['year'].strip()
                # 将内容保存到data.csv
                csvwriter.writerow(dct.values())
                # 将内容保存到data.txt
                # with open('data.txt', 'a', encoding='utf-8') as wstream:
                #     wstream.write(str(dct) + '\n')

            else:
                print(f'{url} Done!')



''' 5bs4解析 '''
'''

1bs4解析案例:北京新发地菜价


安装bs4模块:
    pip install bs4
    pip install -i https://pypi.tuna.tsinghua.edu.cn/simple bs4
'''

import requests
from bs4 import BeautifulSoup

# 拿到页面源代码
for i in range(1, 6):
    url = 'http://www.xinfadi.com.cn/marketanalysis/0/list/%s.shtml' % i
    resp = requests.get(url)

    f = open('菜价.csv', 'a', encoding='utf-8', newline='')
    csvwriter = csv.writer(f)

    # 使用bs4进行解析,拿到数据
    # 1、解析数据,把页面源代码交给BeautifulSoup进行处理,生成bs对象

    page = BeautifulSoup(resp.text, 'html.parser')  # 指定html解析器
    # print(type(page))   # <class 'bs4.BeautifulSoup'>
    # 报出警告信息如下:,添加'html.parser'后即可消除警告信息
    # D:/Project/Pycharm/Project3 Spider/demo2.py:261: GuessedAtParserWarning: No parser was explicitly specified, so I'm using the best available HTML parser for this system ("html.parser"). This usually isn't a problem, but if you run this code on another system, or in a different virtual environment, it may use a different parser and behave differently.
    # The code that caused this warning is on line 261 of the file D:/Project/Pycharm/Project3 Spider/demo2.py. To get rid of this warning, pass the additional argument 'features="html.parser"' to the BeautifulSoup constructor.
    #   page = BeautifulSoup(resp.text)

    # 2、从bs对象中查找数据。
    # 两种方式:
    # find(标签, 属性=)
    # find_all(标签, 属性=)
    # table = page.find('table', class_="hq_table")   # classpython关键字,为防止报错。加下划线用以区分
    table = page.find('table', attrs={'class': 'hq_table'})  # 和上一行一个意思,可以避免同名关键字
    # print(type(table))  # <class 'bs4.element.Tag'>
    # 拿到所有数据行

    trs = table.find_all('tr')[1:]
    for tr in trs:  # 遍历每一行
        tds = tr.find_all('td') # 拿到每行中的所有td
        name = tds[0].text  # .text表示拿到被标签标记的内容
        min_price = tds[1].text     # 最低价
        avg_price = tds[2].text     # 平均价
        max_price = tds[3].text     # 最高价
        spec = tds[4].text          # 规格
        unit = tds[5].text          # 单位
        release_date = tds[6].text  # 发布日期
        # print(name, min_price, avg_price, max_price, spec, unit, release_date)

        # 3、将数据写入文件
        csvwriter.writerow([name, min_price, avg_price, max_price, spec, unit, release_date])
    else:
        f.close()
        resp.close()
        print(f'{url} Done!')



'''
2bs4解析案例:优美图库图片

'''

# 1、拿到主页面的源代码,然后提取子页面的链接地址,href
# 2、通过href拿到子页面的内容,从子页面中找到图片的下载地址 imgg -> src
# 3、下载图片

import requests
from bs4 import BeautifulSoup
from time import sleep
import os

# 创建文件夹用以保存下载的图片
path_dir = os.path.join(os.path.dirname(__file__), 'img')
if not os.path.exists(path_dir):
    os.mkdir(path_dir)

url = 'https://www.umei.net/bizhitupian/weimeibizhi/'
resp = requests.get(url)
resp.encoding = 'utf-8'     # 处理乱码
# 把源代码交给bs
main_page = BeautifulSoup(resp.text, 'html.parser')
alist = main_page.find('div', class_='TypeList').find_all('a')  # 缩小查找范围到div标签
# 遍历获取a标签href属性的值
for a in alist:
    href = 'https://www.umei.net/' + a.get('href')
    # 拿到子页面源代码
    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')  # 缩小查找范围到p标签
    img = p.find('img')
    # 获取img标签src属性的值
    src = img.get('src')

    # 下载图片到本地,直接请求地址即可下载
    img_resp = requests.get(src)
    # print(img_resp.content)    # 这里拿到的是图片字节

    # rfind()切片获取文件名
    # img_name = src[src.rfind('/') + 1:]
    ##  split()切片获取文件名

    img_name = src.split('/')[-1]

    # 判断图片是否存在,不存在则下载图片
    if os.path.exists('img/' + img_name):
        print(img_name, '已存在此图片!')
    else:
        # 注意此处有坑!!!
        # 不能是with open('img/img_name', 'wb'),否则会有错误结果。

        with open('img/' + img_name, 'wb') as wstream:
            wstream.write(img_resp.content)     # 图片内容写入文件
            print(f'{img_name} Done!!!')
            sleep(1)

 

提示:

当你下载图片、视频、音频时,若Pycharm会特别卡顿,是因为下载内容是,Pycharm会逐个标记已下载的内容并建立索引,此过程会感觉明显的卡顿,可用如下方法解决卡顿问题:

右键文件夹,选择Mark Directory as”,点击“Excluded”即可。

如下图所示:

Xpath解析

''' 6Xpath解析 '''
''' Xpath入门1


<book>
    <id>1</id>
    <name>野花遍地香</name>
    <price>1.23</price>
    <author>
        <nick>周大强</nick>
        <nick>周芷若</nick>
    </author
</book>
在以上html中,
    1book, id, name, price...等都被称为节点
    2id, name, price, author都被称为book的子节点
    3book被称为id, name, price, author的父节点
    4id, name, price, author都被称为兄弟节点

安装lxml
    pip install lxml

用法:
    1、将要解析的html内容构造出etree对象
    2、使用etree对象的xpath()方法配合xpath表达式来完成对数据的提取

'''
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>
             <div>
                <nick>热热热3</nick>
            </div>
        </span>
    </author>

    <partner>
        <nick id='ppd'>胖胖东</nick>
        <nick id='ppbd'>胖胖不东</nick>
    </partner>
</book>
'''
tree = etree.XML(xml)
# 找到book节点
result = tree.xpath('/book')    # /表示层级关系,第一个/是根节点
print(result)   # [<Element book at 0x18a285ca6c0>]

# 找到book节点下的name节点里的文本内容野花遍地香
result2 = tree.xpath('/book/name')  # 此时找的是name节点而不是文本内容
print(result2)  # [<Element name at 0x21974761cc0>]
result2 = tree.xpath('/book/name/text()')   # text()表示提取文本内容
print(result2)  # ['野花遍地香']

# 找到author下的nick节点的内容
result3 = tree.xpath('/book/author/nick/text()')
print(result3)  # ['周大强', '周芷若', '周杰伦', '蔡依林']

# 找到nick节点里的内容热热热
result4 = tree.xpath('/book/author/div/nick/text()')
print(result4)  # ['热热热']

# 找到author中所有的nick节点内容
result5 = tree.xpath('/book/author//nick/text()')   # //表示后代,所有后代节点nick都将被匹配
print(result5)  # ['周大强', '周芷若', '周杰伦', '蔡依林', '热热热1', '热热热2', '热热热3']

# 找到author热热热1”热热热2”
# 类似于
# /author/div/nick
# /author/span/nick
# 实现方式:
# /author/*/nick,即将divspan都使用通配符 * 号表示

result6 = tree.xpath('/book/author/*/nick/text()')  # *表示任意节点,通配符,应该只是author下的所有孙子节点nick
print(result6)  # ['热热热1', '热热热2']

# 找到author热热热1”热热热2”热热热3”
result7 = tree.xpath('/book/author//*/nick/text()') # //*表示后代任意子孙节点,所有后代任意子孙节点
print(result7)  # ['热热热1', '热热热2', '热热热3']

# 找到book内所有的nick内容
result8 = tree.xpath('/book//nick/text()')
print(result8)  # ['臭豆腐', '周大强', '周芷若', '周杰伦', '蔡依林', '热热热1', '热热热2', '热热热3', '胖胖东', '胖胖不东']

 


''' Xpath入门2

test.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8"/>
    <title>Title</title>
</head>
<body>
<div>
    <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>
    <h1>
        <span class="job">品鉴师</span>
        <span class="common">胡辣汤</span>
    </h1>
</div>
</body>
</html>
'''
from lxml import etree

# 解析一个文件用parse()
tree = etree.parse('test.html')
# result = tree.xpath('/html')
# print(result)

# 找到ul/li/a中的所有文本内容

result = tree.xpath('/html/body/div/ul/li/a/text()')
print(result)   # ['百度', '谷歌', '搜狗']
'''
错误描述:
    Traceback (most recent call last):
      File "D:/Project/Pycharm/Project3 Spider/demo2.py", line 466, in <module>
        tree = etree.parse('test.html')
      File "src\lxml\etree.pyx", line 3521, in lxml.etree.parse
      File "src\lxml\parser.pxi", line 1859, in lxml.etree._parseDocument
      File "src\lxml\parser.pxi", line 1885, in lxml.etree._parseDocumentFromURL
      File "src\lxml\parser.pxi", line 1789, in lxml.etree._parseDocFromFile
      File "src\lxml\parser.pxi", line 1177, in lxml.etree._BaseParser._parseDocFromFile
      File "src\lxml\parser.pxi", line 615, in lxml.etree._ParserContext._handleParseResultDoc
      File "src\lxml\parser.pxi", line 725, in lxml.etree._handleParseResult
      File "src\lxml\parser.pxi", line 654, in lxml.etree._raiseParseError
      File "test.html", line 6
    lxml.etree.XMLSyntaxError: Opening and ending tag mismatch: meta line 4 and head, line 6, column 8
错误原因:
    test.html文件中的开始和结束标签不匹配
    <meta charset="UTF-8">少了结束标签/
解决方法:
    <meta charset="UTF-8"/>中添加结束标签/即可
'''

# 找到ul/li/a中的第一个文本内容
result2 = tree.xpath('/html/body/div/ul/li[1]/a/text()')  # []表示取索引,xpath的下标是从1开始的
print(result2)  # ['百度']

# 找到href="dapao"的标签的值
result3 = tree.xpath('/html/body/div/ol/li/a[@href="dapao"]/text()')  # [@属性=属性值]表示属性的筛选
print(result3)  # ['大炮']

# 遍历获取ol/li/a中的所有文本内容
ol_li_list = tree.xpath('/html/body/div/ol/li')
# [<Element li at 0x26a63207cc0>, <Element li at 0x26a63207d40>, <Element li at 0x26a63207d80>]
for li in ol_li_list:
    result4 = li.xpath('./a/text()')  # ./表示当前所在位置,可理解为相对路径
    print(result4)
    # ['飞机']
    # ['大炮']
    # ['火车']


    # 获取a标签中属性href的属性值
    result5 = li.xpath('./a/@href')  # @href表示获取href的属性值
    print(result5)
    # ['feiji']
    # ['dapao']
    # ['huoche']


# 找到ul/li/a中所有href的值
result6 = tree.xpath('/html/body/div/ul/li/a/@href')
print(result6)
# ['http://www.baidu.com', 'http://www.google.com', 'http://www.sogou.com']

# 找到所有a标签中href的值
result7 = tree.xpath('/html//a/@href')
print(result7)
# ['http://www.baidu.com', 'http://www.google.com', 'http://www.sogou.com', 'feiji', 'dapao', 'huoche']

# 找到div的孙子标签spanahref
result8 = tree.xpath('/html/body/div/*/span/@class')
print(result8)
# ['job', 'common']

# 找到div下的所有任意子孙节点中a的文本内容
result9 = tree.xpath('/html/body/div//*/a/text()')
print(result9)
# ['百度', '谷歌', '搜狗', '飞机', '大炮', '火车']

小技巧:

# 在网页中按F12打开开发者工具,找到相应内容后,

# 右键,选择“copy”,选择“copy XPath”,即可复制该内容的xpath路径。

如下图所示

Xpath案例:抓取猪八戒网信息

''' Xpath案例:抓取猪八戒网信息
为什么找猪八戒网作为参考案例呢?因为它足够复杂!!!
这个网站搞定之后,xpath也基本掌握了。

'''

import requests
from lxml import etree
from time import sleep

url = 'https://wuhan.zbj.com/search/f/?'
headers = {
    'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3756.400 QQBrowser/10.5.4043.400'
}
params = {
    'type': 'new',
    'kw': 'saas',
}
# 请求页面获取相应
resp = requests.get(url, headers=headers, params=params)

# 解析相应
html = etree.HTML(resp.text)
# 拿到每一个服务商的div
divs = html.xpath('/html/body/div[6]/div/div/div[2]/div[5]/div[1]/div')
for div in divs:    # 每一个服务商信息
    # price = div.xpath('./div/div/a/div[2]/div[1]/span[1]/text()')[0].replace('¥', '') # 去除¥符号
    #
    price = div.xpath('./div/div/a/div[2]/div[1]/span[1]/text()')[0].strip('¥') # 去除¥符号
    trading_volume = div.xpath('./div/div/a[1]/div[2]/div[1]/span[2]/text()')[0]
    # info = div.xpath('./div/div/a/div[2]/div[2]/p/text()')
    # 此时的info中有多余的逗号,通过观察网页发现,逗号都可以用SaaS关键词替换
    # 现在进行拼接与替换,如下所示
    # 注意:这里是将列表拼接,join()语法为:str.join(sequence)

    info = 'SaaS'.join(div.xpath('./div/div/a/div[2]/div[2]/p/text()'))
    company_name = div.xpath('./div/div/a[2]/div[1]/p/text()')[0]
    location = div.xpath('./div/div/a[2]/div[1]/div/span/text()')[0]
    # print(price, trading_volume, info, company_name, location)
    '''
            一个一个地找标签很痛苦,也可以使用另一种方法:
            复制你要找的内容的xpath路径,比如这里要找公司名称,//*[@id="utopia_widget_74"]/a[2]/div[1]/p/text()
            通过id的属性值,在网页中找到utopia_widget_74所在的位置,
            往上找,看看现在/a[2]/div[1]/p/text()距离当前的相对位置有多少个标签,
            然后将//*[@id="utopia_widget_74"]修改成当前所在的相对位置,
            例如 ./div/div/a[2]/div[1]/p/text()
            该路径就是一个完整的xpath路径,即可找到你想要的内容,且不用一个一个地找标签,提高了效率。

            例子:
                name = div.xpath('./div/div/a[2]/div[1]/p/text()')
                print(name) # ['美讯科技-软件开发']
            '''
    '''
    下面的代码只能放在for div in divs:之内,放在外面则 猪八戒网.csv 中没有数据,为什么???
    '''
    with open('猪八戒网.csv', 'a', encoding='utf-8', newline='') as wstream:
        csvwriter = csv.writer(wstream)
        csvwriter.writerow([price, trading_volume, info, company_name, location])
        print('Done!')
        # sleep(0.5)

【生长吧!Python】有奖征文火热进行中:https://bbs.huaweicloud.com/blogs/278897

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。