1. 从“奇葩”需求到自动化方案:为什么需要CSV转WOS?

做文献计量分析的朋友,尤其是用CiteSpace的,估计都从Web of Science(WOS)上下载过数据。标准的流程是:在WOS上检索,导出为“纯文本”或“全记录与引用的参考文献”格式,得到一堆.txt文件,然后直接扔进CiteSpace里分析。但有时候,现实总爱开点玩笑。

我就遇到过这么个情况。当时一门研究方法课的老师,不知道是出于什么考虑,从WOS上检索了2000多篇文献,自己捣鼓了一番,把数据整理成了一个CSV表格发给我们,让我们用CiteSpace做聚类分析。拿到文件我一看就懵了:CiteSpace只认WOS、Scopus、CNKI等特定格式的文本数据,它可不认识你这规规矩矩的CSV表格啊。每一行是一条文献记录,每一列是字段,像标题、作者、摘要、关键词、发表年份等等,整整齐齐。这格式给人看是清晰了,但对CiteSpace来说,就是一堆“乱码”。

手动把两千多条记录,按照WOS那种一行字段名、一行字段值,中间用空格隔开,每条记录以“PT J”开头、“ER”结尾的格式去改?想想都觉得是噩梦。这种重复、机械、易出错的工作,正是Python脚本大显身手的地方。所以,我一气之下(或者说,灵机一动)决定写个Python脚本,把这个“奇葩”的CSV文件,重新转换回CiteSpace能吃的“正经”WOS格式。

这个过程,本质上是一个数据格式的逆向工程与重构。我们有一个结构化的、表格化的数据源(CSV),需要将它转换成一种特定的、带标记的文本格式(WOS)。这不仅解决了眼前的课程作业问题,更是一个通用的数据管道技巧。以后只要你拿到结构化的文献数据(比如从其他数据库导出的CSV,或者自己用爬虫整理的表格),都可以用类似的思路,把它“喂”给CiteSpace、VOSviewer等只认特定格式的软件。接下来,我就把自己踩坑和填坑的全过程,掰开揉碎了分享给你,哪怕你代码基础一般,跟着做也能搞定。

2. 解密WOS数据格式:知其然,更要知其所以然

在动手写代码之前,我们得先搞清楚目标长什么样。你得先弄明白WOS导出的数据文件是什么结构,才能知道我们的脚本要输出什么。

你从WOS官网,选择“导出”->“纯文本文件”或者“全记录与引用的参考文献”,下载下来的通常是一个名为savedrecs.txt之类的文本文件。用记事本打开它,你会发现它不是简单的表格。它的结构是这样的:

FN Thomson Reuters Web of Science
VR 1.0

PT J
TI The title of paper one
AU Author A
   Author B
AB This is the abstract of paper one.
C1 [Author A] University; [Author B] Company
PY 2023
ER

PT J
TI The title of paper two
AU Author C
AB This is the abstract of paper two.
PY 2022
ER

EF

我来解释一下这些“暗号”:

  • FNVR:这是文件的“抬头”,标明文件来源和版本,通常出现在文件最开头。
  • PT J:表示记录类型是“Journal”(期刊文章),这是每条文献记录的起始标志。CiteSpace就靠这个识别一条新记录开始了。
  • TI, AU, AB, PY:这些是字段标签TI代表标题,AU代表作者,AB代表摘要,PY代表发表年份。WOS有几十个这样的固定标签。
  • 字段值:紧跟在字段标签后面(通常是一个空格隔开),就是该字段的具体内容。比如 TI The title of paper one
  • ER:这是每条文献记录的结束标志PT JER 成对出现,像括号一样包住一条完整记录。
  • 记录分隔:每条ER后面,通常会跟一个空行,再开始下一条记录,这样视觉上更清晰。
  • EF:这是整个文件的结束标志,放在所有文献记录之后。

关键点在于:WOS格式是“纵向”的,它是一条记录接一条记录地排列,每条记录内部是“字段标签 + 值”的线性列表。而我们的CSV是“横向”的,它是一条记录占一行,各个字段作为列横向展开。我们的转换,核心就是一次“行列转置”,把CSV的“行”变成WOS里一条记录的“多行”,同时把CSV的表头(列名)替换成WOS认识的字段标签。

