DragonEnglish——COCA20000单词+音频+释义+例句及翻译内容聚合

DragonEnglish——COCA20000单词+音频+释义+例句及翻译内容聚合


前言: 前段时间,我了解到了 COCA 20000 词汇,并且从网上得到了一个 excel 版的文件。不过,由于这个文件只有序号和单词,学习起来实在是感觉到困难重重。因为平时习惯使用网易有道词典,并且很喜欢它的例句功能,通过例句学习更容易理解单词的含义和用法。所以希望 COCA 这个词汇表也可以有单词的释义,例句等。因此,萌生了自己做一个工具的想法,这也是受到了 B 站大佬杨中科英语学习网站的启发。我的想法是比较简单的,因为前端的东西我并不是太熟悉,所以只要求最好它能够使用就好了。一开始设想的是做成 GUI 的形式,不过受限于技术栈,最终变成了一个基于 Web 的项目。
最初的想法是,通过特殊手段去爬取网易有道的例句,这个我去做了调研,也确实可以使用。不过由于它的速度太慢了(太快了就会被封锁),而且有一定的问题,最终我决定弃用这种方式了,如果有想要了解的,可以参考我的这篇博客: 基于 pywinauto 的自动化采集任务。然后,我就换了一种思路。因为我需要的只是例句,所以我想到了利用从大量文本中提取例句的方式。所以也就顺便了解了一下 COCA 和 BNC 的语料库。不过由于 COCA 的语料库需要付费下载,所以我选择了 BNC 的语料库。

数据来源:

  • COCA 20000 词汇
  • BNC corpus 语料库

COCA 词汇我是从 Github 获取的,不过我忘记了它的项目地址了,再次检索也没有找到了。BNC corpus 是一个英国的语料库,它里面包含了很多的英语文本。

视频演示

DragonEnglish 演示

每 100 个单词是一个文件夹,然后做了一个聚合索引的目录。最终做成了一个包含 200 多个 html 文件的形式,视频里面我是本地演示的。当然也可以放入服务器中,这里推荐使用 docker 的方式,一键启动。

生成数据库

首先是对语料库进行一个清洗,把 xml 中的数据抽取出来即可,这是原始的数据文件,总共有 4000 多个文件。
在这里插入图片描述

提取语料库的代码
注:这里我只选择了长度在 35 到 100 个字符的句子,所以生成的文本文件较小。

import os
import re
from typing import List

MIN_LEN = 35     # 文本行的最小长度
MAX_LEN = 100    # 文本行的最大长度
SUFFIX = ".xml"  # .xml 后缀


def text_extract(path: str) -> str:
    """
    path: input file path
    处理输入文件, 提取其中的文本行.
    """
    # 1.读取文件中的所有文本
    # 2.正则表达式, 匹配所有的标签.
    # 3.把所有匹配的标签替换成空字符串
    # 4.对文本进行过滤, 去除较短的行
    with open(path, "r", encoding="utf-8") as f:
        content = f.read()
    REGEXP = r'<[^<>]+>'
    pattern = re.compile(REGEXP)
    result = pattern.sub("", content)

    lines = text_filter(result.splitlines())

    # 对列表操作一定要看长度不要超过范围了!
    # 如果列表是空是,就返回空即可.
    if not lines:
        return ""

    # 在最后一行写入一个换行符, 因为 join 只能在中间拼接
    lines[len(lines)-1] = lines[len(lines)-1] + "\n"

    return "\n".join(lines)


def text_filter(rows: List[str]) -> List[str]:
    """
    对文件进行一些简单的过滤操作, 现在是去除较短的行.
    """
    rs = []
    for row in rows:
        # 选择长度在 MIN_LEN 和 MAX_LEN 之间的文本行
        if len(row) >= MIN_LEN and len(row) <= MAX_LEN:
            rs.append(row)
    return rs


def find_all_text(dir: str) -> List[str]:
    """
    获取指定目录下所有的xml文件
    """
    file_paths = []
    for root, _, files in os.walk(dir):
        for filename in files:
            if filename.endswith(SUFFIX):
                file_path = os.path\
                    .join(root, filename)\
                    .replace("\\", "/")      # 把正斜杠替换成反斜杠(Windows系统)
                file_paths.append(file_path)
    # show number of all files that been found.
    print("Total find %d files." % len(file_paths))

    return file_paths


