用python将Android资源文件中的不同语言的ResourceName提取到excel中

使用Python程序自动分析Android APP的多语种strings.xml文件,高效统计并以Excel形式展示,极大提升工作效率。

背景

公司最近要输出app的各语种的翻译内容,但app的语种太多,人工统计极为麻烦,通过python程序自动识别资源文件中不同语种的strings.xml的分析,并将最终结果以excel的方式展示出来。

工具

xlwt xml.dom

准备工作

  • python环境
  • 安装xlwt

实现

分析Android资源文件的目录
Android资源文件
所有的资源都存放在values*文件夹下,我们只要values文件夹下的strings.xml文件

初始化excel表操作部分
def init_xls(sheetname):
    font = xlwt.Font()
    font.name = 'SimSun'
    style = xlwt.XFStyle()
    style.font = font
    fxls = xlwt.Workbook(encoding='utf-8')
    table = fxls.add_sheet(sheetname, cell_overwrite_ok=True)
    return fxls, table
解析strings.xml,将解析结果存到字典中
def read_string_xml(xmlname):
    sdict = {}
    doc = minidom.parse(xmlname)
    stringtaglen = len(doc.getElementsByTagName('string'))
    itemlen = len(doc.getElementsByTagName('item'))

    for idx in range(0, stringtaglen):
        nameattr = doc.getElementsByTagName('string')[idx].getAttribute('name')
        fc = doc.getElementsByTagName('string')[idx].firstChild
        if fc is None:
            content = ""
        else:
            content = fc.data
        sdict[nameattr] = content

    for idx in range(0, itemlen):
        nameattr = doc.getElementsByTagName('item')[idx].getAttribute('name')
        fc = doc.getElementsByTagName('item')[idx].firstChild
        if fc is None:
            content = ""
        else:
            content = fc.data
        sdict[nameattr] = content
    return sdict
输出到excel中
def main():
    inputfilepath = r'C:\Users\XXX\Desktop\res-MediaCenter'
    xls, table = init_xls('Language')
    table.write(0, 0, 'ResourceName')
    dir_list = os.listdir(inputfilepath)
    list = []
    dict = {}
    i = 1
    for file in dir_list:
        if file.__contains__('values'):
            filename = inputfilepath + '/' + file + '/' + 'strings.xml'
            if not os.path.exists(filename):
                continue
            resourcedict = read_string_xml(filename)
            for resource in resourcedict:
                if resource not in list:
                    list.append(resource)
                    dict[resource] = i
                    table.write(i, 0, resource)
                    i = i + 1
    k = 1
    for file in dir_list:
        if file.__contains__('values'):
            filename = inputfilepath + '/' + file + '/' + 'strings.xml'
            if not os.path.exists(filename):
                continue
            resourcedict = read_string_xml(filename)
            colname = file[7:]
            if colname == "":
                colname = "default"
            table.write(0, k, colname)
            for resource in resourcedict:
                table.write(dict[resource], k, resourcedict[resource])
            k = k + 1
    outputfilepath = r'C:\Users\XXX\Desktop\MediaCenter.xls'
    if os.path.exists(outputfilepath):
        os.remove(outputfilepath)
    xls.save(outputfilepath)

结语

一秒钟完成统计,编程提高工作效率。