所以,转换任务可以拆解成几个明确的步骤:

  1. 字段映射:把我们CSV里的列名(如“文章标题”、“作者”),翻译成WOS的官方标签(如TI, AU)。
  2. 数据转置:把CSV的矩阵数据“旋转”90度,让每一列(代表一个字段的所有值)变成WOS格式里需要逐条记录拼接的部分。
  3. 格式组装:在每条记录前后加上PT JER,在文件头尾加上FNVREF,并确保字段标签和值之间用空格(而不是逗号)分隔。
  4. 输出为文本:最终生成一个.txt文件。

理清了思路,我们就可以打开代码编辑器,开始动手了。

3. 手把手实战:Python脚本编写全流程

我建议你在电脑上新建一个专属文件夹,比如叫 csv_to_wos,把所有的脚本和输入输出文件都放在这里,管理起来方便。我们的操作会分成几个小步骤,每个步骤一个脚本,这样逻辑清晰,也便于调试。

3.1 第一步:准备原料——清洗与映射你的CSV数据

首先,你得有原料。把老师给的、或者你自己整理的CSV文件,放到项目文件夹里。为了保险起见,我强烈建议你先用Excel或WPS打开这个CSV文件,做一次简单的“体检”:

  • 检查编码:确保文件是UTF-8编码,否则中文可能会乱码。用记事本“另存为”时可以查看和选择编码。
  • 处理特殊字符:看看摘要、标题里有没有多余的换行符(\n)、引号,这些可能会干扰CSV解析。在Excel里可以查找替换。
  • 统一列名:确保你的CSV第一行是表头,并且列名明确。比如“Title”, “Authors”, “Abstract”, “Year”等。如果列名是中文,比如“文章标题”,也没关系,我们在脚本里做映射。

接下来,就是最关键的字段映射。你需要一张“翻译表”。WOS的官方字段标签很多,我们常用的是下面这些,你可以根据自己CSV里的列名进行对应:

你的CSV列名 (示例) WOS 字段标签 含义
Title, 文章标题 TI 标题
Authors, 作者 AU 作者
Abstract, 摘要 AB 摘要
Keywords, 关键词 DE 作者关键词
Keywords Plus, 扩展关键词 ID WOS扩展关键词
Year, 出版年 PY 出版年
Source, 来源期刊 SO 期刊名称
Volume, 卷 VL
Issue, 期 IS
Pages, 页码 BP 起始页码
DOI DI 数字对象标识符
Addresses, 机构 C1 作者地址
... ... ...

小技巧:你可以在WOS上随便导出一两条记录,打开看看它用了哪些标签,对照着建立你自己的映射字典。这个字典是我们后续脚本的“核心配置”。

假设我的CSV文件列名是中文,我准备了一个映射字典。现在,我们先不急着在代码里映射,而是用Excel手动操作,更直观。把CSV文件用Excel打开,直接修改第一行的表头单元格,把“文章标题”改成“TI”,“作者”改成“AU”,以此类推。改完后另存为一个新的CSV文件,比如叫 input_mapped.csv。这一步是让数据“准备就绪”。

3.2 第二步:核心转换——用Python实现行列转置与记录组装

原料准备好了,开始烹饪。在项目文件夹里新建一个Python脚本,命名为 step1_transpose.py。这个脚本要完成的任务是:读取映射好表头的CSV,将其转换成每条记录以PT J开头、ER结尾的中间格式。

# step1_transpose.py
import csv

def csv_to_wos_intermediate(input_csv, output_txt):
    """
    将表头已映射为WOS标签的CSV文件,转换为中间WOS格式文本。
    参数:
        input_csv: 输入的CSV文件路径(表头应为WOS标签,如TI, AU)
        output_txt: 输出的文本文件路径
    """
    with open(input_csv, 'r', encoding='utf-8-sig') as csvfile: # 注意编码,utf-8-sig处理BOM
        reader = csv.DictReader(csvfile) # 使用DictReader,方便按列名读取
        fieldnames = reader.fieldnames # 获取所有字段标签(WOS标签)

        all_records_lines = [] # 用来存储最终所有行的列表

        # 遍历CSV的每一行(即每一条文献记录)
        for row in reader:
            record_lines = [] # 存储当前这条记录的所有行
            record_lines.append('PT J') # 每条记录以PT J开始

            # 遍历每个字段标签
            for field in fieldnames:
                value = row[field]
                if value and str(value).strip(): # 如果字段值非空
                    # 将“字段标签”和“字段值”组合成一行
                    line = f'{field} {value}'
                    record_lines.append(line)

            record_lines.append('ER') # 每条记录以ER结束
            record_lines.append('')   # 记录后加一个空行,更美观
            # 将当前记录的所有行,添加到总列表中
            all_records_lines.extend(record_lines)

    # 写入输出文件
    with open(output_txt, 'w', encoding='utf-8') as txtfile:
        # 先写入文件头
        txtfile.write('FN Thomson Reuters Web of Science\n')
        txtfile.write('VR 1.0\n\n')
        # 再写入所有记录行
        txtfile.write('\n'.join(all_records_lines))
        # 最后写入文件尾
        txtfile.write('\nEF')

    print(f"转换完成!中间文件已保存至: {output_txt}")
    print(f"共处理了 {sum(1 for _ in open(input_csv, 'r', encoding='utf-8-sig')) - 1} 条记录。") # 粗略计算记录数

