爬虫Part2——数据解析与提取

[TOC]

数据解析概述

当需要只是需要部分网页的内容而不是全部时,就要用到数据提取:

  1. Re解析

  2. Bs4解析

  3. Xpath解析

正则表达式

语法

正则表达式(Regular Expression)是⼀种使用表达式的方式对字符串进行匹配的语法规则。

在线测试正则表达式:https://tool.oschina.net/regex/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
元字符: 具有固定含义的特殊符号

. 匹配除换⾏符以外的任意字符
\w 匹配字⺟或数字或下划线
\s 匹配任意的空⽩符
\d 匹配数字
\n 匹配⼀个换⾏符
\t 匹配⼀个制表符

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

\W 匹配⾮字⺟或数字或下划线
\D 匹配⾮数字
\S 匹配⾮空⽩符
a|b 匹配字符a或字符b
() 匹配括号内的表达式,也表示⼀个组
[...] 匹配字符组中的字符 # 是否属于[a-zA-Z0-9]
[^...] 匹配除了字符组中字符的所有字符 # 这里^表示非


量词: 控制前⾯的元字符出现的次数

* 重复零次或更多次(尽可能多地去匹配)
+ 重复⼀次或更多次
? 重复零次或⼀次
{n} 重复n次
{n,} 重复n次或更多次
{n,m} 重复n到m次


匹配:贪婪匹配和惰性匹配

.* 贪婪匹配 # 尽可能长的匹配
.*? 惰性匹配 # 回溯到最短的一次匹配

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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模块(Regular Expression)

基本使用

findall()

1
2
3
4
5
# findall: 匹配字符串中所有符合正则的内容,返回list
lst = re.findall(r"\d+", "5点之前. 你要给我5000万")
print(lst)

# ['5', '5000']

字符串前加r防止转义,表示原生字符串(rawstring)。

不使用r,则匹配时候需要4个反斜杠:正则需要转化一次,python解释器需要转化。

finditer()

1
2
3
4
5
6
7
8
9
10
# finditer: 匹配字符串中所有符合正则的内容,返回iter
it = re.finditer(r"\d+", "5点之前. 你要给我5000万") # 字符串前加r防止转义
for i in it:
print(i)
print(i.group())

# <_sre.SRE_Match object; span=(0, 1), match='5'>
# 5
# <_sre.SRE_Match object; span=(10, 14), match='5000'>
# 5000
  • 要使用.group()提取match对象的value
1
2
3
4
5
# search: 找到第一个就返回,返回的是match,使用group()提取
s = re.search(r"\d+", "5点之前. 你要给我5000万")
print(s.group())

# 5

match()

1
2
3
4
5
# match: 从第一个字符开始要求匹配,返回的是match
m = re.match(r"\d+", "5点之前. 你要给我5000万")
print(m.group())

# 5
  • 如果字符串为"在5点之前. 你要给我5000万"则匹配失败
  • 相当于自带^

compile()

1
2
3
4
5
6
7
8
9
10
rule = re.compile(r"\d+")
lst = rule.findall("5点之前. 你要给我5000万")
print(lst)

# ['5', '5000']

m = rule.match("5点之前. 你要给我5000万")
print(m.group())

# 5
  • 预加载正则表达式
  • 可以反复使用

group()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
s = """
<div class='⻄游记'><span id='1'>中国联通</span></div>
<div class='⻄'><span id='2'>中国</span></div>
<div class='游'><span id='3'>联通</span></div>
<div class='记'><span id='4'>中通</span></div>
<div class='⻄记'><span id='5'>国联</span></div>
"""
obj = re.compile(r"<div class='(?P<class>.*?'><span id)='(?P<id>\d+)'>(?P<val>.*?)</span></div>", re.S) # re.S表示使.能够匹配换行符
result = obj.search(s)
print(result.group()) # <div class='⻄游记'><span id='1'>中国联通</span></div>
print(result.group("id")) # 1
print(result.group("val")) # 中国联通
print(result.group("class")) # ⻄游记'><span id

  • 分组:使用(?P<变量名>正则表达式)进一步提取内容

实战案例

豆瓣top250电影排行

  • 首先要确认目标数据位置(源码or抓包)
  • 抓不到的时候首先看User-Agent
  • 次数太频繁的时候记得Keep-Alive
  • 正则表达式写的越详细越容易匹配
  • ①用\n\s*去匹配换行和空格;②用.strip()去除空格
  • match.groupdict()转换成字典
  • newline=""不使用自动换行符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import csv
import re
import requests