import queue import threading import xml.etree.ElementTree as ET import os from concurrent.futures.thread import ThreadPoolExecutor import re from pathlib import Path from collections import defaultdict import copy from pathlib import Path import openpyxl from openpyxl import Workbook import tkinter as tk from tkinter import ttk suffixes_android = ['strings.xml', 'strings_v6.xml', 'strings_v2.xml'] suffixes_ios = ['Localizable.strings', 'LocalizableAdded.strings'] title_path = set() all_differ_key_value = [] queues_name_gloable = ['remediate_list'] queues_name = {name: queue.Queue() for name in queues_name_gloable} # 创建布尔变量及选框 language_vars = {} # 公共资源 ios_self_common = [] # 定制资源 ios_self_tai = [] lock = threading.Lock() class xmlObj: def __init__(self, key, value, old_value, old_key, path, ios_key, ios_value, ios_path, languages): self.key = key self.xml_path = path self.value = value self.languages = languages self.ios_key = ios_key self.ios_value = ios_value self.ios_path = ios_path self.old_value = old_value self.old_key = old_key def __eq__(self, other): if isinstance(other, xmlObj): return (self.key == other.key and self.xml_path == other.xml_path and self.value == other.value and self.languages == other.languages and self.ios_key == other.ios_key and self.ios_value == other.ios_value and self.ios_path == other.ios_path and self.old_value == other.old_value and self.old_key == other.old_key) return False def __str__(self): attrs = [ f"Key: {self.key}", f"Value: {self.value}", f"Old Value: {self.old_value}", f"Old Key: {self.old_key}", f"XML Path: {self.xml_path}", f"iOS Key: {self.ios_key}", f"iOS Value: {self.ios_value}", f"iOS Path: {self.ios_path}", f"Languages: {self.languages}" ] return ', '.join(attrs) class iosObj: def __init__(self, key, value, path, androidKey, androidValue, androidPath, languages): self.key = key self.ios_path = path self.value = value self.androidKey = androidKey self.androidValue = androidValue self.androidPath = androidPath self.languages = languages def merge_xml_files(xml_files): try: title_path.add(xml_files.split("\\")[5]) tree = ET.parse(xml_files) root = tree.getroot() language_code = get_android_language(xml_files) android_list = [] for child in root.findall("string"): key = child.attrib.get("name") value = child.text android_list.append(xmlObj(key, value, '', '', xml_files, '', '', '', language_code)) return get_android_ios_obj(android_list) except ET.ParseError as e: print(f"Error parsing file {xml_files}: {e}") def get_android_language(xml_files): switch_dice = { "zh-rMO": "TW", "zh-rZH": "zh-rCN", "pt-rBR": "pt", "es-rLA": "es", "el-rGR": "el", "es-rES": "es", } direct_name = os.path.dirname(xml_files) # 获取目录路径 -> 'res/values-pt-rBR' # 使用正则表达式提取语言标识(如 'pt-rBR') match = re.search(r'values-([a-z]{2}(-[a-zA-Z0-9]{1,8})?)', direct_name) if match: language_code = match.group(1) # 输出: pt-rBR count = language_code.count('-') if count > 0: language_code = switch_dice.get(language_code, language_code) else: language_code = "en" return language_code def get_ios_language(file_path): direct_name = os.path.dirname(file_path) language = os.path.basename(direct_name) count = language.count('-') if count == 0: language = language.split('.')[0] elif count == 1: language = language.split('.')[0].split('-')[0] elif count == 2: language = language.split('.')[0].split('-')[2] if language == "zh": language = "zh-rCN" elif language == "Base": language = "en" elif language == "CN": language = "TW" return language def parse_strings_files(ios_files): language = get_ios_language(ios_files) pattern = re.compile(r'"([^"]+)"\s*=\s*"([^"]*)";') with open(ios_files, 'r', encoding='utf-8', errors='ignore') as f: for line in f: match = pattern.match(line) if match: key = match.group(1) value = match.group(2) key = key.replace('\\', '"') value = value.replace('\\', '"') obj = iosObj(key, value, ios_files, '', '', '', language) if "Resource" in ios_files: ios_self_common.append(obj) else: ios_self_tai.append(obj) ANDROID_FILE_PATTERN = re.compile(r'^strings(_v\d+)?\.xml$') def is_valid_android_file(file_name: str) -> bool: """判断文件是否为 Android 类型,并且不等于 menustrings.commonstrings,country_strings""" return (ANDROID_FILE_PATTERN.match(file_name) and file_name != "menustrings.xml" and file_name != "commonstrings.xml" and file_name != "country_strings.xml") def travel_root(root_path): android_list = [] path_android = [] try: for root, dirs, files in os.walk(root_path, followlinks=False): valid_android = [os.path.join(root, file) for file in files if is_valid_android_file(file) and "Build" not in os.path.abspath(os.path.join(root, file)) and file.endswith(tuple(suffixes_android)) and contains_language(os.path.join(root, file))] print(f"Directory: {root}, Android files: {valid_android}") path_android.extend(valid_android) [parse_strings_files(os.path.join(root, file)) for file in files if file.endswith(tuple(suffixes_ios))] print(f"Total Android files collected: {len(path_android)}") with ThreadPoolExecutor(max_workers=12) as executor: android_sublist = list(executor.map(merge_xml_files, path_android)) android_list.extend(android_sublist) except Exception as e: print(f"Error: {e}") return android_list def contains_language(path): for key, value in language_vars.items(): if value.get() and key in path: return True return False def get_android_ios_obj(android_list): android_self_common = [] android_self_tai = [] for items_android in android_list: if "main" in items_android.xml_path: android_self_common.append(items_android) else: android_self_tai.append(items_android) merge_xml_obj = merge_list(compare_android_ios(android_self_common, ios_self_common), compare_android_ios(android_self_tai, ios_self_tai)) return merge_xml_obj def merge_list(android_list_self_common, android_list_self_tai): with lock: merge_xml_obj = [] merge_xml_obj.extend(android_list_self_common + android_list_self_tai) return merge_xml_obj def is_same_language(android_value, ios_value): if android_value.languages == ios_value.languages: return True else: return False def get_all_differ(android_copy, ios_self): try: android_by_key_and_value = defaultdict(list) ios_by_key_and_value = defaultdict(list) flatten_list = [item for sublist in android_copy for item in sublist] for item in flatten_list: if hasattr(item, "key") and hasattr(item, "value"): key = f"{item.key}{item.value}" android_by_key_and_value[key] = item else: print(f"无效对象,不包含 key 或 value 属性: {item}") for item in ios_self: if hasattr(item, "key") and hasattr(item, "value"): key = f"{item.key}{item.value}" ios_by_key_and_value[key] = item else: print(f"无效对象,不包含 key 或 value 属性: {item}") for key in android_by_key_and_value: if key not in ios_by_key_and_value: item = android_by_key_and_value.get(key, 'default') if item != 'default': all_differ_key_value.append(item) except Exception as e: print(f"get_all_differ函数执行出错: {e}") def compare_android_ios(android_self, ios_self): print("正在比较......") get_differ_key(android_self, ios_self) android_copy = copy.deepcopy(android_self) return get_differ_value(android_copy, ios_self) def get_differ_key(android_self, ios_self): try: for self_android in android_self: for self_ios in ios_self: if self_android.value == self_ios.value and is_same_language(self_android, self_ios): if self_android.key != self_ios.key: old_key = self_android.key self_android.key = self_ios.key xml = xmlObj(self_android.key, self_android.value, self_android.value, old_key, self_android.xml_path, self_ios.key, self_ios.value, self_ios.ios_path, '') queues_name['remediate_list'].put(xml) except Exception as e: print(f"get_differ_key 报错:{e}") def get_differ_value(android_copy, ios_self): for self_android in android_copy: for self_ios in ios_self: if self_android.key == self_ios.key and is_same_language(self_android, self_ios): if self_ios.value != self_android.value: self_android.value = self_ios.value xml = xmlObj(self_android.key, self_android.value, self_android.old_value, self_android.old_key, self_android.xml_path, self_ios.key, self_ios.value, self_ios.ios_path, '') queues_name['remediate_list'].put(xml) return copy.deepcopy(android_copy) def write_dict_to_xml(data_dict): try: group_data = defaultdict(list) for obj in data_dict: for item in obj: group_data[item.xml_path].append(item) for path, items in group_data.items(): resource = ET.Element('resources') for item in items: et = ET.SubElement( resource, "string", {"name": item.key} ) et.text = item.value tree = ET.ElementTree(resource) ET.indent(tree, space=" ") # 使 XML 格式整齐 tree.write(path, encoding="utf-8", xml_declaration=True, method='xml') print(f"写入文件中{path}请稍等") except IOError as e: print(f"写入xml文件操作失败({path}): {str(e)}") except AttributeError as e: print(f"写入xml对象属性缺失: {str(e)}") except ET.ParseError as e: print(f"写入XML格式错误: {str(e)}") except Exception as e: print(f"写入xml未知错误: {str(e)}") def write_to_excel(android_list_self, is_same, path): try: print("正在写入excel") wb = Workbook() for item in android_list_self: title = get_parent_directory(item.xml_path) for elem in title_path: if elem in item.xml_path: title = elem # 根据 title 获取或创建工作表 if title in wb.sheetnames: ws = wb[title] else: ws = wb.create_sheet(title=title) if is_same: ws.append( ["old_key", "old_value", "ios_key", "ios_value", "ios_path", "android_path", "key", "value"]) else: ws.append( ["old_key", "old_value", "ios_key", "ios_value", "ios_path", "android_path", "key", "value"]) # 将数据追加到对应工作表 ws.append( [item.old_key, item.old_value, item.ios_key, item.ios_value, item.ios_path, item.xml_path, item.key, item.value]) if is_same: file_path = os.path.join(path, 'android_same.xlsx') else: file_path = os.path.join(path, 'android_not_same.xlsx') file_path = os.path.join(path, file_path) wb.save(file_path) except PermissionError as e: print(f"文件保存失败:权限不足 ({str(e)})") except FileNotFoundError as e: print(f"文件路径无效:路径不存在 ({str(e)})") except openpyxl.utils.exceptions.IllegalCharacterError as e: print(f"Excel文件数据异常:包含非法字符 ({str(e)})") except AttributeError as e: print(f"对象属性错误:请检查数据完整性 ({str(e)})") def get_parent_directory(xml_path): path = Path(xml_path) parts = path.parts for i in range(len(parts) - 1): if parts[i].lower() == 'res': return path.resolve().parts[i - 1] return None is_all_selected = False def on_submit(): selected = [lang for lang, var in language_vars.items() if var.get()] print(f"你选择了: {selected}") root.destroy() def toggle_all(): global is_all_selected is_all_selected = not is_all_selected for var in language_vars.values(): var.set(is_all_selected) # 初始化窗口 root = tk.Tk() root.title("语言选择") root.geometry("400x800") # 语言选项(使用语言代码) languages = ['ar', 'ca', 'de', 'el', 'en', 'enm', 'es-rES', 'es-rLA', 'eu', 'fr', 'pt-rBR', 'ru', 'th', 'zh-rCN', 'zh-rMO', 'zh-rZH'] # 全选按钮 ttk.Button(root, text="全选", command=toggle_all).grid(row=len(languages) + 1, column=0, padx=20, pady=20) # 提交按钮 ttk.Button(root, text="提交", command=on_submit).grid(row=len(languages) + 1, column=1, padx=20, pady=20) # 复选框按钮 for idx, lang in enumerate(languages): var = tk.BooleanVar() ttk.Checkbutton(root, text=lang, variable=var).grid(row=idx, sticky="ew", padx=10, pady=5) language_vars[lang] = var # 启动事件循环 root.mainloop() if __name__ == '__main__': android_map = {} android_root_path = input("请输入Android路径:") ios_root_path = input("请输入ios路径:") travel_root(ios_root_path) key_value_list_android = travel_root(android_root_path) ios_self_common.extend(ios_self_tai) get_all_differ(key_value_list_android, ios_self_common) data_dict = list(queues_name['remediate_list'].queue) thread_remediate_list = threading.Thread(target=write_to_excel, args=(data_dict, True, android_root_path)) thread_all_differ_key_value = threading.Thread(target=write_to_excel, args=(all_differ_key_value, False, android_root_path)) thread_remediate_list.start() thread_all_differ_key_value.start() write_dict_to_xml(key_value_list_android)
最新发布
07-17
merge_root_path = os.path.join(Config.CURRENT_WORKSPACE_ROOT_PATH, Config.CURRENT_WORKSPACE_MERGE_NAME) translation_excel_folder_path = os.path.join(merge_root_path, 'Translation_Excel') if not os.path.exists(translation_excel_folder_path): print('目标翻译文件 {0} 路径不存在'.format(translation_excel_folder_path)) exit(1) merge_android_src_path = os.path.join(merge_root_path, 'Android', 'values-old') merge_android_dst_path = os.path.join(merge_root_path, 'Android', 'values-new') merge_ios_src_path = os.path.join(merge_root_path, 'iOS', 'strings-old') merge_ios_dst_path = os.path.join(merge_root_path, 'iOS', 'strings-new') merge_flutter_src_path = os.path.join(merge_root_path, 'Flutter', 'l10n-old') merge_flutter_dst_path = os.path.join(merge_root_path, 'Flutter', 'l10n-new') merge_pc_src_path = os.path.join(merge_root_path, 'PC', 'old') merge_pc_dst_path = os.path.join(merge_root_path, 'PC', 'new') enable_android_merge = os.path.exists(merge_android_src_path) enable_ios_merge = os.path.exists(merge_ios_src_path) enable_flutter_merge = os.path.exists(merge_flutter_src_path) enable_pc_merge = os.path.exists(merge_pc_src_path) if not os.path.exists(merge_android_dst_path): os.makedirs(merge_android_dst_path) if not os.path.exists(merge_ios_dst_path): os.makedirs(merge_ios_dst_path) if not os.path.exists(merge_flutter_dst_path): os.makedirs(merge_flutter_dst_path) if not os.path.exists(merge_pc_dst_path): os.makedirs(merge_pc_dst_path) if enable_android_merge or enable_ios_merge or enable_flutter_merge or enable_pc_merge: refer_excel_file = os.path.join(Config.CURRENT_WORKSPACE_ROOT_PATH, Config.CURRENT_WORKSPACE_COMMON_NAME, 'Refer_Standard.xls') if os.path.exists(refer_excel_file): refer_tuple = TranslateInfoExcelReader.read_excel_content_list(refer_excel_file) else: refer_tuple = [] translation_excel_file_list = os.listdir(translation_excel_folder_path) translation_excel_file_list = [x for x in translation_excel_file_list if x.endswith(".xlsx")] curr_time = time.strftime('%Y-%m-%d-%X', time.localtime(time.time())).replace(":", '') android_log_file_name = os.path.join(Config.CURRENT_WORKSPACE_ROOT_PATH, Config.CURRENT_WORKSPACE_MERGE_NAME, 'Translation_Log', "AndroidLog" + str(curr_time) + ".txt") flutter_log_file_name = os.path.join(Config.CURRENT_WORKSPACE_ROOT_PATH, Config.CURRENT_WORKSPACE_MERGE_NAME, 'Translation_Log', "FlutterLog" + str(curr_time) + ".txt") pc_log_file_name = os.path.join(Config.CURRENT_WORKSPACE_ROOT_PATH, Config.CURRENT_WORKSPACE_MERGE_NAME, 'Translation_Log', "PCLog" + str(curr_time) + ".txt") ios_log_file_name = os.path.join(Config.CURRENT_WORKSPACE_ROOT_PATH, Config.CURRENT_WORKSPACE_MERGE_NAME, 'Translation_Log', "Log-漏翻记录.txt") extract_root_path = os.path.join(Config.WORKSPACE_BASE_PATH ) locale_json_dst_file = os.path.join(os.path.join(extract_root_path), 'all_englist_translate_dict.text') all_english_translate_dict = dict() with open(locale_json_dst_file, 'r', encoding='utf-8') as f: all_english_translate_dict = json.load(f) if os.path.exists(ios_log_file_name): os.remove(ios_log_file_name) for excel in translation_excel_file_list: excel_file = os.path.join(translation_excel_folder_path, excel) print(excel_file) excel_dict = TranslateInfoExcelReader.read_excel_content_dict(excel_file) for item in excel_dict.values(): check_formatter(item.english, item.translate_result) for refer in refer_tuple: if refer.translate_result in excel_dict: english = refer.english platform = refer.platform no = refer.no note = refer.note translate = FormatSpecifierParser.build_format_char(refer.english, refer.translate_result, excel_dict[ refer.translate_result].translate_result) excel_dict[english] = TranslateInfo(english, translate, platform, no, note) locale = excel.replace('.xlsx', '') for item in excel_dict.values(): # print(excel, item.english, item.translate_result) if item.english in all_english_translate_dict: all_english_translate_dict[item.english][locale] = item.translate_result else: tmp_translate_dict = dict() tmp_translate_dict[locale] = item.translate_result all_english_translate_dict[item.english] = tmp_translate_dict if enable_android_merge and locale in Config.ANDROID_LOCALE_DICT: locale_path = 'values-' + Config.ANDROID_LOCALE_DICT[locale] merge_android_src_english_file = os.path.join(merge_android_src_path, 'values', 'strings.xml') merge_android_src_locale_file = os.path.join(merge_android_src_path, locale_path, 'strings.xml') merge_android_dst_locale_path = os.path.join(merge_android_dst_path, locale_path) XmlMerger(excel_dict, merge_android_src_english_file, merge_android_src_locale_file, merge_android_dst_locale_path, android_log_file_name).merge(1) if enable_ios_merge and locale in Config.IOS_LOCALE_DICT: locale_path = Config.IOS_LOCALE_DICT[locale] + '.lproj' merge_ios_src_english_path = os.path.join(merge_ios_src_path, 'en.lproj') merge_ios_dst_english_path = os.path.join(merge_ios_dst_path, 'en.lproj') merge_ios_src_locale_path = os.path.join(merge_ios_src_path, locale_path) merge_ios_dst_locale_path = os.path.join(merge_ios_dst_path, locale_path) StringsMerger(locale, excel_dict, merge_ios_src_english_path, merge_ios_dst_english_path, merge_ios_src_locale_path, merge_ios_dst_locale_path).merge() if enable_flutter_merge and locale in Config.FLUTTER_LOCALE_DICT: locale_path = Config.FLUTTER_LOCALE_DICT[locale] merge_flutter_src_english_file = os.path.join(merge_flutter_src_path, 'intl_en.arb') merge_flutter_src_locale_file = os.path.join(merge_flutter_src_path, 'intl_' + locale_path + '.arb') merge_flutter_dst_locale_path = os.path.join(merge_flutter_dst_path, 'intl_' + locale_path + '.arb') FlutterMerger(excel_dict, merge_flutter_src_english_file, merge_flutter_src_locale_file, merge_flutter_dst_locale_path, flutter_log_file_name).merge(1) if enable_pc_merge and locale in Config.PC_LOCALE_DICT: locale_path = Config.PC_LOCALE_DICT[locale] merge_pc_src_english_file = os.path.join(merge_pc_src_path, 'en.json') merge_pc_src_locale_file = os.path.join(merge_pc_src_path, locale_path + '.json') merge_pc_dst_locale_path = os.path.join(merge_pc_dst_path, locale_path + '.json') FlutterMerger(excel_dict, merge_pc_src_english_file, merge_pc_src_locale_file, merge_pc_dst_locale_path, pc_log_file_name).merge(1) locale_json_dst_file = os.path.join(os.path.join(extract_root_path), 'all_englist_translate_dict_new.text') with open(locale_json_dst_file, 'w', encoding='utf8') as f2: json.dump(all_english_translate_dict, f2, ensure_ascii=False, indent=2) 代码解析
06-28
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值