# 使用示例
if __name__ == '__main__':
    input_file = 'input_mapped.csv' # 你修改好表头的CSV文件
    output_file = 'intermediate_output.txt'
    csv_to_wos_intermediate(input_file, output_file)

运行这个脚本 (python step1_transpose.py),你会得到 intermediate_output.txt。用记事本打开看看,应该已经很像WOS格式了:有FN/VR头,每条记录有PT J和ER包裹,字段标签和值在同一行。但是,这里有一个关键的细节问题:我们CSV里的值,如果本身包含逗号,在写入时会被加上引号,或者导致格式错乱。而且,WOS格式中,一个字段值如果很长(比如摘要),它是可以自然换行的,但字段标签只出现一次。我们上面的脚本简单地将一行CSV记录的所有字段堆叠起来,对于多作者等情况,可能需要更精细的处理(比如WOS中每个作者单独一行,但都以AU开头)。不过别担心,对于CiteSpace的基本分析(如关键词、机构、作者共现),这个格式通常已经足够被识别。

3.3 第三步:格式精修——处理多值字段与最终清理

上一步的输出可能已经能用,但为了更规范,我们处理两个常见问题:

  1. 作者字段:在CSV里,所有作者可能都在一个单元格里,用分号隔开,如“Author A; Author B; Author C”。在WOS格式中,更标准的做法是每个作者单独成行,像这样:
    AU Author A
    AU Author B
    AU Author C
    
  2. 地址字段:同样,多个机构地址也可能挤在一个单元格里。

我们来写一个精修脚本 step2_refine.py,专门处理像AU(作者)、C1(地址)这类可能需要拆分的字段。

# step2_refine.py

def refine_wos_file(input_txt, output_txt, fields_to_split=None, separator=';'):
    """
    精修WOS格式文件,将指定字段的多值按分隔符拆分。
    参数:
        input_txt: 输入的中间WOS文本文件路径
        output_txt: 输出的精修后文本文件路径
        fields_to_split: 需要拆分的字段标签列表,如 ['AU', 'C1']
        separator: 字段值内的分隔符,默认是分号
    """
    if fields_to_split is None:
        fields_to_split = ['AU', 'C1'] # 默认处理作者和地址字段

    with open(input_txt, 'r', encoding='utf-8') as infile:
        lines = infile.readlines()

    new_lines = []
    i = 0
    while i < len(lines):
        line = lines[i].rstrip('\n') # 去掉行尾换行符
        if line.startswith('ER') or line.startswith('PT') or line == '' or line.startswith('FN') or line.startswith('VR') or line.startswith('EF'):
            # 如果是记录标记、文件头尾或空行,原样保留
            new_lines.append(line)
            i += 1
            continue

        # 判断是否是待拆分的字段行
        field_match = False
        for field in fields_to_split:
            if line.startswith(field + ' '): # 行以“字段标签+空格”开头
                field_match = True
                field_content = line[len(field)+1:] # 获取字段值部分
                # 按分隔符拆分,并去除首尾空格
                parts = [part.strip() for part in field_content.split(separator) if part.strip()]
                if len(parts) > 1:
                    # 如果拆出多个部分,为每个部分生成新行
                    for part in parts:
                        new_lines.append(f'{field} {part}')
                else:
                    # 如果只有一个部分或拆分失败,保留原行
                    new_lines.append(line)
                break # 匹配到一个字段就跳出循环

        if not field_match:
            # 不是待拆分的字段,原样保留
            new_lines.append(line)
        i += 1

    # 写入新文件
    with open(output_txt, 'w', encoding='utf-8') as outfile:
        outfile.write('\n'.join(new_lines))

    print(f"精修完成!最终文件已保存至: {output_txt}")

# 使用示例
if __name__ == '__main__':
    input_file = 'intermediate_output.txt'
    output_file = 'final_output_for_citespace.txt'
    # 指定需要拆分的字段,这里以作者(AU)和地址(C1)为例
    refine_wos_file(input_file, output_file, fields_to_split=['AU', 'C1'], separator=';')

