[TOC] # Web页面数据解析处理方法 > `urllib3`和`requests`库都是围绕支持`HTTP`协议实现`客户端`功能。但是在对页面解析处理上他们可能并不能提供更多的帮助,我们需要借助专门的Web页面数据解析库来解析并提取出结构化结果数据。 Web页面的解析方法通常可以用下面三种方法: - `正则表达式` : 将页面当做文本处理,简单直接,大面积撒网。 - `XPath` : `XPath`路径表达式可以让我们像访问目录一样访问Web页面的所有节点元素, 精准匹配。 - `CSS Selector` : CSS 选择器`Selector` 与`XPath`相似,以CSS样式的表达式来定位节点元素, 精准匹配。 ## 正则表达式 `Python`中的正则表达式库`re` 是我们最为常用的正则库,一条正则表达式可以帮我们过滤掉无用数据,只提取我们需要的格式数据。 在处理网页时,我们并不会频繁的全页面匹配,可想而知这样的效率极低,通常我们需要缩小数据匹配的范围节点,在小范围内进行正则匹配。而缩小范围的方法常常会结合`XPath`或者`Selector`一起完成,所以要学会三种方法的配合使用。 有时候,可能一条正则表达式就可以提取出所有我们需要的结果,例如下面这个提取`IP:端口`的正则表达式: ```Python import requests import re url = 'https://free-proxy-list.net/anonymous-proxy.html' r = requests.get(url, timeout=10) pr_re = r'<td.*?>.*?(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}).*?</td>.*?<td.*?>.*?(\d+).*?</td>' proxies = re.findall( pr_re, r.text) proxy_list=[] for proxy in proxies: proxy_list.append(':'.join(proxy[0:2])) print('\n'.join(proxy_list)) ``` ## `XPath` > `XPath`路径表达式可以让我们像访问目录一样访问Web页面的所有节点元素, 精准匹配。 想了解`XPath`的详细信息,可以阅读[XPath快速了解](XPath快速了解.md),接下来我们说明下如何在`Python`中使用`XPath` 支持`XPath`的库有`lxml`、`parsel` ,其实还有很多,只不过这两个是`API`接口非常好用的库。 `lxml`是基于C语言开发库`libxml2`和`libxslt`实现的,因此速度上是非常快的(远高于Python自带的`ElementTree`,所以`ElementTree`很少被使用)。并且使用`cssselect`库扩展支持了`CSS选择器`接口。 `parsel`则是在`lxml`基础上的更高级别封装,并且提供了`XPath`、`CSSSelector`和`re`正则表达式三种提取方式的支持,封装的接口也是更加简单易用。同时,`parsel`也是`scrapy`所使用的选择器。 ### lxml`XPath`解析示例 `XPath`表达式有时候我们不知道如何写时,我们可以通过功浏览器的开发者工具帮助获取`XPath`,具体方法为: `访问目标URL` => 按`F12`打开开发者模式 => 选择`Elements`tab页 => 右键要定位的元素 => 选择`Copy`中的`Copy XPath`。 如下图所示: ![xpath](https://img.kancloud.cn/1d/cc/1dcce1d6d7bf625c949f406b690bc072_841x729.png =300x) 通过此方法得到的`XPath`,可能很长,或者冗余信息太多,我们只需要在得到的`XPath`表达式上进行优化即可。 #### 示例一:简单的xpath使用提取博客文章列表 ```Python import requests as req from lxml import etree url='https://www.learnhard.cn/feed' resp = req.get(url) doc = etree.HTML(resp.content) items = doc.xpath('//item/title/text()') print('\n'.join(items)) ``` #### 示例二:获取微博实时热搜排行榜 > 微博实时热搜的`cookies`信息需要设置一下,不用登录。 ```Python import requests as req from lxml import etree import re headers = { 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36', 'authority': 'weibo.com', 'cookie': 'UM_distinctid=171437c9856a47-0cd9abe048ffaf-1528110c-1fa400-171437c9857ac3; CNZZDATA1272960323=1824617560-1569466214-%7C1595951362; SCF=AhjAkJNek3wkLok6WSbiibV1WsGffKPYsDlTZtFqiUH_YWL81nk-0xKkiukxpRoDMoIoV0IClwWecgXLLPiBZrw.; SUHB=0gHTlGutSIGq9P; ALF=1628229198; SUB=_2AkMoasG0f8NxqwJRmfoUxG7ibIl_ww7EieKeNjBvJRMxHRl-yT9jqlYktRB6A-rvW2hROYk5DlHgX7r_dk67bcEdfhBN; SUBP=0033WrSXqPxfM72-Ws9jqgMF55529P9D9WWh..ORuiFeK.mEWDWeecX1; SINAGLOBAL=1133654064055.2583.1597394566906; UOR=,,www.comicyu.com; login_sid_t=f855cdd8714fdb25dee824ce5ff8d792; cross_origin_proto=SSL; Ugrow-G0=6fd5dedc9d0f894fec342d051b79679e; TC-V5-G0=4de7df00d4dc12eb0897c97413797808; wb_view_log=1914*10771.0026346445083618; _s_tentry=weibo.com; Apache=4531467438705.743.1597800659782; ULV=1597800659793:3:2:1:4531467438705.743.1597800659782:1597394566920; TC-Page-G0=d6c372d8b8b800aa7fd9c9d95a471b97|1597800912|1597800912; WBStorage=42212210b087ca50|undefined' } def main(): url='https://weibo.com/a/hot/realtime' resp = req.get(url, headers = headers) doc = etree.HTML(resp.text) topic_list = doc.xpath('//div[@class="UG_content_row"]') for topic in topic_list: desc = topic.xpath('.//div[@class="list_des"]')[0] topic_title = desc.xpath('h3[@class="list_title_b"]/a/text()')[0].strip() subinfo = desc.xpath('./div')[0].xpath('string(.)').strip().replace(' ','') subinfo = re.sub('\s+',',', subinfo) subinfo = re.findall(r'(.*?),(.*?),.*?([0-9]*?),.*?([0-9]*?),.*?([0-9]+)', subinfo)[0] print(topic_title + ',' + ','.join(subinfo)) if __name__ == '__main__': main() ``` 运行结果: ```sh python ./demo_weibo_realtime.py 特朗普反击奥巴马夫人,徐子森先生,今天11:39,10,54,11789 ... 迪丽热巴广州,芒果娱乐,今天21:30,1786,2600,31562 ``` ### parsel`XPath`解析示例 我们以微博实时热门关键词为例: ```Python import requests as req from parsel import Selector import re headers = { 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.116 Safari/537.36', 'authority': 'weibo.com', 'cookie': 'UM_distinctid=171437c9856a47-0cd9abe048ffaf-1528110c-1fa400-171437c9857ac3; CNZZDATA1272960323=1824617560-1569466214-%7C1595951362; SCF=AhjAkJNek3wkLok6WSbiibV1WsGffKPYsDlTZtFqiUH_YWL81nk-0xKkiukxpRoDMoIoV0IClwWecgXLLPiBZrw.; SUHB=0gHTlGutSIGq9P; ALF=1628229198; SUB=_2AkMoasG0f8NxqwJRmfoUxG7ibIl_ww7EieKeNjBvJRMxHRl-yT9jqlYktRB6A-rvW2hROYk5DlHgX7r_dk67bcEdfhBN; SUBP=0033WrSXqPxfM72-Ws9jqgMF55529P9D9WWh..ORuiFeK.mEWDWeecX1; SINAGLOBAL=1133654064055.2583.1597394566906; UOR=,,www.comicyu.com; login_sid_t=f855cdd8714fdb25dee824ce5ff8d792; cross_origin_proto=SSL; Ugrow-G0=6fd5dedc9d0f894fec342d051b79679e; TC-V5-G0=4de7df00d4dc12eb0897c97413797808; wb_view_log=1914*10771.0026346445083618; _s_tentry=weibo.com; Apache=4531467438705.743.1597800659782; ULV=1597800659793:3:2:1:4531467438705.743.1597800659782:1597394566920; TC-Page-G0=d6c372d8b8b800aa7fd9c9d95a471b97|1597800912|1597800912; WBStorage=42212210b087ca50|undefined' } def main(): url='https://weibo.com/a/hot/realtime' resp = req.get(url, headers = headers) # print(resp.text) selector = Selector(resp.text) topic_list = selector.xpath('//div[@class="UG_content_row"]') for topic in topic_list: desc = topic.xpath('.//div[@class="list_des"]') topic_title = desc.xpath('h3[@class="list_title_b"]/a/text()').get().strip() subinfo = desc.xpath('./div').xpath('string(.)').get().strip().replace(' ','') subinfo = re.sub('\s+',',', subinfo) subinfo = re.findall(r'(.*?),(.*?),.*?([0-9]*?),.*?([0-9]*?),.*?([0-9]+)', subinfo)[0] print(topic_title + ',' + ','.join(subinfo)) if __name__ == '__main__': main() ``` 与`lxml`的示例比较可以发现,两者使用方法非常相近,`parsel`的`xpath()`方法每次返回的都是`SelectorList`对象实例,当需要提取节点值时使用`get()`或者`getall()`方法解析,前者返回单个值,而后者返回一个列表,及时只有一个结果也会返回列表。 ## CSSSelector > `CSS`是`HTML`页面的样式描述语言,`CSS选择器`其实就是用样式特征来定位元素。 关于`CSS选择器`详细语法可以阅读 [CSS选择器参考手册](CSS选择器参考手册.md) 这一节。 在你掌握了`CSS选择器`语法后,接下来就来了解如何在`Python`中使用它。 `Python`中支持`CSS选择器`的库包含了`lxml`和`parsel`和`pyquery`,他们内部都是依赖于`cssselect`库实现。`cssselect`库原本是`lxml`的一个模块,后来独立成为一个项目,但我们依然可以在`lxml.cssselect`中使用它。 支持`CSS选择器`的库还有`bs4`, `bs4`依赖 `soupsieve`库实现`CSS选择器功能`。 同样的,我们以示例作为学习参考来了解如何使用`CSS选择器`。 ### lxml中的`CSS选择器`用法 通过调用`cssselect()`方法使用`CSS选择器`表达式,如下面示例用于提取博客文章列表信息: ```Python import requests as req from lxml import etree url='https://www.learnhard.cn' resp = req.get(url) doc = etree.HTML(resp.content) title_list = doc.cssselect('article > header > h2 > a') for item in title_list: title = item.xpath('string(.)').strip() url = item.xpath('./@href')[0] print(f'- [{title}]({url})') ``` ### `bs4`中的`CSS选择器`用法 通过调用`select()`方法使用`CSS选择器`表达式,如下面示例用于提取博客文章列表信息: ```Python import requests as req from bs4 import BeautifulSoup as bs url='https://www.learnhard.cn' resp = req.get(url) soup = bs(resp.content, 'lxml') item_list = soup.select('article > header > h2 > a') for item in item_list: title = item.get_text().strip() # url = item.attrs['href'] url = item['href'] print(f'- [{title}]({url})') ``` `bs4`可以让我们访问一个实例的属性一样来访问标签元素及其属性信息,如本例中我们获取`url`地址时是通过`item.a['href']`获取当前元素下`<a>`标签中`@href`属性值。 ### `pyquery`中的`CSS选择器`用法 ```Python import requests as req from pyquery import PyQuery as pq url='https://www.learnhard.cn' resp = req.get(url) query = pq(resp.content) item_list = query('article > header > h2 > a') for item in item_list: title = item.text url = item.attrib['href'] print(f'- [{title}]({url})') ``` ### `parsel`中的`CSS选择器`用法 ```Python import requests as req from parsel import Selector url='https://www.learnhard.cn' resp = req.get(url) sel = Selector(resp.text) title_list = sel.css('article > header > h2 > a') for item in title_list: title = item.css('::text').get() url = item.css('::attr("href")').get() # url = item.attrib['href'] print(f'- [{title}]({url})') ``` 看到这里,你会发现`parsel`的文本和属性被当做(伪)节点处理了,这与其他的处理方式都不同,但是这样的好处也显而易见,我们处理属性和文本变得更加直观容易了。 关于`CSS选择器`需要说明的是,多数的`伪类`和`伪元素`选择器是不支持的,例如`p:first-child` 和 `p::first-line`。虽然如此,支持的`CSS选择器`已经提供了足够的功能。`cssselect`的文档中有详细说明[Supported selectors](https://cssselect.readthedocs.io/en/latest/#supported-selectors)。 另外`cssselect`支持一些不在`CSS`规范中的选择器: - `:contains(text)`伪类选择器 - `[foo!=bar]`中的`!=`属性操作符,等同于`:not([foo=bar])`。 - `:scope` 允许访问选择器的直接子级, 但是必须放在最开头位置, `:scope > div::text` - 命名空间的用法`ns|div` --- ~END~