├── .circleci └── config.yml ├── .gitignore ├── LICENSE ├── README.md ├── commandict ├── __init__.py └── get_result.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py └── test_get_result.py /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # commandict CI yaml file 3 | 4 | version: 2 5 | 6 | jobs: 7 | test: 8 | docker: 9 | - image: circleci/python:3.7.3 10 | steps: 11 | - checkout 12 | 13 | # Download and cache dependencies 14 | - restore_cache: 15 | keys: 16 | - venv-{{ checksum "requirements.txt" }} 17 | 18 | - run: 19 | name: Installing package dependencies... 20 | command: | 21 | if [[ ! -f ../venv ]]; then 22 | python3 -m venv ../venv 23 | source ../venv/bin/activate 24 | fi 25 | ../venv/bin/pip install -r requirements.txt 26 | 27 | - save-cache: 28 | key: venv-{{ checksum "requirements.txt" }} 29 | paths: ../venv 30 | 31 | # Running test 32 | - run: 33 | name: Running test... 34 | command: | 35 | ../venv/bin/pytest 36 | ../venv/bin/flake8 37 | ../venv/bin/yamllint .circleci/config.yml 38 | 39 | - store_artifacts: 40 | path: test-reports 41 | destination: test-reports 42 | 43 | 44 | workflows: 45 | version: 2 46 | test-then-build: 47 | jobs: 48 | - test 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */__pycache__/* 2 | *.egg-info/* 3 | *build/ 4 | *dist/ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Gallen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Commandict 2 | Use DAUM dictionary via terminal! 3 | 4 | 5 | # Prerequisites 6 | Python 3.7+ 7 | 8 | # Install 9 | ```$ pip install commandict``` 10 | 11 | # Usage 12 | ``` 13 | $ cmdct sapphire 14 | 15 | $ cmdic sapphire 16 | ``` 17 | -------------------------------------------------------------------------------- /commandict/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nellaG/commandict/09528bb4c503b82f576a17e0d78c7315575baa0f/commandict/__init__.py -------------------------------------------------------------------------------- /commandict/get_result.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ get_result.py """ 3 | 4 | import click 5 | from urllib.parse import parse_qsl, urljoin, urlparse 6 | 7 | import requests 8 | from bs4 import BeautifulSoup 9 | 10 | 11 | DAUM_DICT_HOST = "https://dic.daum.net/" 12 | 13 | LANG = 'eng' 14 | 15 | COMMAND_SET = { 16 | 'a': 'antonym', 17 | 'e': 'example sentences', 18 | 's': 'synonym', 19 | 'q': 'quit' 20 | } 21 | 22 | 23 | COMMANDS = "more: " + ' | '.join( 24 | [f'{COMMAND_SET[key]}({key})' for key in COMMAND_SET] 25 | ) 26 | 27 | 28 | def example_url(wordid: str, page: int = 1): 29 | example_host = f'{DAUM_DICT_HOST}/word/view_example_more.do' 30 | qsl = f'?wordid={wordid}&summaryid=etc&page={page}' 31 | return urljoin(example_host, qsl) 32 | 33 | 34 | def parse(html: str): 35 | bs = BeautifulSoup(html, 'html.parser') 36 | content = bs.findAll('meta', attrs={'property': 'og:description'})[0]\ 37 | .get('content') 38 | if not content: 39 | return 'No results found.', '' 40 | 41 | try: 42 | redir_url = bs.findAll('meta', attrs={'http-equiv': 'Refresh'})[0]\ 43 | .get('content').split('URL=')[1] 44 | except IndexError: 45 | # the result comes with polysemic words 46 | redir_url = bs.findAll('a', attrs={'txt_cleansch'})[0].attrs['href'] 47 | dic_query = urlparse(redir_url).query 48 | wordid = dict(parse_qsl(dic_query))['wordid'] 49 | return content, wordid 50 | 51 | 52 | def parse_detail(html: str, wordid: str, category: str): 53 | """ parse once more to get the detailed view """ 54 | 55 | bs = BeautifulSoup(html, 'html.parser') 56 | 57 | id_set = { 58 | 'antonym': 'OPPOSITE_WORD', 59 | 'synonym': 'SIMILAR_WORD' 60 | } 61 | if category not in id_set.keys(): 62 | pass 63 | else: 64 | words = bs.find(id=id_set[category]) 65 | if not words: 66 | # there's no antonym of this keyword 67 | return 'No results found.' 68 | tags = words.findAll('li') 69 | result = [ 70 | f"{tag.find('a').text}: {tag.find('span').text}" for tag in tags 71 | ] 72 | return '\n'.join(result) 73 | 74 | 75 | def parse_example(url: str): 76 | """ extract the example sentences """ 77 | 78 | html = requests.get(url).text 79 | bs = BeautifulSoup(html, 'html.parser') 80 | list_ = bs.findAll('li') 81 | sentences = [] 82 | for l in list_: 83 | eng_phrase = l.find('span', attrs={'txt_example'}).text.split('\n')[0] 84 | mean_phrase = l.find('span', attrs={'mean_example'}).text 85 | phrase_set = f'{eng_phrase}\n -> {mean_phrase}\n\n' 86 | sentences.append(phrase_set) 87 | return ''.join(sentences) 88 | 89 | 90 | @click.command() 91 | @click.argument('keyword', metavar='') 92 | def main(keyword): 93 | """ Use DAUM Dictionary via terminal """ 94 | click.echo('Searching...') 95 | url = f'{DAUM_DICT_HOST}search.do?q={keyword}&dic={LANG}' 96 | response = requests.get(url) 97 | meanings, wordid = parse(response.text) 98 | detailed_url = f'https://dic.daum.net/word/view.do?wordid={wordid}' 99 | detailed_text = None 100 | click.echo(meanings) 101 | if meanings == 'No results found.' and wordid == '': 102 | return 103 | 104 | while(True): 105 | value = click.prompt(click.style(COMMANDS, fg='white', bg='blue')) 106 | try: 107 | command = COMMAND_SET[value] 108 | except KeyError: 109 | click.echo("Sorry, I don't understand.") 110 | continue 111 | 112 | if value != 'q': 113 | if value == 'e': 114 | result = parse_example(example_url(wordid)) 115 | click.echo(result) 116 | 117 | else: 118 | # a / s 119 | if detailed_text is None: 120 | detailed_text = requests.get(detailed_url).text 121 | 122 | result = parse_detail(detailed_text, wordid, command) 123 | click.secho(command, fg='green') 124 | click.echo(result) 125 | else: 126 | break 127 | 128 | 129 | if __name__ == "__main__": 130 | main() 131 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click >= 7.0, < 7.0.1 2 | beautifulsoup4 >= 4.7.1, < 4.7.2 3 | flake8 >= 3.7.7, < 3.8.0 4 | requests >= 2.22.0, < 2.23.0 5 | pytest >= 5.0.0, < 5.1.0 6 | yamllint >= 1.16.0, < 1.17.0 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | 4 | install_requires = { 5 | 'click >= 7.0, < 7.0.1', 6 | 'requests >= 2.22.0, < 2.23.0', 7 | 'beautifulsoup4 >= 4.7.1, < 4.7.2', 8 | } 9 | 10 | 11 | setup(name='commandict', 12 | version='0.1.5', 13 | description='Use Daum dic via CLI', 14 | url='http://github.com/nellag/commandict', 15 | author='nellaG', 16 | author_email='seirios0107@gmail.com', 17 | license='MIT', 18 | packages=['commandict'], 19 | entry_points=''' 20 | [console_scripts] 21 | cmd = commandict.get_result:main 22 | cmdct = commandict.get_result:main 23 | cmdic = commandict.get_result:main 24 | ''', 25 | install_requires=list(install_requires), 26 | python_requires='>=3.6, <4', 27 | zip_safe=False) 28 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nellaG/commandict/09528bb4c503b82f576a17e0d78c7315575baa0f/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_get_result.py: -------------------------------------------------------------------------------- 1 | """ test_get_result.py """ 2 | 3 | import requests 4 | 5 | from click.testing import CliRunner 6 | from commandict.get_result import (example_url, main, parse, parse_detail, 7 | parse_example) 8 | 9 | 10 | DAUM_DICT_HOST = "https://dic.daum.net/" 11 | LANG = 'eng' 12 | 13 | 14 | # TODO: test with more words 15 | def test_parse_no_polysemy(): 16 | 17 | KEYWORD = 'buy' 18 | url = f'{DAUM_DICT_HOST}/search.do?q={KEYWORD}&dic={LANG}' 19 | response = requests.get(url) 20 | meanings, wordid = parse(response.text) 21 | assert meanings.startswith('1.사다') 22 | assert wordid == 'ekw000024208' 23 | 24 | 25 | def test_parse_with_polysemy(): 26 | 27 | KEYWORD = 'test' 28 | url = f'{DAUM_DICT_HOST}/search.do?q={KEYWORD}&dic={LANG}' 29 | response = requests.get(url) 30 | meanings, wordid = parse(response.text) 31 | assert meanings.startswith('1.시험') 32 | assert wordid == 'ekw000167718' 33 | 34 | 35 | def test_parse_no_result(): 36 | KEYWORD = 'cthulhu' 37 | url = f'{DAUM_DICT_HOST}/search.do?q={KEYWORD}&dic={LANG}' 38 | response = requests.get(url) 39 | result, wordid = parse(response.text) 40 | assert result == 'No results found.' 41 | assert wordid == '' 42 | 43 | 44 | def test_parse_example(): 45 | KEYWORD = 'buy' 46 | url = f'{DAUM_DICT_HOST}/search.do?q={KEYWORD}&dic={LANG}' 47 | response = requests.get(url) 48 | _, wordid = parse(response.text) 49 | exp_url = example_url(wordid) 50 | result = parse_example(exp_url) 51 | sentence_sample = 'Surprisingly, he has spent about $80,000 to buy the dolls!' # noqa 52 | assert sentence_sample in result 53 | 54 | 55 | def test_parse_detail(): 56 | # TODO: test with more words 57 | KEYWORD = 'buy' 58 | url = f'{DAUM_DICT_HOST}/search.do?q={KEYWORD}&dic={LANG}' 59 | response = requests.get(url) 60 | meanings, wordid = parse(response.text) 61 | detailed_url = f'https://dic.daum.net/word/view.do?wordid={wordid}' 62 | detailed_text = requests.get(detailed_url).text 63 | result = parse_detail(detailed_text, wordid, 'synonym') 64 | synonym = '''purchase: 구매하다, 구입하다, 매수하다, 사다, 인수하다 65 | pay for: 대가를 지불하다, 돈을 내다, 내다, 부담하다, 계산하다 66 | procure: 조달하다, 입수, 구하다, 도입, 얻다''' 67 | assert result == synonym 68 | result = parse_detail(detailed_text, wordid, 'antonym') 69 | antonym = 'sell: 팔다, 판매하다, 매각하다, 매도하다, 매매' 70 | assert result == antonym 71 | 72 | 73 | def test_parse_detail_no_antonym(): 74 | KEYWORD = 'test' 75 | url = f'{DAUM_DICT_HOST}/search.do?q={KEYWORD}&dic={LANG}' 76 | response = requests.get(url) 77 | meanings, wordid = parse(response.text) 78 | detailed_url = f'https://dic.daum.net/word/view.do?wordid={wordid}' 79 | detailed_text = requests.get(detailed_url).text 80 | result = parse_detail(detailed_text, wordid, 'synonym') 81 | synonym = '''work: 일하다, 연구, 작업, 작품, 작동하다 82 | study: 연구, 조사, 공부, 검토하다, 관찰하다 83 | ask: 묻다, 요청하다, 질문하다, 부탁하다, 말씀하다 84 | game: 게임, 경기, 시합 85 | research: 연구, 조사, 탐구, 탐사''' 86 | assert result == synonym 87 | result = parse_detail(detailed_text, wordid, 'antonym') 88 | antonym = 'No results found.' 89 | assert result == antonym 90 | 91 | 92 | def test_main_no_result(): 93 | KEYWORD = 'cthulhu' 94 | 95 | runner = CliRunner() 96 | result = runner.invoke(main, KEYWORD) 97 | assert result.exit_code == 0 98 | assert 'Searching...' in result.output 99 | assert 'No results found.' in result.output 100 | 101 | 102 | def test_main_undefined_command(): 103 | KEYWORD = 'command' 104 | runner = CliRunner() 105 | result = runner.invoke(main, 106 | args=KEYWORD, 107 | input='\n'.join(['undefined_command', 'q'])) 108 | assert result.exit_code == 0 109 | assert "Sorry, I don't understand." in result.output 110 | --------------------------------------------------------------------------------