ある個人開発 OSS の歩み: 5 歳になった Janome のこれまでと,これから

ある個人開発 OSS の歩み: 5 歳になった Janome のこれまでと,これから

PyConJP 2020 の登壇資料です

8261d04bf57a042c8eab6757c386f7b2?s=128

Tomoko Uchida

August 28, 2020
Tweet

Transcript

  1. ͋Δݸਓ։ൃ OSS ͷาΈɿ 5 ࡀʹͳͬͨ Janome ͷ͜Ε·Ͱͱɼ͜Ε͔Β PyConJP 2020 ଧాஐࢠ

  2. ࣗݾ঺հ ଧాஐࢠ ڵຯɿݕࡧʢຊۀʣɼػցֶशɼࣗવݴޠॲཧ ❤ OSS : Janome ։ൃऀɼApache Lucene committer

    ॴଐɿʢגʣLegalForce ݚڀ։ൃΤϯδχΞ @moco_beta
  3. Agenda Janome ͷ঺հ ॳظϦϦʔε͔Β͜Ε·Ͱ 0.4.0 ϦϦʔεͷ͓஌Βͤ ͜Ε͔Β΍͍͖͍ͬͯͨ͜ͱ https://mocobeta.github.io/janome/ 扉絵の この子は絵師さんに描いてもらいました

    (*´Ŗ`*)
  4. Hello, Janome $ pip install janome $ python >>> from

    janome.tokenizer import Tokenizer >>> t = Tokenizer() >>> for token in t.tokenize('౦ژ౎͸౦ͷژ౎'): ... print(token) ... ౦ژ ໊ࢺ,ݻ༗໊ࢺ,஍Ҭ,Ұൠ,*,*,౦ژ,τ΢Ωϣ΢,τʔΩϣʔ ౎ ໊ࢺ,઀ඌ,஍Ҭ,*,*,*,౎,τ,τ ͸ ॿࢺ,܎ॿࢺ,*,*,*,*,͸,ϋ,ϫ ౦ ໊ࢺ,Ұൠ,*,*,*,*,౦,ώΨγ,ώΨγ ͷ ॿࢺ,࿈ମԽ,*,*,*,*,ͷ,ϊ,ϊ ژ౎ ໊ࢺ,ݻ༗໊ࢺ,஍Ҭ,Ұൠ,*,*,ژ౎,Ωϣ΢τ,Ωϣʔτ
  5. ಛ௃, Pros / Cons Python ඪ४ϥΠϒϥϦͷΈͰॻ͔Εͨɼࣙॻ಺แܕͷܗଶૉղੳϥΠϒϥϦ Pros : Πϯετʔϧ͕؆୯ɼ؀ڥΛબ͹ͳ͍ʢ512MB ఔ౓ͷϝϞϦ͑͋͞Ε

    ͹ʣɼՄൖੑ͕ߴ͍ Cons : ಈత(ܕ෇͚)ݴޠͰॻ͔Ε͓ͯΓղੳ଎౓͕஗͍ɼϝϞϦ࢖༻ྔ͕େ͖͍ རศੑͱύϑΥʔϚϯεͷؒʹ͸ৗʹτϨʔυΦϑ͕͋ΓɼόϥϯεΛऔΓͭͭ ։ൃ͍ͯ͠Δɾɾɾͭ΋Γ 辞書データは mecab-ipadic (MeCab のデフォルト) を流用
  6. ॳϦϦʔε(2015೥)͔Β͜Ε·Ͱ 3େτϐοΫ 1. ϝϞϦϦʔΫରࡦͷ࿩ 2. େ͖ͳࣙॻ (mecab-ipadic-neologd) ʹରԠ͠Α͏ͱؤுͬͨ࿩ 3. Analyzer

    ϑϨʔϜϫʔΫͷ࿩
  7. 1. ϝϞϦϦʔΫରࡦ ॳظͷ࣮૷͸ͱͯ΋φΠʔϒͩͬͨ ௕͍ೖྗ͕༩͑Β͑ΕΔͱɼҰ౓ʹڊେͳϥςΟεάϥϑΛ࡞ͬͯղੳ͠Α͏ͱ͢Δ άϥϑͷେ͖͞͸ೖྗͷ2৐Ͱޮ͍ͯ͘Δ => ͙͢ʹݶք͕... ϝϞϦΛ৯͍ͭͿ͞ΕͨɼPC͕མͪͨɼͳͲใࠂଟ਺ ʢਖ਼௚ͦΜͳ͕ͬͭΓ࢖ΘΕΔͱࢥ͍ͬͯͳ͔ͬͨʣ

  8. 1. ϝϞϦϦʔΫରࡦ v0.3.0 Ͱɼ௕͍จࣈྻ͸۠੾Γͳ͕Β෦෼తʹղੳ͢ΔΑ͏ʹมߋ def __should_split(self, text, pos): return \

    pos >= len(text) or \ pos >= Tokenizer.MAX_CHUNK_SIZE or \ (pos >= Tokenizer.CHUNK_SIZE and self.__splittable(text[:pos])) def __splittable(self, text): return self.__is_punct(text[-1]) or self.__is_newline(text) def __is_punct(self, c): return c == u'ɺ' or c == u'ɻ' or c == u',' or c == u'.' or c == u'ʁ' or c == u'?' or c == u'ʂ' or c == u'!' def __is_newline(self, text): return text.endswith('\n\n') or text.endswith('\r\n\r\n') Ͳ͜Ͱ੾Δ͔ɿ ͳΔ΂ࣗ͘વͳҐஔʢվߦ΍۟ ಡ఺ʣͰ੾Δ ࠷େνϟϯΫαΠζΛ௒͑ͨΒ ͹ͬ͞Γ੾Δ
  9. 2. େ͖ͳࣙॻ΁ͷରԠ mecab-ipadic-neologd ΛೖΕͯ࢖͍͍ͨͱ͍͏੠͕ฉ͑ͨ͜ʢؾ͕ͨ͠ʣ ໰୊͸ɼࣙॻαΠζ Ͳͷ͘Β͍େ͖͍ͷ͔ mecab-ipadic : 39ສΤϯτϦʔ /

    30 MB (CSVϑΝΠϧαΠζ) mecab-ipadic-neologd : 466ສΤϯτϦʔ / 659 MB (CSVϑΝΠϧαΠζ) 
 (※20200813Ξοϓσʔτ)
  10. 2. େ͖ͳࣙॻ΁ͷରԠɿLet's try it! ໰୊1 ϓϩηε༻ͷϝϞϦ্ۭؒʹࣙॻશମΛϩʔυ͢Δͱɼ໓ଟʹ͸࢖ΘΕͳ͍σʔλͰϝϞϦۭ͕ؒ઎༗͞Ε ΔɻTokenizer ͷॳظԽ΋஗͘ͳΔ Memory-mapped file

    (mmap) ϑΝΠϧγεςϜ্ͷϑΝΠϧΛɼԾ૝ϝϞϦ্ʹ௚઀Ϛοϐϯά͢Δ OS ͷػೳʢʹγεςϜίʔϧʣ "ϑΝΠϧʹ௕͍࣌ؒΞΫηε͢Δඞཁ͕͋ΓɺΞΫηεύλʔϯ͕ϥϯμϜΞΫηεͱ͍͏৔߹͸ੑೳతͳԸܙ ͕ड͚ΒΕΔՄೳੑ͕ߴ͍" (https://www.allbsd.org/~hrs/blog/2017-01-02-read-mmap.html) Python ͷ mmap ඪ४ϥΠϒϥϦ mmap ͞ΕͨϑΝΠϧΦϒδΣΫτ͸ɼϝϞϦ্ͷόΠτ഑ྻ (bytearray) ͱಉ͡ΠϯλϑΣʔεͰΞΫηεͰ͖Δ େ͖ͳࣙॻσʔλ͸ɼTokenizer ॳظԽ࣌ʹ memory mapped ͓ͯ͘͠ʢϓϩηε্ۭؒʹಡΈࠐ·ͳ͍ʣ ղੳͰඞཁʹͳͬͨ࣌ʹɼඞཁͳࣙॻΤϯτϦʔ͚ͩΛϓϩηεۭؒʹίϐʔͯ͠࢖͏
  11. 2. େ͖ͳࣙॻ΁ͷରԠɿLet's try it! ໰୊2 σʔλ͕େ͖͗ͯࣙ͢ॻϏϧυ͕ऴΘΒͳ͍ ࣙॻσʔλΛద౰ͳόέοτʹ౳෼͠ɼෳ਺ϫʔΧʔεϨουͰ෼ׂϏϧυ def create_pool(processes): global

    pool pool = Pool(processes=processes) # multiprocessing.Pool def build_dict(dicdir, outdir, workdir, pool): input_files = glob.glob(os.path.join(workdir, 'input*.pkl')) func = functools.partial(save_partial_fst, outdir=outdir) pool.map(func, enumerate(input_files)) # Pool.map() pool.close() pool.join() ...
  12. 2. େ͖ͳࣙॻ΁ͷରԠ ͳΜͱ͔Ϗϧυ͕Ͱ͖ͯಈ͍ͨͱ͜ΖͰ࣮ݧతʹެ։ ϏϧυࡁΈύοέʔδ΋഑෍ʢෆఆظߋ৽ʣ https://github.com/mocobeta/janome/wiki/(very-experimental)-NEologd-%E8%BE%9E%E6%9B%B8%E3%82%92%E5%86%85%E5%8C%85%E3%81%97%E3%81%9F- janome-%E3%82%92%E3%83%93%E3%83%AB%E3%83%89%E3%81%99%E3%82%8B%E6%96%B9%E6%B3%95

  13. 3. Analyzer ϑϨʔϜϫʔΫ ܗଶૉղੳͷલॲཧɾޙॲཧΛςϯϓϨʔτԽ͢Δ֦ுػೳ Apache Lucene (Solr / Elasticsearch) ͷ

    Analyzer ϑϨʔϜϫʔΫΛ༌ೖͨ͠΋ͷ શ֯ɾ൒֯จࣈͷਖ਼نԽ େจࣈɾখจࣈͷਖ਼نԽ ඼ࢺϑΟϧλʔ ͳͲɼϏϧτΠϯͷલॲཧɾޙॲཧΫϥεΛ༻ҙ
  14. 3. Analyzer ϑϨʔϜϫʔΫ from janome.tokenizer import Tokenizer from janome.analyzer import

    Analyzer from janome.charfilter import UnicodeNormalizeCharFilter, RegexReplaceCharFilter from janome.tokenfilter import LowerCaseFilter, CompoundNounFilter a = Analyzer( tokenizer=Tokenizer(), char_filters=[UnicodeNormalizeCharFilter(), RegexReplaceCharFilter('ऄͷ໨', 'janome')], token_filters=[CompoundNounFilter(), LowerCaseFilter()]) for token in a.analyze('ऄͷ໨͸Pure ̷̥͈̓̾̽ͳܗଶૉղੳثͰ͢ɻ'): print(token.surface, end=' / ') ... janome / ͸ / pure / / python / ͳ / ܗଶૉղੳث / Ͱ͢ / ɻ/ ↓前処理(文字の正規化) ←後処理
  15. ͦͷଞͷ׆ಈ PythonɼࣗવݴޠॲཧʗςΩετ෼ੳೖ໳ऀ޲͚ͷνϡʔτϦΞϧΛެ։͍ͯ͠· ͢ Janome Ͱ͸͡ΊΔςΩετϚΠχϯά https://github.com/mocobeta/janome-tutorial Google Colab ϕʔεͳͷͰɼඞཁͳͷ͸ϒϥ΢βͷΈ

  16. ؓ࿩ɿଓ͚ͯΑ͔ͬͨ͜ͱ͍Ζ͍Ζ ࣗ෼ͷ஌ࣝͱεΩϧΛ޲্ͤ͞ͳ͕Βɼ Python ίϛϡχςΟʹߩݙͰ͖ͨ ੵۃతʹએ఻͸͍ͯ͠ͳ͔͕ͬͨɼؾͮ͘ͱ࡞ऀͷ૝૾Ҏ্ͷϖʔεͰ޿·͍ͬͯͨ ͨ͘͞ΜͷϒϩάͰऔΓ্͛ͯ΋Βͬͨʢ͋Γ͕ͱ͏͍͟͝·͢ʂʣ Python ΍ࣗવݴޠॲཧͷೖ໳ॻ੶Ͱ঺հ͞ΕΔΑ͏ʹͳͬͨ ֶߍ΍ΦϯϥΠϯίʔεͷೖ໳ڭࡐͰ࢖ΘΕΔΑ͏ʹͳͬͨ ίϛϡχςΟʹΑΔ͕ʮjanome

    ͷਓʯͱ͍͏ࣗݾ঺հ͕Ͱ͖ΔΑ͏ʹͳͬͨ ϦΞϧɾΠϯλʔωοτͰͷ஌ਓ͕૿͑ͨ
  17. ؓ࿩ɿָ͍͠ݸਓOSS։ൃͷ͸͡Ί͔ͨ ʮझຯͰOSS։ൃΛ͢Δʯʹ͋ͨͬͯ ͔ͤͬ͘༨Ջͷ࣌ؒΛ࢖͏ͳΒɼਅ໘໨ʹ༡΅͏ ͓खຊͷ໛฿ʢंྠͷ࠶ൃ໌ɼଞݴޠ͔Βͷ༌ೖʣ͔Β͸͡ΊΔ ·ͬͨ͘ಉ͜͡ͱ͸୭΋΍͍ͬͯͳͯ͘ɼগ͠എ৳ͼΛͨ͠Β࣮ݱͰ͖ͦ͏ͳ͜ͱ͸ָ͍͠ ࡞Δલ͔Βษڧ͗͢͠ͳ͍ʢखΛಈ͔͢ϋʔυϧΛ্͛ͳ͍ʣ ͔͕ͨझຯ͚ͩͲɼϓϩτλΠϐϯάҎ্Λҙࣝͨ͠ʮ΋ͷͮ͘Γʯ͸ָ͍͠ NOTE ͜͜ʹॻ͍ͯ͋Δ͜ͱ͸ࢲͷϙΤϜͳͷͰɼΈΜͳ޷͖ʹͨ͠Β͍͍ͱࢥ͍·͢ :)

  18. ؓ࿩ɿંΕͳ͍ݸਓOSS։ൃͷଓ͚͔ͨ 1ਓͰ΋͘΋͘͸ऐ͍͠ɻϢʔβʔʹϦʔν͠Α͏ ‷( ŋŷŋ)و ŞƄŕ υΩϡϝϯςʔγϣϯɼSNSͰͷΞφ΢ϯεɼొஃʢ৺ͷϋʔυϧ͕௿͍ॱʣ ϓϩμΫτ໊ͰͷΤΰαʔν ຊ࣭తͳཁ๬ɼෆ۩߹ใࠂ͸໓ଟʹ issue ʹ͸্͕ͬͯ͜ͳ͍

    ൓Ԡ͸͠ͳ͍͍ͯ͘ɻ৺ͷόοΫϩάʹೖΕͯɼ͕࣌ؒ͋Δ࣌ʹϐοΫΞοϓ ͕Μ͹Βͳ͍ɻϞνϕʔγϣϯ͸๨Εͨࠒʹ߱ͬͯ͘Δ
  19. • 0.4.0 ͰมΘΔ͜ͱ • Deep dive... v0.4.0 ϦϦʔεͷ͓஌Βͤ https://medium.com/@mocobeta/janome-%E9%96%8B%E7%99%BA%E6%97%A5%E8%AA%8C-v0-4-0- %E3%82%92%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9%E3%81%97%E3%81%BE%E3%81%97

    %E3%81%9F- %E3%83%A1%E3%83%A2%E3%83%AA%E4%BD%BF%E7%94%A8%E9%87%8F%E3%81%AE%E5%89%8 A%E6%B8%9B%E3%82%84-python2-7- %E3%82%B5%E3%83%9D%E3%83%BC%E3%83%88%E5%81%9C%E6%AD%A2%E3%81%AA%E3%81%A 9%E3%81%AA%E3%81%A9-d91ec3642d7
  20. Breaking Changes : Python 2.7 αϙʔτఀࢭ 2020೥ͳͷͰɼPython 2.7 ͷαϙʔτΛམͱ͠·ͨ͠ 0.4.0

    ͷαϙʔτର৅͸ɼPython 3.6 Ҏ্ Python 3.5 ͸ 2020/9/23 Ͱ End of support ͳͷͰɼ·ͩগ͠ૣ͍͕αϙʔτର৅֎
  21. Breaking Changes : Python 2.7 αϙʔτఀࢭ ϞμϯͳγϯλοΫεɼϥΠϒϥϦ͕࢖͑ΔΑ͏ʹͳͬͨ ϦϑΝΫλࡇΓ Public API

    ʹ Type Hint Λಋೖ Public API ʹΩʔϫʔυઐ༻Ҿ਺Λಋೖ Abstract Base Class, Enum Ͱந৅Խ
 ➡ ੑೳӨڹ͕େ͖͘ɼޙʹ revert ... orz
  22. ϦϑΝΫλϦϯάͷ͓ڙʹ ʮEffective Python ୈ 2 ൛ʯͰɼPython 3 ͷ஌ࣝΛΞοϓσʔτ Pythonic ΁ͷͩ͜ΘΓ͕ɼ90

    ͷখ߲໨ʹίϯύΫτʹڽॖ͞Ε͍ͯΔ ͳ͓ɼࢲ͸ຊॻͷؔ܎ऀͰ͸͋Γ·ͤΜ :)
  23. Breaking Changes : tokenize() ϝιουͷมߋ 0.4.0 Ҏ߱ɼtokenize() ϝιου͸ ετϦʔϛϯάϞʔυ ͷΈαϙʔτ͠·͢

    ۩ମతʹมΘΔ͜ͱ v0.3 Ͱͷ stream=True ࣌ͷڍಈͷΈΛαϙʔτ͠ɼstream Φϓγϣϯ͸ഇࢭ ໭Γ஋ͷܕ͸ generator ͷΈͰɼlist ͸ฦ͞ͳ͍ มߋཧ༝ɿ௕͍ೖྗΛ༩͑ͨ࣌ͷϝϞϦϦʔΫΛ๷͙ͨΊ
  24. Behavior Changes : mmap ͕σϑΥϧτʹ ʮେ͖ͳࣙॻ΁ͷରԠʯͰ৮Εͨɼmmap mode ͕ Tokenize ΦϒδΣΫτͷσϑΥϧ

    τʹͳΓ·ͨ͠ 64bit ΞʔΩςΫνϟͰಈ࡞ͤͨ࣌͞ͷΈ 32bit ΞʔΩςΫνϟͰ͸͜Ε·Ͱ௨Γ mmap=False
  25. Behavior Changes : mmap ͕σϑΥϧτʹ վળ͞Εͨ͜ͱ Python ϓϩηεͷϝϞϦ࢖༻ྔ࡟ݮ : 30~40%

    ݮ Tokenizer ॳظԽͷߴ଎Խ : 0.48 sec => 0.06 sec (on Core i7-8700) ࠓ·ͰσϑΥϧτͰͳ͔ͬͨཧ༝ φΠʔϒʹ࢖͏ͱղੳ͕஗͘ͳΔ OS ͷϖʔδΩϟογϡػߏ͕͏·͘΍ͬͯ͘ΕΔͱ͸͍͑ɼେ͖ͳϑΝΠϧ΁ͷϥϯμϜΞ ΫηεΛ܁Γฦ͢ͱ෺ཧI/OʢϖʔδϑΥʔϧτʣ͕සൃ͢Δ
  26. Behavior Changes : mmap ͕σϑΥϧτʹ mmap Λ࢖͍ͳ͕Βɼղੳ଎౓ΛσάϨͤ͞ͳ͍ͨΊʹ Α͘࢖͏ܭࢉ్த݁Ռ͸ɼΩϟογϡʹอ࣋ʢϝϞԽʣ mmap ΦϒδΣΫτ΁ͷϥϯμϜΞΫηεස౓Λ཈͑Δ

    ϝϞԽʁ໘౗ͦ͏ʁ Python ʹ͸ɼϝϞԽͷͨΊͷ functools.lru_cache ͕ඪ४Ͱ͍͍ͭͯΔ
  27. Behavior Changes : mmap ͕σϑΥϧτʹ janome.dic.MMapDictionary # mmap ΦϒδΣΫτ͔ΒࣙॻΤϯτϦΛ lookup

    ͢Δϝιου @lru_cache(maxsize=8192) # decorator Λ͚ͭΔ͚ͩͰϝϞԽ͕׬ྃ def _find_entry(self, idx): bucket = next(filter(lambda b: idx >= b[0] and idx < b[1], self.bucket_ranges)) mm, mm_idx = self.entries_compact[bucket] rel_idx = idx - mm_idx['offset'] _pos1s = mm_idx['positions'][rel_idx] + 2 _pos1e = mm.find(b"',", _pos1s) _pos2s = _pos1e + 2 _pos2e = _pos2s + 4 _pos3s = _pos2e + 1 ɹɹɹɹ ... _entry = ( mm[_pos1s:_pos1e].decode('unicode_escape'), int(mm[_pos2s:_pos2e]), int(mm[_pos3s:_pos3e]), int(mm[_pos4s:_pos4e])) return _entry
  28. Behavior Changes : mmap ͕σϑΥϧτʹ ࠷దԽͷ໨੕͸͍͕ͭͨɾɾɾʮਪଌ͢ΔͳɼܭଌͤΑʯ ϓϩϑΝΠϦϯάεΫϦϓτΛ࡞ΓɼϘτϧωοΫΛಛఆͯ͠νϡʔχϯά cProfile : ؔ਺ݺͼग़͠ճ਺ɼ࣮ߦ࣌ؒͷτϨʔε

    tracemalloc : ϝϞϦׂΓ౰ͯͷτϨʔε ଟ͘ͷ৔߹ɼܭࢉ࣌ؒͱϝϞϦ࢖༻ྔ͸τϨʔυΦϑɻύϥϝʔλมߋͱܭଌΛ܁Γฦ ͯ͠εΠʔτεϙοτ ⚖ Λݟ͚ͭΔཱྀ΁
  29. Behavior Changes : mmap ͕σϑΥϧτʹ cProfile ͷ࢖͍ํ͸؆୯ janome/profiler/run_cprofile.py t =

    Tokenizer(mmap=mmap) with open('text_lemon.txt') as f: s = f.read() profiler = Profile() profiler.runcall( lambda: [list(t.tokenize(s)) for i in range(repeat)]) stats = Stats(profiler) stats.strip_dirs() stats.sort_stats('tottime') stats.print_stats() CPUコストの高いメソッドが一目瞭然
  30. Behavior Changes : mmap ͕σϑΥϧτʹ tracemalloc ͷ࢖͍ํ͸؆୯ janome/profiler/run_tracemalloc.py # Start

    tracing tracemalloc.start(10) # blocks allocated by initializing Tokenizer t = Tokenizer(mmap=mmap) snapshot1 = tracemalloc.take_snapshot() top_stats1 = snapshot1.statistics('lineno') with open(dump_file, 'w') as f: f.write('**Initializing Tokenizer**\n') f.write('[Top 10 lines]\n') for stat in top_stats1[:10]: f.write(str(stat)) f.write('\n') f.write('\n') メモリを沢山使っているオブジェクトが 一目瞭然
  31. ൒೔΄Ͳνϡʔχϯά৬ਓΛͨ݁͠Ռ mmap ͰϓϩηεͷϝϞϦ࢖༻ྔΛԼ͛ͭͭɼղੳ଎౓͸ mmap ແޮ࣌ͱಉ౳ʹ ղੳର৅ͷςΩετʹΑ্ͬͯৼΕɾԼৼΕ͸͢Δ͔΋͠Εͳ͍ σϑΥϧτ mmap ༗ޮͰ OK

    ͱ൑அ mmap=False Ͱ v0.3 ͱಉ༷ͷڍಈ Behavior Changes : mmap ͕σϑΥϧτʹ
  32. v0.4.0 ͸ʢ಺෦తʹʣେܕΞοϓσʔτ όάɼ͓͔͠ͳڍಈΛݟ͚ͭͨΒ Issue ౳Ͱڭ͍͑ͯͩ͘͞

  33. ͜Ε͔Β΍͍͖͍ͬͯͨ͜ͱ mmap mode ΛσϑΥϧτʹ͢Δ (✅ Done!) ϦϏϧυͤͣʹγεςϜࣙॻΛ੾Γସ͑Δ͘͠Έ Unidic ରԠ ֤छ࠷దԽʢ୭ಘͳͷ͕ͩ....ݸਓతڵຯʣ

    ࣙॻαΠζ࡟ݮ ϝϞϦ࢖༻ྔ࡟ݮ ղੳ଎౓ͷ޲্
  34. ͋Γ͕ͱ͏͍͟͝·ͨ͠ Questions? [PR] ࢲͷॴଐ͢Δ LegalForce R&DνʔϜͰ͸ Python ΤϯδχΞΛืू͍ͯ͠·͢ ػցֶशʢࣗવݴޠॲཧʣɼݕࡧվળ https://www.wantedly.com/projects/490879

    Zoom ϒʔεΛग़͍ͯ͠ΔͷͰ༡ͼʹདྷ͍ͯͩ͘͞