运行这个脚本后,你会得到 final_output_for_citespace.txt。打开看看,原先挤在一行的多个作者,现在是不是已经乖乖地每人独占一行了?这样就更加符合WOS原生数据的风格了。

4. 导入CiteSpace验证与效果分析

脚本跑通了,生成了一堆 .txt 文件,最关键的一步来了:它们能被CiteSpace认出来吗?让我们来实际验证一下。

首先,打开CiteSpace(这里以CiteSpace 6.3.R1版本为例)。在启动界面,你需要创建一个新的项目(Project)。给你的项目起个名字,比如“MyCSVConversionTest”,并指定一个空文件夹作为项目目录。

进入主界面后,找到 Data(数据) -> Import/Export(导入/导出)。在数据导入界面,最关键的是选择正确的数据来源(Data Source)。我们的文件是模仿WOS格式的,所以这里要选择 “Web of Science”

接下来,点击 “Select Directory” 按钮,选择你存放最终 final_output_for_citespace.txt 文件的文件夹。注意,CiteSpace在导入WOS数据时,通常是读取一个文件夹下的所有.txt文件。所以,你可以把最终文件复制一份,或者直接把它放在一个干净的文件夹里让CiteSpace去读。

点击 “Import” 按钮,CiteSpace就会开始解析你的文本文件。如果格式基本正确,你会在下方的日志窗口看到类似 “Processing: final_output_for_citespace.txt... Records loaded: 2000” 的成功信息,这表明它成功识别并加载了你转换的2000条记录。

导入成功后,你就可以像使用常规WOS数据一样进行各种分析了:

  • 去重:在Data菜单下进行去重操作。
  • 时区划分:设置时间切片(Time Slicing),比如从1990年到2024年,每2年或5年一个切片。
  • 节点类型选择:你可以选择分析关键词(Keyword)、作者(Author)、机构(Institution)、国家(Country)等。
  • 运行分析:设置好阈值(如Top N per slice),点击“Go”开始运算。

实测效果如何? 根据我的经验,只要字段映射正确(特别是TI, AU, PY, SO这几个CiteSpace用于识别记录的核心字段),并且每条记录被PT JER正确包裹,CiteSpace的导入成功率是非常高的。转换后的数据完全可以用于生成共现网络、聚类图、时间线图、突现检测等标准分析。

你可能会遇到的一些小问题及解决办法:

  • 警告“Invalid Tag”:这说明有字段标签不被CiteSpace识别。检查你的映射字典,是否用了WOS不存在的标签。可以暂时注释掉不重要的字段行。
  • 作者或机构名识别混乱:如果原始CSV数据中作者名格式不统一(如“Last, First” vs “First Last”),可能会导致CiteSpace将同一个人识别成两个节点。这需要在CSV数据清洗阶段下更多功夫,比如统一姓名格式。
  • 时间(PY)字段缺失或格式错误:这会影响时区分析。确保你的PY字段是四位数的年份,如“2023”。

5. 进阶技巧与脚本优化

基本的流程走通了,我们可以让这个工具变得更强大、更智能。这里分享几个我后来迭代脚本时加入的实用功能。

1. 自动化字段映射集成 我们之前是在Excel里手动改表头,其实完全可以写在Python脚本里,一步到位。创建一个 config.py 文件或者直接在主脚本里定义一个映射字典:

# 在 step1_transpose.py 开头加入
FIELD_MAPPING = {
    '文章标题': 'TI',
    '标题': 'TI',
    'Title': 'TI',
    '作者': 'AU',
    'Authors': 'AU',
    '摘要': 'AB',
    'Abstract': 'AB',
    '关键词': 'DE',
    'Keywords': 'DE',
    '出版年': 'PY',
    'Year': 'PY',
    'Publication Year': 'PY',
    '来源期刊': 'SO',
    'Journal': 'SO',
    '卷': 'VL',
    'Volume': 'VL',
    '期': 'IS',
    'Issue': 'IS',
    '页码': 'BP',
    'Pages': 'BP',
    'DOI': 'DI',
    '机构': 'C1',
    'Addresses': 'C1',
    # ... 添加更多映射
}

# 然后在读取CSV后,动态替换表头
import pandas as pd
df = pd.read_csv('your_original.csv', encoding='utf-8-sig')
df.rename(columns=FIELD_MAPPING, inplace=True)
# 删除那些没有映射的列(可选)
df = df.loc[:, df.columns.isin(FIELD_MAPPING.values())]
df.to_csv('input_mapped.csv', index=False, encoding='utf-8-sig')