if __name__ == "__main__":
    print("The programe is Processing...")
    # 输入文件目录路径
    dirPath = r"输入文件夹的路径,到 Texts 就行了"
    # 输出文件路径
    output_file_path = r"输出文件的路径"
    with open(output_file_path, "w", encoding="utf-8") as f:
        for file_path in find_all_text(dirPath):
            content = text_extract(file_path)
            # 只针对非空字符串进行写入操作
            if content:
                f.write(content)
    print("The programe is end.")

执行输出
在这里插入图片描述
生成文件
在这里插入图片描述

匹配例句

匹配例句,就是读取给定的 20000 个单词的文件,然后对每一个单词,在语料库文件中匹配例句。这个过程是非常耗时的,因为例句的数量太大了。所以,我给每个单词定了一个数量,如果一个单词匹配到了 10 个例句,就结束匹配过程。同时,我还开启了多进程的方式,直接把 CPU 跑满了,不过整个过程还是很长,大概4个多小时吧(7代i7笔记本)。

import logging
from multiprocessing import Pool, Manager
import os
import re
import sqlite3 as db
from datetime import datetime
from typing import List, Tuple, Dict

"""
sqlite table structure:
id word original translate
"""

MAX_COUNT = 10  # 最大匹配次数
MAX_TASK = 20  # 最大任务数


# 日志设置
logging.basicConfig(
    level=logging.INFO,
    encoding="utf-8"
)


def load_lines(src: str) -> str:
    """
    从指定src载入目标文件, 但是这个文件挺大的, 可能会有效率问题.
    """
    # 这一次性载入了上百万行数据
    logging.info(f"Start to load lines: {datetime.now()}.")
    with open(src, "r", encoding="utf-8") as f:
        lines = f.read()
    logging.info(f"Load lines end: {datetime.now()}.")

    return lines


def load_words(src: str) -> List[str]:
    """
    从指定 src 载入目标单词, 大概 20000 个.
    """
    logging.info(f"Start to load words: {datetime.now()}.")
    with open(src, "r", encoding="utf-8") as f:
        lines = [line.rstrip() for line in f.readlines()]

    # 切分开序号和单词
    words = []
    for line in lines:
        word = line.split(" ", 2)[1]
        words.append(word)

    logging.info(f"Load words end: {datetime.now()}.")

    return words


def match_line(res, tasks, lines, index):
    """
    从载入的行中寻找匹配目标单词的行, 即单词所在的行.
    """
    pid = os.getpid()              # 获取当前进程的id
    word_count = 0                 # 单词计数器, 每隔100个单词打印一次信息, 缓解等待的焦虑感.
    word_order = tasks["start"]     # 起始单词的序号
    rs: List[Dict[str, str]] = []  # 存储匹配结果的数组
    for word in tasks["words"]:
        regex = r'^.*?\b{}\b.*?$'.format(word)
        count = 1      # 计数器
        matches = re.finditer(regex, lines, re.M)
        flag = False  # 是否匹配到的标志位
        for r in matches:
            # 超过最大匹配次数则跳出该层循环
            if count > MAX_COUNT:
                break
            rs.append({
                "word": word,
                "sentence": r.group(),
                "order": word_order
            })
            count = count+1
            flag = True
        if not flag:
            # 没有匹配的结果也入库, 后期再处理
            rs.append({
                "word": word,
                "sentence": "",
                "order": word_order
            })
            logging.info(f"The word: {word} no matched!")
        word_order = word_order + 1    # 单词序号 + 1
        word_count = word_count + 1    # 单词计数
        if word_count % 100 == 0:
            logging.info(f"The process {pid} Processed {word_count}th word...")
    # 将所有获取的数据, 全部存入共享数组中
    res[index] = rs


def init_db(db_path: str) -> Tuple[db.Connection, db.Cursor]:
    """
    db_path: database file storage path
    Iinit DataBase and create table.
    """
    # 在指定目录创建数据库文件并返回连接
    conn = db.connect(db_path)
    # 创建表 t_dragon_english, 表结构如下:
    # [f_id] [f_order] [f_word] [f_original] [f_translate]
    conn.execute("CREATE TABLE IF NOT EXISTS t_dragon_english(\
            f_id INTEGER PRIMARY KEY AUTOINCREMENT,\
            f_order INTEGER NOT NULL,\
            f_word CHAR(25) NOT NULL,\
            f_original TEXT NOT NULL,\
            f_translate TEXT NULL\
        )")

    conn.commit()
    # 创建游标对象
    cursor = conn.cursor()
    return conn, cursor


