Python文件处理三板斧:文本CSV与HTML的高效读写技巧

📅 发布时间:2026/10/21 18:31:15
Python文件处理三板斧:文本CSV与HTML的高效读写技巧
写 Python 写了好些年回头看真正每天都在用的反而不是那些花哨的框架和高大上的设计模式而是最不起眼的文件处理。文本、CSV、HTML 这三类文件几乎出现在每一个项目里配置信息要读文本数据交换要做 CSV页面抓取和报告输出又离不开 HTML。把这三板斧练扎实了日常开发里至少一半的“重复劳动”都能又快又稳地解决。这篇文章不是照搬文档的教程而是我在实际开发里踩过不少坑之后沉淀下来的处理套路。文本怎么读最不容易出错、CSV 有哪些隐性问题、HTML 解析用什么姿势既快又省心都会讲到。内容主要是给两类读者准备的一类是刚学完 Python 语法、想系统搞定文件处理的新手另一类是写了一阵子代码、但老在编码、换行、解析器选择上被绊住的朋友。1. 文本、CSV、HTML三类文件为什么值得一起练1.1 从需求场景看三者的分工差异先说清楚这三类文件各自擅长什么很多人的困惑其实来自“用错文件格式处理场景”。文本文件是最通用的信息载体。日志、配置文件、脚本输出、接口返回的原始报文基本都是纯文本。它的特点是结构简单没有强制的字段约束适合存储“给人读”的内容。处理文本的核心诉求是读得进来、写得出去、不漏内容、不卡编码。CSV 文件则是典型的“给机器读”的表格数据。它的本质虽然也是文本但内部有明确的行列结构第一行通常是列名后面每一行是一条记录。来自数据库的导出、报表系统的输入、各种业务系统之间的数据交换CSV 出现频率极高。处理 CSV 的核心诉求是保持字段完整、严格遵守分隔规则、编码不能乱。HTML 文件属于半结构化文本里面有标签、属性、嵌套关系。它的应用场景基本分两类一类是从现成的网页里提取信息比如抓取商品列表、新闻标题、舆情数据另一类是动态生成网页或报告比如把数据分析结果输出成一个可视化页面。处理 HTML 的核心诉求是能从杂乱的标签里精准取数也能按模板批量产出页面。这三个场景凑在一起刚好覆盖了文件处理的三种典型难度无结构、简单结构、复杂结构。一层层递进着练基本功会很扎实。实际项目里也常常需要把它们串起来用比如从 HTML 页面里采集数据清洗后存成 CSV再把统计结果渲染成 HTML 报告典型的完整闭环。1.2 库的选型标准库优先第三方库按需引入我在选型上有一条很朴素的准则能用标准库解决的绝不为了省事去装第三方依赖。文本和 CSVPython 自带的open和csv模块完全够用而且性能稳定、兼容性好。第三方库虽然用起来更“爽”但会引入依赖问题换一台机器跑不起来的情况我遇到过太多次。如果一个脚本只需要读两三个文件没必要拖一个几十 MB 的依赖进来。HTML 处理稍有不同。如果只是做非常简单的正则匹配标准库确实能应付但 HTML 的标签结构经常不按常理出牌手动解析很容易出错。所以我通常直接用第三方库来做最常用的是 BeautifulSoup配合标准库里的html.parser作为解析引擎。这个组合的优势在于容错能力强实际的网页很少有完全合法的 HTMLBeautifulSoup 能容忍标签不闭合、属性缺引号等各类问题。当然如果仅限于把 HTML 作为纯文本提取html.parser和正则也不是不能用但阈值就在“页面结构是否规范”。自己写的爬虫采集外部页面结构的不可控性很高这时候就把 BeautifulSoup 请出来如果是自己程序生成的 HTML 模板结构完全可控那直接用字符串格式化和html.parser就够了。2. 文本文件读写最基础也最容易被忽略的细节2.1 打开模式看似简单用错照样出问题文本操作的第一步是open()这函数写了无数遍但打开模式的细微差别很多人其实没仔细想过。平时最常见的三个模式组合是r、w、a分别对应读、写、追加。# 读取 with open(config.txt, r, encodingutf-8) as f: content f.read() # 写入会覆盖原有内容 with open(output.txt, w, encodingutf-8) as f: f.write(新内容) # 追加在文件末尾继续写 with open(log.txt, a, encodingutf-8) as f: f.write(2024-01-01 执行成功)这里有个很容易被忽略的组合r。它表示可读可写但是写入的位置跟你以为的不一样。默认情况下r打开文件后指针在文件开头直接写入会覆盖开头的内容而不是追加到末尾。很多人想修改文件里的某个字段就顺手用了r结果把前面的内容冲掉了。# 不推荐随手用 r容易破坏原文件内容 with open(data.txt, r, encodingutf-8) as f: content f.read() # 此时文件指针已经在末尾但如果你先 write 就会从开头覆盖如果你真的需要修改文件中的局部内容更稳妥的做法是读出来、做替换、再写回新文件最后用os.replace原子替换原文件。这也是我处理“修改文件”需求的标准姿势。每次open之后都要记得关闭文件。虽然现代 Python 有垃圾回收机制文件对象最终会被回收但关闭的时机不可控。最好的习惯就是使用with语句它会保证代码块执行完毕后自动关闭文件无论中途是否抛异常。这个习惯帮我挡掉了不少“文件被占用”和“内容没写完”的诡异问题。2.2 编码统一把 UTF-8 写进每一个 open文本处理最大的坑不是读写逻辑而是编码。不信你去读一个用 GBK 编码保存的中文文本不加encodingutf-8立刻报UnicodeDecodeError。我在团队里一直强调一条铁律所有代码里打开文件必须显式写encoding参数不要依赖系统默认值。Windows 下默认编码经常是 GBKLinux 和 macOS 下默认是 UTF-8同一段代码在不同系统上跑出不同结果这就是默认编码在作祟。# 显式声明编码跨平台行为一致 with open(test.txt, r, encodingutf-8) as f: content f.read() # 如果文件是 GBK 编码需要指定对应编码 with open(legacy.txt, r, encodinggbk) as f: content f.read()关于编码需要补充一点读写两边编码要对应。读的时候用 UTF-8写的时候也得用 UTF-8。如果要把一个 GBK 文件无损转换成 UTF-8先按 GBK 读出内容再按 UTF-8 写入新文件这是最标准的两步法with open(source_gbk.txt, r, encodinggbk) as f: content f.read() with open(target_utf8.txt, w, encodingutf-8) as f: f.write(content)如果文件编码确实不清楚可以尝试用charset_normalizer这类库做检测不过检测结果只能作为参考最根本的办法还是让数据源头统一成 UTF-8。数据入口不统一后续每次读写都是给自己埋雷。2.3 大文件别用 read()逐行处理才是正解读文件时新手最容易犯的错就是一上来就.read()把整个文件内容塞进内存。文件小当然无所谓但遇上几个 GB 的日志文件内存直接告急。正确的姿势是逐行迭代文件对象本身就是可迭代对象配合for循环可以做到读一行处理一行内存里始终只保留当前行。with open(big_log.txt, r, encodingutf-8) as f: for line in f: # 处理每一行 print(line.strip())如果是按行读取并做条件统计这种写法近乎完美。要注意.strip()的使用它去掉的是行尾的换行符否则你打印或拼接时会出现奇怪的换行。还有一个不算冷门的技巧如果想知道文件有多少行不用先读进内存再数。一行行数就行速度也不慢line_count 0 with open(big_log.txt, r, encodingutf-8) as f: for _ in f: line_count 1对于超大文件还有一种更细的控制方式用readline()手动循环。它和for line in f本质上差不多但for写法内部还做了缓冲优化一般来说优先用for。只有需要在一个循环里交替读取两个文件的场景才需要显式调用readline。文本文件处理有一个原则叫“能用流式绝不用全量”。无论是读是写能一行一行处理的就不要整体加载。当你的脚本从“能跑”升级到“能扛住大数据量”时这个意识会让代码质变。3. CSV 处理别用 split 硬拆标准库 csv 才是正道3.1 为什么手写 split 处理 CSV 会翻车很多初学者碰到 CSV 的第一反应是用类似line.split(,)的方式解析。数据简单时确实能跑但一旦遇到 CSV 的经典问题就立刻翻车字段里包含逗号、字段里包含换行、字段用引号包裹。举个例子一行的数据是张三, 工程师, 28, 备注,含逗号用split(,)拆出来的结果根本对不上列因为逗号既可以当分隔符也可以出现在被引号包裹的字段内部。CSV 协议有一套完整的转义规则字段包含特殊字符时用双引号包裹字段内部的双引号用两个连续双引号转义。自己写正则处理这些边界情况成本极高。标准库的csv模块把这些规则都封装好了。读取解析交给它写入转义也交给它import csv with open(users.csv, r, encodingutf-8, newline) as f: reader csv.reader(f) for row in reader: print(row) # row 是列表这段代码里有个很多人没注意到的参数newline。如果不加这个参数在 Windows 上写 CSV 时会在每行末尾多出一个空行因为csv模块自己会处理换行符再加上系统默认的换行转换就会出现双换行。读了 CSV 还要写回去的朋友尤其容易踩这个坑。3.2 字典式读写DictReader 与 DictWriter 实战csv.reader返回的是列表形式每一行的数据靠下标访问比如row[0]、row[1]。如果 CSV 的列顺序变了代码里的所有下标都要重新对应维护起来很伤。更好的方式是用DictReader它会自动把第一行当作列名每一行返回一个字典直接按列名取值import csv with open(users.csv, r, encodingutf-8, newline) as f: reader csv.DictReader(f) for row in reader: name row[name] age row[age] print(f{name}: {age})这种写法的好处不仅仅是语义清晰更重要的是抗结构变化。只要列名不变列的顺序随便调整代码都不用改。对于经常变动结构的业务数据来说这可以节省大量维护时间。写入端对应的是DictWriter。它需要先声明字段名列表fieldnames写入时的顺序由这个列表决定import csv fieldnames [name, age, city] with open(output.csv, w, encodingutf-8, newline) as f: writer csv.DictWriter(f, fieldnamesfieldnames) writer.writeheader() writer.writerow({ name: A同学, age: 22, city: 某城市 })注意writeheader()单独调用一次它的作用是把字段名本身写进第一行。忘记这行会导致后续读取时没有列名DictReader只能把第一行数据当成表头。3.3 Excel 兼容性utf-8-sig 和换行符的坑CSV 文件经常要交给 Excel 打开。用 UTF-8 编码写出来的 CSVExcel 打开时中文可能会乱码。原因在于 Excel 默认用本地系统编码去猜测 CSV 的编码UTF-8 的字节序列在它眼里不是合法的非编码内容。解决办法是写入时不要用utf-8改用utf-8-sig。这个编码会在文件开头写入一个 BOM字节顺序标记Excel 看到这个标记就能正确识别文件是 UTF-8中文显示就正常了。with open(report.csv, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([姓名, 分数])从兼容性角度看交给外部系统使用的中文 CSV 我基本都用utf-8-sig尽量降低沟通成本。如果文件是自己程序读写不经过 Excel用utf-8就够了。还有一个很隐蔽的坑写 CSV 时如果某个字段内部含有换行符csv模块会正确地用引号把这个字段包裹起来。但当你用纯文本编辑器查看时会看到这个字段被拆成多行这是符合 CSV 协议的。有些下游系统解析时如果不支持多行字段就会报错。这时候需要在上游把字段里的换行符替换成空格再做写入。4. HTML 解析与生成从页面里取数也把结果写回页面4.1 解析器的选择html.parser 与 BeautifulSoup 的取舍处理 HTML入门级方案是正则表达式但这东西在 HTML 面前很脆弱。HTML 的标签嵌套深度、属性顺序、自闭标签、大小写混用都会让正则写出“今天能跑明天崩”的效果。标准库里有一个html.parser模块可以基于事件解析标签。它的优点是零依赖但使用体验比较原始需要自己继承HTMLParser类然后在handle_starttag、handle_data这类回调方法里手工积累数据。拿来做简单提取足够但代码会偏啰嗦。日常实战我更推荐 BeautifulSoup它的选择器语法简单定位元素的效率很高。用法上第一步是解析 HTML 字符串需要指定解析器。from bs4 import BeautifulSoup html_doc html body h1标题/h1 p classcontent正文内容/p a href/page/1链接一/a /body /html soup BeautifulSoup(html_doc, html.parser) print(soup.find(h1).text) print(soup.find(p, class_content).text)html.parser作为解析引擎的特点是纯 Python 实现兼容性好不用额外装底层库。另一个常用的解析引擎是lxml速度更快但对系统编译环境有一定要求。如果你的环境装lxml顺利追求性能时可以用它如果遇到安装问题退回html.parser完全够用。4.2 提取文本和链接的常用套路真实开发里从 HTML 提取信息频率最高的两个操作是取文本和取链接。取文本用.text或.get_text()取链接则要遍历a标签拿到href属性。# 提取页面所有链接 for a in soup.find_all(a): href a.get(href) text a.text.strip() if href: print(text, href)这个场景里比较坑的是相对路径。页面上很多链接是/detail/123这样的形式需要手动拼上域名前缀才能得到完整的 URL。拼接时要注意域名末尾的斜杠和路径开头斜杠的处理用urljoin会比字符串相加更可靠。提取表格数据是另一个高频需求。HTML 里的表格结构是table tr td一层层取table soup.find(table) for row in table.find_all(tr): cells row.find_all([td, th]) row_data [c.text.strip() for c in cells] print(row_data)这个写法在处理多行表格时很稳定。需要留意的是有些单元格里还嵌套着div或span.text会把这些嵌套标签里的文本全部拼接进来如果拼出来的结果里混入了不该出现的内容可以考虑只取直接文本节点。提取信息时还有一个常见问题同一个 class 名字对应多个不同含义的元素。这时可以结合父级节点来定位比如soup.find(div, class_list).find_all(a)先把范围锁定到一个区域内再在局部查找能省掉很多麻烦。4.3 用 HTML 模板生成报告文件HTML 处理不只有解析生成报告也是实用场景。数据分析的结果想让人快速浏览HTML 表格比纯文本醒目得多。最简单的生成方式是准备一个模板字符串用占位符替换内容html_template html headmeta charsetutf-8title统计报告/title/head body h1统计数据/h1 table border1 trth项目/thth数值/th/tr {rows} /table /body /html rows_html .join( ftrtd{item}/tdtd{value}/td/tr for item, value in data ) with open(report.html, w, encodingutf-8) as f: f.write(html_template.format(rowsrows_html))这里有个安全细节值得多说一句如果item或value来自用户输入或外部抓取的数据直接拼进 HTML 可能会产生脚本注入风险。稳妥做法是对内容做 HTML 转义把、、等字符转成实体。标准库的html.escape可以完成这个工作import html safe_item html.escape(item) safe_value html.escape(value)输出 HTML 文件的编码我已经固定在utf-8同时模板里的meta charsetutf-8也要保持一致否则浏览器打开时会乱码。这个细节是“页面里字符集声明”和“文件实际编码”双重要求少一个就会出问题。5. 实战中的高频报错与排查速查5.1 UnicodeDecodeError定位文件真实编码报错示例UnicodeDecodeError: utf-8 codec cant decode byte 0xd5 in position 0: invalid continuation byte这种报错十有八九是文件本身不是 UTF-8却用 UTF-8 去解码。最常见的实际情况是文件从 Windows 老程序里导出的 GBK 编码。排查步骤很直接用不同编码试探读取或者用文件命令工具查看编码信息。我平时最常用的排查方式是先用二进制模式读一下文件头部然后依次尝试gbk、gb18030、utf-8看哪个能正常解码并输出可读文本。gb18030是 GBK 的超集兼容性更好很多情况用它比gbk更稳。for enc in [utf-8, gbk, gb18030]: try: with open(unknown.txt, r, encodingenc) as f: content f.read() print(f成功用 {enc} 解码) break except UnicodeDecodeError: continue如果你要长期处理来源不定的文件最好在入口处做统一编码转换先把所有输入转成 UTF-8 落地后面所有程序都只认这一种编码。5.2 csv 模块写入后出现空行的原因表现是写的 CSV 每一行之间多了一个空行在文本编辑器里看尤其明显。原因在前面提到过写入时open没有加newline。# 错误写法 with open(data.csv, w, encodingutf-8) as f: writer csv.writer(f) writer.writerow([a, b]) # 正确写法 with open(data.csv, w, encodingutf-8, newline) as f: writer csv.writer(f) writer.writerow([a, b])这个问题的原理是 CSV 的换行符由自身控制open默认的 universal newlines 模式会做换行转换双重转换就产生了空行。解决方式就是显式关闭内部换行转换让csv模块全权负责换行行为。5.3 HTML 解析结果为空时的检查顺序用 BeautifulSoup 解析页面却取不到预期内容排查顺序我建议从三个方向来。先看是不是解析器选错了。如果你用了lxml而页面中有一些非标准但又常见的写法lxml的容错行为和html.parser不一样换解析器试一下可能就出来了。再看标签结构是不是和直觉不符。实际页面的 HTML 往往嵌套很深你要找的a标签可能在一个注释里或者在某个 JavaScript 字符串里。这时候先用print(soup.prettify())把解析后的结构打出来人工比对定位。还有一个高频原因目标内容是通过 JavaScript 异步加载渲染的直接用请求拿到的 HTML 里根本没有那些节点。这种情况靠解析静态 HTML 无能为力需要换用模拟浏览器的方式加载整个页面后再去解析渲染生成的 DOM。5.4 文件路径拼接的坑很多人处理文件时直接写相对路径脚本在某个目录下能跑换个目录就报FileNotFoundError。排查这类问题先用os.getcwd()看看当前工作目录在哪里再用绝对路径定位文件位置。可靠的做法是相对于脚本所在目录来定位资源from pathlib import Path BASE_DIR Path(__file__).resolve().parent file_path BASE_DIR / data / config.txt with open(file_path, r, encodingutf-8) as f: content f.read()6. 三板斧之外的几点个人体会文件处理这块很多时候报错信息看起来是代码问题实际上是对文件格式的理解不够深。文本的编码、CSV 的引号转义、HTML 的嵌套结构这些知识不属于 Python 语法本身但 Python 提供了处理它们的工具。基础不牢换任何语言都会遇到同类问题。另外一个强烈建议是在处理任何文件之前先用一小段代码探明文件的基本情况编码是什么、分隔符是什么、整体结构长什么样。不要上来就闷头写完整业务逻辑。先用一个临时脚本打印前五行内容确认预期再动手实现省下的调试时间远超过这几分钟的“浪费”。回到最初的话题文件处理三板斧之所以值得反复打磨是因为它几乎不会过时。数据格式会变框架会换代但这些基础文件操作背后的思维方式始终成立先搞清数据结构再选对工具最后把异常情况处理干净。把这三步养成本能后面写任何文件相关的程序都会顺手很多。