ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

上市公司社会责任报告txt语料整理与文本分析实践

上市公司社会责任报告txt语料整理与文本分析实践 做上市公司社会责任报告文本研究的同学大概率都有过这样的经历从各类数据库和网站上一份份手动下载PDF报告再转成Word、再想方设法转成txt中间各种排版错位、乱码、表格丢失折腾两三天才能凑齐一个像样的样本。最近我为了更新自己的研究语料把2006年到2024年上市公司社会责任报告的txt格式文本数据做了一次比较彻底的整理和复验前前后后花了一周多时间。这篇内容就把完整的处理过程、踩坑记录和应用思路都放出来给正在找数据或已经拿到数据但不知道怎么下手的读者做个参考。先说清楚一个前提这份语料的样本单位是一家上市公司在某一年发布的“社会责任报告”全文格式是纯txt。它不是一个带字段的数据库也不是已经清洗好的结构化表格而是一堆能直接打开读的文本文件。这反而是我觉得最舒服的地方——文本分析的工作本来就不该从“别人整理好的Excel”开始而应该从最原始的文本出发自己掌控每一步清洗和特征提取的逻辑。1. 这批数据到底覆盖了什么以及它能支撑哪些研究1.1 时间跨度从2006年开始意味着什么严格来说A股上市公司社会责任报告的公开披露不是从2006年才出现的但2006年是一个比较关键的分水岭。在那之前主动发布社会责任报告的上市公司数量很少样本零散内容也多偏向企业宣传册式的专刊或者年报里顺带的一个小节很难形成连续可比的文本语料。2006年以后披露数量稳步上升到2010年以后很多行业的头部公司开始把社会责任报告作为一份独立文件来编制发布篇幅和结构也随之规范化。这个时间起点对研究设计非常关键。如果你的研究需要观察“披露行为变化”的时间序列2006-2024是一段相当完整的周期能覆盖企业从被动披露转向主动披露、从单纯展示公益到体系化披露ESG信息的变化过程。我统计过自己这份语料里的年份分布2012年以前的报告数量少单篇文本的重心集中在公司治理和公益捐赠2015年之后环境类词汇和具体量化指标明显增多。研究结论不一定放之四海而皆准但至少能真实反映这段时间里的文本趋势。如果只需要“最近五年”的截面数据做横截面分析2006年的起点意义不大但如果你要做面板数据需要对公司逐年评分那就必须先把每一年的公司名单和报告文件对应起来不能做一年拉一年。因为很多公司并不是连续披露的缺失年份不是数据质量问题而是披露行为本身的问题。这个区别在做计量的朋友那里应该很熟悉。1.2 能用它回答哪些研究问题纯文本形式的CSR报告语料最擅长处理的是四类问题。第一披露内容的议题迁移。环境、员工、供应链、公益、公司治理等不同议题在报告里的占比变化可以通过章节数量、关键词频次、句子长度这些指标来观察。比如“员工培训”这个词在2008年的报告里往往出现在“人力资源”小节到2020年以后更多出现在“人才发展”或“员工成长”章节这种位置迁移本身就是一种信号。第二披露质量与语调。通过情感词典、不确定词占比、积极措辞比例衡量管理层在报告中的语气管理行为。CSR报告有一个特点它不像财报那样有强制的格式要求文字表达的自由度很高这给语气分析提供了充足空间。一家企业在报告里频繁用“坚决”“全力”“持续提升”这类词和另一家用“努力”“尽量”“将视情况”这类词传达的信息密度完全不同。第三信息可比性。不同公司、不同行业之间的报告长度、章节结构、量化指标个数是否存在系统性差异。比如金融业报告的“绿色金融”章节普遍比制造业厚制造业报告的“安全生产”章节又比金融业详细这些都是可以做行业间对比的好素材。第四和企业行为的关联研究。把文本指标披露倾向、环保词频、语调积极度和财务指标、处罚记录、ESG评分做关联分析。我曾做过一个小测试统计每家公司在报告正文里“生态”“绿色”“双碳”三个词每千字出现的次数再和企业当年的环保处罚数据做相关性分析能看到一些有意思的分化——部分公司文本上很积极但实际处罚数量也在增加这就是典型的“漂绿”风险信号。这类分析如果只有财务数据没有文本数据根本做不出来。1.3 谁最适合用这份数据以我的实际接触来看大概分三类人。学术研究者是主力。研究ESG披露、公司治理、企业社会责任的硕博生和教师需要一份带年份、带公司标识的标准化中文语料来验证假设。txt格式足够配上自己写的清洗脚本任何研究设计都跑得通。量化分析师和策略研究员是第二类。他们希望把CSR文本转成可用因子比如“报告语调变化”“环境词频增速”“社会责任相关句子占比”然后塞进多因子模型或事件研究框架。这类用户对预处理的一致性要求很高后面我会专门讲怎么构建面板数据。第三类是NLP算法工程师和数据产品经理。他们需要一份干净、规模适中的中文长文本语料来训练或微调模型比如摘要模型、信息抽取模型、文本主题分类模型。CSR报告的领域特征很强连“质量”这个词在不同段落里的含义都不同适合做有挑战性的微调训练集。2. 为什么在这个场景里txt格式反而是最“皮实”的选择2.1 PDF、Word、扫描件和txt的实际体验差异很多人拿到数据的第一反应是为什么不用PDF或Word答案很简单做文本分析时PDF和Word都会给你增加好几层不必要的负担。PDF的问题在于解析。一份排版复杂的PDF用pdfplumber或PyMuPDF抽文字的时候表格区域会乱跨栏内容会被切断双栏排版的报告抽出来以后阅读顺序完全是乱的。更麻烦的是很多早期报告是扫描件根本没有做OCR抽出来全是空白少量有图层的PDF里文字也是嵌在注释块里的提取时需要额外参数。Word格式的问题在于兼容性。docx本质上是一个zip压缩包需要解析document.xml文件虽然python-docx能处理常规文档但遇到带目录、文本框、域代码的复杂报告很容易丢内容。doc老格式就更麻烦了需要额外转换工具批量处理时经常出现“某个文件打不开”的情况。而txt格式最大的优点是简单。它没有复杂的结构没有格式标记就是纯粹的字符流。对文本挖掘来说结构越简单处理越稳定。你在txt里拿到的内容和原始排版相比确实少了很多视觉信息但做词频、情感、主题模型时这些视觉信息本来就会被扔掉。打个比方你研究一个人的观点不需要纠结他今天穿什么颜色的衣服txt直接帮你把“衣服”全部脱掉只留下“观点”。当然txt不是没有缺点。表格里的量化指标、图表里的数值、图片中的文字信息在txt里几乎全部丢失。而CSR报告里很多高价值内容恰恰是表格式数据比如污染物排放数据、员工培训人次、慈善捐赠金额。所以txt适合做整体文本层分析不适合做精确到指标值的抽取任务。这一点一定要在选题阶段就想清楚。2.2 编码问题txt唯一真正需要小心的地方txt的“简单”是有代价的代价就藏在编码里。我打开同一批数据时遇到过三种编码的txt文件UTF-8、UTF-8 with BOM、GBK。如果用Python带着encodingutf-8的固定参数去读GBK文件会直接抛UnicodeDecodeError如果文件带BOM读取后第一个字符会多出一个不可见的\ufeff这个字符不报错但会让你后面所有字符串统计和词典匹配都出错。处理办法不复杂。最稳妥的是写一个自动识别编码的函数先用utf-8-sig尝试失败后再用gb18030gb18030是GBK的超集能兼容绝大多数中文编码场景def smart_read_text(path): for enc in [utf-8-sig, gb18030, utf-16]: try: with open(path, r, encodingenc) as f: return f.read(), enc except UnicodeDecodeError: continue raise ValueError(f无法识别编码: {path})这个函数我几乎所有项目都会先用一遍。重点不是直接用它读完所有文件而是一次性把所有文件的编码识别结果记录下来看看数据源是否用了统一的编码。如果发现两三种编码混用说明这批数据很可能是不同时间和来源拼接出来的后续清洗时要格外小心。还有一个容易被忽略的点txt文件的行尾符不统一。老文件是\r\n新文件是\n个别文件是\r。在Windows上统计行数没问题但到Linux、macOS上用splitlines()会比split(\n)更安全因为splitlines()能自动处理所有换行符类型。2.3 从txt到结构化数据的最短路径如果只是快速验证不需要上数据库直接用Python几行代码就可以把问题跑起来import pandas as pd def load_reports(file_list): rows [] for file in file_list: text, enc smart_read_text(file) rows.append({file: file, text: text, len: len(text), enc: enc}) return pd.DataFrame(rows)然后再把file列拆成证券代码、年份、公司名df[code] df[file].str.extract(r(\d{6})) df[year] df[file].str.extract(r(20[0-2]\d))这就是最原始但足够支撑分析的“语料面板”。后面所有更复杂的操作都是从这个DataFrame出发的。下一节我会重点讲拿到这批txt之后第一件该做的事其实不是建模而是对数据本身做一次彻底的“验货”。3. 拿到txt数据以后建议先做完这三件事再做分析3.1 检查编码BOM顺便看文件是否完整拿到一份更新至2024年的txt数据集第一件事不是跑词频而是写一个脚本把所有文件都过一遍smart_read_text记录四个信息文件大小、字符数、识别编码、是否正常读完。这一步相当于给数据建立基础台账后面任何清洗操作都能追溯到源头。我当时跑完就发现有少数文件的字符数明显少于同年份其他文件。比如某家制造业公司2022年的报告只有三千字而同行业同期报告平均有两万字以上。这种文件有两种情况一是报告原文本身很短早期报告确实存在这种情况二是文件在传输或转码过程中被截断了。怎么区分打开文件看结尾是否完整。正常的报告最后一个词基本是“特此报告”“特此发布”或者“工作持续改进”之类的收尾语被截断的文件往往停在某个句子中间甚至停在半个词上。为了避免误判建议输出一份“文件大小-字符数”分布表按年份分组看中位数和异常值。凡是低于同年度中位数三分之一以下、字数又小于5000的txt都要单独人工看一眼。这一步能省掉后面很多因为数据不完整导致的假结论。3.2 文件名映射证券代码、年份、公司名必须三位一体文件名是这批txt数据能否被有效使用的关键。最理想的情况是类似“000001_平安银行_2023_社会责任报告.txt”的三段式命名解析起来毫无压力。但实际数据集里会出现各种变体有的文件名只有证券代码有的公司名中间有空格或括号有的年份字段写的是“2023年度”而不是“2023”还有的干脆叫“未命名文档.txt”这类文件如果不处理后面所有按文件名做的分组都会出错。我处理这类问题的经验是不要直接改原始文件而是先建一个元数据表。用正则从文件名提取code、year、name三个字段提取失败的就放进“待人工处理”名单。然后拿这个元数据表和证券代码库做一次join能对应上的文件保留对不上的一对一核对。要避免的情况是“某个公司名字写错了但没被发现”这种错误在后续分析里几乎不可能看出来因为单个文件不报错只有做汇总对比时才可能出现轻微异常。3.3 人工抽样校验认真读五份报告比写十次脚本管用在开始写清理函数之前我会先认真读五份不同年份的报告。这不是浪费时间而是建立对数据的真实感受。读完你就会发现很多报告的开头几页是封面、目录、董事长致辞中间是各种“亮点汇总”最后才是正式章节。如果对整个格式没概念后面的章节切分和关键词统计很容易把目录和正文混在一起。另外不同年份的报告标题措辞差异很大有的叫“企业社会责任报告”有的叫“可持续发展报告”还有的叫“社会责任暨ESG报告”。文件名里可能都写着“社会责任”但正文标题根本不含这几个字。抽样校验时顺手把实际标题词记下来后面做关键词词典时才能覆盖全面。4. 清洗、去噪和切片把原始txt变成可以计算的语料4.1 基础清洗不可见字符与全角半角统一这一步像做饭前的洗菜。原始txt里会有大量无用的隐形内容BOM头、全角空格、不间断空格、零宽字符、Windows行尾符偶尔还有乱码替换符。我一般是先把这些全部清理掉再统一全角半角数字和标点。核心清洗函数大概长这样import re import unicodedata def clean_text(text): # 去掉不可见字符和零宽字符 text re.sub(r[\x00-\x08\x0b\x0c\x0e-\x1f\x7f], , text) text text.replace(\ufeff, ).replace(\u200b, ) # 全角空格转半角 text text.replace(\u3000, ) # 统一换行 text text.replace(\r\n, \n).replace(\r, \n) # NFKC统一全角字符 text unicodedata.normalize(NFKC, text) # 压缩连续空行 text re.sub(r\n\s*\n, \n\n, text) return text.strip()这里有个细节要特别提醒unicodedata.normalize(NFKC, text)会把全角字母数字转成半角但也会把一些特殊符号转成ASCII符号对中文文本一般无害可如果你的分析对象涉及“Ⅰ”“Ⅱ”“Ⅲ”这样的罗马数字它会把“Ⅰ”转成“I”导致后续把“第三章”识别成“第三I章”正则匹配就容易出问题。所以建议在清洗前先判断有没有需要保留的特殊符号或者清洗后做一次抽查专门看罗马数字、破折号、顿号这些容易出问题的字符。4.2 切分策略按章节切还是按自然段切文本分析里的“切分粒度”决定了后续方法的可用性。我常用的策略有三种没有绝对的好坏只看研究问题需要什么。整篇作为一个文档适合做报告级别的词频、情感、文本分类比如计算整篇报告的情感得分、环境词密度或者拿全文做LDA主题模型。这种粒度最省事清洗完后直接统计。按章节切分适合研究“环境信息披露”“员工权益披露”等具体议题。做法是先准备目标章节标题清单比如“环境绩效”“环境责任”“绿色运营”“生态环境保护”然后用正则去匹配从命中位置截取到下一个章节标题出现为止。问题在于中文报告的小标题格式极不统一有“一、”“1.”“一”“第一章”等不同年份同一公司的格式都可能不一样。我的做法是先用粗粒度规则预切分把“一、”这种最通用的标题符号切成一级块再在块内用二级关键词定位。按自然段切分适合做句子级或段落级的向量化。比如想知道“双碳”这个主题出现在哪些段落上下文里或者统计积极词所在段落的其他词共现关系这种粒度最有价值但处理量也最大。我一般是先把报告按空行拆成段落去掉少于五个字的碎片段再保留含中文的段落。4.3 报告特有噪声的去除报告文本最大的噪声来源是封面、目录、页眉页脚和免责声明。这些内容混在主体文本里做词频统计时会产生系统性的干扰。封面和目录的处理用正则把“目录”到“正文”之间的大段文本删除但这只对格式规范的新报告有效。老报告的目录未必叫“目录”有的叫“CONTENTS”有的叫“本报告说明”需要灵活处理。我的经验是先抽取二十份报告看看目录标题的实际用词建一个同义词集合再做删除。页眉页脚的处理很多txt转换工具会把页眉页脚重复插入每一行导致公司名在一份文件里出现几十次。一个简单的判定方法是如果“某某股份有限公司”这个完整名称在文件中出现超过20次它大概率是页眉或落款可以考虑替换成空字符串。但要注意如果报告中确实反复提到公司全名这很常见也不要全部替换最好只在句首位置做替换或者统计时用正则排除前后没有中文标点的孤立公司名。免责声明的处理报告末尾“本报告所涉及的计划、目标等前瞻性陈述”这类套话如果做情感分析它们会带来稳定的乐观噪声。可以单独统计这类句子的数量作为“合规声明密度”指标也可以在情感分析时直接把整段移除。到底怎么选取决于你的研究问题没有标准答案。4.4 构建公司-年份-文本面板清洗完之后最终要产出一张数据表每一行是一家公司在某一年的一份报告文本及若干衍生指标panel pd.DataFrame({ code: codes, name: names, year: years, text: cleaned_texts, char_cnt: char_cnts, para_cnt: para_cnts, env_word_cnt: env_word_cnts, })我一般会额外计算几个派生字段报告总字数、段落数、环境词频数、员工词频数、积极词比例、消极词比例、标题中的董事会辞字数。这样后续分析可以直接用这些字段不用重新跑全文。注意一点char_cnt最好用“去除空白后的字符数”而不是len(原始文本)二者差距很大尤其txt文件里可能包含大量换行和空格如果不统一跨公司跨年份的统计就不可比。5. 从txt到结论一条可以落地的完整分析链路5.1 关键词词典的构建不能从现成词表直接抄用CSR文本做词频分析词典是最关键的部分。词典不是随便找几个高频词而是必须和研究假设强绑定。比如我想研究“双碳”目标提出后企业披露内容的变化就准备三组词环境类环境、生态、低碳、绿色、节能、排放、污染、双碳类双碳、碳达峰、碳中和、碳足迹、漂绿类减排、环保投入、绿色转型。然后在清洗后的文本上统计每家公司每年每千字中各组词的相对频次。用相对频次而不是绝对频次是必须的。报告字数在2006-2024年间增长太快绝对次数无法跨年份比较。举一个直观的例子一份2010年的报告只有8000字出现“环境”5次一份2023年报告有30000字出现“环境”12次。绝对次数已经翻了一倍多但相对频次反而下降了。如果不归一化任何时间趋势都会被报告篇幅增长这件事污染掉。5.2 一个可以复现的时间序列过程我利用这份txt数据做过一个简单验证按年度统计环境相关词的平均相对频次2006年时每千字大约只有1.8次2015年是4.5次左右到2023年接近9次。员工相关词的上升速度没有这么陡说明环境议题在报告文字议程里的权重确实在逐步上升。这里要说明这些数字只对我的抽样样本有效不代表市场整体但它演示了一个完整流程原始txt → 清洗 → 词典匹配 → 统计 → 画趋势图。整个过程所有代码加起来不到100行但每一步都依赖前面数据整理的质量。如果要做更细的报告长度控制还可以在相对频次的基础上再做一次回归调整把char_cnt作为控制变量放进去提取残差作为“额外环境关注度”。这比单纯看相对频次更接近“剔除篇幅影响后的披露强度”这一概念研究里用起来也更有说服力。5.3 情感分析先跑词典法再用模型法做稳健性检验如果要分析报告语调我建议先使用词典法因为词典法可解释性强、稳定且容易在论文里复现。通用中文金融情感词典未必完全适合CSR文本需要手动补充CSR语境下的词积极词如“高质量”“持续”“提升”“改善”“履行”“共创”消极词如“缺陷”“事故”“处罚”“污染”“超标”。一个容易出的问题是CSR报告整体措辞偏正面消极词本身出现频率就很低直接计算“消极比例”会非常稀疏。这时候要换一个思路统计“积极词比例减去消极词比例”得到净语调指标或者统计“确定性修饰词”的密度来判断语气强度。另外词典匹配对分词敏感同一个词在不同报告里可能被分成“环境/保护”和“环境保护”建议先把清洗后的文本做统一分词再建词典。模型法用来做稳健性检验是很好的选择。比如用中文金融预训练模型对所有段落打分然后和词典法得分做相关分析相关系数在0.6以上就能相互印证。模型法的优势是能捕捉上下文缺点是成本高、对超长文本不友好而且结果的分词解释性差。前期用词典法跑通链路后期用模型法做验证这是最稳妥的组合。6. 数据处理过程中踩过的五个典型的坑6.1 乱码不止一种形态我最早遇到乱码时第一反应是编码识别错了换编码重读后还是乱码。后来才发现有一部分文件在生成txt时已经“二次转码”损坏了源头文本是GBK中间被当成UTF-8读取过一次再用GBK写回导致损坏无法恢复。这种文件没法用一个标准函数逆转只能重新从原始来源下载。怎么判断文件是否可修复尝试把文件内容重新编码回原始编码比如把读取出来的字符串用gbk再编码一次如果抛UnicodeEncodeError说明内容里已经出现了无法映射的字符大概率是已损坏。6.2 更名公司会让时间序列断成两截A股市场里公司更名非常常见比如“XX科技”改名为“XX数智”或者证券简称本身就带“ST”“*ST”前缀。如果只用公司名去匹配一条时间序列会被断成两截。保险做法是优先用证券代码做唯一标识同时保留一个“曾用名”字段不要用名称直接做join。另外同一家公司在更名前后报告标题措辞也会变化文件名解析时要把公司名字段存成“原始名称”和“标准名称”两个字段避免后面统计口径混乱。6.3 面板不平衡未必是数据缺失可能是披露规则早期做论文时我发现2023年的公司数量比2021年少了一块第一反应是数据不全后来去核对披露名单才发现有些公司确实没有发布当年报告或者发布了但报告名称里不含“社会责任”四个字而叫“可持续发展报告”“ESG报告”所以没被过滤规则捞进来。遇到面板不平衡先判断是真实未披露还是命名差异导致的漏抓再决定要不要补齐。这个判断直接影响后续分析方法的选型如果是真实未披露就需要用处理非平衡面板的计量模型如果是漏抓补数据就行。6.4 文件名跟内容对不上比没有数据更麻烦有一次我按文件名里的年份分组统计结果发现某一年环境词频整体异常偏高查了很久才发现那份文件名里写的是2022但文本内容实际是2023年的报告因为下载时被自动添加了“(1)”我的正则提取年份时把后面的“2023”抓了出来两个年份就这样混在同一批样本里了。从那次以后我加了人工抽检机制随机抽取3%的文件读取前200个字符和尾部50个字符确认标题在报告内页显示的年份和文件名年份一致。这个抽检不费多少时间但能避免一次非常严重的计量错误。6.5 报告长度方差极大所有跨样本比较都要谨慎同一批数据里有的报告只有6000字有的是接近50000字的“精装版”。如果直接对比关键词绝对次数头部公司天然占优势很多结论都是假的。解决办法是全文都用相对频次、比例、百分比来比较或者在模型里控制总字数变量。还有一个更细的点不同行业的报告结构差别也很明显银行业报告偏重责任金融制造业偏重安全环保跨行业做词频比较时最好做行业层面的分层检验至少也要在回归里加行业固定效应。说到底整理这2006-2024年txt语料真正的难点不在读文件和写代码而在“把文本变成可对比的指标”这一层。如果你也是刚开始接触这批数据建议从最简单的词频分析跑起先把清洗和校验流程理顺再逐步加入情感、主题模型这类进阶方法。流程跑通以后你会发现这套语料能提供的价值远远超过最初的预期。
返回列表