def add_record(cursor: db.Cursor, **kwargs) -> None:
    """
    向数据库插入记录
    """
    sql = """INSERT INTO t_dragon_english(f_id, f_order, f_word, f_original) \
        VALUES (NULL, ?, ?, ?)"""
    cursor.execute(sql, (kwargs["order"], kwargs["word"], kwargs["original"]))


def store2db(res):
    """
    把数据存入数据库, 一次性写入
    """
    conn, cursor = init_db(db_path)
    c = 0
    for rs in res:
        c = c + 1
        for r in rs:
            add_record(
                cursor,
                word=r["word"],
                original=r["sentence"],
                order=r["order"]
            )
        if rs:
            logging.info(f"第 {c} 个进程结果写入完成.")
        else:
            logging.info(f"第 {c} 个进程结果为空, 不写入.")
        conn.commit()  # 写完一个进程的数据, 提交一次


if __name__ == "__main__":
    # 定义变量
    base_path = "C:/Users/Alfred/Desktop/DragonEnglish/"
    lines_path = os.path.join(base_path, "corpus.txt")
    words_path = os.path.join(base_path, "COCA_20000.txt")
    db_path = os.path.join(base_path, "dragon_english.db")
    # 加载文件数据
    lines = load_lines(lines_path)
    words = load_words(words_path)
    # 开始查找
    logging.info("The programe start to processing...")
    # 使用进程池来优化程序的执行时间
    cpu_count = os.cpu_count()   # 获取 cpu 核数, 将任务切割成相应的份数
    if not cpu_count:
        logging.info("System Error!")
        exit(-1)

    # 分割单词
    # [{
    #     "words": [],
    #     "start": 0
    # }]
    tasks = [{} for i in range(MAX_TASK)]
    num = len(words) // MAX_TASK
    start = 0
    end = num
    for i in range(MAX_TASK):
        if i == MAX_TASK-1:
            end = len(words)
        tasks[i]["words"] = words[start:end]
        tasks[i]["start"] = start+1  # start 是下标, 序号要加 1
        start = end
        end = end + num
    with Manager() as manager:
        res = manager.list([[]]*MAX_TASK)
        # 调度原则: 进程要够多, 任务要够小
        with Pool(processes=cpu_count) as pool:
            proc_task = [(res, tasks[MAX_TASK-i-1], lines, MAX_TASK-i-1) for i in range(MAX_TASK)]
            pool.starmap(match_line, proc_task)
            pool.close()
            pool.join()

        # 把结果写入数据库
        logging.info("Starting to wiret result to db:")
        store2db(res)
    logging.info(f"The programe is end: {datetime.now()}.")

翻译例句

翻译例句我选择的是百度翻译和腾讯翻译的免费api,不过百度翻译的额度变低了,我没有控制住,导致额度满了,所以我花了 1 元钱充值。这一块的代码,就不展示了,直接使用官方文档中对应语言的示例简单修改就可以使用了。我只翻译每个单词所有例句中最短的前 3 句,因为 10 句太多了,暂时用不到。

百度翻译 api 使用情况
在这里插入图片描述
腾讯翻译 api 使用情况
在这里插入图片描述

数据恢复

由于我在双十一期间换了新的电脑,在数据迁移时漏了之前的项目。这也就导致,我失去了原先的数据库和一部分代码,里面包含大概20万条数据。不过,我还是保存了上面演示视频里面的文件,所以我就直接来一个反向操作,从文件中恢复数据库。

"""
之前的数据丢失了,现在试着利用已有的数据,来恢复数据库。
"""
import os
import sqlite3 as db
from typing import Dict, List, Tuple

from bs4 import BeautifulSoup

DIR_PATH = r"E:/dragon_english/content"
DB_PATH = r"C:/Users/alfred/Desktop/DragonEnglish/db/dragon_english.db"

def extract_info(path: str) -> List[Dict[str, str|List[Dict[str, str]]]]:
    """
    data structure
    [
        {
            "order": "1",
            "word": "fuck",
            "sentences": [
                {
                    "original": "",
                    "translate": ""
                }
            ]
        }
    ]
    """
    # 创建 soup 对象
    soup = BeautifulSoup(open(path, "r", encoding="utf-8").read(), 'html.parser')
    word_objs = []
    # 获取 word_obj div
    for word_obj_tag in soup.find_all(name="div", attrs={"class": "word_obj"}):
        # 每个单词分为三个部分:word trans sentences,所以依次提取它们即可(这里我不要 trans)
        order_word = word_obj_tag.find(name="p", attrs={"class": "word"}).text.strip()
        
        word_obj = {
            "order": order_word.split(".")[0],  # 单词order
            "word": order_word.split(".")[1],   # 单词本身
            "sentences" : []                    # 单词例句
        }

        ""

        for sentence in word_obj_tag.find_all(name="li"):
            word_obj["sentences"].append({                  # 这里把双引号给替换掉了,否则插入会有问题
                "original": sentence.find(name="p", attrs={"class": "original"}).text.strip().replace('"', ''),
                "translate": sentence.find(name="p", attrs={"class": "translate"}).text.strip()
            })
        word_objs.append(word_obj)

    return word_objs

