# Text Processing In Python

# 0x00 前言

本文为 Cheatsheet 类型文章,用于记录我在日常编程中的文本处理相关思路。

本文的目录为:

  • 正则处理相关
  • HTML/XML 处理相关
  • Python 中的文本处理

# 0x01 正则处理相关

# 1.1. 正则表达式

正则是个很奇葩的名字,为什么叫做正则表达式呢,首先是个表达式,其次,这是一种叫做正则 (regular expression, rational expression) 的表达式。
名称为什么叫做 regular 呢,因为它基于 regular language. 而 regular language 是一种 formal language. 得,现在又开始是编译原理相关概念了。为了逃避概念,通过用途来简单定义正则表达式。

简而言之,就是一种用于字符串搜索的模式。或者就是一种领域专用编程语言。

https://en.wikipedia.org/wiki/Regular_expression

# 1.2. Python 中正则表达式语法

# 元字符
. ^ $ * + ? { } [ ] \ | ( )

* # 速记,天上一个星星都没有,0 到多个。
+ # 一加手机..... 1 到多个。
? # 有还是没有 即 0 or 1
*? # 没有疑问就是贪婪,有疑问就是非贪婪
+?
??

{m} # m 份
{m,n} # 优先匹配 a{2,}b 优先匹配 aaaab 中 aaaab
{m,n}? # 优先匹配 a{2,}b 优先匹配 aaaab 中 aab

[] # [a\-z] == [az-]
# 1. [\w] [\S]
# 2. [^5]
# 3. [akm$][] 中 $ 并不具备元字符特点

PattenA | PattenB
(...) # 捕获 , 引用可以使用、1 , 但是还有一种扩展语法

(?...) # 扩展
# - (?aiLmsux)
# - (?:...) 不捕获
# - (?P<quote>...) 正则内引用 (?P=quote);python 内获取 m.group('quote') ,m.end('quote');re.sub 内 repl 参数为、g<quote> \g<1> \1

# 1.3. Python 中使用正则的方法

# 1.3.1. re 模块的用法

  • sub 替换
  • match / fullmatch 匹配
  • search 搜索
  • split 分片
re.split('(\W+)', '...words, words...')
# ['', '...', 'words', ', ', 'words', '...', '']

match 为匹配起始字符 / fullmatch 为全部字符 / search 为搜索

# 1.3.2. match object 的用法

m.group(0)
m.group(1, 2)
>>> m = re.match(r"(?P<first_name>\w+) (?P<last_name>\w+)", "Malcolm Reynolds")
>>> m.group('first_name')
'Malcolm'
>>> m.group('last_name')
'Reynolds'
m.start() # 起始
m.end()   # 结尾

# 1.4. 正则表达式性能

# 编译优于不编译
prog = re.compile(pattern)
result = prog.match(string)
re.match(pattern,string)

# 0x02 HTML/XML 处理相关

# 2.1. Beautifulsoup 处理 HTML

解析往往伴随着各种各样奇葩的不奇葩的,诡异的不诡异的网页数据抽取,这个过程中,我们常使用两个库来解决问题,一个库叫做 lxml, 另一个库叫做 BeautifulSoup.

beautifulsoup 可是让我们通过直接手动编写遍历 dom 树的方法来快速遍历 dom 树从而获得数据。相比自己写解析器而言,可以算得上非常的节省时间了。

只要能手动遍历 dom 树,基本上所有的数据都是可以获取的。痛点就是手动编写遍历 Dom 树并且完成测试的时间可能长一些。

但是开发效率就比较低了。

举个例子:

<div id="lal">
  <span class="item" itemprop="street-address" title="浦东南路八佰伴西面">
    地址:浦东南路八佰伴西面
  </span>
  <div class="item" itemprop="street-address" title="浦东南路">名称:xxxx</div>
</div>

我想要地址属性,如果是 beautifulsoup, 则我们需要先定位到 id 为 lal 的 div 元素。然后获取每个元素的 text 部分,然后使用 if 判断地址属性,然后提取 text.

但是如果用 xpath, 则可以把对元素的简单定位简单判断直接写在 xpath 表达式。

