文盲的Python入门日记:第七天,学习python下xml,xslt,xpath的使用,以及第一次python的抓取

17 篇文章 1 订阅
8 篇文章 0 订阅

现在使用python爬虫进行数据采集好像很流行啊,那我们也来学习下python的爬虫吧,嗯,看看龙叔的博客里python的技术路线图,很好,都涵。。。。等等,是不是漏了点什么?

仔细又看了遍技术路线图。。。里面好像没有提到xml,xslt,仅仅有xpath?

要问为什么老顾突然关心起了xml、xslt,那是因为,在老顾以往的采集生涯中,遇到过这样的奇葩。。。给个实例页面117 HR 3237 IH: Emergency Security Supplemental to Respond to January 6th Appropriations Act, 2021,美国一家法律服务网站里的历史案例信息,这个页面。。。看起来很像html,在浏览器里打开控制台查看元素,嗯,的确是html,但你一旦查看源文件,那就会一脸的发蒙。。。这是自带xslt样式的xml信息!!!

为了以后采集不会再被这样那样的xml站点挡住,所以我们还是先学好基础,把xml学好了再继续,毕竟html相对来说很容易解析了,什么etree啦,dom啦,xpath啦,甚至连css选择器我们基本都能实现了。虽然我给出的这个页面相对简单,xml比较规范,很容易提取信息,但。。。这不是html的内容!你得用xml的方式去搞!更何况,老顾碰到过在xslt中进行了一系列复杂的组合拼装后才能正确得到预期结果的xml内容!所以,在老顾的意识中,xml和xslt是必须优先于爬虫的学习的。

-----------------------------------

先贴知识来源XML处理模块 — Python 3.11.3 文档,在该文中,提到了xml的六个子模块,我们先一个一个的看过去,把用的到的先整理出来。带有命名空间的暂时不讨论。(题外话,老顾一直认为,编程最重要的是思路,其次是对使用的工具、语言、环境的了解,只需要达到知道这个语言里有什么包,每个包大概能做什么事,如何将思路用这些东西实现出来,就可以上手编程了,具体实现其实是个机械劳动)

1、xml.etree.ElementTree: ElementTree API,一个简单而轻量级的XML处理器

import xml.etree.ElementTree as ET

# 从文档加载xml,获取ElementTree ,即 XmlDocument
tree = ET.parse('xml_file.xml')
# 从ElementTree 对象获取根节点(Element),即 Node
root = tree.getroot()

# 使用字符串加载xml,并直接获取根节点(Element)
root = ET.fromstring(xml_doc_string)

# 使用ET.tostring将指定的Element对象输出为xml字符串文档,即 Node.OuterXML
xml_str = ET.tostring(root)

# Element.tag 获取指定的Element的标签名,即 NodeName
print(root.tag)

# Elment.attrib 获取指定的Element的属性词典,即 Attributes
print(root.attrib)

# Element 对象被遍历则获取当前Element的子节点,被遍历相当于 Node.ChildNodes
for e in root:
    print(e.tag)

# Element 可以直接当做list处理,任意字节点都可以叠加,直接使用数字做索引
root[0] # 返回 root的第一个子节点
root[0][0] # 返回 root的第一个子节点的第一个子节点

# Element 可以使用 iter 方法迭代所有后代(不限于子级)中的指定名称的Element
print([n.tag for n in root.iter('item')])

# Element.findall() 仅查找当前元素的直接子元素中带有指定标签的元素。 
# Element.find() 找带有特定标签的 第一个 子级,
# 然后可以用 Element.text 访问元素的文本内容。 
# Element.get 访问元素的属性:

# Element 的 text 属性为获取当前节点的文本内容(不含子节点),即 InnerText
print(root.text)

# Element 的 findall() 可以支持xpath,不加路径修饰为当前节点,如果加路径修饰,必须先声明当前节点,即 xpath 中带路径的内容必须 . 开头
print([n.tag for n in root.findall('.//item')])