# 预编译正则表达式: "电影名称","上映年份","评分","评分⼈数"
rule = re.compile(r'<li>.*?<span class="title">(?P<name>.*?)</span>.*?'
r'<br>\n\s*(?P<year>\d+)&nbsp;/&nbsp;.*?'
r'property="v:average">(?P<score>.*?)</span>.*?'
r'<span>(?P<person>.*?)人评价</span>', re.S)

# 准备文件写入
f = open("data.csv", mode="w", encoding="utf-8", newline="") # 设置newline=""不使用多余换行符
csv_writer = csv.writer(f)

# 获取网页源码
url = "https://movie.douban.com/top250"
headers = {
"user-agent": "user-agent",
'Keep-Alive': 'timeout=15'
}

for p in range(0, 250, 25):
# 跳转多个页面
params = {
'start': p
}
resp = requests.get(url=url, headers=headers, params=params)
# print(resp.text)

# 数据解析
result = rule.finditer(resp.text)
for i in result:
# print(i.group("name"))
# print(i.group("year")) # 取出空白符也可以使用.strip()
# print(i.group("score"))
# print(i.group("person"))
dic = i.groupdict() # 转换成dict
print(dic)
csv_writer.writerow(dic.values())

f.close()

电影天堂板块信息

  • html中标签<a href='url'>xxx</a>表示超链接
  • 当编码不一致时,根据网页源代码的标注信息charset=gb2312进行修正: resp.encoding = "gb2312"
  • verify=False去除安全认证
  • 拼接域名时要注意/数量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import re
import requests

# 1.获取主页源码
domain = "https://www.dytt89.com/"
resp = requests.get(domain, verify=False) # 去除安全认证
resp.encoding = "gb2312" # 修改成指定字符集
# print(resp.text)

# 2.定位2021必看片板块
rule1 = re.compile(r"2021必看热片.*?<ul>(?P<ul>.*?)</ul>", re.S)
result1 = rule1.search(resp.text)
ul = result1.group("ul").strip()
# print(ul)

# 3.从板块中提取子页面的链接地址
rule2 = re.compile(r"href='(?P<href>.*?)'", re.S)
result2 = rule2.finditer(ul)
child_href_list = []
for h in result2:
child_href = domain + h.group("href").strip("/") # domain中也有/,去掉一个
child_href_list.append(child_href)
# print(child_href)

# 4.请求子页面地址,拿到下载地址
rule3 = re.compile(r'<meta name=keywords content="(?P<name>.*?)下载">.*?'
r'<td style="WORD-WRAP: break-word" bgcolor="#fdfddf"><a href="(?P<download>.*?)">', re.S)
download_list = {}
for c in child_href_list:
child_resp = requests.get(c, verify=False) # 去除安全认证
child_resp.encoding = "gb2312" # 修改成指定字符集
result3 = rule3.search(child_resp.text)
download_list[result3.group("name")] = result3.group("download")

for d in download_list.items():
print(d)

Bs4模块(Beautiful Soup)

html语法规则

HTML(Hyper Text Markup Language)超文本标记语言,是我们编写网页的最基本也是最核心的⼀种语⾔,其语法规则就是用不同的标签对网页上的内容进行标记,从⽽使网页显示出不同的展示效果。

1
2
3
4
5
6
7
8
9
10
11
<!--第一种标记-->
<标签
属性="值"
属性="值">
被标记的内容
</标签>

<!--第二种标记-->
<标签 属性="值"
属性="值"/>

基本使用

通过网页源码建立BeautifulSoup对象,来检索页面源代码中的html标签。

  • find(标签, 属性=值) 只找一个
  • find_all(标签, 属性=值) 找出所有

实战案例

新发地菜价

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import csv
import requests
from bs4 import BeautifulSoup

url = "http://www.xinfadi.com.cn/marketanalysis/0/list/1.shtml"
resp = requests.get(url)

f = open("菜价.csv", mode="w", encoding="utf-8", newline="")
csv_writer = csv.writer(f)

# 1. 把页面源代码交给BeautifulSoup进行处理, 生成bs对象
page = BeautifulSoup(resp.text, "html.parser") # 指定html解析器

# 2. 从bs对象中查找数据,找到目标表格
attrs = {
"class": "hq_table"
}
table = page.find("table", attrs=attrs)

# 3. 拿到有效的数据行
trs = table.find_all("tr")[1:]

# 4. 对每行数据分解属性
for tr in trs:
tds = tr.find_all("td") # 拿到每行中的td
name = tds[0].text # .text 表示拿到被标签标记的内容
low = tds[1].text
avg = tds[2].text
high = tds[3].text
spec = tds[4].text # 规格
unit = tds[5].text # 单位
day = tds[6].text # 发布日期
print(name, low, avg, high, spec, unit, day)
csv_writer.writerow([name, low, avg, high, spec, unit, day])