这样,无论你的原始CSV列名是中文还是英文,脚本都能自动将其转换为WOS标签。

2. 处理缺失值与特殊字符 文献数据常有缺失,比如有些记录没有摘要。在生成WOS格式时,如果字段值为空,最好跳过该字段,而不是生成一个“AB ”(空值)的行。可以在组装记录行的循环中加入判断:if pd.notna(value):

另外,文本字段(如摘要AB)里可能有换行符\n,这会在WOS文本中造成混乱。一个简单的处理办法是在写入前将其替换为空格:value = str(value).replace('\n', ' ')

3. 批量处理与错误日志 如果你的数据来源是多个CSV文件,可以写一个循环,批量转换。同时,建议加入简单的日志功能,记录下转换成功的记录数,以及可能出错的记录ID(比如缺少必填字段TIPY的记录),方便后续核对。

4. 封装成函数或命令行工具 你可以把整个流程封装成一个函数,甚至用 argparse 库做成命令行工具,通过指定输入文件、输出目录、映射配置文件等参数来运行,这样就更像是一个专业的小工具了。

# 示例:简单的命令行接口
import argparse
parser = argparse.ArgumentParser(description='Convert CSV literature data to WOS format for CiteSpace.')
parser.add_argument('input', help='Path to the input CSV file')
parser.add_argument('-o', '--output', default='output_wos.txt', help='Path to the output WOS text file')
args = parser.parse_args()
# 然后调用你的转换函数
convert_csv_to_wos(args.input, args.output)

这样,在终端里输入 python csv2wos.py my_literature.csv -o result.txt 就能直接运行,非常方便。

6. 避坑指南与经验之谈

最后,分享几个我在这个过程中踩过的坑和总结的经验,希望能帮你少走弯路。

第一个大坑:编码问题。 这是最常遇到的问题,没有之一。中文环境下的CSV文件,编码可能是GBKUTF-8 with BOM。如果你用Python默认的utf-8读取GBK编码的文件,或者用utf-8读取带BOM的文件,开头可能会多出奇怪的字符(如\ufeff),导致字段名匹配失败。解决方案:在打开文件时,明确指定编码。对于可能带BOM的UTF-8文件,使用 encoding='utf-8-sig';对于其他情况,可以先用记事本打开CSV文件,点击“另存为”,在底部确认编码格式。

第二个坑:字段分隔符。 CSV虽然是“逗号分隔值”,但有时数据内本身就包含逗号(比如作者地址),这时导出软件可能会用引号将整个字段包起来。Python的csv模块能很好地处理这种情况。但如果你用pandasread_csv,需要注意quotechar参数。最稳妥的方法是,先用文本编辑器打开CSV看一眼它的具体格式。

第三个坑:CiteSpace的严格性。 CiteSpace对WOS格式有一定容错,但并非无限。PT JER必须成对出现,且PT J必须单独一行。字段标签(如TI)和值之间的空格最好只有一个。文件末尾的EF有时不加也能运行,但为了规范最好加上。我建议在生成最终文件后,用WOS官方导出的一个小文件和你生成的文件进行对比,用文本比较工具(如VS Code的对比功能)查看差异,能帮你快速定位格式问题。

关于数据质量。 转换工具只能解决格式问题,解决不了数据本身的质量问题。比如,作者名不统一(“Wang, Xiao” vs “Xiao Wang”)、关键词的同义词和近义词(“machine learning” vs “ML”)、机构名称的缩写和全称,这些都会直接影响CiteSpace分析结果的准确性。格式转换是第一步,而数据清洗和标准化才是产出可靠分析结果更耗时、更关键的一步。 你可以在Python脚本中加入一些简单的清洗规则,比如用正则表达式统一作者姓名的格式,或者用词典映射来规范关键词。

整个过程走下来,你会发现,用Python脚本处理这类格式转换任务,核心思路并不复杂:理解目标格式 -> 解析源数据 -> 映射和转换 -> 输出。它最大的价值在于将你从繁琐、重复的机械劳动中解放出来,让你能更专注于分析本身。当你看到CiteSpace成功加载了你亲手转换的几千条数据,并开始绘制出知识图谱时,那种成就感,绝对比手动一条条粘贴复制要强得多。希望这个详细的流程和代码,能切实地帮到你。如果在实际操作中遇到新的问题,不妨多看看CiteSpace的官方文档和社区论坛,很多时候答案就在那里。

更多推荐