# ElementTree.SubElement 可以新增子节点
ET.SubElement(root,newNodeName,{属性词典})
ElementTree 看完了,有几个总结
1、xpath 为简单的 xpath,没有全 xpath 支持,例如[name()="item"],除了文本比较([.]),属性比较([@])和子节点比较([tag])就好像没几个支持的函数,目前仅知道一个last()
2、因为 * 当做通配符了,在加上第一点,ET 不支持正则 xpath
3、text 仅为当前节点文本,不含子节点文本,虽然这个是一个很好的,但暂时还不太习惯

2、xml.dom:DOM API 定义

看了半天。。。。这是个基类,没任何实现方法,跳过。整体看这个文档的说明,和浏览器DOM是同一套东西,习惯js的人用这个应该很方便

3、xml.dom.minidom:最小的 DOM 实现

看了看这个部分,很简单的一点点内容,除了parse个parseString 之外没什么好看的了

结合xml.dom:DOM API定义的部分,基本就确定了,这个minidom是js很久以前的版本的DOM实现,不说没有querySelector这样的新鲜玩意,连getElementById都没给实现,就只有getElementsByXXXX,然后用 parseString测试了下,如果返回的html不标准,还不能解析,不能完全当做html解析器使用

4、xml.dom.pulldom:支持构建部分 DOM 树

这个部分,暂时没看懂。拉取解析器???这是干嘛的?看了看这个部分的几个例子,大概的感觉就是支持一些dom事件?算了吧,做采集,可以解析js,可以解析xml/xslt,可以解析html,就是用不到事件支持,跳过跳过。

5、xml.sax:SAX2 基类和便利函数

这又是什么鬼。。。直接 a = xml.sax.parseString('<r />'),居然提示 missing 1 required positional argument: 'handler',需要自己提供句柄?解析器?百度百度百度百度。。。

https://www.cnblogs.com/hongfei/p/python-xml-sax.html,这个文章里提供了一个实例,嗯。。。嗯嗯。。。嗯嗯。。。明白了,sax是包含了部分xsd、部分xslt、序列化结构化的一种解析方式,需要自己提供解析格式(类似xsd的工作),数据读取(类似xslt的工作),以及使之结构化序列化,具体的结构化序列化则靠handler的重写定义来实现,嗯,这些都很好,但我们采集的时候,很多时候是不看xml具体内容的,读取也用xml本身指定的xslt来解析,所以,还是跳过跳过

6、xml.parsers.expat:Expat解析器绑定

嗯嗯嗯。。。。第一感觉好像是 sax 的简化版?不管了,总之这个对采集没帮助。

------------------------------

看完了以上关于xml的介绍,大概只有etree方式比较符合c#、vb关于xml的处理?总之先这样,然后看看python怎么使用xslt解析xml,还得继续百度,这个 xml 包里,没有相关内容。。。晕死

https://www.cnblogs.com/gooseeker/p/5501716.html

哦吼。。。

还得从新安装一个python包,lxml,好吧。。。。

------------------------------

还是以117 HR 3237 IH: Emergency Security Supplemental to Respond to January 6th Appropriations Act, 2021这个页面为例,他的xslt样式在声明里已经给出了,xslt地址是https://www.govinfo.gov/content/pkg/BILLS-117hr3237ih/xml/billres.xsl

做个简单的测试,来尝试我们的第一次抓取和xslt解析,慢慢来,这个过程是个漫长的过程,试错的过程,另外,对现在的学生也劝告一句,学会看提示,学会百度,才能真正的自学并且提高自己,不要那么简单的问题就上问答。

# 第一步,采集我们的目标内容,地址如下
# https://www.govinfo.gov/content/pkg/BILLS-117hr3237ih/xml/BILLS-117hr3237ih.xml

from urllib import request

xml_doc = request.urlopen('https://www.govinfo.gov/content/pkg/BILLS-117hr3237ih/xml/BILLS-117hr3237ih.xml')
print(xml_doc)

第一步就出错了,嘿嘿,意料之中 HTTPError: Forbidden 

为什么呢,因为使用python采集,默认的请求头信息缺失太多了,很多网站对请求头信息都有基本的识别,最起码agent信息要看一下的,如果是什么php开头的,包含spider或者bot的,什么 java-的,都直接拒掉了,python这个,默认的请求头是什么我虽然没看,不过估计也是不含浏览器信息的,所以,我们第二步,伪造一个user-agent

from urllib import request

