
[{"content":" Hi World! This is a test note. Please see the following very lengthy equation of the expanded form of the 4 equations of Maxwell\u0026rsquo;s equations in differential form:\n$$\n\\begin{aligned} \\nabla \\cdot \\mathbf{E} \u0026amp;= \\frac{\\rho}{\\varepsilon_0} \\ \\nabla \\cdot \\mathbf{B} \u0026amp;= 0 \\ \\nabla \\times \\mathbf{E} \u0026amp;= -\\frac{\\partial \\mathbf{B}}{\\partial t} \\ \\nabla \\times \\mathbf{B} \u0026amp;= \\mu_0 \\mathbf{J} + \\mu_0 \\varepsilon_0 \\frac{\\partial \\mathbf{E}}{\\partial t} \\end{aligned}\n$$\n","date":"Jan 25, 2025","externalUrl":null,"permalink":"/private/test-note/","section":"Privates","summary":"Ignore this note.","title":"A Test Note","type":"private"},{"content":"","date":"Jan 25, 2025","externalUrl":null,"permalink":"/","section":"jskherman","summary":"","title":"jskherman","type":"page"},{"content":"","date":"Jan 25, 2025","externalUrl":null,"permalink":"/private/","section":"Privates","summary":"","title":"Privates","type":"private"},{"content":"","date":"Jan 01, 2025","externalUrl":null,"permalink":"/categories/","section":"Categories","summary":"","title":"Categories","type":"categories"},{"content":"","date":"Jan 01, 2025","externalUrl":null,"permalink":"/tags/info/","section":"Tags","summary":"","title":"info","type":"tags"},{"content":"This is an \u0026ldquo;impossible list\u0026rdquo;. It\u0026rsquo;s a list of ambitious goals that I want to achieve in my lifetime. I\u0026rsquo;ll be updating this list as I go along and cross off items that I\u0026rsquo;ve completed.\n👉 This is not a bucket list.1\nSee the following examples: Impossible HQ and College Info Geek.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"Jan 01, 2025","externalUrl":null,"permalink":"/moonshots/","section":"jskherman","summary":"A list of ambitious goals that I want to achieve in my lifetime.","title":"Moonshots: An Impossible List","type":"page"},{"content":"","date":"Jan 01, 2025","externalUrl":null,"permalink":"/categories/page/","section":"Categories","summary":"","title":"Page","type":"categories"},{"content":"","date":"Jan 01, 2025","externalUrl":null,"permalink":"/tags/","section":"Tags","summary":"","title":"Tags","type":"tags"},{"content":"This is a list of tools and services that I use. I\u0026rsquo;ll be updating this list as I go along and cross off items that I\u0026rsquo;ve stopped using.\nHardware # Truly Wireless Earphones: The Sony LinkBuds S. I use these for listening to listening music mostly but also for calls and meetings. They\u0026rsquo;re great for noise-cancellation and not as expensive nor sensitive to sweat as its cousin, the WF-1000XM4. With it being wireless, I can move around freely without worrying about cables getting in the way, which I used to have a lot of trouble with when I cannot afford a decent pair of earphones, way back when I was still in high school. Software # Website: Hugo. I use Hugo to generate this website. It\u0026rsquo;s a static site generator written in Go. I like it because it\u0026rsquo;s fast and easy to use. I can write my posts in Markdown and then run a command to generate the site. It\u0026rsquo;s also easy to customize the theme and there are a lot of high-quality themes you can choose from if you know where to look. ","date":"Jan 01, 2025","externalUrl":null,"permalink":"/uses/","section":"jskherman","summary":"A list of tools and services that I use.","title":"What I Use","type":"page"},{"content":"This is the job description for the role of Process Engineer in the O\u0026amp;G industry. This is from the posting for my role at Petron Corporation.\nResponsible to, advises and assists the Chief Process Engineer on matters pertaining to the technical aspects of the overall refinery process operations, operating data analysis of projects to improve the efficiency, safety of operations, and/or support additional requirements in terms of production, new product, or enhanced product quality.\nShow Picture? Description from Similar Roles Works together with the Process Control Supervisors in reviewing day-to-day process operations to spot and correct potential problems or deviations from operating plans. Compiles and analyzes daily operating data and processing set-up. Maintains and updates refinery data on equipment and processing set-up. Constantly reviews product qualities to ensure adherence to standard manufacturing specification without sacrificing blending optimization. Provides necessary technical assistance in trouble shooting refinery operating difficulties, process unit shutdown, turnaround, and start-up. Conducts technical studies, prepares, and recommends measures to reduce costs and improve operations, safety, energy efficiency and product quality. Plans, follows-up and evaluates process unit test runs and makes recommendations to reduce costs and improve operation, safety, energy efficiency and product quality. Conducts surveys/studies and makes necessary recommendations to improve yield and product quality, oil loss, cost reduction, energy conservation protection of t he refinery. Monitors and optimizes the consumption of his/her unit assignment. Conducts technical evaluation of catalyst and chemical proposals. Updates as necessary chemical ordering specifications. Conducts updating of operating manual, P\u0026amp;ID and IMS work procedures and work instructions Formal Education: Preferably B.S. in Engineering (preferably Chemical Engineering). Experience: Preferably two (2) to eight (8) years petroleum refining, with emphasis on process engineering and design, project handling and refinery operations. Others: Candidate should be initiative, tactful, resourceful, creative, and possess alertness, good judgment, analytical thinking, and ability to get along well with people. Knows how to drive. Job Segment: Process Engineer, Refinery, Engineer, Engineering, Data Analyst, Energy, Data ","date":"Aug 08, 2024","externalUrl":null,"permalink":"/gists/process_engineer_jd/","section":"Gists","summary":"A job description for the role of Process Engineer in the O\u0026amp;G industry.","title":"📋process_engineer_jd.md","type":"gists"},{"content":"","date":"Aug 08, 2024","externalUrl":null,"permalink":"/tags/career/","section":"Tags","summary":"","title":"career","type":"tags"},{"content":"","date":"Aug 08, 2024","externalUrl":null,"permalink":"/tags/chemical-engineering/","section":"Tags","summary":"","title":"chemical engineering","type":"tags"},{"content":"","date":"Aug 08, 2024","externalUrl":null,"permalink":"/categories/gist/","section":"Categories","summary":"","title":"Gist","type":"categories"},{"content":"","date":"Aug 08, 2024","externalUrl":null,"permalink":"/gists/","section":"Gists","summary":"","title":"Gists","type":"gists"},{"content":"","date":"Aug 08, 2024","externalUrl":null,"permalink":"/tags/job-hunting/","section":"Tags","summary":"","title":"job hunting","type":"tags"},{"content":"","date":"Jun 04, 2024","externalUrl":null,"permalink":"/tags/cv/","section":"Tags","summary":"","title":"cv","type":"tags"},{"content":"","date":"Jun 04, 2024","externalUrl":null,"permalink":"/tags/github/","section":"Tags","summary":"","title":"github","type":"tags"},{"content":"imprecv is a no-frills curriculum vitae (CV) template for Typst that uses a YAML file for data input in order to version control CV data easily.\nThis is based on the popular template on Reddit by u/SheetsGiggles and the recommendations of the r/EngineeringResumes wiki.\nSample CV See example CV and @jskherman\u0026rsquo;s CV.\nGitHub • Typst Universe\nUsage # This imprecv is intended to be used by importing the template\u0026rsquo;s package entrypoint from a \u0026ldquo;content\u0026rdquo; file (see template.typ as an example). In this content file, call the functions which apply document styles, show CV components, and load CV data from a YAML file (see template.yml as an example). Inside the content file you can modify several style variables and even override existing function implementations to your own needs and preferences.\nWith the Typst CLI # The recommended usage with the Typst CLI is by running the command typst init @preview/imprecv:1.0.0 in your project directory. This will create a new Typst project with the imprecv template and the necessary files to get started. You can then run typst compile template.typ to compile your file to PDF.\nTake a look at the example setup for ideas on how to get started. It includes a GitHub action workflow to compile the Typst files to PDF and upload it to Cloudflare R2.\nWith typst.app # From the Dashboard, select \u0026ldquo;Start from template\u0026rdquo;, search and choose the imprecv template. From there, decide on a name for your project and click \u0026ldquo;Create\u0026rdquo;. You can now edit the template files and preview the result on the right.\nYou can also click the Create project in app button in Typst Universe to create a new project with the imprecv template.\n","date":"Jun 04, 2024","externalUrl":null,"permalink":"/projects/imprecv/","section":"Projects","summary":"A no-frills CV template using Typst and YAML to version control CV data.","title":"imprecv","type":"projects"},{"content":"","date":"Jun 04, 2024","externalUrl":null,"permalink":"/categories/project/","section":"Categories","summary":"","title":"Project","type":"categories"},{"content":"","date":"Jun 04, 2024","externalUrl":null,"permalink":"/projects/","section":"Projects","summary":"","title":"Projects","type":"projects"},{"content":"","date":"Jun 04, 2024","externalUrl":null,"permalink":"/tags/resume/","section":"Tags","summary":"","title":"resume","type":"tags"},{"content":"","date":"Jun 04, 2024","externalUrl":null,"permalink":"/tags/template/","section":"Tags","summary":"","title":"template","type":"tags"},{"content":"","date":"Jun 04, 2024","externalUrl":null,"permalink":"/tags/typst/","section":"Tags","summary":"","title":"typst","type":"tags"},{"content":"This is a Python script that converts a selected region of cells in Microsoft Excel to a table in Typst. The code below was created by @flaribbit and was originally shared on the Typst Discord server on 2024-05-11. The comments are not included in the original snippet and were added after the fact using Meta\u0026rsquo;s Llama 3 70B LLM for documentation.\nUse the script below at your own risk. The owner of the current website does not guarantee the accuracy of the code provided below (nor of any code provided on this website) and is not responsible for any damages that may arise from using the code. Please review the code and understand what it does before using it.\nThe owner of the current website does not also claim ownership of the code below. The code is rightfully owned by the original author, @flaribbit. The code is shared here for preservation and for educational purposes.\n# Import the win32com.client module, which provides an interface to interact with Microsoft Office applications import win32com.client # Get the active Excel application object excel = win32com.client.GetActiveObject(\u0026#34;Excel.Application\u0026#34;) # Get the active workbook and sheet wb = excel.ActiveWorkbook sheet = wb.ActiveSheet # Get the selected range of cells selected = excel.Selection # Get the number of columns in the selected range columns = selected.Columns.Count # Open a file to write the Typst table to out = open(\u0026#34;output.typ\u0026#34;, \u0026#34;w\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) # Write the table header out.write(f\u0026#34;#table(columns: {columns},\u0026#34;) # Iterate over each row in the selected range for i in range(selected.Rows.Count): out.write(\u0026#34;\\n \u0026#34;) # Iterate over each column in the selected range for j in range(selected.Columns.Count): # Get the current cell cell = selected.Cells(i + 1, j + 1) # Check if the cell is part of a merged range if cell.MergeCells: # Check if the cell is the top-left cell of the merged range is_top_left = cell.Address == cell.MergeArea.Cells(1, 1).Address if is_top_left: # Get the number of rows and columns in the merged range rows = cell.MergeArea.Rows.Count cols = cell.MergeArea.Columns.Count # Get the text value of the cell value = cell.Text # Write the table cell with rowspan and colspan attributes if necessary out.write(\u0026#34;table.cell(\u0026#34;) if rows \u0026gt; 1: out.write(f\u0026#34;rowspan: {rows}, \u0026#34;) if cols \u0026gt; 1: out.write(f\u0026#34;colspan: {cols}, \u0026#34;) # Remove the trailing comma and space out.seek(out.tell() - 2) out.write(f\u0026#34;)[{value}], \u0026#34;) else: # If the cell is not the top-left cell of the merged range, skip it pass else: # If the cell is not part of a merged range, just write its text value value = cell.Text out.write(f\u0026#34;[{value}], \u0026#34;) # Close the table and file out.write(\u0026#34;\\n)\\n\u0026#34;) out.close() A caveat: for cells that only have rowspans (no unit cells), they will not span the proper cell size (and produce a wrong output) in Typst because the row height is auto. A suggested workaround is by assigning a fixed height to the rows in the output table in Typst (e.g. table(rows: 2em)).\nSee related GitHub discussion.\n","date":"May 11, 2024","externalUrl":null,"permalink":"/gists/excells2typst.py/","section":"Gists","summary":"A Python script that converts a selected region in Microsoft Excel to a table in Typst.","title":"📋excells2typst.py","type":"gists"},{"content":"","date":"May 11, 2024","externalUrl":null,"permalink":"/tags/data-extraction/","section":"Tags","summary":"","title":"data extraction","type":"tags"},{"content":"","date":"May 11, 2024","externalUrl":null,"permalink":"/tags/ms-excel/","section":"Tags","summary":"","title":"ms excel","type":"tags"},{"content":"","date":"May 11, 2024","externalUrl":null,"permalink":"/tags/python/","section":"Tags","summary":"","title":"python","type":"tags"},{"content":"","date":"May 11, 2024","externalUrl":null,"permalink":"/tags/scripting/","section":"Tags","summary":"","title":"scripting","type":"tags"},{"content":"A Templater template for quickly toggling between custom checkboxes available in the Minimal theme for Obsidian using a hotkey.\nSee original showcase/discussion.\n\u0026lt;%* // List of all defined checkbox states. Adjust to match your setup. // Remove \u0026#34;//\u0026#34; to enable additional states. const states = { \u0026#39; \u0026#39;: \u0026#39;Unchecked | Pending\u0026#39;, \u0026#39;/\u0026#39;: \u0026#39;In Progress\u0026#39;, \u0026#39;x\u0026#39;: \u0026#39;Checked | Done\u0026#39;, \u0026#39;\u0026gt;\u0026#39;: \u0026#39;Rescheduled\u0026#39;, \u0026#39;\u0026lt;\u0026#39;: \u0026#39;Scheduled\u0026#39;, \u0026#39;-\u0026#39;: \u0026#39;Cancelled\u0026#39;, // \u0026#39;\u0026#39;: \u0026#39;Clear Checkbox | Reset\u0026#39;, // \u0026#39;!\u0026#39;: \u0026#39;Important\u0026#39;, // \u0026#39;?\u0026#39;: \u0026#39;Question\u0026#39;, // \u0026#39;*\u0026#39;: \u0026#39;Star\u0026#39;, // \u0026#39;n\u0026#39;: \u0026#39;Note\u0026#39;, // \u0026#39;l\u0026#39;: \u0026#39;Location\u0026#39;, // \u0026#39;i\u0026#39;: \u0026#39;Information\u0026#39;, // \u0026#39;I\u0026#39;: \u0026#39;Idea\u0026#39;, // \u0026#39;S\u0026#39;: \u0026#39;Amount | Price | Money\u0026#39;, // \u0026#39;p\u0026#39;: \u0026#39;Pro | Good\u0026#39;, // \u0026#39;c\u0026#39;: \u0026#39;Con | Bad\u0026#39;, // \u0026#39;b\u0026#39;: \u0026#39;Bookmark\u0026#39;, // \u0026#39;\u0026#34;\u0026#39;: \u0026#39;Quote\u0026#39;, // \u0026#39;u\u0026#39;: \u0026#39;Up | Like\u0026#39;, // \u0026#39;d\u0026#39;: \u0026#39;Down | Dislike\u0026#39;, // \u0026#39;w\u0026#39;: \u0026#39;Win\u0026#39;, // \u0026#39;k\u0026#39;: \u0026#39;Key\u0026#39;, // \u0026#39;f\u0026#39;: \u0026#39;Fire | Hot\u0026#39; }; // Whether to show the new-state input dialog. // When set to false, the checkbox loops though all defined states. let promptUser = false; // Default state if this line: Undone checkbox. // This state is used, if the current line is no checkbox yet, or if // it\u0026#39;s current state is unknown. const defaultState = \u0026#39; \u0026#39;; // -- end of configuration. // Helper functions. const getLabel = key =\u0026gt; (key ? `[${key}] ` : \u0026#39;\u0026#39;) + `${states[key]}`; const stateByListType = (listNumber, key) =\u0026gt; key ? (listNumber ? `${listNumber} [${key}] ` : `- [${key}] `) : \u0026#39; \u0026#39;; const setState = (key, line) =\u0026gt; line.replace( /^(\\s*)(-|\\*|(\\d\\.))?(\\s+\\[.\\])?\\s*/, \u0026#39;$1$2\u0026#39; + stateByListType(\u0026#39;$3\u0026#39;, key) ); // Prepare vars, inspect current editor line. const stateKeys = Object.keys(states); const editor = app.workspace.activeLeaf.view.editor; const cursor = editor.getCursor(\u0026#39;from\u0026#39;); const currentLine = editor.getLine(cursor.line); const stateMatch = currentLine.match(/^[\\s-\\*]+\\s\\[(.)\\]\\s/); const currentState = stateMatch?.[1] || \u0026#39;\u0026#39;; let newState = defaultState; // We found a valid checkbox state. Let\u0026#39;s find the next state in the queue. if (currentState) { let nextIndex = stateKeys.indexOf(currentState) + 1 if (nextIndex \u0026gt;= stateKeys.length) { nextIndex = 0; } newState = stateKeys[nextIndex]; } // Optional branch: Let the user choose the new state. if (promptUser) { // Display the \u0026#34;next state\u0026#34; as first option, so it\u0026#39;s auto-selected. const suggestLabels = [getLabel(newState)]; const suggestKeys = [newState]; stateKeys.map(key =\u0026gt; { if (key === newState) { return; } suggestLabels.push(getLabel(key)) suggestKeys.push(key) }); newState = await tp.system.suggester( suggestLabels, suggestKeys, false, \u0026#34;Select new checkbox state\u0026#34; ); } if (\u0026#39;string\u0026#39; === typeof newState) { editor.setLine(cursor.line, setState(newState, currentLine)); } %\u0026gt; ","date":"May 01, 2024","externalUrl":null,"permalink":"/gists/obsidian_toggle_checkbox/","section":"Gists","summary":"A Templater template for quickly toggling between custom checkboxes using a hotkey.","title":"📋obsidian_toggle_checkbox.md","type":"gists"},{"content":"","date":"May 01, 2024","externalUrl":null,"permalink":"/tags/javascript/","section":"Tags","summary":"","title":"javascript","type":"tags"},{"content":"","date":"May 01, 2024","externalUrl":null,"permalink":"/tags/markdown/","section":"Tags","summary":"","title":"markdown","type":"tags"},{"content":"","date":"May 01, 2024","externalUrl":null,"permalink":"/tags/obsidian/","section":"Tags","summary":"","title":"obsidian","type":"tags"},{"content":"","date":"May 01, 2024","externalUrl":null,"permalink":"/tags/templates/","section":"Tags","summary":"","title":"templates","type":"tags"},{"content":"Some themes for Miniflux (RSS Reader).\nBelow is a theme that has a few tweaks of the default sans-serif theme.\n/* Need to allow api.fonts.coollabs.io in Content Security Policy */ /* @import url(\u0026#39;https://api.fonts.coollabs.io/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700\u0026amp;family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900\u0026amp;display=swap\u0026#39;); */ :root { --red: #f38ba8; --orange: #fab387; --green: #a6e3a1; --light-blue: #74c7ec; --blue: #77bdfb; --purple: #cea5fb; --sans-serif: \u0026#34;IBM Plex Sans\u0026#34;, system-ui, -apple-system, \u0026#34;Segoe UI\u0026#34;, Roboto, \u0026#34;Helvetica Neue\u0026#34;, Arial, \u0026#34;Noto Sans\u0026#34;, sans-serif, \u0026#34;Apple Color Emoji\u0026#34;, \u0026#34;Segoe UI Emoji\u0026#34;, \u0026#34;Segoe UI Symbol\u0026#34;, \u0026#34;Noto Color Emoji\u0026#34; !important; --serif: \u0026#34;Source Serif 4\u0026#34;, Georgia, \u0026#34;Times New Roman\u0026#34;, Times, serif; } .entry-content { font-size: 1em; } .item { border: none; } .logo a { font-weight: 600; } nav { font-family: var(--sans-serif); } :root { --font-family: var(--serif); --entry-content-font-family: var(--sans-serif); --entry-content-font-weight: 400; /* Light */ --entry-content-color: #424242; /* Dark */ /* --entry-content-color: #D3D3D3; */ } A longer version tweaked from the original based on the GitHub Dark theme.\n/* Need to allow api.fonts.coollabs.io in Content Security Policy */ /* @import url(\u0026#39;https://api.fonts.coollabs.io/css2?family=IBM+Plex+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700\u0026amp;family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900\u0026amp;display=swap\u0026#39;); */ /* @import url(\u0026#39;https://api.fonts.coollabs.io/css2?family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900\u0026amp;family=Source+Serif+4:ital,opsz,wght@0,8..60,200..900;1,8..60,200..900\u0026amp;display=swap\u0026#39;); */ :root { --base0: #1e1e2e; --base1: #363654; --base2: #4e4e7a; --base3: #bac2de; --base4: #cdd6f4; --base5: #ecf2f8; --red: #f38ba8; --orange: #fab387; --green: #a6e3a1; --light-blue: #74c7ec; --blue: #77bdfb; --purple: #cea5fb; --sans-serif: \u0026#34;IBM Plex Sans\u0026#34;, system-ui, -apple-system, \u0026#34;Segoe UI\u0026#34;, Roboto, \u0026#34;Helvetica Neue\u0026#34;, Arial, \u0026#34;Noto Sans\u0026#34;, sans-serif, \u0026#34;Apple Color Emoji\u0026#34;, \u0026#34;Segoe UI Emoji\u0026#34;, \u0026#34;Segoe UI Symbol\u0026#34;, \u0026#34;Noto Color Emoji\u0026#34; !important; --serif: \u0026#34;Source Serif 4\u0026#34;, Georgia, \u0026#34;Times New Roman\u0026#34;, Times, serif; } .logo a span { color: var(--orange) !important; } .entry-content { font-size: 1em; } .item { border: none; } :root { --font-family: var(--sans-serif); --entry-content-font-family: var(--serif); --entry-content-font-weight: 400; --item-status-read-title-link-color: var(--link-color); --body-color: var(--base5); --body-background: var(--base0); --hr-border-color: var(--base2); --title-color: var(--light-blue); --link-color: var(--base5); --link-focus-color: var(--base4); --link-hover-color: var(--base3); --link-visited-color: var(--base4); --header-list-border-color: var(--base2); --header-link-color: var(--base5); --header-link-focus-color: var(--base4); --header-link-hover-color: var(--base3); --header-active-link-color: var(--light-blue); --table-border-color: var(--base2); --table-th-background: var(--base1); --table-th-color: var(--base5); --table-tr-hover-background-color: var(--base2); --table-tr-hover-color: var(--base5); --button-primary-border-color: var(--green); --button-primary-background: var(--green); --button-primary-color: var(--base0); --button-primary-focus-border-color: var(--base2); --button-primary-focus-background: var(--base2); --input-background: var(--base1); --input-color: var(--base5); --input-placeholder-color: var(--base3); --input-focus-color: var(--base5); --input-focus-border-color: var(--blue); --input-focus-box-shadow: 0 0 0 2px var(--base2); --alert-color: var(--base5); --alert-background-color: var(--base0); --alert-border-color: var(--base2); --alert-success-color: var(--base0); --alert-success-background-color: var(--green); --alert-success-border-color: var(--base2); --alert-error-color: var(--base5); --alert-error-background-color: var(--red); --alert-error-border-color: var(--base2); --alert-info-color: var(--base5); --alert-info-background-color: var(--base2); --alert-info-border-color: var(--base2); --page-header-title-border-color: var(--base2); --logo-color: var(--light-blue); --logo-hover-color-span: var(--blue); --panel-background: var(--base1); --panel-border-color: var(--base2); --panel-color: var(--base5); --modal-background: var(--base0); --modal-color: var(--base5); --modal-box-shadow: 2px 0 5px 0 var(--base2); --pagination-link-color: var(--base5); --pagination-border-color: var(--base2); --category-color: var(--base5); --category-background-color: var(--base2); --category-border-color: var(--base2); --category-link-color: var(--base5); --category-link-hover-color: var(--light-blue); --item-border-color: var(--base2); --item-status-read-title-link-color: var(--light-blue); --item-status-read-title-focus-color: var(--base4); --item-meta-focus-color: var(--base4); --item-meta-li-color: var(--base5); --current-item-border-color: var(--blue); --entry-header-border-color: var(--base2); --entry-header-title-link-color: var(--base5); --entry-content-color: var(--base5); --entry-content-code-color: var(--light-blue); --entry-content-code-background: var(--base1); --entry-content-code-border-color: var(--base2); --entry-content-quote-color: var(--base4); --entry-content-abbr-border-color: var(--base2); --entry-enclosure-border-color: var(--base2); --parsing-error-color: var(--base5); --feed-parsing-error-background-color: var(--red); --feed-parsing-error-border-color: var(--base2); --feed-has-unread-background-color: var(--green); --feed-has-unread-border-color: var(--base2); --category-has-unread-background-color: var(--green); --category-has-unread-border-color: var(--base2); --keyboard-shortcuts-li-color: var(--base5); --counter-color: var(--base5); } ","date":"Apr 30, 2024","externalUrl":null,"permalink":"/gists/miniflux-themes.css/","section":"Gists","summary":"Some themes for Miniflux (RSS Reader).","title":"📋miniflux-themes.css","type":"gists"},{"content":"","date":"Apr 30, 2024","externalUrl":null,"permalink":"/tags/css/","section":"Tags","summary":"","title":"css","type":"tags"},{"content":"","date":"Apr 30, 2024","externalUrl":null,"permalink":"/tags/design/","section":"Tags","summary":"","title":"design","type":"tags"},{"content":"","date":"Apr 30, 2024","externalUrl":null,"permalink":"/tags/rss/","section":"Tags","summary":"","title":"rss","type":"tags"},{"content":"This privacy policy outlines how we collect, use, and process your data on this website. We are committed to protecting your privacy and ensuring transparency.\nData We Collect # General Browsing # We do not collect personal information when you browse the site. However, we may collect the following non-personal data:\nIP address and browser type. Referrer information: the website you visited before arriving at ours. Browsing activity: pages accessed, duration spent on the site, and the site you visit upon leaving. Device information: type of device, operating system version, and screen size. Comments # Comments and reactions submitted via Bluesky are collected by Bluesky, not this website, to facilitate the comment service.\nAnalytics # For improving our website, we collect non-personal analytics data through a self-hosted instance of Umami Analytics, which includes:\nReferrer: the source that led you to our site. Requested URL: the specific page visited. User-Agent: your browser and operating system details (e.g., \u0026ldquo;Chrome 118, Windows 10\u0026rdquo;). Country: determined via IP address. Language: your browser’s default language. Screen size: device screen dimensions. Access time: timestamp of your visit. Browsing session: a temporary identifier combining your IP address, browser data, and a random number to recognize sessions. This identifier is automatically deleted after 8 hours. We do not track unique visitors across sessions or gather data on your activities after leaving the site.\nInformation You Provide # We collect information you share through:\nContact forms. Email correspondence. Online feedback tools (e.g., Google Forms, Typeform, GitHub). How We Collect Your Data # Directly Provided:\nData shared through contact forms, email, or feedback submissions.\nComments:\nCollected and managed by Bluesky.\nAnalytics:\nNon-personal data is collected through Umami Analytics, hosted on Fly.io servers located in Los Angeles, California, USA.\nHow We Use Your Data # We use the data collected to:\nDisplay your comments (managed by Bluesky). Improve website performance and content based on anonymized statistics. Respond to queries or requests submitted via forms or emails. Protect the website, its owner, and users against misuse. We do not use the data for advertising, profiling, or any other unrelated purposes.\nData Sharing # We may share information in the following circumstances:\nLegal Requirements:\nTo comply with legal obligations, such as court orders or subpoenas.\nBusiness Partners:\nIf a third party sponsors content or operates an affiliate program, limited data may be shared, as described in their respective privacy policies.\nWe do not sell your data.\nData Storage # Analytics Data: Stored on Fly.io servers in the United States. Comments: Managed and stored by Bluesky, as per their privacy policy and FAQ. Data Retention # Aggregated and anonymized statistics are stored indefinitely for performance tracking. Data provided through comments or forms is retained indefinitely unless deletion is requested. Your Data Rights # You have the following rights regarding your data:\nAccess: Request a copy of your data. Amend: Request corrections to your data. Object: Opt-out of data collection where applicable. Limit Processing: Restrict how your data is processed. Delete: Request the deletion of your data. Withdraw Consent: Revoke permissions granted for data use. To exercise these rights, contact us via email icon using the button at the bottom of this page. Requests will be addressed within 30 days.\nCookies # We do not use cookies on this site.\nThird-Party Links # This site contains links to third-party websites. We are not responsible for their privacy practices. Please review their privacy policies when visiting external links.\nChanges to This Policy # We regularly update this privacy policy. The latest update date and history are displayed at the top of this page.\nContact Information # For inquiries, concerns, or requests related to your data or this policy, contact us via email using the button at the bottom of this page.\nJurisdiction # This policy is governed by the laws of the Philippines, including the Data Privacy Act of 2012. Any disputes are subject to Philippine jurisdiction.\n","date":"Apr 27, 2024","externalUrl":null,"permalink":"/privacy/","section":"jskherman","summary":"The privacy policy of this site.","title":"Privacy Policy","type":"page"},{"content":"A raw text dump.\nIyBNVGMzTURFek16TXlOVGN5TXpjMU9USTJOREUwT1RrMk5EWXpORFV3TXpRME56TXpOVEEyTXpZME1qTTVORFl6TWpBNU56YzFNakV3TURFd016QTNORGc1TVRZNU16WXdORGswTkRZMk16Y3hPRGd5TkRrd09UVTVORE0xT0RrNU16WXhORE16TXpRek1UUXhORFV4T0RRMk1UWXhOVEF5TXpjek1EVXdOVEF4TXpNd016ZzFOREF6TkRNME5UY3lOREEyTXprNU1qYzROVGMxTXpjNU16azNNelV3TmpVNU1qQTJOREE1TWpreE1EWXdORE15T0RVNE5ESTJOVEk1TkRBNU1UVXpNelkwT0RZMU5EYzBNRGcxTkRJMU56UTIKCmltcG9ydCBhcmdwYXJzZQppbXBvcnQgYmFzZTY0CmltcG9ydCBqc29uCmltcG9ydCBsaW5lY2FjaGUKCmRlZiBsb2FkX2ZpbGUoKSAtPiBzdHI6CiAgICAiIiIKICAgIExvYWRzIHRoZSBiYXNlNjQgZW5jb2RlZCBkYXRhIGFuZCBkZWNvZGVzIGl0IGludG8gYSBzdHJpbmcuCgogICAgUmV0dXJucwogICAgLS0tLS0tLS0tCiAgICAgICAgc3RyOiBUaGUgZGVjb2RlZCBjb250ZW50cyBvZiB0aGUgIm0tY29kZS50eHQiIGZpbGUKICAgICIiIgogICAgIyBHZXQgdGhlIGZpcnN0IGxpbmUgb2YgdGhpcyBzY3JpcHQKICAgIHNjcmlwdF9saW5lcyA9IGxpbmVjYWNoZS5nZXRsaW5lcyhfX2ZpbGVfXykKICAgIGZpcnN0X2xpbmUgPSBzY3JpcHRfbGluZXNbMF0uc3RyaXAoKSBpZiBzY3JpcHRfbGluZXMgZWxzZSAiIgoKICAgICMgQ2hlY2sgaWYgdGhlIGZpcnN0IGxpbmUgc3RhcnRzIHdpdGggJyMnCiAgICBpZiBmaXJzdF9saW5lLnN0YXJ0c3dpdGgoJyMnKToKICAgICAgICAjIEV4dHJhY3QgdGhlIGJhc2U2NCBzdHJpbmcKICAgICAgICBiYXNlNjRfc3RyaW5nID0gZmlyc3RfbGluZVsxOl0uc3RyaXAoKQogICAgICAgIHByaW50KCJEYXRhIGZvdW5kISIpCgogICAgICAgICMgRGVjb2RlIHRoZSBiYXNlNjQgc3RyaW5nCiAgICAgICAgZGVjb2RlZF9ieXRlcyA9IGJhc2U2NC5iNjRkZWNvZGUoYmFzZTY0X3N0cmluZykKICAgICAgICBjb250ZW50cyA9IGRlY29kZWRfYnl0ZXMuZGVjb2RlKCd1dGYtOCcpCiAgICBlbHNlOgogICAgICAgIHByaW50KCJFcnJvcjogRmlsZSBjb3JydXB0ZWQuIE5vIGRhdGEgZm91bmQhIikKICAgICAgICBleGl0KCkKCiAgICAjIENsZWFuIHVwIHRoZSBsaW5lY2FjaGUKICAgIGxpbmVjYWNoZS5jbGVhcmNhY2hlKCkKICAgIHJldHVybiBjb250ZW50cwoKCmRlZiBwcm9jZXNzX2RpZ2l0cygKICAgICAgICBkaWdpdF9zdHJpbmc6IHN0ciwKICAgICAgICB3cml0ZV9jc3Y6IGJvb2wgPSBGYWxzZSwKICAgICAgICB3cml0ZV9qc29uOiBib29sID0gRmFsc2UKICAgICk6CiAgICAiIiIKICAgIFNwbGl0IGEgc3RyaW5nIG9mIGRpZ2l0cyBpbnRvIHNpeC1kaWdpdCBudW1iZXJzIGFuZAogICAgd3JpdGUgdGhlbSB0byBDU1YgYW5kIEpTT04gZmlsZXMuCgogICAgQXJndW1lbnRzCiAgICAtLS0tLS0tLS0KICAgICAgICBkaWdpdF9zdHJpbmcgKHN0cik6IFRoZSBzdHJpbmcgb2YgZGlnaXRzIHRvIGJlIHByb2Nlc3NlZC4KICAgICAgICB3cml0ZV9jc3YgKGJvb2wsIG9wdGlvbmFsKTogV2hldGhlciB0byB3cml0ZSB0aGUgc2l4LWRpZ2l0IG51bWJlcnMgdG8gCiAgICAgICAgICAgIGEgQ1NWIGZpbGUuIERlZmF1bHRzIHRvIFRydWUuCiAgICAgICAgd3JpdGVfanNvbiAoYm9vbCwgb3B0aW9uYWwpOiBXaGV0aGVyIHRvIHdyaXRlIHRoZSBzaXgtZGlnaXQgbnVtYmVycyB0byAKICAgICAgICAgICAgYSBKU09OIGZpbGUuIERlZmF1bHRzIHRvIFRydWUuCgogICAgUmFpc2VzCiAgICAtLS0tLS0tLS0KICAgICAgICBTeXN0ZW1FeGl0OiBJZiB0aGUgbGVuZ3RoIG9mIHRoZSBkaWdpdCBzdHJpbmcgaXMgbm90IGRpdmlzaWJsZSBieSA2LgogICAgIiIiCiAgICAjIENoZWNrIGlmIHRoZSBsZW5ndGggb2YgdGhlIHN0cmluZyBpcyBkaXZpc2libGUgYnkgNgogICAgaWYgbGVuKGRpZ2l0X3N0cmluZykgJSA2ICE9IDA6CiAgICAgICAgcHJpbnQoIkVycm9yOiBGaWxlIGNvcnJ1cHRlZC4gVGhlIGRpZ2l0IGNoZWNrIGZhaWxlZC4iKQogICAgICAgIGV4aXQoKQoKICAgICMgU3BsaXQgdGhlIHN0cmluZyBpbnRvIHNpeC1kaWdpdCBudW1iZXJzCiAgICBzaXhfZGlnaXRfbnVtYmVycyA9IFtkaWdpdF9zdHJpbmdbaTppKzZdIGZvciBpIGluIHJhbmdlKDAsIGxlbihkaWdpdF9zdHJpbmcpLCA2KV0KCiAgICBpZiB3cml0ZV9jc3Y6CiAgICAgICAgcHJpbnQoIldyaXRpbmcgQ1NWIGZpbGUuLi4iKQogICAgICAgICMgU2F2ZSBhcyBDU1YKICAgICAgICB3aXRoIG9wZW4oImNvZGVzLmNzdiIsICJ3IikgYXMgZmlsZToKICAgICAgICAgICAgZm9yIG51bWJlciBpbiBzaXhfZGlnaXRfbnVtYmVyczoKICAgICAgICAgICAgICAgIGZpbGUud3JpdGUoZiJ7bnVtYmVyfVxuIikKCiAgICBpZiB3cml0ZV9qc29uOgogICAgICAgIHByaW50KCJXcml0aW5nIEpTT04gZmlsZS4uLiIpCiAgICAgICAgIyBTYXZlIGFzIEpTT04KICAgICAgICB3aXRoIG9wZW4oImNvZGVzLmpzb24iLCAidyIpIGFzIGZpbGU6CiAgICAgICAgICAgIGpzb24uZHVtcChzaXhfZGlnaXRfbnVtYmVycywgZmlsZSwgaW5kZW50PTQpCgogICAgaWYgbm90KHdyaXRlX2NzdiBvciB3cml0ZV9qc29uKToKICAgICAgICBwcmludCgiTm8gZGlnaXRzIG91dHB1dDogQ1NWIG9yIEpTT04gb3V0cHV0IGRpc2FibGVkLiIpCgpkZWYgd3JpdGVfZmlsZXMoCiAgICAgICAgY29udGVudHM6IHN0ciwKICAgICAgICB3cml0ZV9jc3Y6IGJvb2wgPSBUcnVlLAogICAgICAgIHdyaXRlX2pzb246IGJvb2wgPSBUcnVlCiAgICApOgogICAgIiIiCiAgICBXcml0ZSB0aGUgY29udGVudHMgb2YgYSBzdHJpbmcgdG8gYSB0ZXh0IGZpbGUgYW5kCiAgICBwcm9jZXNzIHRoZSBmaXJzdCBsaW5lIGFzIGEgc3RyaW5nIG9mIGRpZ2l0cy4KCiAgICBBcmd1bWVudHMKICAgIC0tLS0tLS0tLQogICAgICAgIGNvbnRlbnRzIChzdHIpOiBUaGUgY29udGVudHMgdG8gYmUgd3JpdHRlbiB0byBhIHRleHQgZmlsZS4KICAgICAgICB3cml0ZV9jc3YgKGJvb2wsIG9wdGlvbmFsKTogV2hldGhlciB0byB3cml0ZSB0aGUgc2l4LWRpZ2l0IG51bWJlcnMgdG8KICAgICAgICAgICAgYSBDU1YgZmlsZS4gRGVmYXVsdHMgdG8gVHJ1ZS4KICAgICAgICB3cml0ZV9qc29uIChib29sLCBvcHRpb25hbCk6IFdoZXRoZXIgdG8gd3JpdGUgdGhlIHNpeC1kaWdpdCBudW1iZXJzIHRvCiAgICAgICAgICAgIGEgSlNPTiBmaWxlLiBEZWZhdWx0cyB0byBUcnVlLgogICAgIiIiCiAgICBwcmludCgiV3JpdGluZyBkZWNvZGVkIGNvbnRlbnRzIHRvIGZpbGUuLi4iKQogICAgIyBXcml0ZSB0aGUgZGVjb2RlZCBjb250ZW50cyBpbnRvIGEgdGV4dCBmaWxlCiAgICB3aXRoIG9wZW4oImNvZGVzLnR4dCIsICJ3IikgYXMgZmlsZToKICAgICAgICBmaWxlLndyaXRlKGYie2NvbnRlbnRzfSIpCgogICAgIyBTcGxpdCB0aGUgc3RyaW5nIG9mIHRoZSBmaWxlJ3MgY29udGVudHMgYnkgbmV3bGluZXMKICAgIGxpbmVzID0gY29udGVudHMuc3BsaXQoIlxuIikKCiAgICAjIEdldCB0aGUgbG9uZyBzdHJpbmcgb2YgZGlnaXRzIG9uIHRoZSBmaXJzdCBsaW5lCiAgICBkaWdpdF9zdHJpbmcgPSBsaW5lc1swXQoKICAgICMgV3JpdGUgQ1NWIGFuZCBKU09OIGZpbGVzIGNvbnRhaW5pbmcgdGhlIHZhbHVlcwogICAgcHJvY2Vzc19kaWdpdHMoZGlnaXRfc3RyaW5nLCB3cml0ZV9jc3Y9d3JpdGVfY3N2LCB3cml0ZV9qc29uPXdyaXRlX2pzb24pCgogICAgcHJpbnQoIkFsbCBGaWxlcyB3cml0dGVuLiIpCiAgICAKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICBwYXJzZXIgPSBhcmdwYXJzZS5Bcmd1bWVudFBhcnNlcihkZXNjcmlwdGlvbj0nRGVjb2RlIGRpZ2l0cyBkYXRhLicpCiAgICBwYXJzZXIuYWRkX2FyZ3VtZW50KCctYycsICctLWNzdicsIGFjdGlvbj0nc3RvcmVfdHJ1ZScsIGhlbHA9J0VuYWJsZSBDU1Ygb3V0cHV0IGZvciBkaWdpdHMnKQogICAgcGFyc2VyLmFkZF9hcmd1bWVudCgnLWonLCAnLS1qc29uJywgYWN0aW9uPSdzdG9yZV90cnVlJywgaGVscD0nRW5hYmxlIEpTT04gb3V0cHV0IGZvciBkaWdpdHMnKQogICAgYXJncyA9IHBhcnNlci5wYXJzZV9hcmdzKCkKCiAgICB3cml0ZV9maWxlcyhsb2FkX2ZpbGUoKSwgd3JpdGVfY3N2PWFyZ3MuY3N2LCB3cml0ZV9qc29uPWFyZ3MuanNvbikK ","date":"Apr 25, 2024","externalUrl":null,"permalink":"/gists/mega-data.txt/","section":"Gists","summary":"A raw text dump.","title":"📋mega-data.txt","type":"gists"},{"content":"Frequently Asked Questions:\nHow can I remove/delete a large file from the commit history in the Git repository? Use the BFG Repo-Cleaner, a simpler, faster alternative to git-filter-branch, specifically designed for removing unwanted files from Git history.\nCarefully follow the usage instructions. The core part is just this:\njava -jar bfg.jar --strip-blobs-bigger-than 100M my-repo.git Any files over 100 MB in size (that aren\u0026rsquo;t in your latest commit) will be removed from your Git repository\u0026rsquo;s history. You can then use git gc to clean away the dead data:\ngit reflog expire --expire=now --all \u0026amp;\u0026amp; git gc --prune=now --aggressive After pruning, we can force push to the remote repo*\ngit push --force Note: cannot force push a protect branch on GitHub\nThe BFG is typically at least 10-50 times faster than running git-filter-branch, and generally easier to use.\nFull disclosure: I\u0026rsquo;m the author of the BFG Repo-Cleaner.\n—Roberto Tyley (on StackOverflow)\nHow do I remove a submodule? In modern Git (I\u0026rsquo;m writing this in 2022, with an updated git installation), this has become quite a bit simpler:\nRun git rm \u0026lt;path-to-submodule\u0026gt;, and commit.\nThis removes the filetree at \u0026lt;path-to-submodule\u0026gt;, and the submodule\u0026rsquo;s entry in the .gitmodules file. I.e. all traces of the submodule in your repository proper are removed.\nAs the docs note however, the .git dir of the submodule is kept around (in the modules/ directory of the main project\u0026rsquo;s .git dir), \u0026ldquo;to make it possible to checkout past commits without requiring fetching from another repository\u0026rdquo;.\nIf you nonetheless want to remove this info, manually delete the submodule\u0026rsquo;s directory in .git/modules/, and remove the submodule\u0026rsquo;s entry in the file .git/config. These steps can be automated using the commands:\nrm -rf .git/modules/\u0026lt;path-to-submodule\u0026gt; git config --remove-section submodule.\u0026lt;path-to-submodule\u0026gt; —John Douthat (on StackOverflow)\n","date":"Apr 25, 2024","externalUrl":null,"permalink":"/gists/git-faqs/","section":"Gists","summary":"Save the BFG Repo-Cleaner for later for fixing mistakes in Git commit history.","title":"📋Things I always search for when working with Git","type":"gists"},{"content":"","date":"Apr 25, 2024","externalUrl":null,"permalink":"/tags/data/","section":"Tags","summary":"","title":"data","type":"tags"},{"content":"","date":"Apr 25, 2024","externalUrl":null,"permalink":"/tags/git/","section":"Tags","summary":"","title":"git","type":"tags"},{"content":"","date":"Apr 25, 2024","externalUrl":null,"permalink":"/tags/plaintext/","section":"Tags","summary":"","title":"plaintext","type":"tags"},{"content":"","date":"Apr 25, 2024","externalUrl":null,"permalink":"/tags/tools/","section":"Tags","summary":"","title":"tools","type":"tags"},{"content":"A Python script that parses an Atom RSS feed from status.cafe and exports the data to a CSV file\n# ============================================================================= # # Author: jskherman # Date: 2024-03-08 # Description: # This script parses an XML file from status.cafe in the Atom format, which # typically represents an RSS feed. The script extracts relevant information # from the entries in the feed, such as the title, author, published # timestamp, content, ID, and link. It then processes this information and # writes it to a CSV file. # # Specifically, the script performs the following tasks: # # 1. Prompts the user to enter the name of the XML file to parse. # 2. Parses the provided XML file using the xml.etree.ElementTree module. # 3. Extracts the following information from each entry in the feed: # - Title # - Author # - Published timestamp (converted to a UNIX timestamp) # - Content (with HTML entities decoded) # - ID (extracted from the URL in the \u0026#39;id\u0026#39; node) # - Link (for the \u0026#39;alternate\u0026#39; link) # - Emoji (extracted from the title, assuming it\u0026#39;s the second word) # 4. Checks for duplicate IDs in the feed entries. If any duplicates are # found, it raises a ValueError with the duplicate IDs and their # corresponding status updates. # 5. Writes the extracted information to a CSV file named \u0026#39;output.csv\u0026#39;, # with the following columns: # - ID # - Timestamp # - Author # - Emoji # - Status # - Link # 6. Prints a success message if the execution is successful. # 7. If an exception occurs during execution, it prints the error message. # # Dependencies: # - xml.etree.ElementTree: for parsing the XML file # - csv: for writing the data to a CSV file # - html: for decoding HTML entities in the content # - re: for using regular expressions to extract the ID from the URL # - datetime: for converting the timestamp string to a UNIX timestamp # # Usage: # 1. Save this script to a file (e.g., feedparser.py). # 2. Run the script using the Python interpreter: `python feedparser.py` # 3. When prompted, enter the name of the XML file to parse (e.g., feed.xml). # 4. The script will process the file and generate an \u0026#39;output.csv\u0026#39; file in # the same directory. # 5. If any errors occur, the script will print the error message and provide # helpful information. # # ============================================================================= import xml.etree.ElementTree as ET import csv from html import unescape import re from datetime import datetime, timezone def extract_emoji(title): \u0026#34;\u0026#34;\u0026#34; Extract the emoji from the title string. Args: title (str): The title string from which to extract the emoji. Returns: str: The extracted emoji, or an empty string if no emoji is found. \u0026#34;\u0026#34;\u0026#34; # Split the title by whitespace title_parts = title.split() # Get the second element as the emoji if len(title_parts) \u0026gt; 1: return title_parts[1] else: return \u0026#34;\u0026#34; def get_status_id(id_url): \u0026#34;\u0026#34;\u0026#34; Extract the status ID from the given URL. Args: id_url (str): The URL containing the status ID. Returns: str: The extracted status ID, or an empty string if no ID is found. \u0026#34;\u0026#34;\u0026#34; # Extract the digits at the end of the URL match = re.search(r\u0026#34;/(\\d+)$\u0026#34;, id_url) if match: return match.group(1) else: return \u0026#34;\u0026#34; def main(): try: # Ask the user for the file name file_name = input(\u0026#34;Enter the name of the file to parse: \u0026#34;) # Parse the XML file tree = ET.parse(file_name) root = tree.getroot() # Open a CSV file for writing with open(\u0026#34;output.csv\u0026#34;, \u0026#34;w\u0026#34;, newline=\u0026#34;\u0026#34;, encoding=\u0026#34;utf-8\u0026#34;) as csvfile: fieldnames = [ \u0026#34;ID\u0026#34;, \u0026#34;Timestamp\u0026#34;, \u0026#34;Author\u0026#34;, \u0026#34;Emoji\u0026#34;, \u0026#34;Status\u0026#34;, \u0026#34;Link\u0026#34;, ] writer = csv.DictWriter(csvfile, fieldnames=fieldnames) # Write the header row in the CSV file writer.writeheader() # Initialize a set to store unique IDs unique_ids = set() # Initialize a dictionary to store duplicate IDs and their corresponding status updates duplicate_ids = {} # Iterate over the entries in the XML file for entry in root.findall(\u0026#34;{http://www.w3.org/2005/Atom}entry\u0026#34;): title = entry.find(\u0026#34;{http://www.w3.org/2005/Atom}title\u0026#34;).text author = entry.find( \u0026#34;{http://www.w3.org/2005/Atom}author/{http://www.w3.org/2005/Atom}name\u0026#34; ).text timestamp_str = entry.find( \u0026#34;{http://www.w3.org/2005/Atom}published\u0026#34; ).text # Convert the timestamp string to a UNIX timestamp timestamp = int( datetime.fromisoformat( timestamp_str.replace(\u0026#34;Z\u0026#34;, \u0026#34;+00:00\u0026#34;) ).timestamp() ) content = unescape( entry.find(\u0026#34;{http://www.w3.org/2005/Atom}content\u0026#34;).text ) id_url = entry.find(\u0026#34;{http://www.w3.org/2005/Atom}id\u0026#34;).text status_id = get_status_id(id_url) link = entry.find(\u0026#39;{http://www.w3.org/2005/Atom}link[@rel=\u0026#34;alternate\u0026#34;]\u0026#39;) if link is not None: link = link.attrib[\u0026#34;href\u0026#34;] else: link = \u0026#34;\u0026#34; emoji_mood = extract_emoji(title) # Check if the ID is already in the set if status_id in unique_ids: # If it\u0026#39;s a duplicate, store it in the duplicate_ids dictionary duplicate_ids[status_id] = content else: # If it\u0026#39;s a unique ID, add it to the set and write the row to the CSV unique_ids.add(status_id) writer.writerow( { \u0026#34;ID\u0026#34;: status_id, \u0026#34;Timestamp\u0026#34;: timestamp, \u0026#34;Author\u0026#34;: author, \u0026#34;Emoji\u0026#34;: emoji_mood, \u0026#34;Status\u0026#34;: content, \u0026#34;Link\u0026#34;: link, } ) # Check if there were any duplicate IDs if duplicate_ids: # Raise an error with the duplicate IDs and their corresponding status updates duplicate_ids_str = \u0026#34;\\n\u0026#34;.join( [ f\u0026#34;ID: {id}, Status: {duplicate_ids[id]}\u0026#34; for id in duplicate_ids ] ) raise ValueError( f\u0026#34;Duplicate IDs found in the input file:\\n{duplicate_ids_str}\u0026#34; ) print(\u0026#34;Execution successful! CSV file \u0026#39;output.csv\u0026#39; has been created.\u0026#34;) except Exception as e: print(f\u0026#34;An error occurred: {e}\u0026#34;) print(\u0026#34;Please ensure that the file you provided is a valid XML file.\u0026#34;) if __name__ == \u0026#34;__main__\u0026#34;: main() ","date":"Mar 08, 2024","externalUrl":null,"permalink":"/gists/statuscafe_parser.py/","section":"Gists","summary":"A Python script that parses an Atom RSS feed from status.cafe and exports the data to a CSV file.","title":"📋statuscafe_parser.py","type":"gists"},{"content":"","date":"Mar 08, 2024","externalUrl":null,"permalink":"/tags/csv/","section":"Tags","summary":"","title":"csv","type":"tags"},{"content":"","date":"Feb 20, 2024","externalUrl":null,"permalink":"/blog/","section":"Blogs","summary":"","title":"Blogs","type":"blog"},{"content":"I recently discovered the Telegram Sync plugin for Obsidian, which allows me to seamlessly transfer messages from Telegram to a file of my choice in my Obsidian Vault. I\u0026rsquo;ve set it up to append new messages under the ## Log heading in my daily note for the current day.\nTo make this workflow efficient, I\u0026rsquo;ve also configured my daily note to automatically create a ## Log heading upon creation, along with Obsidian opening (and creating, if necessary) the daily note upon startup. This integration eliminates the need to choose between Telegram for quick capture and Obsidian for long-form writing and data ownership. I can now enjoy the best of both worlds.\nHere\u0026rsquo;s an example of how a daily note would look like:\n... other daily note content ## Log `15:18` Nec dubitamus multa iter quae et nos invenerat. `19:30` Lorem ipsum dolor sit amet. The template I\u0026rsquo;m using automatically adds a timestamp in HH:mm format at the start of each message. While I still need to test the template\u0026rsquo;s compatibility with images and other files sent via Telegram, it\u0026rsquo;s not a priority for me since I rarely take pictures.\n","date":"Feb 20, 2024","externalUrl":null,"permalink":"/blog/logs-in-telegram-obsidian/","section":"Blogs","summary":"This is a note to myself that\u0026rsquo;s a list of resources to refer to when I\u0026rsquo;m feeling stuck or in a rut epsecially if I\u0026rsquo;m browsing the internet mindlessly.","title":"Interstitial journaling with Telegram and Obsidian","type":"blog"},{"content":"","date":"Feb 20, 2024","externalUrl":null,"permalink":"/tags/journaling/","section":"Tags","summary":"","title":"journaling","type":"tags"},{"content":"","date":"Feb 20, 2024","externalUrl":null,"permalink":"/categories/microblog/","section":"Categories","summary":"","title":"Microblog","type":"categories"},{"content":"","date":"Feb 20, 2024","externalUrl":null,"permalink":"/tags/telegram/","section":"Tags","summary":"","title":"telegram","type":"tags"},{"content":"Situation: I am on a data plan that offers \u0026ldquo;unlimited data\u0026rdquo; if I am on a 5G connection, otherwise I have a limited amount of data on 4G and lower bands of the network. It has become annoying for me that my phone just spontaneously switches to 4G even if I am well within a 5G network (and, yes, I have turned off the option for \u0026ldquo;Smart 5G\u0026rdquo; that\u0026rsquo;s there to save power). It also becomes a bigger headache when I run out of non-5G data since I lose access to the \u0026ldquo;unlimited data\u0026rdquo; even though I am connected on a 5G band.\nAction: Thus, I have looked for ways to force my phone to only be in 5G mode. One of the posts I stumbled upon suggests installing an app on the Google Play Store called Netmonitor1 in order to set the setting of my Preferred Network Type to NR only. This may be a bad idea doing something that I do not fully understand but so far it does work for quite some time now that I have tested it. The setting sometimes just reverts back when my phone kills the Netmonitor app in order to save power and prolong battery life. I resolved that immediately with some fiddling with the battery settings and locking the app in the recent apps screen.\nHowever, in the end, one big problem that I cannot resolve is the lack of proper standalone 5G towers in the area I\u0026rsquo;m currently residing at. Looking at the app, there\u0026rsquo;s seems to be only LTE Advanced + 5G (NSA) cell towers at most (and the signal sometimes gets lost and only 4G is available). Making it hard to actually get my money\u0026rsquo;s worth out of the \u0026ldquo;unlimited\u0026rdquo; 5G data + extra 12GB data (non-5G) data plan.\nIt seems like this mobile data plan was too good to be true and the catch is in the technical details that a layperson might not see. Given the sparse 5G infrastructure and (possibly) deprioritization of your requests in the network because of having \u0026ldquo;unlimited\u0026rdquo; data, it\u0026rsquo;s probably to better to go with the conventional optical fiber plans for your internet connection unless you only have mobile data available to you.\nThere\u0026rsquo;s also the option to dial *#*#4636#*#* in your Dialer app to open a hidden menu and change the setting there. However, I cannot get it to launch the hidden menu for me (probably blocked by phone manufacturer).\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"Feb 19, 2024","externalUrl":null,"permalink":"/blog/forcing-5g/","section":"Blogs","summary":"A write up about finding a way to force my phone to only use 5G.","title":"Forcing my phone to only use 5G","type":"blog"},{"content":"","date":"Feb 19, 2024","externalUrl":null,"permalink":"/tags/internet/","section":"Tags","summary":"","title":"internet","type":"tags"},{"content":"","date":"Feb 19, 2024","externalUrl":null,"permalink":"/tags/networking/","section":"Tags","summary":"","title":"networking","type":"tags"},{"content":"","date":"Feb 19, 2024","externalUrl":null,"permalink":"/tags/phones/","section":"Tags","summary":"","title":"phones","type":"tags"},{"content":" Recently, I noticed that the VS Code extension for the Typst LSP added support for textmate scopes, which means that I can now use HyperSnips snippets for my Typst files! 1 On another note, there\u0026rsquo;s also support now for contexts with the typst.vim plugin in Neovim, just like in the vimtex plugin. Now making it possible to have UltiSnips and LuaSnip work with Typst files as well.\nSeeing as writing snippets for Typst is now possible with Neovim and/or VS Code. I set out last night to convert some of the snippets in Gilles Castel\u0026rsquo;s original blog post on his LaTeX snippets that I found useful for my own use.\nIn the process of porting over the snippets from \\(\\LaTeX\\) to Typst, a lot of the snippets in Castel\u0026rsquo;s post are not applicable anymore just because Typst\u0026rsquo;s syntax is far easier, brief, and more convenient to write. For example, the fraction snippet in \\(\\LaTeX\\) which involves using / and regex in order to transform the 1/2 way of writing fractions into proper \\(\\LaTeX\\) syntax is no longer necessary in Typst because fractions are now just 1/2 in Typst literally.2\nLinks # These snippets are hacky and unpolished. They\u0026rsquo;re provided as is so use them at your own risk. The Typst snippets I made are available in my old dotfiles repository:\nHyperSnips snippets3 UltiSnips snippets4 LuaSnip snippets Caveats # A caveat for the HyperSnips snippets is that there is no such thing as a \u0026ldquo;VISUAL\u0026rdquo; mode in VS Code like in Vim/Neovim, so the snippets that rely on it won\u0026rsquo;t work. To work around this, I used a suggested solution in the HyperSnips repo which makes use of the VS Code API which replaces the VISUAL mode with TextMate\u0026rsquo;s TM_SELECTED_TEXT variable.5\nHyperSnips is a snippet engine for VS Code heavily inspired by vim\u0026rsquo;s UltiSnips.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nScoping is also done, as you would expect, with parentheses which is good and a lot like written math instead of numerous curly brackets.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nThese snippets require some reworking in order to work with the updates to the Typst LSP in VS Code.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nI don\u0026rsquo;t use Ultisnips anymore, see LuaSnip.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nAn example is this snippet which surrounds selected text with parentheses.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"Oct 07, 2023","externalUrl":null,"permalink":"/blog/typst-snippets/","section":"Blogs","summary":"My attempt at converting Gilles Castel\u0026rsquo;s LaTeX snippets to Typst on Neovim and VS Code to make writing math faster just like in Castel\u0026rsquo;s original post.","title":"Adapting Gilles Castel’s LaTeX snippets for Typst","type":"blog"},{"content":"","date":"Oct 07, 2023","externalUrl":null,"permalink":"/categories/blog/","section":"Categories","summary":"","title":"Blog","type":"categories"},{"content":"","date":"Oct 07, 2023","externalUrl":null,"permalink":"/tags/neovim/","section":"Tags","summary":"","title":"neovim","type":"tags"},{"content":"","date":"Oct 07, 2023","externalUrl":null,"permalink":"/tags/snippets/","section":"Tags","summary":"","title":"snippets","type":"tags"},{"content":"","date":"Oct 07, 2023","externalUrl":null,"permalink":"/tags/vim/","section":"Tags","summary":"","title":"vim","type":"tags"},{"content":"","date":"Oct 07, 2023","externalUrl":null,"permalink":"/tags/vs-code/","section":"Tags","summary":"","title":"vs code","type":"tags"},{"content":"A Python script for exporting a GitHub user\u0026rsquo;s daily commit counts to a CSV file.\n# requirements.txt # Do `pip install beautifulsoup4 pandas` beautifulsoup4 pandas def get_gh_commit_data(github_username: str) -\u0026gt; list: \u0026#34;\u0026#34;\u0026#34; Get GitHub commit data for a given username by scraping the GitHub contribution calendar heatmap for each year of activity. This function returns a list of dictionaries with the following keys: - date: The date of the commit - commits: The number of commits on that date Parameters ---------- github_username : str The GitHub username to get commit data for. Returns ------- list A list of dictionaries with the commit data. \u0026#34;\u0026#34;\u0026#34; # Import the requests and BeautifulSoup libraries import requests import datetime from bs4 import BeautifulSoup _r1 = requests.get(f\u0026#34;https://github.com/{github_username}/\u0026#34;) # Parse HTML and save to BeautifulSoup object html = BeautifulSoup(_r1.text, \u0026#34;html.parser\u0026#34;) # Select the \u0026#34;year\u0026#34; links year_list = html.select(\u0026#34;a.js-year-link\u0026#34;) # Get the page links for each year pages = [] for year in year_list: pages.append(\u0026#34;https://github.com\u0026#34; + year.attrs[\u0026#34;href\u0026#34;]) # Get the commit data for each year commits = [] today = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) for page in pages: # Get the page HTML for a given year _r = requests.get(page) html = BeautifulSoup(_r.text, \u0026#34;html.parser\u0026#34;) # Get all the \u0026#34;rect\u0026#34; elements in the Calendar heatmap days = html.find_all(\u0026#34;td\u0026#34;, class_=\u0026#34;ContributionCalendar-day\u0026#34;) # Get the commit data for each day for day in days: # Check if the rect element has a \u0026#34;data-date\u0026#34; and \u0026#34;data-count\u0026#34; attribute if (\u0026#34;data-date\u0026#34; in day.attrs): # Check if the date is in the past if datetime.datetime.strptime(day[\u0026#34;data-date\u0026#34;], \u0026#34;%Y-%m-%d\u0026#34;) \u0026lt; today: # Append the commit data to the list commits commit_count = day.text.split(\u0026#34; \u0026#34;)[0] if commit_count == \u0026#34;No\u0026#34;: commit_count = 0 commits.append( {\u0026#34;date\u0026#34;: day[\u0026#34;data-date\u0026#34;], \u0026#34;commit_count\u0026#34;: commit_count} ) return commits def main(args=None): import pandas as pd # Get the GitHub username from the user username = input(\u0026#34;Enter GitHub username: \u0026#34;) # Get the commit data and save to a pandas DataFrame df = pd.DataFrame(get_gh_commit_data(username)) df.date = pd.to_datetime(df.date) df = df.sort_values(by=\u0026#34;date\u0026#34;) # Save the commit data to a CSV file df.to_csv(f\u0026#34;{username}_commit_data.csv\u0026#34;, index=False) # Check if the script is being run directly and run the main function if __name__ == \u0026#34;__main__\u0026#34;: main() ","date":"Aug 05, 2023","externalUrl":null,"permalink":"/gists/get_gh_commits.py/","section":"Gists","summary":"A Python script for exporting a GitHub user\u0026rsquo;s daily commit counts to a CSV file.","title":"📋get_gh_commits.py","type":"gists"},{"content":" TL;DR: It might not be possible. But there are workarounds via capture groups. Today I learned how to apply multiple transforms to a variable in VS Code snippets using RegEx capture groups. I was trying to create a VS Code snippet for quickly creating new markdown files with prefilled Hugo post metadata fields. I wanted to be able to type @note and have it expand to the following (using an example filename):\n\u0026lt;!-- Filename: 2023-05-24_example-post-1.md --\u0026gt; --- slug: example-post-1 title: Example Post 1 date: 2023-05-24T16:47:35+08:00 tags: [] --- your text here Setting up the snippet # I was able to get the ISO timestamp easily as well as tabstops for the tags field and body of the post by following the documentation for VS Code snippets. So far, I got the following:\n{ \u0026#34;SITREP\u0026#34;: { \u0026#34;scope\u0026#34;: \u0026#34;md, markdown\u0026#34;, \u0026#34;prefix\u0026#34;: \u0026#34;@note\u0026#34;, \u0026#34;body\u0026#34;: [ \u0026#34;---\u0026#34;, \u0026#34;slug: \u0026#34;, \u0026#34;title: \u0026#34;, \u0026#34;date: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}T${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}+08:00\u0026#34;, \u0026#34;tags: [$1]\u0026#34;, \u0026#34;---\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;$0\u0026#34; ], \u0026#34;description\u0026#34;: \u0026#34;Create a new post\u0026#34; } } Transform: extracting the slug with RegEx # The next step now is figuring out how to extract the slug from the filename of my markdown file, which is example-post-1 from 2023-05-24_example-post-1.md. Diving into the documentation again, I saw that variable transforms would be able to do it. After a while of tinkering, I got it to work:\n{ // ... \u0026#34;body\u0026#34;: [ \u0026#34;---\u0026#34;, \u0026#34;slug: ${TM_FILENAME_BASE/^(\\\\d{4}-\\\\d{2}-\\\\d{2})_(.*)/$2/}\u0026#34;, \u0026#34;title: \u0026#34;, \u0026#34;date: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}T${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}+08:00\u0026#34;, \u0026#34;tags: [$1]\u0026#34;, \u0026#34;---\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;$0\u0026#34; ], // ... } Now that is out of the way, it\u0026rsquo;s time to deal with the title field next. Surely, I can just use the same RegEx transform and add some additional rules to create the title from the filename, right? It will be just that simple, right? Nope.\nTransform: extracting the title with RegEx # After countless tries I had with the documentation, it\u0026rsquo;s time to bring in some help from StackOverflow. I found that someone had a similar problem to mine. They wanted to extract the title from a filename test-file-name.md, and end up with Test File Name as the output of their snippet. The StackOverflow solution was to use the RegEx transform /([^-]+)(-*)/g in the snippet:\n{ \u0026#34;body\u0026#34;: \u0026#34;${TM_FILENAME_BASE/([^-]+)(-*)/${1:/capitalize}${2:+ }/g}\u0026#34; } \u0026ldquo;\u0026hellip;Because of the g flag it will get all the occurrences and do each transform of the two capture groups multiple time. In your test case (test-)(file-)(name) that would be three times. It should work for any number of hyphenated words.\n([^-]+) everything up to a hyphen.\n${1:/capitalize} capitalize capture group 1.\n${2:+ } means if there is a 2nd capture group, the (-*), add a space. I added this because at the end there is no hyphen - and thus there will be no 2nd capture group and thus no extra space should be added at the end.\u0026rdquo;\n—Mark (StackOverflow)\nHowever this did not work for me since I have a YYYY-MM-DD date in front plus an underscore to be mindful of: 2023-05-24_example-post-1.md.\nAfter tinkering for a while again, I decided to just use another capture group to capture my prefix of 2023-05-24_ and ignore that capture group in the transform. Since there were three capture groups, I had to increment the integers in the original solution by one. The solutions seems to also work with symbols as well. The regex and snippet look like these:\n(\\\\d{4}-\\\\d{2}-\\\\d{2}_)|([^-]+)(-*) { \u0026#34;body\u0026#34;: \u0026#34;title: ${TM_FILENAME_BASE/(\\\\d{4}-\\\\d{2}-\\\\d{2}_)|([^-]+)(-*)/${2:/capitalize}${3:+ }/g}\u0026#34; } Therefore, the final snippet overall is as follows:\n{ \u0026#34;SITREP\u0026#34;: { \u0026#34;scope\u0026#34;: \u0026#34;md, markdown\u0026#34;, \u0026#34;prefix\u0026#34;: \u0026#34;@sitrep\u0026#34;, \u0026#34;body\u0026#34;: [ \u0026#34;---\u0026#34;, \u0026#34;slug: ${TM_FILENAME_BASE/^(\\\\d{4}-\\\\d{2}-\\\\d{2})_(.*)/$2/}\u0026#34;, \u0026#34;title: ${TM_FILENAME_BASE/(\\\\d{4}-\\\\d{2}-\\\\d{2}_)|([^-]+)(-*)/${2:/capitalize}${3:+ }/g}\u0026#34;, \u0026#34;date: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}T${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}+08:00\u0026#34;, \u0026#34;tags: [$1]\u0026#34;, \u0026#34;---\u0026#34;, \u0026#34;\u0026#34;, \u0026#34;$0\u0026#34; ], \u0026#34;description\u0026#34;: \u0026#34;Create a new post\u0026#34; } } Conclusion # I do not know if there is a better way to do this rather than to use capture groups, but this is the best I could come up with. I\u0026rsquo;m not sure if there really is a way to chain multiple transforms to a variable in VS Code Snippets, but I\u0026rsquo;m glad that I was able to find a workaround. I hope this helps someone else out there who is trying to do something similar.\n","date":"May 24, 2023","externalUrl":null,"permalink":"/blog/vscode-var-transform-snippets/","section":"Blogs","summary":"Figuring out how to apply multiple transforms to a variable in VS Code snippets using RegEx capture groups.","title":"Applying multiple variable transforms in VS Code snippets","type":"blog"},{"content":"","date":"May 24, 2023","externalUrl":null,"permalink":"/tags/blogging/","section":"Tags","summary":"","title":"blogging","type":"tags"},{"content":"","date":"May 24, 2023","externalUrl":null,"permalink":"/tags/hugo/","section":"Tags","summary":"","title":"hugo","type":"tags"},{"content":"I just finished migrating my website again for the fourth time. Yay! 🎉\nThe migration this time was driven by my inability to build my Hugo website using the Stack theme by @CaiJimmy because for some reason it cannot fetch the site templates for some pages. It probably has something to do with how the theme is distributed over Hugo modules and on GitHub since I cannot fetch the theme\u0026rsquo;s templates via the Hugo module method or the Git submodule method. With this, all that I\u0026rsquo;m left with is a site that cannot build and a previously deployed version on Netlify.\nThis is the first time I have been alerted to the risk to Hugo websites regarding this template issue. And this seems to be a common risk shared by all sites generated using Hugo from an externally developed theme. The probability of me encountering this risk again seems small but still possible.\nOn a side note, the last three Hugo migrations I did was because I did not fancy the style, nor the features associated with the theme I selected for long enough. The last three themes I tried was @dsrkafuu\u0026rsquo;s Fuji theme, @dillonzq\u0026rsquo;s LoveIt theme, and @adityatelange\u0026rsquo;s PaperMod theme. In the past before I knew/decided to use Hugo, I also tinkered with Jekyll themes, Gatsby and Next.js site templates, and even tried out WordPress using a DigitalOcean droplet and Namecheap\u0026rsquo;s (paid) hosting at first when I was learning how websites were deployed and how they worked.\nLooking back, there was always something that I eventually discovered and learned about that would compel me to seek a better setup. It felt like when I started learning programming all over again. There was always the question of what the best choice is to make here. Instead of just getting started, I kept getting sidetracked trying to make the experience of my personal website better and suited to my tastes — i.e., shiny object syndrome (SOS).\nOn that note though, I did not regret learning about static site generators (SSG). I went from paying 5 or 6 USD/month for WordPress hosting to absolutely free (as in free beer) static site hosting. What I have is a personal blog. It\u0026rsquo;s not a website of a large corporation with millions of visitors every day. Hosting this should be inexpensive.12 In terms of possible costs saved, this is a HUGE win.\nIn hindsight, it was probably inevitable since I was largely ignorant of what\u0026rsquo;s possible to achieve with websites. It\u0026rsquo;s mind-bogglingly complex\u0026hellip; and I haven\u0026rsquo;t even touched JavaScript or Node.js yet!\nWho knows if I will settle with my currently chosen theme of @reorx\u0026rsquo;s PaperModX theme (a fork of the earlier @adityatelange\u0026rsquo;s PaperMod theme)? So far it seems good enough for what I need currently. Maybe this time, I\u0026rsquo;ll add functionality myself instead of relying so much on the pre-made theme\u0026rsquo;s features out-of-the-box.\n2024-04-26 UPDATE: I moved to using Zola over Hugo. The Tera templating seems easier to read and work with than Go\u0026rsquo;s templating in Hugo. The new theme I settled on is @welpo\u0026rsquo;s tabi theme. Some of the shortcodes I used in PaperModX were already implemented plus a few more bonus shortcodes. The theme also by default uses Inter for the sans-serf font, Source Serif 4 as the serif font, and Cascadia Code for the monospace font which I was thinking.\nMoving forward, I\u0026rsquo;ll archive past website iterations in subdomains of jskherman.com such as 2023.jskherman.com and 2024.jskherman.com. I highly doubt I\u0026rsquo;ll be migrating my website again more than once in a year so this seems fine for now. Even if I did have multiple iterations, I\u0026rsquo;ll just choose the definitve iteration to have the year subdomain.\n2024-11-30 UPDATE: Well well well\u0026hellip; I am back again with Hugo. This time it is with the Blowfish theme. And the year is not even 2025 yet! Maybe I am the problem? :P\nฅ⁠^⁠•⁠ﻌ⁠•⁠^⁠ฅ\nI\u0026rsquo;m grateful that services like Netlify, Vercel, Cloudflare Pages, GitHub Pages, and Surge.sh exist. They make it possible for something amazing like hosting a website for free to be possible for students like me.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\nYou do have to be careful with platforms like Netlify or Vercel because they will charge you a lot if you exceed their free tier limits, especially when you can be DDoSed or have your site be popular overnight. Since this site is just a personal blog, I don\u0026rsquo;t want to be charged for something that I don\u0026rsquo;t make money from and I would want this site to be down instead of me paying for it to be up in such an event.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"May 24, 2023","externalUrl":null,"permalink":"/blog/migrating-sites/","section":"Blogs","summary":"Lessons I learned in my journey of migrating my website four times, encountering template issues, and exploring various themes and site generators.","title":"Migrating my website *again* for the sixth time","type":"blog"},{"content":"","date":"May 24, 2023","externalUrl":null,"permalink":"/tags/regex/","section":"Tags","summary":"","title":"regex","type":"tags"},{"content":"","date":"May 24, 2023","externalUrl":null,"permalink":"/tags/static-site-generators/","section":"Tags","summary":"","title":"static site generators","type":"tags"},{"content":"","date":"May 24, 2023","externalUrl":null,"permalink":"/tags/websites/","section":"Tags","summary":"","title":"websites","type":"tags"},{"content":"","date":"May 24, 2023","externalUrl":null,"permalink":"/tags/zola/","section":"Tags","summary":"","title":"zola","type":"tags"},{"content":"","date":"Jan 02, 2023","externalUrl":null,"permalink":"/categories/code/","section":"Categories","summary":"","title":"Code","type":"categories"},{"content":"Scraping a GitHub user\u0026rsquo;s profile for their daily commit data can be a useful way to track their activity on the platform and potentially even analyze their work habits. In this tutorial, we\u0026rsquo;ll go through how to use the provided Python script to scrape a user\u0026rsquo;s profile and export the data to a CSV file.\nPrerequisites # Before you begin, you\u0026rsquo;ll need to make sure you have the following tools installed on your computer:\nPython 3 The requests library for Python: pip install requests The BeautifulSoup library for Python: pip install beautifulsoup4 The pandas library for Python: pip install pandas Or in short: pip install requests beautifulsoup4 pandas\nStep 1: Define the get_gh_commit_data function # The first step in the script is to define the get_gh_commit_data function. This function takes a single parameter, github_username, which is a string representing the username of the GitHub user whose commit data we want to scrape.\nInside the function, we first import the requests, datetime, and BeautifulSoup libraries. We\u0026rsquo;ll use these libraries to make HTTP requests to the user\u0026rsquo;s profile page, parse the HTML of the page, and extract the commit data we\u0026rsquo;re interested in.\nNext, we use the requests.get function to make an HTTP GET request to the user\u0026rsquo;s profile page. The f string syntax is used to insert the github_username variable into the URL of the page.\nWe then parse the HTML of the page using the BeautifulSoup library and save the resulting object to a variable called html.\nStep 2: Extract the commit data # Next, we use the html.select method to find all elements on the page with the class js-year-link filter-item px-3 mb-2 py-2. These elements represent the links to the calendar pages for each year in which the user made commits. You can find this out by inspecting the HTML of the user\u0026rsquo;s profile page. For example in my GitHub profile, we can see the following:\nHTML source of a GitHub Profile listing the years with commit data \u0026lt;div style=\u0026#34;top: 74px; position: static;\u0026#34; class=\u0026#34;js-profile-timeline-year-list color-bg-default js-sticky float-right col-2 pl-5\u0026#34; data-original-top=\u0026#34;74px\u0026#34;\u0026gt; \u0026lt;ul class=\u0026#34;filter-list small\u0026#34;\u0026gt; \u0026lt;li\u0026gt; \u0026lt;a id=\u0026#34;year-link-2023\u0026#34; class=\u0026#34;js-year-link filter-item px-3 mb-2 py-2 selected\u0026#34; aria-label=\u0026#34;Contribution activity in 2023\u0026#34; data-hydro-click=\u0026#34;{\u0026amp;quot;event_type\u0026amp;quot;:\u0026amp;quot;user_profile.click\u0026amp;quot;,\u0026amp;quot;payload\u0026amp;quot;:{\u0026amp;quot;profile_user_id\u0026amp;quot;:68434444,\u0026amp;quot;target\u0026amp;quot;:\u0026amp;quot;CONTRIBUTION_YEAR_LINK\u0026amp;quot;,\u0026amp;quot;user_id\u0026amp;quot;:68434444,\u0026amp;quot;originating_url\u0026amp;quot;:\u0026amp;quot;https://github.com/jskherman\u0026amp;quot;}}\u0026#34; data-hydro-click-hmac=\u0026#34;456359f111c2486c99fd6a3677edf26cf1dacd3586eac6c789cc10e0ea1a7a8c\u0026#34; data-turbo=\u0026#34;false\u0026#34; href=\u0026#34;/jskherman?tab=overview\u0026amp;amp;from=2023-01-01\u0026amp;amp;to=2023-01-02\u0026#34;\u0026gt;2023\u0026lt;/a\u0026gt; \u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;...\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;...\u0026lt;/li\u0026gt; \u0026lt;li\u0026gt;...\u0026lt;/li\u0026gt; \u0026lt;/ul\u0026gt; \u0026lt;/div\u0026gt; We create an empty list called pages and then iterate over the elements we found. For each element, we extract the href attribute, which contains the URL of the calendar page, and append it to the pages list.\nNow that we have a list of all the calendar pages for the user, we can start extracting the commit data for each page. We create an empty list called commits and then iterate over the pages list.\nFor each page, we make another HTTP GET request using the requests.get function and parse the HTML of the page using BeautifulSoup.\nWe then use the find_all method to find all rect elements on the page. These elements represent the commit data for each day of the month. The GitHub commits heatmap is actually an SVG on the page made up of many columns of square rect elements. Each rect element has attributes to them and we are particularly interested in the data-date and data-count attributes. The data-date attribute contains the date of the commit and the data-count attribute contains the number of commits made on that day.\nHTML source of a GitHub Profile\u0026rsquo;s commits heatmap We iterate over the rect elements and check if they have both a data-date and a data-count attribute. If they do, we extract the values of these attributes and add them to a dictionary with keys \u0026ldquo;date\u0026rdquo; and \u0026ldquo;commits\u0026rdquo;, respectively. We then append this dictionary to the commits list.\nStep 3: Export the commit data to a CSV file # Finally, we define the main function, which is the entry point of the script. Inside this function, we first import the pandas library and then prompt the user to enter their GitHub username.\nWe then call the get_gh_commit_data function, passing in the user\u0026rsquo;s username, and save the returned data to a variable called df.\nNext, we use the to_csv method of the df object to export the data to a CSV file. The to_csv method takes two arguments: the file name, which we create by concatenating the user\u0026rsquo;s username with the string \u0026quot;_commit_data.csv\u0026quot;, and the index parameter, which we set to False to prevent the index column from being included in the CSV file.\nFinally, we have an if statement that checks whether the script is being run as the main module. If it is, we call the main function.\nPutting it all together # To use the script, simply run it from the command line, enter the GitHub username of the user whose commit data you want to scrape, and the script will create a CSV file with the commit data.\nHere\u0026rsquo;s an example of how the script might be used:\n$ python scrape_github_commits.py Enter GitHub username: your-cool-username This will create a file called your-cool-username_commit_data.csv in the same directory as the script, containing the commit data for the user with the username your-cool-username.\nCheck out the completed Python script on GitHub.\nI hope this tutorial has helped you understand how to scrape a GitHub user\u0026rsquo;s profile for their daily commit data and export it to a CSV file using Python.\n","date":"Jan 02, 2023","externalUrl":null,"permalink":"/blog/github-contributions/","section":"Blogs","summary":"\u003cp\u003eScraping a GitHub user\u0026rsquo;s profile for their daily commit data can be a useful way to track their activity on the platform and potentially even analyze their work habits. In this tutorial, we\u0026rsquo;ll go through how to use the provided Python script to scrape a user\u0026rsquo;s profile and export the data to a CSV file.\u003c/p\u003e","title":"Exporting all of my GitHub contributions history","type":"blog"},{"content":"","date":"Jan 02, 2023","externalUrl":null,"permalink":"/tags/guide/","section":"Tags","summary":"","title":"guide","type":"tags"},{"content":"","date":"Jan 02, 2023","externalUrl":null,"permalink":"/tags/web-scraping/","section":"Tags","summary":"","title":"web scraping","type":"tags"},{"content":"Reading a csv file on GitHub, appending a new row to it, and committing it back again to GitHub.1\n# Import packages import os import dotenv # For .env files import datetime import pandas as pd # import base64 import requests import io # Do `pip install pygithub` first: https://stackoverflow.com/a/50072113 from github import Github from github import InputGitTreeElement # dotenv.load_dotenv() # Make sure to have an environment variable for GitHub authentication user = os.environ[\u0026#34;GITHUB_USERNAME\u0026#34;] access_token = os.environ[\u0026#34;GITHUB_TOKEN\u0026#34;] # Specify repo and path to CSV file repo_name = \u0026#39;your-github-repo-name\u0026#39; path = \u0026#39;path-to-file/relative-to/the-root-path/of-the-repo\u0026#39; # Make GET request for current CSV file r = requests.get( f\u0026#39;https://api.github.com/repos/{user}/{repo_name}/contents/{path}\u0026#39;, headers={ \u0026#39;accept\u0026#39;: \u0026#39;application/vnd.github.v3.raw\u0026#39;, \u0026#39;authorization\u0026#39;: f\u0026#39;token {access_token}\u0026#39; } ) # Convert CSV file to pandas DataFrame string_io_obj = io.StringIO(r.text) df = pd.read_csv(string_io_obj) # Specify contents of the new row in key-value pairs new_content = { \u0026#34;header1\u0026#34;: \u0026#34;value1\u0026#34;, \u0026#34;header2\u0026#34;: \u0026#34;value2\u0026#34;, } # Concatenate new row to df new_row = pd.DataFrame(new_content, index=[0]) df = pd.concat([df.loc[:],new_row]).reset_index(drop=True) # Write new version of the CSV file with appended row df.to_csv(path, index=False) # -------------------------------------------------------- # user = os.environ[\u0026#34;GITHUB_USERNAME\u0026#34;] # access_token = os.environ[\u0026#34;GITHUB_TOKEN\u0026#34;] # password = os.environ[\u0026#34;GITHUB_PASSWORD\u0026#34;] # Authenticate with GitHub g = Github(access_token) # Get Repository # repo_name = \u0026#39;your-github-repo-name\u0026#39; repo = g.get_user().get_repo(repo_name) # Save path to current working directory work_dir = os.getcwd() # Add file_names relative to the root file_names = [\u0026#34;test_data.csv\u0026#34;] file_list = [] # Directory separator if os.name == \u0026#34;nt\u0026#34;: # Windows separator = \u0026#34;\\\\\u0026#34; if os.name == \u0026#34;posix\u0026#34;: # Linux/MacOS separator = \u0026#34;/\u0026#34; # Save full-length file paths for i in range(len(file_names)): file_list.append(work_dir + separator + file_names[i]) # Commit Message commit_message = \u0026#34;Add new row on \u0026#34; + datetime.datetime.now().strftime(\u0026#34;%Y-%m-%dT%H:%M:%S\u0026#34;) # Get latest commit refs main_ref = repo.get_git_ref(\u0026#39;heads/main\u0026#39;) # on main branch main_sha = main_ref.object.sha base_tree = repo.get_git_tree(main_sha) # Initialize empty list for git elements element_list = list() # Populate list for i, entry in enumerate(file_list): with open(entry) as input_file: data = input_file.read() element = InputGitTreeElement(file_names[i], \u0026#39;100644\u0026#39;, \u0026#39;blob\u0026#39;, data) element_list.append(element) # Commit new files tree = repo.create_git_tree(element_list, base_tree) parent = repo.get_git_commit(main_sha) commit = repo.create_git_commit(commit_message, tree, [parent]) main_ref.edit(commit.sha) 👀 Might be useful??? It\u0026rsquo;s not like I\u0026rsquo;m using a CSV file as a database\u0026hellip;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"Nov 27, 2022","externalUrl":null,"permalink":"/gists/new_row_on_github_csv.py/","section":"Gists","summary":"Reading a csv file on GitHub, appending a new row to it, and committing it back again to GitHub.","title":"📋new_row_on_github_csv.py","type":"gists"},{"content":"Lists of Philippine Regions, Provinces, Municipalities/Cities, Barangays from the Department of Interior and Local Government. Accessed on June 30, 2022.\nJSON: All Data, Hierarchical CSV: Regions CSV: Provinces CSV: Municipalities/Cities CSV: Barangays ","date":"Jun 30, 2022","externalUrl":null,"permalink":"/gists/ph-locations/","section":"Gists","summary":"Philippine Regions, Provinces, Municipalities/Cities, Barangays from the DILG.","title":"📋Philippine locations data","type":"gists"},{"content":"","date":"Jun 30, 2022","externalUrl":null,"permalink":"/tags/geography/","section":"Tags","summary":"","title":"geography","type":"tags"},{"content":"","date":"Jun 30, 2022","externalUrl":null,"permalink":"/tags/json/","section":"Tags","summary":"","title":"json","type":"tags"},{"content":" This is a guide to locating the encoded 3200 digits of \\(\\pi\\) in the Library of Babel.\nThe Decoding Key # The decoding key is as follows:\nCharacter Digit a 0 b 1 c 2 d 3 e 4 f 5 g 6 h 7 i 8 j 9 . . The Location # The digits are located at the following links:\nInternet Archive copy Bookmarked/Short version: https://libraryofbabel.info/bookmark.cgi?encoded_digits_of_pi Full/Long Version: https://libraryofbabel.info/book.cgi?0rryncr595ap4ewrd70mmhqmk3yxkwmtdtsx0ufa9j3s7bjyiq8oef6ossnjvss1vb0gzp0qa3ipg3rg1o29c5kolxd7zfvoc5t4lkpppzvtjpaxpn791mjm4sivpo2h4omyncso7tpjklsjavgoblmyfjel6kwk8svk6x0ohbfvzo0iavtfyjv8eet4j5fljmm6lv0vz42wosy61dhnt5sc5dmxd8unw4mx09ffuzd3xqrvgt3frrda14j04s1mwu1hz8g0c9jv0cj60bk27i4nz99y7gjyn9b23u64jbhqhnhy8jsgdvb7i5zgch7g4qsa5wmvfljt2fin2r5apgk7uqe4bwbzpxcr7vhh6pjpgbtarqu2wded3ulhgix8bohnut8t1we9jwzdh0pqiyzoygt72oui3l8n48vhtki5efri8ml6pgqpuvdlwxow5olq70awh057xcgydiqu2inybxfq4o0f6zj6rf0dpy8h66l04e7l5jwzu9p1t6ekqalxaszpmelsf61z9ewulhv2rt4uk6yk979eepe8swkwee7wenvwgaieyeowqozsj5zabocyoseqhp4lasch3vxl1iops3l24wock991zcu3nzr0ag90cn0d2h8oqp4idljggytwy3f0qstbp6ucjzchqz0bfx764vfvblxumvz4xdhmxuflw20fjl4es8v66ye8dosmo58qztupljteh0bmy20a1r2go0u97x13n610v5me25q5fvtmwz7o3so459pi2a2d2nykg6guwqweuxjf6422yrm4kzfznfexoeq353mk3lzc43ifk8j0lai1k8c900scn6nczezc9cttffu8vr2cw4ojgutzqvjlzh7nkrnood5dotv2tix79906wwhp0hua0k44yvm68jig7ofhqpc31j9ykijxogiupbmqqvq03u0cyrhid6twqj10q4a3e2wqpvolpku2outwlbcmb0prckdfy8s1c2f769cl09twik645tycsaopobp4c7yxeo3qbo0jky65xga1l38fs8srcvklytahxo438d8m5x1flg5q1nudiuduw0ib0d4wopu0tdnaluqaciv1y52usbgvdglg7poosezn8gvn3g2x5ds87tliqor35qrk66bpdfdy7y2nwak47gqtkho6juj0yvb81fzcax3bw48syfa848dxxrwj6e4jjf3dpx0lo4yttww25vpakmzbu8c731muui02cy4718v7qyj0a43m0afs4ly2lpaje0t9ofyc7of81b6wuz0qn894t3ynwvqzuv0eyftzmenb1jsr18slihe0558zk9lxvcha8csm2p14b4nzzfohnp5hpw49cxpt4qflyv5055zkx5605qdux374bes0mcjf5rxezxg514vui6c3keniwhodco4yl0zroe82xfct08yad2rst9eq9aencfxljgws8gqzw16pakimtwpxrkhynbrziby6163u4j1dnw1tb6egkdbx2xj7n8wpgkls5gxjqmy4ig45x5csnyixmubsvgzn0349dm3n0zb2ngp8r6wdo1nd3asofda0l4iv5r3qdnoam6l1uzrft6ssshga3opx8c5ex7y5mg7bg535ghtvrlk8qzbune10bkjcehk6whwrv6vtrlexuazdy5mjxpeonumig9xgf45uaoi0igmr1qu5zpgv8avi0k85l44yx7l9obdlrww1nab1yjfm6kpk8ogxg1w16yndaea896lxktom990ktbqc8z49ncc8757guzasb9fap42ab9kb1nllzod99eu6sumomlmmxxpl21evqwlessep8paz6jh8y4r3kcrso1wadi1bilz74m2bigp6thgx47j93e37cdz212omdjsv5aswfk7hvxqu8yehgb69rxvippe8fy7pnm1lvjh3ovzkkjinbio5a2m0lxb42knclph9ul4ptp4isnc131m4wv4u2m87ktp8ql521x406o56u0c0wdle8wfpv7bqmmxl1ghc33cg1pcdeiyeyvyh0omzg01a0sfd46na7p3296gvim9y3638xmfbko07ww6vzioife89n00pyptnjhu8i7wb30t9v7cvr7cbe84jzdrnmhka0du45bitx8saqe37l5z7g14xlkpgqbhvh1o3t9ackt6dhjmbcj1d2e77vy5rtq5hidcoomba9vqv0zrm1dk8pxbnq2c64fzf8urtw8wcv480ryild4fogmakvizcsa4dvwse165a0ylo84h28cf6fvzi3njuaza8nxmf540i635f52l8jd7eu4qkop3tqk16rpsay8eeh3jid5j9sugcembi2ysm7yj44sr03yve7wog85famycizepww1874nc5vka3xgf0e5dxu64bhe5dum4mapu7khvqwl1ha3h7wbyf0cxlxmd5e0iluml5ohufwnqu9xqwto1oez83l29ygyn1esyf0gil1mq0zjozbwda88z8xszybgvslv8669qyebokafises9awzjpbru44t6uzk7pw0pq687do7s9s2dcmkpoah6c9wihl30vz5n0i62hlt51cp61m3d6qfrc86etlja1s212l9d4mmm8y5ti8odorf6izw7n7eqtbbbgn47sxsxfykvp5r5fvvjh3eqcgoe6846la3j5pav0m1thmjhz2wr5yrm4vs91kwlhbe3ktwuc8yfs1cic2k6yvjx6zoemp4elcgi812skjtqp7f2ryg6c1rpj4zjr9un4e30y2skjoufhkkxsoursxik9sciyqtgrr3lv9xt1wd0r59p6dg4t3blrlx2rkhgsji0tdv05lib2emzm1wjlb3dyb2k0uqvj0oz01p2j89n9k5a832l6qouvkcx0vdr5x7aa0wlw3c44fgtxzz643ot0ravgom6fukeazas4si5i2oah8i3acasoj05qmscc2sblm6htu2kw0ng3fcpe96mlprfjg2j3nuifl5pzu4b48fw24k5plvxymqkltnmynh59ehakeui1arvgixkfscm9xgevotdh9bid7xhjbd4xiqtosselujqe1ff6vses0by05pmufc58kgw5l90o89cizmqm67p079qm5y6yrsiwjl74yklgo7xgidisvn62jov98hkiu7fsvwl231xfphcyobj7vr3vy6d6ffn5tyegatk0hoyg99e8avazte9ktzj1b8ll4uv8hydi6156xf-w2-s2-v20:190 ","date":"Feb 01, 2022","externalUrl":null,"permalink":"/gists/pi-in-the-library-of-babel/","section":"Gists","summary":"A guide to locating the 3200 digits of π in the Library of Babel.","title":"📋π to 3200 digits in the Library of Babel","type":"gists"},{"content":"","date":"Feb 01, 2022","externalUrl":null,"permalink":"/tags/ciphers/","section":"Tags","summary":"","title":"ciphers","type":"tags"},{"content":"","date":"Feb 01, 2022","externalUrl":null,"permalink":"/tags/math/","section":"Tags","summary":"","title":"math","type":"tags"},{"content":"In this post, we\u0026rsquo;ll explore how to scrape and prepare PCSO lottery data in Excel, in order to better inform your next plays to take it to the next level.1\nScraping the Data # Let us first install the modules we\u0026rsquo;re going to need for scraping the website (skip this if you already have them).\npython -m pip install selenium beautifulsoup4 pandas And then import them for our project:\n# Selenium for simulating clicks in a browser from selenium import webdriver from selenium.webdriver.support.ui import Select from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as ec # BeautifulSoup for scraping from bs4 import BeautifulSoup # pandas for processing the data import pandas as pd # datetime for getting today\u0026#39;s date and formatting from datetime import datetime from datetime import date from datetime import datetime, timedelta Simulate clicks in the Browser with Selenium # For simulating clicks in a web browser, we are going to use selenium. We\u0026rsquo;re also going to need to download a web driver such as Microsoft Edge\u0026rsquo;s webdriver (of course, you need the corresponding browser Microsoft Edge to be installed). Since I\u0026rsquo;m on Windows, MS Edge comes right out of the box so I will use it, but feel free to use your own preferred browser that has a driver such as Chrome or Firefox.\nNow set the path to where the webdriver executable is as well as the URL to the lottery data is located.\n# Set the path to where the webdriver executuble is # For me it\u0026#39;s the following: path = (\u0026#34;path/to/your/\u0026lt;browser\u0026gt;driver.exe\u0026#34;) # Designate the url to be scraped to a variable url = \u0026#34;https://www.pcso.gov.ph/SearchLottoResult.aspx\u0026#34; Now initialize a Selenium session by directing it to the webdriver executable.\n# Initialize the webdriver, here we use Edge driver = webdriver.Edge(executable_path=path) # Grab the web page driver.get(url) One problem with the page though is that if you inspect the page\u0026rsquo;s source there is class called pre-con in a div. If you would try to just have the driver proceed without waiting for a few seconds some of the buttons are unclickable and blocked by this div container, so we have to tell the WebDriver to wait for a set amount of time. I discovered this after a while of troubleshooting why the selenium cannot give any input to the web form.\n# Designate a variable for waiting for the page to load wait = WebDriverWait(driver, 5) # wait for the div class \u0026#34;pre-con\u0026#34; to be invisible to ensure clicks work wait.until(ec.invisibility_of_element_located((By.CLASS_NAME, \u0026#34;pre-con\u0026#34;))) Now that the wait is over let us now proceed to entering our parameters in the ASP.NET web form (the form with filters we see if we visit the web page) for the data we need. We are going to do that by using the .find_element_by_id method here for the options because we know their ids by inspecting the page\u0026rsquo;s source at the place where the dropdown menus are.\nThe PCSO search Lotto form To inspect the dropdown menu, right-click on it and navigate to \u0026ldquo;Developer Tools\u0026rdquo; and select \u0026ldquo;Inspect\u0026rdquo; (or press F12). We then get the value inside of the id parameter. We want the end date to be today to get the latest data from all the games and the start date to be the earliest possible option which is January 1, 2012 in the dropdown menu. As for the lotto game we want all games. We will just split the data up later into smaller dataframes using pandas for each lotto game, so that we only need to scrape the website every time we want to update our data. But first get today\u0026rsquo;s date which will be used later.\n# Get today\u0026#39;s date with the datetime import today = date.today() # Store the current year, month, and day to variables td_year = today.strftime(\u0026#34;%Y\u0026#34;) td_month = today.strftime(\u0026#34;%B\u0026#34;) td_day = today.strftime(\u0026#34;%d\u0026#34;).lstrip(\u0026#34;0\u0026#34;).replace(\u0026#34; 0\u0026#34;, \u0026#34; \u0026#34;) startyr = int(td_year) - 10 startyr = str(startyr) print(\u0026#34;Today is \u0026#34; + td_month + \u0026#34; \u0026#34; + td_day + \u0026#34;, \u0026#34; + td_year + \u0026#34;.\\n\u0026#34;) [OUT]: Today is May 9, 2022. Now let\u0026rsquo;s have Selenium and the webdriver find the elements of the form and select the parameters we want in the form options.\n# Select Start Date as January 1, 2012 start_month = Select(driver.find_element_by_id( \u0026#34;cphContainer_cpContent_ddlStartMonth\u0026#34;)) start_month.select_by_value(\u0026#34;January\u0026#34;) start_day = Select(driver.find_element_by_id( \u0026#34;cphContainer_cpContent_ddlStartDate\u0026#34;)) start_day.select_by_value(\u0026#34;1\u0026#34;) start_year = Select(driver.find_element_by_id( \u0026#34;cphContainer_cpContent_ddlStartYear\u0026#34;)) start_year.select_by_value(startyr) # Select End Date as Today end_month = Select(driver.find_element_by_id( \u0026#34;cphContainer_cpContent_ddlEndMonth\u0026#34;)) end_month.select_by_value(td_month) end_day = Select(driver.find_element_by_id(\u0026#34;cphContainer_cpContent_ddlEndDay\u0026#34;)) end_day.select_by_value(td_day) end_year = Select(driver.find_element_by_id( \u0026#34;cphContainer_cpContent_ddlEndYear\u0026#34;)) end_year.select_by_value(td_year) # Lotto Game game = Select(driver.find_element_by_id( \u0026#34;cphContainer_cpContent_ddlSelectGame\u0026#34;)) # If you inspect the page, the value of the option for \u0026#34;All Games\u0026#34; is 0 game.select_by_value(\u0026#39;0\u0026#39;) # Submit the parameters by clicking the search button search_button = driver.find_element_by_id(\u0026#34;cphContainer_cpContent_btnSearch\u0026#34;) search_button.click() Scraping the data using BeautifulSoup # Now it\u0026rsquo;s time to scrape the data from the current page\u0026rsquo;s session with BeautifulSoup to get the data we need.\nFirstly, feed the page\u0026rsquo;s source code into Beautiful Soup and then have it find our results by id. Inspect the source again and get all the table\u0026rsquo;s rows by their attributes such as class.\n# Feed the page\u0026#39;s source code into Beautiful Soup doc = BeautifulSoup(driver.page_source, \u0026#34;html.parser\u0026#34;) # Find the table of the results by id ( rows = doc.find(\u0026#39;table\u0026#39;, id=\u0026#39;cphContainer_cpContent_GridView1\u0026#39;).find_all( \u0026#39;tr\u0026#39;, attrs={\u0026#39;class\u0026#39;: \u0026#34;alt\u0026#34;}) Now time to put the data in a python list/dictionary.\n# Initialize a list to hold our data entries = [] # Now loop through the rows and put the data into the list to make a table for row in rows: cells = row.select(\u0026#34;td\u0026#34;) entry = { \u0026#34;Game\u0026#34;: cells[0].text, \u0026#34;Combination\u0026#34;: cells[1].text, \u0026#34;Date\u0026#34;: cells[2].text, \u0026#34;Prize\u0026#34;: cells[3].text, \u0026#34;Winners\u0026#34;: cells[4].text, } entries.append(entry) Processing the Data # Cleaning Up the Data with Pandas # Now that we have the data in a list, it is now time to put it in a pandas dataframe and clean it up. There are duplicates in the data if you examine it closely so we have to remove those. We also need to get the data into the proper data types to make it easier for us to process down the line (i.e. sanitization).\n# Turn the list into a DataFrame df = pd.DataFrame(entries) # Remove duplicate rows df.drop_duplicates(inplace=True, keep=False) # Remove rows that have no combination associated df = df[df[\u0026#34;Combination\u0026#34;] != \u0026#34;- \u0026#34;] The part df = df[df[\u0026quot;Combination\u0026quot;] != \u0026quot;- \u0026quot;] above is to look for and remove entries that do not have a combination. I also found this after hours of figuring out why I cannot do certain operations on the data like converting them into the proper data types. Speaking of data types, let\u0026rsquo;s go convert the data now.\n# Convert the dates to datetime type df[\u0026#34;Date\u0026#34;] = df[\u0026#34;Date\u0026#34;].astype(\u0026#39;datetime64[ns]\u0026#39;) # Remove the commas in the prize amounts df[\u0026#34;Prize\u0026#34;] = df[\u0026#34;Prize\u0026#34;].replace(\u0026#39;,\u0026#39;,\u0026#39;\u0026#39;, regex=True) # Convert data types of Prize and Winners to float and integers df[\u0026#34;Prize\u0026#34;] = df[\u0026#34;Prize\u0026#34;].astype(float) # float because there are still centavos df[\u0026#34;Winners\u0026#34;] = df[\u0026#34;Winners\u0026#34;].astype(int) Now let\u0026rsquo;s look at our data so far:\ndf Here\u0026rsquo;s a preview of the cleaned-up data:\nID Game Combination Date Prize Winners 0 Superlotto 6/49 18-24-04-26-47-36 2022-05-08 67522822.8 0 1 Suertres Lotto 4PM 1-3-9 2022-05-08 4500.0 279 2 EZ2 Lotto 11AM 08-04 2022-05-08 4000.0 251 3 EZ2 Lotto 9PM 07-04 2022-05-08 4000.0 784 4 Lotto 6/42 14-24-08-16-22-37 2022-05-07 6051682.0 0 \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; 15880 4D Vismin 0-6-3-3 2012-01-02 40672.0 7 15881 Suertres Lotto 4PM 3-9-3 2012-01-02 4500.0 256 15882 EZ2 Lotto 9PM 03-13 2012-01-02 4000.0 540 15883 EZ2 Lotto 11AM 31-24 2012-01-02 4000.0 68 15884 Grand Lotto 6/55 44-14-51-52-39-08 2012-01-02 71768080.8 0 15878 rows × 5 columns\nSaving the data to an MS Excel workbook # So far it\u0026rsquo;s looking good. Since we\u0026rsquo;re now here it\u0026rsquo;s time for us to split this huge dataframe of ours into smaller dataframes by the type of lotto game. While we\u0026rsquo;re at it let\u0026rsquo;s also fix the time for the Suertres Lotto and EZ2 Lotto games so that they are included in the data and not as a separate category.\nAfter doing that, let\u0026rsquo;s save that into an Excel workbook so that we do not have to scrape every time we want to analyze the data.\n# Sort the DataFrame by game df.sort_values(by=[\u0026#34;Game\u0026#34;], inplace=True) # Get a list of the games games = df[\u0026#34;Game\u0026#34;].unique().tolist() # Now we can create DataFrames for each game lotto_658 = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;Ultra Lotto 6/58\u0026#34;].copy() lotto_655 = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;Grand Lotto 6/55\u0026#34;].copy() # Sidenote: Super Lotto 6/49 and Mega Lotto 6/45 have different values from # what was on the dropdown menu lotto_649 = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;Superlotto 6/49\u0026#34;].copy() lotto_645 = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;Megalotto 6/45\u0026#34;].copy() # Anyways, continuing... lotto_642 = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;Lotto 6/42\u0026#34;].copy() lotto_6d = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;6Digit\u0026#34;].copy() lotto_4d = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;4Digit\u0026#34;].copy() lotto_3da = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;Suertres Lotto 11AM\u0026#34;].copy() lotto_3db = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;Suertres Lotto 4PM\u0026#34;].copy() lotto_3dc = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;Suertres Lotto 9PM\u0026#34;].copy() lotto_2da = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;EZ2 Lotto 11AM\u0026#34;].copy() lotto_2db = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;EZ2 Lotto 4PM\u0026#34;].copy() lotto_2dc = df.loc[df[\u0026#34;Game\u0026#34;]==\u0026#34;EZ2 Lotto 9PM\u0026#34;].copy() Let\u0026rsquo;s look at one of the dataframes:\nlotto_2da.tail() ID Game Combination Date Prize Winners 11555 EZ2 Lotto 11AM 29-28 2014-09-24 4000.0 54 15533 EZ2 Lotto 11AM 27-16 2012-03-12 4000.0 55 15563 EZ2 Lotto 11AM 03-08 2012-03-06 4000.0 206 9533 EZ2 Lotto 11AM 24-30 2016-01-06 4000.0 121 2394 EZ2 Lotto 11AM 27-08 2020-11-07 4000.0 127 For the Suertres Lotto and EZ2 Lotto games the games are split into 11:00 AM, 4:00 PM, and 9:00 PM games. Let\u0026rsquo;s fix that by assigning them the proper datetime values in the Date column and combining them into bigger dataframes\n# Add 11 hours to the datetime for Suertres Lotto 11AM game to match because of time zones lotto_3da[\u0026#34;Date\u0026#34;] = lotto_3da[\u0026#34;Date\u0026#34;] + timedelta(hours=11) # Add 16 hours to the datetime for Suertres Lotto 4PM game to match lotto_3db[\u0026#34;Date\u0026#34;] = lotto_3db[\u0026#34;Date\u0026#34;] + timedelta(hours=16) # Add 21 hours to the datetime for Suertres Lotto 9PM game to match lotto_3dc[\u0026#34;Date\u0026#34;] = lotto_3dc[\u0026#34;Date\u0026#34;] + timedelta(hours=21) # Rename all the game entries as just Suertres Lotto lotto_3da[\u0026#34;Game\u0026#34;] = \u0026#34;Suertres Lotto\u0026#34; lotto_3db[\u0026#34;Game\u0026#34;] = \u0026#34;Suertres Lotto\u0026#34; lotto_3dc[\u0026#34;Game\u0026#34;] = \u0026#34;Suertres Lotto\u0026#34; # Combine the three Suertres Lotto DataFrames into one lotto_3d = lotto_3da lotto_3d = lotto_3d.append(lotto_3db) lotto_3d = lotto_3d.append(lotto_3dc) # Do the same for EZ2 Lotto lotto_2da[\u0026#34;Date\u0026#34;] = lotto_2da[\u0026#34;Date\u0026#34;] + timedelta(hours=11) lotto_2db[\u0026#34;Date\u0026#34;] = lotto_2db[\u0026#34;Date\u0026#34;] + timedelta(hours=16) lotto_2dc[\u0026#34;Date\u0026#34;] = lotto_2dc[\u0026#34;Date\u0026#34;] + timedelta(hours=21) # Rename all the game entries as just EZ2 Lotto lotto_2da[\u0026#34;Game\u0026#34;] = \u0026#34;EZ2 Lotto\u0026#34; lotto_2db[\u0026#34;Game\u0026#34;] = \u0026#34;EZ2 Lotto\u0026#34; lotto_2dc[\u0026#34;Game\u0026#34;] = \u0026#34;EZ2 Lotto\u0026#34; # Combine the three EZ2 Lotto DataFrames into one lotto_2d = lotto_2da lotto_2d = lotto_2d.append(lotto_2db) lotto_2d = lotto_2d.append(lotto_2dc) Now let\u0026rsquo;s look at one of them again to see if we were successful:\nlotto_2da ID Game Combination Date Prize Winners 2877 EZ2 Lotto 25-06 2020-02-21 11:00:00 4000.0 54 3858 EZ2 Lotto 05-06 2019-07-18 11:00:00 4000.0 164 7987 EZ2 Lotto 30-25 2016-12-24 11:00:00 4000.0 223 3876 EZ2 Lotto 29-29 2019-07-14 11:00:00 4000.0 231 14423 EZ2 Lotto 19-25 2012-11-12 11:00:00 4000.0 135 \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; 11555 EZ2 Lotto 29-28 2014-09-24 11:00:00 4000.0 54 15533 EZ2 Lotto 27-16 2012-03-12 11:00:00 4000.0 55 15563 EZ2 Lotto 03-08 2012-03-06 11:00:00 4000.0 206 9533 EZ2 Lotto 24-30 2016-01-06 11:00:00 4000.0 121 2394 EZ2 Lotto 27-08 2020-11-07 11:00:00 4000.0 127 1815 rows × 5 columns\nLet\u0026rsquo;s see also one of the combined dataframes:\nlotto_2d ID Game Combination Date Prize Winners 2877 EZ2 Lotto 25-06 2020-02-21 11:00:00 4000.0 54 3858 EZ2 Lotto 05-06 2019-07-18 11:00:00 4000.0 164 7987 EZ2 Lotto 30-25 2016-12-24 11:00:00 4000.0 223 3876 EZ2 Lotto 29-29 2019-07-14 11:00:00 4000.0 231 14423 EZ2 Lotto 19-25 2012-11-12 11:00:00 4000.0 135 \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; \u0026hellip; 4686 EZ2 Lotto 16-27 2019-01-11 21:00:00 4000.0 402 4681 EZ2 Lotto 15-08 2019-01-12 21:00:00 4000.0 249 12361 EZ2 Lotto 16-29 2014-03-16 21:00:00 4000.0 530 4695 EZ2 Lotto 06-20 2019-01-09 21:00:00 4000.0 335 4290 EZ2 Lotto 01-20 2019-04-09 21:00:00 4000.0 339 5334 rows × 5 columns\nFinally, let\u0026rsquo;s save our data to an Excel file using pandas:\n# Create Excel writer object writer = pd.ExcelWriter(\u0026#34;lotto.xlsx\u0026#34;) # Write DataFrames to excel worksheets df.to_excel(writer, \u0026#34;All Data\u0026#34;) lotto_658.to_excel(writer, \u0026#34;Ultra Lotto 6-58\u0026#34;) lotto_655.to_excel(writer, \u0026#34;Grand Lotto 6-55\u0026#34;) lotto_649.to_excel(writer, \u0026#34;Super Lotto 6-49\u0026#34;) lotto_645.to_excel(writer, \u0026#34;Mega Lotto 6-45\u0026#34;) lotto_642.to_excel(writer, \u0026#34;Lotto 6-42\u0026#34;) lotto_6d.to_excel(writer, \u0026#34;6 Digit\u0026#34;) lotto_4d.to_excel(writer, \u0026#34;4 Digit\u0026#34;) lotto_3d.to_excel(writer, \u0026#34;Suertres Lotto\u0026#34;) lotto_2d.to_excel(writer, \u0026#34;EZ2 Lotto\u0026#34;) # Save the Excel workbook writer.save() In a Nutshell # In this post, we have successfully extracted PCSO lottery winning combinations using Python and Selenium, and saved the data to an Excel workbook. Here\u0026rsquo;s a summary of what we\u0026rsquo;ve achieved:\nInstalled necessary modules: We installed Selenium, BeautifulSoup, and pandas using pip. Simulated clicks in the browser: We used Selenium to simulate clicks in a web browser and navigate to the PCSO lottery website. Selected parameters: We selected the start and end dates, and the lotto game using Selenium. Scraped the data: We used BeautifulSoup to scrape the data from the current page\u0026rsquo;s session. Processed the data: We cleaned up the data using pandas, removed duplicates, and converted data types. Split the data into smaller DataFrames: We split the data into smaller DataFrames by lotto game. Fixed time for Suertres Lotto and EZ2 Lotto games: We adjusted the time for Suertres Lotto and EZ2 Lotto games to match the correct datetime values. Saved the data to an Excel workbook: We saved the data to an Excel workbook using pandas. We now have a clean and organized dataset of PCSO lottery winning combinations, ready for further analysis and visualization.\nWhich is me, basically, saying to you to not play because this lottery is totally random based on the data extracted using this post.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"Aug 05, 2021","externalUrl":null,"permalink":"/blog/lotto-data-scraper/","section":"Blogs","summary":"\u003cp\u003eIn this post, we\u0026rsquo;ll explore how to scrape and prepare PCSO lottery data in Excel, in order to better inform your next plays to take it to the next level.\u003csup id=\"fnref:1\"\u003e\u003ca href=\"#fn:1\" class=\"footnote-ref\" role=\"doc-noteref\"\u003e1\u003c/a\u003e\u003c/sup\u003e\u003c/p\u003e","title":"How to extract PCSO lottery winning combinations using Python","type":"blog"},{"content":"This is a \u0026ldquo;now page\u0026rdquo;. If you have a site, you can make one too.1\nI\u0026rsquo;m Working On # 2024-08-16: Just got hired! This is my first day at Petron Corporation as a Process Engineer at the Petron Bataan Refinery—the one and only refinery in the Philippines. Yay! I finally got to update my LinkedIn after it has been bugging me all this time to add Work Experience to get the \u0026ldquo;All-Star\u0026rdquo; rating during account creation. Reading Now # None at the moment.\nFor past updates see this page.\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"Jun 21, 2021","externalUrl":null,"permalink":"/now/","section":"jskherman","summary":"What\u0026rsquo;s the current status of jskherman.","title":"Now","type":"page"},{"content":" The Year 2024 # June # 2024-06-05: I\u0026rsquo;m trying to learn how to use NiceGUI to help me rebuild my Quantified Self dashboard with more proper controls for operations that were limited by Streamlit\u0026rsquo;s design (e.g explicit cron jobs for refreshing data). May # 2024-05-29: I recently passed the May 2024 Chemical Engineering Licensure Exam held by the Professional Regulation Commission of the Philippines with a rating of 83.70% (so close with just one point more to place in the Top 10\u0026hellip;). I\u0026rsquo;m currently working on getting my license and looking for a job.1 I\u0026rsquo;m also planning to update my résumé to reflect my new status as a licensed Chemical Engineer. Having the title of \u0026ldquo;Engr.\u0026rdquo; has not sunk in yet for me. The Year 2023 # December # 2023-12-04 I have switched over my quantified self site to use Holoviz\u0026rsquo;s Panel library to create the dashboards. I\u0026rsquo;m planning to add more data sources and pages to it when I have free time. I currently have on there a page tracking how much time I have spent studying for the Chemical Engineering board exam.\n2023-12-05 I\u0026rsquo;m currently working on adjusting my body clock to wake up earlier for the review. I found that it was easier to do in a new environment than when I was at home, despite both places of sleep having no access to sunlight.\n2023-12-15 Working on some small tweaks/fixes on the side for my résumé/CV template made with \\(\\ \\cancel{\\LaTeX} \\ \\) Typst. I sometimes find myself checking the repository to see if it has reached 100 stars yet. It\u0026rsquo;s currently at 97 stars on 2023-12-27. Imaginary internet points, lol. It got featured on Twitter by the Typst team, which made me a bit enthused.\nJuly # 2023-07-01 Just graduated as Cum Laude with a degree of Bachelor of Science in Chemical Engineering. Right now, I am preparing for the board exams for Chemical Engineering. I still have a lot of things to do and this was not the rest I was hoping after graduating. Life happens I guess. While procrastinating I created a quick prototype of a problem set generator to help me study for the board exams. I\u0026rsquo;m still thinking of how to improve it and make it more useful for other people. I may or may not be thinking of how to monetize it. Most of the technical work is done, it\u0026rsquo;s just gathering the questions and answers that is the hard part. I also created a curriculum vitae/résumé template in \\(\\ \\cancel{\\LaTeX}\\ \\) Typst so that I can easily update my CV in the future. I made it so that I can easily put my resume under version control with Git. I also made it available for others to use and you can download the template on GitHub here. On the background, I was trying a lot of self-hosted solutions on the Fly.io platform. I currently have the following deployed: A web app for Plain Text (Double-Entry) Accounting with beancount and fava (for the data visualization) to make my personal finances more transparent and convenient to me; An uptime monitor using Uptime Kuma; An instance of n8n.io, which is a Zapier/IFTTT alternative, as my automation platform for various things (like gathering data periodically); A PostgreSQL database for my Quantified Self data; and howis.jskherman.com for my personal statistics dashboard because I was inspired by howisFelix.today? and this Reddit post. 2023-07-26 During June 2023, while I did have some time to rest for a month, I did some progress towards my pending side projects that I wanted to do after school. I shipped a bare-bones reimplentation of my dashboard site for my Quantified Self statistics at howis.jskherman.com. It currently only has my Time Tracking data and temperature data that I have been collecting since February 2023 in my PostgreSQL database. I am still working on implementing the data visualizations of the other data I have collected. This is a work in progress and postponed for now because of my board exam preparations. January # 2023-01-16 Classes are starting again and this time this is the 2nd semester of my fourth year in college for my Bachelor\u0026rsquo;s degree in Chemical Engineering. Judging from the 2018-2019 curriculum, this semester is going to be hectic with classes need to be virtually done by the end of April 2023. The Year 2022 # December # 2022-12-28 Doing some improvements to my streamlit app for personal statistics/dashboard (something like the idea behind Exist.io but I get to build it myself from scratch). Have made progress creating a page of buttons and small forms to quickly log data for various things I am tracking. Have also made progress in learning how to use DigitalOcean\u0026rsquo;s serverless functions to scrape and save data from the web to a MongoDB database on a regular schedule. November # 2022-11-28 Doing some improvements to my streamlit app for personal statistics/dashboard (something like the idea behind Exist.io but I get to build it myself from scratch). Have made progress integrating my Todoist tasks, Telegram, and Notion.\n2022-11-15 I am reading Ryan Holiday\u0026rsquo;s book \u0026ldquo;Ego is the Enemy\u0026rdquo;. So far after glancing at the table of contents and the first few pages, it seems it would be an interesting read.\n2022-11-05 I am reading through the Mushoku Tensei Light Novel Series. I am now at volume 19 of the official English-translated light novels.\nAugust # 2022-08-15 The start of the 1st semester of academic year 2022–2023, and the 1st semester of my 4th year studying Chemical Engineering. It seems like things are ramping up and everyone seems eager for the face-to-face hybrid classes. July # 2022-07-08 I participated as a semi-finalist in the SCG Bangkok Business Challenge @ Sasin 2022 at Bangkok, Thailand. I also presented at the 60-second pitch round (01:35:35) for the Opening Reception.\n2022-07-29 I joined the National Nutrional Council\u0026rsquo;s first ever Nutri-Hackathon in a team of four and won first place with them.\nThe Year 2021 # June # 2021-06-21: Learned the ins and outs of being a Vice President for Internal Affairs in my university\u0026rsquo;s student organization for the Chemical Engineering department: Philippine Institute of Chemical Engineering - Junior Chapter V (PIChE-JCV).\n2021-06-26: Developed this website and the another site for my notes. I have learned a lot about the Zettelkasten method and the world of static site generators on the side, little by little, throughout last year.\nHere\u0026rsquo;s a link to my CV and LinkedIn if you want to hire me. :\u0026gt;\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"Jun 21, 2021","externalUrl":null,"permalink":"/past/","section":"jskherman","summary":"Archived entries from the Now page","title":"The Past","type":"page"},{"content":"A Python script to get the Hacker News favorites for a user.\nSee original source and license.\n#!/usr/bin/env python3 # Author: gabrielsroka # Source: https://github.com/gabrielsroka/gabrielsroka.github.io/blob/master/getHNFavorites.py # License: MIT License import requests import time from bs4 import BeautifulSoup # pip install beautifulsoup4 username = input(\u0026#39;username: \u0026#39;) # session uses connection pooling, often resulting in faster execution. session = requests.Session() base = \u0026#39;https://news.ycombinator.com/\u0026#39; path = f\u0026#39;favorites?id={username}\u0026#39; while path: r = session.get(base + path) s = BeautifulSoup(r.text, \u0026#39;html.parser\u0026#39;) for a in s.select(\u0026#39;span.titleline a\u0026#39;): print(a.text, a[\u0026#39;href\u0026#39;]) more = s.select_one(\u0026#39;a.morelink\u0026#39;) path = more[\u0026#39;href\u0026#39;] if more else None if path: time.sleep(0.850) ","date":"Jul 15, 2020","externalUrl":null,"permalink":"/gists/get_hn_favorites.py/","section":"Gists","summary":"A Python script to get HN favorites for a user.","title":"📋get_HN_favorites.py","type":"gists"},{"content":" I\u0026rsquo;m Je Sian Keith Herman, an #INTJ Chemical Engineer that likes 🍨 ice cream, ☕ iced coffee, and 🍹 iced tea in no particular order. This is the site where I share my thoughts, tools, interests, projects, showcases, experiences, etc. Feel free to reach out and give feedback through email or any of my contact details.\nWhat\u0026rsquo;s On Here? # I am still building up the contents here on this website from time to time. For now, here\u0026rsquo;s some links to things on this site that might be of interest that I have already built up:\nMy CV: if you\u0026rsquo;re looking for it. The Blog section: self-explanatory section that lists the blog posts on this site. The Gists section: code snippets and random pieces of text. The Projects section: a list of projects that I have worked on. The Now page: if you\u0026rsquo;re looking up what I am up to these days.1 The Web Archive: if you\u0026rsquo;re looking for a collection of links that I found interesting and have saved. Impact # A running list of things that have had a big impact on me:\nBooks \u0026ldquo;A Mind For Numbers\u0026rdquo; by Barbara Oakley: A book on learning how to learn. The World God Only Knows by Wakaki Tamiki: reading this for the plot and the humor. Games and Visual Novels Rewrite by Key: A visual novel with supernatural elements that has a special place in my heart. Katawa Shoujo by Four Leaf Studios: A visual novel about coming to terms with one\u0026rsquo;s disabilities and understanding others. Persona 3 (FES/Portable/Reload): A JRPG exploring the concept of \u0026ldquo;Memento Mori\u0026rdquo;, shadows (Jungian psychology), and the self. From the Internet 3Blue1brown: Math and more explained visually. Check out a commencement speech of his. WaitButWhy: A blog that explains things in a simple and fun way with stick figures! Frequently Asked Questions # How do you pronounce your first name? My first name \u0026ldquo;Je Sian Keith\u0026rdquo; is pronounced as /ʤi ʃan kiːθ/ (ji shan kith) in the International Phonetic Alphabet (IPA). What is your favorite font? A monospace font with ligatures called: Cascadia Code. What is your horoscope? It is Gemini. Do you have some ASCII Art? ┌─────────────────────────────────────────────────────────────┐ │ ┌-────────────────────────────────────────────────────────┐ │ │ │ │ │ │ │ .-=- │ │ │ │ :+- =@###### │ │ │ │ :##% x#+. ###+ │ │ │ │ ##@. *#x ##= │ │ │ │ ###. .##: +# │ │ │ │ .+###* -## .%@. │ │ │ │ .+#@###- =## .+#x │ │ │ │ -x#%- ###- *##:%#x: │ │ │ │ .-=*x@#@+: ##. *###x: │ │ │ │ +##@x*: =#x -x### │ │ │ │ +#x+@@+### .=**#-: │ │ │ │ .###*: +#x -%#%####@: │ │ │ │ :*%###. x#+ .x#*. . │ │ │ │ .-x@#%#### x## *#%. │ │ │ │ -+%#@+= ###= x###%#= │ │ │ │ #+@@@x=. ### ####% │ │ │ │ #####: ##- .#### │ │ │ │ ###### *#x ##### │ │ │ │ =##### -%#x #### │ │ │ │ ####@%+*x%##%- .### │ │ │ │ .:*++*=: .@#: │ │ │ │ │ │ │ └─────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ Fancy some background music while you\u0026#39;re here? What is it that keeps you going? Comic by Poorly Drawn Lines. Don\u0026rsquo;t know what a Now page is? Learn about the idea and make your own too!\u0026#160;\u0026#x21a9;\u0026#xfe0e;\n","date":"Feb 02, 2020","externalUrl":null,"permalink":"/about/","section":"jskherman","summary":"About this site.","title":"About","type":"page"},{"content":"","externalUrl":null,"permalink":"/authors/","section":"Authors","summary":"","title":"Authors","type":"authors"},{"content":"","externalUrl":null,"permalink":"/series/","section":"Series","summary":"","title":"Series","type":"series"}]