sel.xpath('//div[@id="lal"]/*[contains(text(),"地址")]/text()').extract_first()
# 如果还需要添加筛选名称,则可是使用
sel.xpath('//div[@id="lal"]/*[contains(text(),"名称")]/text()').extract_first()

这样可以极大的提升开发效率。

页面的结构越复杂,则 xpath 带来的开发效率越高。

# 2.2. XPath 处理 HTML

# 2.2.1. 概念

XPath 是一种通过路径表达式定位 XML 文档内容的语法。
由于内置了大量的表达式函数,可以通过极少的代码完成定位。
有七种节点类型:

  • element
  • attribute
  • text
  • namespace
  • processing-instruction
  • comment
  • document nodes

有五种节点间关系:

  • 父节点 Parent
  • 子节点 Children
  • 兄弟节点 Siblings
  • 先祖节点,即父与父父节点。Ancestors
  • 后代节点,即子与子子节点。Descendants
语法 描述 例子
nodename 节点名称 a
/ 根节点 /
// 匹配所有 bookstore//book
. 当前节点
父节点 a/…/a/…
@ 属性 a/@href
[] 谓语 book[1] , book[last()-1]
func() 表达式函数 postion()
response.xpath("//*[@id=\"landlb_B04_04\"]/span[2]/a[contains(@href,'market')]")
response.xpath("//*[@id=\"landlb_B04_04\"]/span[2]/a[not(@class)]")
response.xpath("//ul/li/b[contains(text(),'什么玩意')]/following-sibling::span/text()")
response.xpath("//div[@class='address']/text()[preceding::span[@class='item' and contains(text(),'地址:')]]")
response.xpath("//ul/li/b[contains(text(),'什么玩意:')]/following-sibling::a/text()")
//*[contains(text(),'ABC')]
# http://stackoverflow.com/questions/3655549/xpath-containstext-some-string-doesnt-work-when-used-with-node-with-more/3655588#3655588

<div class="atag btag" />
//div[contains(@class, 'atag') and contains(@class ,'btag')]

# 2.2.2. lxml parsel

这两个库是 Python 中常用的解析表达式, parsel 依赖于 lxml , 安装完 lxml 后直接安装即可。

# 2.2.3. lxml 的番外

众所周知,Mac 的 Homebrew 很方便,每一次遇到需要下载编译的组件的时候,只需要执行 brew install xxx, 很快就可以使用了。

但 homebrew 安装的软件都是最新的,这很容易导致部分软件由于版本更新带来的兼容性问题。

这不,最近在 Mac 上进行开发的时候每次调用初始化 lxml 的时候总是无法进行解析,最后经过排查发现问题是 lxml 在编译的时候使用的 libxml 2.9.4 但是 使用的版本为 2.9.2 , 于是每当我使用 lxml 的时候,就会报错。

不得已,找到 lxml 的 F&Q 部分发现提 issue 之前需要先查看依赖版本。

于是进入 IPython 排查。

import sys
from lxml import etree

print("%-20s: %s" % ('Python', sys.version_info))
print("%-20s: %s" % ('lxml.etree', etree.LXML_VERSION))
print("%-20s: %s" % ('libxml used', etree.LIBXML_VERSION))
print("%-20s: %s" % ('libxml compiled', etree.LIBXML_COMPILED_VERSION))
print("%-20s: %s" % ('libxslt used', etree.LIBXSLT_VERSION))
print("%-20s: %s" % ('libxslt compiled', etree.LIBXSLT_COMPILED_VERSION))

# Python : sys.version_info(major=3, minor=5, micro=1, releaselevel='final', serial=0)
# lxml.etree : (3, 6, 2, 0)
# libxml used : (2, 9, 2)
# libxml compiled : (2, 9, 4) # 注意问题出在这里。
# libxslt used : (1, 1, 28)
# libxslt compiled : (1, 1, 28)

于是使用 pip 强制进行安装升级。

STATIC_DEPS=true pip install -i http://pypi.douban.com/simple/ –trusted-host pypi.douban.com lxml –ignore-installed –no-cache-dir –upgrade -vvv

安装完毕即可。

# 2.3. 标准库处理 HTML