req = request.Request('https://www.govinfo.gov/content/pkg/BILLS-117hr3237ih/xml/BILLS-117hr3237ih.xml')
req.encoding = 'utf-8'
req.add_header('user-agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3870.400 QQBrowser/10.8.4405.400')
xml_str = request.urlopen(req).read()
print(xml_str)

python做采集还是真简单,以前老顾在c#里做采集,单单一个采集类,就写了快2千行,用以支持各种情况,不知道urllib.request能做到什么程度,这个我们稍后再讨论,来看看这次采集的结果。。

嗯,采集成功了。。。。问题来了,这采集到的内容是字符串吗?为毛前边还多了个修饰符b?

经过几次折腾,发现带修复符b的时候,对正则支持不友好,使用etree对这个变量解析,直接报错,总是提示第一行第一列不是<尖括号,这就tmd离谱,后来就在想,我直接一个字符一个字符取出来,从新组织一下,弄成不带b修饰的好了

print([n for n in xml_str])

结果在遍历这个字符串的时候。。。。Hmmm。。。这怎么都是数字,哦哦哦!c# byte[] 类型啊!这直接给我显示成字符串,还让我给误会了。。。。加b修饰,是二进制字符串!得了,知道原因,后边就简单了,我直接解码就完了

xml_str = xml_str.decode(encoding='utf-8')
print(xml_str)

哎,这才对吗。。。这才是我们真正采集到的xml文本内容

然后,把采集到的内容转成xml

import lxml.etree as ET

xml_doc = ET.XML(xml_str)
print(xml_doc)

唔。。。是个Element对象 

print(ET.tostring(xml_doc))

看起来, lxml.etree 和 xml.etree.ElementTree 很类似啊,除了加强了xpath和追加了xslt支持,其他的好像和xml.etree.ElementTree都保持了一致,嗯,前边大家都看过ElementTree了,从c#或vb转过来的同学有福了,不用再看lxml.etree了

正好,这是一个比较完整的xml文档,我们来试试xpath能做到什么程度

....

....

....

xpath好像没有加强?又是一怒之下打开定义文件看看 site-packages\lxml\_elementpath.py

节选一下内容啊

    if signature == "@-":
        # [@attribute] predicate
    if signature == "@-='":
        # [@attribute='value']
    if signature == "-" and not re.match(r"-?\d+$", predicate[0]):
        # [tag]
    if signature == ".='" or (signature == "-='" and not re.match(r"-?\d+$", predicate[0])):
        # [.='value'] or [tag='value']
    if signature == "-" or signature == "-()" or signature == "-()-":
        # [index] or [last()] or [last()-index]
    raise SyntaxError("invalid predicate")

?????说好的加强呢?这不就还是这么几个么?不说xpath2.0,xpath1.0也有不少函数和方法呢啊,结果python的这几个包,都是自己实现的xpath简单支持,根本不是真正的xpath啊!!!算了,是我太幼稚了。。。继续搞这个xml的采集。。。。

print(re.findall('<\?xml[:-]stylesheet.*?href="([^"]+)',xml_str))

既然是xml文件,我们就检查一下,这个文件是否指定了xslt样式文件,如果有的话,需要把样式文件也拿下来,作为xslt来解析这个xml本身

xslt_url = re.sub(r'(?<=[/\\])[^/\\]+$',re.findall('<\?xml[:-]stylesheet.*?href="([^"]+)',xml_str)[0],req.full_url)
print(xslt_url)

嗯哼,那就继续采集这个xslt文件

req = request.Request(xslt_url)
req.encoding = 'utf-8'
req.add_header('user-agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3870.400 QQBrowser/10.8.4405.400')
xsl_str = request.urlopen(req).read()
xsl_str = xsl_str.decode(encoding='utf-8')
xslt_doc = ET.XML(xsl_str)
print(xslt_doc)

很好,又一个正常的xml文档,现在,来尝试把他弄成 xslt

xsl_parse = ET.XSLT(xslt_doc)

又是预料之中的报错,不过错误提示没见过,来看看 XSLTParseError: Cannot resolve URI string://__STRING__XSLT__/billres-details.xsl

搜噶,这个xslt文件里,还include了别的xslt文件,结果因为采集过来的文件不全,且没有物理保存,所以相对地址就找不到文件了。那么,直接修改 xslt_str 里的相关内容数据就好,从新生成 xslt_doc

