├── LICENSE ├── Mastering-Programming.md ├── README.md ├── Sacrifice.md └── iOS ├── 1.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── Memory_management.md ├── NSArray.count-and-arithmetic-underflow.md ├── Swift_protocol_extension.md ├── TableViewCell_reuseIdentifier_refactor.md ├── memory-overuse-debug.md ├── process.png ├── swift-crawler.md └── xpath ├── 1.png ├── 10.png ├── 11.png ├── 12.png ├── 13.png ├── 14.png ├── 2.png ├── 3.png ├── 4.png ├── 5.png ├── 6.png ├── 7.png ├── 8.png ├── 9.png └── xpath.html /LICENSE: -------------------------------------------------------------------------------- 1 | CC0 1.0 Universal 2 | 3 | Statement of Purpose 4 | 5 | The laws of most jurisdictions throughout the world automatically confer 6 | exclusive Copyright and Related Rights (defined below) upon the creator and 7 | subsequent owner(s) (each and all, an "owner") of an original work of 8 | authorship and/or a database (each, a "Work"). 9 | 10 | Certain owners wish to permanently relinquish those rights to a Work for the 11 | purpose of contributing to a commons of creative, cultural and scientific 12 | works ("Commons") that the public can reliably and without fear of later 13 | claims of infringement build upon, modify, incorporate in other works, reuse 14 | and redistribute as freely as possible in any form whatsoever and for any 15 | purposes, including without limitation commercial purposes. These owners may 16 | contribute to the Commons to promote the ideal of a free culture and the 17 | further production of creative, cultural and scientific works, or to gain 18 | reputation or greater distribution for their Work in part through the use and 19 | efforts of others. 20 | 21 | For these and/or other purposes and motivations, and without any expectation 22 | of additional consideration or compensation, the person associating CC0 with a 23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright 24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work 25 | and publicly distribute the Work under its terms, with knowledge of his or her 26 | Copyright and Related Rights in the Work and the meaning and intended legal 27 | effect of CC0 on those rights. 28 | 29 | 1. Copyright and Related Rights. A Work made available under CC0 may be 30 | protected by copyright and related or neighboring rights ("Copyright and 31 | Related Rights"). Copyright and Related Rights include, but are not limited 32 | to, the following: 33 | 34 | i. the right to reproduce, adapt, distribute, perform, display, communicate, 35 | and translate a Work; 36 | 37 | ii. moral rights retained by the original author(s) and/or performer(s); 38 | 39 | iii. publicity and privacy rights pertaining to a person's image or likeness 40 | depicted in a Work; 41 | 42 | iv. rights protecting against unfair competition in regards to a Work, 43 | subject to the limitations in paragraph 4(a), below; 44 | 45 | v. rights protecting the extraction, dissemination, use and reuse of data in 46 | a Work; 47 | 48 | vi. database rights (such as those arising under Directive 96/9/EC of the 49 | European Parliament and of the Council of 11 March 1996 on the legal 50 | protection of databases, and under any national implementation thereof, 51 | including any amended or successor version of such directive); and 52 | 53 | vii. other similar, equivalent or corresponding rights throughout the world 54 | based on applicable law or treaty, and any national implementations thereof. 55 | 56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of, 57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and 58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright 59 | and Related Rights and associated claims and causes of action, whether now 60 | known or unknown (including existing as well as future claims and causes of 61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum 62 | duration provided by applicable law or treaty (including future time 63 | extensions), (iii) in any current or future medium and for any number of 64 | copies, and (iv) for any purpose whatsoever, including without limitation 65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes 66 | the Waiver for the benefit of each member of the public at large and to the 67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver 68 | shall not be subject to revocation, rescission, cancellation, termination, or 69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work 70 | by the public as contemplated by Affirmer's express Statement of Purpose. 71 | 72 | 3. Public License Fallback. Should any part of the Waiver for any reason be 73 | judged legally invalid or ineffective under applicable law, then the Waiver 74 | shall be preserved to the maximum extent permitted taking into account 75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver 76 | is so judged Affirmer hereby grants to each affected person a royalty-free, 77 | non transferable, non sublicensable, non exclusive, irrevocable and 78 | unconditional license to exercise Affirmer's Copyright and Related Rights in 79 | the Work (i) in all territories worldwide, (ii) for the maximum duration 80 | provided by applicable law or treaty (including future time extensions), (iii) 81 | in any current or future medium and for any number of copies, and (iv) for any 82 | purpose whatsoever, including without limitation commercial, advertising or 83 | promotional purposes (the "License"). The License shall be deemed effective as 84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the 85 | License for any reason be judged legally invalid or ineffective under 86 | applicable law, such partial invalidity or ineffectiveness shall not 87 | invalidate the remainder of the License, and in such case Affirmer hereby 88 | affirms that he or she will not (i) exercise any of his or her remaining 89 | Copyright and Related Rights in the Work or (ii) assert any associated claims 90 | and causes of action with respect to the Work, in either case contrary to 91 | Affirmer's express Statement of Purpose. 92 | 93 | 4. Limitations and Disclaimers. 94 | 95 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 96 | surrendered, licensed or otherwise affected by this document. 97 | 98 | b. Affirmer offers the Work as-is and makes no representations or warranties 99 | of any kind concerning the Work, express, implied, statutory or otherwise, 100 | including without limitation warranties of title, merchantability, fitness 101 | for a particular purpose, non infringement, or the absence of latent or 102 | other defects, accuracy, or the present or absence of errors, whether or not 103 | discoverable, all to the greatest extent permissible under applicable law. 104 | 105 | c. Affirmer disclaims responsibility for clearing rights of other persons 106 | that may apply to the Work or any use thereof, including without limitation 107 | any person's Copyright and Related Rights in the Work. Further, Affirmer 108 | disclaims responsibility for obtaining any necessary consents, permissions 109 | or other rights required for any use of the Work. 110 | 111 | d. Affirmer understands and acknowledges that Creative Commons is not a 112 | party to this document and has no duty or obligation with respect to this 113 | CC0 or use of the Work. 114 | 115 | For more information, please see 116 | 117 | -------------------------------------------------------------------------------- /Mastering-Programming.md: -------------------------------------------------------------------------------- 1 | #Mastering Programming 2 | #精通程式設計 3 | > [Original article link](https://www.facebook.com/notes/kent-beck/mastering-programming/1184427814923414) 4 | > Translated with the permission from the author, [KENT BECK](https://www.facebook.com/notes/kent-beck/mastering-programming/1184427814923414?comment_id=1185657178133811&comment_tracking=%7B%22tn%22%3A%22R7%22%7D) 5 | 6 | 7 | 8 | From years of watching master programmers, I have observed certain common patterns in their workflows. From years of coaching skilled journeyman programmers, I have observed the absence of those patterns. I have seen what a difference introducing the patterns can make. 9 | 10 | 這幾年來觀察了很多大師級的程式設計師, 發現他們的工作流程中有一些共通的模式。幾年來在輔導很多有水準的資深碼工(程式設計師)時, 卻沒有觀察到這些模式。在這個過程中,我見到這些模式的導入能帶來多大的差別。 11 | 12 | Here are ways effective programmers get the most out of their precious 3e9 seconds on the planet. 13 | 14 | 接下來談談有效率的程式設計師應該如何好好運用他們生活在地球上30億秒的時間。(譯註:約等於95年) 15 | 16 | The theme here is scaling your brain. The journeyman learns to solve bigger problems by solving more problems at once. The master learns to solve even bigger problems than that by solving fewer problems at once. Part of the wisdom is subdividing so that integrating the separate solutions will be a smaller problem than just solving them together. 17 | 18 | 這裡的主旨是規模化你的大腦。工匠藉由一次性解決越多問題來解決大問題;大師則每次盡量解決少量的問題,來解決更龐大的問題。 這其中的智慧是,如何將大問題細分成數個小問題,使得整合之後將會比一次解決全部的問題還要來得輕鬆。 19 | 20 | ## Time 時間 21 | 22 | ### Slicing 細分 23 | 24 | Take a big project, cut it into thin slices, and rearrange the slices to suit your context. I can always slice projects finer and I can always find new permutations of the slices that meet different needs. 25 | 26 | 將一個大專案細分成數個小切片,並且重新排列以符合你的現況。我隨時可以將這些專案再切得更細小,而且還能重新組合來面對不同的需求。 27 | 28 | ### One thing at a time. 一次只做一件事 29 | 30 | We’re so focused on efficiency that we reduce the number of feedback cycles in an attempt to reduce overhead. This leads to difficult debugging situations whose expected cost is greater than the cycle overhead we avoided. 31 | 32 | 由於我們相當注重效率,因此我們總是試圖減少回饋週期的次數以避免重新檢視的無謂開銷。但這往往反而導致了偵錯難度的提升,其付出的成本常大於原本想避免回饋週期所產生的開銷。 33 | 34 | ### Make it run, make it right, make it fast. 讓它動起來,正確,快速 35 | 36 | (Example of One Thing at a Time, Slicing, and Easy Changes) 37 | (「細分」、「一次只做一件事」、「簡化更動」的例子) 38 | 39 | 40 | ### Easy changes. 簡化更動 41 | 42 | When faced with a hard change, first make it easy (warning, this may be hard), then make the easy change. (e.g. slicing, one thing at a time, concentration, isolation). Example of slicing. 43 | 44 | 當我們面對困難的更動,首先要進行簡化 (警告, 這可能很難),然後再實施簡化後的更動。 (例如:「切割」、「一次只做一件事」、「集中」、「離析」等原則) 這是「切割」原則的一個例子。 45 | 46 | ### Concentration. 集中 47 | 48 | If you need to change several elements, first rearrange the code so the change only needs to happen in one element. 49 | 50 | 如果你需要一次修改很多元件,先重新整理程式碼(譯註:或重構),使得只需要修改一個元件。 51 | 52 | ### Isolation. 離析 53 | 54 | If you only need to change a part of an element, extract that part so the whole subelement changes. 55 | 56 | 如果你只需要改變一個元件的一部份, 把這部分抽離出來使得只要修改這個子元件。 57 | 58 | ### Baseline Measurement. 基準測量 59 | 60 | Start projects by measuring the current state of the world. This goes against our engineering instincts to start fixing things, but when you measure the baseline you will actually know whether you are fixing things. 61 | 62 | 在專案開始時先考量真實環境的狀態。這違背工程師馬上動手修東西的本能,但是有了基準線之後,你才知道你真的有把東西修好。 63 | 64 | ## Learning 65 | ### Call your shot. 當家作主 66 | 67 | Before you run code, predict out loud exactly what will happen. 68 | 69 | 在執行程式前, 先大膽的預測將會發生什麼事。 70 | 71 | ### Concrete hypotheses. 具體假設 72 | 73 | When the program is misbehaving, articulate exactly what you think is wrong before making a change. If you have two or more hypotheses, find a differential diagnosis. 74 | 75 | 當程式執行不合預期時,在做修改前, 先明確指出你覺得什麼事做錯了。如果你有兩種以上的假設,分析這些假設有哪些差異。 76 | 77 | 78 | ### Remove extraneous detail. 移除外部細節 79 | 80 | When reporting a bug, find the shortest repro steps. When isolating a bug, find the shortest test case. When using a new API, start from the most basic example. “All that stuff can’t possibly matter,” is an expensive assumption when it’s wrong. 81 | E.g. see a bug on mobile, reproduce it with curl 82 | 83 | 當回報出錯誤時,找出最簡短的重現步驟。 當把錯誤離析出來後,找到最短的測試案例。當使用新的API時,從最基本的範例開始。「這些部分應該無關緊要吧」,錯誤假設的代價只有出事的時候才能察覺。 84 | 舉例:在手機上發現錯誤, 用 curl 重現他. 85 | 86 | ### Multiple scales. 多重規模 87 | 88 | Move between scales freely. Maybe this is a design problem, not a testing problem. Maybe it is a people problem, not a technology problem [cheating, this is always true]. 89 | 90 | 時時在不同規模中遊走。「這也許是設計問題, 而不是測試問題」。「也許這是人的問題,而不是技術問題」。 [這有點作弊,因為永遠都是人的問題] 91 | 92 | ## Transcend Logic 超越邏輯 93 | 94 | ### Symmetry. 對稱性 95 | 96 | Things that are almost the same can be divided into parts that are identical and parts that are clearly different. 97 | 98 | 幾乎等同的事物,可以被切割成重複到的部分跟完全不一樣的部分。 99 | 100 | 101 | ### Aesthetics. 美感 102 | 103 | Beauty is a powerful gradient to climb. It is also a liberating gradient to flout (e.g. inlining a bunch of functions into one giant mess). 104 | 105 | 「美感」是一個難以攀爬、有影響力的梯度,同時也是一個常被嘲笑忽視的、奔放的梯度。(例如: 行內定義一堆函數然後變得一團亂) 106 | 107 | 108 | 109 | ### Rhythm. 節奏 110 | 111 | Waiting until the right moment preserves energy and avoids clutter. Act with intensity when the time comes to act. 112 | 113 | 等待時機,儲備精力,避免混亂。時機成熟時,將爆發力化為行動。 114 | 115 | 116 | ### Tradeoffs. 折衷 117 | 118 | All decisions are subject to tradeoffs. It’s more important to know what the decision depends on than it is to know which answer to pick today (or which answer you picked yesterday). 119 | 120 | 所有決策都有可能要面對折衷。更重要的是去了解這些決策取決於什麼,而不是今天(或昨天)選擇了哪個選項。 121 | 122 | 123 | ## Risk 風險 124 | 125 | ### Fun list. 趣事清單 126 | 127 | When tangential ideas come, note them and get back to work quickly. Revisit this list when you’ve reached a stopping spot. 128 | 129 | 當離題的靈感一來,記下來然後快速地回到工作上。當你空閒下來時,再回來查看這些清單。 130 | 131 | ### Feed Ideas. 餵養靈感 132 | 133 | Ideas are like frightened little birds. If you scare them away they will stop coming around. When you have an idea, feed it a little. Invalidate it as quickly as you can, but from data not from a lack of self-esteem. 134 | 135 | 靈感就像受驚嚇的小鳥,如果你嚇跑牠們,牠們就不會再來了。當你有靈感時,給他一點時間。即時拋棄不可行的點子,但是用數據說話,而不是因為單純沒自信而這麼做。 136 | 137 | ### 80/15/5. 138 | 139 | Spend 80% of your time on low-risk/reasonable-payoff work. Spend 15% of your time on related high-risk/high-payoff work. Spend 5% of your time on things that tickle you, regardless of payoff. Teach the next generation to do your 80% job. By the time someone is ready to take over, one of your 15% experiments (or, less frequently, one of your 5% experiments) will have paid off and will become your new 80%. Repeat. 140 | 141 | 把 80% 的時間拿來從事低風險/合理報酬的工作。把 15% 的時間拿來從事高風險/高報酬的有關實驗。剩下 5% 的時間拿來從事會讓你開心且不在意是否有報酬的實驗。訓練下一代來做你 80% 的工作,等到有人準備好代替你,你那15%的實驗(或者,你那5%的實驗,雖然很少見)就能收成而成為你新的 80%。 不斷重複這個過程。 142 | 143 | ## Conclusion 結論 144 | 145 | The flow in this outline seems to be from reducing risks by managing time and increasing learning to mindfully taking risks by using your whole brain and quickly triaging ideas. 146 | 147 | 這份綱要中的流程說明了:從透過管理時間、加強學習來降低風險,到用你的大腦來謹慎地承擔風險,還有快速分類你的靈感。 148 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Articles 2 | 3 | | 連結 | 介紹 | 4 | |---|---| 5 | | [Swift protocol extension](https://github.com/willard1218/Articles/blob/master/iOS/Swift_protocol_extension.md) | 介紹如何用 Swift 實作一個 API Request client (What),會大量使用 Protocol extension (How) 特性來擴充這個模組,近而讓程式變得容易維護且擴充性高 (Why)。 | 6 | | [使用 Swift 寫爬蟲](https://github.com/willard1218/Articles/blob/master/iOS/swift-crawler.md) | 使用 XPath + Swift 套件 爬 104 頁面 7 | | [memory overuse debug](https://github.com/willard1218/Articles/blob/master/iOS/memory-overuse-debug.md) | 如何找出記憶體飆升的過程 | 8 | | [TableViewCell reuseIdentifier refactor](https://github.com/willard1218/Articles/blob/master/iOS/TableViewCell_reuseIdentifier_refactor.md) | 一步一步重構TableViewCell_reuseIdentifier 的過程 | 9 | | [NSArray.count-and-arithmetic-underflow ](https://github.com/willard1218/Articles/blob/master/iOS/NSArray.count-and-arithmetic-underflow.md) | 在型別 underflow 上踩過的雷 | 10 | | [Mastering-Programming(譯)](Mastering-Programming.md) | 原作 Kent Beck | 11 | | [Sacrifice(翻譯)](Sacrifice.md) | 發人深省的影片 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /Sacrifice.md: -------------------------------------------------------------------------------- 1 | http://www.mitchellroth.com/sacrifice-motivational-montage-with-les-brown-eric-thomas-ray-lewis/ 2 | 3 | There will never be a point in your ti — in your life — where it’s the right time to do a great thing. If you’re waiting for that perfect perfect moment, that perfect timing, it’s not going to happen. You know what you have to do? You have to create the perfect time, and the perfect opportunity, and the perfect situation. 4 | 5 | > 人生中沒有「作大事」的完美時機。 6 | 7 | > 如果你在等待那完美時機自己出現,那是永遠不會發生的。 8 | 9 | > 你知道該怎麼辦嗎? 10 | 11 | > 你必須自己去創造一個完美的時機,完美的機遇,和完美的局勢 12 | 13 | So a lot of people become comfortable. They stop growing, they stop wanting anything, they become satisfied. 14 | > 大多數的人逐漸變得安逸。他們不再成長,不再渴望任何東西,他們滿足於現況。 15 | 16 | People getting ready to go to jobs that they don’t like, jobs that are making them sick. You see when you are not pursuing your goal, you are literally committing spiritual suicide. When you have some goal out here that you are stretching for and reaching for that takes you out of your comfort zone, you’ll find out some talents and abilities you have that you didn’t know you have. 17 | 18 | > 許多人作著他們不滿意的工作 -- 讓他們覺得反胃的工作。 19 | 20 | > 聽著:當你不努力追求你的人生目標,那你就是在心靈與精神上自殘。 21 | 22 | > 當你有想努力伸出手去拼命掙取、強制你走出舒適圈的人生目標時,你會發現你從來不知道你有的天賦與能力。 23 | 24 | When the messenger of misery visits you, what are you going to do? What will keep you in the game. 25 | > 當你遇到挫敗時,你會怎麼作?什麼樣的信念可以支持你屢敗屢戰? 26 | 27 | There are things that you think you’ll never need to know. That you may only need to know one time in your life, but that could save your life because you had that knowledge. 28 | > 有些事你覺得你沒必要去學習。 29 | 30 | > 但你一生中可能就正好有那麼一次需要那些知識,而那些知識將救你一命。 31 | 32 | Unless you attempt to do something beyond that, which you’ve already mastered, you will never grow. What is it that you looked at, at some point in time and you decided that you couldn’t do it, that you talk yourself out of it. 33 | > 除非你嘗試去作你已駕輕就熟以外的事,你將永遠不會成長。 34 | 35 | > 想想有哪些事,是你曾經想過去嘗試,但覺得自己辦不到,最後說服你自己放棄。 36 | 37 | You’re waiting on your next door neighbor to make it happen for you, it may not happen. If you’re waiting on your mother, or your father, they may be so ancient in their thinking, that they don’t understand this opportunity that you have. And if you’re waiting on them it may never get done. 38 | > 你想等別人帶著你作大事,那大概不會成真。 39 | 40 | > 你想等你的父母帶著你作大事?他們的思想可能是如此古老,他們根本不了解你在這時代所擁有的機會。 41 | 42 | > 如果你打算依賴父母領導你作大事,那大概永遠辦不成。 43 | 44 | You don’t beg average people to be phenomenal. You don’t beg good people to be phenomenal. You just are phenomenal, and you will attract phenomenal. 45 | > 你無法要求普通人搖身一變變成作大事的人。 46 | 47 | > 你無法要求有德人搖身一變變成作大事的人。 48 | 49 | > 作大事的人就是有作大事的人的自覺,而作大事的人將會吸引作大事的人。 50 | 51 | What reason can you remember, that you can call on, that you can reach on, that can make you get back up. Find that reason. 52 | > 是什麼樣的信念與理由,可以讓你依恃,可以讓你想伸手拼命掙取,可以讓你重新站起來? 53 | 54 | > 去找到那樣的信念與理由! 55 | 56 | If you’re not where you are. If you’re not where you want to be. If you don’t have what you want, want to have. If you’re not where you think you should be at this particular place. It has nothing to do with the system, but it has everything to do with the fact that you’re not making the sacrifice. 57 | > 如果你不知道你在人生道路上的位置。 58 | 59 | > 如果你不知道你在人生道路上該往哪走。 60 | 61 | > 如果你還沒有取得你想要的東西。 62 | 63 | > 如果你還沒有爬上你想要的位置。 64 | 65 | > 別怨天尤人,這一切都只是因為你還沒有付出相對的代價與犧性。 66 | 67 | I want you to make that dream become a reality, because if you don’t, you will be working for somebody else to make their dreams become a reality. 68 | > 我要你去實現你的夢想。 69 | 70 | > 因為如果你不去作,那你最後就會去為別人工作,去實現別人的夢想。 71 | 72 | And everybody is against you, or don’t believe in you no more. And let me tell you something, that’s a lonely feeling. That’s a lonely feeling. Particularly people that you are doing it for. 73 | > 當每個人都反對你,每個人都不看好你時,我知道,那是種非常孤獨的感覺,真的非常孤獨的感覺。 74 | 75 | > 尤其是當不看好你的人就正是你想努力為他們打拼的人。 76 | 77 | Most people take their greatness, take their ideas to the graveyard with them. 78 | > 大多數人帶著他們的天賦與理想步入墳墓。 79 | 80 | Listen to me, if it was easy, everybody would do it. There are people right now who are working who don’t want to work. There are people who hate their jobs, but they keep getting up to do it. 81 | > 聽好 82 | 83 | > 如果這很簡單,那其他人早就在作了。 84 | 85 | > 此時此刻,有人心不甘情不願地工作、作著他們厭惡的工作,但他們最後還是乖乖照作。 86 | 87 | The wealthiest place on the planet, is the graveyard. Because in the graveyard we will find inventions that we never ever were exposed to. Ideas, dreams, that never became reality. Hopes and aspirations that were never acted upon. 88 | > 這星球上最富有的地方,就是墳場。 89 | 90 | > 因為在墳場裡,埋著我們從來沒看過的、沒有成真的發明、理想、夢想;沒有被實踐的希望與靈感。 91 | 92 | The question is what are you going to do with your time. What drives you. Greatness is a lot of small things done well. Day, after day. Workout after workout. Obedience after obedience. Day after day. 93 | > 最重要的問題就是: 「你如何運用你的時間?驅使你前進的原動力是什麼?」 94 | 95 | > 「偉大的成就」就是把許多微不足道的小事作好所累積起來的。 96 | 97 | > 一天又一天。 98 | 99 | > 一次又一次。 100 | 101 | > 忠實地遵循這個原則。 102 | 103 | > 一天又一天,把無數微不足道的小事,作好。 104 | 105 | When things don’t work out for you. When things happen that you could not anticipate. What are the reasons that you can think of that can keep you strong. 106 | > 當事情進展不如預期, 107 | 108 | > 當意外狀況發生時, 109 | 110 | > 你必須想想: 有什麼樣的理由、人事物,能給你堅強的意志,支持你繼續走下去? 111 | 112 | You will never ever be successful, until you turn your pain into greatness, until you allow your pain to push you from where you are to push you to where you need to be. Stop running from your pain and embrace your pain. Your pain is going to be a part of your prize, a part of your product. I challenge you to push yourself. 113 | > 除非你能把痛苦轉化成你的偉大志向,除非你讓「困境」驅使你從「現況」邁向理想,不然,你永遠也無法成功。 114 | 115 | > 正面面對困境,不要再逃避。 116 | 117 | > 「克服困境中痛苦」將變成你的成就,將會是你給自己最好的獎品。 118 | 119 | > 我在此挑戰你: 超越你的極限! 120 | 121 | See it’s easy to be on the bottom, it doesn’t take any effort to be a loser. It doesn’t take any motivation and any drive in order to stay down there on a low level. But it calls on everything in you. You have to harness your will to say I’m going to challenge myself. 122 | > 你看,要留在底層是很簡單的;不費吹灰之力,不需要任何動力,不需要任何決心,就能變成魯蛇。 123 | 124 | > 但是! 125 | 126 | > 若要挑戰自己、超越自己極限,你必須使出洪荒之力,凝聚你所有的意志力,向你自己下戰書: 我要超越我自己! 127 | 128 | I mean that what you did last week don’t count. Today today is the only important day. There are eighty-six thousand, four hundred seconds in a day and how you use those are critical. You got eighty-six thousand, four hundred today and what you do today is going to cement who you are. Nobody gonna talk about what you did last week. 129 | > 告訴你: 你上週作過的努力,已經是過去了。 130 | 131 | > 「今天」,才是最重要的。 132 | 133 | > 一天有八萬六千四百秒,你如何運用這八萬六千四百秒才是最關鍵的。 134 | 135 | > 你「今天」作的事才會定義你是誰;沒有人會去關心你上週有多努力。 136 | 137 | Yet the biggest enemy that you have to deal with is yourself. There’s an old African proverb that says “If there’s no enemy within, the enemy outside can do us no harm.” 138 | > 你最大的敵人,是你自己。 139 | 140 | > 有句非洲俗話說: 「如果沒有內賊,那外敵將無法傷我分毫」 141 | 142 | You have this opportunity of a lifetime. It means absolutely nothing if you don’t take advantage of it in the lifetime of this opportunity. 143 | > 這是你人生中的大好機會。 144 | 145 | > 但如果你在這大好機會消逝前不好好把握的話,那就什麼也不是。 146 | 147 | I got a saying that when life knocks you down, try to land on your back, because if you can look up, you can get up. If you want a thing bad enough to go out and fight for it, to work day and night for it, to give up your time and your peace and your sleep for it. If all that you dream and scheme is about it. And life seems useless and worthless without it. 148 | > 如果你有渴望、追求的事物、願意為它奮戰的事物、為它日夜不停努力的事物、為它不惜放棄睡眠與安逸的生活的事物、你所有夢想與計畫寄託於上的的事物、少了它生命就索然無味的事物, 149 | 150 | > 那麼: 當命運把你打倒在地上時,用你的背部去著地;因為,如果你仍能往上看,那你就能爬起來。 151 | 152 | See it’s time now. If you want to make this your decade, you’ve to start saying yes to your life. You’ve got to start saying yes to your dreams. Yes to your unfolding future. Yes to your potential. As opposed to saying no. 153 | > 就是現在! 154 | 155 | > 如果你要造就你的時代,你必須正面面對你的人生,你必須正面面對你的夢想,你必須正面面對未來,你必須正面面對你的潛力與可能性。 156 | 157 | > 而不是虛與委蛇、欺騙自己。 158 | 159 | When you die, die on E. Leave no dream left behind guys. Leave no opportunity left behind. When you leave this earth, accomplish every single thing you can accomplish. 160 | > 當你停下腳步時,確認你已經使盡渾身解數。 161 | 162 | > 不要留下任何未完成的夢想。 163 | 164 | > 不要留下任何沒抓住的機會。 165 | 166 | > 當你離開人世時,完成所有你可以完成的事。 167 | 168 | > 註: 此處 E 應該是指車子油箱「空(Empty)」的狀態, 相對於「滿(Full)」 169 | 170 | 171 | Listen to me, you’re going to be here one day, but you’ll never get here if you give up, if you give in, if you quit. And finally guys, you gotta wanna succeed, as bad, as you wanna, breath. 172 | > 聽好!你有一天會成功,但前提是你不能放棄、不能投降、不能離場。 173 | 174 | > 你對於成功的渴望,必須要像你對於呼吸空氣的渴望一樣,沒有它,你就活不下去。 175 | -------------------------------------------------------------------------------- /iOS/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/1.png -------------------------------------------------------------------------------- /iOS/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/2.png -------------------------------------------------------------------------------- /iOS/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/3.png -------------------------------------------------------------------------------- /iOS/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/4.png -------------------------------------------------------------------------------- /iOS/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/5.png -------------------------------------------------------------------------------- /iOS/Memory_management.md: -------------------------------------------------------------------------------- 1 | 2 | ##Memory management 3 | 4 | 先宣告一個 People 的物件 5 | 6 | ```objective-c 7 | @interface People : NSObject 8 | @property NSUInteger age; 9 | @end 10 | 11 | @implementation People 12 | - (void)dealloc { 13 | NSLog(@"%s",__PRETTY_FUNCTION__); 14 | } 15 | @end 16 | ``` 17 | 18 | 在另一個物件宣告成 Property: 19 | 20 | ```objective-c 21 | @property (weak) People *weakPeople; 22 | @property (strong) People *strongPeople; 23 | @property (assign) People *assignPeople; 24 | ``` 25 | 26 | 並在某個函式使用: 27 | 28 | ```objective-c 29 | if (true) { 30 | People *people1 = [[People alloc] init]; 31 | _weakPeople = people1; 32 | 33 | People *people2 = [[People alloc] init]; 34 | _strongPeople = people2; 35 | 36 | People *people3 = [[People alloc] init]; 37 | _assignPeople = people3; 38 | } 39 | ``` 40 | 41 | 在結束 if 這個 block 時: 42 | 43 | | | _weakPeople | _strongPeople | _assignPeople | 44 | | --- | --- | --- | --- | 45 | | 是否有被 retain | ✕ | ✔ | ✕ | 46 | | 值 | nil | 有效的值 | 垃圾值 | 47 | | 備註 | | | 拿 _assignPeople.age 會 crash | 48 | 49 | -------------------------------------------------------------------------------- /iOS/NSArray.count-and-arithmetic-underflow.md: -------------------------------------------------------------------------------- 1 | NSArray.count and arithmetic underflow 2 | == 3 | 4 | 5 | ```objective-c 6 | NSMutableArray *arr = [NSMutableArray array]; 7 | 8 | int i = 1; 9 | if (i < arr.count - 1) { 10 | [arr removeObjectAtIndex:i]; 11 | } 12 | ``` 13 | 14 | 你覺得這段 code 有沒有問題? 15 | 16 | 如果我告訴你,這 code 會噴 *NSRangeException*,你信不信?為什麼? 17 | 18 | 19 | * . 20 | * . 21 | * . 22 | * . 23 | * 思考時間 24 | * . 25 | * . 26 | * . 27 | * . 28 | * . 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | *** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayM objectAtIndex:]: index 0 beyond bounds for empty array' 38 | 39 | 根據蘋果文件的 [NSArray count](https://developer.apple.com/reference/foundation/nsarray/1409982-count?language=objc) 的解說,發現 count 這個方法是回傳 NSUInteger 型態的數值,於是再做了個實驗: 40 | 41 | ```objective-c 42 | NSMutableArray *arr = [NSMutableArray array]; 43 | 44 | NSInteger i = arr.count - 1; 45 | // i = -1 46 | NSUInteger j = arr.count - 1; 47 | // j = 18446744073709551615 48 | ``` 49 | 50 | 結論 51 | === 52 | 因為 NSUInteger 的範圍是 0 ~ 18446744073709551615, 53 | 故如果是最小值 - 1 就會變成最大值;如果是最大值 + 1 就會變成最小值。 54 | 55 | -------------------------------------------------------------------------------- /iOS/Swift_protocol_extension.md: -------------------------------------------------------------------------------- 1 | 這篇文章是介紹如何用 Swift 實作一個 `API Request client` *(What)*, 2 | 會大量使用 `Protocol extension` *(How)* 特性來擴充這個模組, 3 | 近而讓`程式變得容易維護且擴充性高` *(Why)*。 4 | 5 | 所以此篇文章重點不是在寫一個 API Request module,而是藉由這個例子了解 `Protocol extension` 的功能強大之處。 6 | 7 | # 情境 8 | 常常我們需要跟後端 API 做溝通,會建立 URLRequest,基本上都需要做各種設定: 9 | 10 | 1. URL 11 | 2. Http Method 12 | 3. Request Header 13 | 4. Request Body 14 | 5. Response Body 15 | 16 | 這篇文章主要針對 3, 4, 5 項來進行擴充, 17 | 因為此三類裡面都會有很多不同的轉換邏輯(灰色方框), 18 | 轉換邏輯是指,例如 API a 的 Request Body 裡面可能要將 JSON 轉成 Data; 19 | 而 API b 的 Request Body 裡面可能要將 JSON array 轉成 data,當 API 越來越多,轉換邏輯可能會重複寫很多次, 20 | 這文章會介紹如何將這轉換邏輯統一寫在 protocol extension,達到重複使用的效果。 21 | 22 | 因此,以下每個設定簡單列出會有哪些分類: 23 | 24 | 1. Request Header 25 | 1. FormURLEncodedRequestHeader 26 | 2. JSONRequestHeader 27 | 2. Request Body 28 | 1. IgnorableRequestBody 29 | 2. JSONRequestBody 30 | 3. JSONArrayRequestBody 31 | 3. Response Body 32 | 1. StringResponseBody 33 | 2. IgnorableResponseBody 34 | 3. JSONResponseBody 35 | 4. JSONModelResponseBody 36 | 37 | 會介紹如何擴充這些灰色方框,達到 [Design principle S.O.I.L.D](http://teddy-chen-tw.blogspot.tw/2014/04/solid.html) 裡的 [OCP](http://teddy-chen-tw.blogspot.tw/2011/12/2.html) 原則。 38 | 39 | ![](https://i.imgur.com/spCmYSe.png) 40 | 41 | 一個 API Request Client 模組,如果一開始為了應付幾個簡單的 API,在還沒使用 protocol 前,可能會這樣設計: 42 | 43 | ```swift 44 | struct Request { 45 | var baseURL: String 46 | var path: String 47 | var method: String 48 | var headers : [String: String] 49 | var body : Data? 50 | 51 | func buildRequest() -> URLRequest { 52 | let urlString = "\(baseURL)/\(path)" 53 | let url = URL(string: urlString) 54 | var request = URLRequest(url: url!) 55 | 56 | for (key, value) in headers { 57 | request.setValue(value, forHTTPHeaderField: key) 58 | } 59 | 60 | request.httpMethod = method 61 | request.httpBody = body 62 | 63 | return request 64 | } 65 | 66 | func sendRequest(success: @escaping(String) -> (), failure: @escaping(String) -> ()) { 67 | let session = URLSession.shared 68 | 69 | session.dataTask(with: buildRequest()) { (data, response, error) in 70 | if error != nil { 71 | failure(error!.localizedDescription) 72 | return 73 | } 74 | 75 | let responseString = String(data: data!, encoding: .utf8)! 76 | success(responseString) 77 | }.resume() 78 | } 79 | } 80 | ``` 81 | 82 | 83 | 84 | ```swift 85 | let fetchAllUsersRequest = Request(baseURL: "http://api.xxx.com", 86 | path: "users", 87 | method: "get", 88 | headers: ["Content-Type":"application/json"], 89 | body: nil) 90 | 91 | fetchAllUsersRequest.sendRequest(success: { (users) in 92 | print(users) 93 | }) { (failure) in 94 | print(failure) 95 | } 96 | ``` 97 | 98 | 99 | 會發現有個問題,每個 API 都要把 request header 再寫一次, 100 | 但也不能直接寫在 `buildRequest()` 裡面,因為有時候要指定別的 header, 101 | 例如: 102 | 103 | 1. `Content-Type=application/json` 104 | 2. `Content-Type=application/x-www-form-urlencoded; charset=UTF-8` 105 | 106 | 107 | 這時我們就可以用 Protocol extension 來讓 header 的設定封裝在 extension,而不用每個 API request 都指定一次。 108 | # 建立 RequesHeader Protocol 109 | 110 | ```swift 111 | protocol RequestHeader { 112 | var headers: [String : String] { get } 113 | } 114 | 115 | protocol FormURLEncodedRequestHeader : RequestHeader {} 116 | 117 | extension FormURLEncodedRequestHeader { 118 | var headers: [String : String] { 119 | return ["Content-Type" : "application/x-www-form-urlencoded; charset=UTF-8"] 120 | } 121 | } 122 | 123 | protocol JSONRequestHeader : RequestHeader {} 124 | 125 | extension JSONRequestHeader { 126 | var headers: [String : String] { 127 | return ["Content-Type" : "application/json"] 128 | } 129 | } 130 | ``` 131 | 132 | 宣告兩個 header,讓不同 API 可以指定。 133 | 134 | 135 | ```swift 136 | protocol Request : RequesHeader { 137 | var baseURL: String {get} 138 | var path: String {get} 139 | var method: String {get} 140 | var body : Data? {get} 141 | } 142 | 143 | 144 | extension Request { 145 | var baseURL: String { 146 | return "http://api.xxx.com" 147 | } 148 | var body: Data? { 149 | return nil 150 | } 151 | } 152 | ``` 153 | 154 | 先將 `struct Request` 改成 `protocol Request`, 155 | 因為 `headers` 改由 `RequestHeader` 裡面拿,所以要 confirms to RequesHeader。 156 | 再 `extension Request` 裡面寫預設的 `baseURL`,就不用每個 API 都寫一次。 157 | 158 | 159 | 160 | ```swift 161 | extension Request { 162 | func buildRequest() -> URLRequest { 163 | let urlString = "\(baseURL)/\(path)" 164 | let url = URL(string: urlString) 165 | var request = URLRequest(url: url!) 166 | 167 | for (key, value) in headers { 168 | request.setValue(value, forHTTPHeaderField: key) 169 | } 170 | 171 | request.httpMethod = method 172 | request.httpBody = body 173 | 174 | return request 175 | } 176 | 177 | func sendRequest(success: @escaping(String) -> (), failure: @escaping(String) -> ()) { 178 | let session = URLSession.shared 179 | 180 | session.dataTask(with: buildRequest()) { (data, response, error) in 181 | if error != nil { 182 | failure(error!.localizedDescription) 183 | return 184 | } 185 | 186 | let responseString = String(data: data!, encoding: .utf8)! 187 | success(responseString) 188 | }.resume() 189 | } 190 | } 191 | 192 | ``` 193 | 194 | 原本放到 `struct Request` 裡面的方法,移至到 `extension Request`。 195 | 196 | ```swift 197 | struct FetchAllUsersRequest : Request, JSONRequestHeader { 198 | let path = "users" 199 | let method = "get" 200 | } 201 | ``` 202 | 203 | 原本是用 `struct Request` 類別產生 API Request 物件,現在直接定義 struct RequestAPI。 204 | 這樣一來,如果這個 API 會呼叫一次以上,有些固定的設定(`path`, `method`)就不需要重打一次。 205 | 206 | 207 | 208 | ```swift 209 | FetchAllUsersRequest().sendRequest(success: { (users) in 210 | print(users) 211 | }) { (failure) in 212 | print(failure) 213 | } 214 | ``` 215 | 使用的時候,不再需要打 `在 app 期間都不會修改的設定`,因為已經封裝到 protocol 裡面。 216 | 217 | # 建立 RequestBody Protocol 218 | 219 | ```swift 220 | protocol RequestBody { 221 | func buildRequestBody() -> Data? 222 | } 223 | 224 | protocol IgnorableRequestBody : RequestBody {} 225 | 226 | extension IgnorableRequestBody { 227 | func buildRequestBody() -> Data? { 228 | return nil 229 | } 230 | } 231 | 232 | 233 | protocol JSONRequestBody : RequestBody { 234 | var bodyDict : [String : Any] {get set} 235 | } 236 | 237 | extension JSONRequestBody { 238 | func buildRequestBody() -> Data? { 239 | do { 240 | let data = try JSONSerialization.data(withJSONObject: bodyDict, options: []) 241 | return data 242 | } catch let err { 243 | print(err) 244 | } 245 | 246 | return nil 247 | } 248 | } 249 | 250 | protocol JSONArrayRequestBody : RequestBody { 251 | var bodyDicts : [[String : Any]] {get set} 252 | } 253 | 254 | 255 | 256 | extension JSONArrayRequestBody { 257 | func buildRequestBody() -> Data? { 258 | do { 259 | let data = try JSONSerialization.data(withJSONObject: bodyDicts, options: []) 260 | return data 261 | } catch let err { 262 | print(err) 263 | } 264 | 265 | return nil 266 | } 267 | } 268 | ``` 269 | 270 | 這邊建立常用的三個 `RequetBody`: 271 | 1. `IgnorableRequestBody` 272 | 2. `JSONRequestBody` 273 | 3. `JSONArrayRequestBody` 274 | 275 | 以上這些 protocol 都 confirms to `RequetBody` 的 `buildRequestBody` 方法, 276 | 必須將特定格式的資料轉成統一的 `Data` 型別。 277 | 278 | ```swift 279 | protocol Request : RequestHeader, RequestBody { 280 | var baseURL: String {get} 281 | var path: String {get} 282 | var method: String {get} 283 | } 284 | 285 | extension Request { 286 | func buildRequest() -> URLRequest { 287 | // ... 288 | request.httpBody = buildRequestBody() 289 | 290 | return request 291 | } 292 | } 293 | ``` 294 | 295 | 因為 `body` 改由 `RequestBody` 裡面拿,所以要 confirms to RequestBody。 296 | 297 | 298 | ```swift 299 | struct UpdateUserNameRequest : Request, JSONRequestHeader, JSONRequestBody { 300 | let path = "user" 301 | let method = "post" 302 | var bodyDict: [String : Any] 303 | } 304 | 305 | UpdateUserNameRequest(bodyDict: ["name":"David", "id": 4]).sendRequest(success: { (result) in 306 | print(result) 307 | }) { (failure) in 308 | print(failure) 309 | } 310 | ``` 311 | 312 | # 建立 ResponseBody Protocol 313 | 314 | 原本的 `sendRequest` 在 success closure 裡面只支援 `String` 315 | 316 | ```swift 317 | func sendRequest(success: @escaping(String) -> (), failure: @escaping(String) -> ()) 318 | ``` 319 | 320 | 但實際上 ResponseBody 的格式會有很多種, 321 | 例如 JSON, String, 322 | 甚至不在乎會傳什麼,只要知道成功就好了, 323 | 所以會希望 success closure 都支援這些型態,就像這樣: 324 | 325 | ```swift 326 | fileprivate func sendURLRequest(request: URLRequest, success: @escaping(Data) -> (), failure: @escaping(String) -> ()) { 327 | let session = URLSession.shared 328 | 329 | session.dataTask(with: request) { (data, response, error) in 330 | if error != nil { 331 | failure(error!.localizedDescription) 332 | return 333 | } 334 | 335 | if let data = data { 336 | success(data) 337 | return 338 | } 339 | 340 | failure("data is nil") 341 | 342 | }.resume() 343 | } 344 | ``` 345 | 346 | 先寫一個共用的 `sendURLRequest`,因為處理錯誤的邏輯可以共用。 347 | 348 | ```swift 349 | protocol ResponseBody { 350 | var urlRequest : URLRequest {get} 351 | } 352 | 353 | extension ResponseBody where Self : Request { 354 | var urlRequest : URLRequest { 355 | return buildRequest() 356 | } 357 | } 358 | ``` 359 | 360 | 新增 `protocol ResponseBody` 361 | 362 | ```swift 363 | protocol StringResponseBody : ResponseBody {} 364 | extension StringResponseBody { 365 | func sendRequest(success: @escaping(String) -> (), 366 | failure: @escaping(String) -> ()) { 367 | 368 | sendURLRequest(request: urlRequest, success: { (result) in 369 | let responseString = String(data: result, encoding: .utf8)! 370 | success(responseString) 371 | }, failure: failure) 372 | 373 | } 374 | } 375 | 376 | protocol IgnorableResponseBody : ResponseBody {} 377 | extension IgnorableResponseBody { 378 | func sendRequest(success: @escaping() -> (), 379 | failure: @escaping(String) -> ()) { 380 | sendURLRequest(request: urlRequest, success: { (result) in 381 | success() 382 | }, failure: failure) 383 | 384 | } 385 | } 386 | 387 | protocol JSONResponseBody : ResponseBody {} 388 | extension JSONResponseBody { 389 | func sendRequest(success: @escaping([String: Any]) -> (), 390 | failure: @escaping(String) -> ()) { 391 | sendURLRequest(request: urlRequest, success: { (data) in 392 | do { 393 | print("ffff") 394 | if let jsonDictionary = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any] { 395 | success(jsonDictionary) 396 | } 397 | } 398 | catch _ { 399 | failure("jsonParseError") 400 | } 401 | }, failure: failure) 402 | 403 | } 404 | } 405 | 406 | protocol JSONModelResponseBody : ResponseBody { 407 | associatedtype ResponseModel : Decodable 408 | } 409 | 410 | extension JSONModelResponseBody { 411 | func sendRequest(success: @escaping(ResponseModel) -> (), 412 | failure: @escaping(String) -> ()) { 413 | sendURLRequest(request: urlRequest, success: { (data) in 414 | let decoder = JSONDecoder() 415 | do { 416 | let model = try decoder.decode(ResponseModel.self, from: data) 417 | success(model) 418 | } 419 | catch { 420 | print("decoder error") 421 | } 422 | }, failure: failure) 423 | 424 | } 425 | } 426 | 427 | protocol Request : RequestHeader, RequestBody, ResponseBody { 428 | var baseURL: String {get} 429 | var path: String {get} 430 | var method: String {get} 431 | } 432 | ``` 433 | 434 | ResponseBody 支援以下四種格式: 435 | 1. `StringResponseBody` 436 | 2. `IgnorableResponseBody` 437 | 3. `JSONResponseBody` 438 | 4. `JSONModelResponseBody` 439 | 440 | 其中 `JSONModelResponseBody` 比較特別, 441 | 先在 `protocol` 裡面宣告返回的 dataModel 需要是 `Decodable`, 442 | 這樣在 `sendRequest` 時就會自動將 data 轉成物件。 443 | 444 | Request 也要多 confirms to `ResponseBody` 445 | 446 | 實際使用就會變成: 447 | 448 | ```swift 449 | struct User : Decodable { 450 | var name: String 451 | } 452 | 453 | struct FindUserRequest : Request, JSONRequestHeader, IgnorableRequestBody, JSONModelResponseBody { 454 | typealias ResponseModel = User 455 | 456 | let path = "users/1" 457 | let method = "get" 458 | } 459 | 460 | 461 | FindUserRequest().sendRequest(success: { (user) in 462 | print(user.name) 463 | }) { (failure) in 464 | print(failure) 465 | } 466 | ``` 467 | 468 | 469 | 到目前為止我們已經支援的格式擴充到這樣: 470 | 471 | 472 | 1. Request Header 473 | 1. FormURLEncodedRequestHeader 474 | 2. JSONRequestHeader 475 | 2. Request Body 476 | 1. IgnorableRequestBody 477 | 2. JSONRequestBody 478 | 3. JSONArrayRequestBody 479 | 3. Response Body 480 | 1. StringResponseBody 481 | 2. IgnorableResponseBody 482 | 3. JSONResponseBody 483 | 4. JSONModelResponseBody 484 | 485 | 當然還有很多可以擴充的, 486 | 例如 Response Body 可能會多個 `JSONArrayResponseBody`、`JSONModelArrayResponseBody` 等等, 487 | 等未來如果擴充這些格式時,既有的程式不必做修改,只需定義新的 protocal 即可。 488 | 489 | 文中的程式還有許多地方可以修改的,例如: 490 | 491 | 1. API 錯誤、Http Method 應該要用 enum 定義比較洽當,但本文章的目的不是這些,所以不再另外寫。 492 | 2. 還可以擴充 URL 的產生方式,例如有些參數會打在網址上,但目前擴充以上三類,覺得已經足夠說明 `Protocol Extension` 這個概念,所以就不再贅述。 493 | 494 | [程式](https://gist.github.com/willard1218/958eb31fa49b2c15ca1fdbae75c5b40d) -------------------------------------------------------------------------------- /iOS/TableViewCell_reuseIdentifier_refactor.md: -------------------------------------------------------------------------------- 1 | # 從 TableViewCell reuseIdentifier 探討重構這件事 2 | 3 | 在 iOS 裡,有以下兩種 UI 元件: 4 | 5 | 1. `TableViewCell` 6 | 2. `TableView` 7 | 8 | 9 | 此兩種元件相輔相成,`TableView` 是一種用來顯示表格的佈局方式; 10 | 而 `TableViewCell` 則是表格裡面每行 (row) 要顯示什麼內容。 11 | 12 | 我們經常會要客製化 `TableViewCell`, 13 | 必要時,更會使用新增類別,來繼承 `TableViewCell` 的方式, 14 | 來達到 *重複使用* 的目的。 15 | 16 | 如果今天有一個 **TodoList** 的 App, 17 | 用途是使用者可以在畫面上新增、修改、刪除、查詢待辦事項, 18 | 畫面一開始一定是列出所有待辦事項,就會出現一個 `TableView`, 19 | 表格的每一行,我會新增一個類別:`TodoItemCell` 來繼承 `TableViewCell`。 20 | 21 | 在 iOS 裡,因為 `TableView` 是可以捲動的, 22 | 如果今天項目太多,為了避免造成一次建立太多 `TableViewCell`, 23 | 會有一個 `ReuseIdentifier` 來跟你說現在可以重複使用哪一個 `TableViewCell`, 24 | 你只要重新改變這個 `TableViewCell` 的樣子, 25 | 而不用重新建立,節省記憶體空間。 26 | 27 | 28 | 程式一般如下寫: 29 | 30 | ## 一般寫法 31 | 32 | ```objective-c 33 | - (void)viewDidLoad { 34 | // do something ... 35 | [self.tableView registerClass:TodoItemCell.class forCellReuseIdentifier:@"cellId"]; 36 | } 37 | 38 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 39 | TodoItemCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cellId" forIndexPath:indexPath]; 40 | // do something ... 41 | return cell; 42 | } 43 | ``` 44 | 45 | 46 | 問題來了:*因為兩邊使用到 `ReuseIdentifier` 都是寫死,* 47 | *很可能改一個地方忘記改另外一個地方。* 48 | 49 | ## 重構 - 將共同的抽離出來 50 | 51 | ```objective-c 52 | const NSString *kTodoItemCellIdentifier = @"TodoItemCellIdentifier"; 53 | - (void)viewDidLoad { 54 | // do something ... 55 | [self.tableView registerClass:TodoItemCell.class forCellReuseIdentifier:kTodoItemCellIdentifier]; 56 | } 57 | 58 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 59 | TodoItemCell *cell = [tableView dequeueReusableCellWithIdentifier:kTodoItemCellIdentifier forIndexPath:indexPath]; 60 | // do something ... 61 | return cell; 62 | } 63 | ``` 64 | 65 | 改成這樣後,不會再發生改下面忘記改上面的 `ReuseIdentifier` 的情形發生, 66 | 但問題來了:*如果我今天這個 `TodoItemCell` 要給其他類別使用呢?* 67 | 68 | 有些人可能會想到,將 `kTodoItemCellIdentifier` 放到一個 Constant.h, 69 | 好讓其他類別能夠使用, 70 | 但問題來了:*每個 XXXCell,你都要在宣告一次常數,* 71 | *日後 Constant.h 會隨著專案越來越大而越來越多行,變得更難維護。* 72 | 73 | 我的解法是,宣告在 `UITableViewCell` 的 **catrgory**, 74 | 增加一個 **class method** : `- (NSString *)Identifier`, 75 | 好讓各式各樣不同的 Cell 繼承他時,就自動具備了這樣的功能, 76 | 實作如下: 77 | 78 | 79 | ## 重構 - 寫在 UITableViewCell 的 Catrgory 80 | 81 | ```objective-c 82 | @interface UITableViewCell (Helpers) 83 | + (NSString *)identifier; 84 | @end 85 | 86 | 87 | @implementation UITableViewCell (Helpers) 88 | + (NSString *)identifier { 89 | NSString *reuseIdentifier = [NSString stringWithFormat:@"%@Identifier", NSStringFromClass(self.class)]; 90 | return reuseIdentifier; 91 | } 92 | @end 93 | ``` 94 | 95 | 如此一來,因為 `TodoItemCell` 繼承 `UITableViewCell`, 96 | 所以只要使用 `[TodoItemCell identifier]` 就可以拿到我想要的東西, 97 | 98 | 因為會使用目前的 class 名字,產生動態的 `ReuseIdentifier`, 99 | 所以具備唯一性, 100 | 即使以後新增 `XXXCell`、`YYYCell`, 101 | 也不需要重新定義常數 `kXXXCellIdentifier`、`kYYYCellIdentifier`, 102 | 藉以達到 **Write once, run anywhere**。 103 | 104 | ### 後記 105 | 此篇文章分享在 [Swift Taiwan](https://www.facebook.com/groups/swifttw/permalink/1487577911254580/) 後,有網友分享更方便的 [寫法](http://qiita.com/tattn/items/bdce2a589912b489cceb#uitableview), 106 | 已將內文中的 Swift 版本改成 Objectice-c: 107 | 108 | 109 | ## 重構 - 寫在 UITableView 的 Category 110 | ```objective-c 111 | // UITableView+Helper.h 112 | 113 | #import 114 | 115 | @interface UITableView (Helper) 116 | 117 | - (void)registerClass:(_Nonnull Class)cellClass; 118 | 119 | - (void)registerClassList:(NSArray *_Nonnull)cellClassList; 120 | 121 | - (__kindof UITableViewCell *_Nonnull)dequeueReusableCellWithClass:(_Nonnull Class)cellClass forIndexPath:(NSIndexPath *_Nonnull)indexPath; 122 | 123 | @end 124 | ``` 125 | 126 | 127 | ```objective-c 128 | // UITableView+Helper.m 129 | #import "UITableView+Helper.h" 130 | 131 | @implementation UITableView (Helper) 132 | 133 | - (void)registerClass:(_Nonnull Class)cellClass { 134 | NSString *reuseIdentifier = NSStringFromClass(cellClass); 135 | [self registerClass:cellClass forCellReuseIdentifier:reuseIdentifier]; 136 | } 137 | 138 | 139 | - (void)registerClassList:(NSArray *)cellClassList { 140 | for (Class class in cellClassList) { 141 | [self registerClass:class]; 142 | } 143 | } 144 | 145 | - (__kindof UITableViewCell *_Nonnull)dequeueReusableCellWithClass:(_Nonnull Class)cellClass forIndexPath:(NSIndexPath *_Nonnull)indexPath { 146 | return [self dequeueReusableCellWithIdentifier:NSStringFromClass(cellClass) forIndexPath:indexPath]; 147 | } 148 | 149 | @end 150 | ``` 151 | 152 | 相較於原本的寫法,現在使用更方便了: 153 | 154 | ```objective-c 155 | - (void)viewDidLoad { 156 | // do something ... 157 | 158 | [self.tableView registerClass:TodoItemCell.class forCellReuseIdentifier:[TodoItemCell Identifier]]; 159 | // 重構 - 寫在 UITableViewCell 的 Catrgory 160 | 161 | [self.tableView registerClass:TodoItemCell.class]; 162 | // 重構 - 寫在 UITableView 的 Category 163 | } 164 | 165 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 166 | TodoItemCell *cell = [tableView dequeueReusableCellWithIdentifier:[TodoItemCell Identifier] forIndexPath:indexPath]; 167 | // 重構 - 寫在 UITableViewCell 的 Catrgory 168 | 169 | TodoItemCell *cell = [tableView dequeueReusableCellWithClass:TodoItemCell.class forIndexPath:indexPath]; 170 | // 重構 - 寫在 UITableView 的 Category 171 | 172 | // do something ... 173 | return cell; 174 | } 175 | ``` 176 | 177 | 可以看出有以下好處: 178 | 1. 註冊 cell 的時候,從兩個參數縮減為一個。 179 | 2. 隱藏了 `ReuseIdentifier`,讓介面統一使用 `TodoItemCell.class` 當參數。 -------------------------------------------------------------------------------- /iOS/memory-overuse-debug.md: -------------------------------------------------------------------------------- 1 | 這邊文章主要是在整理在開發過程中,遇到一些記憶體管理的問題,文中會從發現問題,怎麼找問題,以及如何解決來探討。 2 | 3 | ## 需求 4 | 將 `APNG` 格式的圖片轉到 `GIF` 格式的圖片, 5 | 6 | ## 流程 7 | 使用 [YYImage](https://github.com/ibireme/YYImage) 可以達到這件事, 8 | 9 | 10 | ![](process.png) 11 | 1. 用 `YYImageDecoder` 來 decode 圖片,會得到 UIImage array,和 NSTimeInterval array。 12 | 2. 用 `YYImageEncoder` 來 encode 圖片。 13 | 14 | ## 問題 - 記憶體飆升 15 | 在跑很多張圖片的時候,發現 memory 會持續升高: 16 | ![](1.png) 17 | 18 | 跑完的時候,記憶體才降下來: 19 | ![](2.png) 20 | 21 | 其中使用到的程式如下: 22 | 23 | 24 | ```objective-c 25 | - (void)viewDidLoad { 26 | [super viewDidLoad]; 27 | NSString *path = [[NSBundle mainBundle] pathForResource:@"1" ofType:@"png"]; 28 | NSData *apngData = [[NSData alloc] initWithContentsOfFile:path]; 29 | 30 | [self convertToGifDataFromApngData1:apngData]; 31 | } 32 | 33 | - (void)convertToGifDataFromApngData1:(NSData *)data { 34 | for (NSUInteger i = 0; i < 1000; i++) { 35 | NSMutableArray *frameImages = [NSMutableArray array]; 36 | NSMutableArray *frameTimeIntervals = [NSMutableArray array]; 37 | 38 | [self setFrameImages:frameImages 39 | frameTimeIntervals:frameTimeIntervals 40 | fromData:data]; 41 | NSLog(@"%ld",i); 42 | NSData *gifData = [self gifDataWithFrameImages:frameImages frameTimeIntervals:frameTimeIntervals]; 43 | } 44 | } 45 | 46 | 47 | - (void)setFrameImages:(NSMutableArray *)frameImages 48 | frameTimeIntervals:(NSMutableArray *)frameTimeIntervals 49 | fromData:(NSData *)data { 50 | YYImageDecoder *decoder = [YYImageDecoder decoderWithData:data scale:1.0]; 51 | 52 | for (NSUInteger i = 0 ; i < decoder.frameCount ; i++ ) { 53 | YYImageFrame *imageFrame = [decoder frameAtIndex:i decodeForDisplay:YES]; 54 | UIImage *image = imageFrame.image; 55 | NSTimeInterval duration = imageFrame.duration; 56 | 57 | [frameImages addObject:image]; 58 | [frameTimeIntervals addObject:@(duration)]; 59 | } 60 | 61 | } 62 | 63 | - (NSData *)gifDataWithFrameImages:(NSMutableArray *)frameImages frameTimeIntervals:(NSMutableArray *)frameTimeIntervals { 64 | YYImageEncoder *gifEncoder = [[YYImageEncoder alloc] initWithType:YYImageTypeGIF]; 65 | gifEncoder.loopCount = 0; 66 | 67 | for (NSUInteger i = 0 ; i < frameImages.count ; i++ ) 68 | { 69 | [gifEncoder addImage:frameImages[i] duration:[frameTimeIntervals[i] doubleValue]]; 70 | } 71 | 72 | NSData *gifData = [gifEncoder encode]; 73 | return gifData; 74 | } 75 | ``` 76 | 77 | ## 找問題 78 | 其中發現 79 | `NSData *gifData = [self gifDataWithFrameImages:frameImages frameTimeIntervals:frameTimeIntervals];` 80 | 81 | 是讓記憶體飆高的關鍵,因為註解掉就沒事了, 82 | 於是 trace YYImageEncoder 裡面的程式,看看是如何 *將多張圖片組合成一張 GIF* 的,總共會經過 [YYImageCoder](https://github.com/ibireme/YYImage/blob/master/YYImage/YYImageCoder.m) 裡面的三個關鍵 function 83 | 84 | 85 | 1. `YYImageCoder::encode()` -> 86 | 2. `YYImageCoder::_encodeWithImageIO()` -> 87 | 3. `YYImageCoder::_encodeImageWithDestination()` 88 | 89 | 為了釐清問題,我把有用到的程式獨立成一個 function: 90 | 91 | 92 | ```objective-c 93 | - (NSMutableData *)_encodeWithImageIO:(NSArray *)_images 94 | timeIntervals:(NSArray *)_durations { 95 | NSMutableData *data = [NSMutableData new]; 96 | NSUInteger count = _images.count; 97 | CGImageDestinationRef destination = CGImageDestinationCreateWithData((CFMutableDataRef)data, kUTTypeGIF, count, NULL); 98 | BOOL suc = NO; 99 | if (destination) { 100 | NSDictionary *gifProperty = @{(__bridge id)kCGImagePropertyGIFDictionary: 101 | @{(__bridge id)kCGImagePropertyGIFLoopCount: @0}}; 102 | CGImageDestinationSetProperties(destination, (__bridge CFDictionaryRef)gifProperty); 103 | for (int i = 0; i < count; i++) { 104 | @autoreleasepool { 105 | UIImage *imageSrc = _images[i]; 106 | NSDictionary *frameProperty = NULL; 107 | frameProperty = @{(__bridge id)kCGImagePropertyGIFDictionary : @{(__bridge id) kCGImagePropertyGIFDelayTime:_durations[i]}}; 108 | 109 | if ((imageSrc).CGImage) 110 | CGImageDestinationAddImage(destination, imageSrc.CGImage, (__bridge CFDictionaryRef)frameProperty); 111 | // 將圖片加到 gif 112 | } 113 | } 114 | suc = CGImageDestinationFinalize(destination); 115 | CFRelease(destination); 116 | } 117 | if (suc && data.length > 0) { 118 | return data; 119 | } else { 120 | return nil; 121 | } 122 | } 123 | ``` 124 | 125 | 126 | 仔細檢查了一下,發現該 release 的都有被 release 掉,再測試一次: 127 | 128 | ```objective-c 129 | - (void)convertToGifDataFromApngData2:(NSData *)data { 130 | for (NSUInteger i = 0; i < 1000; i++) { 131 | NSMutableArray *frameImages = [NSMutableArray array]; 132 | NSMutableArray *frameTimeIntervals = [NSMutableArray array]; 133 | 134 | [self setFrameImages:frameImages 135 | frameTimeIntervals:frameTimeIntervals 136 | fromData:data]; 137 | NSLog(@"%ld",i); 138 | NSData *gifData = [self _encodeWithImageIO:frameImages timeIntervals:frameTimeIntervals]; 139 | } 140 | } 141 | ``` 142 | 143 | 發現記憶體還是一樣會飆高,看起來不是 `YYImage` 的問題, 144 | 使用 Xcode profile 觀察也沒有 memory leak 的問題,只看到記憶體一直飆高, 145 | 在想應該是這行會一直產生 NSData,等到 function 執行完畢才會釋放。 146 | `NSData *gifData = [self _encodeWithImageIO:frameImages timeIntervals:frameTimeIntervals];` 147 | 148 | 為了驗證這點,我先透過監聽 `NSData::dealloc` 方法 (使用 [method swizzing](http://nshipster.com/method-swizzling/) 來交換 `NSData::dealloc` 和 `NSData::xxx_dealloc` ) 來得知 NSData 真正被釋放的時機, 149 | 並下一些 log : 150 | 151 | `ViewController.m` 152 | 153 | ```objective-c 154 | - (void)viewDidLoad { 155 | [super viewDidLoad]; 156 | NSString *path = [[NSBundle mainBundle] pathForResource:@"1" ofType:@"png"]; 157 | NSData *apngData = [[NSData alloc] initWithContentsOfFile:path]; 158 | 159 | [self convertToGifDataFromApngData2:apngData]; 160 | 161 | NSLog(@"done"); 162 | sleep(5); 163 | NSLog(@"pop func"); 164 | } 165 | ``` 166 | 167 | `NSData Category` 168 | 169 | ```objective-c 170 | - (void)xxx_dealloc { 171 | NSLog(@"NSData delloc"); 172 | [self xxx_dealloc]; 173 | } 174 | ``` 175 | 176 | ![](3.png) 177 | 178 | 由圖中可以看到在執行 `convertToGifDataFromApngData2` 裡面的 for 迴圈時,NSData 一直沒有被釋放,直到整個 function 執行完畢時,才開始釋放。 179 | 180 | ## 解決方法 181 | 182 | 這邊分享兩個方法,都可以解決問題。 183 | 184 | ### 只 new 一次物件 185 | 186 | ```objective-c 187 | - (void)convertToGifDataFromApngData3:(NSData *)data { 188 | NSMutableData *tempData = [NSMutableData new]; 189 | for (NSUInteger i = 0; i < 1000; i++) { 190 | tempData.length = 0; 191 | NSMutableArray *frameImages = [NSMutableArray array]; 192 | NSMutableArray *frameTimeIntervals = [NSMutableArray array]; 193 | 194 | [self setFrameImages:frameImages 195 | frameTimeIntervals:frameTimeIntervals 196 | fromData:data]; 197 | NSLog(@"%ld",i); 198 | NSData *gifData = [self _encodeWithImageIO:frameImages timeIntervals:frameTimeIntervals fromData:tempData]; 199 | } 200 | } 201 | ``` 202 | 203 | 原本 `NSMutableData` 是在 `_encodeWithImageIO:timeIntervals` 裡面建立, 204 | 現在改由在外面建立,並傳入 `_encodeWithImageIO:timeIntervals:fromData`。 205 | 而在每次解析圖片時,需要執行 `tempData.length = 0` 清空裡面的資料,避免會有兩張圖片同時在一個 `NSMutableData` 物件裡面的錯誤。 206 | 207 | 記憶體使用量已經改善了 : 208 | ![](4.png) 209 | 210 | 211 | 212 | ### 在 for 裡面使用 @autoreleasepool 213 | 214 | 215 | ```objective-c 216 | - (void)convertToGifDataFromApngData4:(NSData *)data { 217 | for (NSUInteger i = 0; i < 1000; i++) { 218 | @autoreleasepool { 219 | NSMutableArray *frameImages = [NSMutableArray array]; 220 | NSMutableArray *frameTimeIntervals = [NSMutableArray array]; 221 | 222 | [self setFrameImages:frameImages 223 | frameTimeIntervals:frameTimeIntervals 224 | fromData:data]; 225 | NSLog(@"%ld",i); 226 | NSData *gifData = [self _encodeWithImageIO:frameImages timeIntervals:frameTimeIntervals]; 227 | } 228 | } 229 | } 230 | ``` 231 | 232 | 233 | 使用 `autoreleasepool` 強制在 for 裡面每次都釋放 裡面的物件。 234 | 235 | 記憶體使用量同樣也獲得改善了 : 236 | ![](5.png) 237 | 238 | 完整專案網址 239 | 240 | [https://github.com/willard1218/YYImage-Memory-leak-issue](https://github.com/willard1218/YYImage-Memory-leak-issue) -------------------------------------------------------------------------------- /iOS/process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/process.png -------------------------------------------------------------------------------- /iOS/swift-crawler.md: -------------------------------------------------------------------------------- 1 | # 使用 Swift 寫爬蟲 (以 104 網站為例) 2 | 3 | 大綱 : 4 | 5 | 1. 如何解析 html (使用 XPath) 6 | - 環境建置 7 | - XPath 查詢語法教學 8 | 2. Swift 爬蟲套件使用方法 9 | 3. 爬 104 公司頁面 10 | 11 | 12 | 13 | 14 | ## 如何解析 html (使用 XPath) 15 | 16 | 要寫爬蟲,要先理解**如何抓取網頁上特定的資訊**,本文會以一個 html 範例檔,搭配 XPath 語言,來一步一步介紹如何拿取畫面上的資訊。 17 | 18 | 首先要先了解 XPath 語言 是什麼,以下摘自[維基百科](https://zh.wikipedia.org/zh-tw/XPath): 19 | 20 | > XPath 即為 XML路徑語言(XML Path Language),它是一種用來確定XML文檔中某部分位置的語言。 XPath 基於 XML 的**樹狀結構**,**提供在資料結構樹中找尋節點的能力**。 21 | 22 | 簡單來說,可以將網頁的 html 架構比喻成一棵樹,使用 XPath 能幫助我們找到樹上的任何一個分支,也就是網頁的資訊。 23 | 24 | 為了要學習 XPath,要先介紹如何建置環境,以及 XPath 基本用法。 25 | 26 | ### 環境建置 27 | 要先將下方的 html 存成文字檔 `xpath.html`,並使用 chrome 開始。 28 | 29 | ```html 30 | 31 | 32 | 33 | 34 | 35 |
36 |

XPath

37 | 補充資料 : XML XPath的選擇節點語法 38 |
    39 |
  • text 1
  • 40 |
  • text 2
  • 41 |
  • text 3
  • 42 |
  • text 4
  • 43 |
44 |
45 |

text

46 | 47 | 48 | ``` 49 | 50 | 下載 `xpath-helper` 的 [chrome plugin](https://chrome.google.com/webstore/detail/xpath-helper/hgimnogjllphhhkhlmebbmlgjoejdpjl?hl=zh-TW) 51 | 52 | 下載完安裝後,右上角就可以直接使用: 53 | ![](https://i.imgur.com/5B34GUW.png) 54 | 55 | 為了方便可以看到 html 原始碼,請在Chrome菜單中選擇 更多工具 > [開發者工具](https://developers.google.com/web/tools/chrome-devtools/?hl=zh-tw) 56 | 57 | ### XPath 查詢語法教學 58 | #### 基本查詢 59 | 要把 html 結構想成一個階層式的架構,就像檔案路徑。 60 | 61 | 如果要取得 tag h1 時,就在左上角的 `QUERY` 輸入: 62 | `/html/body/h1` 63 | ![](https://i.imgur.com/ol0LV6I.png) 64 | 65 | 可以清楚地看到符合條件的結果會被 highlight 起來,因為 `xpath-helper` 會將找到的元素,新增 `class="xh-highlight"`,背景就變成黃色。 66 | 67 | #### 全域搜尋 68 | 如果不確定要搜尋的 tag 在哪一層,就可以使用 `//` 來表示要搜尋在 html 裡面,所有 tag 為 `h1` 的: 69 | 70 | `//h1` 71 | 72 | ![](https://i.imgur.com/08R2QRW.png) 73 | 74 | #### 取第 n 個 75 | 如果相同的 tag 有很多個,想拿第一個可以在 tag 名稱後面加 `[1]`: 76 | 77 | `/html/body/ul/li[1]` 78 | 79 | ![](https://i.imgur.com/d6CBpXx.png) 80 | 81 | 拿最後一個可以加 tag 名稱加上 `[last()]` 82 | 83 | `/html/body/ul/li[last()]` 84 | 85 | ![](https://i.imgur.com/jJGcDpn.png) 86 | #### 取得屬性 87 | 88 | 想拿屬性名稱只要打 `@屬性名稱` 就可以了 89 | 90 | `/html/body/a/@href` 91 | 92 | ![](https://i.imgur.com/N27szIJ.png) 93 | 94 | 95 | #### 條件式搜尋 96 | 97 | 找尋 tag 名稱是 `li` 並且 屬性 `class` 為 `a class`: 98 | 99 | `//li[@class='a class']` 100 | 101 | 102 | ![](https://i.imgur.com/Efwjchz.png) 103 | 找尋 tag 名稱是 `li` 並且 值 為 `text 1`: 104 | 105 | `//li[text()='text 1']` 106 | 107 | ![](https://i.imgur.com/ji8eyc5.png) 108 | 109 | 找尋 tag 名稱是 `li` 並且 屬性 `class` 包含 `test`: 110 | 111 | `//li[contains(@class,'test')]` 112 | 113 | ![](https://i.imgur.com/EPPHisQ.png) 114 | 115 | 找尋 tag 名稱是 `li` 並且 值 包含 `text 1`: 116 | 117 | `//li[contains(text(),'text 1')]` 118 | 119 | ![](https://i.imgur.com/0Xr5271.png) 120 | 121 | #### 分支條件 122 | 使用 `|` 合併多個搜尋結果: 123 | 124 | `//li[1]|//h1` 125 | 126 | ![](https://i.imgur.com/pYIATbG.png) 127 | 128 | ## Swift 爬蟲套件使用方法 129 | 130 | 會使用 [Kanna](https://github.com/tid-kijyun/Kanna) 套件來實作,先以剛剛的 html 當範例 131 | 132 | ### 取節點或屬性 133 | 134 | 135 | ```swift 136 | import Kanna 137 | 138 | let html = """ 139 | 140 | 141 | 142 | 143 | 144 |
145 |

XPath

146 | 補充資料 : XML XPath的選擇節點語法 147 |
    148 |
  • text 1
  • 149 |
  • text 2
  • 150 |
  • text 3
  • 151 |
  • text 4
  • 152 |
153 |
154 |

text

155 | 156 | 157 | """ 158 | 159 | if let doc = HTML(html: html, encoding: .utf8) { 160 | let nodeA = doc.xpath("/html/body/a").first! 161 | print(nodeA.innerHTML) 162 | // Optional("補充資料 : XML XPath的選擇節點語法") 163 | 164 | print(nodeA.text) 165 | // Optional("補充資料 : XML XPath的選擇節點語法") 166 | 167 | print(nodeA["href"]) 168 | // Optional("http://www.hosp.ncku.edu.tw/mis/48-netdisk/57-xml-xpath.html") 169 | 170 | let nodeAHref = doc.xpath("/html/body/a/@href").first! 171 | print(nodeAHref.innerHTML) 172 | // Optional(" href=\"http://www.hosp.ncku.edu.tw/mis/48-netdisk/57-xml-xpath.html\"") 173 | 174 | print(nodeAHref.text) 175 | // Optional("http://www.hosp.ncku.edu.tw/mis/48-netdisk/57-xml-xpath.html") 176 | 177 | print(nodeAHref["href"]) 178 | // nil 179 | } 180 | ``` 181 | 182 | 因為透過 `xpath()` 方法拿回來是 `XPathObject`,是一個集合,所以要取 `first!`, 183 | 藉由這兩個例子能發現 `nodeA` 是拿一整個 tag a,所以可以拿到文字,取出後如果要取屬性也可以用 `["attrname"]`,以這例子是拿 `href`; 184 | 而 `nodeAHref` 是只有拿屬性 `href`,就拿不到裡面的文字了。 185 | 186 | ### 取大量節點 187 | 188 | 189 | ```swift 190 | if let doc = HTML(html: html, encoding: .utf8) { 191 | let liNodes = doc.xpath("/html/body/ul/li") 192 | for liNode in liNodes { 193 | print(liNode["class"], liNode.text) 194 | } 195 | } 196 | // Optional("a class") Optional("text 1") 197 | // Optional("b class test") Optional("text 2") 198 | // Optional("c class test") Optional("text 3") 199 | // Optional("a class") Optional("text 4") 200 | ``` 201 | 202 | ## 爬 104 公司頁面 203 | 204 | 先到任意的公司頁面,本文以 [QNAP](https://www.104.com.tw/jobbank/custjob/index.php?r=cust&j=4c4a44263c36402230323c1d1d1d1d5f2443a363189j99) 為例,因為該公司個職缺較多。 205 | 206 | ### 使用 chrome 直接複製 xpath 207 | 先對想要查詢的資訊按右鍵,選`檢查` 208 | 209 | ![](https://i.imgur.com/lJFV1bt.png) 210 | 211 | 在對 node 點右鍵,選 `Copy XPath` 212 | ![](https://i.imgur.com/fkwU3a4.png) 213 | 214 | 接著就可以打開 `XPath Helper`,來驗證剛剛的 XPath 語法是否能拿到我們要的。 215 | 216 | ### 取得公司名稱 217 | 剛剛拿到的 XPath 是 218 | 219 | `//*[@id="comp_header"]/ul/li[2]/h1` 220 | 221 | 我通常會把雙引號變成單引號,因為這樣丟到 swift 當 String 時,不用再 escape: 222 | 223 | `//*[@id='comp_header']/ul/li[2]/h1` 224 | 225 | 然後會把`第幾個`改成`條件式搜尋`,避免以後元素的位置換了就抓錯的問題: 226 | 227 | `//*[@id='comp_header']/ul/li[@class='comp_name']/h1` 228 | 229 | 這時就可以直接貼到程式裡面使用了: 230 | 231 | ```swift 232 | let html = "要先透過 URLSession 拿到此網頁的 html,就不再贅述" 233 | if let doc = HTML(html: html, encoding: .utf8) { 234 | let comp_name = doc.xpath("///*[@id='comp_header']/ul/li[@class='comp_name']/h1").first!.text 235 | print(comp_name) 236 | // Optional("QNAP_威聯通科技股份有限公司 ") 237 | } 238 | ``` 239 | 240 | ### 取得工作機會列表 241 | 242 | 如果只想拿`職務名稱`,就可以一樣看點右鍵,選 檢查然後點 Copy XPath, 243 | 但如果想拿所有資料,包含`日期`、`職務名稱` 等等,就要觀察 html 的結構: 244 | 245 | ![](https://i.imgur.com/MsIQuAq.png) 246 | 247 | 看得出來有分兩種 `div class`: 248 | 1. `joblist_cont focus` 是重點職務,亮紅燈的。 249 | 2. `joblist_cont` 250 | 251 | 一樣針對這一層點右鍵,取得 `XPath`,會是: 252 | 253 | `//*[@id="intro"]/form/div[4]` 254 | 255 | 可以修改成: 256 | 257 | `//*[@id="intro"]/form/div[@class='joblist_cont focus']` 258 | 259 | 因為有兩種 class,所以可以用 `|` 來串連兩個搜尋結果 260 | 261 | 262 | `//*[@id="intro"]/form/div[@class='joblist_cont focus']|//*[@id="intro"]/form/div[@class='joblist_cont']` 263 | 264 | 接著就可以開始寫程式了: 265 | 266 | ```swift 267 | 268 | let html = "要先透過 URLSession 拿到此網頁的 html,就不再贅述" 269 | if let doc = HTML(html: html, encoding: .utf8) { 270 | let jobNodes = doc.xpath("//*[@id="intro"]/form/div[@class='joblist_cont focus']|//*[@id="intro"]/form/div[@class='joblist_cont']") 271 | for jobNode in jobNodes { 272 | let jobnameNode = jobNode.xpath("ul/li/div[@class='jobname']/a").first! 273 | let name = jobnameNode.text! 274 | var date = jobNode.xpath("ul/li/div[@class='date']").first!.innerHTML! 275 | if date.contains("重點職務") { 276 | date = "重點職務" 277 | } 278 | 279 | print(name, date) 280 | } 281 | } 282 | ``` 283 | 284 | 透過這樣的方式就能取得`日期`、`職務名稱`等等,其中要注意到的,日期有兩種類型, 285 | 如果是重點職務就取不到日期,所以要使用 `innerHTML` 而不是 `text`。 286 | 287 | 288 | 289 | 290 | -------------------------------------------------------------------------------- /iOS/xpath/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/1.png -------------------------------------------------------------------------------- /iOS/xpath/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/10.png -------------------------------------------------------------------------------- /iOS/xpath/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/11.png -------------------------------------------------------------------------------- /iOS/xpath/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/12.png -------------------------------------------------------------------------------- /iOS/xpath/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/13.png -------------------------------------------------------------------------------- /iOS/xpath/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/14.png -------------------------------------------------------------------------------- /iOS/xpath/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/2.png -------------------------------------------------------------------------------- /iOS/xpath/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/3.png -------------------------------------------------------------------------------- /iOS/xpath/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/4.png -------------------------------------------------------------------------------- /iOS/xpath/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/5.png -------------------------------------------------------------------------------- /iOS/xpath/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/6.png -------------------------------------------------------------------------------- /iOS/xpath/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/7.png -------------------------------------------------------------------------------- /iOS/xpath/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/8.png -------------------------------------------------------------------------------- /iOS/xpath/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/willard1218/Articles/9f69c42472d88244bbc8dd429a69bcc57c6310e0/iOS/xpath/9.png -------------------------------------------------------------------------------- /iOS/xpath/xpath.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 |

XPath

8 | 補充資料 : XML XPath的選擇節點語法 9 |
    10 |
  • text 1
  • 11 |
  • text 2
  • 12 |
  • text 3
  • 13 |
  • text 4
  • 14 |
15 |
16 |

text

17 | 18 | 19 | 20 | 21 | 22 | 23 | --------------------------------------------------------------------------------