lxml text_content()为何无法获取div的完整文本?
结论
<br> 不会导致 text_content() 停止提取。只要后续文字仍在 lxml 解析出的同一个 div 节点中,text_content() 就会返回该节点及其所有后代节点的文本,也会包含 <br>、<img> 后面的尾文本。
如果只能得到开头一段,通常需要排查这些情况:
- 实际响应中没有完整正文;
- 选错了
.posts-cont节点; - 原始 HTML 不规范,lxml 修复结构时提前闭合了
div; - 页面编码声明错误,导致解析结果异常;
- 浏览器显示的是 JavaScript 处理后的 DOM,与
requests获取的响应不同。
先检查响应和解析结果
先确认内容是否进入了解析树。itertext()、XPath text() 和 text_content() 都只能读取解析树中已有的内容。
import requests
from lxml import html
url = 'http://club.kdnet.net/dispbbs.asp?boardid=1&id=9964430'
r = requests.get(url, timeout=20)
r.raise_for_status()
print('status:', r.status_code)
print('url:', r.url)
print('content-type:', r.headers.get('Content-Type'))
print('response bytes:', len(r.content))
doc = html.document_fromstring(r.content)
nodes = doc.cssselect('div.posts-cont')
print('matched nodes:', len(nodes))
for index, node in enumerate(nodes):
text = node.text_content()
print(index, len(text), repr(text[:100]))
如果匹配到多个 .posts-cont,不能直接认定 [0] 就是正文。可以根据内容特征选择节点:
node = next(
node for node in nodes
if '文/新浪财经专栏作家' in node.text_content()
)
还可以把 lxml 实际解析出的节点序列化,直接查看它的结构:
parsed_html = html.tostring(node, encoding='unicode')
print(parsed_html[-1000:])
如果序列化结果在第一段附近就结束了,后续内容并不属于这个节点。改用 itertext() 也无法找回这些内容。
提取全部文本
解析树正确时,下面几种写法都能提取节点内的全部文本。
text = node.text_content()
也可以写成:
text = ''.join(node.itertext())
使用 XPath 也可以:
text = ''.join(node.xpath('.//text()'))
这三种方式在这里的效果基本相同。<img> 后面的文字存放在该元素的 tail 属性中,text_content() 和 itertext() 都会读取它。
<br> 本身没有文本,所以这些方法虽然能提取全部文字,却不会自动在 <br> 所在位置插入换行。
保留 <br> 对应的换行
如果需要保留段落结构,可以遍历节点,把 <br> 转成 \n:
def iter_text_with_breaks(element):
if element.text:
yield element.text
for child in element:
if isinstance(child.tag, str) and child.tag.lower() == 'br':
yield '\n'
else:
yield from iter_text_with_breaks(child)
if child.tail:
yield child.tail
text = ''.join(iter_text_with_breaks(node))
之后可以根据需要处理不换行空格和多余空行:
text = text.replace('\xa0', ' ')
lines = [
' '.join(line.split())
for line in text.splitlines()
]
text = '\n'.join(
line for line in lines
if line
)
print(text)
还有一种更简单的写法:给每个 <br> 添加尾文本。这个操作会修改当前解析树。
for br in node.xpath('.//br'):
br.tail = '\n' + (br.tail or '')
text = ''.join(node.itertext())
如果后续还要继续使用这棵 DOM,采用前面的递归遍历方法更合适。
检查网页编码
示例输出中的 \u6587 和 \xa0 只是 Unicode 字符串的转义形式。\xa0 对应 ,不会造成文本截断。
如果服务器的 HTTP 头或 HTML 声明了错误的字符集,解析树可能出现异常。确认网页的实际编码后,可以显式指定编码。例如,页面确实使用 GB18030 时:
parser = html.HTMLParser(encoding='gb18030', recover=True)
doc = html.document_fromstring(r.content, parser=parser)
也可以先正确解码响应,再交给 lxml:
r.encoding = 'gb18030' # 仅在确认实际编码后设置
doc = html.document_fromstring(r.text)
r.apparent_encoding 只是编码检测结果,可以用来排查问题,但不能直接当作网页的真实编码。
判断内容在哪一步丢失
可以从正文后半部分选一句话作为标记:
marker = '完全沒有必要再建一个1000千伏交流特高压电网'
print('in response:', marker in r.text)
print('in document:', marker in doc.text_content())
print('in selected div:', marker in node.text_content())
根据结果判断问题所在:
r.text中没有:服务器返回的响应不完整,或者返回了其他页面;r.text中有,doc.text_content()中没有:编码或 HTML 解析有问题;- 文档中有,选中的
div中没有:可能选错了节点,也可能是解析器修复畸形 HTML 时改变了节点边界; - 选中节点中存在:文本已经成功提取,问题可能出在后续的打印、保存或展示环节。
浏览器可能执行 JavaScript、携带 Cookie,或使用不同的请求头。因此,浏览器能显示完整内容,不代表 requests.get() 得到的响应中也有相同内容。排查时应以 r.content 和 lxml 序列化后的解析树为准。
备注:内容仅供参考。