def init_db(db_path: str) -> Tuple[db.Connection, db.Cursor]:
    """
    db_path: database file storage path
    Iinit DataBase and create table.
    """
    # 在指定目录创建数据库文件并返回连接
    conn = db.connect(db_path)
    # 创建表 t_dragon_english, 表结构如下:
    # [f_id] [f_order] [f_word] [f_original] [f_translate]
    conn.execute("CREATE TABLE IF NOT EXISTS t_dragon_english(\
            f_id INTEGER PRIMARY KEY AUTOINCREMENT,\
            f_order INTEGER NOT NULL,\
            f_word CHAR(25) NOT NULL,\
            f_original TEXT NOT NULL,\
            f_translate TEXT NULL\
        )")

    conn.commit()
    # 创建游标对象
    cursor = conn.cursor()
    return conn, cursor

def insert_db(cursor: db.Cursor, word_objs: List[Dict[str, str|List[Dict[str, str]]]]):
    """
    每个文件中的所有的 word_obj 生成一个 sql 语句
    """
    if word_objs:
        # 最终的拼接 SQL 的头
        header = """INSERT INTO t_dragon_english (f_id, f_order, f_word, f_original, f_translate) values"""
        flag = False
        # 所有的值组成的字符串列表
        values = []
        for word_obj in word_objs:
            order = word_obj["order"]
            word = word_obj["word"]
            for sentence in word_obj["sentences"]:
                original = sentence["original"]  # type: ignore
                translate = sentence["translate"]  # type: ignore
                value = f'(NULL, {order},"{word}","{original}","{translate}")'
                if flag:
                    values.append(value)
                else:
                    # 只执行一次
                    values.append(header+value)
                    flag = True
        sql = ",".join(values)
        try:
            cursor.execute(sql)
        except Exception as err:
            print(sql)
            print(err)
            exit(-1)
    else:
        print("empty word_objs")


if __name__ == "__main__":
    # 初始化数据库
    conn, cursor = init_db(DB_PATH)
    # 计数器
    counter = 1
    # 遍历文件夹
    for root, _, filenames in os.walk(DIR_PATH):
        for filename in filenames:
            insert_db(cursor, extract_info(os.path.join(root, filename)))
            conn.commit()
            print(f"第 {counter} 次提交")
            counter += 1

*最终,花费了11秒恢复了数据库(代码写了几小时,因为是现学的 BeautifulSoup)
不过,这只是原先 10 句例句里面的 3 句,但是由于剩下的也没有进行翻译,所以丢失了也就接受这个现实吧。

在这里插入图片描述

数据库文件
在这里插入图片描述

数据条数
在这里插入图片描述

单词音频获取

单词音频获取是利用了 Github 上面的一个项目 English Words Pronunciation MP3 Audio Download,它收录了 119,376 单词的 mp3 音频。因为我需要的只是它的一个子集,所以我是排除掉了我不需要的文件,然后下载了我需要的文件。通过这样可以获取大部分的单词,因为有些单词音频链接已经失效了,有些则是不存在于其中。所以剩下的一部分使用了谷歌的发音链接+国内的发音链接,最终下载了 17,632 个单词的发音文件。COCA 20000,其中有 2400 多个重复的单词,但是含义不同,所以发音文件就是差不多这些了。

在这里插入图片描述

项目文件结构

现在已经有的数据一个包含单词的序号、单词、单词的例句和翻译的数据库以及单词的音频文件。注意,这里是不包含单词的释义的,我没有将其存入数据库中。利用这些我就可以直接通过模板来生成需要的最终格式了,最终的格式是如下(图示为1-1000单词的文件结构):

1-1000单词文件结构
在这里插入图片描述
在这里插入图片描述

总结

为了做这个东西,倒是花费了不少时间,学习了几个技术,不过做出来的东西还是感觉很丑。不过逻辑上是没有什么问题了,如果大家感兴趣的话,可以去 B站看看视频。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值