PAYATHON 2026

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 对应 &nbsp;,不会造成文本截断。

如果服务器的 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 序列化后的解析树为准。

备注:内容仅供参考。