xsl_str = re.sub('(<xsl:include href=")([^"]+)("/>)',r'\1'+re.sub(r'(?<=[/\\])[^/\\]+$','',req.full_url)+r'\2\3',xsl_str)
xslt_doc = ET.XML(xsl_str)
print(xslt_doc)
xsl_parse = ET.XSLT(xslt_doc)

还报错 XSLTParseError: Cannot resolve URI https://www.govinfo.gov/content/pkg/BILLS-117hr3237ih/xml/billres-details.xsl

Hmmmm.....无法解析URI,和刚才的错误一样,除了地址不一样,Hmmm.....好吧,不在同一个文件夹,他没有办法自动加载进来,换个方式,把相关xslt文件,都下载到同一个文件夹里,然后再进行加载试试看。hmmmmmm,又要去补知识点了,文件读写怎么搞,文件带编码怎么搞,判断文件是否存在怎么搞。。。。转一门新语言真费劲。

好了,我们把代码重置一下,重置到刚刚获得xml内容的时候,代码如下

import lxml.etree as ET
from urllib import request

req = request.Request('https://www.govinfo.gov/content/pkg/BILLS-117hr3237ih/xml/BILLS-117hr3237ih.xml')
req.encoding = 'utf-8'
req.add_header('user-agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3870.400 QQBrowser/10.8.4405.400')
xml_str = request.urlopen(req).read()
xml_str = xml_str.decode(encoding='utf-8')
xml_doc = ET.XML(xml_str)

然后,从xml里使用正则获取所有的 xslt 文件,嗯还要记录这个xml的uri地址,用来计算xslt文件的位置,否则采集不到了哦

base_url = re.sub(r'(?<=[/\\])[^/\\]+$','',req.full_url)
xslt_url = re.findall('<\?xml[:-]stylesheet.*?href="([^"]+)',xml_str)
xslt_first = xslt_url[0]

因为不确定 xslt 里是否还嵌套这其他 xslt 文件,所以,我们这里用数组来表示 xslt 文件,并记录 xslt 的入口文件

if os.path.exists(r'D:\\work\\Log\\' + xslt_first)==False: # 如果入口 xslt 文件不存在
    while len(xslt_url)>0: # 如果待采集的 xlst 文件列表不为空
        fn  = xslt_url.pop(0) # 赋值给 fn 为当前采集 xslt 文件名,并从列表中移除
        url = base_url + fn   # 计算出 xslt 的真实 uri
        req_xslt = request.Request(url)
        req_xslt.encoding = 'utf-8'
        req_xslt.add_header('user-agent', 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.25 Safari/537.36 Core/1.70.3870.400 QQBrowser/10.8.4405.400')
        xsl_str = request.urlopen(req_xslt).read()
        xsl_str = xsl_str.decode(encoding='utf-8')  # 获取到 xslt 文件内容
        f = open(r'D:\\work\\Log\\'+fn,'w+',encoding='utf-8')  # 以覆盖写方式打开临时文件夹同名文件
        f.write(xsl_str) # 写入 xslt 信息
        f.close()
        xslt_url += re.findall(r'(?<=xsl:include href=")([^"]+)',xsl_str) # 如果当前 xslt 文件中有引入其他 xslt 文件,则加入到待下载列表中

嗯嗯,这样一看,我们就下载了三个 xslt 文件,除了之前报错的那个,还多了一个 table.xsl

来进行最后一步吧,让我们变个魔术,把 xml 变成 html ~~~~

xslt_doc = ET.parse(r'D:\\work\\Log\\' + xslt_first)
xsl_parse = ET.XSLT(xslt_doc)
result = xsl_parse(xml_doc)
print(result)

so,我们的第一次采集尝试完毕,xml及xslt解析尝试完毕,文件读写尝试完毕,可喜可贺可喜可贺。

---------------------------------

后记:这篇博文比较难产了,因为老顾平时也是在正常工作,都是抽时间研究研究,希望大家多多提一些意见。

预告:下一篇,封装一个采集类,用以继承采集到的信息,伪造头信息等,用来准备正式的采集。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

文盲老顾

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值