使用scrapy的XMLFeedSpider解析XML
0x00
scrapy中,内置了高效解析XML格式文本的XMLFeedSpider
.但是由于对XML文档的不熟悉,还有XMLFeedSpider
内部的一些坑,学起来并不像文档里介绍的那样简单.
0x01
XMLFeedSpider
先来个官网实例:
from scrapy import log
from scrapy.contrib.spiders import XMLFeedSpider
from myproject.items import TestItem
class MySpider(XMLFeedSpider):
name = 'example.com'
allowed_domains = ['example.com']
start_urls = ['http://www.example.com/feed.xml']
iterator = 'iternodes' # This is actually unnecessary, since it's the default value
itertag = 'item'
def parse_node(self, response, node):
log.msg('Hi, this is a <%s> node!: %s' % (self.itertag, ''.join(node.extract())))
item = TestItem()
item['id'] = node.xpath('@id').extract()
item['name'] = node.xpath('name').extract()
item['description'] = node.xpath('description').extract()
return item
iterator
用于确定使用哪个迭代器的string。可选项有:
iternodes
- 一个高性能的基于正则表达式的迭代器html
- 使用 Selector 的迭代器。 需要注意的是该迭代器使用DOM进行分析,其需要将所有的DOM载入内存, 当数据量大的时候会产生问题。xml
- 使用 Selector 的迭代器。 需要注意的是该迭代器使用DOM进行分析,其需要将所有的DOM载入内存, 当数据量大的时候会产生问题。
默认值为 iternodes 。*迭代器有坑,稍后细说*
itertag
一个包含开始迭代的节点名的string。例如:
itertag = 'product'
parse_node(response, selector)
当节点符合提供的标签名时(itertag)该方法被调用。 接收到的response以及相应的 Selector 作为参数传递给该方法。 该方法返回一个 Item 对象或者 Request 对象 或者一个包含二者的可迭代对象(iterable)。
其他组件:
namespaces
一个由 (prefix, url) 元组(tuple)所组成的list。 其定义了在该文档中会被spider处理的可用的namespace。 prefix 及 uri 会被自动调用 register_namespace() 生成namespace。
您可以通过在 itertag 属性中制定节点的namespace。例如:
class YourSpider(XMLFeedSpider):
namespaces = [('n', 'http://www.sitemaps.org/schemas/sitemap/0.9')]
itertag = 'n:url'
# ...
adapt_response(response)
该方法在spider分析response前被调用。您可以在response被分析之前使用该函数来修改内容(body)。 该方法接受一个response并返回一个response(可以相同也可以不同)。
process_results(response, results)
当spider返回结果(item或request)时该方法被调用。 设定该方法的目的是在结果返回给框架核心(framework core)之前做最后的处理, 例如设定item的ID。其接受一个结果的列表(list of results)及对应的response。 其结果必须返回一个结果的列表(list of results)(包含Item或者Request对象)。
0x02
xmlns
首先介绍一个xml文档标签xmlns
<Vulnerability xmlns="http://www.icasi.org/CVRF/schema/vuln/1.1" Ordinal="1573">
在这里用xpath按照其他HTML文档的样子提取不能得到这个标签包含的文档:
response.xpath(//Vulnerability[@xmlns])
原因在于xmlns
是命名空间的标签,而*用于标示命名空间的地址不会被解析器用于查找信息。其惟一的作用是赋予命名空间一个惟一的名称。*详见W3C-XML
XMLFeedSpider
爬坑之旅
用XMLFeedSpider
解析带xmlns
标签的文档
根据官方例子,构造XMLFeedSpider
类:
class CVEspider(XMLFeedSpider):
name = 'CVEspider'
allowed_domains = 'cve.mitre.org'
start_urls = [
'https://cve.mitre.org/data/downloads/allitems-cvrf-year-1999.xml'
]
iterator = 'iternodes'
itertag = 'Vulnerability'
def parse_node(self, response, node):
# prase codes....
坑 · No.1
运行发现程序根本进不到parse_node
方法,而且报错: IndexError: list index out of range
定位到报错文件:scrapy.utils.iterators.xmliter()
def xmliter(obj, nodename):
"""Return a iterator of Selector's over all nodes of a XML document,
given the name of the node to iterate. Useful for parsing XML feeds.
obj can be:
- a Response object
- a unicode string
- a string encoded as utf-8
"""
nodename_patt = re.escape(nodename)
HEADER_START_RE = re.compile(r'^(.*?)<\s*%s(?:\s|>)' % nodename_patt, re.S)
HEADER_END_RE = re.compile(r'<\s*/%s\s*>' % nodename_patt, re.S)
text = _body_or_str(obj)
header_start = re.search(HEADER_START_RE, text)
header_start = header_start.group(1).strip() if header_start else ''
header_end = re_rsearch(HEADER_END_RE, text)
header_end = text[header_end[1]:].strip() if header_end else ''
r = re.compile(r'<%(np)s[\s>].*?</%(np)s>' % {'np': nodename_patt}, re.DOTALL)
for match in r.finditer(text):
nodetext = header_start + match.group() + header_end
yield Selector(text=nodetext, type='xml').xpath('//' + nodename)[0]
报错原因就是
yield Selector(text=nodetext, type='xml').xpath('//' + nodename)[0]
这句代码中xpath('//' + nodename)
并不能获取到有xmlns
标签的XML文档。一个可行的解决办法是重构xmliter()
方法,将最后一句改为:
yield Selector(text=nodetext, type='xml').xpath('//*[local-name()="%s"]' % nodename)[0]
这里的local-name()
方法作用是获取不包含namespace
的前缀。
然而重构scrapy的底层代码并不优雅。
更好的方法是使用xml
迭代器+namespace
class CVEspider(XMLFeedSpider):
name = 'CVEspider'
allowed_domains = 'cve.mitre.org'
start_urls = [
'https://cve.mitre.org/data/downloads/allitems-cvrf-year-1999.xml'
]
iterator = 'xml'
namespaces = [
("vuln", "http://www.icasi.org/CVRF/schema/vuln/1.1")
]
itertag = 'vuln:Vulnerability'
def parse_node(self, response, node):
# prase codes...
坑 · No.2
parse_node
方法中的node
是符合itertag
的一个节点。然而坑爹的是node
会为它的子节点都加上itertag
的namespace。直接打开xml文档是看不到这些子节点上的xmlns
标签的!所以要在解析时加上namespace
:
cve_item['CVE_id'] = node.xpath("//CVE/text()").extract()[0] # 不能正确解析
cve_item['CVE_id'] = node.xpath("vuln:CVE/text()").extract()[0] # 可以正确解析
cve_item['CVE_id'] = node.xpath("//*[local-name()='CVE']/text()").extract()[0] # 可以正确解析
对于多重嵌套的节点,要在每个节点标签上都加上namespace
:
cve_item['published_at'] = self.convert_time(node.xpath("vuln:Notes/vuln:Note[@Title='Published']/text()").extract()[0])
0x03
之前解析xml文档都没注意过xmlns
标签,而这些问题,都是因为这个标签所引起的。
总结一下:
- 用
XMLFeedSpider
解析XML文档时,如果文档含有xmlns
标签,最好使用iterator = 'xml'
这个迭代器。 parse_node
方法中的node
会为它的所有子节点加上namespace
,解析时要加上相应的namespace
。
0x04
references:
不同的节点有不通的命名空间,这个在定义itertag时该如何定义啊