f.close()
  • 这里通过URL可以发现

  • 规避Python关键字属性的方法

    1
    2
    table = page.find("table", class_="hq_table")
    table = page.find("table", attrs={"class": "hq_table"})

    优美图库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import requests
from bs4 import BeautifulSoup

url = "https://www.umei.cc/bizhitupian/weimeibizhi/"
resp = requests.get(url=url)
resp.encoding = "utf-8"

# 解析主页面
main_page = BeautifulSoup(resp.text, "html.parser")
a_list = main_page.find("div", class_="TypeList").find_all("a")
for a in a_list:
href = url + a.get("href").split("/")[-1] # 手动拼接url
# print(href)
child_page_resp = requests.get(href)
child_page_resp.encoding = "utf-8"
# 解析子界面
child_page = BeautifulSoup(child_page_resp.text, "html.parser")
p = child_page.find("p", align="center") # 找到标签为p、居中对齐的超文本
img = p.find("img") # 找到标签为img的超文本
src = img.get("src") # 取出src属性的值
# 下载图片
img_resp = requests.get(src) # 该响应文件本身就是图片
img_name = src.split("/")[-1] # 设定文件名
with open("source/" + img_name, mode="wb") as f:
f.write(img_resp.content) # 将拿到的响应内容以字节流形式写入文件
print(img_name + " has been downloaded!")

Xpath

基本使用

XPath是一门在 XML 文档中查找信息的语言。XPath可⽤来在 XML文档中对元素和属性进行遍历,而我们熟知的HTML恰巧属于XML的⼀个⼦集,所以完全可以用Xpath去查找html中的内容。

  • demo1:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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>惹了</nick>
</div>
</author>
<partner>
<nick id="ppc">胖胖陈</nick>
<nick id="ppbc">胖胖不陈</nick>
</partner>
</book>
"""

tree = etree.XML(xml)
result = tree.xpath("/book") # [<Element book at 0x26943009048>]
result = tree.xpath("/book/name") # [<Element name at 0x2403a5ffb48>]
result = tree.xpath("/book/name/text()") # ['野花遍地⾹']
result = tree.xpath("/book/author/nick") # [<Element nick at 0x2702b1dfb08>, ……, <Element nick at 0x2702b3516c8>]
result = tree.xpath("/book/author/nick/text()") # ['周⼤强', '周芷若', '周杰伦', '蔡依林']
result = tree.xpath("/book/author/div/nick/text()") # ['惹了']
result = tree.xpath("/book/author//nick/text()") # 任意层后代 ['周⼤强', '周芷若', '周杰伦', '蔡依林', '惹了']
result = tree.xpath("/book/author/*/nick/text()") # 单层通配符 ['惹了']

print(result)
  • demo2:
1
2
3
4
5
6
7
8
9
10
11
12
13
from lxml import etree

tree = etree.parse("my_html.html")
result = tree.xpath("/html/body/ul/li/a/text()") # ['百度', '⾕歌', '搜狗'] 使用开发者工具快速定位xpath
result = tree.xpath("/html/body/ul/li[1]/a/text()") # ['百度'] 从1开始计数
result = tree.xpath("/html/body/ol/li/a[@href='dapao']/text()") # ['⼤炮'] 属性筛选
result_list = tree.xpath("/html/body/ol/li")
for result in result_list:
r1 = result.xpath("./a/text()")
print(r1) # ./ 当前位置
r2 = result.xpath("./a/@href")
print(r2) # @ 提取属性值

开发者工具使用技巧

  • 快速定位网页资源在HTML中的位置

image-20210602214418408

  • 快速获取XPath,并做一些微调

image-20210602214849284

实战案例

猪八戒网

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import requests
from lxml import etree
import csv

f = open("猪八戒.csv", mode="w", encoding="utf-8", newline="")
csv_writer = csv.writer(f)

# 获得主页面
url = "https://beijing.zbj.com/search/f/?type=new&kw=saas"
resp = requests.get(url=url)
html = etree.HTML(resp.text)
divs = html.xpath("/html/body/div[6]/div/div/div[2]/div[5]/div[1]/div") # 先定位到div[1],然后改成div
# 每一个服务商
for div in divs:
price = div.xpath('./div/div/a[1]/div[2]/div[1]/span[1]/text()')[0].strip("¥") # 使用工具定位完整xpath,然后将重复地址替换./
title = "SAAS".join(div.xpath('./div/div/a[1]/div[2]/div[2]/p/text()')) # join()通过指定字符连接序列中元素,生成新字符串
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]
csv_writer.writerow([price, title, company_name, location])
print([price, title, company_name, location])